Bounded report rendering
The opt-in reporting feature is a Preview source-level contract with two
related inputs. It retains the standalone renderer for a host-owned typed
RunReport, and—when scanning is also enabled—adds the central typed
assessment composition and rendering path used by completed CLI web-review
runs. Neither path is a scanner, persistence layer, or independent verdict
authority.
Runtime scope
| Surface | Input | Schema | Caller and redaction boundary |
|---|---|---|---|
| Generic run report | Immutable, constructor-validated RunReport |
venom-rendered-run/v1 |
Standalone library hosts call ReportGenerator::generate; they must pre-redact every projected free-text field |
| Typed assessment report | Completed runtime-owned WebAssessmentRunReport plus the exact validated ScanProfileV1, composed into AssessmentRunReport |
venom-rendered-assessment/v1 |
scanning + reporting library hosts and the CLI call the central composition/renderer; assessment summaries and references are already redacted before rendering |
Both paths return Result<String, ReportError>, support the same
ReportFormat values, and enforce MAX_RENDERED_REPORT_BYTES (16 MiB). A
rendering failure returns no partial document. Rendering itself performs no
filesystem or network I/O and does not persist output.
Human assessment presentation
Completed typed assessments use a three-part English presentation in HTML and Markdown:
- Decision overview reports independently derived counts for verifier-bound confirmed items, needs-review candidates, and informational observations. These are typed item counts, not a count of confirmed vulnerabilities. An unassigned severity is not silently treated as low or zero severity.
- Actionable items presents verifier-bound items, then review candidates, then observations as an evidence-assurance order, not severity, impact, or remediation priority. Each item retains its redacted observation, opaque assessment-subject reference (not proof of affectedness or location), claim basis, capability-owned recommendation and ID, evidence references and count, explicit interpretation limit, and safe verification guidance. Human formats also show a closed structural target kind while withholding the resource location, query name/value, and stable locator hashes. A recommendation is not a verified fix, and report integrity does not verify remediation. Active revalidation requires separately established exact target/context/action authority; report references do not grant it.
- Technical audit appendix retains report metadata and available selected capability/source audit details, including their local accounting when present, source context, and collection limitations.
The current general item contract does not bind each item to a principal. When a supplied-session audit is present, the human report therefore points to its run-level health and coverage records instead of inventing item-level principal attribution. The operator-declared principal alias is not identity authentication. An empty item list is reported as an empty bounded projection, not evidence that an application is secure or that coverage was exhaustive.
This presentation is deliberately additive to the human formats. Assessment
JSON remains venom-rendered-assessment/v1, CSV keeps its existing columns,
and Verify and Compare continue to consume the existing machine-readable
contracts. There is no locale-selection contract yet; changing narrative
language must not be inferred from the host locale.
The no-profile CLI path never calls the assessment composer and its
decision-scan/v1 contract remains
unchanged. The explicit baseline profile likewise does not use the typed
assessment renderer.
Optional control-reference mapping audit
When the development-only control-reference-mapping feature and explicit
--profile web-review --control-reference-mapping are selected, the central
composer may attach one strict security.control-reference-mapping-audit/v1
section. The audit records the built-in catalogue identity/revision, bounded
source metadata and item-to-reference relationships, reconciled mapped,
unmapped and omitted counts, and explicit zero target/provider request counts.
It contains no standards body text beyond the permitted identifiers and exact
title references described in the
mapping contract.
Optional reconnaissance snapshot import audit
With recon-snapshot-import compiled and --profile web-review
--recon-snapshot FILE selected, the central document may contain one
recon_snapshot_import object using
security.recon-snapshot-import-audit/v1. It records the exact supplied-byte
identity separately from declared snapshot/source provenance, bounded
source-qualified hypotheses, reconciled coverage/index accounting, zero import
network activity, and explicit claim limits. It never changes items, item
counts, dispositions, severity, evidence authority, or target scope. The source
file and its local path are not bundled.
Feature-independent Verify validates the saved structure and bundle bytes, not
source authenticity or asset truth. Compare reports methodology, provenance,
coverage, and hypothesis-set differences separately; one-sided audit presence
is not_comparable, and no difference establishes target change or remediation.
See the import contract.
This appendix is descriptive. It does not modify any item's stable identity, severity, disposition, remediation, evidence or claim authority. It is not a score, pass/fail result, certification, legal advice or compliance determination. An item with no catalogue relationship is unmapped under this finite catalogue, not a fulfilled control; an item absent from the report says nothing about control fulfilment.
Standalone generic API
Enable only reporting for a host that already owns a RunReport:
[dependencies]
termivar-scanner = { path = "/path/to/reviewed/termivar/crates/termivar-scanner", default-features = false, features = ["reporting"] }
use termivar_scanner::{ReportFormat, ReportGenerator, RunReport};
fn render(report: &RunReport) -> Result<String, termivar_scanner::ReportError> {
ReportGenerator::generate(report, ReportFormat::Json)
}
The same input and format produce the same document. Format encoding is not
redaction: this generic path copies target, authorized_origin, step/outcome
action_id, and outcome redacted_summary. The library host must pre-redact
those fields and decides whether and where to persist the returned string.
Typed assessment API
With both scanning and reporting, a host can compose only completed,
runtime-owned assessment truth:
[dependencies]
termivar-scanner = { path = "/path/to/reviewed/termivar/crates/termivar-scanner", default-features = false, features = ["scanning", "reporting"] }
use termivar_scanner::{ReportFormat, ReportGenerator};
use termivar_scanner::web_runtime::{ScanProfileV1, WebAssessmentRunReport};
fn render_assessment(
runtime_report: WebAssessmentRunReport,
profile: ScanProfileV1,
) -> Result<String, Box<dyn std::error::Error>> {
let report = ReportGenerator::compose_assessment(runtime_report, profile)?;
Ok(ReportGenerator::generate_assessment(
&report,
ReportFormat::Json,
)?)
}
Composition validates that the assessment completed, that its limits and
defense mode match the selected web-review profile, and that accounting and
opaque item references belong to the same runtime truth. The generic run
envelope is minted internally from runtime-owned clock and accounting data;
the caller cannot substitute a generic RunReport as assessment authority.
The typed renderer keeps Informational, NeedsReview, and Confirmed
visibly distinct in every format. It preserves each item's claim basis and,
when present, its complete opaque verifier/case/outcome linkage. Incomplete or
cross-context linkage fails closed. It does not promote an item, infer a claim
from action success, synthesize CVSS/risk, or accept legacy ScanFinding
records. The currently implemented native passive header/cookie capabilities
emit only Informational; no native assessment capability currently produces
Confirmed.
The exact origin root retains authorized-root@1. Eligible discovered
exact-origin subjects can enter this completed-report path through opaque,
deterministic discovered-resource@1 identities; renderers receive only the
existing references and digests, never query values or readable path material.
A non-root starting target remains typed incompleteness unless the explicitly
selected WordPress metadata-discovery path binds it to a validated application
reference. The projected item keeps the stable origin-root subject; deployment
scope is carried by the versioned WordPress discovery audit.
CLI assessment output
Completed --profile web-review runs always use the typed assessment renderer:
# Default text selection maps to Markdown.
termivar scan <AUTHORIZED_TARGET> --profile web-review
# Existing --format json maps to the additive assessment JSON schema only
# because web-review was explicitly selected.
termivar scan <AUTHORIZED_TARGET> --profile web-review --format json
# Select any central renderer explicitly.
termivar scan <AUTHORIZED_TARGET> --profile web-review --report-format csv
termivar scan <AUTHORIZED_TARGET> --profile web-review \
--report-format html --report-output assessment.html
--report-format accepts json, csv, html, or markdown and requires
--profile web-review. --report-output additionally requires an explicit
--report-format. A completed file-output run writes no report document to
stdout.
The CLI creates a same-directory temporary file with exclusive creation, writes and synchronizes the complete rendered bytes, then publishes the new destination with a hard link. It never overwrites an existing destination and attempts best-effort temporary-file cleanup on failure. If cleanup after the hard link fails, the complete destination and temporary file can both remain while the command returns nonzero; it does not report publication success. Directory-metadata crash durability is best effort, and filesystems without the required same-directory hard-link semantics fail nonzero.
Optional live progress channel
The unreleased 0.10.0-alpha.3 development source accepts --progress only
with an explicit --profile web-review. Published v0.10.0-alpha.2 archives
do not contain the flag. It is a bounded, lossy stderr presentation channel;
it does not alter assessment execution, deadlines, request ceilings, report
selection, or any report schema.
termivar scan <AUTHORIZED_TARGET> \
--profile web-review \
--progress \
--report-format html \
--report-output assessment.html 2> progress.log
Every emitted line is fixed-shape ASCII beginning with [progress]. Lifecycle
states separate assessment execution, composition, rendering, stdout writing,
file or bundle publication, and terminal completion/failure. Periodic running
snapshots are sampled once per second and may be coalesced or dropped when the
stderr sink is slow. A blocked stderr sink cannot exert assessment
backpressure; at most one detached presentation writer may remain until process
exit. If that OS write retains the shared process stderr lock, later diagnostics
can also wait behind it; the 250 ms worker handoff is not a universal bounded
process-exit guarantee. Progress I/O failure disables the channel without
changing the scan result.
All numeric fields use counts=last_observed. Accounted requests and active
verifications are cumulative parent-budget charges, not completed responses or
current concurrency. subjects_started marks child-runtime execution and
subjects_processed marks state committed for report projection; neither is a
success or finding count. Counts not yet supplied by a runtime checkpoint are
rendered as unknown, not inferred zeroes. The stream provides no percentage,
ETA, target/path, finding, severity, or vulnerability verdict. It remains
outside stdout, rendered documents, bundle payloads, and manifests, preserving
machine-readable output purity. A successful terminal state follows successful
selected-output publication; a bundle publication failure can report only its
typed manifest commit state.
The monotonic elapsed_ms presentation clock starts after runtime construction,
immediately before the one assessment analysis. Its terminal observation is
taken after the selected stdout or file publication succeeds or fails, so it
includes composition, rendering, publication, and progress handoff time. It is
separate from and does not replace, pause, extend, or alter the assessment
deadline clock.
Single-run report bundles
The published v0.10.0-alpha.2 binary and later development source can render
both assessment formats from one completed run:
termivar scan <AUTHORIZED_TARGET> \
--profile web-review \
--report-dir ./assessment-001
--report-dir requires an explicit --profile web-review and conflicts with
both --report-format and --report-output. It does not change feature
defaults. The existing --format text|json selection remains independent: it
controls the diagnostic envelope if a started assessment is incomplete or
fails, not the two formats in a successful bundle. A successful bundle writes
no report document to stdout.
The assessment executes once and is composed once into the existing immutable typed assessment model. The existing renderer then produces HTML and JSON from that same value; output selection does not replay target requests. A successful destination contains exactly:
assessment-001/
assessment.html
assessment.json
manifest.json
The HTML remains the existing self-contained, script-free assessment document.
The JSON retains venom-rendered-assessment/v1 and can be supplied directly to
termivar report compare. Each report keeps the existing 16 MiB document
ceiling. The manifest is bounded to 64 KiB and uses the additive
termivar-report-bundle/v1 schema. It records the Termivar package version,
completed profile/status and available typed counts, plus exactly two sorted
payload entries with fixed relative names, media types, exact byte lengths, and
lowercase SHA-256 digests. It neither hashes itself nor includes a target URL,
credential, local absolute path, response body, or invented source/run identity.
Checked arithmetic also caps the total in-memory bundle payload at two report
ceilings plus the manifest ceiling (32 MiB + 64 KiB).
The destination must not already exist. Its parent must already exist as a
trusted, private, user-owned directory; Termivar does not create missing
ancestors. Files, directories, links, ., .., roots, and invalid final
components are rejected as destinations. Exclusive creation of the final
directory reserves it before credential loading or network construction, so a
competing writer cannot reuse an existing directory. Newly owned outputs use
restrictive permissions where the platform supports them. On Windows, the new
directory inherits its parent's ACL; Termivar does not claim to install a new
ACL policy. Trust in the parent path remains an explicit boundary.
Termivar verifies that the immediate parent is an existing non-link directory;
the operator's ownership/private-directory assertion is a precondition, not a
whole-path ownership or ancestor-integrity check performed by this command.
That trust boundary also assumes another actor able to mutate the parent does
not race file replacement against publication or cleanup.
Publication renders and bounds all documents before committing final report
names. It publishes and synchronizes assessment.html and assessment.json,
then publishes manifest.json last with no-overwrite file semantics. The final
manifest is the bundle's completion marker: readers must verify its two names,
lengths, and hashes before treating the directory as complete. Payload files
and temporary construction state can be visible before that point, so the
three-file operation is not an atomic directory snapshot for arbitrary readers.
The digests identify exact bytes; they are not signatures and do not establish
source authenticity, trusted scope, remote delivery, or remediation.
Before the manifest commit point, ordinary failures attempt to remove only known files owned by that publication and then only an empty owned directory. Cleanup uncertainty is reported instead of deleting unknown contents. If the manifest was committed but temporary-file housekeeping later fails, the valid committed files are retained and the command reports the post-commit error; it does not claim rollback. A crash or forced termination can leave an incomplete directory, and that directory must be inspected or removed deliberately rather than reused for another bundle. File synchronization and best-effort supported directory synchronization do not promise universal power-loss durability.
To compare JSON from two deliberately selected bundles without starting a new scan:
termivar report compare \
--before assessment-001/assessment.json \
--after assessment-002/assessment.json \
--same-scope
The preserved v0.10.0-alpha.1 archives predate --report-dir; their genuine
first-use JSON and HTML captures remain separate assessment executions. The
published alpha.2 archives include the bundle command.
The development report-bundle example
records one local loopback run, exact payload hashes, and an offline
self-comparison without relabelling it as release or effectiveness evidence.
Offline report-bundle verification
The published v0.10.0-alpha.2 binary and later development source can check a
saved bundle with Report Bundle Verification V1 without starting a scan:
# Human-readable result (the default).
termivar report verify --dir ./assessment-001
# One bounded structured result on stdout.
termivar report verify --dir ./assessment-001 --format json
The verifier accepts one explicit directory and the strict
termivar-report-bundle/v1 layout only:
assessment-001/
assessment.html
assessment.json
manifest.json
It rejects missing or additional entries and does not recurse. Manifest names
are validated against the two fixed payload names rather than treated as paths.
The command reads the manifest within its 64 KiB limit and each payload within
the existing 16 MiB report limit. It checks the fixed formats and media types,
measures and hashes the exact bytes captured from each opened payload, validates
assessment.json through the existing display-only assessment importer, and
compares its completed profile, subject count, and item count with the manifest.
HTML is checked only for bounded UTF-8 bytes, length, and digest; it is never
launched, rendered, or executed.
The result schema is termivar-report-verification/v1. Its checks distinguish
checked_matched, checked_mismatched, and not_checked; a failure that stops
the pipeline cannot make a dependent check look successful. integrity_match
means only that the supported layout, manifest contract, captured payload bytes,
and assessment JSON summary matched during that invocation. not_verified
uses bounded reason codes such as missing_manifest,
payload_digest_mismatch, or assessment_summary_mismatch without echoing
document contents or absolute paths.
Exit 0 means every supported check completed and matched. Exit 1 means the
bundle was incomplete, unreadable, unsupported, mismatched, or otherwise could
not be verified. Invalid command syntax exits 2 through Clap. After valid
argument parsing, both an integrity match and an ordinary verification failure
produce exactly one text or JSON result on stdout. An output-write failure is a
separate nonzero error because the CLI cannot guarantee that the result reached
the caller completely.
Verification is read-only application behavior: it initializes no scanner, network client, credential source, or provider, and it does not write, repair, remove, rename, or change permissions inside the bundle. Filesystem access-time metadata remains outside that application-level promise. A caller-selected mounted filesystem may still be remotely backed; "offline" means the command itself issues no network requests. The bundle must be quiescent beneath trusted parent directories. The command rejects a symlink/reparse-point final directory and non-regular payloads, retaining and validating opened handles; this does not establish whole-path containment when an untrusted actor can replace parent components, or establish provenance for a regular hard link. Hashing and assessment parsing use the same captured payload bytes, but verification is not an atomic snapshot and cannot guarantee that the directory remains unchanged afterward or detect every concurrent same-size in-place write.
Matching hashes are integrity metadata, not a signature. Verification does not
establish producer/source authenticity, the validity or authorization scope of
the original scan, finding accuracy, remediation, HTML-to-JSON semantic
equivalence, or executable HTML safety. An editor can modify a payload and its
manifest consistently and still receive integrity_match; authenticity remains
not_established. For a control-reference audit, Verify likewise checks only
the supported schema and saved bundle bytes. It does not establish catalogue
truth, source authenticity, applicability, legal meaning, compliance or control
fulfilment.
After verification, the same validated bundle JSON can be used by the existing offline comparison command without conversion:
termivar report compare \
--before assessment-001/assessment.json \
--after assessment-002/assessment.json \
--same-scope
Both report commands are offline document operations, but they answer different
questions: verification checks one bundle's supported internal consistency;
comparison groups imported observations from two operator-selected reports.
Neither certifies security or remediation. The published alpha.2 archives
contain report verify; the preserved alpha.1 archives do not.
Offline assessment report comparison
termivar report compare reads exactly two explicit local files and performs
no scan, target request, credential lookup, provider operation, or browser
launch. It accepts supported, complete venom-rendered-assessment/v1 JSON
documents, including renderer output with the current optional audit sections.
It rejects operational decision-scan/v1 output, incomplete diagnostic
envelopes, unknown schemas, duplicate JSON keys or item identities, malformed
fingerprints, inconsistent counts, and inputs above the 16 MiB per-file limit.
# Markdown to stdout (the default).
termivar report compare \
--before before.json \
--after after.json \
--same-scope
# Structured comparison to a new file.
termivar report compare \
--before before.json \
--after after.json \
--same-scope \
--format json \
--output changes.json
# Standalone interactive comparison to a new file.
termivar report compare \
--before before.json \
--after after.json \
--same-scope \
--format html \
--output changes.html
--same-scope is required. It records only the operator's assertion that the
two files were deliberately selected for comparable assessment scope; parsing
does not authenticate either source or reconstruct target identity. Source
hashes are included, but a hash provides integrity identification rather than
authenticity. The documents may differ in enabled work or retained evidence,
so equivalent assessment coverage is not established.
Items match by the renderer's existing stable fingerprint together with the
compatible capability identity. Array/key order and report-local subject,
evidence, case, and outcome reference numbering do not define identity. The
new termivar-report-comparison/v1 projection has four mutually exclusive
groups:
only_in_after: present only in the supplied after document;only_in_before: present only in the supplied before document;changed: matched identity with different supported comparable content;unchanged: matched identity with equal supported comparable content.
When either input contains a supported control-reference mapping audit, the comparison keeps catalogue/source identity and relationship coverage separate from target items. A catalogue revision, source edition, rights status, rationale or mapping-set change is a methodology change, not a newly observed target condition, remediation, compliance transition or control-status change. The existing four top-level item arrays remain the item partition; no fictional top-level counts object is introduced.
When at least one imported assessment contains a supported WordPress audit,
the comparison includes the additive optional
wordpress_review_comparison section. Its nested schema is
termivar-wordpress-review-comparison/v1, or additive /v2 when either input
contains discovery source content such as page-scoped records; the outer
comparison remains termivar-report-comparison/v1, and the four item groups
above retain their existing identities and meanings. If neither input has a
WordPress audit, the section is omitted.
WordPress correlation is valid only inside the same operator-declared
--same-scope boundary. Components use component kind plus canonical slug as
their stable key. Native advisory keys add catalog.id and the upstream
advisory ID; external keys instead add the source namespace and upstream ID.
External audit /v6 keys use the retained raw source kind and source identifier,
so case-only source spelling and otherwise non-canonical provider identities are
not silently collapsed. Structurally valid source associations that cannot be
represented as canonical Termivar components remain accounted for in the
unresolved total. Up to the bounded deterministic projection is emitted as
explicit canonical_identity_unavailable / not_evaluated advisory
limitations; those projected rows keep their affected ranges, source-declared
patch/remediation fields, bibliography, and bound notices visible and
comparable. The remaining unprojected rows are represented only by reconciled
coverage counts, without inventing an installed component or applicability
result.
Array position, display title, CVE, and report-local ordering are not identity.
The comparison keeps component evidence, advisory content, affected ranges,
source-declared fix/remediation information, methodology, provenance, coverage,
and applicability changes separate rather than attributing one to another.
If exactly one input has a WordPress audit, the WordPress result is
not_compared with coverage_changed; records on the present side are not
classified as newly vulnerable or remediated. More generally, an advisory's
disappearance or an applicability transition is document change, not proof of
a fix, a new vulnerability, or a change at the installation.
The importer supports the historical
security.wordpress-review-audit/v1 through /v4 contracts and the additive
/v5, /v6, and /v7 contracts. /v7 binds the strict page-scoped discovery
audit /v3, including its opaque page sources and association/accounting
coverage, without importing page bodies or granting scan authority. The strict
v3 reader vocabulary reserves linked / fetched rows for the coordinated
bounded linked-page slice; reader acceptance alone does not dispatch a request,
and the current CLI exposes only observed mode. /v6
carries the explicit raw-source identity
mapping and bounded-capacity policy (including the additive 160 MiB V3 import
envelope while preserving the older V1/V2 ceilings), accounted retained-import
bytes, reconciled
mapped/unresolved association partitions, and the non-canonical identity
limitations described above. The complete unresolved total is retained while
at most 64 deterministically ordered limitation rows are expanded; the explicit
projected/unprojected counts prevent that bounded report view from looking like
complete per-record detail. Historical /v4 and /v5 external rows are
normalized as canonical exact mappings for semantic comparison, so a
capacity-only /v5 to /v6 transition does not falsely report every exact
advisory identity as changed. The /v5 contract is produced when an operator explicitly selects a Termivar
interpretation profile for local Wordfence ranges. The /v5 comparison keeps
the selected profile, explicit-operator basis, and unestablished source-semantics
assurance in the methodology facet. A transition from an unresolved /v4
result to a calculated /v5 result, or between /v5 profiles, is therefore a
methodology change—not evidence that an installation or vulnerability changed.
For wire compatibility, the v5 unsupported_associations count is the same
partition as indeterminate_associations; human output shows the precise
indeterminate label once rather than describing missing, conflicting, or invalid
cases as unsupported.
Each version is compared only to the depth of typed meaning it actually carries.
Exact-input byte digests identify provenance bytes, not semantic identity or
source truth: bytes can differ while the supported semantic projection remains
equal. This comparison only reads the two saved reports; it does not rerun the
scanner, WP-CLI, PHP, an advisory lookup, or any network operation.
Only-in-before does not mean fixed, resolved, verified-remediated, or safe. Only-in-after does not establish when an observation first appeared. Unchanged means only that the supported display projection is equal; it is not proof of security or equal scan coverage. Imported disposition, claim basis, severity, CWE, confidence, summary, remediation, and selected evidence-linkage metadata are displayed without independent endorsement. No CVSS, evidence, verdict, or target identity is reconstructed.
Without --output, the complete selected encoding goes to stdout and
diagnostics go to stderr. With --output, the existing same-directory atomic,
no-overwrite publisher is used: an existing destination or an input/output
collision fails, and malformed input produces no partial destination. Inputs
are opened as bounded regular files and never modified. Exit 0 means the
offline comparison document was produced; invalid CLI usage exits 2; input,
comparison, or publication failures exit nonzero (normally 1). Error text
does not echo supplied paths or document bodies.
Markdown is the terminal default. JSON is deterministic structured output.
The standalone HTML includes four count cards, group filters, text search,
expandable before/after fields, responsive dark/light styling, keyboard focus,
and print styling. It has no external resources, storage, forms, frames, or
network capability. Imported values are pre-rendered as encoded text. A small
fixed CSP-hashed script changes only textContent, hidden, expanded details,
and filter-button state; with scripts blocked, all comparison entries remain in
the document and can be read or expanded. Encoding prevents active markup; it
does not remove secrets from untrusted imported text, so review output before
sharing it.
A small synthetic CLI-generated example demonstrates all four groups. Its edited input is document-processing fixture data, not another assessment and not evidence that a security condition changed.
An incomplete or started-failed web-review assessment is not a partial typed
report. It emits the redacted web-assessment/v2 diagnostic audit to stdout,
marks assessment items unavailable, returns nonzero, and creates no requested
report artifact. A failure before runtime execution starts also returns nonzero
without creating an artifact.
Formats and bounds
Format negotiation is available through
ReportGenerator::available_formats():
| Variant | Token | Media type | Extension |
|---|---|---|---|
Json |
json |
application/json |
json |
Csv |
csv |
text/csv; charset=utf-8 |
csv |
Html |
html |
text/html; charset=utf-8 |
html |
Markdown |
markdown |
text/markdown; charset=utf-8 |
md |
ReportFormat::as_str, media_type, and extension expose those values. A
render can fail with ReportError::Serialization or
ReportError::OutputLimitExceeded; neither error returns a truncated document.
JSON preserves full-width integer fields as decimal strings where the v1 schema requires portability and escapes controls and bidirectional controls. CSV quotes every cell, neutralizes spreadsheet-formula prefixes, and uses visible reversible escapes. HTML and Markdown apply context-specific encoding to every projected text value.
See ADR 0021 for the original generic
renderer boundary and ADR 0023
for the additive CLI composition and publication boundary. The typed assessment
schema does not reinterpret the generic renderer contract or decision-scan/v1.