Documents & Reports
The last mile of most AI programs is a document: a report, a case summary, a briefing. The tempting shortcut is to ask a model for Markdown and hope it comes back well-formed — then patch it with regexes when it doesn’t. Sema splits the job cleanly: the model fills a typed structure, and rendering that structure to Markdown is a pure, deterministic function. Layout is structural, not a prompt convention.
This guide uses the std.document module for the report IR
and anchors on the finops-ledger
example for grounded, review-gated report generation.
The pieces
Section titled “The pieces”| Concern | Construct | Where |
|---|---|---|
| Typed report IR | Report struct + render |
std.document |
| Model fills a typed value | simulate def … by <model> |
simulate & Models |
| Grounding the output | check semantics(…) |
Contracts |
| Docs from the program | sema doc (docstrings + reflection) |
this guide, §“Docs as artifact” |
A typed report
Section titled “A typed report”std.document defines a Report the model (or your code) fills, and a
deterministic render(report, nl) that emits Markdown. nl is the newline
separator — parametrizable so the same renderer emits a single-line form for
tests or real newlines for output.
struct Report: title: str context: str confidence: f64 # < 0 means "no confidence section" rationale: str takeaways: list[str] section_titles: list[str] sections_text: str conclusion: strRendering is a function, so what you build is what you get — no regex repair of freeform LLM Markdown, and section placement is structural:
from std.document import Report, render
r = Report( title="Renewable Energy Findings", context="Auto-synthesized from 2 sources.", confidence=0.82, rationale="Confidence is a Beta-Bernoulli posterior over iteration evidence.", takeaways=["costs fell", "capacity grew"], section_titles=["Findings"], sections_text="Solar capacity grew [1]. Costs fell [2].", conclusion="Costs continue to decline as capacity scales.",)print(render(r, "\n"))Set confidence below zero to omit the confidence section entirely — the
renderer treats it as a signal, not just a number.
Letting a model fill the report
Section titled “Letting a model fill the report”For a report whose prose comes from a model, don’t ask the model for Markdown —
ask it for the typed fields. A simulate def … by <model> has the model
implement the body, decoded into your struct, with contracts on the result. The
finops-ledger example drafts a compliance case summary this way:
simulate def draft_suspicious_activity( decision: ReconciliationDecision, bank: BankLine, ledger: list[LedgerEntry],) -> SuspiciousActivityDraft by anomaly_writer: sem "Draft a cautious case summary for a compliance analyst" sem "Do not claim criminality; state uncertainty and cite evidence references" budget tokens=768, time="3s" ensure len(result.reasons) >= 1 ensure result.subject_counterparty_id != "" check semantics( "draft is grounded in the reconciliation decision and does not overstate certainty", decision, result, judge=report_grounder, alpha=0.01, )Three things make this an auditable document rather than a hopeful one:
ensurecontracts are hard — a draft with no reasons raisesContractViolation.check semantics(…)is a soft, monitored guard: a judge model verifies the draft is grounded in the decision and doesn’t overstate certainty, at a calibrated significancealpha.budget tokens=…, time=…caps what the draft may cost.
See Contracts for the full contract vocabulary and Schemas & Typed Decode for how the model’s output is decoded into the struct.
Gating a report before it leaves the building
Section titled “Gating a report before it leaves the building”A generated document usually needs a review gate before it becomes an artifact.
In finops-ledger, the export path requires a human approval record, sanitizes
the draft, and wraps the write in a semantic guard so a non-conforming draft is
quarantined instead of shipped:
@RegulatedExportdef export_after_approval(draft: SuspiciousActivityDraft, approval: AnalystApproval) -> None !{fs.write, net.connect, model.invoke}: safe = sanitize_draft(draft) report_path = validate f"out/regulatory/{approval.decision_id}.json": sem "Local regulated-report path derived from analyst approval" ensure path.is_relative_to(value, "out/regulatory") ensure not path.contains_parent_ref(value) expect semantics("regulated draft contains only approved evidence and no raw account number", safe, judge=policy_judge, alpha=0.01): write_report(report_path, safe) log.info("regulated export prepared") submit_report("https://regulator-gateway.internal:443/drafts", safe) except SemanticsViolation as violation: quarantine(safe, evidence=violation)The validate f"…" block turns an interpolated path into a checked value — the
ensure guards keep the write inside out/regulatory and reject ..
traversal — so the document’s destination is as governed as its contents.
Documentation as a reflected artifact
Section titled “Documentation as a reflected artifact”Your program itself is a document too. In Sema, documentation is generated by
reflection over the code, merged with prose you write inline as docstrings — a
triple-quoted string as the first statement of a module, def, struct, or
enum (as in Python). Unlike a comment, a docstring is a real value the runtime
can reflect:
"""Geometry helpers."""
def norm(x: f64, y: f64) -> f64 !{}: """ The Euclidean norm of a 2-D vector: $\|v\|_2 = \sqrt{x^2 + y^2}$.
> [!NOTE] > The result is always non-negative.
```sema n = norm(3.0, 4.0) # -> 5.0 ``` """ return math.sqrt(x * x + y * y)sema doc <project> then emits Markdown that combines reflection (the exact
signature, params, return type, effect row, struct fields with their sem
descriptors, enum variants — always accurate because it is the code) with your
docstring prose (Markdown, LaTeX, > [!NOTE] admonitions, sema examples,
passed straight through). Docstrings are dedented like Python’s
inspect.cleandoc, and being triple-quoted they are raw — LaTeX backslashes
survive untouched.
Two flags close the loop:
--htmlrenders a self-contained page (KaTeX-typeset math, admonitions, code blocks) with no build step — the “nice page”.--skillsemits each module’s doc with skill frontmatter, so generated docs load as model context viaskills.load(see Tools, Skills & MCP) — code that documents itself to humans and to the models that read it.
Run and verify
Section titled “Run and verify”From the sema/ directory:
sema check examples/finops-ledgerSEMA_STRICT=1 sema run examples/finops-ledgersema assure examples/finops-ledger --grade goldsema doc examples/finops-ledger --htmlfinops-ledger is an assure gold project — the strongest grade, which
mutation-tests on top of running test blocks and fuzzing ensure properties.
Variations
Section titled “Variations”- A single-page HTML report — render a
Report, write it withfs.write, thensema doc --htmlthe module for the reflected companion page. - A briefing instead of a report — the
crisis-logisticsexample drafts a public safetyPublicBriefingwith the samesimulate def+check semanticsshape, then redacts and gates it before publication. - Test the exact Markdown — because
renderis pure, put a golden string in atestblock and letsema assureverify layout stability.