From e8db9ef8c3f45f207127ca7d6a23a502b24d8712 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:18:58 +0000 Subject: [PATCH 1/2] fix(gates): a mid-stage decision is not the stage approval (#477) The approved-over-a-failing-gate flag (#207) read any CONFIRMED row of stage N as the approval, so a stage still in progress was flagged the moment any question in it was answered. New stage_exit_recorded counts an explicit exit row ("Stage N approved", "Build plan approved") or a CONFIRMED decision for a later stage. Stage 3/4 gate checks unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/gate-check.sh | 63 +++++++++++++++++++++++++++++++-- tests/wave2/test-bug03-gates.sh | 42 ++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..8907f816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(gates): **a decision taken during a stage no longer reads as that stage's approval** (#477). The #207 flag prefixed a FAIL with `APPROVED OVER A FAILING GATE` whenever any CONFIRMED row carried the stage's number, so a Stage-5 build with `| 5 | Sub-agents run on Sonnet | CONFIRMED |` was flagged, and `status.sh` told the lead to re-open an approval nobody gave while the real gap (PENDING obligations) sat underneath. The register has no dedicated exit row, so `gate-check.sh` now counts a stage as approved on either of two pieces of evidence in the Decisions table: an explicit exit row ("Stage N approved", "Stage N gate: signed off", "Gate approved", or a short "Build plan approved"), or a CONFIRMED decision for a later stage, i.e. the project moved past it (the #207 field case). The Stage 3/4 gate checks and every verdict and exit code are unchanged. `test-bug03-gates.sh` T17 (8 cases): 3 FAIL on master, 39/39 with the fix. — MendixMau ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 9ec15e29..0b3d64c0 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1059,6 +1059,63 @@ has_confirmed_decision() { ' "$f" } +# stage_exit_recorded — does the register say Stage N was APPROVED AS DONE, not merely that +# some decision was taken during it? Used only by the "approved over a failing gate" flag (#207); +# the Stage 3/4 checks keep has_confirmed_decision, where any CONFIRMED row is the evidence that +# the stage's interview was held. +# +# Why not has_confirmed_decision (#477). Any CONFIRMED row whose Stage cell is N counted as the +# approval, so a stage still being worked on raised the flag the moment the user answered any +# question in it: a Stage-5 build with "| 5 | Sub-agents run on Sonnet | CONFIRMED |" read +# "APPROVED OVER A FAILING GATE", and status.sh told the lead to re-open an approval nobody gave, +# burying the real gap (PENDING obligations) under a wrong diagnosis. +# +# The register has no dedicated exit row (init-project.sh writes `| Stage | Decision | Status | +# Notes |`; conversion-runbook.md §2 closes a gate on "its ✋ decisions CONFIRMED and gate-check +# run"), so a stage counts as approved on either of two pieces of evidence, both in CONFIRMED +# rows of the Decisions table (same table scoping and status anchoring as above): +# 1. an explicit exit row for Stage N: its decision reads "Stage N approved / exit / gate +# approved / signed off / closed / complete", "Gate approved|signed off|passed|closed", or +# is a short " approved|signed off" phrase ("Build plan approved"); +# 2. the project moved past it: a CONFIRMED row for a LATER stage (P < 0 < 1 … < 7), which is +# how the #207 field case read (a Stage-4 approval, then 47 scripts built on it). +# A mid-stage decision is neither, so it no longer raises the flag. +stage_exit_recorded() { + local stage="$1" + local f="$REGISTER" + [ -n "$f" ] && [ -f "$f" ] || return 1 + awk -F'|' -v want="$stage" ' + function rank(s) { s = toupper(s); if (s == "P") return 0; if (s ~ /^[0-7]$/) return s + 1; return -1 } + BEGIN { wr = rank(want); w = tolower(want) } + !/^\|/ { intab = 0; next } + { + h = tolower($2); gsub(/^[ \t]+|[ \t]+$/, "", h) + if (h == "stage") { intab = 1; hdr = 1; next } + if ($2 ~ /^[ \t:]*-+[ \t:-]*$/) next + s = $2 + gsub(/^[ \t]+|[ \t]+$/, "", s) + sub(/^[Ss]tage[ \t]*/, "", s) + confirmed = 0 + for (i = 3; i <= NF; i++) { + f = toupper($i); gsub(/^[ \t]+|[ \t]+$/, "", f) + if (f ~ /^CONFIRMED([ \t]|$)/) confirmed = 1 + } + if (!confirmed) next + r = rank(s) + if (wr >= 0 && r > wr) { if (intab) tlater = 1; alater = 1 } + if (tolower(s) != w) next + d = tolower($3); gsub(/^[ \t]+|[ \t.]+$/, "", d) + words = split(d, junk, /[ \t]+/) + if (d ~ ("(^|[^a-z0-9])stage[ \t]*" w "[ \t:-]*((gate|exit)[ \t:-]*)?(approv|sign|closed|complete|done|passed)") || + d ~ /(^|[^a-z])gate[ \t:-]*(approved|signed[ -]off|sign-off|passed|closed)/ || + (words <= 6 && d ~ /[ \t](approved|signed[ -]off|sign-off)$/)) { + if (intab) texit = 1; aexit = 1 + } + } + END { exit !(hdr ? (texit || tlater) : (aexit || alater)) } + ' "$f" +} + # Counts the Screen Inventory table in design/target-ui.md (design-artifacts.md Step 2): a # markdown table under a "## Screen Inventory" heading, one row per screen. Prints two numbers # separated by a space: total inventoried rows, and how many of those rows record that the @@ -2825,8 +2882,10 @@ for stage in "${STAGE_NAMES[@]}"; do # and 47 scripts were built on that plan. Delegating the approval is legitimate; an approval # nobody compared to its gate is not. Same verdict, same exit code — the note names the # mismatch, and the summary repeats it so status.sh can quote it. - if [ "$status" = "FAIL" ] && ! reg_unavailable && has_confirmed_decision "$stage"; then - note="APPROVED OVER A FAILING GATE — Stage $stage is CONFIRMED in $(basename "${REGISTER:-PROJECT.md}"), but: $note" + # Approval means an exit row or a later stage's decision (stage_exit_recorded, #477), not any + # decision taken while the stage is still open. + if [ "$status" = "FAIL" ] && ! reg_unavailable && stage_exit_recorded "$stage"; then + note="APPROVED OVER A FAILING GATE — Stage $stage is approved in $(basename "${REGISTER:-PROJECT.md}"), but: $note" APPROVED_OVER_FAIL="${APPROVED_OVER_FAIL:+$APPROVED_OVER_FAIL, }Stage $stage" fi case "$status" in diff --git a/tests/wave2/test-bug03-gates.sh b/tests/wave2/test-bug03-gates.sh index ed97e13d..4d083701 100755 --- a/tests/wave2/test-bug03-gates.sh +++ b/tests/wave2/test-bug03-gates.sh @@ -282,6 +282,48 @@ V="$(verdict "$P" 4)" case "$V" in *FAIL*'no module has one'*) ok "with no briefs yet, a plan still owes at least one module close row" ;; *) bad "a plan with no module dirs and no close row passed: $V" ;; esac +echo "== T17: a mid-stage decision is not the stage approval (#477) ==" +# #207 prefixes a FAIL with APPROVED OVER A FAILING GATE when the register says the stage was +# approved. It read ANY CONFIRMED row of that stage as the approval, so the field project's +# mid-Stage-5 "| 5 | Sub-agents run on Sonnet | CONFIRMED | user in chat |" (verbatim from the +# issue) raised it on a stage still being built, and status.sh told the lead to re-open an +# approval nobody gave. Stage 4 stands in for 5 here: its FAIL needs only a plan with no closing +# rows (T16), so no harness obligations have to be staged. The approval must still be flagged +# when it is real: an explicit exit row, or the project having moved on to a later stage. +# Positive control: on the pre-fix script t17a and t17f print the flag. +t17() { # t17 -> the Stage 4 line, and the summary line if any + local P; P="$(mkproj "$1")"; shift + printf '%s\n' "$@" >> "$P/PROJECT.md" + mkdir -p "$P/architecture/modules/Orders" + printf "# Plan\n\n$HDR| 1 | BUILD | 10-orders.mdl | | | none | built |\n" > "$P/architecture/build-plan.md" + "$GATE" --no-html "$P" 2>&1 | grep -E '^Stage 4 |^Approved over a failing gate:' +} +flagged() { case "$1" in *'APPROVED OVER A FAILING GATE'*'Approved over a failing gate: Stage 4'*) return 0 ;; esac; return 1; } +MID='| 4 | Sub-agents run on Sonnet | CONFIRMED | user in chat |' +V="$(t17 t17a "$MID")" +case "$V" in *FAIL*) ;; *) bad "t17a setup: Stage 4 did not FAIL: $V" ;; esac +case "$V" in *'APPROVED OVER'*|*'Approved over'*) bad "a mid-stage decision raised APPROVED OVER A FAILING GATE (#477): $V" ;; + *) ok "a mid-stage CONFIRMED decision does not read as the stage approval" ;; esac +V="$(t17 t17b "| 4 | Purchase requests above 5k need approval | CONFIRMED | |")" +case "$V" in *'APPROVED OVER'*) bad "a domain decision mentioning approval raised the flag: $V" ;; + *) ok "a domain decision that mentions approval is not a stage approval" ;; esac +V="$(t17 t17c "$MID" "| 4 | Stage 4 approved | CONFIRMED | user in chat |")" +flagged "$V" && ok "an explicit 'Stage 4 approved' row over a FAIL is still flagged, in the note and the summary" \ + || bad "an explicit Stage-4 approval over a FAIL was not flagged: $V" +V="$(t17 t17d "| 4 | Stage 4 gate: signed off | CONFIRMED 2026-10-20 | |")" +flagged "$V" && ok "'Stage 4 gate: signed off' with a dated CONFIRMED is an approval" || bad "dated gate sign-off not flagged: $V" +V="$(t17 t17e "| 4 | Build plan approved | CONFIRMED | |")" +flagged "$V" && ok "a short ' approved' row is an approval" || bad "'Build plan approved' not flagged: $V" +V="$(t17 t17f "$MID" "| 5 | Test depth: journeys per module | CONFIRMED | |")" +flagged "$V" && ok "a CONFIRMED decision for a later stage means the project moved past Stage 4: flagged" \ + || bad "moving on to Stage 5 over a failing Stage 4 was not flagged: $V" +V="$(t17 t17g "| 4 | Stage 4 approved | ASSUMED | |" "| 3 | Stage 4 approved | CONFIRMED | |")" +case "$V" in *'APPROVED OVER'*) bad "an ASSUMED exit row, or another stage's row, raised the flag: $V" ;; + *) ok "only a CONFIRMED exit row of Stage 4 itself counts" ;; esac +V="$(t17 t17h "$MID" "## Open questions" "" "| # | Question | Raised at | Status |" "|---|---|---|---|" "| 5 | Stage 4 approved? | Stage 4 | CONFIRMED |")" +case "$V" in *'APPROVED OVER'*) bad "an Open-questions row stood in for the approval: $V" ;; + *) ok "an Open-questions row is not a stage approval (same table scoping as T13)" ;; esac + printf '\n%s: %d ok, %d FAIL\n' "$(basename "$0")" "$PASS" "$FAIL" rm -rf "$WORK" [ "$FAIL" -eq 0 ] From aa6545acc833cac9bb12838320279627bd4f7f2c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:44:26 +0000 Subject: [PATCH 2/2] fix(gates): tighten stage-exit evidence after review (#477) Rule 2 now needs the stage's own CONFIRMED rows plus a later stage up to 6, and skips N/A / skip / no-cutover rows, so an early Stage-7 "no cutover" row or a forward-filed Stage-6 decision no longer flags a mid-build Stage 5. Rule 1 accepts em/en dashes and "Approved: Stage N", and its short form is limited to an artifact-noun allowlist, so domain decisions ("Requests over 5k must be approved") and "Stage 40 approved" no longer match. T17 grows to 16 cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 3 ++- bin/gate-check.sh | 46 +++++++++++++++++++++++---------- tests/wave2/test-bug03-gates.sh | 26 +++++++++++++++++++ 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8907f816..253782ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(gates): **a decision taken during a stage no longer reads as that stage's approval** (#477). The #207 flag prefixed a FAIL with `APPROVED OVER A FAILING GATE` whenever any CONFIRMED row carried the stage's number, so a Stage-5 build with `| 5 | Sub-agents run on Sonnet | CONFIRMED |` was flagged, and `status.sh` told the lead to re-open an approval nobody gave while the real gap (PENDING obligations) sat underneath. The register has no dedicated exit row, so `gate-check.sh` now counts a stage as approved on either of two pieces of evidence in the Decisions table: an explicit exit row ("Stage N approved", "Stage N gate: signed off", "Gate approved", or a short "Build plan approved"), or a CONFIRMED decision for a later stage, i.e. the project moved past it (the #207 field case). The Stage 3/4 gate checks and every verdict and exit code are unchanged. `test-bug03-gates.sh` T17 (8 cases): 3 FAIL on master, 39/39 with the fix. — MendixMau +- fix(gates): **a decision taken during a stage no longer reads as that stage's approval** (#477). The #207 flag prefixed a FAIL with `APPROVED OVER A FAILING GATE` whenever any CONFIRMED row carried the stage's number, so a Stage-5 build with `| 5 | Sub-agents run on Sonnet | CONFIRMED |` was flagged, and `status.sh` told the lead to re-open an approval nobody gave while the real gap (PENDING obligations) sat underneath. The register has no dedicated exit row, so `gate-check.sh` now counts a stage as approved on either of two pieces of evidence in the Decisions table: an explicit exit row ("Stage N approved", "Stage 4 — approved with conditions", "Approved: Stage N …", "Gate approved", or " approved" for a fixed artifact list such as "Build plan approved"), or the stage having CONFIRMED rows of its own plus a CONFIRMED decision for a later stage up to 6 (the #207 field case); an early Stage-7 "no cutover" row or an N/A/skip row is not evidence. The Stage 3/4 gate checks and every verdict and exit code are unchanged. `test-bug03-gates.sh` T17 (16 cases, checked under gawk, mawk and original-awk): 7 FAIL on master, 47/47 with the fix. — MendixMau + ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 0b3d64c0..7759f539 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1074,11 +1074,22 @@ has_confirmed_decision() { # Notes |`; conversion-runbook.md §2 closes a gate on "its ✋ decisions CONFIRMED and gate-check # run"), so a stage counts as approved on either of two pieces of evidence, both in CONFIRMED # rows of the Decisions table (same table scoping and status anchoring as above): -# 1. an explicit exit row for Stage N: its decision reads "Stage N approved / exit / gate -# approved / signed off / closed / complete", "Gate approved|signed off|passed|closed", or -# is a short " approved|signed off" phrase ("Build plan approved"); -# 2. the project moved past it: a CONFIRMED row for a LATER stage (P < 0 < 1 … < 7), which is -# how the #207 field case read (a Stage-4 approval, then 47 scripts built on it). +# 1. an explicit exit row for Stage N, in one of four wordings: +# "Stage N approved|exit…|gate: signed off|closed|complete|done|passed" (separators may be +# spaces, ":", "-", or an em/en dash: "Stage 4 — approved with conditions"); +# "Approved: Stage N …"; +# "Gate approved|signed off|passed|closed" ("gate" as the first word); +# " approved|signed off", the artifact from a fixed list (build plan, plan, +# blueprint, architecture, design (system), wireframes, scope, triage, BRDs, +# requirements), so domain decisions ("Requests over 5k must be approved") never match. +# No checkpoint writes such a row today, so in practice rule 2 does the work; having the +# CAC checkpoint write one is the follow-up. +# 2. the project moved past it: Stage N has CONFIRMED rows of its own AND a CONFIRMED row for +# a later stage up to 6 exists (the #207 field case: a Stage-4 approval, then 47 scripts +# built on it). Stage 7 is excluded and so is any row reading N/A / skip / no cutover: +# checkpoint-cutover.md and existing-app-change.md tell projects to record "No legacy +# system, no cutover" early, and that is not evidence that Stage 5 was signed off. A stage +# with no rows of its own is never "approved" by a forward-filed decision either. # A mid-stage decision is neither, so it no longer raises the flag. stage_exit_recorded() { local stage="$1" @@ -1086,7 +1097,13 @@ stage_exit_recorded() { [ -n "$f" ] && [ -f "$f" ] || return 1 awk -F'|' -v want="$stage" ' function rank(s) { s = toupper(s); if (s == "P") return 0; if (s ~ /^[0-7]$/) return s + 1; return -1 } - BEGIN { wr = rank(want); w = tolower(want) } + BEGIN { + wr = rank(want); w = tolower(want); r6 = rank("6") + # Separators: space, tab, colon, hyphen, or an em/en dash. The dashes are multi-byte, so + # they are an alternation, never inside a bracket class (POSIX awk reads bytes there). + SEP = "([ \t:-]|—|–)*" + ARTS = "^(the )?(build plan|plan|blueprint|architecture|design( system)?|wireframes?|scope|triage|brds?|requirements)" + } !/^\|/ { intab = 0; next } { h = tolower($2); gsub(/^[ \t]+|[ \t]+$/, "", h) @@ -1101,18 +1118,21 @@ stage_exit_recorded() { if (f ~ /^CONFIRMED([ \t]|$)/) confirmed = 1 } if (!confirmed) next + d = tolower($3); gsub(/^[ \t]+|[ \t.]+$/, "", d) r = rank(s) - if (wr >= 0 && r > wr) { if (intab) tlater = 1; alater = 1 } + if (wr >= 0 && r > wr && r <= r6 && d !~ /(^|[^a-z])(n\/a|not applicable|skip(ped|s)?|no cutover)([^a-z]|$)/) { + if (intab) tlater = 1; alater = 1 + } if (tolower(s) != w) next - d = tolower($3); gsub(/^[ \t]+|[ \t.]+$/, "", d) - words = split(d, junk, /[ \t]+/) - if (d ~ ("(^|[^a-z0-9])stage[ \t]*" w "[ \t:-]*((gate|exit)[ \t:-]*)?(approv|sign|closed|complete|done|passed)") || - d ~ /(^|[^a-z])gate[ \t:-]*(approved|signed[ -]off|sign-off|passed|closed)/ || - (words <= 6 && d ~ /[ \t](approved|signed[ -]off|sign-off)$/)) { + if (intab) town = 1; aown = 1 + if (d ~ ("(^|[^a-z0-9])stage[ \t]*" w SEP "((gate|exit)" SEP ")?(approv|sign|closed|complete|done|passed)") || + d ~ ("^approved" SEP "stage[ \t]*" w "([^0-9]|$)") || + d ~ ("^gate" SEP "(approved|signed[ -]off|sign-off|passed|closed)") || + d ~ (ARTS SEP "(approved|signed[ -]off)$")) { if (intab) texit = 1; aexit = 1 } } - END { exit !(hdr ? (texit || tlater) : (aexit || alater)) } + END { exit !(hdr ? (texit || (town && tlater)) : (aexit || (aown && alater))) } ' "$f" } diff --git a/tests/wave2/test-bug03-gates.sh b/tests/wave2/test-bug03-gates.sh index 4d083701..8b92fe53 100755 --- a/tests/wave2/test-bug03-gates.sh +++ b/tests/wave2/test-bug03-gates.sh @@ -324,6 +324,32 @@ V="$(t17 t17h "$MID" "## Open questions" "" "| # | Question | Raised at | Status case "$V" in *'APPROVED OVER'*) bad "an Open-questions row stood in for the approval: $V" ;; *) ok "an Open-questions row is not a stage approval (same table scoping as T13)" ;; esac +# Review of #483: the exit wordings people actually write, and rows that must not count. +V="$(t17 t17i "| 4 | Stage 4 — approved with conditions | CONFIRMED | |")" +flagged "$V" && ok "'Stage 4 — approved with conditions' (em dash) is an approval" || bad "em-dash exit row not flagged: $V" +V="$(t17 t17j "| 4 | Approved: Stage 4 build plan | CONFIRMED | |")" +flagged "$V" && ok "'Approved: Stage 4 build plan' is an approval" || bad "'Approved: Stage N' form not flagged: $V" +for d in 'Status values: draft, submitted, approved' 'Requests over 5k must be approved' 'Approval gate closed after 30 days' 'Stage 40 approved'; do + V="$(t17 t17k "| 4 | $d | CONFIRMED | |")" + case "$V" in *'APPROVED OVER'*) bad "a domain decision raised the flag: '$d'" ;; + *) ok "domain wording is not an approval: '$d'" ;; esac +done +# A failing Stage 5 with no rows of its own: the LOOK obligation owes one unseen page. +t17s5() { # t17s5 -> the Stage 5 line, and the summary line if any + local P; P="$(mkproj "$1")"; shift + printf '%s\n' "$@" >> "$P/PROJECT.md" + mkdir -p "$P/.claude/loop/look" + printf '%s\t%s\t%s\t%s\n' "$(date +%s)" "2026-10-20T10:00:00Z" "Orders.Order_Overview" "10-orders.mdl" > "$P/.claude/loop/look/owed.tsv" + "$GATE" --no-html "$P" 2>&1 | grep -E '^Stage 5 |^Approved over a failing gate:' +} +V="$(t17s5 t17l "| 7 | No legacy system, no cutover | CONFIRMED | |")" +case "$V" in *FAIL*) ;; *) bad "t17l setup: Stage 5 did not FAIL: $V" ;; esac +case "$V" in *'APPROVED OVER'*|*'Approved over'*) bad "an early 'no cutover' Stage-7 row flagged a mid-build Stage 5: $V" ;; + *) ok "a Stage-7 'No legacy system, no cutover' row is not evidence Stage 5 was approved" ;; esac +V="$(t17s5 t17m "| 6 | Test depth: journeys per module | CONFIRMED | |")" +case "$V" in *'APPROVED OVER'*|*'Approved over'*) bad "a forward-filed Stage-6 row flagged a Stage 5 with no rows of its own: $V" ;; + *) ok "a forward-filed Stage-6 decision does not approve a Stage 5 with no rows of its own" ;; esac + printf '\n%s: %d ok, %d FAIL\n' "$(basename "$0")" "$PASS" "$FAIL" rm -rf "$WORK" [ "$FAIL" -eq 0 ]