Rationale
On 2026-09-29 two read-only audits (the Cockpit and the public site) classified every fact
and list the interfaces show. Nothing was mocked, but a thin layer was not what it looked
like. A health check was ok because its status was the literal HealthStatus::Ok. Effect
chips counted 0 because the count was the literal 0. An upstream with no tracking data
read +0 −0. A model with no declared status read "available" because that was the enum's
#[default]. A stat strip dropped the tallies that were bad. On the site, a declared
guarantee was coloured as proven. Each rule that bears on this (product-surface-derived,
entry-reports-only-evidence, web-surface-declared-once, the ADR 0012 projection gate) holds
one part of it. None of them says the whole invariant, and none reached Cockpit Rust literals.
ADR 0089 records the decision.
Required behaviour
- No mocked production UI data. A page renders what a capability answered or what a generator derived. Fixture or illustration data never ships as a production value.
- No hardcoded domain state. A status, count, maturity or verdict is never a literal
in a template, a page or a handler. A literal that is right by construction says why in a
// ui-integrity: <reason>comment. - No duplicated inventories. A list of routes, areas, states, effects or tallies is read from the one place that declares it. It is not written out again beside that place.
- Evidence required. A verdict that asserts health, currency or proof names what it
was decided from (
decided_by,evidence, the preflightCheck). A declared status is styled apart from an observed one. - Unknown is not healthy. An absent, failed or undecided value renders as unknown. It never renders as ok, healthy, available, current or zero. No enum defaults to a healthy variant.
- Freshness is explicit. A value read once and shown later says when it was observed. Where a staleness bound is declared, the value says when it went stale.
- The frontend computes presentation, not reality. A Cockpit page asks a capability
through
Context::execute. It reads neither the index nor, outside the navigation's declared allow-list, the registry, the product model or the Why catalogue. A verdict is decided in the capability, not in the view.
Failure behaviour
The ui-integrity gate (scripts/ci/cockpit-projection-check --ui-integrity, job
structure, every plan) exits 10 on a finding that .ai/repo/ui-integrity-baseline.txt
does not hold. The findings it can see are:
- a direct
ctx.registry,ctx.productorctx.whyread in a Cockpit file outside the allow-list; - a health check given
status: HealthStatus::Okas a literal; - a
#[default]on a healthy enum variant; - an absent count rendered as zero in
pages.rs.
The cockpit-projection gate exits 10 on a direct index read. The baseline only shrinks: a
line is paid by fixing its finding. --strict ignores the baseline, and that is the end
state.
Verification
test/cases/547_the_ui_says_only_what_is_derived.sh plants each violation in a fixture tree.
It asserts that each one is refused by check, file and line, that a clean tree and an
exempted site pass, and that the ratchet admits known debt, refuses new findings, reports
paid lines and is ignored under --strict. test/cases/389_cockpit_projection.sh holds the
index half. Sub-rules 1, 3, 4 and 6 are enforced here only as far as a line scan reaches.
The rest, and the site's templates, are the follow-ups ADR 0089 lists.