Skip to content

Guard reach: a gate expression is counted as consumption but never resolved #341

Description

@m2ux

Problem

#327 taught check-binding-fidelity to treat two expression forms as consumption sites: a validate action's target and a step's when. Before that, an output whose only consumer was one of those gates read as dead — the opposite of dead, since a gate is the one place a value is enforced. That fix cleared three false positives.

It only went one way. Those same names are counted as consumption, but they are never checked for resolution:

// scripts/check-binding-fidelity.ts
for (const name of expressionNames(o.target)) expressionConsumes.push({ rel, name });
for (const name of expressionNames(o.when))   expressionConsumes.push({ rel, name });
...
for (const v of expressionConsumes) add(v.name, v.rel);   // feeds dead-output only

expressionConsumes reaches collectConsumedSites and nothing else. The reads array — the one the read-resolution check walks — is populated only from {token} interpolations and structured variable: keys.

So a gate expression naming a value nothing produces passes silently. when: has_typo == true or target: verdict_taht_never_existed != false is accepted, the condition evaluates against an absent bag name, and the gate never fires. That is the #324 A2 defect class — "a checkpoint gate can never fire" — in the one construct the guard now reads but does not validate.

It is also a class the corpus can grow into at any time, because the two fixes #327 made in this area both added validate targets: three verdicts gained action: validate gates. Every one of those is a hand-typed identifier the guard will not check.

Evidence

Reproduce by adding a nonsense name to any gate expression in the corpus and running npm run check:binding — the guard reports nothing. Compare with the same name in a {token}, which is reported as read-resolution.

The asymmetry is visible in the source: expressionNames has three call sites, none of them reads.push.

Requirements

R1 — Resolve gate-expression names against the same scope as a {token} · S

Feed expressionConsumes into the read-resolution check as well, so a when or validate target naming an unproducible value is a finding.

Two things to get right, and they are the reason #327 did not just do this:

  • A gate expression is not the same namespace as an interpolation. missing_prerequisites.length == 0 reads a field of a collection; signing.configured == true reads a nested object. The expression scanner already strips operators and literals (true, false, null, length), and the head-name split handles dotted paths — but the exemption list was written for consumption-marking, where a false name costs nothing. Used for resolution it becomes load-bearing, so it needs a pass over the shapes actually in the corpus — 33 step when expressions and 47 validate actions across the activity files.
  • Expect a first-run finding set. Some of those 80 sites may already name values nothing produces — which is the point, but it means R1 lands with a triage step rather than a green run. Classify anything real as live-bug in scripts/binding-fidelity-triage.json; the guard reports live bugs, so they cannot be parked.

R2 — Drop the duplicated site checks from deploy-docs.yml · S

deploy-docs.yml runs npm run check:site and npm run check:svg on pull requests. Both are now registry entries that npm run check:all runs in verify.yml, so every PR touching src/** or site/** runs them twice.

Harmless but wasteful, and it splits the answer to "did the site checks pass?" across two workflows. verify.yml runs on every PR with no path filter, so deploy-docs.yml can keep its build-and-deploy job and drop the two guard steps. Leave its git diff --exit-code -- site freshness check where it is — that one is about generated-page drift, not a registry guard.

Not in scope

Broadening the expression scanner into a general condition evaluator. The structured condition: form is already covered through the variable: key scan; this is specifically about the two string-expression forms.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions