From f202b180e7ba934a97766a8bcf005ee0579c2c4f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 15:42:44 +0000 Subject: [PATCH] fix(facts-lock): 0 frozen facts reads PENDING, not "agree with all 0" (#519) A corpus whose identifiers sit only under keys the harvest does not read froze an empty lock, and check printed "N BRD(s) agree with all 0 frozen fact(s)", which reads as a pass over a comparison that never happened. With 0 frozen facts, check and build now print a PENDING line naming why (no identifiers under any known key, or no lock built yet). Exit codes are unchanged; name-key harvesting is not part of this change. Fixture: test-facts-lock.sh (3 FAIL on master; 19/19 with the fix). Fixes #519 (the safe half; see PR) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + bin/facts-lock.sh | 12 +++++++++++- tests/wave2/test-facts-lock.sh | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee82ab78..6aa8e763 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(facts-lock): **`check` no longer reports "N BRD(s) agree with all 0 frozen fact(s)" when nothing was compared** (#519). A corpus whose BRDs carry identifiers only under keys the harvest does not read (`domainEntities[].name`, `pages[].name`) froze an empty lock, and `check` printed what reads as a pass. With 0 frozen facts, `check` now prints `PENDING facts-lock: 0 identifiers found under any known key in N BRD(s): the lock is empty and check proves nothing (keys read: ...)`, or, when identifiers exist but no lock was built, `PENDING ... run build first; check compared nothing`; `build` prints the same empty-lock line when it froze 0 facts and found no conflicts. Exit codes are unchanged (an exit-3 "nothing measured" and harvesting `name` keys are left for a separate change). Fixture: `test-facts-lock.sh` (the issue's repro: 3 fail on master; 19/19). — AlvinnYeo (#519) - fix(progress-report, build-plan-status): **HARNESS rows are no longer counted as built rows, and a module's `verify-module.sh` row shows once, as that module's sign-off status on its phase card, instead of as a row.** A HARNESS step is a module's sign-off, not a build, so it is out of every "x of y built" count: the phase cards, the plan rollup and the Stage 5 card in `progress-report.html`, and the lede of `build-plan-status.sh --html` ("N build rows", plus a "Not counted: N HARNESS rows" note). The verify row's evidence is read exactly as #501 reads it (`harness_verify`'s rule, now `verify_evidence` + `module_status` in `progress-report.py`; nothing new measured, no extra script run, no mxcli or mxbuild call) and shown as one line per module: `Verified ✓ (LOOK n/n)`, `Verify owed - LOOK + CONFIRM`, `Verify stale (N builds since)` (or `(a script was marked done- after it)`), `Verify not run`, `built? no verify summary`, `Verify not clean ()` for a summary with a FAULT, FINDING or SKIPPED row, or `Verify clean , LOOK n/n - held: State says ` when State is blocked or pending a person (held is not signed off, as #501 never promoted it); a State cell that disagrees is flagged under it, the rollup says how many modules are verified, and the Stage 5 card, KPI and owed text add ", k of n modules signed off". The Phase state badge is read from the build rows only (a phase of HARNESS rows alone says "sign-off only" and its card "Sign-off phase: no build rows"). A module with no verify row shows no status. Other HARNESS rows (a coherence pass, a harness run) stay in the row list, marked "(not counted)". `build-plan.md`, `build-plan.json` and the text output are unchanged, so agents still see every HARNESS step. **Behaviour change to check in your project:** the progress report's built counts drop the HARNESS rows (a plan of 10 rows with 2 HARNESS reads "x of 8"), and the verify rows leave the row tables. Fixture: `test-progress-harness-verify.sh` (H1-H10, N1-N6, both layouts). — MendixMau (decision 2026-10-21) - fix(exec): **a script restated from a stale DESCRIBE now WARNs instead of silently reverting an earlier exec** (#261). A script drafted from a DESCRIBE taken before an earlier script of the batch ran put four old messages back, with check, mxbuild and lint green. New `project-bin/describe-baseline.sh stamp` writes `-- baseline: sha256=` (plus `-- baseline-mxcli: `) from a DESCRIBE at draft time; `exec.sh` runs `verify` only for a script that carries the header, so other scripts cost one grep and no mxcli call. It reports DRIFT with the first lines of the diff, STALEHEADER, and RESTAMP after an mxcli upgrade (DESCRIBE text may change, so that is not called drift). A re-run by the script's own last recorded write (the #269 ledger) and a fresh model are not drift. Nothing is refused and the exit codes do not change. Preflight STOP 21 gains one clause (`-- supersedes:`, `stamp`, exec.sh only warns) and its trigger names `create or modify` (+24 words). Not-found DESCRIBE and `--version` output are captured from mxcli v0.26.0. Lands after the #269 PR; with it, supersedes draft #311. **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: stale draft: …` lines, and the BUILD-LOG detail cell `WARN stale baseline: `; `MXTK_RESTATE_GUARD=off` skips the check. — AlvinnYeo (#261) - fix(exec): **re-running an older `mdlsource/` script over a newer owner's live work now WARNs instead of passing silently** (#269). Three field apps had 99/579, 130/357 and 38/159 documents created by more than one script, and a replay of an older one reverted the newer with exit 0 and every gate green. New `project-bin/script-owners.sh` maps each microflow, nanoflow, page and snippet to the scripts that create or restate it, in run order (digit runs compared as numbers, so `2-core/` runs before `11-fixes/`; `done-` ignored), and reads `-- supersedes:` / `-- superseded-by:` headers. `exec.sh` records every kept write in a machine-local last-writer ledger (`.claude/loop/script-owners/`, self-gitignored, compacted per write); `restore-mpr.sh` marks it on a manual restore. A replay WARNs only when the newer owner's write is live: ledger first, else one DESCRIBE per document, at most 5 per exec (the rest print `CONFLICT (not checked)`). A new owner without a supersedes header gets one `NOTE:` line on its first run, kept out of BUILD-LOG. Nothing is refused and the exit codes do not change. With no co-owner, `exec.sh` runs one grep over `mdlsource/` and no mxcli call. `gate-check.sh` prints the co-owned count when it is above 0. Part of the rebuilt draft #311 (with the #261 PR). **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: restatement: …` and `NOTE: new owner of …` lines; the BUILD-LOG detail cell can carry `WARN reverts: `. Set `MXTK_RESTATE_GUARD=off` to skip the check. — MendixMau (#269) diff --git a/bin/facts-lock.sh b/bin/facts-lock.sh index 2f303451..09a5668c 100755 --- a/bin/facts-lock.sh +++ b/bin/facts-lock.sh @@ -212,6 +212,8 @@ for ns, clusters in sorted(harvest.items()): conflicts.append((ns, ck, {v: sorted(spellings[v]) for v in variants})) # --- act ---------------------------------------------------------------------------------------- +EMPTY_MSG = ("PENDING facts-lock: 0 identifiers found under any known key in %d BRD(s): the lock is empty " + "and check proves nothing (keys read: " + ", ".join(k for ks in KEYED.values() for k in ks) + ", *_CODE_SET)") if CMD == "build": for ns, ck, canon, srcs in new_facts: frozen.setdefault(ns, {})[ck] = {"canonical": canon, "sources": srcs} @@ -229,6 +231,8 @@ if CMD == "build": total = sum(len(v) for v in frozen.values()) print("facts-lock: %d fact(s) frozen across %d namespace(s), from %d BRD(s)" % (total, len(frozen), len(brds))) print(" %s" % LOCK) + if total == 0 and not conflicts: + print(EMPTY_MSG % len(brds)) for src, err in unreadable: print(" UNREADABLE %s — %s (its facts are NOT in the lock)" % (src, err)) @@ -253,7 +257,13 @@ if CMD == "check": print(" UNFROZEN %s/%s is contested (%s) — build has not resolved it" % (ns, ck, "/".join(sorted(sp)))) if not deviations and not conflicts: total = sum(len(v) for v in frozen.values()) - print("facts-lock: %d BRD(s) agree with all %d frozen fact(s)" % (len(brds), total)) + if total == 0: + # 0 compared is not agreement (#519): a lock with no facts proves nothing. + print(EMPTY_MSG % len(brds) if not harvest else + "PENDING facts-lock: the lock holds 0 facts while %d identifier(s) were found in %d BRD(s): run build first; check compared nothing" + % (sum(len(c) for c in harvest.values()), len(brds))) + else: + print("facts-lock: %d BRD(s) agree with all %d frozen fact(s)" % (len(brds), total)) if unreadable: sys.exit(1) diff --git a/tests/wave2/test-facts-lock.sh b/tests/wave2/test-facts-lock.sh index df62fd08..8b27dd9e 100644 --- a/tests/wave2/test-facts-lock.sh +++ b/tests/wave2/test-facts-lock.sh @@ -119,6 +119,33 @@ esac bash "$SUBJECT" "$P" show >/dev/null 2>&1 [ "$?" -ne 0 ] && ok "an unreadable BRD makes the run non-zero" || bad "exited 0 despite not reading a BRD" +# --- 0 facts is not agreement (#519) ------------------------------------------------------------------- +# The issue's repro: BRDs that carry identifiers only under `name` keys give the harvest nothing. +# check used to print "2 BRD(s) agree with all 0 frozen fact(s)", which reads as a pass over a +# comparison that never happened. Exit codes are unchanged; the line must say PENDING, not agree. +Z="$WORK/zero"; mkdir -p "$Z/analysis/x/knowledge-base/brd" +echo '{"id":"F001","domainEntities":[{"name":"PaymentPlan"}],"pages":[{"name":"PaymentPlan_Overview"}]}' > "$Z/analysis/x/knowledge-base/brd/F001-a.brd.json" +echo '{"id":"F002","domainEntities":[{"name":"Paymentplan"}],"pages":[{"name":"PaymentPlan_Overview"}]}' > "$Z/analysis/x/knowledge-base/brd/F002-b.brd.json" +ZC=$(bash "$SUBJECT" "$Z" check 2>&1) +case "$ZC" in + *"agree with all 0"*) bad "check over 0 harvested facts still reads as agreement"; echo " $ZC" | head -2 ;; + *"PENDING facts-lock: 0 identifiers found under any known key in 2 BRD(s)"*) ok "check over 0 harvested facts says PENDING, not agree" ;; + *) bad "check over 0 harvested facts printed neither agree nor PENDING"; echo " $ZC" | head -2 ;; +esac +ZB=$(bash "$SUBJECT" "$Z" build 2>&1) +case "$ZB" in + *"PENDING facts-lock: 0 identifiers found"*) ok "build that freezes 0 facts says the lock is empty" ;; + *) bad "build froze 0 facts and did not say the lock is empty"; echo " $ZB" | head -3 ;; +esac +# A lock not yet built over a corpus that does have identifiers: check says build first. +N="$WORK/notbuilt"; mkdir -p "$N/analysis/x/knowledge-base/brd" +echo '{"id":"F001","domainEntities":[{"entity":"PaymentPlan"}]}' > "$N/analysis/x/knowledge-base/brd/F001-a.brd.json" +NC=$(bash "$SUBJECT" "$N" check 2>&1) +case "$NC" in + *"run build first"*) ok "check with no lock built says run build first, not agree" ;; + *) bad "check with no lock built did not say run build first"; echo " $NC" | head -2 ;; +esac + # --- usage -------------------------------------------------------------------------------------------- bash "$SUBJECT" 2>/dev/null; [ "$?" -eq 2 ] && ok "no argument exits 2" || bad "no argument did not exit 2" bash "$SUBJECT" "$P" nonsense 2>/dev/null; [ "$?" -eq 2 ] && ok "unknown subcommand exits 2" || bad "unknown subcommand accepted"