From b082a85a3165d7dab83cea7e2615a41565ee3d49 Mon Sep 17 00:00:00 2001 From: r Date: Tue, 29 Sep 2026 12:59:15 +0000 Subject: [PATCH] fix(gate-check): read stage decisions from the Decisions table only; Stage 7 accepts a dated CONFIRMED has_confirmed_decision and check_stage_7 scanned every pipe row, so an Open-questions row numbered 4 or 7 with Status CONFIRMED passed the stage. Both now count only rows under a header whose first cell is Stage, falling back to the old scan when no such header exists. Stage 7 now uses the word-anchored CONFIRMED match the other stages use. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/gate-check.sh | 42 +++++++++++++++++++++-------- tests/wave2/test-bug03-gates.sh | 48 ++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f65597..8756c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ three commits past it), and a bug report can name a release instead of a sha nob Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- fix(gate-check): **the stage-decision readers now read only the Decisions table, and Stage 7 accepts a dated CONFIRMED.** `has_confirmed_decision` (Stages 3 and 4) and the Stage 7 cutover check scanned every pipe row in the register, so an Open-questions row numbered 4 or 7 with Status CONFIRMED passed that stage with no decision behind it. Both now count only rows under a header whose first cell is `Stage`; a register with no such header keeps the old every-row scan. Stage 7 also matched the status string-exactly, so `CONFIRMED 2026-08-10` failed there while passing every other stage (TD-07); it now uses the same word-anchored match. Positive control: the pre-fix script passes Stages 4 and 7 on an Open-questions-only register and fails Stage 7 on the dated status. Fixture T13–T15 in `test-bug03-gates.sh` — MendixMau - chore(inbox): **triage of `contrib/inbox/`: removed 15 promoted/obsolete files, gave the rest neutral names and scrubbed identifiers.** Gone: the ten machine-diff `*-patches.md` files from 2026-09-14, the 2026-09-27 field-project patch file, the built company-brain idea, and three already-promoted or superseded bug dumps. Kept for now: `2026-09-27-mxcli-upstream-issues.md`, because five drafts under `bug-logs/pending-github-issues/` cite it as their source. Four remaining files were renamed to `field-project-{a,b,c}` names; project and app names, home-style paths and personal names inside the 17 remaining entries were replaced with neutral labels or placeholders (technical content unchanged). — MendixMau - learn(learned-mdl-preflight, learned-detection-gaps, microflow-preflight, testing-shape, learned-mdl-cannot-express, learned-page-patterns, rest-integration-first-time-right, journey-proof, learned-datagrid-customcontent-binding): **the skills side of BUG-142 to BUG-154.** STOP row 12 rewritten shorter for v0.24.0 — `ContentParams` takes a bare attribute or a quoted literal only, and the `toString(Attr)` fix it taught no longer builds; row 14 notes a narrowing `grant` merges, row 23 that `diff` prints false `-`/`~` lines. Detection gaps: the ContentParams row now covers any object-bound `dynamictext` and says check passes `if … then … else`; reserved words cover entities; new CE1870 row. microflow-preflight: CONV013 false positive and doc-comment round-trip resolved from "Not verified"; error handler without `return` is the second merge-overlap shape. testing-shape: `--test-endpoint` writes into the working model until a clean stop; `run --local` starts with scheduled events off (new register row). Also: five cannot-express rows (tab badge, validate-while-typing, AppTitle, always-open nav groups, sidebar toggle / page-load / Marketplace layout) and the DG2 `emptyplaceholder` correction; control-bar attributes, nested list view `Editable` and REPLACE-not-re-run-safe in page patterns; v0.24.0 REST bullets; two OQL journey traps. Triages three inbox notes (ContentParams, REST/JSON/page field notes, scheduled events) — card-disbursement requirements-driven build - learn(bug-logs): **thirteen new ledger entries (BUG-142 to BUG-154) and five retests from mxcli v0.24.0 / Mendix 11.13.0.** New: `ContentParams` builds only from a bare attribute or a quoted literal, and `if … then … else` passes check and fails CE1613 (supersedes the BUG-23 fix); theme seeder and Atlas-map gaps; `create user role` omits `CheckSecurity`/`GUID`; a narrowing `grant` merges; `diff` false modifications; check/exec disagreeing on same-script documents; E007 on a last quoted enum literal; a table of check/lint misfires and check-green/mxbuild-red constructs; DG2 and filter definition gaps; `rest call … body $Var` sends the literal text; published-REST, mapping and Java action gaps; OQL `HAVING` and date aggregates; MDL surface gaps. Retests: BUG-08 (REPLACE still collides, and on reused child names), BUG-77 reproduced with its trigger isolated, BUG-92 and BUG-117 field instances, BUG-122 on a snippet list view; the layout-merge draft gains the error-handler shape. Each from a `check`/exec/mxbuild A/B or a BSON read on a scratch copy — card-disbursement requirements-driven build diff --git a/bin/gate-check.sh b/bin/gate-check.sh index c87d6c7..c86ce31 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1022,8 +1022,16 @@ has_confirmed_decision() { local stage="$1" local f="$REGISTER" [ -n "$f" ] && [ -f "$f" ] || return 1 + # Table-scoped: only rows of the Decisions table (a header row whose first cell is + # "Stage") count. Scanning every pipe row let the Open-questions table + # ("| 7 | ... | Raised at | CONFIRMED |") stand in for a stage decision. A register with + # no such header anywhere falls back to scanning every table, as before. awk -F'|' -v want="$stage" ' - /^\|/ { + !/^\|/ { 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) @@ -1043,10 +1051,10 @@ has_confirmed_decision() { # yet") does not qualify, and neither does "NOT CONFIRMED". What this newly # admits is a field that BEGINS with CONFIRMED and then continues — which is # the convention it exists to accept. - if (f ~ /^CONFIRMED([ \t]|$)/) found = 1 + if (f ~ /^CONFIRMED([ \t]|$)/) { anyf = 1; if (intab) tabf = 1 } } } - END { exit !found } + END { exit !(hdr ? tabf : anyf) } ' "$f" } @@ -1212,7 +1220,7 @@ check_stage_7() { echo "PENDING|no decision register (PROJECT.md) under $PROJECT_DIR — nothing to read a cutover decision from" return fi - # Field-exact on the STATUS (as has_confirmed_decision already is, since d8117be) and now + # Anchored on the STATUS (as has_confirmed_decision is, since d8117be) and now # also column-aware on ROW SELECTION. `tolower($0) ~ /cutover/` scanned the whole row # including the free-text Notes cell, so an unrelated CONFIRMED row whose notes said # "groundwork for the eventual cutover" passed Stage 7 — the ✋ gate whose stated purpose is @@ -1220,22 +1228,34 @@ check_stage_7() { # (col 2) equal to 7, or the decision field (col 3) naming the cutover. Distinguish "no such # row" from "row present but not CONFIRMED" — they need different fixes. local verdict rows + # + # Table-scoped the same way as has_confirmed_decision: only the Decisions table (header + # row starting "| Stage |") is read, so an Open-questions row "| 7 | ... |" or any other + # table mentioning the cutover is not a candidate. Status matching is word-anchored like + # has_confirmed_decision (TD-07), so "CONFIRMED 2026-08-10" passes here as it does for + # every other stage; "NOT CONFIRMED" and a Notes cell mentioning the word still do not. verdict=$(awk -F'|' ' - /^\|/ { + !/^\|/ { 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) d=(NF>=3)?tolower($3):"" if (s != "7" && d !~ /cutover/) next - rows++ - for (i=2;i<=NF;i++){ v=toupper($i); gsub(/^[ \t]+|[ \t]+$/,"",v); if (v=="CONFIRMED") found=1 } + c=0 + for (i=2;i<=NF;i++){ v=toupper($i); gsub(/^[ \t]+|[ \t]+$/,"",v); if (v ~ /^CONFIRMED([ \t]|$)/) c=1 } + anyr++; if (c) anyf=1 + if (intab) { tabr++; if (c) tabf=1 } } - END { print (found?"PASS":"FAIL") " " rows+0 }' "$f") + END { if (hdr) print (tabf?"PASS":"FAIL") " " tabr+0; else print (anyf?"PASS":"FAIL") " " anyr+0 }' "$f") rows="${verdict#* }" if [ "${verdict%% *}" = "PASS" ]; then - echo "PASS|a Stage-7/cutover decision row in $f has Status exactly CONFIRMED ($rows candidate row(s))" + echo "PASS|a Stage-7/cutover decision row in $f has Status CONFIRMED ($rows candidate row(s))" elif [ "$rows" = "0" ]; then - echo "PENDING|no cutover decision row in $f — add a Decisions row whose Stage field is 7 (or whose Decision names the cutover) with Status exactly CONFIRMED" + echo "PENDING|no cutover decision row in $f — add a Decisions row whose Stage field is 7 (or whose Decision names the cutover) with Status CONFIRMED" else - echo "FAIL|$rows cutover decision row(s) in $f, none with a field exactly CONFIRMED (✋ gate — UNCONFIRMED/ASSUMED does not pass)" + echo "FAIL|$rows cutover decision row(s) in $f, none with a field starting CONFIRMED (✋ gate — UNCONFIRMED/ASSUMED does not pass)" fi } diff --git a/tests/wave2/test-bug03-gates.sh b/tests/wave2/test-bug03-gates.sh index c8a81f7..2d40b0c 100755 --- a/tests/wave2/test-bug03-gates.sh +++ b/tests/wave2/test-bug03-gates.sh @@ -101,7 +101,7 @@ P="$(mkproj t7)" } > "$P/PROJECT.md" V="$(verdict "$P" 7)" case "$V" in - *FAIL*) case "$V" in *'none with a field exactly CONFIRMED'*) ok "notes-only mention fails, and distinguishes 'row present, not CONFIRMED'" ;; + *FAIL*) case "$V" in *'none with a field starting CONFIRMED'*) ok "notes-only mention fails, and distinguishes 'row present, not CONFIRMED'" ;; *) bad "fails but with the wrong diagnosis: $V" ;; esac ;; *) bad "PASSED on a CONFIRMED row that merely MENTIONS the cutover in its notes: $V" ;; esac @@ -207,6 +207,52 @@ V="$(verdict "$P" 1)" case "$V" in *'Path D'*) bad "migration project got the existing-app Stage 1 hint: $V" ;; *) ok "other modes keep the extractor / kb-generation hint" ;; esac +echo "== T13: only the Decisions table counts — an Open-questions row numbered 7 is not a cutover decision ==" +# Both register readers scanned every pipe row in the file, so the template's Open-questions +# table ("| # | Question | Raised at | Status |") could stand in for a stage decision whenever +# a question's number matched the stage and its Status cell said CONFIRMED. +P="$(mkproj t13)" +{ printf 'Toolkit commit: none\n\n## Decisions\n\n| Stage | Decision | Status | Notes |\n|---|---|---|---|\n' + printf '| 3 | Adopt Atlas design system | CONFIRMED | unrelated |\n' + printf '\n## Open questions\n\n| # | Question | Raised at | Status |\n|---|---|---|---|\n' + printf '| 7 | Who owns the cutover weekend? | Stage 5 | CONFIRMED |\n' + printf '| 4 | Build plan order ok? | Stage 4 | CONFIRMED |\n' +} > "$P/PROJECT.md" +V="$(verdict "$P" 7)" +case "$V" in *PASS*) bad "an Open-questions row passed Stage 7: $V" ;; + *'no cutover decision row'*) ok "Open-questions row #7 is not a Stage-7 candidate" ;; + *) bad "wrong diagnosis: $V" ;; esac +mkdir -p "$P/architecture"; printf '# Build plan\n' > "$P/architecture/build-plan.md" +V="$(verdict "$P" 4)" +case "$V" in *PASS*) bad "an Open-questions row passed Stage 4: $V" ;; + *FAIL*) ok "Open-questions row #4 is not a Stage-4 decision" ;; + *) bad "no Stage 4 FAIL: $V" ;; esac +{ printf '\n## Decisions (later)\n\n| Stage | Decision | Status | Notes |\n|---|---|---|---|\n' + printf '| 4 | Build plan approved | CONFIRMED | |\n' +} >> "$P/PROJECT.md" +V="$(verdict "$P" 4)" +case "$V" in *PASS*) ok "a Stage-4 row in a second Stage-headed table still counts" ;; + *) bad "false red on a real Stage-4 row: $V" ;; esac + +echo "== T14: Stage 7 accepts a dated status, like every other stage (TD-07) ==" +P="$(mkproj t14)" +{ printf 'Toolkit commit: none\n\n| Stage | Decision | Status | Notes |\n|---|---|---|---|\n' + printf '| 7 | Cutover plan | CONFIRMED 2026-08-10 | signed off |\n' +} > "$P/PROJECT.md" +V="$(verdict "$P" 7)" +case "$V" in *PASS*) ok "'CONFIRMED 2026-08-10' passes Stage 7" ;; *) bad "dated CONFIRMED rejected at Stage 7: $V" ;; esac +{ printf 'Toolkit commit: none\n\n| Stage | Decision | Status | Notes |\n|---|---|---|---|\n' + printf '| 7 | Cutover plan | NOT CONFIRMED | pending sponsor |\n' +} > "$P/PROJECT.md" +V="$(verdict "$P" 7)" +case "$V" in *FAIL*) ok "'NOT CONFIRMED' still fails Stage 7" ;; *) bad "'NOT CONFIRMED' did not fail: $V" ;; esac + +echo "== T15: a register with no Stage-headed table falls back to scanning every row ==" +P="$(mkproj t15)" +{ printf 'Toolkit commit: none\n\n| 7 | Cutover plan | CONFIRMED | |\n'; } > "$P/PROJECT.md" +V="$(verdict "$P" 7)" +case "$V" in *PASS*) ok "header-less register keeps the old behaviour" ;; *) bad "header-less register regressed: $V" ;; esac + printf '\n%s: %d ok, %d FAIL\n' "$(basename "$0")" "$PASS" "$FAIL" rm -rf "$WORK" [ "$FAIL" -eq 0 ]