Report skill builder
Turns a governed report contract into a skill someone can run by typing the report's name. The
generated skill is self-contained as a skill: it carries a materialized copy of the contract, a
hash manifest, a runner, a renderer and the design system, so it keeps working when copied into
another repo or installed alone. It never depends on this skill being present. (The rendered page
is a different question — its charts come from a pinned CDN.)
The model's job in the generated skill is deliberately small — pick parameters, write two-sentence
summaries, report failures. The numbers come from the contract.
When to use
- The user wants a repeatable, named report ("a skill that generates the monthly P&L").
- A contract exists (or is about to) and needs a consumer on the skill side.
- A generated report reads badly and needs the bundled design system applied.
When NOT to use
- Authoring a general skill →
add-skill in this repo, or skill-creator. This one only emits
report skills bound to a contract.
- Defining or fixing the contract →
governed-report-contract. If the contract does not
validate, stop and go there; do not generate around a broken contract.
- Building the interactive app →
report-to-databricks-app.
- Ad-hoc "just show me the numbers" →
databricks-data-discovery. A skill for a report nobody
will run twice is overhead.
Workflow
Require a valid contract. The generator refuses otherwise, and that refusal is the feature:
python3 "${CLAUDE_SKILL_DIR}/../governed-report-contract/scripts/validate_contract.py" reports/<name>
If the sibling skill is not installed, pass --validator <path> in the next step.
Generate. The output folder name becomes the skill name, so make it kebab-case and specific
(monthly-pnl, not report):
python3 "${CLAUDE_SKILL_DIR}/scripts/new_report_skill.py" \
--contract reports/monthly-pnl \
--out .claude/skills/monthly-pnl
It materializes contract/, writes contract.manifest.json (SHA-256 per file), copies the
runner, renderer and design system, renders SKILL.md, and writes a starter evals/evals.json.
Check what was generated. The description is derived from the report title — read it and make
it sound like something a user would actually type. Confirm it is a single line, 150–400 chars,
and does not collide with a sibling report skill's triggers.
Dry-run the report against a real warehouse before handing it over:
python3 .claude/skills/monthly-pnl/scripts/run_report.py \
--contract .claude/skills/monthly-pnl/contract \
--profile <PROFILE> --warehouse <WAREHOUSE_ID> --out /tmp/pnl.json
python3 .claude/skills/monthly-pnl/scripts/render_report.py \
--envelope /tmp/pnl.json --out /tmp/pnl.html
Open the HTML. Charts need the network (ECharts from a pinned CDN); everything else
renders without it and each chart slot says so when it cannot draw. If it reads like a document
rather than a dashboard, that is a copy problem — see
references/design-system.md, not a CSS problem.
Tune the evals, don't invent them. The generated cases are shaped from the report title;
replace the ones that do not sound like this team's vocabulary. Keep ≥8 should-trigger and ≥8
should-not-trigger, and make sure a sibling report skill is among the negatives.
Report what you built: the skill path, the contract version it pinned, the blocks and their
execution identities, and the one command that regenerates it.
Output spec
<skill>/
├── SKILL.md # single-line description, contract version pinned
├── contract/ # materialized report.yaml + queries/ + metric-views/
├── contract.manifest.json # contract version + SHA-256 per file
├── scripts/run_report.py # contract -> result envelope (binds params, attests principal)
├── scripts/render_report.py # envelope -> an HTML report (shadcn cards + Recharts)
├── assets/report.css # shadcn design tokens
├── assets/report-template.html # page skeleton (pinned CDN tags for the chart runtime)
├── assets/report-charts.js # ECharts options — the same objects AppKit takes
└── evals/evals.json # ≥8 / ≥8 / 3 cases, report-specific
The runner exits non-zero if any block failed; the renderer exits non-zero if the page contains a
failed block. All three scripts take --selftest: the runner replays a fake CLI to check the
endpoint, polling, chunk pagination, parameter rejection and drift; the renderer checks escaping,
numeric fidelity and failure states; the generator checks materialization, hashing and overlap.
The result envelope
The interface between running and rendering. Keeping it explicit is what lets the same numbers be
re-rendered, diffed or shipped elsewhere without re-querying:
{
"contract": {"name": "monthly-pnl", "version": "1.0.0", "title": "Monthly P&L", "owner": "..."},
"generated_at": "2026-08-14T12:00:00Z",
"attested_principal": "someone@example.com",
"params": {"start_date": "2026-01-01"},
"freshness": {"watermark": "2026-08-14T02:15:00Z", "max_lag": "26h"},
"blocks": [{"key": "pnl_summary", "kind": "kpi", "identity": "user", "trust": "certified",
"status": "ok", "columns": [...], "rows": [[...]], "row_count": 1}]
}
status is one of ok, empty, partial, error. A partial block was truncated or exceeded
the contract's row cap — it renders with a flagged line, never silently.
Gotchas
- Never edit the generated
contract/. The runner re-hashes it and refuses to run on a drifted
copy. Change the canonical contract, then re-generate.
- Charts need the network; the numbers do not. The chart runtime is CDN-pinned, so a plot can
fail to draw while every figure, table and state still renders. That is why each chart slot ships
a visible fallback instead of a blank box. If a report must plot offline, inline the runtime.
animation: false is not cosmetic. An animated series is mid-flight when a screenshot,
print or PDF is taken, so the plot comes out blank or half-drawn.
- A generated skill must not import from this one. It gets copied and installed alone; anything
it needs is inside its own folder. That is why the template and scripts are assets, not shared code.
- The generated description is a draft. Derived text routes worse than a sentence written for
how this team talks about the report.
- Two report skills steal each other's triggers. "the report" is not a trigger. Name the report
in the description and add the sibling as a negative case.
- Summaries are the only model-authored text, and the most quotable. They must cite only values
present in the envelope, stay under two sentences, and keep their AI attribution mark.
--skip-validate exists for the case where you already ran the validator. Using it to get past
a failing contract produces a skill that confidently returns wrong numbers.
References
- references/design-system.md — tokens, typography, copy budgets, the
AI-summary rules, chart and state requirements, and how to extend without forking.
scripts/new_report_skill.py — the generator (--selftest proves materialization, hashing and
drift detection still work).
assets/ — everything copied into the generated skill: runner, renderer, CSS, page template,
SKILL.md template.
1---2name: report-skill-builder3description: Generates a self-contained report skill from a validated governed report contract - materializing the trusted queries, a Statement Execution runner, and a shadcn-styled HTML report whose ECharts code ports to AppKit. Use when the user asks for a skill or slash command that produces a specific report. Not for authoring general-purpose skills, defining the contract, or building a Databricks App.4license: MIT5---67# Report skill builder89Turns a governed report contract into a skill someone can run by typing the report's name. The10generated skill is **self-contained as a skill**: it carries a materialized copy of the contract, a11hash manifest, a runner, a renderer and the design system, so it keeps working when copied into12another repo or installed alone. It never depends on this skill being present. (The rendered page13is a different question — its charts come from a pinned CDN.)1415The model's job in the generated skill is deliberately small — pick parameters, write two-sentence16summaries, report failures. The numbers come from the contract.1718## When to use1920- The user wants a repeatable, named report ("a skill that generates the monthly P&L").21- A contract exists (or is about to) and needs a consumer on the skill side.22- A generated report reads badly and needs the bundled design system applied.2324## When NOT to use2526- **Authoring a general skill** → `add-skill` in this repo, or `skill-creator`. This one only emits27 report skills bound to a contract.28- **Defining or fixing the contract** → `governed-report-contract`. If the contract does not29 validate, stop and go there; do not generate around a broken contract.30- **Building the interactive app** → `report-to-databricks-app`.31- **Ad-hoc "just show me the numbers"** → `databricks-data-discovery`. A skill for a report nobody32 will run twice is overhead.3334## Workflow35361. **Require a valid contract.** The generator refuses otherwise, and that refusal is the feature:3738 ```bash39 python3 "${CLAUDE_SKILL_DIR}/../governed-report-contract/scripts/validate_contract.py" reports/<name>40 ```4142 If the sibling skill is not installed, pass `--validator <path>` in the next step.43442. **Generate.** The output folder name becomes the skill name, so make it kebab-case and specific45 (`monthly-pnl`, not `report`):4647 ```bash48 python3 "${CLAUDE_SKILL_DIR}/scripts/new_report_skill.py" \49 --contract reports/monthly-pnl \50 --out .claude/skills/monthly-pnl51 ```5253 It materializes `contract/`, writes `contract.manifest.json` (SHA-256 per file), copies the54 runner, renderer and design system, renders `SKILL.md`, and writes a starter `evals/evals.json`.55563. **Check what was generated.** The description is derived from the report title — read it and make57 it sound like something a user would actually type. Confirm it is a single line, 150–400 chars,58 and does not collide with a sibling report skill's triggers.59604. **Dry-run the report** against a real warehouse before handing it over:6162 ```bash63 python3 .claude/skills/monthly-pnl/scripts/run_report.py \64 --contract .claude/skills/monthly-pnl/contract \65 --profile <PROFILE> --warehouse <WAREHOUSE_ID> --out /tmp/pnl.json66 python3 .claude/skills/monthly-pnl/scripts/render_report.py \67 --envelope /tmp/pnl.json --out /tmp/pnl.html68 ```6970 Open the HTML. Charts need the network (ECharts from a pinned CDN); everything else71 renders without it and each chart slot says so when it cannot draw. If it reads like a document72 rather than a dashboard, that is a copy problem — see73 [references/design-system.md](references/design-system.md), not a CSS problem.74755. **Tune the evals, don't invent them.** The generated cases are shaped from the report title;76 replace the ones that do not sound like this team's vocabulary. Keep ≥8 should-trigger and ≥877 should-not-trigger, and make sure a sibling report skill is among the negatives.78796. **Report what you built:** the skill path, the contract version it pinned, the blocks and their80 execution identities, and the one command that regenerates it.8182## Output spec8384```85<skill>/86├── SKILL.md # single-line description, contract version pinned87├── contract/ # materialized report.yaml + queries/ + metric-views/88├── contract.manifest.json # contract version + SHA-256 per file89├── scripts/run_report.py # contract -> result envelope (binds params, attests principal)90├── scripts/render_report.py # envelope -> an HTML report (shadcn cards + Recharts)91├── assets/report.css # shadcn design tokens92├── assets/report-template.html # page skeleton (pinned CDN tags for the chart runtime)93├── assets/report-charts.js # ECharts options — the same objects AppKit takes94└── evals/evals.json # ≥8 / ≥8 / 3 cases, report-specific95```9697The runner exits non-zero if any block failed; the renderer exits non-zero if the page contains a98failed block. All three scripts take `--selftest`: the runner replays a fake CLI to check the99endpoint, polling, chunk pagination, parameter rejection and drift; the renderer checks escaping,100numeric fidelity and failure states; the generator checks materialization, hashing and overlap.101102## The result envelope103104The interface between running and rendering. Keeping it explicit is what lets the same numbers be105re-rendered, diffed or shipped elsewhere without re-querying:106107```json108{109 "contract": {"name": "monthly-pnl", "version": "1.0.0", "title": "Monthly P&L", "owner": "..."},110 "generated_at": "2026-08-14T12:00:00Z",111 "attested_principal": "someone@example.com",112 "params": {"start_date": "2026-01-01"},113 "freshness": {"watermark": "2026-08-14T02:15:00Z", "max_lag": "26h"},114 "blocks": [{"key": "pnl_summary", "kind": "kpi", "identity": "user", "trust": "certified",115 "status": "ok", "columns": [...], "rows": [[...]], "row_count": 1}]116}117```118119`status` is one of `ok`, `empty`, `partial`, `error`. A `partial` block was truncated or exceeded120the contract's row cap — it renders with a flagged line, never silently.121122## Gotchas123124- **Never edit the generated `contract/`.** The runner re-hashes it and refuses to run on a drifted125 copy. Change the canonical contract, then re-generate.126- **Charts need the network; the numbers do not.** The chart runtime is CDN-pinned, so a plot can127 fail to draw while every figure, table and state still renders. That is why each chart slot ships128 a visible fallback instead of a blank box. If a report must plot offline, inline the runtime.129- **`animation: false` is not cosmetic.** An animated series is mid-flight when a screenshot,130 print or PDF is taken, so the plot comes out blank or half-drawn.131- **A generated skill must not import from this one.** It gets copied and installed alone; anything132 it needs is inside its own folder. That is why the template and scripts are assets, not shared code.133- **The generated description is a draft.** Derived text routes worse than a sentence written for134 how this team talks about the report.135- **Two report skills steal each other's triggers.** "the report" is not a trigger. Name the report136 in the description and add the sibling as a negative case.137- **Summaries are the only model-authored text, and the most quotable.** They must cite only values138 present in the envelope, stay under two sentences, and keep their AI attribution mark.139- **`--skip-validate` exists for the case where you already ran the validator.** Using it to get past140 a failing contract produces a skill that confidently returns wrong numbers.141142## References143144- [references/design-system.md](references/design-system.md) — tokens, typography, copy budgets, the145 AI-summary rules, chart and state requirements, and how to extend without forking.146- `scripts/new_report_skill.py` — the generator (`--selftest` proves materialization, hashing and147 drift detection still work).148- `assets/` — everything copied into the generated skill: runner, renderer, CSS, page template,149 `SKILL.md` template.