feat(guards): resolve gate expressions, scope dead-output to its workflow (#341, #342) - #364
Merged
Conversation
…d the class check-binding-fidelity and guard-protocol each joined a composite key on a literal NUL byte. The delimiter choice is sound — no field content can collide with it — but written as a raw byte it makes the file binary to every text tool, and each tool fails silently and differently. grep prints nothing and exits 1, so searching a file for a symbol it does contain reports absence. git sniffs the first 8000 bytes: guard-protocol's byte sits at 2439, so every diff of it rendered as "Binary files differ" and the file could not be reviewed line by line, while check-binding-fidelity's at 28832 fell outside the window and diffed normally. Same defect, opposite visibility, neither announced. Written as the \u0000 escape the runtime value is identical — the binding-fidelity report is byte-for-byte unchanged across the edit — and the sources are text again. check-source-encoding registers the class so it cannot return. It found a literal NUL in its own header on first run, which is the shape of mistake it exists to catch. CLAUDE.md and AGENTS.md carry the refreshed index stats: the graph was three days and eighteen merges stale, which no tool reported.
…flow Two blind spots in check-binding-fidelity, each a check that went one way only. A gate expression counted as consumption and was never resolved (#341). #327 taught the guard that a step `when` and a `validate` target consume the names they read, which stopped a gated value reading as dead. Nothing checked the other direction, so a gate naming a value nothing produces was accepted, evaluated against an absent bag name, and never fired — the #324 A2 class in the one construct the guard read but did not validate. Resolution makes the extractor load-bearing, so it now parses clauses rather than harvesting identifiers. It takes the left operand of each comparison and a bare clause read for truthiness, never the right side: an unquoted right operand is shaped exactly like an identifier, and `analysis_type == completion` would otherwise read `completion` as a bag name nothing can ever produce. Environment namespaces are named, because dotted-ness cannot separate them — `gh.auth.status` asks the GitHub CLI while `planning_folder_path.writable` is a real bag name carrying a probed field. Sites report at their enclosing step, since the walk reads parsed YAML and a validate target sits inside actions[] where the step id is out of scope. dead-output resolved a consumer by bare name across the whole corpus (#342), so an output in one workflow read as consumed when an unrelated workflow happened to read a name of the same spelling. Consumer resolution is now scoped to the declaring workflow, with meta's library ops reachable from anywhere and cross-workflow reach taken from the bind sites themselves, so a borrowed op's outputs still close. And a stale entry names what now satisfies it: "no longer occurs" had two causes a reader could not tell apart, and deleting the entry on the wrong one is how b41aaac forced real debt out of the ledger. The first run surfaced 31 findings, triaged: 24 dead-output seams the masking had hidden — including both entries b41aaac pruned, so #342 R2 falls out of R1 as its issue predicted — and 7 gate expressions reading values nothing declares. All are the two classes #336 already owns, undeclared-seed and terminal-product-unconsumed, and none is newly broken: each runs today because the executing agent improvises the value. Two are worth taking first there, being a precondition and a safety gate rather than routing flags: work-packages' agents_md_read, and work-package's safety_floor, whose correctly-shaped twin ponytail already models as safety_floor_cleared with a checkpoint producing it. deploy-docs drops its check:site and check:svg steps. Both are registry entries check:all runs in verify.yml, which has no path filter, so every PR touching src or site ran them twice. The generated-page drift check stays — that one is not a registry guard. corpusSha now names the corpus this triage was taken against; it had drifted behind the gitlink, and check-delta materialises the base corpus from it.
…aced baselines Two claims on the quality-system page were false. Dropping the duplicated site checks from deploy-docs made one of them false in this same branch: that workflow no longer runs the site guards, it rebuilds the generated regions and fails on drift. The guards run in verify, which has no path filter, so the page now says that — the property that made the deploy-docs copies redundant is the one worth stating. The other predates this branch. #327 retired the committed baselines and the --update-baseline flag, and the page still described both, so a reader would reach for a flag no guard has. It now describes the triage model that replaced them: a verdict and a named rationale per finding, no re-snapshot because classification is a judgement, and a stale entry reported so a seam cannot leave the ledger by going quiet.
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The server PR #343 routes: two guard blind spots, each a check that went one way only. Plus an encoding fix that made one of these files unreviewable and the other un-greppable.
Merge this before #360. #343 sequences it first because #342 is currently dropping real entries out of the debt ledger and reporting them closed, so any corpus measurement taken before it lands understates by an unknown amount. #360's own
binding-fidelitynumbers carry that caveat until this merges.#341 — a gate expression was counted as consumption and never resolved
#327 taught the guard that a step
whenand avalidatetarget consume the names they read, which stopped a gated value reading as dead. It only went one way: those names never reached the read-resolution check, so a gate naming a value nothing produces was accepted, evaluated against an absent bag name, and never fired. That is the #324 A2 class — "a checkpoint gate can never fire" — in the one construct the guard read but did not validate.Feeding those names to resolution makes the extractor load-bearing, which is why #327 stopped short. It now parses clauses instead of harvesting identifiers:
analysis_type == completionanalysis_typecompletionas a bag name nothing can ever produce.issue_platform == 'jira' && issue_skipped != truemissing_prerequisites.length == 0missing_prerequisitesagents_md_readagents_md_readgh.auth.status == 0planning_folder_path.writable == trueplanning_folder_pathThe exemption list #341 asked to be re-derived was re-derived from the corpus: all 17
validateexpressions and 37 distinctwhenexpressions were enumerated, and the two traps above are what that survey found.EXPRESSION_LITERALSis gone — a literal list was adequate while a false name only marked consumption and cost nothing.Findings report at their enclosing step rather than by line: the walk reads parsed YAML, which carries no line numbers, and a
validatetarget sits insideactions[]where the step id is already out of scope.#342 — dead-output resolved consumers across workflow boundaries
An output in workflow A read as consumed when an unrelated workflow B happened to read a name of the same spelling, and B has no address for A's op, so it cannot bind it. Consumer resolution is now scoped to the declaring workflow, with two deliberate allowances:
meta— the universal library every workflow binds ad hoc. This was theshared-op-return-contractrationale carried per entry; it is now a scoping rule.remediate-vulnborrowing awork-packageop can consume that op's outputs, so those findings stay closed.R2 falls out of R1, exactly as the issue predicted — both entries
b41aaaccpruned are back:workflow-design/techniques/apply-audit-fixes.mdfixes_appliedworkflow-design/techniques/yaml-authoring.mdyaml_fileAnd the masking was broader than the two:
scope_summarydeclared in two differentsummarize-scope.md(prism-audit and prism-evaluate),sub_agent_outputin twoexecute-sub-agent.md(cicd and substrate), plus eight more across theworkflow-design↔workflow-authoringduplicate-basename surface #321's additive migration created. Both directions, as #342 warned.R3 — a stale entry now names what satisfies it. "No longer occurs" had two causes a reader could not tell apart: the seam was closed, or the guard stopped seeing it. Deleting the entry on the wrong one is precisely how
b41aaaccforced real debt out of the ledger, and it presented as progress.The first-run triage: 31 findings
24 dead-output seams the masking had hidden, and 7 gate expressions reading values nothing declares. All classified
fix-laterunder the two rationales #336 already owns —terminal-product-unconsumedandundeclared-seed.Why
fix-laterrather thanlive-bug, which #341 asks for anything real: none is newly broken. Every one runs today because the executing agent improvises the value at the point of use, which is theundeclared-seedclass by definition — #336's largest, at 61 findings, with a plan already sequenced behind #342. Marking themlive-bugwould hold the guard red on pre-existing debt that #343 deliberately schedules last.Two are worth taking first in #336 R1, being a precondition and a safety gate rather than routing flags:
work-packages/activities/01-scope-assessment.yaml—validate target: agents_md_read, a bare truthiness gate meant to block until AGENTS.md is read, on a value nothing sets.work-package/activities/09-lean-coding-audit.yaml—validate target: safety_floor, whose correctly-shaped twinponytailalready models assafety_floor_cleared, with a checkpoint producing it.The other five:
worker_yielded_checkpointgating four steps of meta's dispatch loop — already recorded as pre-existing drift in the #348 planning register alongsidematched_session,worker_resultanduser_selection— andjira_cloud_id, which comes from an Atlassian tool call rather than the bag.#341 R2 — the duplicated site checks
deploy-docs.ymldropscheck:siteandcheck:svg. Both are registry entriescheck:allruns inverify.yml, which triggers onpull_request:with no path filter, so every PR touchingsrc/**orsite/**ran them twice and the answer to "did the site checks pass?" was split across two workflows. Thegit diff --exit-code -- sitefreshness check stays — that one is about generated-page drift, not a registry guard.Encoding: one of these files could not be diffed, the other could not be grepped
check-binding-fidelityandguard-protocoleach joined a composite key on a literal NUL byte. The delimiter is sound — no field content can collide with it — but written as a raw byte it makes the file binary to every text tool, and each fails silently and differently:grepprints nothing and exits 1, so searching a file for a symbol it does contain reports absence. This is how a search forexpressionNames— the function Guard reach: a gate expression is counted as consumption but never resolved #341 quotes from that very file — came back empty.gitsniffs the first 8000 bytes.guard-protocol's byte sits at 2439, so every diff of it rendered as "Binary files differ" and it could not be reviewed line by line.check-binding-fidelity's at 28832 fell outside the window and diffed normally. Same defect, opposite visibility, neither announced.Written as
\u0000the runtime value is identical — the binding-fidelity report is byte-for-byte unchanged across that edit — and both sources are text again.check-source-encodingregisters the class as a 19th guard so it cannot return. It earned its place twice over: it found a literal NUL in its own header on first run, and then caught one in this PR's newdeadOutputSatisfierkey — where the set side had the raw byte and the lookup side a space, so the satisfier lookup would never have matched. That is a real bug in #342 R3, found by the guard rather than by review.Two stale claims on the quality-system page
Dropping the deploy-docs steps made one of them false in this same branch, so the AP-129 sweep caught it: that workflow no longer runs the site guards, it rebuilds the generated regions and fails on drift. The page now says so, and names the property that made the copies redundant —
verifyhas no path filter, so every PR gets the whole suite.The other predates this branch and is worth fixing while here: #327 retired the committed baselines and the
--update-baselineflag, and the page still documented both, so a reader would reach for a flag no guard has. It now describes the triage model that replaced them — a verdict and a named rationale per finding, no re-snapshot because classification is a judgement, and a stale entry reported so a seam cannot leave the ledger by going quiet.Verification
binding-fidelityat 196 triaged, 0 live, 0 untriaged, 0 stale.session-crypto's EACCES case, is pre-existing and environmental — verified by running it on a cleanorigin/maincheckout, where it fails identically: the sandbox returnsENOENTfrommkdir /where the test assertsEACCES.npx tsc --noEmitclean.npm run check:delta --base origin/main: no new findings. Every guard reportsSAME;source-encodingreports[PROTOCOL]because the base tree fails it — the raw NUL bytes this branch fixes — while head passes. That is the attribution evidence for the whole change.session-cryptocase passes in CI, which confirms the local failure is the sandbox'smkdir /behaviour and nothing else.expressionReadsis exported and pinned against every shape the corpus carries — left operand only, conjunction split, dotted head, bare truthiness, probe exemption — because a name this extractor invents is a finding nobody can fix, and a name it drops is a gate that can never fire. Plus a regression test naming the two entriesb41aaaccpruned, so re-masking them fails a test rather than reading as progress.guard-registry's repo-scope assertion gainssource-encoding.corpusShanow names the corpus this triage was taken against (6bc46faf). It had drifted behind the gitlink, andcheck-deltamaterialises the base corpus from it.Not in scope
Broadening the expression scanner into a general condition evaluator, per #341 — the structured
condition:form is already covered through thevariable:key scan, and this is specifically the two string-expression forms. Renaming the duplicated techniques or acceleratingworkflow-design's retirement, per #342 — the duplication is #321's accepted cost and the guard should be correct in its presence. And #336's burn-down itself, which #343 sequences after this and after #360.🤖 Generated with Claude Code