Metadata-Version: 2.4
Name: skaldr
Version: 0.3.0
Summary: Component framework that renders one YAML content file into a self-contained HTML report page
Keywords: report,html,yaml,components,jinja
Author: Alex Yanchenko
Author-email: Alex Yanchenko <a.inbox.2026@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Dist: jinja2>=3.1
Requires-Dist: markupsafe>=2.1
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pyyaml>=6.0
Requires-Python: >=3.11
Project-URL: Repository, https://github.com/alex-yanchenko/skaldr
Project-URL: Issues, https://github.com/alex-yanchenko/skaldr/issues
Description-Content-Type: text/markdown

# skaldr

A component framework for **AI-authored report pages**. An AI (or a person) doing real
work — an investigation, a review, a proposal, an ingestion report — writes **one YAML
content file**; skaldr validates it and renders **one polished, self-contained HTML page**.
No time is spent on design, styling, or layout: those decisions are made once, here, by the
component library.

```bash
# from a checkout
uv run skaldr data/example.yaml
open out/example.html

# or installed (from PyPI, once published)
uv tool install skaldr
skaldr data/example.yaml
```

The CLI is deliberately tiny: `skaldr <content.yaml> [-o out.html]`, plus
`skaldr --write-schema schema/page.schema.json`. There are no styling flags — everything is
in the content file.

## The content file

```yaml
version: 1
meta:
  title: "Q3 Data Import — Issues & Proposed Fixes"
  subtitle: ["Reconciled review of the 10,000-record batch."]
  source: "Salesforce export"   # optional; feeds the provenance footer
  date: "Q3 2026"               # optional; never auto-now (builds are reproducible)
  toc: true                     # optional; auto table-of-contents from level-2 headings
  width: default                # optional; page width cap: default (1600) | wide (1920) | full
badges:                         # author-declared vocabulary (see below)
  OUR_SIDE: { label: "Our side", tone: amber, legend: "We can fix it before import." }
  BUG:      { label: "Bug",      tone: blue,  legend: "Defect in our pipeline code." }
blocks:
  - { type: heading, text: "Overview" }
  - { type: text, body: "Prose with **bold**, *italic*, `code`, ~~strike~~ and [links](https://x)." }
  - { type: cards, items: [{ label: "Imported cleanly", value: 8500, of: 10000, tone: success }] }
  # … more blocks
```

Top level is `version` · `meta` · optional `badges` · `blocks` — nothing else. Every block
carries a `type` discriminator; the model is a pydantic discriminated union, so an unknown
type, a field from the wrong block, or an unknown key each fails with a precise
`blocks.3.items.2.value`-style error. See [`data/example.yaml`](data/example.yaml) for a
complete file exercising every block, [`docs/authoring.md`](docs/authoring.md) for the full
authoring guide, and [`schema/page.schema.json`](schema/page.schema.json) for the JSON Schema
(point your editor's YAML language server at it).

### Blocks

`heading` · `text` · `list` · `fact_strip` · `key_value` · `cards` · `badge_row` · `callout` ·
`status_list` · `meter` · `table` · `code` (plain or diff) · `quote` · `image` (data-URI) ·
`timeline` · `section` (collapsible) · `grid` (bounded 6-column side-by-side layout).

`section` and `grid` are the two containers: `section` holds leaf blocks (no nested section);
`grid` places cells side by side over 6 columns and may nest one level (see
[`docs/design/grid-layout.md`](docs/design/grid-layout.md)).

The **`table`** is the workhorse: typed columns (`text`/`number`/`badge`/`rich`), optional
groups with derived subtotals, sub-rows, an optional `totals` footer, and an optional
`reconcile` block — declare a total and skaldr hard-fails the build if the column sum plus an
optional handled bucket doesn't equal it, naming the delta.

### Badges are author-declared

Every tag/status a report uses is declared in the top-level `badges` map
(`key → { label, tone, legend }`) and referenced by key from `table` badge columns and
`badge_row`. A key that isn't declared is a hard error. **skaldr ships no built-in
vocabulary** — the only fixed, referenced-by-name vocabularies are the design-system
primitives: the tone palette used by cards/callouts/meters
(`neutral · info · success · warning · danger · accent`), the badge-chip colours
(`slate · blue · green · amber · red · violet · teal · sky`), and the state glyphs for
`status_list` / `timeline`.

### Rich text

Prose fields accept a small markdown subset — `**bold**`, `*italic*`, `` `code` ``,
`~~strike~~`, `[text](url)` (http/https/mailto only). Everything else is escaped; raw HTML is
never interpreted.

## Guarantees

- **One self-contained file** — inline CSS, system fonts, no external resources; the page
  carries its own `<!doctype>` + `<meta charset>` so it renders correctly from `file://`, any
  static host, or a claude.ai Artifact.
- **Validation is the product** — structural mistakes fail the build with a field path, never
  reach the reader's eyes.
- **Derived, not authored** — number formatting, percentages, subtotals, the legend, the TOC,
  and the provenance footer are all computed, so they can't drift from the data.

## Layout

- [`src/skaldr/models.py`](src/skaldr/models.py) — the content-file contract (pydantic).
- [`src/skaldr/compute.py`](src/skaldr/compute.py) — derived values (legend, TOC, subtotals, footer).
- [`src/skaldr/render.py`](src/skaldr/render.py) + [`components/`](src/skaldr/components) — Jinja rendering.
- [`src/skaldr/styles.css`](src/skaldr/styles.css) — the single tokenised stylesheet.
- [`docs/design/component-spec.md`](docs/design/component-spec.md) — the normative spec;
  [`docs/design/reference.html`](docs/design/reference.html) — the rendered golden.
