Skip to content

Make the calculator roster a checkable contract: declare which tables each calculator writes, then verify it produces rows #577

Description

@WilfordGrimley

The gap, stated precisely

check_calculator_roster_tether() (PR #562) asserts exactly one thing:

if re.search(r"(?<![\w-])" + re.escape(identity) + r"(?![\w-])", prose):
    continue
findings.append("...calculator roster drift: identity `X` is declared in code but has no entry...")

It searches docs/pipeline-fidelity-gate.md for the identity string. That is the whole check. It verifies the calculator is mentioned in a document — not that the mention is true, and not that the calculator produces anything.

Its own error message asks the author to state dormant or casts no votes. Nothing verifies that claim against reality.

Consequence, live today: the tether passes for local-name-frequency-v1, a calculator that has cast nothing in its entire existence — 0 rows in every vote table, 0 of 994 PilotRunLedger rows, never called by anything. It is documented, so it is clean. That is the exact failure mode the tether was built to catch, surviving inside the tether.

We made the mention mandatory and left the accuracy unchecked.

Why this matters — three channels, zero read as "fine" every time

channel what happened how long undetected
stage-d-illustration-v1 3 votes ever — its eligibility gate read ImageEvidence.layout_class believing it carried faced-ness, when that column holds a border colour (non-blank on 99.34% of rows) since it was written
local-name-frequency-v1 never called, never run — 0 of 994 ledger rows 13 days, across two review passes
local-fallback-v1 chips 53,966 CardTagVote rows (border / frame-style / bleed-edge) destroyed by an identity-scoped purge that was meant to be channel-scoped caught immediately, but only by luck

A dormant channel generates no divergence to explain, so a coverage-based audit reads it as clean by being invisible.

The design: one declaration, two checks

Rather than bolt a second, parallel check alongside the tether, extend the roster entry itself into a machine-readable contract.

Each entry in docs/pipeline-fidelity-gate.md declares which tables that calculator writes — a structured field, not prose. Then:

  • Check A (existing, code -> doc): every declared *_ANONYMOUS_ID has a roster entry. Unchanged.
  • Check B (new, doc -> reality): for every table an entry declares, that (identity, table) pair has rows in production. Zero is a failure, not a quiet outcome.

The (identity, table) distinction then lives in the roster, next to everything else about that calculator, rather than in a separate list that can disagree with it.

Why the pair, not the identity alone

This is the distinction the purge incident turned on. local-fallback-v1 writes CardTagVote and (formerly) CardPrintingTag. Asserting on the identity alone would have seen "this identity has rows somewhere" and passed, while the chips were gone. Under the pair:

  • stage-d-slow-path-v1 declares CardScanLog only -> its 0 votes and 135,362 routing markers are correct by declaration, and no allowlist entry is needed to explain it.
  • local-fallback-v1 declares CardTagVote -> 0 rows would have failed the build.

Note the direction is new

The two existing tethers are code -> doc: derive a roster from source, check the doc mentions it. This adds a doc -> reality direction: the doc states a contract, and production is checked against it. Which tables a calculator writes cannot be reliably derived statically (writes go through shared helpers like purge_and_write_votes), so the declaration is human-authored — but it stops being unfalsifiable prose the moment something checks it.

Design constraints

  • Legitimately-zero cases need an explicit, per-entry-justified allowlist, exactly like CALCULATOR_ROSTER_ALLOWLIST — a retired calculator, or one not yet run. "Expected zero" must be a visible decision, never a silent gap. Note local-ocr-v1 is retiring and local-name-frequency-v1 may be, so both will need one.
  • Assert a floor, not a count. "Produced something" is durable; a hardcoded N rots — see docs/features/catalog-stats.md and catalog_stats.py, which both claimed "~11 distinct reasons" against ~30 in code until PR Adopt a *_SKIP_REASON declaration convention; tether the roster to a doc #567 removed the number entirely.
  • Where does it run? Check A is a lint and runs in CI. Check B needs a database, so it is a post-run report or an ops command, not a CI job. A 7-hour pass should probably report loudly rather than exit non-zero at the end; a pass finding a channel silent in its first batch could fail fast. Decide deliberately.

Immediate concrete use

After the first monolith run, the three destroyed chip channels — cast_border_attribute_vote, cast_frame_style_vote, cast_bleed_edge_vote in local_fallback.py — must be non-zero again. If they are not, the re-derivation did not happen and a recoverable loss became permanent. Right now nothing would tell us.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions