89. An interface shows only what is derived, and unknown is never drawn as healthy
Context
On 2026-09-29 two read-only audits, one of the Cockpit and one of the public site, classified
every fact and list the two interfaces show. Each fact was classed as derived, derived without
provenance, hardcoded, mock, duplicated inventory or optimistic default. The good news came
first: ADR 0012 and the cockpit-projection gate make the Cockpit a real projection. No page
reads the index, and nothing is mocked. What was left was a thin layer that each existing
rule missed:
- A status decided by a literal. "Attached clients" in
health.reportisstatus: HealthStatus::Okand cannot be anything else. Its count includes peers that are gone, while the preflight card on the same page says three of them are gone. - A count that is a literal. The command effect chips pass
0as each count while the table below lists commands of every effect. - Unknown rendered as a value. An upstream with no tracking data reads
+0 −0. A model with no declared status reads "available" becauseModelStatushas#[default] Available. A navigation count is0whenrepository.infofails. - A verdict in the view. The Artifacts page calls a set "current" while ignoring the files that have no recorded hash. The Worktrees strip shows eight of thirteen tallies, and the missing ones are the bad ones.
- A declared status styled as proven. On the site, a
guaranteedclaim shares the success palette, while the evidence baseline records none of those claims as supported. - Inventories kept twice. The navigation areas, the dispatcher arms, the vocabulary chips and the test page list are each hand-kept beside the thing they list.
- No freshness. The index and the git state are read once, at server start, and shown with no time.
The owner stated the invariant that all of these break as UI-DERIVED-STATE, in seven parts:
- no mocked production UI data;
- no hardcoded domain state;
- no duplicated inventories;
- evidence required;
- unknown is not healthy;
- freshness is explicit;
- the frontend computes presentation, not reality.
Several rules and gates already hold parts of it. None of them states the whole, and none of them reads Cockpit Rust literals:
| Mechanism | Holds | Does not reach |
|---|---|---|
project.product-surface-derived, no-counts-in-prose (site-check, derive-check, case 97) | 2 and 3 for features, modules, commands, providers and counts on the site | statuses and verdicts; Cockpit literals |
project.derived-once | 3 as a performance rule (advisory, no gate) | truth of what is shown |
project.ui-conformance (ui-check, ui-audit) | accessibility and layout | data truth |
project.web-surface-declared-once (web validate, cases 84 and 89) | 3 for mounts and routes | area lists, vocabularies, tallies |
ADR 0012, cockpit-projection gate, case 389 | 1 and 7 for the index | the registry, product model and Why catalogue |
homepage-check | 1, 2 and 5 for the homepage narrative, trust and providers | the Cockpit; a claim's evidence (#655 extends it) |
project.entry-reports-only-evidence (case 358, tests/preflight.rs) | 4 and 5, typed, for the preflight card | every other verdict |
Decision
UI-DERIVED-STATE becomes one blocking project rule, project.ui-derived-state, whose seven
sub-rules are the owner's seven invariants. The existing mechanisms stay where they are, and
the rule names them as its parts. What this ADR adds is the statement and a gate that holds
the parts a line scan can see.
The gate is a widening, not a new unit
The ui-integrity gate is scripts/ci/cockpit-projection-check --ui-integrity. It runs in
the structure job in every plan, like cockpit-projection. It widens the existing checker
rather than adding a shell unit, because ADR 0069 has shell only shrink. Its first half is
the same question that checker asks, asked of three more fields of the context, and its other
checks are the same kind of cheap, deterministic line scan. The index half stays under
cockpit-projection unchanged. Moving both into a typed capability of the crate's quality
subsystem is the migration the checker's inventory record already names.
Its first checks:
- direct-read (invariants 1 and 7). A Cockpit file that reads
ctx.registry,ctx.productorctx.whyitself, on one line or split across two, is refused. The exception is a declared, commented allow-list in the checker. Today the allow-list isnav.rs: the navigation's module listing is the registry's module list, grouped by the product model's derived area and titled by the Why catalogue (ADR 0026), andnav.rs's tests assert exactly that. An allow-list line is a file and a field. - unconditional-ok (invariants 2, 4 and 5).
health.rsgiving a checkstatus: HealthStatus::Okas a literal is refused. - healthy-default (invariant 5). A
#[default]on a healthy variant (Ok,Available,Current,Valid,Verified,Liveand the like) anywhere in the crate is refused. A value nobody declared is unknown, not good. - count-default (invariant 5). In
pages.rs,unwrap_or(0), orunwrap_or_default()on a chain that reads a JSON number, is refused. An absent count is unknown, not zero.
A site that is right as written says why in a // ui-integrity: <reason> comment on its line
or directly above it. A reason is read in review, and an empty one is no exemption. The
registry check in health.rs is the expected first user: a registry that does not build
stops the process before any transport is bound.
A ratchet over what master holds
On master at e2c5ab3d44 the gate finds thirteen findings. They are held in
.ai/repo/ui-integrity-baseline.txt, which --write-baseline writes and nobody edits by hand.
A baseline line is a check, a file and a subject (a function, a check id, an enum variant, the
source text), never a line number, so an edit above a finding does not move its key. A finding
the baseline holds is debt and passes. One it does not hold fails. A line nothing matches any
more is reported as paid and does not fail, so the change that fixes a finding lands without a
second edit to this file, and the next --write-baseline drops it. --strict ignores the
baseline, and it is the end state.
The baseline's lines, and who pays them:
- #656 (
fix/the-cockpit-says-only-what-it-knows) pays four:unconditional-ok … peers,healthy-default … ModelStatus::Available, and the twocount-default … upstream_celllines (u.ahead,u.behind). Run against #656's tree, the gate passes and reports those four as paid. - Follow-up pays the other nine:
unconditional-ok … registry: an exemption comment, since it is structural;unconditional-ok … scope: derive it from the scope diagnostics;- the two
count-default … activitylines in the perf counters; - the five
direct-read … ctx.registryreads inpages.rs(capabilities,capability,api,search,topology), which move tocapabilities.listandcapabilities.get. Neither #656 nor #657 touches these, and the gate passes on both trees.
The three open pull requests that fix the audits' violations are the rule's first payments:
- #655 (
fix/a-declared-guarantee-is-not-shown-as-proven): the site stops colouring a declared guarantee as proven and counts declared apart from supported (invariants 4 and 5). - #656 (
fix/the-cockpit-says-only-what-it-knows): the Cockpit's literal statuses, literal counts and optimistic defaults become derived or unknown (invariants 2, 3 and 5). - #657 (
feature/every-answer-says-when-it-was-observed): repository answers carry when they were observed, and a statistic names the capability it asked (invariants 4 and 6).
What this ADR does not add
It does not re-implement homepage-check, which #655 extends. It does not write the
fault-injection sweep. Both are follow-ups:
- Fault injection (invariant 5): run every Cockpit route with each capability it asks forced to fail, and assert that no ok or healthy badge and no number renders.
- Tally exhaustiveness (invariant 3): every numeric field of a
*Talliesanswer appears on the page that renders it (the Worktrees and Artifacts strips). - One route table (invariant 3): the dispatcher, the navigation areas,
tests/cockpit.rsanddocs/COCKPIT.mdread from one table (PR #543, reworked on master). - Site template lints (invariants 2 and 3): no literal array of domain vocabulary in a
forloop, anddefault(value=…)only empty, neutral or "unknown". - An
Observed<T>envelope (invariant 6), building on #657, so that freshness is typed the way preflight typed evidence. - The migration of
cockpit-projection-checkinto a typed capability.
Alternatives rejected
- A hand-maintained allow-list of facts. One option was to list every fact an interface may show, with its source, and check pages against the list. That list would be a third inventory of what the registry, the product model and the generated documents already declare, and it would drift on the first page nobody added to it. That is invariant 3 broken by the mechanism meant to enforce it. The gate's allow-list is the opposite shape: it names the few places that may read a source directly, and each place carries its reason.
- Review only.
no-counts-in-proseis enforced by review, and the audits found what review let through: anOkliteral and a0literal sat in reviewed code. ADR 0048 makes "gated" the mode for a rule a program can check. Four of these sub-rules can be checked cheaply, so the rule is gated. - Failing master on today's findings. A gate red on arrival blocks every other change until all thirteen are fixed, and three pull requests already in flight fix some of them. The ratchet admits what is known, refuses what is new, and lets each fix land alone.
- Failing on a paid baseline line. That would make each fix edit the baseline in the same
change. It would redden master whenever a fix landed before the gate, or the gate before a
fix. A paid line is reported instead, and the next
--write-baselinedrops it. - A new typed capability now. It is the right end state (ADR 0069), but it adds a registry entry, projections and a crate build to a structure check that costs a twentieth of a second as a scan. The widening keeps the check cheap today, and the migration is a named follow-up.
- A syntax-tree lint over the crate. More exact than a line scan, but it needs the crate's parser in the structure job. Every check here is written so that rustfmt's layout is enough, and case 547 pins the shapes it must see.
Consequences
- One rule states the invariant, and each existing mechanism is named as one of its parts. A reader asking "is this UI fact allowed" has one place to look.
- A new
HealthStatus::Okliteral, a new healthy#[default], a new zero-for-absent count or a new direct registry read in a page fails in the structure job, in every plan, with the file, the line and the remedy. - The baseline is visible debt with owners: #656 pays four lines, and the follow-ups above
pay the rest.
--strictpassing is the day the gate's part of the rule is fully met. - The line scans are heuristics with stated limits.
count-defaultseesunwrap_or(0)and a numeric JSON read in the same chain, not every integer type.direct-readsees the context fields by name. A shape they miss is a new case in 547 and a widening of the scan, not an exemption. - Sub-rules 1, 3, 4 and 6 are enforced here only where a line scan reaches. The rest remain review, the existing gates in the table above, and the follow-ups.