gates: two checks report success without having read what they check #116

Closed
opened 2026-08-13 15:43:40 +00:00 by john · 1 comment
Owner

Two gates return a clean answer about input they never successfully read. Both are in commands whose entire job is to not let something through.

1. milestone-prune ships Deferred requirements as Complete

lib/milestone-prune.mjs:107:

const shipped = rows.filter((r) => r.phase !== null && completed.includes(r.phase));

No r.status !== 'Deferred' term. Every sibling consumer has one: planning.mjs:317, :513, :1023. This one writer does not.

Compounding it, the bullet removal at :118-124 scans the WHOLE file unbounded, while the Traceability removal nine lines below IS bounded to its section. So the ## Deferred bullet goes too, and because summaries.set is last-write-wins, the deferral note becomes the shipped row's summary.

Reproduced with PERF-02 | Phase 3 | Deferred and the phase box checked:

| PERF-02 (prefetch - pushed to the next cycle, do not ship) | 3 | Complete | v3.2.0 |

Traceability row deleted, ## Deferred bullet deleted, phases/3/ deleted, ok:true, no warning. A requirement deliberately held back is now permanently recorded as delivered, and no future audit can see it. This is silent scope loss inside the command whose stated job is auditing that nothing was dropped.

Fix: add the status term at :107; bound the bullet filter at :118-124 to the ## Active span the way :127-134 bounds the row filter.

2. The unattended-close halt gate fails open

land-cleanup.mjs:59-69 returns [] on unreadable stdin, on malformed JSON, and on a valid object with no findings array. lib/close-decision.mjs decideGateHalt then returns proceed:

{"findings":[{"severity":"blocker",...}]}   -> action:"halt"
{"findings":[{"severity":"blocker",...}]},  -> action:"proceed"   # one stray char
{"ok":false,"reason":"over-cap",...}        -> action:"proceed"   # the reviewer's own failure envelope

The last two carry reason: "auto_close on, no surviving blocker/high finding: proceed to merge" - an affirmative claim about input the function never read.

Under git.auto_close, references/triage-gate.md:68 suppresses the triage prompt entirely, so this is the ONLY remaining stop before merge, and the payload is model-composed (skills/cad-land/SKILL.md:73).

The irony is in the same file: gate()'s comment at :118-122 argues at length that a torn CONFIG layer must be distinguishable from auto_close: false. The findings side, which is strictly more consequential, got no such distinction.

Fix: return a discriminated result ({ok:false, reason:'unreadable-findings'|'bad-findings'}) and halt on it when autoClose === true. An unprovable finding set is not an empty one.

Two gates return a clean answer about input they never successfully read. Both are in commands whose entire job is to not let something through. ## 1. milestone-prune ships Deferred requirements as Complete `lib/milestone-prune.mjs:107`: ```js const shipped = rows.filter((r) => r.phase !== null && completed.includes(r.phase)); ``` No `r.status !== 'Deferred'` term. Every sibling consumer has one: `planning.mjs:317`, `:513`, `:1023`. This one writer does not. Compounding it, the bullet removal at `:118-124` scans the WHOLE file unbounded, while the Traceability removal nine lines below IS bounded to its section. So the `## Deferred` bullet goes too, and because `summaries.set` is last-write-wins, the deferral note becomes the shipped row's summary. Reproduced with `PERF-02 | Phase 3 | Deferred` and the phase box checked: ``` | PERF-02 (prefetch - pushed to the next cycle, do not ship) | 3 | Complete | v3.2.0 | ``` Traceability row deleted, `## Deferred` bullet deleted, `phases/3/` deleted, `ok:true`, no warning. A requirement deliberately held back is now permanently recorded as delivered, and no future audit can see it. This is silent scope loss inside the command whose stated job is auditing that nothing was dropped. Fix: add the status term at `:107`; bound the bullet filter at `:118-124` to the `## Active` span the way `:127-134` bounds the row filter. ## 2. The unattended-close halt gate fails open `land-cleanup.mjs:59-69` returns `[]` on unreadable stdin, on malformed JSON, and on a valid object with no `findings` array. `lib/close-decision.mjs decideGateHalt` then returns `proceed`: ``` {"findings":[{"severity":"blocker",...}]} -> action:"halt" {"findings":[{"severity":"blocker",...}]}, -> action:"proceed" # one stray char {"ok":false,"reason":"over-cap",...} -> action:"proceed" # the reviewer's own failure envelope ``` The last two carry `reason: "auto_close on, no surviving blocker/high finding: proceed to merge"` - an affirmative claim about input the function never read. Under `git.auto_close`, `references/triage-gate.md:68` suppresses the triage prompt entirely, so this is the ONLY remaining stop before merge, and the payload is model-composed (`skills/cad-land/SKILL.md:73`). The irony is in the same file: `gate()`'s comment at `:118-122` argues at length that a torn CONFIG layer must be distinguishable from `auto_close: false`. The findings side, which is strictly more consequential, got no such distinction. Fix: return a discriminated result (`{ok:false, reason:'unreadable-findings'|'bad-findings'}`) and halt on it when `autoClose === true`. An unprovable finding set is not an empty one.
john added this to the v3.2.0 milestone 2026-08-13 15:43:40 +00:00
john closed this issue 2026-08-14 17:01:42 +00:00
Author
Owner

Fixed in v3.2.0 (PR #124). GAT-01, phase 2. The unattended-close gate distinguishes "no findings" from "could not read the findings": unreadable stdin, malformed JSON and a valid non-findings envelope each halt under git.auto_close with a reason naming the failure, where all three previously returned proceed. milestone-prune no longer ships a Deferred requirement as Complete.

Verified through the phase UAT walk. The milestone audit traced 12/12 requirements with 0 broken and 36/36 acceptance criteria covered.

Fixed in v3.2.0 (PR #124). GAT-01, phase 2. The unattended-close gate distinguishes "no findings" from "could not read the findings": unreadable stdin, malformed JSON and a valid non-findings envelope each halt under git.auto_close with a reason naming the failure, where all three previously returned proceed. milestone-prune no longer ships a Deferred requirement as Complete. Verified through the phase UAT walk. The milestone audit traced 12/12 requirements with 0 broken and 36/36 acceptance criteria covered.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: crenshawdev/cadence-archived#116
No description provided.