G-1: a gate for the dropped field — built first, and seen failing on 40

Campaign 12 ranked this first of eight gating candidates. It is built BEFORE the fixes it finds,
because last night an off-the-shelf tool for a neighbouring class (deadcode, for C6) was made to
prove itself first and found NEITHER of the two defects it was meant for. A gate nobody has watched
fail has not been shown to work.

scripts/wire_contract_gate.py, registered in repo_gates.py as --fast (no network, no container, so
it runs in BOTH the pre-push hook and CI — the R-29 constraint).

THE TEST. For every json tag reachable from a declared wire ROOT, does that literal tag occur
anywhere in the receiving repo's production Go or templates? A tag occurring nowhere cannot be
decoded by any struct, named OR anonymous. That last clause is why a string test is used instead of
comparing struct to struct: Campaign 12's first attempt paired types by shape and false-positived
badly, because the hub decodes one report through several ad-hoc anonymous structs.

RESULT ON TODAY'S TREE: 210 tags checked across 3 declared wires, 51 skipped (generic / opaque /
allowlisted), 40 CONVICTED. Captured verbatim in documentation/tests/wire-contract-gate-2026-08-08/
BEFORE.md, which is deliverable 1 of this session.

The prompt for this session said "465 emitted tags, eight unreachable". Checked against the repo
rather than quoted: R-260's wording was "at least eight DECISION-BEARING facts", not eight tags in
total. The real count on the three declared wires is 40, and R-260's own census already listed more
than eight. Recorded because this prompt's own rule 6 says not to quote a document as source.

TWO THINGS THE CONTROL CAUGHT, both before the gate was trusted:

1. A SUBSTRING FALSE NEGATIVE. `grep -F healed_at` also matches `privsep_healed_at`, so a genuinely
   dropped field read as received — and R-260 named healed_at, so its absence from the output was
   the tell. Now a whole-token regex; healed_at is convicted.
2. dr_recipe IS NOT WHOLLY OPAQUE. The hub stores each half as json.RawMessage and re-emits nested
   shapes verbatim, so the LEAVES are genuinely not on this wire. But the TOP-LEVEL SECTION KEYS are
   decoded by hostHalfShape/appHalfShape, and those are ALLOW-LISTS: a section an emitter adds is
   silently dropped until named in both. That already cost `offsite_restic` (R-122). So the gate is
   opaque BELOW depth 1, not opaque — the sections are checked and pass.

Self-test: `--selftest` plants an unreachable tag on a real root in a throwaway copy and asserts
conviction. Verified: exit 1, planted tag named.

Blind spots are in the module docstring AND in the gate's own output, because Campaign 12's C1 guard
turned out blind to one of the three shapes it was written for: generic tag names are not checked;
reachability of a NAME is not use of a VALUE; only declared ROOTS are covered, and the hub's
desired-state (served as raw stored JSON, no typed emitter) and the agent local API are NOT.

Allowlist entries carry a stated reason. A quiet exclusion is a dropped field with paperwork.

Not pushed alone: the fixes follow in the next commit so main is never red on this check.
This commit is contained in:
2026-08-08 08:28:53 +02:00
parent b7fb2117ae
commit 560f0d4451
3 changed files with 482 additions and 0 deletions
@@ -0,0 +1,77 @@
# The wire-contract gate, run against the tree at b7fb211 on 2026-08-08
Deliverable 1 of the G-1/R-260 session: the gate FAILING, before any field was fixed.
Captured verbatim. If this had been green, the gate would not work and that would be the finding.
```
wire-contract gate — 210 tag(s) checked across 3 declared wire(s); 51 skipped (generic / opaque / allowlisted)
agent -> hub (POST /host-report)
the RECEIVER (hub) contains no occurrence of:
applied_at (emitted at pbs_dr.applied_at)
checked_at (emitted at guest_net.checked_at)
cpu_temp_c (emitted at host.cpu_temp_c)
damped (emitted at guest_net.guests.damped)
dhclient_alive (emitted at guest_net.guests.dhclient_alive)
disk_bytes (emitted at guests.spec.disk_bytes)
guest_net (emitted at guest_net)
has_route (emitted at guest_net.guests.has_route)
heal_succeeded (emitted at guest_net.guests.heal_succeeded)
healed_at (emitted at oob.healed_at)
healed_recently (emitted at mgmt_plane.healed_recently)
heals_last_hour (emitted at guest_net.guests.heals_last_hour)
last_handshake_age_s (emitted at wireguard.last_handshake_age_s)
last_heal_at (emitted at guest_net.guests.last_heal_at)
loadavg (emitted at host.loadavg)
memory_bytes (emitted at guests.spec.memory_bytes)
memory_total_bytes (emitted at host.memory_total_bytes)
memory_used_bytes (emitted at host.memory_used_bytes)
model_name (emitted at storage_targets.smart.model_name)
mount_inventory (emitted at restore_tests.mount_inventory)
mount_parity (emitted at restore_tests.mount_parity)
operator_key_configured (emitted at oob.operator_key_configured)
selfupdate_pending (emitted at selfupdate_pending)
selfupdate_pending_version (emitted at selfupdate_pending_version)
uptime_seconds (emitted at host.uptime_seconds)
wg_handshake_age_s (emitted at oob.wg_handshake_age_s)
controller -> hub (POST /report)
the RECEIVER (hub) contains no occurrence of:
config_hash (emitted at config_hash)
last_db_dump (emitted at backup.last_db_dump)
last_integrity_check (emitted at backup.last_integrity_check)
load_avg_1 (emitted at system.load_avg_1)
load_avg_15 (emitted at system.load_avg_15)
load_avg_5 (emitted at system.load_avg_5)
memory_total_mb (emitted at system.memory_total_mb)
memory_used_mb (emitted at system.memory_used_mb)
migrated_to (emitted at storage.migrated_to)
reporting_disabled (emitted at reporting_disabled)
stacks (emitted at stacks)
temperature_celsius (emitted at system.temperature_celsius)
uptime_seconds (emitted at system.uptime_seconds)
hub -> controller (report ACK, `escrow` object)
the RECEIVER (controller) contains no occurrence of:
escrow_stale (emitted at escrow_stale)
WIRE-CONTRACT GATE FAILED: 40 emitted field(s) cannot be received.
A tag whose literal string occurs nowhere in the receiving repo cannot be decoded
by any struct, named or anonymous — encoding/json discards it on arrival.
Fix: model the field on the receiving side and say what consults it — or, if it is
deliberately not consumed, add it to ALLOWLIST WITH A REASON. Never a quiet skip.
Blind spots (a green is not full coverage — see the module docstring):
generic tag names are not checked; reachability of a NAME is not use of a VALUE;
only the declared ROOTS are covered (hub desired-state and the agent local API
are NOT).
exit=1
```
## Self-test (a planted unreachable tag on a real root)
```
planted an unreachable tag on agent HostReport -> gate exit 1, planted tag convicted: True
unplanted tree -> gate exit 1
SELFTEST OK — the gate convicts a planted unreachable tag and the plant is the only difference.
exit=0
```