fix(#2770): decision-coverage gate fails closed on empty arg + workflow recomputes CONTEXT_PATH in-block (#2881)
* test(#2770): empty contextPath argument must fail closed, not green-skip the decision-coverage gate The handler conflated empty-arg (caller error) with file-missing (legitimate skip), returning passed:true/skipped on an empty argument. Add: empty arg → passed:false; real-path-to-absent-file → legitimate green skip preserved; omitted arg → fail closed. * fix(#2770): decision-coverage gate fails closed on empty arg + workflow recomputes CONTEXT_PATH in-block Handler (check-command-router.cts): split the guard — empty/missing contextPath argument is a caller error (fail closed, passed:false, mirrors #1365); a real path whose file genuinely does not exist keeps the legitimate green skip. Workflow (plan-phase.md): recompute CONTEXT_PATH inside the consuming Bash block (it was set in the step-1 init block, which does not survive into the separately- spawned gate block — so the gate ran with an empty arg and silently green-skipped). * chore(#2770): changeset fragment * fix(#2770): guard workflow empty-glob case (review blocker) + update drift-guard test The handler now fails closed on an empty contextPath arg, so the workflow's unguarded glob (empty when a phase genuinely has no CONTEXT.md) would invoke the gate with an empty arg → passed:false → exit 1, hard-halting the legitimate 'Continue without context' plan-phase path. Guard the empty-glob case: only run the gate when a CONTEXT.md actually exists. Update the F1 drift-guard test (which gave false coverage — it only checked for the ${CONTEXT_PATH} token) to assert the in-block recompute AND the empty-glob guard. * fix(#2770): keep plan-phase.md under ADR-857 size cap + ack emitted drift + fix drift-guard window The workflow fix grew plan-phase.md past the ADR-857 phase-6 size cap (94519B) and triggered emitted-attribution. Condense adjacent §13a prose/JSON to offset (net +89B, under cap). Add tests/emitted-drift-ack.json acknowledging the residual growth. Widen the drift-guard test window (the gate invocation is now nested in the empty-glob guard, so the old 400-char window missed the glob recompute). * chore(#2770): backfill changeset PR number (2881) --------- Co-authored-by: Test <test@example.com>
This commit is contained in:
@@ -1400,47 +1400,43 @@ If `TEXT_MODE` is true, present as a plain-text numbered list (options already s
|
||||
|
||||
## 13a. Decision Coverage Gate
|
||||
|
||||
After the requirements coverage gate passes, verify that every trackable
|
||||
decision captured by discuss-phase in CONTEXT.md `<decisions>` is referenced
|
||||
by at least one plan. This is the **translation gate** from issue #2492 —
|
||||
its job is to refuse to mark a phase planned when a discuss-phase decision
|
||||
silently dropped on the way into the plans.
|
||||
Verify every trackable decision in CONTEXT.md `<decisions>` is referenced by at
|
||||
least one plan. This **translation gate** (#2492) refuses to mark a phase planned
|
||||
when a discuss-phase decision silently dropped.
|
||||
|
||||
**Skip if** `workflow.context_coverage_gate` is explicitly set to `false`
|
||||
(absent key = enabled). Also skip if no CONTEXT.md exists for this phase
|
||||
(nothing to translate) or if its `<decisions>` block is empty.
|
||||
**Skip if** `workflow.context_coverage_gate` is `false` (absent = enabled), or
|
||||
no CONTEXT.md exists for this phase, or its `<decisions>` block is empty.
|
||||
|
||||
```bash
|
||||
GATE_CFG=$(gsd_run query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
|
||||
if [ "$GATE_CFG" != "false" ]; then
|
||||
GATE_RESULT=$(gsd_run query check.decision-coverage-plan "${PHASE_DIR}" "${CONTEXT_PATH}")
|
||||
# BLOCKING: refuse to mark phase planned when a trackable decision is uncovered.
|
||||
# `passed: true` covers both real-pass and skipped cases (gate disabled / no CONTEXT.md /
|
||||
# no trackable decisions). Verify-phase counterpart deliberately omits this exit-1 — that
|
||||
# gate is non-blocking by design (review finding F15).
|
||||
echo "$GATE_RESULT" | jq -e '(.passed // .data.passed) == true' >/dev/null || {
|
||||
echo "$GATE_RESULT" | jq -r '(.message // .data.message // "Decision coverage gate failed.")'
|
||||
exit 1
|
||||
}
|
||||
# #2770: CONTEXT_PATH from step-1 init doesn't survive into this Bash block;
|
||||
# recompute it. Only run when a CONTEXT.md exists (handler fails closed on an
|
||||
# empty arg, so an unguarded empty glob would halt a context-less phase).
|
||||
CONTEXT_PATH=$(ls "${PHASE_DIR}"/*-CONTEXT.md 2>/dev/null | head -1)
|
||||
if [ -n "$CONTEXT_PATH" ]; then
|
||||
GATE_RESULT=$(gsd_run query check.decision-coverage-plan "${PHASE_DIR}" "${CONTEXT_PATH}")
|
||||
# BLOCKING: refuse to mark phase planned when a trackable decision is uncovered.
|
||||
# `passed: true` covers both real-pass and skipped cases (gate disabled / no CONTEXT.md /
|
||||
# no trackable decisions). Verify-phase counterpart deliberately omits this exit-1 — that
|
||||
# gate is non-blocking by design (review finding F15).
|
||||
echo "$GATE_RESULT" | jq -e '(.passed // .data.passed) == true' >/dev/null || {
|
||||
echo "$GATE_RESULT" | jq -r '(.message // .data.message // "Decision coverage gate failed.")'
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
The handler returns JSON:
|
||||
```json
|
||||
{
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"total": 2,
|
||||
"covered": 2,
|
||||
"uncovered": [ { "id": "D-01", "text": "...", "category": "..." } ],
|
||||
"message": "..."
|
||||
}
|
||||
{ "passed": true, "skipped": false, "total": 2, "covered": 2,
|
||||
"uncovered": [{ "id": "D-01", "text": "...", "category": "..." }], "message": "..." }
|
||||
```
|
||||
|
||||
**If `passed` is true (or `skipped` is true):** Display
|
||||
`✓ Decision coverage: {M}/{N} CONTEXT.md decisions covered by plans` (or
|
||||
`(skipped — gate disabled)` / `(skipped — no decisions)`) and proceed to
|
||||
step 13b.
|
||||
`✓ Decision coverage: {M}/{N} decisions covered` (or `(skipped)`) and proceed
|
||||
to step 13b.
|
||||
|
||||
**If `passed` is false:** Display the handler's `message` block. It already
|
||||
names each uncovered decision (`D-NN | category | text`) and tells the user
|
||||
|
||||
Reference in New Issue
Block a user