Skip to main content
A report (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 query must 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_from must resolve to an existing knowledge-page id.
Because reports are committed inside the same project as 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.