reports/<id>.yaml) is a committed YAML file, validated against Report at load time and on every canonic validate run. It names an ordered sequence of sections, each an unmodified SemanticQuery — the same shape a query() call or a -f/--file JSON query file accepts — plus an optional attached narrative. See canonic report for running one.
Top-level fields
ReportSection
A section’s
query.context and query.as_of, when set, always win over the report-level context and the run_report(as_of=...) call argument — the report/call-level values only fill in when the section itself doesn’t specify one.
What a report is not
A report introduces no new execution semantics (run_report is a deterministic loop over core.query) and no new authority: it cannot declare a canonical binding, introduce a metric, or relax/add a guardrail — it can only reference entities that already resolve. It carries no layout, chart type, or rendering; its output is an ordered array of unmodified QueryResult objects, so presentation stays the concern of whichever adapter or downstream consumer renders the response.
There is also no per-report access control today: canonic’s guardrails (min_trust, context) govern whether a query is correct/trusted enough to answer, not whether a given caller may invoke a given report at all.
Validation
canonic validate checks every committed report the same way it already checks semantics//contracts/:
- Each section’s
querymust compile (dry-run, no execution) against the current semantic layer. An unresolved metric or unreachable dimension fails validation with the report id and section index — never a silent skip. - Each
narrative_frommust resolve to an existing knowledge-page id.
semantics//contracts/, renaming a metric a report depends on is caught in the same canonic validate run, the same PR — no separate external CI gate is required for this class of drift.