ADR 011: Three report presentations from one report
- Status: Accepted. Implementation progress is tracked in the decision register, which is the single place it is stated
- Date: 2026-08-14
- Scope: How a finished report is shown — chat, Markdown file, HTML file, and
jsonfor a machine (added by the D201 amendment below) — and how the user chooses - Related: ADR 008, ADR 009, product intent
Context
The product already has one report dictionary and several writers: Markdown (--output), a PR comment, a remediation prompt, SARIF. Chat (MCP / IDE agent) returns that Markdown. There is no HTML. Choosing a format is a flag, not a question.
1.0 needs three user-facing skins of the same data: what the human sees in IDE chat, a Markdown file, and a single packaged HTML file that an executive will actually open. The bounded work order remains the product (product intent); these are presentations of it, not a dashboard product.
MCP is a local stdio process and does not run input(). It may write exactly five local artifacts: the repository config, user config, user state, repository scan history at .maintainability/history.jsonl and repository baseline at .maintainability/baseline.json; it never writes source or a report. First-run setup writes the first three, the audit's history rule may append the fourth, and an explicit baseline write controls the fifth. The host agent is what can ask the user where to save returned report text.
Options
A. Three independent report builders. Each walk of the tree produces its own numbers. Rejected: two scores is the class of bug this repo already paid for (P4).
B. One report dict, three renderers; flags only. Smallest change. Rejected: the user wants the agent to ask on invoke, default chat if they hit enter.
C. One report dict, three renderers; ask on every interactive invoke; flags win; CI never asks. Chosen.
D. MCP server writes .html / .md into the repo. Rejected: the MCP write boundary permits setup configuration and state, never reports. Chat shows text. Files are written by the CLI or saved by the host after it asks the user for a location.
Decision
-
One view model, three renderers. Chat/CLI text, Markdown file, and one self-contained HTML file all read the report dictionary (and the scan-history records). None compute a score. Architecture rule 3 and the existing extension point stand.
-
Default is chat/CLI text — the Markdown the host already prints in an IDE conversation. Enter / go with no choice selects this.
-
TTY: ask every interactive invoke which of the three to produce. Do not persist the choice.
--format/--output/--html-outputskip the question and win. Non-TTY and CI never callinput()(same class as 6.1).
Amended 2026-09-20 (D201):
jsonis a fourth option at the question. This document defined three user-facing skins and said nothing aboutjson, so for eleven releases the two doors that acceptformat="json"per call —--format jsonon the CLI, theformatargument on MCP — offered it to nobody. The reason given for excluding it, thatjsonis the dictionary the other three render rather than a rendering of it, is true and is not a reason to hide a shipped capability behind a flag a reader has to find. It is built, it is validated on both doors, and the operator's decision is that it is offered:PRESENTATIONSis nowchat, markdown, html, json, in that order, and every surface that recites the options derives them from it.The invariants below are unchanged.
jsoncomputes no score — it emitsjson.dumps(report)of the same dictionary the three renderers read, so the guarantee that no two presentations disagree covers it by construction. Chat stays the Enter default;jsonis the fourth thing asked, not a new default.
- MCP: first-run setup uses structured elicitation. The
maintainability-agentprompt still tells the host in prose to ask, then callaudit_repositorywith a format argument; that free-text ask was closed under D3, which replaced setup and the slash prompt with structured choices. Chat returns Markdown. HTML and Markdown files are written by the CLI or saved by the host after a location ask, never by the MCP process.
Amended 2026-08-22 (D3 closed). The free-text ask closed under D3. Presentation now arrives through the
formatargument;action/choice_neededseparately governs whether an audit runs at all.
Chat-path amendment (2026-08-16). First-run setup persists a default presentation for later chat calls. An explicit per-call format and the host's own presentation ask always win over that stored default. The TTY rule is unchanged: it asks on every interactive invocation unless an explicit format or output option already decides.
-
HTML is one file: inlined CSS, charts as deterministic SVG generated from stored fields (no CDN, no fetch at view time — P1). It leads with an executive summary (estimate, grade, range, series direction or “no history yet”). Required charts, from schema-2 records only: - estimate + range over recorded scans - five pillars over time (condition) - practice / maturity level over time (separate series — never averaged with condition, ADR 007) - current-run category bars Schema-1 scans appear on the rollup series and are gaps on pillar/practice charts. Empty history is an empty state, not a fabricated sparkline. Same remaining sections as Markdown: coverage, trend text, economic context, pillars (with practice caps), ISO categories, aspect scores including the unscored list, environment work order, largest files, function hotspots, work order / prompt, findings. A heading Markdown prints because the report dict has the data cannot be HTML-only-absent;
tests/test_html_section_parity_class.pyholds the catalog, not two leftover headings. Finding rows carry a presentation severity derived from the published class risk in the standard andCLASS_RISK_EFFORT: risk 5 is Severe, 4 is High, 3 is Medium, and 1–2 is Low. Hard-gate failures display as Severe. These labels organize the existing findings; they do not change the estimate, range, or verified grade. -
SARIF and the PR comment stay. They are CI artifacts, not this trio.
-
History for charts is ADR 009 schema 2. No second store. Empty history is an empty state, not a fabricated series. Trends describe past scans; nothing forecasts.
Consequences
Presentation grows an HTML renderer and a TTY format question. MCP grows a format parameter and a prompt line. The CLI writes --output and --html-output. Consumers who only ever use --output keep today’s Markdown path.
A pretty HTML page that invents a second score, or that cannot be reproduced from tree + config + history, is a defect.
Invariants
- Two renderers of the same report dict never disagree on estimate, range, grade, or finding identity.
- HTML contains no
http://orhttps://resource load (no CDN, no live chart library). - Non-TTY runs never call
input()for format. - A set
--format/ output path suppresses the TTY question. - MCP never writes source or report files. Its write boundary is exactly repository configuration, user configuration, user state, repository scan history at
.maintainability/history.jsonland repository baseline at.maintainability/baseline.json. - Charts and trend sentences are computed only from fields present on stored
ScanRecords (schema 2). A missing series is omitted or named empty, never interpolated.
Amendment, 2026-09-27 — the chat door saves the chosen file (D216)
Supersedes, in part, the Context's write boundary, decision §4's "never by the MCP process", the rejection of option D, and invariant 5. The maintainer's standing spec: the report is saved as a file, and chat only ever gets a summary.
What failed was the handoff this ADR relied on: return the report as text and let the host save it. That worked while the HTML was small. From 2026-08-31 the HTML is the complete report — every work item with its prompt, and the history charts — and on a real repository it is far over a host's result limit. The size fix of that day bounded the Markdown field only and left the complete HTML in the same reply. On a private application's first run the reply was 308,957 characters, the host refused it, and no report reached anyone.
- markdown and html are saved by the MCP server, to
output_path: the location the person chose when the host asked where to save. A directory gets the report's own name. The location must be inside the repository or an allowed root, and the write goes through the same symlink-refusing, bounded writer as every other artifact. An existing file is replaced only when it is a report this tool wrote (its first line is the banner); anything else — the config, the history, a source file — is refused. - The reply carries the bounded view and
report_path, never the complete report. A file format without a location is refused before the audit runs, so the host asks. - A saved report declares itself generated on its first line, HTML and Markdown alike, so the next audit measures the repository and not the last run's report.
- Invariant 5 becomes: the MCP server never writes source. Its writes are the repository configuration, user configuration, user state, the scan history, a requested baseline, secure-code-agent's trend, and a report at the location the person chose.
- The guard is the whole reply, not one field:
tests/test_the_chat_payload_has_a_budget.pymeasures every presentation's entire reply against a budget on a repository whose complete report is far over it.