Skip to content

An interface shows only what is derived, and unknown is never drawn as healthy

An interface shows only what is derived, and unknown is never drawn as healthy

majordomus://adr/adr-0089proposed uicockpitsiteevidenceenforcement

it references

referenced by

Nothing in the layer names it. Backlinks are derived, so an empty list is an answer rather than an omission.

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.report is status: HealthStatus::Ok and 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 0 as 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" because ModelStatus has #[default] Available. A navigation count is 0 when repository.info fails.
  • 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 guaranteed claim 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:

  1. no mocked production UI data;
  2. no hardcoded domain state;
  3. no duplicated inventories;
  4. evidence required;
  5. unknown is not healthy;
  6. freshness is explicit;
  7. 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:

MechanismHoldsDoes 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 sitestatuses and verdicts; Cockpit literals
project.derived-once3 as a performance rule (advisory, no gate)truth of what is shown
project.ui-conformance (ui-check, ui-audit)accessibility and layoutdata truth
project.web-surface-declared-once (web validate, cases 84 and 89)3 for mounts and routesarea lists, vocabularies, tallies
ADR 0012, cockpit-projection gate, case 3891 and 7 for the indexthe registry, product model and Why catalogue
homepage-check1, 2 and 5 for the homepage narrative, trust and providersthe 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 cardevery 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.product or ctx.why itself, on one line or split across two, is refused. The exception is a declared, commented allow-list in the checker. Today the allow-list is nav.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), and nav.rs's tests assert exactly that. An allow-list line is a file and a field.
  • unconditional-ok (invariants 2, 4 and 5). health.rs giving a check status: HealthStatus::Ok as a literal is refused.
  • healthy-default (invariant 5). A #[default] on a healthy variant (Ok, Available, Current, Valid, Verified, Live and 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), or unwrap_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 two count-default … upstream_cell lines (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 … activity lines in the perf counters;
    • the five direct-read … ctx.registry reads in pages.rs (capabilities, capability, api, search, topology), which move to capabilities.list and capabilities.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 *Tallies answer appears on the page that renders it (the Worktrees and Artifacts strips).
  • One route table (invariant 3): the dispatcher, the navigation areas, tests/cockpit.rs and docs/COCKPIT.md read from one table (PR #543, reworked on master).
  • Site template lints (invariants 2 and 3): no literal array of domain vocabulary in a for loop, and default(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-check into 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-prose is enforced by review, and the audits found what review let through: an Ok literal and a 0 literal 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-baseline drops 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::Ok literal, 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. --strict passing is the day the gate's part of the rule is fully met.
  • The line scans are heuristics with stated limits. count-default sees unwrap_or(0) and a numeric JSON read in the same chain, not every integer type. direct-read sees 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.

Where this comes from

canonical file
.ai/repo/adrs/0089-an-interface-shows-only-what-is-derived-and-unknown-is-never-healthy.md
identity
adr-0089
also served as
MCP resource majordomus://adr/adr-0089 · majordomus entity show adr/adr-0089 · GET /api/v1/entity?uri=majordomus://adr/adr-0089
in the Cockpit
/cockpit/objects/adr/adr-0089

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.