---
name: list-vs-predicate-assertions
description: "FEEDBACK 2026-07-20 (doyle) — a rule stated as a LIST is keyed on the cases someone thought of; state it as a PREDICATE over the shape. My [profiles.ccs.digest] assert passed by naming the only profile that existed."
metadata: 
  node_type: memory
  type: feedback
  originSessionId: 66f829b9-b55e-4e0b-89f4-1053391a6d32
  modified: 2026-08-04T23:07:18.543Z
---

State guards as a **predicate over the shape**, never as a list of the instances that exist today.

The catch (doyle, 2026-07-20): my v0.25.5 test asserted `no [profiles.ccs.digest] leaf` — keyed on
the only profile that existed. Profiles resolve **per endpoint and the overlay wins**, so the hazard
was never `ccs`: it was ANY profile declaring a `[digest]` overlay. Adding `:glm`/`:kimi` would keep
the assertion green while endpoints under the new profile silently resolved to a shallower digest
window — the exact partial no-op the check existed to prevent, reading as success because the tested
endpoints stayed fine. See [[digest-json-consumption-contract]].

**Then I overcorrected, and doyle caught that too** (`10b9fa6`): my first predicate demanded that
*every* `[profiles.*.digest]` section declare the depth — but `[digest]` also carries
`extractor`/`strategy`/`source`/`arg_truncation`/`sprint_collapse`, and profiles are **sparse
leaf-replace**, so a profile overlaying only the extractor inherits base's depth and is SAFE. The
rule red a correct config. Right key = **the leaf, not the section**: no `window_turns` leaf ⇒ skip;
leaf present ⇒ must be deep enough, because only a declared leaf overrides base.

**Why:** doyle says the same defect ran through their whole last milestone — comments, doc-strings,
prompts, assertions, a nextest filter, a release brief. It is a class, not a slip, and the list form
is what everyone writes until they get caught. It fails *silently and green*, which is the worst
failure mode a guard can have.

**How to apply:**
- Writing an assert/filter/doc rule, ask: *am I naming instances or describing the shape?* Iterate the
  container (all profiles, all sections, all variants); match by shape, not by name.
- **A vacuous predicate proves nothing.** If the loop has zero iterations against today's input, add a
  synthetic fixture carrying the future shapes — otherwise the test is decoration.
- **Mutation-check the real target**, not just the helpers: inject the violation into the actual file,
  watch it fail, revert, watch it pass. A fabricated fixture is exactly why v0.25.2 shipped broken
  ([[v0252-interrupt-heal-agent-activity]]).
- **Widening is not free — a predicate that reds a CORRECT config is the arm that gets weakened under
  time pressure**, and it is always the arm covering the hazard. Mutation-check BOTH directions: the
  shape that must red, *and* the safe shapes that must not.
- **Don't hedge a rule you can't state precisely.** I wrote "safe under either merge semantics"
  instead of pinning down leaf-replace — that hedge is exactly what let the overstated rule through.
  Get the semantics, then key the rule on the thing that actually wins.
- Same instinct as [[signature-match-is-not-root-cause]]: a check that reproduces/passes on the case
  in front of you is not a check that covers the class.

**SIBLING CASE — the probe, not just the predicate (2026-08-04, deployah caught it at the spt-core
0.54.0 tag; repo copy: `docs/KNOWN-HAZARDS.md` §2.14, `REQ-HAZARD-VERSION-GATE-FAIL-CLOSED`).** A CI
gate chose its contract with `spt endpoint run --help | grep -q -- '--subnet'` and SKIPPED with
"needs v0.14.0" on a miss. 0.54.0 *retired* `endpoint run`, so the command refused, the grep missed
for a reason having nothing to do with v0.14.0, and the int **exited 0 announcing it skipped for
being too OLD** — passing by skipping on the exact version it existed to test, while stating the
opposite of the truth. Its three sibling ints called the same verb and announced themselves by
failing; only this one went quiet.

- **An ABSENT feature and a MOVED one are byte-indistinguishable in a grep count.** Before a
  zero-match may mean "absent", the probe must prove it read a real body — a positive control that
  MUST match (here: `--help` exiting 0 *and* carrying `usage`). A zero from a command that REFUSED
  describes the probe, not the contract. Exactly [[v0261-hints-never-called]] and F-044 again, one
  layer down: this time the untrustworthy instrument was the version sniffer itself.
- **Fail closed.** When the shape can't be established, FAIL and name the port needed; never skip. A
  skip is a verdict — "not applicable" — and a gate may not assert more than it measured.
- **Two instruments bit me the same day, both reading like a pass.** (1) A backgrounded
  `sh ci/run-gates.sh >log; echo "GATES=$?"; traceable-reqs check; echo …` reports the task exit code
  of the LAST command — always 0. The harness notification said "exit code 0" while gates were
  failing. End the command with the thing under test, or capture its status explicitly and read THAT.
  (2) The mirror leak guard reads **COMMITTED HEAD**, so running gates before `git add` inspects a
  tree that does not contain the new file — my commit landed red on a guard I had "run". *Gates
  before the commit ≠ gates on what the commit contains.* Re-run after staging, or commit then verify.
- **Test the gate by DRIVING it, never by grepping its text**: a shape-check reproduces the very
  defect. Stub the CLI, and give the *stub* a positive control so a stub that cannot execute FAILS
  the test rather than passing it vacuously. Mutation-check both arms — the old gate must red, and
  the honest skip must still work, because failing closed must not degrade into failing always.
