ADR 0021: Render bounded typed run reports
- Status: Accepted
- Date: 2026-08-20
This decision supersedes the reporting-specific compatibility decision in ADR 0020. The broader platform and distribution quarantine remains in force.
Context
The opt-in reporting module accepted raw ScanFinding records and created a
second report model. It assigned aggregate severity counts and a synthetic risk
score from caller-controlled strings, captured wall-clock time, interpolated
unescaped values into output, and returned an unbounded String. That contract
could turn compatibility observations into a stronger-looking vulnerability
verdict even though it had no verifier authority. It also forced the reporting
feature to enable venom-core/legacy-contracts.
The deterministic and legacy runtimes already converge on the typed,
constructor-validated RunReport boundary. A report renderer should preserve
that boundary instead of inventing another finding model or execution path.
Decision
- The opt-in
reportingfeature depends on exactlycore; it does not enablevenom-core/legacy-contractsand cannot importScanFinding. ReportGenerator::generateaccepts an immutableRunReportand aReportFormat, and returnsResult<String, ReportError>.- The renderer preserves the input report's typed status, stop classification code, accounting, and steps, then emits a privacy-minimized outcome projection containing kind, action identifier, severity, disposition, confidence, evidence count, and the caller-supplied redacted summary. It does not serialize private stop reason/detail text, calculate risk, classify severity, create findings, or promote an observation into a verdict.
- Every format identifies its
venom-rendered-run/v1document contract withREPORT_DOCUMENT_SCHEMA. Every rendered document is limited by the 16 MiBMAX_RENDERED_REPORT_BYTESceiling; a document that cannot fit fails closed with a typed error instead of returning a truncated or oversized success. - Text inserted into a structured format is encoded for that format. Encoding
is not redaction: the renderer copies the report's
target,authorized_origin, step/outcomeaction_id, and outcomeredacted_summaryfields, so callers must redact those values before constructing or passing the report. Rendering is deterministic for the sameRunReportand format and does not read the clock, filesystem, network, process environment, randomness, or mutable scanner state. - The module is a source-level Preview host-library API. No repository CLI, default scan path, persistence layer, or distribution artifact calls it. Hosts explicitly choose a format and own any storage or delivery after a successful render.
- Static architecture checks pin the feature closure, constants, exact public items and signatures, public-type trait implementations, public attributes, re-exports, lifecycle/host classification, retired API absence, output-order determinism, statelessness, and ambient-authority-free source boundary.
Consequences
VulnerabilityReport,severity_stats,phase_stats, andrisk_scoreare removed. The reporting API changes intentionally on the unreleased alpha line.- The renderer can present only claims already encoded by
RunReport. It is not a vulnerability report, security verdict, audit result, or persistence mechanism. - The projection intentionally omits outcome fingerprints and private evidence identifiers, subjects, rationales, cases, rules, hypotheses, step details, and stop detail; these are not renderer output fields.
- The default
venom scancommand and its historicaldecision-scan/v1JSON wire remain unchanged. Adding a repository caller requires a separate composition decision and compatibility review. - Bounded input records plus an independent output ceiling make memory growth reviewable, but they do not make an external host's storage or delivery bounded.
Alternatives considered
- Harden the raw-finding renderer in place. Rejected because escaping and output limits would not repair its authority problem or duplicated report model.
- Make rendering part of the default CLI. Rejected because the CLI already has a versioned output contract; silently replacing or wrapping it would be a product and wire change.
- Write files from the reporting module. Rejected because format conversion and persistence have different authority, failure, and path-safety concerns.
- Truncate oversized documents. Rejected because truncation could silently remove stop, accounting, or outcome context and make a partial report appear complete.