From 300a80e587a715a32e8a2ea68a366a17156e622e Mon Sep 17 00:00:00 2001 From: MendixMau <55099313+MendixMau@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:29:08 +0800 Subject: [PATCH 01/18] fix(build-plan-status): --refresh keeps an existing build-plan.json current (#437) fix(build-plan-status) # PR 437 review r1 (self-review, small delta) Verdict: APPROVE - --refresh path: parse into TMP_JSON as before; only when json already exists and --json not given, compare minus generatedAt, cp on difference. Never creates. Correct. - Same parser as --json (parse_plan), so content identical to an explicit run. - Test T11 covers flip, no-rewrite, no-create. 70/70 pass. - Docs/routing surfaces in sync (render-routing --check). --- CHANGELOG.md | 1 + README.md | 2 +- ROUTING.md | 2 +- bin/lib/artifact-manifest.tsv | 2 +- bin/lib/skill-routing.tsv | 2 +- project-bin/build-plan-status.sh | 18 +++++++++++++++++- tests/wave2/test-build-plan-json.sh | 29 +++++++++++++++++++++++++++++ 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fa96ab..1a2899e3 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(build-plan-status): --refresh keeps an existing build-plan.json current, so a day of building no longer reads as 0 built — MendixMau - learn(unit-test): the unit-test skill names the per-flow `Unit flows:` list next to `Unit modules:`, so whether a flow gets a test reads the same in both places (follow-up to #435) — MendixMau - learn(load-test): **new on-demand skill for load testing a page without correlating Mendix's client protocol by hand: Playwright records the real `/xas/` calls, a probe maps each opaque `runtimeOperation` to its microflow through Jaeger, and the chosen call is replayed with the session's own cookie and CSRF token.** Ten field traps (HTTP 200 on a failed action, caught REST errors under a green microflow span, the wrong call hammered, a recording that still held the previous page, a tracing check that failed open, a harness that throttled itself, harness vs app limits, requests vs users, sampled trace counts, empty data) and a four-line report that says what the run does not prove. Routed ondemand for lead, test and review; no instrument ships yet and the roadmap's "no load testing" line is reworded. Field evidence: a warehouse demo app, Mendix 11, 16 runs 2026-09-09 to 2026-10-08, bursts of 1000 and 2000 concurrent calls on one session — MendixMau - new(process-map): **Process map: one swimlane map per business process, each step on its wireframe once screens exist, every open question and ASSUMED decision pinned to the step it is about; drawn twice, a draft at Stage 2 close without screens (pass 1, shown at CAC-3) and the full map at Stage 3 with screens (pass 2, walked at the Stage 3 gate close-out).** `skills/process-map.md` (when, model, anchoring, build, verify by looking) and `project-bin/process-map.js` (`validate`, `anchor`, `build`; Node stdlib, deterministic) with its template. Named "map" because "process" in the progress report means the toolkit's own stages, while this shows the app's business process. With no wireframes, a step may name a screen that is not drawn yet or have none, and a person step stays a person step; once a wireframe exists `validate` fails a step page with no wireframe, a use case on no process and not standalone, a decision with one outcome, two steps in one cell. `anchor` places items mechanically (named microflow, listed or named use case, named screen, overlapping source lines) and keeps judged placements by fingerprint, so a reworded question is unplaced again instead of silently wrong; global buckets are allowed and counted. Wired into the Stage 3 Agent produces and Surface rows, a `process-map` opt-in manifest row (an existing model counts as opting in), the `swimlanes` opt-in row (the model discharges it), CAC-3 and CAC-4, and a check_stage_3 FAIL when the map is older than its model, its anchors or a wireframe. `tests/wave2/test-process-map.sh` on a synthetic order approval fixture with its own committed BRD, covering both passes. Field evidence: a requirements-driven rebuild POC, 2026-10-16, where the reviewer could not tell where the sixty questions fit; the map placed 102 items with 0 unplaced. — MendixMau diff --git a/README.md b/README.md index 59d5cbae..32d73a18 100644 --- a/README.md +++ b/README.md @@ -605,7 +605,7 @@ Every mxcli project has a `.ai-context/skills/` directory (bundled by `mxcli ini |---|---| | Dispatching a build step to a helper — generate its one-file pack (the brief's Build steps row, mxcli brain brief, live DESCRIBEs, example, folder) and hand over the path instead of a reading list. Measured: 25-38% fewer tool calls and 30-41% less time, same quality; the token saving is not proven | `project-bin/context-pack.sh` | | An exec.sh guard refused a write (not a permission prompt), or building a module with mxcli — verified, iterative, coverage-checklist gated; on a refusal: commit, wait or stop, never FORCE_EXEC | `skills/iterative-build-loop.md` | -| After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file) | `project-bin/build-plan-status.sh` | +| After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file); --refresh keeps an existing build-plan.json current but never creates one | `project-bin/build-plan-status.sh` | | Turning a client-derived Mendix app into a clean, shareable demo with zero client fingerprint — branding, data, custom widgets | `skills/anonymize-client-app-for-demo.md` | | Handing a headless-built model to a person — opening it in Studio Pro, a free sandbox, or a colleague's machine: the model travels, the demo data and runtime config (keys, an agent's bound model) do not, and each needs its own re-establish step | `skills/handoff-to-studio-pro.md` | | Stage 5 start, before the first module of any entry mode — one entity, flow, page, nav, demo user, journey and screenshot proven in the running app, so build/run/look/test are known to work before a module depends on them | `skills/walking-skeleton.md` | diff --git a/ROUTING.md b/ROUTING.md index e2fe31e1..e75816da 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -144,7 +144,7 @@ picks the row up. That is the whole procedure — there is no second list to rem | Before any mxcli exec / exec.sh / --mcp write — ask or run? the knob decides | `bin/exec-approval.sh` | mdl,gate | 5,6 | baseline | | Dispatching a build step to a helper — generate its one-file pack (the brief's Build steps row, mxcli brain brief, live DESCRIBEs, example, folder) and hand over the path instead of a reading list. Measured: 25-38% fewer tool calls and 30-41% less time, same quality; the token saving is not proven | `project-bin/context-pack.sh` | mdl | 5 | ondemand | | An exec.sh guard refused a write (not a permission prompt), or building a module with mxcli — verified, iterative, coverage-checklist gated; on a refusal: commit, wait or stop, never FORCE_EXEC | `skills/iterative-build-loop.md` | mdl,gate | 5 | ondemand | -| After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file) | `project-bin/build-plan-status.sh` | architect,gate,review | 4,5,6 | ondemand | +| After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file); --refresh keeps an existing build-plan.json current but never creates one | `project-bin/build-plan-status.sh` | architect,gate,review | 4,5,6 | ondemand | | Turning a client-derived Mendix app into a clean, shareable demo with zero client fingerprint — branding, data, custom widgets | `skills/anonymize-client-app-for-demo.md` | mdl,review | 6 | ondemand | | Handing a headless-built model to a person — opening it in Studio Pro, a free sandbox, or a colleague's machine: the model travels, the demo data and runtime config (keys, an agent's bound model) do not, and each needs its own re-establish step | `skills/handoff-to-studio-pro.md` | test,review | 6,7 | ondemand | | Sharing the project with a colleague on Mendix Team Server while the build loop stays on GitHub — content-transplant snapshots between the two clones (status/push/pull); SUPERSEDED by platform-link.md §3 (adopt without rewriting history, field-proven) — only if that path fails; UNPROVEN against a real Team Server | `project-bin/ts-sync.sh` | mdl,gate | 6,7 | experimental | diff --git a/bin/lib/artifact-manifest.tsv b/bin/lib/artifact-manifest.tsv index 6808cadd..e866a5eb 100644 --- a/bin/lib/artifact-manifest.tsv +++ b/bin/lib/artifact-manifest.tsv @@ -92,7 +92,7 @@ wireframes 3 design/wireframes/*.html skills/design-artifacts.md Step 3, one per module-definition 3 architecture/modules/*/definition.md skills/architecture-blueprint.md Step 1 (:60 — the directory shape, one doc per module) THIS ROW IS ITS FIRST MECHANICAL CONSUMER — as of 2026-08-22 nothing executable read definition.md (improvement-plan P7 residue, the newest instance of the pattern this table exists to stop); prose consumers: skills/architecture-blueprint.md:43, skills/coverage-ledger.md:27 migration,requirements,existing-app-change report build-plan 4 architecture/build-plan.md skills/brd-to-build-plan.md bin/gate-check.sh:955 (check_stage_4), project-bin/build-plan-status.sh (progress board), project-bin/coverage-preflight.sh:238 (the claims join) migration,requirements,existing-app-change gate build-plan-html 4 architecture/build-plan.html project-bin/build-plan-status.sh --html (skills/brd-to-build-plan.md:47 — derived view, never hand-edited; refreshed by exec.sh and full gate-check.sh runs via --refresh) conversion-runbook.md §2 Stage-4 Surface row, parsed at bin/gate-check.sh:1284 migration,requirements,existing-app-change report -build-plan-json 4 architecture/build-plan.json project-bin/build-plan-status.sh --json (derived from build-plan.md's Step-5 Phase headings, row tables and claims: blocks, never hand-edited; not written when the markdown has no Phase headings) MXTK Studio Plan room (a view built from data, not from the HTML); nothing in the toolkit reads it and no verdict may depend on it migration,requirements,existing-app-change report +build-plan-json 4 architecture/build-plan.json project-bin/build-plan-status.sh --json (derived from build-plan.md's Step-5 Phase headings, row tables and claims: blocks, never hand-edited; not written when the markdown has no Phase headings; --refresh keeps an existing file current but never creates it) MXTK Studio Plan room (a view built from data, not from the HTML); nothing in the toolkit reads it and no verdict may depend on it migration,requirements,existing-app-change report module-brief 4 architecture/modules/*/module-brief.md|architecture/modules/*-brief.md skills/module-brief.md — ba-agent writes the first at Stage 4, the rest just-in-time bin/gate-check.sh:1121 (build-ready fails without one), project-bin/build-plan-status.sh:109 migration,requirements,existing-app-change gate coverage-ledger 4 architecture/modules/*/coverage-ledger.md|architecture/modules/*-coverage-ledger.md|architecture/coverage-ledger.md skills/coverage-ledger.md procedure at Stage 4 (BRD leaves × build-plan claims); the three shapes are §"Where the ledger lives", first canonical project-bin/conformance-check.sh:85-93, project-bin/coverage-preflight.sh:134-137, project-bin/verify-module.sh:299-300, project-bin/review-module.sh:289, conversion-runbook.md:487 (Stage-4 produces row) — the eleven-consumers-no-producer artifact of improvement-plan Finding 12 migration,requirements report ui-reviews 5 design/ui-reviews/ui-review-*.html skills/module-review.md pass (review-agent), one report per module close-out bin/gate-check.sh:984 (check_stage_6), bin/lib/obligations.tsv:47 (the look row — per-module coverage and denominators live THERE; this row only asks whether the stage produced any report at all) migration,requirements,greenfield,existing-app-change gate diff --git a/bin/lib/skill-routing.tsv b/bin/lib/skill-routing.tsv index 27d8707a..ab3e2207 100644 --- a/bin/lib/skill-routing.tsv +++ b/bin/lib/skill-routing.tsv @@ -188,7 +188,7 @@ tool-output-is-not-ground-truth skills/tool-output-is-not-ground-truth.md Any ti agent-permission-friction skills/agent-permission-friction.md Any refused, denied or blocked command — BEFORE rewriting a permission rule and before telling the user a tool is blocked. A rule matches the START of the command line, so an allowlisted tool prefixed with cd matches nothing all - ondemand diagnose process-coherence-pass skills/process-coherence-pass.md Checking whether the whole journey hangs together rather than each piece — finds correctly-built components nothing reaches, which per-element conformance and UI tests both miss review 5,6 ondemand verify coherence-cadence project-bin/coherence-cadence.sh After every module's CONFIRM stage — counts proven modules since the last cluster/full coherence pass and exits DUE once the threshold is reached, so the cadence isn't left to memory mdl,gate,review 5,6 ondemand verify -build-plan-status project-bin/build-plan-status.sh After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file) architect,gate,review 4,5,6 ondemand build +build-plan-status project-bin/build-plan-status.sh After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file); --refresh keeps an existing build-plan.json current but never creates one architect,gate,review 4,5,6 ondemand build e2e-evidence-report skills/e2e-evidence-report.md Turning an already-rigorous run into a narrated proof a stakeholder can trust without running anything test,review 6 ondemand verify record-demo-video skills/record-demo-video.md Recording a narrated screen-capture demo of a running app for a human to watch — opening on the app instead of a blank frame, and keeping captions synced to the pixels test 6 ondemand verify demo-recording-polish skills/demo-recording-polish.md A demo film someone will watch looks captured, not produced — 25 fps smear, scroll jumps, no visible pointer, a dense page nobody can read: 2x CDP screencast at 60 fps, drawn cursor and click ripple, eased scroll, and a zoom rendered afterwards from the click log (never a CSS transform in the page) test 6 ondemand verify diff --git a/project-bin/build-plan-status.sh b/project-bin/build-plan-status.sh index 779ae491..f96bc1b5 100755 --- a/project-bin/build-plan-status.sh +++ b/project-bin/build-plan-status.sh @@ -34,7 +34,9 @@ # by views A and B # --refresh the unattended form of --html, for exec.sh and gate-check.sh: quiet, writes only # when build-plan.md exists, never over a build-plan.html this script did not -# write, and leaves the file untouched when nothing but the timestamp would change +# write, and leaves the file untouched when nothing but the timestamp would change. +# It also keeps an EXISTING architecture/build-plan.json current (same parser as +# --json, same no-rewrite-for-a-timestamp rule) and never creates one # --json also write architecture/build-plan.json, parsed from architecture/build-plan.md's # Step-5 Phase headings, row tables and claims: blocks (see "C." below) # --quiet suppress the stdout table (useful when only --html/--json output is wanted) @@ -425,6 +427,20 @@ if [ "$WRITE_JSON" -eq 1 ] || [ "$WRITE_HTML" -eq 1 ]; then case $? in 0) PLAN_JSON="$PLAN_TARGET" JSON_MSG="wrote ${JSON_OUT#$ROOT/}" + if [ "$REFRESH" -eq 1 ] && [ "$WRITE_JSON" -eq 0 ] && [ -f "$JSON_OUT" ]; then + # --refresh keeps an EXISTING build-plan.json current (never creates one): exec.sh and + # gate-check.sh run --refresh after every build step, so a json written once at Stage 4 + # would otherwise read 0 built after a day of building. Same parser as --json; the + # write is skipped when only generatedAt would differ, like the html below. + if "$PY" -c 'import json,sys +def load(f): + d=json.load(open(f,encoding="utf-8")); d.pop("generatedAt",None); return d +sys.exit(0 if load(sys.argv[1])==load(sys.argv[2]) else 1)' "$JSON_OUT" "$PLAN_TARGET" >/dev/null 2>&1; then + JSON_MSG="${JSON_OUT#$ROOT/} already current" + else + cp -f "$PLAN_TARGET" "$JSON_OUT" 2>/dev/null && JSON_MSG="refreshed ${JSON_OUT#$ROOT/}" + fi + fi if [ "$WRITE_JSON" -eq 1 ]; then # A producer guarantees its own output is ignored (snapshot-mpr.sh's rule). The JSON # is a build product: regenerated on demand in under a second, derived entirely from diff --git a/tests/wave2/test-build-plan-json.sh b/tests/wave2/test-build-plan-json.sh index 589813d7..07410fdf 100644 --- a/tests/wave2/test-build-plan-json.sh +++ b/tests/wave2/test-build-plan-json.sh @@ -39,6 +39,8 @@ # --json, a `done-` script reads done-, and a row whose State says built while its script # is not done- is flagged. --refresh leaves an unchanged page byte-identical, never writes # over a page this script did not generate, and writes nothing without a build-plan.md. +# T11 --refresh keeps an existing build-plan.json current (a State flipped in the md shows in +# the json), rewrites nothing when only generatedAt would differ, and never creates a json. # # Usage: bash tests/wave2/test-build-plan-json.sh /path/to/build-plan-status.sh @@ -365,6 +367,33 @@ P10B="$(new_project htmlnoplan)" bash "$SCRIPT" "$P10B" --refresh >/dev/null 2>&1 [ ! -f "$P10B/architecture/build-plan.html" ] && ok "T10 --refresh writes nothing without build-plan.md" || bad "T10 --refresh wrote a page with no plan" +# ── T11: --refresh keeps an existing build-plan.json current, never creates one ── +P11="$(new_project jsonrefresh)" +cat > "$P11/architecture/build-plan.md" <<'MD' +# Build Plan + +### Phase 1 — Alpha + +| # | Kind | Step | Produces/Proves | Depends on | Skills | State | +|---|---|---|---|---|---|---| +| 1.1 | script | `01-alpha.mdl` | Alpha entity | — | — | not built | +MD +J11="$P11/architecture/build-plan.json" +bash "$SCRIPT" "$P11" --json --quiet >/dev/null 2>&1 +q "$J11" "json.dumps(d)" | grep -q 'not built' && ok "T11 --json snapshot shows the row not built" || bad "T11 setup: snapshot not 'not built'" "$(cat "$J11" 2>/dev/null)" +sed 's/| not built |/| built |/' "$P11/architecture/build-plan.md" > "$P11/md.tmp" && mv "$P11/md.tmp" "$P11/architecture/build-plan.md" +bash "$SCRIPT" "$P11" --refresh >/dev/null 2>&1 +S11="$(q "$J11" "d['phases'][0]['steps'][0]")" +case "$S11" in *"'not built'"*|*PARSE-ERROR*) bad "T11 --refresh left the json stale" "$S11" ;; *built*) ok "T11 --refresh updates the existing json to built" ;; *) bad "T11 unexpected step" "$S11" ;; esac +SUM11="$(cksum < "$J11")" +sleep 1 +bash "$SCRIPT" "$P11" --refresh >/dev/null 2>&1 +[ "$(cksum < "$J11")" = "$SUM11" ] && ok "T11 --refresh rewrites nothing when only the timestamp would change" || bad "T11 --refresh rewrote an unchanged json" +P11B="$(new_project jsonrefreshnone)" +cp "$P11/architecture/build-plan.md" "$P11B/architecture/build-plan.md" +bash "$SCRIPT" "$P11B" --refresh >/dev/null 2>&1 +[ ! -f "$P11B/architecture/build-plan.json" ] && ok "T11 --refresh does not create a json when there is none" || bad "T11 --refresh created build-plan.json" + echo "" echo "PASS=$PASS FAIL=$FAIL ($WORK)" [ "$FAIL" -eq 0 ] From eb7cc80a063abce86e4ee0965bfd3d211f5c98ed Mon Sep 17 00:00:00 2001 From: MendixMau <55099313+MendixMau@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:05:24 +0800 Subject: [PATCH 02/18] inbox: five field notes from an existing-app legacy-widget rebuild (from #309 by @AlvinnYeo) (#450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands #309 by @AlvinnYeo. Independent review: APPROVE. # PR 450 (lands #309 by @AlvinnYeo) review r1 (delta self-review) Verdict: APPROVE - w1 must-fix 1 done: every #244 status now says closed 2026-10-14 by #322, upstream root cause remains (gh issue view 244 confirms). No STOP-table wording left in the note. - w1 must-fix 2 done: body drops the stale byte-identical claim. - Inbox lane only (contrib/inbox + one CHANGELOG line, credit " — AlvinnYeo"); nothing loaded or routed; zero behaviour risk. - Optional word limits not enforced (inbox notes, maintainer call). --- CHANGELOG.md | 1 + ...0-13-durable-rebuild-kit-legacy-widgets.md | 83 +++++++++++ ...xec-keeps-write-when-mxbuild-cannot-run.md | 75 ++++++++++ ...-10-13-learned-widgets-and-data-sources.md | 135 ++++++++++++++++++ ...2026-10-13-patch-unit-row-delete-orphan.md | 43 ++++++ ...ndpoint-after-startup-left-after-signal.md | 56 ++++++++ 6 files changed, 393 insertions(+) create mode 100644 contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md create mode 100644 contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md create mode 100644 contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md create mode 100644 contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md create mode 100644 contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2899e3..7cc9b9fd 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 +- process(inbox): **five field notes from an existing-app legacy-widget rebuild** — exec.sh keeps an MDL write when mxbuild crashes on it and the next exec runs on "Baseline NOT measured"; a signal stop of `run --test-endpoint` leaves AfterStartupMicroflow on `MxTest.RegisterEndpoint` (second field case for #244); Unit-row delete refused as ORPHAN, rename-then-drop works; a durable-rebuild kit with a "restate from DESCRIBE" STOP row (DESCRIBE omits module-role and layout-row visibility: 97 role locks in 16 documents, none shown); learned entries for File Uploader image mode (CE0463), native-mapped `radiobuttons`/`image`, tab-page `CaptionParams`/`Visible:` silently dropped, BUG-39 on widget XPath, and two-hop data-source paths. Three further notes (restore-mpr usage, build-plan-status flag order, another project's mxcli test blocking exec.sh) were dropped because master already fixes them. Mendix 11.14.0 Beta, mxcli v0.25.0, toolkit c1efcb3 — AlvinnYeo - fix(build-plan-status): --refresh keeps an existing build-plan.json current, so a day of building no longer reads as 0 built — MendixMau - learn(unit-test): the unit-test skill names the per-flow `Unit flows:` list next to `Unit modules:`, so whether a flow gets a test reads the same in both places (follow-up to #435) — MendixMau - learn(load-test): **new on-demand skill for load testing a page without correlating Mendix's client protocol by hand: Playwright records the real `/xas/` calls, a probe maps each opaque `runtimeOperation` to its microflow through Jaeger, and the chosen call is replayed with the session's own cookie and CSRF token.** Ten field traps (HTTP 200 on a failed action, caught REST errors under a green microflow span, the wrong call hammered, a recording that still held the previous page, a tracing check that failed open, a harness that throttled itself, harness vs app limits, requests vs users, sampled trace counts, empty data) and a four-line report that says what the run does not prove. Routed ondemand for lead, test and review; no instrument ships yet and the roadmap's "no load testing" line is reworded. Field evidence: a warehouse demo app, Mendix 11, 16 runs 2026-09-09 to 2026-10-08, bursts of 1000 and 2000 concurrent calls on one session — MendixMau diff --git a/contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md b/contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md new file mode 100644 index 00000000..94dd1cf7 --- /dev/null +++ b/contrib/inbox/2026-10-13-durable-rebuild-kit-legacy-widgets.md @@ -0,0 +1,83 @@ +# Durable rebuild of legacy-widget pages: a four-part kit, and "restate from DESCRIBE" as a STOP item + +**From:** a field project (anonymised) — an existing Mendix app (old widgets throughout) changed with mxcli + this toolkit +**Date:** 2026-10-13 +**Kind:** process +**Field evidence:** 79 non-writable widgets on 40 live documents (DESCRIBE `-- NOT re-executable` count) +and 97 role-locked widgets in 16 documents (BSON scan; DESCRIBE showed none). 34 pages and snippets +were restated whole; the read-back afterwards found 0 non-writable widgets and 0 role locks on them. +Two drafted scripts would have silently dropped role or row visibility had they run. Mendix 11.14.0 +Beta (Classic client, Production security), mxcli v0.25.0, toolkit c1efcb3. +**Proposed target:** `skills/existing-app-change.md` (a "legacy-widget pages" section), a STOP row in +`skills/learned-mdl-preflight.md`, possibly a `project-bin/` scan once field-proven as a file + +**Currency check:** 2026-10-16 against master d1cbd41: partly stale. STOP row 21 in `skills/learned-mdl-preflight.md` now covers restating a document another script also edits. Still open: the legacy-widget section in `skills/existing-app-change.md` and the scan for role locks that DESCRIBE does not show. +--- + +## What is already covered + +- `bug-logs/mxcli-bugs.md` BUG-115 addendum: "Grep a generated script for `NOT re-executable` before + executing it" (for the File Manager widget). +- `bug-logs/mxcli-bugs.md` (the `hasRole()` entry): no MDL syntax writes `ConditionalVisibilitySettings.ModuleRoles`. +- `skills/learned-mdl-cannot-express.md` (master bda5bed, #277): "A widget visible to one role and not + another — nothing", with the execute-grant recipe for action buttons. + +## What is not covered (findings) + +1. **DESCRIBE does not print module-role visibility.** One portal page had 19 role-locked widgets; DESCRIBE + printed none of them (nor a hidden "show page" action behind 9 image widgets). A page restated from that + DESCRIBE builds clean and shows every widget to every role. +2. **A drafted restatement said "the snippet has no visibility rules".** It restated a side-menu snippet + from DESCRIBE (navigation-list items have no names, so one item cannot be dropped alone). A BSON audit + found 13 role-locked items. Running the draft would have exposed all of them, with a clean build. +3. **DESCRIBE does not print layout-grid row visibility.** Two form pages had nine detail sections shown + by type through row visibility; a snippet had 56 row-level "show only when it has a value" rules. None + appear in DESCRIBE. The drafters re-wrote them as container `Visible:` (which DESCRIBE does print and + mxcli does write); an audit confirmed every rule was carried over. +4. **Tab-page `Visible:` and `CaptionParams` are accepted and not written** (see the learned-entries note). + Found only by reading the unit back after exec. + +So "copy the page from DESCRIBE, edit, `create or replace`" is silently lossy on any app built in Studio Pro. + +## Proposed STOP row (learned-mdl-preflight.md) + +| # | Operation | Why STOP | Do instead | +|---|---|---|---| +| new | Restate an existing page, snippet or layout from `describe` output (`create or replace`, or `alter … replace` of a container) | DESCRIBE omits module-role visibility, layout-grid row/column visibility and legacy-widget internals; check, exec and mxbuild all pass while the rules vanish | Run the visibility scan (below) on the unit first. Zero role locks and zero row rules: go. Otherwise rewrite each rule as container `Visible:` or flow access, or leave the page to `alter page` / Studio Pro | + +Probe that falsifies it: in Studio Pro, set one widget visible to one module role and one layout row +visible on an attribute; `describe page`; grep the output for the role name and the attribute. Today +(v0.25.0): 0 hits for both. + +## The kit (each part was done by hand in the field; none is a toolkit file yet) + +1. **BSON visibility scan, per document.** Decode each page/snippet/layout unit under `mprcontents/` + (the toolkit's `project-bin/mxunit_bson.py` codec is enough) and count (a) any + `ConditionalVisibilitySettings` whose `ModuleRoles` list is non-empty, (b) any + `ConditionalVisibilitySettings` on a `Forms$LayoutGridRow` or `Forms$LayoutGridColumn`. Report per + document: widget name, kind, roles or attribute. The field scans were inline one-offs, not a saved + script; a shipped version needs its own fixture. +2. **Non-writable count, per document.** `describe` every page, snippet and layout in the app's own + modules (Marketplace modules excluded) and count `-- NOT re-executable` lines, grouped by the BSON + type in the comment. Field result: `Forms$DropDown` 29, `Forms$ReferenceSelector` 27, + `Forms$DataGrid` 16, sign-in widgets 4, `Forms$TemplateGrid` 1, `Forms$ImageUploader` 1, + `Forms$SidebarToggleButton` 1. +3. **Rebuild brief per document.** Inventory first (DESCRIBE plus the scan), then each widget marked + KEEP / CUT / MOVED / NEW against the wireframe, then the restatement. Replacements used: + drop-down → `combobox` on the enumeration; reference selector → `combobox` on the association (a + microflow data source when the filter needs the context object); old data grid → Data grid 2; + template grid → `listview`; image uploader → File Uploader in files mode plus a `dynamicimage`; + role-only visibility → a per-viewer flag from a data-source microflow, or flow access (Mendix hides a + button whose flow the user cannot run). The restating script becomes the document's single owner; + older `alter page` blocks on it are marked superseded (compare #269). +4. **Post-exec read-back.** After each exec: DESCRIBE again (0 `NOT re-executable`), rerun the scan + (0 role locks unless intended), and grep the unit for every `Visible:` / `CaptionParams` the script + wrote. Item 4 above was caught only this way. + +Cost, for scale: six drafting agents in parallel; one gated exec per document (about 5–10 minutes each +with mxbuild and lint), serial because there is one writer to the `.mpr`. + +Upstream asks (mxcli), for the maintainer to file or link: print module-role and row/column visibility in +DESCRIBE (or at least a `-- role-based visibility not shown (N roles)` comment), accept it on write, give +navigation-list items stable names, and a legacy-widget migration (`DataGrid` → Data grid 2, +`ReferenceSelector`/`DropDown` → combobox) behind a dry run. diff --git a/contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md b/contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md new file mode 100644 index 00000000..3ebeb7f3 --- /dev/null +++ b/contrib/inbox/2026-10-13-exec-keeps-write-when-mxbuild-cannot-run.md @@ -0,0 +1,75 @@ +# exec.sh keeps an MDL write when mxbuild crashes after it, and the next exec runs on an unloadable model + +**From:** a field project (anonymised) — an existing Mendix app being changed with mxcli + this toolkit +**Date:** 2026-10-13 +**Kind:** bug +**Field evidence:** the project's BUILD-LOG shows two "gate could not run" rows 46 s apart: the +script that made the model unloadable, then the next script, which exec.sh accepted and ran on top +of it. Mendix 11.14.0 Beta (Classic client), mxcli v0.25.0, toolkit commit c1efcb3 (`project-bin/exec.sh` +is byte-identical on master 0c62f6f, blob a03adec). +**Proposed target:** `project-bin/exec.sh` (gate branch "mxbuild failed to run" and the pre-flight +"Baseline NOT measured" branch); one line in `skills/iterative-build-loop.md` + +--- + +## What happened (finding, reproduced once in the field) + +A page script set a data view's data source to a two-hop association path in its full form +(`$Parent/MyModule.A_B/MyModule.B/MyModule.B_C/MyModule.C`). `mxcli check` passed. After the +exec, the mxbuild loader itself crashed instead of reporting CE errors: + +``` +An error occurred when trying to set the 'DestinationEntity' property of a Entity ref step in a Page. +``` + +exec.sh printed: + +``` + ✗ mxbuild failed to run (exit 1) — gate could not verify the model. + → Snapshot preserved. Open in SP to verify manually before proceeding. +``` + +and kept the write. It did exit 1 and cleared the verification stamp (good), but the agent driving +the build ran the next script. exec.sh's pre-flight printed `⚠ Baseline NOT measured: mxbuild exited 1 +without checking the model.`, carried on, applied the next script, and hit the same crash. + +BUILD-LOG rows, verbatim except the script names: + +``` +| 2026-10-13T12:26:18 | `25g-PageA-rebuild.mdl` | fail | ❌ gate failed | 5 error(s): CE1613 — script rolled back · approval: auto (PROJECT.md) | +| 2026-10-13T12:28:46 | `25g-PageA-rebuild.mdl` | fail | ❌ gate could not run | mxbuild exit 1 · approval: auto (PROJECT.md) | +| 2026-10-13T12:29:32 | `25h-SnippetB-rebuild.mdl` | fail | ❌ gate could not run | mxbuild exit 1 · approval: auto (PROJECT.md) | +``` + +Recovery: `./bin/restore-mpr.sh ` then +`./bin/verify-model.sh` (0 errors). The 25h row had to be re-run afterwards. + +## Why it matters + +- The header comment of exec.sh says an unverified gate is rolled back **for `--patch` only**. That is + true and deliberate, but for MDL the "kept" case was designed for "mxbuild could not start" + (wrong version, no JDK). Here mxbuild started, ran on the baseline cleanly a minute earlier, and + crashed only on the written model. That is a property of the write, not of the machine. +- The next exec has no way to tell the two apart: "Baseline NOT measured" is a warning, not a refusal. + So in an unattended chain the damage compounds: every following script is written onto a model + nothing can load, and each one gets its own snapshot of the broken state. + +## Proposed fix (hypothesis, not built) + +1. **Restore when mxbuild ran clean on the baseline but cannot run after the write.** exec.sh already + has both facts: the pre-flight measured a baseline (`BASE_KNOWN=1`) and the post-exec run exits + non-zero with no errors file. In that case, call `restore_snapshot` exactly as the `--patch` path + does, and log `↩ rolled back: mxbuild crashed on the written model`. Keep today's behaviour when the + baseline itself was not measured (that is the real "machine cannot build" case). +2. **Refuse a write when the baseline cannot be measured** and the last BUILD-LOG row for this model + is `gate could not run`, with one STOP line naming `bin/restore-mpr.sh` and the snapshot to restore. + `ALLOW_UNVERIFIED=1` stays the explicit override. +3. Reword the kept-write message: "Open in SP to verify" is not something an agent can do. Something + like `STOP: the model may not load. Restore with ./bin/restore-mpr.sh ` is actionable. + +Probe that would falsify the claim (for the retest fixture): a fake mxbuild that exits 0 on the +baseline and exits 1 with no errors file after the exec; today exec.sh exits 1 and leaves the +written model in place; after the fix the model on disk equals the snapshot. + +Related: `--patch` already restores on an unverified gate (header lines 25–29); PR #304 touches the +same file (per-project lock) but not this branch. diff --git a/contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md b/contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md new file mode 100644 index 00000000..e5d7d710 --- /dev/null +++ b/contrib/inbox/2026-10-13-learned-widgets-and-data-sources.md @@ -0,0 +1,135 @@ +# Learned entries from a legacy-widget rebuild: File Uploader image mode, native-mapped keywords, tab pages, XPath comparands, multi-hop data sources + +**From:** a field project (anonymised) +**Date:** 2026-10-13 +**Kind:** learning +**Field evidence:** each entry below was hit while restating 34 pages of an existing app, through +`bin/exec.sh` (mxbuild delta gate) on mxcli v0.25.0, Mendix 11.14.0 Beta, Classic (Dojo) client, +toolkit c1efcb3. Error texts are verbatim; module/entity names are genericised. +**Proposed target:** per entry below (`skills/learned-file-upload-widget.md`, +`skills/learned-mdl-cannot-express.md`, `skills/learned-page-patterns.md`, `bug-logs/mxcli-bugs.md`) + +--- + +## 1. File Uploader: image mode fails the build; files mode against a `System.Image` subtype works + +Target: `skills/learned-file-upload-widget.md` §7 ("Image mode … was not tested"). +Status: files mode is ALREADY covered there (PR #139); this adds image mode and the image-preview pattern. + +```sql +fileuploader upPictures ( + uploadMode: 'images', + associatedImages: association $currentObject/MyModule.Picture_Asset, + readOnlyMode: true, + enableCustomButtons: false +) +``` + +`mxcli check` passed. mxbuild: `CE0463 The definition of this widget has changed.` on +`File uploader upPictures`; the gate rolled back (rollback verified by DESCRIBE). The same widget with +`uploadMode: 'files'` builds clean (finding). + +What built clean instead (finding, mxbuild 0 errors): uploads in **files** mode against an entity that +extends `System.Image` (which extends `FileDocument`), with a separate `dynamicimage` preview beside it; +read-only picture lists as a `listview` whose template holds a `dynamicimage`. + +Also stale: `skills/learned-mdl-cannot-express.md` row 1 still says "A file upload control — nothing … +⛔ not buildable", which contradicts `learned-file-upload-widget.md`. Suggest pointing that row at the +File Uploader skill. + +Related (finding): the installed FileDropper widget's definition lists `dataFileEntity` and +`dataContextAssociation` as known properties with no mapping; `check` accepts them, exec does not write +them, and mxbuild then fails. Upstream ask: refuse unmapped definition properties with MDL-WIDGET07. + +## 2. `radiobuttons` and `image` can resolve to NATIVE mobile widgets on a web page + +Target: `skills/learned-page-patterns.md` (or the widgets section of `learned-mdl-cannot-express.md`). +Status: NEW (no toolkit mention). + +In this project `DESCRIBE WIDGET TYPE radiobuttons` resolves to `com.mendix.widget.native.radiobuttons`, +and `check` warns MDL-WIDGET10 that the label is dropped. The project's `image.def.json` maps `image` to +`com.mendix.widget.native.image.Image`; a probe wrote `image` with no properties. DESCRIBE prints existing +**web** radio groups with the same `radiobuttons` keyword, so re-running DESCRIBE output writes a native +widget into a web page (finding for the resolution and the warning; the radiobuttons case was not taken +through mxbuild). + +Rule used: Boolean → `checkbox`, enumeration → `combobox`, images → `dynamicimage`. Before relying on a +keyword in an existing app, run `DESCRIBE WIDGET TYPE ` and check the id is a `web` widget. + +## 3. Tab pages: `CaptionParams` and `Visible:` pass every gate and are not written + +Target: `bug-logs/mxcli-bugs.md` as `## BUG-DRAFT-tabpage-captionparams-visible-dropped:`, plus a row in +`learned-mdl-cannot-express.md` next to the existing "count badge on a tab" row. +Status: NEW. (The existing row covers `Badge:`, which `check` refuses; these two are accepted.) + +```sql +tabpage tpDue (Caption: 'Due today or earlier · {1}', CaptionParams: ({1} = DueCount)) { … } +tabpage tpOther (Caption: 'Unassigned · {1}', Visible: $currentObject/ShowOtherTab) { … } +``` + +`check`, exec and mxbuild all passed, with no MDL-WIDGET07. The stored caption is literal text (the tab +reads `· {1}`), and the tab page has no visibility setting: `ShowOtherTab` occurs 0 times in the unit. +Found by a BSON read-back after exec (finding). + +Rule: no dynamic captions and no visibility on tab pages; put the count in a line inside the tab, and +put anything conditional in a container with `Visible:` (container visibility is written correctly). + +## 4. DESCRIBE omits module-role visibility and layout-grid row visibility + +Target and detail: see the inbox note "Durable rebuild of legacy-widget pages" (same date). Listed +here so triage sees it among the learned entries. Status: PARTLY (role visibility is known to be +unwritable; that DESCRIBE also hides it is not recorded). + +## 5. Page XPath with a `$currentObject/` comparand → CE0161 (BUG-39 on widgets) + +Target: `bug-logs/mxcli-bugs.md` BUG-39 (add a confirmation), `learned-page-patterns.md`. +Status: PARTLY. BUG-39 records it for microflow retrieves only. + +```sql +combobox cbLevel2 ( + Attribute: Helper_Level2, + DataSource: database from "MyModule"."Level2" + where [MyModule.Level2_Level1 = $currentObject/MyModule.Helper_Level1] +) +``` + +`check --references` passed; mxbuild: 3 × `CE0161 Error(s) in XPath constraint`, one per cascading combo +box; rolled back (finding). Studio Pro accepts the same constraint. + +Workaround that built: each combo box takes its options from a microflow data source that retrieves the +parent into a variable and filters on it. + +## 6. Two-hop association data source: neither path form is writable → use a microflow data source + +Target: `learned-page-patterns.md` (next to the BUG-04 multi-hop ContentParams section), and +`bug-logs/mxcli-bugs.md` as `## BUG-DRAFT-two-hop-datasource-path:`. +Status: NEW. BUG-20 (cross-module traversal writing a null `DestinationEntityId`) was fixed in v0.17.0; +this is a same-module, two-hop path on v0.25.0. + +Short form, exactly as DESCRIBE prints it: + +```sql +DataSource: $Parent/MyModule.A_B/MyModule.B_C +``` + +mxcli stored the last segment as an entity: +`CE1613 The selected entity 'MyModule.B_C' no longer exists.` (5 errors); rolled back. + +Full form: + +```sql +DataSource: $Parent/MyModule.A_B/MyModule.B/MyModule.B_C/MyModule.C +``` + +`check` passed; the mxbuild loader crashed: +`An error occurred when trying to set the 'DestinationEntity' property of a Entity ref step in a Page.` +(This crash is what exposed the exec.sh gap in the separate inbox note.) + +So DESCRIBE output containing such a path is not re-executable. Rule: no multi-hop association path as a +data-view source; use a microflow data source that retrieves over each association in turn (built clean). + +## Not included here + +Other cases from the same run (Data grid 2 custom-content column losing its Attribute; XPath through a +reverse association needing read access mxbuild does not check; names `check --references` does not +validate) are upstream mxcli material and are left for a separate note. diff --git a/contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md b/contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md new file mode 100644 index 00000000..9355c51b --- /dev/null +++ b/contrib/inbox/2026-10-13-patch-unit-row-delete-orphan.md @@ -0,0 +1,43 @@ +# exec.sh --patch: deleting a Unit row is refused by `mxcli fix hashes --repair` (ORPHAN file); rename-then-drop is the route that works + +**From:** a field project (anonymised) +**Date:** 2026-10-12 +**Kind:** learning +**Field evidence:** two pages with the same qualified name (a live one and an excluded, archived copy in +another folder). mxcli v0.25.0, Mendix 11.14.0 Beta, MPR v2, toolkit c1efcb3. The failed patch was +rolled back by exec.sh; the second route built at 0 errors. +**Proposed target:** `skills/learned-mdl-preflight.md` (a workaround row) or `skills/learned-mdl-cannot-express.md`; +an upstream mxcli ask + +--- + +## Situation + +`MyModule.Detail` existed twice: the live page and an archived copy (excluded from the project) with +the same name in `Archive/Pages`. mxcli addresses documents by qualified name only, and different +commands resolved the name to different copies: `DESCRIBE PAGE` found the live one, `SHOW ACCESS ON PAGE` +the archived one, so a `grant view` by name landed on the archived copy. There is no `drop page … in +folder '…'` and no drop by unit id. + +## Route 1, refused (finding) + +A gated patch through `./bin/exec.sh --patch` deleted the archived page's row from the `.mpr` `Unit` +table and left its `.mxunit` file on disk (file removal being left to the owner). The patch's +`mxcli fix hashes --repair` step then reported the file as **ORPHAN** (present on disk, not indexed) +and failed; exec.sh restored the snapshot. Afterwards both pages were present and 3161 units verified. + +## Route 2, worked (finding) + +1. A gated patch renamed only the archived unit's top-level `Name` to `Detail_Archived`, after a + byte-identical decode/encode round-trip check of that unit. mxbuild stayed at 0 errors. +2. Then plain MDL, through exec.sh: `drop page if exists "MyModule"."Detail_Archived";` + +## Rule proposed + +To remove one of two same-named documents: rename it by patch, then drop it by MDL. Never delete a +`Unit` row by hand; `fix hashes --repair` treats the leftover file as an orphan and the gate rolls back. + +Ask for upstream mxcli: warn when a qualified name resolves to more than one document, make every +command resolve it the same way, and add `drop page … in folder '…'` or a drop by unit id. + +Related, not the same: #294 (a shared patch helper) and #295 (adopt `mxcli fix hashes` in `--patch`). diff --git a/contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md b/contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md new file mode 100644 index 00000000..a2e11133 --- /dev/null +++ b/contrib/inbox/2026-10-13-test-endpoint-after-startup-left-after-signal.md @@ -0,0 +1,56 @@ +# `mxcli run --local --test-endpoint` stopped by a signal leaves AfterStartupMicroflow on the injected flow (second field case for #244, and the toolkit-side half) + +**From:** a field project (anonymised) +**Date:** 2026-10-13 +**Kind:** bug +**Field evidence:** an agent stopped a running `mxcli run --local --test-endpoint` (a background-task +stop, so a signal, not a clean exit) to restart the app. Later a clean-up removed the injected +`MxTest` module but not the setting; exec.sh's pre-flight then reported the model "ALREADY has 1 error" +(CE1613), and the next app start recursed with a StackOverflowError in `MxTest.RegisterEndpoint`. +mxcli v0.25.0, Mendix 11.14.0 Beta, toolkit c1efcb3. +**Proposed target:** upstream mxcli for the root cause (already logged here as #244, closed 2026-10-14 by #322 (toolkit refusal); the upstream root cause remains); in the +toolkit, `skills/testing-shape.md` (the `--test-endpoint` paragraph) and an exec.sh pre-flight hint + +**Currency check:** 2026-10-16 against master d1cbd41: partly stale. exec.sh's pre-flight now refuses on a leftover injected setting and prints the `ALTER SETTINGS` repair (`_common.sh`), so the exec.sh hint half is done. Still open: the upstream root cause behind #244 closed 2026-10-14 by #322 (toolkit refusal); the upstream root cause remains, and `skills/testing-shape.md` does not say that a stop by signal skips the restore. +--- + +## What is already covered + +- #244 (closed 2026-10-14 by #322 (toolkit refusal); the upstream root cause remains) describes the same family on mxcli v0.25.0 / Mendix 11.15.0: the wrapper chains the + "current" after-startup flow, which is already `MxTest.RegisterEndpoint`, so it calls itself. It + asks upstream to restore on interrupt and to refuse a self-wrap. +- `skills/testing-shape.md` says `--test-endpoint` writes into the working model while the app runs and + that "a clean stop prints 'test endpoint removed; project restored'". It does not say that a stop by + signal skips that restore. + +## What this case adds + +1. **Trigger is a signal stop of `run --test-endpoint` itself**, not an interrupted `mxcli test` run. + Agents routinely stop background tasks this way to restart an app. +2. **A partial clean-up makes it worse.** Removing the `MxTest` module afterwards (by hand or by a later + cleanup) leaves `AfterStartupMicroflow` pointing at a flow that no longer exists: CE1613 at build, + which exec.sh's delta gate then carries as a "pre-existing" error on every later script. Re-creating + it by starting with `--test-endpoint` again gives the self-recursion from #244. +3. **The repair that worked**, run through `bin/exec.sh` (an `ALTER SETTINGS`, said in chat): + + ```sql + mdl 1; + alter settings runtime (AfterStartupMicroflow: 'MyModule.ASU_Startup'); + ``` + + mxbuild went back to 0 errors; the app then started normally. + +## Proposed toolkit changes (hypothesis, not built) + +- `testing-shape.md`: add "Stop `run --test-endpoint` with Ctrl+C in its own terminal or let it exit; + a kill or task stop skips the restore. Afterwards `git status` on the model must be empty; if the + Settings unit is still modified, restore it from git (or with the `alter settings runtime` line + above) before any exec." Run build-time apps without `--test-endpoint` (the skill already says so). +- `exec.sh` pre-flight: when the baseline holds CE1613 on the project Settings, or `describe settings` + shows `AfterStartupMicroflow` starting with `MxTest.`, print one line naming the cause and the repair, + instead of only "Model ALREADY has 1 error(s)". +- Upstream (#244): restore the original after-startup on SIGTERM/SIGINT too, and refuse to wrap an + after-startup that is already `MxTest.RegisterEndpoint`. + +Probe: start `mxcli run --local --test-endpoint` on a scratch copy, stop it with `kill -TERM `, +then `describe settings`. Today (v0.25.0): after-startup is still `MxTest.RegisterEndpoint`. From 591b0536af449122cb16ed4363b3647bf39fcd43 Mon Sep 17 00:00:00 2001 From: MendixMau <55099313+MendixMau@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:33:15 +0800 Subject: [PATCH 03/18] learn(multilingual-app): source language, enabled languages, starting language, a switch a normal role can use (from #387 by @CarlesZ) (#449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands #387 by @CarlesZ. Independent review: APPROVE. # PR 449 (lands #387 by @CarlesZ) review r1 (delta self-review) Verdict: APPROVE - Content identical to #387 head reviewed APPROVE in 387-w1.md; delta = merge from release/v1.0, routing re-render (--check in sync), CHANGELOG credit " — CarlesZ" and roles "architect, mdl + review". - Squash message scoped; no "not grantable"/"cannot be granted". - Leak guard, private citations, PR discipline clean. --- CHANGELOG.md | 1 + README.md | 1 + ROUTING.md | 1 + agents/architect-agent.md | 1 + agents/mdl-agent.md | 1 + agents/review-agent.md | 1 + bin/gate-check.sh | 6 +- bin/lib/skill-routing.tsv | 1 + skills/learned-mdl-cannot-express.md | 1 + skills/learned-multilingual-app.md | 187 +++++++++++++++++++++++++++ 10 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 skills/learned-multilingual-app.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc9b9fd..27bd4957 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 +- learn(multilingual-app): **a second language is four decisions the translations never see — source language, enabled languages, the user's starting language, and a switch a normal role can use.** New `skills/learned-multilingual-app.md` (routed on demand, stages 3/5/6, architect, mdl + review). Field run 2026-10-09 (Mendix 11.15.0, mxcli v0.25.0, an app authored in Spanish gaining ES + PT): 418 strings per language written correctly, and still the language picker raised an error, showed a blank label and changed nothing, with check, exec.sh and lint green. Causes: captions authored under the default `en_US` (so `en_US` is the authored language); demo users on that hidden source language; the Marketplace LanguageSelector writing `System.User_Language` from the browser, which this project's non-admin role could not, with no grant route found (mxcli `check` passes the grant, `exec` refuses it; Studio Pro does not offer the member, so the first fix — a Studio Pro hand step — was impossible; a probe on the `System.User` module role now comes first); `login.html` outside the translation system. Recipe: a microflow sets `$currentUser`'s language, a nanoflow reloads, CSS highlights from ``; seeded users start in a visible language. `learned-mdl-cannot-express.md` gains the LanguageSelector row. — CarlesZ - process(inbox): **five field notes from an existing-app legacy-widget rebuild** — exec.sh keeps an MDL write when mxbuild crashes on it and the next exec runs on "Baseline NOT measured"; a signal stop of `run --test-endpoint` leaves AfterStartupMicroflow on `MxTest.RegisterEndpoint` (second field case for #244); Unit-row delete refused as ORPHAN, rename-then-drop works; a durable-rebuild kit with a "restate from DESCRIBE" STOP row (DESCRIBE omits module-role and layout-row visibility: 97 role locks in 16 documents, none shown); learned entries for File Uploader image mode (CE0463), native-mapped `radiobuttons`/`image`, tab-page `CaptionParams`/`Visible:` silently dropped, BUG-39 on widget XPath, and two-hop data-source paths. Three further notes (restore-mpr usage, build-plan-status flag order, another project's mxcli test blocking exec.sh) were dropped because master already fixes them. Mendix 11.14.0 Beta, mxcli v0.25.0, toolkit c1efcb3 — AlvinnYeo - fix(build-plan-status): --refresh keeps an existing build-plan.json current, so a day of building no longer reads as 0 built — MendixMau - learn(unit-test): the unit-test skill names the per-flow `Unit flows:` list next to `Unit modules:`, so whether a flow gets a test reads the same in both places (follow-up to #435) — MendixMau diff --git a/README.md b/README.md index 32d73a18..7fc9c472 100644 --- a/README.md +++ b/README.md @@ -605,6 +605,7 @@ Every mxcli project has a `.ai-context/skills/` directory (bundled by `mxcli ini |---|---| | Dispatching a build step to a helper — generate its one-file pack (the brief's Build steps row, mxcli brain brief, live DESCRIBEs, example, folder) and hand over the path instead of a reading list. Measured: 25-38% fewer tool calls and 30-41% less time, same quality; the token saving is not proven | `project-bin/context-pack.sh` | | An exec.sh guard refused a write (not a permission prompt), or building a module with mxcli — verified, iterative, coverage-checklist gated; on a refusal: commit, wait or stop, never FORCE_EXEC | `skills/iterative-build-loop.md` | +| Adding a second language, a language picker or a translation script — and the moment a language picker errors, shows a blank label or changes nothing: source language, enabled languages, the user's starting language, a switch a non-admin role can use | `skills/learned-multilingual-app.md` | | After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file); --refresh keeps an existing build-plan.json current but never creates one | `project-bin/build-plan-status.sh` | | Turning a client-derived Mendix app into a clean, shareable demo with zero client fingerprint — branding, data, custom widgets | `skills/anonymize-client-app-for-demo.md` | | Handing a headless-built model to a person — opening it in Studio Pro, a free sandbox, or a colleague's machine: the model travels, the demo data and runtime config (keys, an agent's bound model) do not, and each needs its own re-establish step | `skills/handoff-to-studio-pro.md` | diff --git a/ROUTING.md b/ROUTING.md index e75816da..024dedee 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -144,6 +144,7 @@ picks the row up. That is the whole procedure — there is no second list to rem | Before any mxcli exec / exec.sh / --mcp write — ask or run? the knob decides | `bin/exec-approval.sh` | mdl,gate | 5,6 | baseline | | Dispatching a build step to a helper — generate its one-file pack (the brief's Build steps row, mxcli brain brief, live DESCRIBEs, example, folder) and hand over the path instead of a reading list. Measured: 25-38% fewer tool calls and 30-41% less time, same quality; the token saving is not proven | `project-bin/context-pack.sh` | mdl | 5 | ondemand | | An exec.sh guard refused a write (not a permission prompt), or building a module with mxcli — verified, iterative, coverage-checklist gated; on a refusal: commit, wait or stop, never FORCE_EXEC | `skills/iterative-build-loop.md` | mdl,gate | 5 | ondemand | +| Adding a second language, a language picker or a translation script — and the moment a language picker errors, shows a blank label or changes nothing: source language, enabled languages, the user's starting language, a switch a non-admin role can use | `skills/learned-multilingual-app.md` | architect,mdl,review | 3,5,6 | ondemand | | After marking a module done, or any time "how much is built vs proven" is asked — renders build-plan.html from done- prefixes and verify-module.sh/improvement-register.md, kept as two honestly separate views; --json writes architecture/build-plan.json parsed from build-plan.md's Phase headings (a plan with no Phase headings gets no file); --refresh keeps an existing build-plan.json current but never creates one | `project-bin/build-plan-status.sh` | architect,gate,review | 4,5,6 | ondemand | | Turning a client-derived Mendix app into a clean, shareable demo with zero client fingerprint — branding, data, custom widgets | `skills/anonymize-client-app-for-demo.md` | mdl,review | 6 | ondemand | | Handing a headless-built model to a person — opening it in Studio Pro, a free sandbox, or a colleague's machine: the model travels, the demo data and runtime config (keys, an agent's bound model) do not, and each needs its own re-establish step | `skills/handoff-to-studio-pro.md` | test,review | 6,7 | ondemand | diff --git a/agents/architect-agent.md b/agents/architect-agent.md index a651dda6..de060966 100644 --- a/agents/architect-agent.md +++ b/agents/architect-agent.md @@ -73,6 +73,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/brd-to-build-plan.md` | Turning BRDs plus architecture into a numbered, dependency-ordered build plan | | `skills/coverage-ledger.md` | Building the Stage 4 coverage ledger | | `bin/coverage-check.sh` | Checking a coverage ledger against its BRD | +| `skills/learned-multilingual-app.md` | Adding a second language, a language picker or a translation script | | `skills/mendix-agents.md` | Building a Mendix AI agent | | `skills/company-brain.md` | Setting up or wiring a COMPANY BRAIN | | `skills/close-the-loop.md` | Cutover and retrospective | diff --git a/agents/mdl-agent.md b/agents/mdl-agent.md index ccdd7709..8d769e90 100644 --- a/agents/mdl-agent.md +++ b/agents/mdl-agent.md @@ -77,6 +77,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/mdl-cookbook-microflows.md` | Writing MDL microflow scripts | | `skills/build/mdl/oneshot-mdl-method.md` | Writing a single MDL script that takes a project from nothing to a working vertical slice | | `skills/learned-page-patterns.md` | Building and auditing Mendix pages, or starting a UI fix round across many pages | +| `skills/learned-multilingual-app.md` | Adding a second language, a language picker or a translation script | | `skills/oneshot-page-structure-patterns.md` | Generating a whole page tree in one script | | `skills/mendix-agents.md` | Building a Mendix AI agent | | `skills/mendix-agent-ui.md` | Embedding a copilot chat panel | diff --git a/agents/review-agent.md b/agents/review-agent.md index 21a2677c..27fbd5a4 100644 --- a/agents/review-agent.md +++ b/agents/review-agent.md @@ -65,6 +65,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/module-dependency-review.md` | Judging module tangles, bidirectional pairs, cohesion and hubs from analysis/app-facts/dependencies.json; stock graph_module_* views split names on the first dot and are not trusted | | `skills/microflow-loop-antipatterns.md` | Reading what loop bodies do (LOOP_TQ, deferred commit, nested loop, REST in loop, transaction control per item, scheduled-event reachability) from described MDL; the catalog holds top-level activities only and cannot see inside a loop | | `skills/process-map.md` | At Stage 2 close (draft, before the questions are raised) and again after the wireframes (Stage 3 re-render), when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane map per process, each step on its wireframe, every question pinned to a step (project-bin/process-map.js validate, anchor, build) | +| `skills/learned-multilingual-app.md` | Adding a second language, a language picker or a translation script | | `project-bin/conformance-check.sh` | Running the ledger rung alone | | `project-bin/graph-sweep.sh` | Running the wiring rung alone | | `skills/fixture-seeding.md` | Seed data, test data, demo data or fixtures are needed, or pages come up empty | diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 78a62a9a..accd8dc6 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1787,10 +1787,10 @@ stage_protocol_paths() { 0) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-scope.md skills/source-triage.md skills/image-transcription.md skills/small-project-tier.md skills/existing-app-change.md skills/decompose-app.md skills/app-analysis.md skills/layering-review.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/assess-migration.md skills/migration-pipeline.md skills/rehost-app.md skills/migrate-general.md skills/migrate-outsystems.md skills/source-os11.md skills/os-xml-schema.md skills/source-node-express-react.md skills/document-discovery.md skills/extractor-quality-loop.md skills/qa-loop-goal-pattern.md skills/mendix-epics-api.md skills/corpus-extraction-integrity.md skills/gate-check-file-locations.md" ;; 1) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-extraction.md skills/dispatching.md skills/image-transcription.md skills/small-project-tier.md skills/migration-pipeline.md skills/rehost-app.md skills/source-os11.md skills/os-xml-schema.md skills/source-node-express-react.md skills/document-discovery.md skills/extractor-quality-loop.md skills/kb-generation.md skills/corpus-extraction-integrity.md" ;; 2) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-brd.md skills/checkpoints/checkpoint-architecture.md skills/dispatching.md skills/image-transcription.md skills/small-project-tier.md skills/kb-generation.md skills/brd-generation.md skills/brd-validation.md" ;; - 3) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-design.md skills/dispatching.md skills/small-project-tier.md skills/mendix-best-practices-index.md skills/decompose-app.md skills/mendix-service-seam.md skills/layering-review.md skills/architecture-blueprint.md skills/modularize-domain.md skills/design-artifacts.md skills/process-map.md skills/interaction-model.md skills/brd-to-build-plan.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/learned-mdl-cannot-express.md" ;; + 3) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-design.md skills/dispatching.md skills/small-project-tier.md skills/mendix-best-practices-index.md skills/decompose-app.md skills/mendix-service-seam.md skills/layering-review.md skills/architecture-blueprint.md skills/modularize-domain.md skills/design-artifacts.md skills/process-map.md skills/interaction-model.md skills/brd-to-build-plan.md skills/learned-multilingual-app.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/learned-mdl-cannot-express.md" ;; 4) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-build.md skills/agent-roles.md skills/dispatching.md skills/small-project-tier.md skills/module-brief.md skills/module-folder-convention.md skills/brd-to-build-plan.md skills/coverage-ledger.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/learned-constants-and-secrets.md" ;; - 5|build-ready) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/agent-roles.md skills/dispatching.md skills/module-brief.md skills/learned-mdl-preflight.md skills/module-folder-convention.md skills/learned-microflow-patterns.md skills/microflow-preflight.md skills/mendix-best-practices-index.md skills/ui-preflight-pages.md skills/design-spacing.md skills/learned-stylegallery.md skills/ui-loop.md skills/module-review.md skills/testing-shape.md skills/decompose-app.md skills/mendix-service-seam.md skills/microflow-loop-antipatterns.md skills/rehost-app.md skills/iterative-build-loop.md skills/mdl-cookbook-microflows.md skills/build/mdl/oneshot-mdl-method.md skills/learned-page-patterns.md skills/oneshot-page-structure-patterns.md skills/mendix-agents.md skills/mendix-agent-ui.md skills/mendix-agent-setup.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/load-test.md skills/unit-test.md skills/e2e-locators-that-lie.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/learned-workflow-patterns.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/learned-sidebar-collapse-icons.md skills/learned-popup-navigation.md skills/learned-datagrid-customcontent-binding.md skills/learned-popup-feedback-pattern.md skills/learned-mdl-cannot-express.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-dg2-patterns.md skills/learned-file-upload-widget.md skills/security-is-not-a-later-script.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/preview-over-hub-tunnel.md skills/walking-skeleton.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; - 6) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/dispatching.md skills/mendix-best-practices-index.md skills/module-review.md skills/testing-shape.md skills/existing-app-assurance.md skills/decompose-app.md skills/app-analysis.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/rehost-app.md skills/qa-loop-goal-pattern.md skills/mendix-agent-setup.md skills/e2e-harness-base.md skills/learned-db-assertions.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/load-test.md skills/unit-test.md skills/e2e-locators-that-lie.md skills/learned-skill-ux-audit.md skills/learned-skill-scope-delta.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/e2e-evidence-report.md skills/record-demo-video.md skills/demo-recording-polish.md skills/share-demo-package.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/anonymize-client-app-for-demo.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/handoff-to-studio-pro.md skills/preview-over-hub-tunnel.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; + 5|build-ready) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/agent-roles.md skills/dispatching.md skills/module-brief.md skills/learned-mdl-preflight.md skills/module-folder-convention.md skills/learned-microflow-patterns.md skills/microflow-preflight.md skills/mendix-best-practices-index.md skills/ui-preflight-pages.md skills/design-spacing.md skills/learned-stylegallery.md skills/ui-loop.md skills/module-review.md skills/testing-shape.md skills/decompose-app.md skills/mendix-service-seam.md skills/microflow-loop-antipatterns.md skills/rehost-app.md skills/iterative-build-loop.md skills/mdl-cookbook-microflows.md skills/build/mdl/oneshot-mdl-method.md skills/learned-page-patterns.md skills/learned-multilingual-app.md skills/oneshot-page-structure-patterns.md skills/mendix-agents.md skills/mendix-agent-ui.md skills/mendix-agent-setup.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/load-test.md skills/unit-test.md skills/e2e-locators-that-lie.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/learned-workflow-patterns.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/learned-sidebar-collapse-icons.md skills/learned-popup-navigation.md skills/learned-datagrid-customcontent-binding.md skills/learned-popup-feedback-pattern.md skills/learned-mdl-cannot-express.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-dg2-patterns.md skills/learned-file-upload-widget.md skills/security-is-not-a-later-script.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/preview-over-hub-tunnel.md skills/walking-skeleton.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; + 6) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/dispatching.md skills/mendix-best-practices-index.md skills/module-review.md skills/testing-shape.md skills/existing-app-assurance.md skills/decompose-app.md skills/app-analysis.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/rehost-app.md skills/qa-loop-goal-pattern.md skills/learned-multilingual-app.md skills/mendix-agent-setup.md skills/e2e-harness-base.md skills/learned-db-assertions.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/load-test.md skills/unit-test.md skills/e2e-locators-that-lie.md skills/learned-skill-ux-audit.md skills/learned-skill-scope-delta.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/e2e-evidence-report.md skills/record-demo-video.md skills/demo-recording-polish.md skills/share-demo-package.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/anonymize-client-app-for-demo.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/handoff-to-studio-pro.md skills/preview-over-hub-tunnel.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; 7) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/close-the-loop.md skills/share-demo-package.md skills/handoff-to-studio-pro.md skills/platform-link.md skills/teamserver-alignment.md skills/deploy-to-sandbox.md" ;; *) echo "" ;; # diff --git a/bin/lib/skill-routing.tsv b/bin/lib/skill-routing.tsv index ab3e2207..81d28c6f 100644 --- a/bin/lib/skill-routing.tsv +++ b/bin/lib/skill-routing.tsv @@ -161,6 +161,7 @@ iterative-build-loop skills/iterative-build-loop.md An exec.sh guard refused a w mdl-cookbook-microflows skills/mdl-cookbook-microflows.md Writing MDL microflow scripts — worked recipes mdl 5 ondemand build/mdl oneshot-mdl-method skills/build/mdl/oneshot-mdl-method.md Writing a single MDL script that takes a project from nothing to a working vertical slice — execution order, why it is deliberately non-idempotent, the instrument hierarchy, and the silent failures that pass every check mdl 5 ondemand build/mdl learned-page-patterns skills/learned-page-patterns.md Building and auditing Mendix pages, or starting a UI fix round across many pages — widget patterns, datasource shapes; for a round: SCSS first, four regressions to check mdl 5 ondemand build/pages +learned-multilingual-app skills/learned-multilingual-app.md Adding a second language, a language picker or a translation script — and the moment a language picker errors, shows a blank label or changes nothing: source language, enabled languages, the user's starting language, a switch a non-admin role can use architect,mdl,review 3,5,6 ondemand build oneshot-page-structure-patterns skills/oneshot-page-structure-patterns.md Generating a whole page tree in one script — the structure patterns that survive it mdl 5 ondemand build/pages mendix-agents skills/mendix-agents.md Building a Mendix AI agent — the agent is runtime data not a model document, so JSON import, tool microflows, knowledge base chunk loading and the runtime wiring all sit outside MDL, and mxbuild stays green when they are wrong mdl,architect 5 ondemand build/agents mendix-agent-ui skills/mendix-agent-ui.md Embedding a copilot chat panel — the frame is yours, ConversationalUI owns the conversation; wireframe to tokens to snippet to page placement mdl 5 ondemand build/agents diff --git a/skills/learned-mdl-cannot-express.md b/skills/learned-mdl-cannot-express.md index 659b9945..507965a8 100644 --- a/skills/learned-mdl-cannot-express.md +++ b/skills/learned-mdl-cannot-express.md @@ -22,6 +22,7 @@ would have taken four minutes to find at Stage 3, when the design was still chea | A **file upload** control | nothing. `mxcli syntax page.widgets` prints the complete keyword set and there is no `filemanager`/`fileuploader`/`imageuploader` in it — `filemanager fm (…)` is a parse error, not an unknown property | ⛔ **not buildable** | | A **pluggable widget with a list-valued property** (an HTML element's `attributes`, a chart's `series`, a datagrid's `columns` written by hand) | nothing. On v0.20.0 a pluggable widget takes **scalar properties in parens and no body at all**: `pluggablewidget '' ( tagName: 'div' )` parses, and *every* body form is rejected with `mismatched input '' expecting '}'` — the object-list keywords (`attribute`, `event`) and the child-slot keywords (`tagcontentcontainer`) alike | ⛔ **not buildable** | | A page-level **boolean a button toggles** (a collapsible section, a show/hide) | page `variables:` exist and can be READ by `visible:`, but nothing in MDL writes one — that needs a nanoflow | ⛔ **not buildable** | +| A **language picker** a non-admin user can use (the Marketplace LanguageSelector) | no grant route found on one project for `System.User_Language`: `grant read (User_Language) on entity Administration.Account …` passes `check` and `exec` refuses it (`has no member(s) User_Language`, v0.25.0); Studio Pro does not offer the member either, so the widget errors on pick | ⚠️ **workaround**: a microflow sets `$currentUser`'s language + a nanoflow reloads — `learned-multilingual-app.md` §4 | | A **gallery empty-placeholder** | no `emptyState`/`emptyplaceholder` block on `gallery`. On v0.24.0 the Data grid 2 `emptyplaceholder` slot fails mxbuild with CE0463 too, because the bundled definition fixes `showEmptyPlaceholder` to `none` (BUG-150) | ⚠️ **workaround**: a one-attribute non-persistent entity the page switches on | | An **aggregate over an association** inside a gallery/listview template | client expressions have no `count()`; XPath does, but a template expression is not XPath | ⚠️ **workaround**: denormalise, with ONE writer that recomputes rather than increments | | A **count badge on a tab** ("Documents 3") | nothing on v0.24.0: a tab page is written with Caption and Widgets only, `Badge:` is refused by `check`, and `update widgets` reaches only pluggable property bags. `Forms$TabPage.Badge` exists in the metamodel | ⛔ **not buildable** — say the count in the pane's lead line | diff --git a/skills/learned-multilingual-app.md b/skills/learned-multilingual-app.md new file mode 100644 index 00000000..f269daa0 --- /dev/null +++ b/skills/learned-multilingual-app.md @@ -0,0 +1,187 @@ +# A second language — the four decisions that decide whether a user ever sees the translations + +**Applies to:** any mxcli project. +**Purpose:** adding a language to a Mendix app built with MDL: which language is the source, which +are enabled, which one a user starts in, and a language switch that works for a non-admin role. +Read it before the first translation script, and the moment a language picker errors, shows a blank +label, or changes nothing. +**Source:** field run 2026-10-09, a regulatory-QA demo app (Mendix 11.15.0, mxcli v0.25.0) authored +in Spanish, with Spanish and Portuguese added as a change request. Writing the strings went cleanly +(418 per language, `create or modify translations without marketplace`). Everything around them +failed — four separate failures, each green on `mxcli check`, `exec.sh` (mxbuild 0 errors) and +`mxcli lint`. Writing the strings themselves is mxcli's `translations` skill; this file is the rest. + +--- + +## The one-line finding + +> The translations were correct the whole time. What the user saw was decided by the source +> language, the user's language and the switch — none of which a gate looks at. + +| What the user saw | What was actually wrong | § | +|---|---|---| +| Picking a language raised an error and nothing changed | The Marketplace LanguageSelector writes `System.User_Language` from the browser; a normal role could not, and on this project no grant route was found | 4 | +| The picker showed no text at all | The demo users were on `en_US`, which this project's picker configuration left out of its list | 3 | +| There is no English to offer | Captions were authored in Spanish under the default `en_US` | 1 | +| The login page stayed Spanish | `theme/web/login.html` is outside the translation system | 5 | + +--- + +## 1. Decide the source language before the first caption + +Every caption is stored under the project's **default language**, whatever language it is written +in. This app's captions were authored in Spanish under the default `en_US`, so `en_US` *is* Spanish +— a user on `en_US` sees Spanish, and there is no English to offer without retranslating. + +Changing the default later moves no text and nothing warns (`mx check` is 0 errors either way). +mxcli's own fix for a wrong source is to change `DefaultLanguageCode` and re-run the `create` +statements that wrote the captions — workable while the scripts are the whole source of truth. +So, at Stage 3, ask the user one question and record the answer in `PROJECT.md`: *which language +are captions written in, and is that the language users get, or a hidden source?* + +When the authored language is not the default (the common case on a project that started from +`mxcli new`): keep the default as a hidden source, enable the real languages, and give the authored +language a translation that **copies its strings verbatim**. That same file is where Mendix's own +English platform texts — validation messages, paging, confirmation buttons — are translated; +otherwise they stay English in the Spanish UI. + +```sql +mdl 1; +alter settings language add or modify 'es_ES' ( CheckCompleteness: false ); +alter settings language add or modify 'pt_PT' ( CheckCompleteness: false ); + +create or modify translations without marketplace for es_ES ( + 'Guardar' as 'Guardar', -- authored in Spanish: copied as is + 'Go to first page' as 'Ir a la primera página' -- Mendix platform text: translated +); +``` + +Settings writes work from the CLI (`learned-mdl-preflight.md` STOP #2, retired 2026-09-30), so +this needs no stop-and-ask. Keep the dictionary in one generator (a small script that writes one `.mdl` per language), +not in hand-edited MDL: a renamed caption has to change in one place, and the scripts are +re-applied after every caption change — this project re-applied both the same day it wrote them, after the switch's captions changed. + +**Probe:** `./mxcli -p .mpr -c "DESCRIBE SETTINGS" | grep -E "DefaultLanguageCode|LANGUAGE add"` +— the default and the enabled list. `describe translations for ` ends with +`N source string(s), M translated, K to go`. + +## 2. Agree what is not translated + +Captions are model text; **data is not**. Product names, imported documents, generated rule +explanations, certificate and CSV contents stay in the language they were stored in. Ask the user +before translating (this project: *data stays Spanish*, recommended in chat, not contested) and +record it — or the first review reports a half-translated app. + +## 3. Start users in a language they can see + +A user's language is `System.User_Language`. A user on no language or on the hidden source gets +source text, and a picker whose list leaves that language out has nothing to show as the current +one. That was this project's blank label — observed with this widget configuration: the list was +`System.Language` filtered `[Code != 'en_US']`, so the demo users' own language was not in it. + +Set it where accounts are created; for seeded demo accounts, in after-startup, leaving a choice the +user already made alone: + +```sql +retrieve $Spanish from System.Language where Code = 'es_ES' first; +retrieve $Demo from Administration.Account + where [Name = 'demo.user@example.com' and not(System.User_Language/System.Language[Code != 'en_US'])] first; +if $Spanish != empty and $Demo != empty then + change $Demo (System.User_Language = $Spanish) commit without events; +end if; +``` + +## 4. The language switch: do not use the Marketplace LanguageSelector for a normal role + +**What failed.** LanguageSelector v1.1.3 reads `System.User_Language` from the session user and +commits a new value **from the browser**. For this project's non-admin role the pick raised an +error and changed nothing, and **on this project no grant route was found**: + +- **mxcli** — `grant read (User_Language) on entity Administration.Account …` *passes `check`* + and is refused by `exec`, verbatim (recorded 2026-10-09, v0.25.0): + `entity Administration.Account has no member(s) User_Language`. The qualified + `System.User_Language` does not parse there (`mismatched input '.' expecting {',', ')'}`). + This is deliberate in mxcli: its access-rule walk stops at `System.User`, so a platform member + listed in a rule on a specialization is CE0066. +- **Studio Pro** — the first fix was written as a Studio Pro hand step (Account → access rule → + tick `User_Language`). The user tried it: **Studio Pro does not offer that member.** The hand + step was impossible, and a script had already shipped describing it. + +What is **not** established is that a normal user can never write their own `User_Language`: that +member is governed by the System module's own access rules, which a user role receives through the +`System.User` module role. So before replacing the widget, run one probe: + +- `./mxcli -p .mpr -c 'DESCRIBE USER ROLE ""'` — does `ModuleRoles` include `System.User`? + (On this project it did, for both roles, and the pick still failed.) +- Is the picker reading the session user (`System.User`) rather than a specialization such as + `Administration.Account`? This widget takes no user binding; it uses the session user. + +If both hold and the pick still fails, do not write the Account hand step and do not spend a session +on the access rule. Replace the widget — the recipe below works either way. + +**What works.** A microflow applies no entity access, so it may set the signed-in user's own +language; Mendix applies a language when the client loads, so the button reloads afterwards. + +```sql +mdl 1; +/** Sets the signed-in user's language to the System.Language with this code. Only touches the current user; ignores an unknown code. */ +create or modify microflow MyModule.SUB_SetLanguage ($Code: String) +begin + retrieve $Language from System.Language where Code = $Code first; + if $Language != empty and $currentUser != empty then + change $currentUser (System.User_Language = $Language) commit; + end if; +end; + +/** ES button: switch the user to Spanish, then reload so the language applies. */ +create or modify nanoflow MyModule.ACT_LanguageES () +begin + call microflow MyModule.SUB_SetLanguage(Code = 'es_ES'); + call javascript action NanoflowCommons.Reload(); +end; + +grant execute on microflow MyModule.SUB_SetLanguage to MyModule.User; +grant execute on nanoflow MyModule.ACT_LanguageES to MyModule.User; +``` + +One nanoflow per language, two buttons in the layout's top bar (`alter layout … { replace + with { container … { actionbutton … } } }` swaps the widget in place). Needs +NanoflowCommons for `Reload`. Label the buttons in their own words (`ES`, `PT`), not with +`System.Language.Description`, which is whatever the runtime stored and is usually English. +`mxcli lint` flags the nanoflow — CONV010, *"ACT_ microflows should only contain UI actions and +sub-microflow calls"*, raised here on the JavaScript `Reload` call; accept it in the baseline with +that reason, or name the nanoflow without the `ACT_` prefix. + +Highlight the active language from **``**, which Mendix writes from the user's language +— no data read, so no access question at all: + +```scss +html[lang^="es"] .lang-switch-es, +html[lang^="pt"] .lang-switch-pt { background: var(--color-primary); color: var(--color-on-primary); } +``` + +**Probe:** in the running app, after switching, `document.documentElement.lang` starts with the new +code. Measured here: `pt` after switching to `pt_PT`, with the menu reading *Histórico*. + +## 5. The login page is yours to translate + +`theme/web/login.html` is a static file: no Mendix translation reaches it and there is no user +language before sign-in. This project gave it `data-i18n` keys, a small inline script swapping them +and an `ES | PT` toggle remembered in `localStorage`. It does not set the user's language after +sign-in — §3 and §4 do. + +## 6. Verify in the running app — per language, per role + +A green gate proves none of §1–§5. **Sign in as a non-admin user** — the §4 failure does not +reproduce as an administrator — and for each enabled language: + +1. Switch; the page reloads in that language: menu, page titles, buttons, grid headers, and one + Mendix platform text (a validation message or the paging bar). +2. The active button is highlighted (`` changed). +3. Sign out and in again: the choice persisted. +4. A page with data shows the data in its stored language (§2). + +**Done means** 4 of 4 checks, for every enabled language × every user role, written down with a +screenshot each (this project: 2 languages × 2 roles, the screenshots in its demo guides). A +language showing source text everywhere is an enabled-language question first +(`describe settings`), before anything is re-translated. From d99c83dcf0ec71abb9825d8285eb02a68b86e85a Mon Sep 17 00:00:00 2001 From: MendixMau <55099313+MendixMau@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:57:50 +0800 Subject: [PATCH 04/18] new(brd-report): process map at the top, open-questions folded in (#438) Process map in the BRD report. Independent review r2: APPROVE. Verdict: APPROVE PR #438 re-review at 98f452fa (base release/v1.0). Earlier review: 438.md at 11cfe065 (CHANGES, 7 must-fix). Checked in a detached worktree /tmp/rv438, now removed. Tests: test-process-map-field 52 ok, test-process-map 74 ok, test-brd-report-process-map 73 ok, test-interview-gate 11, test-interview-mode 76, all 0 FAIL. (The tests live in tests/wave2/; there is no tests/test-process-map*.sh.) check-portability clean (280 files). check-pr-discipline origin/release/v1.0 clean. No client, brand or project names in the added lines. ## Earlier must-fix items 1. Map clipped on the right: FIXED. fit() now scales every board in embed mode (template `scalable = long || EMBED`), the frame gets a computed first height, the host ignores a height post under 80 px, and a new render check passes: `FIT:inside:905/970:zoom0.83`. 2. "Scroll sideways" hint on a scaled long process: FIXED. The hint is built per case: "scaled to fit" when zoom < 1, "split it in two" above 12 steps, "scroll sideways" only when it still overflows. The embed path gets its own hint, hidden unless it overflows. 3. Stage 0/1 gate with only sme-questions.md got no report: FIXED. brd-report counts collector questions when there are no BRDs and renders a questions-only page ("Open questions, stage N view"); exit 3 stays for no BRDs and no questions. Tests cover the gate at stage 1 writing the file, the question in it, the header, and the exit-3 case. 4. Migrate lost judged placements on a collision: FIXED. An old judged entry replaces a current derived one and prints a note. When both are judged and differ, the current one is kept and a note names the .bak. Test "the judged placement won" (judged P1-07). 5. Judged entries with no target: FIXED. anchorEntries leaves them out and lists them, and counts entries with no id. anchor now warns on a judged entry with empty targets and does not count it, so it ends up derived or unanchored, never silently "global". 6. Stale questions-report.sh in docs/pipeline-walks.html: FIXED (now bin/brd-report.sh). 7. Migrate tests: FIXED. New cases: old shape to targets (step/page/bucket/string target, compared as objects), the .bak to .bak.1 path, the collision, the empty-target and no-id entries, a second migrate that leaves the files byte-identical. Also the whole-app collapse above 8, --stage passthrough, and the CAUSE line for page names. Nice-to-haves also done: the duplicate I6 line, .bak.N, write order (anchors file, then model key, then rename, each write atomic through tmp+rename), only the model key is patched, PAGE-MAP precedence matching the docs plus a warning for a row naming a missing file, ResizeObserver on wrap, "Questions shown: up to stage N" header, the CAC-3 line in checkpoint-architecture. Not done: a staleness FAIL for the embedded map in brd-report (fine as a follow-up). ## New must-fix items None. ## Follow-ups (not blocking) - Model key patch can hit the wrong key. process-map.js migrate replaces the FIRST `"anchors": ""` anywhere in the model text. Reproduced: a model with `{"meta":{"anchors":"notes.txt"}, ..., "anchors":"design/old/a.json"}` gets meta rewritten, the top-level key still names the old file, which was just moved to .bak. The output still says "only that key was changed". The schema has the key only at top level, so normal models never hit this. Fix: after the replace, `JSON.parse(newText).anchors === want`, else fall back to the re-serialize branch. - `"targets": "P1-02"` (a string, not an array) in an old file crashes migrate with a TypeError stack trace at process-map.js:89. Nothing is written, so no data is lost. Wrap a non-array value in an array, or die() with a clear message. - Entries in the CURRENT anchors file with empty targets (an `anchor --json` skeleton pasted in but not filled) are dropped silently on migrate (haveE.noTarget/noId are never printed). anchor lists them as unanchored again, so nothing is lost, but print them for symmetry. - brd-report runs open-questions.sh one extra time to count when there are no BRDs. Harmless. --- CHANGELOG.md | 2 + agents/ba-agent.md | 2 +- bin/brd-report.sh | 369 +++++++++++++++++- bin/gate-check.sh | 19 +- bin/init-project.sh | 5 +- bin/install-claude-hooks.sh | 2 +- bin/lib/artifact-manifest.tsv | 2 +- bin/questions-report.sh | 299 -------------- claude-hooks/hooks/interview-gate.sh | 8 +- docs/pipeline-walks.html | 2 +- docs/windows-test-run.md | 2 +- process/improvement-plan-e2e-reporting.md | 2 +- project-bin/assemble-prototype.js | 8 +- project-bin/process-map.js | 192 ++++++++- project-bin/process-map.template.html | 117 +++++- project-tests/e2e/report-render.js | 2 +- skills/checkpoints/checkpoint-architecture.md | 2 +- skills/conversion-runbook.md | 8 +- skills/coverage-ledger.md | 2 +- skills/interview-protocol.md | 16 +- skills/process-map.md | 18 +- skills/report-schema.md | 2 +- tests/wave2/test-brd-report-process-map.sh | 298 ++++++++++++++ tests/wave2/test-interview-gate.sh | 2 +- tests/wave2/test-interview-mode.sh | 2 +- tests/wave2/test-process-map-field.sh | 230 +++++++++++ tests/wave2/test-questions-report.sh | 123 ------ toolkit-guide.html | 4 +- 28 files changed, 1222 insertions(+), 518 deletions(-) delete mode 100755 bin/questions-report.sh create mode 100755 tests/wave2/test-brd-report-process-map.sh create mode 100755 tests/wave2/test-process-map-field.sh delete mode 100644 tests/wave2/test-questions-report.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bd4957..8f107cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- new(brd-report): the Stage 2 report opens with the pass-1 process map (drawn by `process-map.js build --embed`, not a copy) and lists requirements and open questions under it, each tied to its step; a click on a step filters the list, what no step covers stays under "whole app". `open-questions.html` and `bin/questions-report.sh` are retired: the gate and the interview hook write `analysis/brd-report.html`, and an old project's `docs/open-questions.html` is never an error, only pointed away from. Stage 3: the prototype links to the full map and the map links back — a business owner could not tell where in the work a question fits (field run, 2026-10-16) — MendixMau + - learn(multilingual-app): **a second language is four decisions the translations never see — source language, enabled languages, the user's starting language, and a switch a normal role can use.** New `skills/learned-multilingual-app.md` (routed on demand, stages 3/5/6, architect, mdl + review). Field run 2026-10-09 (Mendix 11.15.0, mxcli v0.25.0, an app authored in Spanish gaining ES + PT): 418 strings per language written correctly, and still the language picker raised an error, showed a blank label and changed nothing, with check, exec.sh and lint green. Causes: captions authored under the default `en_US` (so `en_US` is the authored language); demo users on that hidden source language; the Marketplace LanguageSelector writing `System.User_Language` from the browser, which this project's non-admin role could not, with no grant route found (mxcli `check` passes the grant, `exec` refuses it; Studio Pro does not offer the member, so the first fix — a Studio Pro hand step — was impossible; a probe on the `System.User` module role now comes first); `login.html` outside the translation system. Recipe: a microflow sets `$currentUser`'s language, a nanoflow reloads, CSS highlights from ``; seeded users start in a visible language. `learned-mdl-cannot-express.md` gains the LanguageSelector row. — CarlesZ - process(inbox): **five field notes from an existing-app legacy-widget rebuild** — exec.sh keeps an MDL write when mxbuild crashes on it and the next exec runs on "Baseline NOT measured"; a signal stop of `run --test-endpoint` leaves AfterStartupMicroflow on `MxTest.RegisterEndpoint` (second field case for #244); Unit-row delete refused as ORPHAN, rename-then-drop works; a durable-rebuild kit with a "restate from DESCRIBE" STOP row (DESCRIBE omits module-role and layout-row visibility: 97 role locks in 16 documents, none shown); learned entries for File Uploader image mode (CE0463), native-mapped `radiobuttons`/`image`, tab-page `CaptionParams`/`Visible:` silently dropped, BUG-39 on widget XPath, and two-hop data-source paths. Three further notes (restore-mpr usage, build-plan-status flag order, another project's mxcli test blocking exec.sh) were dropped because master already fixes them. Mendix 11.14.0 Beta, mxcli v0.25.0, toolkit c1efcb3 — AlvinnYeo - fix(build-plan-status): --refresh keeps an existing build-plan.json current, so a day of building no longer reads as 0 built — MendixMau diff --git a/agents/ba-agent.md b/agents/ba-agent.md index 6bf934d2..3febbc36 100644 --- a/agents/ba-agent.md +++ b/agents/ba-agent.md @@ -105,7 +105,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. - Run the full interview protocol from `conversion-runbook.md` §1 at every gate: homework first, 2-4 options with evidence, assumptions stated out loud, user answers in the terminal, decision written to the stage HTML *and* `PROJECT.md`, unknowns default + `ASSUMED` + proceed. - Never invent legacy intent from code alone — that's what Path C (the SME interview) is for. Log it as an open question if no SME is available, don't guess. - **Logging an open question is half the job. Raising it is the other half** — see the Open-questions batch below. A question written to a BRD and never put in the chat is not "logged", it is buried. -- **Read `interview-protocol.md` before raising anything.** Every question you hand over carries two named options and your recommendation; a bare question moves your analysis onto the user. Generate `docs/open-questions.html` with `bin/questions-report.sh` and give them the path. +- **Read `interview-protocol.md` before raising anything.** Every question you hand over carries two named options and your recommendation; a bare question moves your analysis onto the user. Generate `analysis/brd-report.html` with `bin/brd-report.sh` and give them the path. - `✋` gates (Stages 0 and 7) do not resolve to `ASSUMED` — they need an explicit `CONFIRMED` answer before the run proceeds. ## Workflow diff --git a/bin/brd-report.sh b/bin/brd-report.sh index 03e9040b..10edb651 100755 --- a/bin/brd-report.sh +++ b/bin/brd-report.sh @@ -40,7 +40,7 @@ # this instrument never judges whether the CONTENT is right, only whether it is there. # # Usage: -# brd-report.sh [--html PATH] [--json] [--quiet] +# brd-report.sh [--html PATH] [--stage N] [--json] [--quiet] # # exit 0 rendered (faults included — a fault is information, and the gate is gate-check's job) # 2 usage 3 no BRDs found (never a pass) 4 a BRD is not valid JSON @@ -60,12 +60,13 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat >&2 <<'USAGE' -usage: brd-report.sh [--html PATH] [--json] [--quiet] +usage: brd-report.sh [--html PATH] [--stage N] [--json] [--quiet] Renders the Stage 2 BRD surface from every BRD in the project, whatever produced them — a pipeline extractor, kb-generation.md, an SME interview, or a text editor. --html PATH where to write (default: /analysis/brd-report.html) + --stage N open questions up to stage N (the gate passes its own stage) --json machine-readable summary on stdout instead of prose --quiet render, say nothing @@ -80,11 +81,13 @@ shift 2>/dev/null || true case "$PROJECT_DIR" in -h|--help|'') usage ;; esac HTML_OUT="" +STAGE_ARG="" AS_JSON=0 QUIET=0 while [ $# -gt 0 ]; do case "$1" in --html) HTML_OUT="${2:-}"; shift 2 ;; + --stage) STAGE_ARG="${2:-}"; shift 2 ;; --json) AS_JSON=1; shift ;; --quiet) QUIET=1; shift ;; -h|--help) usage ;; @@ -101,7 +104,17 @@ require_py BRD_FILES=$(discover_brds "$PROJECT_DIR") BRD_COUNT=$(count_lines "$BRD_FILES") -if [ "$BRD_COUNT" = "0" ]; then +# Stage 0/1 has no BRDs yet, but the gate can still block on sme-questions.md questions and must +# hand the user a page. With no BRDs and no questions found, nothing is rendered (exit 3). +SME_ONLY=0 +if [ "$BRD_COUNT" = "0" ] && [ -x "$HERE/open-questions.sh" ]; then + _n=$(bash "$HERE/open-questions.sh" "$PROJECT_DIR" ${STAGE_ARG:+--stage "$STAGE_ARG"} --json 2>/dev/null | "$PY" -c 'import json,sys +try: print(json.load(sys.stdin)["counts"]["total"]) +except Exception: print(0)' 2>/dev/null) + [ "${_n:-0}" -gt 0 ] 2>/dev/null && SME_ONLY=1 +fi + +if [ "$BRD_COUNT" = "0" ] && [ "$SME_ONLY" = "0" ]; then # An empty result set is never a pass (report-schema.md). Distinct exit code so no caller # can read "found nothing" as "nothing to find". echo "brd-report: no BRDs found under $PROJECT_DIR — nothing rendered." >&2 @@ -114,7 +127,7 @@ fi mkdir -p "$(dirname "$HTML_OUT")" 2>/dev/null || true BRD_FILES="$BRD_FILES" BRD_COUNT="$BRD_COUNT" HTML_OUT="$HTML_OUT" AS_JSON="$AS_JSON" \ -QUIET="$QUIET" PROJECT_DIR="$PROJECT_DIR" "$PY" <<'PY' +QUIET="$QUIET" PROJECT_DIR="$PROJECT_DIR" TOOLKIT_BIN="$HERE" STAGE_ARG="$STAGE_ARG" "$PY" <<'PY' import json, os, sys, html, datetime files = [f for f in os.environ['BRD_FILES'].splitlines() if f.strip()] @@ -749,12 +762,8 @@ for a in analysed: note='

Rules found inside extracted logic that the domain model does ' 'not show. Derived from microflows[].hiddenRules.

'), section_block(a, 'openQuestions', 'Open questions', - '
    ' - + ''.join('
  • ' + esc(q.get('question') if isinstance(q, dict) else q) - + (f' {esc(q.get("status"))}' - if isinstance(q, dict) and q.get('status') else '') - + '
  • ' for q in a['raw']['openQuestions']) - + '
'), + '

Listed with their state, drafting position and process step in ' + 'Requirements and open questions at the top of this page.

'), ] chips = ''.join(f'{esc(m)}' for m in a['modules']) \ @@ -768,6 +777,331 @@ for a in analysed: f'

{esc(a["path"])}

' '') +# --- process map + open questions, fused (2026-10-16) ----------------------------------------- +# +# The page opens with the process map (pass 1: no screens needed) and lists, under it, every +# requirement (use case) and every open question or assumption. A click on a step filters that +# list to the rows about the step. Rows tied to no step stay listed under "whole app". +# +# The map is NOT drawn here: `project-bin/process-map.js build --embed` renders it (the one +# renderer, also used for the Stage 3 map beside the wireframes) and this page frames it. The +# open questions are NOT collected here either: `bin/open-questions.sh --json` is the one +# collector. This block only joins the two onto the BRDs. It replaces docs/open-questions.html. +import subprocess, shutil, tempfile, re + +toolkit_bin = os.environ['TOOLKIT_BIN'] +stage_arg = os.environ.get('STAGE_ARG', '') +proj_abs = os.path.abspath(project) + +def _find_model(): + cands = [os.path.join(proj_abs, 'design', 'process-model.json')] + adir = os.path.join(proj_abs, 'analysis') + if os.path.isdir(adir): + for d in sorted(os.listdir(adir)): + cands.append(os.path.join(adir, d, 'design', 'process-model.json')) + for c in cands: + if os.path.isfile(c): + return c + return None + +def _find_pm(): + for c in (os.path.join(toolkit_bin, '..', 'project-bin', 'process-map.js'), + os.path.join(proj_abs, 'bin', 'process-map.js')): + if os.path.isfile(c): + return os.path.normpath(c) + return None + +pm_data, pm_frame, pm_note = None, '', '' +_model, _pm, _node = _find_model(), _find_pm(), shutil.which('node') +if not _model: + pm_note = ('No process map yet. Draw it at Stage 2 close (skills/process-map.md, pass 1); ' + 'until then every requirement and question is listed under "whole app".') +elif not (_pm and _node): + pm_note = ('A process model exists (' + os.path.relpath(_model, proj_abs) + ') but the map ' + 'could not be drawn: ' + ('node is not on PATH' if not _node else 'process-map.js not found') + '.') +else: + _tmp = tempfile.mkdtemp(prefix='brdmap.') + try: + _r = subprocess.run([_node, _pm, 'build', '--embed', '--project', proj_abs, '--model', _model, + '--out', os.path.join(_tmp, 'embed.html'), + '--data-out', os.path.join(_tmp, 'data.json')], + capture_output=True, text=True, timeout=120) + if _r.returncode == 0 and os.path.isfile(os.path.join(_tmp, 'data.json')): + with open(os.path.join(_tmp, 'data.json')) as fh: + pm_data = json.load(fh) + with open(os.path.join(_tmp, 'embed.html')) as fh: + pm_frame = fh.read() + else: + _errs = [l for l in (_r.stdout + _r.stderr).splitlines() if l.startswith(('ERROR', 'process-map:'))] + pm_note = ('The process model (' + os.path.relpath(_model, proj_abs) + ') did not build: ' + + ' | '.join(_errs[:4]) + '. Fix the model (skills/process-map.md) and re-run.') + except Exception as e: + pm_note = 'The process map could not be drawn: ' + str(e) + finally: + shutil.rmtree(_tmp, ignore_errors=True) + +# open questions: the one collector +oq, oq_note = None, '' +try: + _c = subprocess.run(['bash', os.path.join(toolkit_bin, 'open-questions.sh'), project] + + (['--stage', stage_arg] if stage_arg else []) + ['--json'], + capture_output=True, text=True, timeout=120) + oq = json.loads(_c.stdout) +except Exception as e: + oq_note = ('The open-questions collector did not run (' + str(e)[:120] + '), so states are unknown. ' + 'The questions below are read straight from the BRDs.') + oq = None + +brd_of_base = {os.path.basename(a['path']): a['id'] for a in analysed} +BIG = 1 << 30 +step_ord, step_meta = {}, {} +if pm_data: + for _p in pm_data['processes']: + for _s in _p['steps']: + step_ord[_s['id']] = len(step_ord) + step_meta[_s['id']] = {'label': f"{_p['id']} step {_s['n']}: {_s['title']}", 'proc': _p['id'], 'n': _s['n']} +steps_of_ref = {} # "F001:Q01" -> {step ids}, from the map's items +if pm_data: + for _it in pm_data['items']: + for _ref in _it['refs']: + steps_of_ref.setdefault(_ref, set()).update(_it['steps']) + +def _order(steps): + return sorted(steps, key=lambda s: step_ord.get(s, BIG)) + +def _first(steps): + return min([step_ord.get(s, BIG) for s in steps] or [BIG]) + +def step_chips(steps): + if not steps: + return 'whole app' + return ''.join(f'{esc(step_meta[s]["proc"])} · {step_meta[s]["n"]}' + for s in _order(steps)) + +def _attr_steps(steps): + return esc(' '.join(_order(steps))) + +# rows: (..., steps, html). The second-last element is always the step list. +req_rows, q_rows = [], [] +for a in analysed: + for u in a['raw']['useCases']: + if not isinstance(u, dict) or not u.get('id'): + continue + ref = f"{a['id']}:{u['id']}" + info = (pm_data or {}).get('useCases', {}).get(ref) + steps = info['steps'] if info else [] + actors, _ = alist(u, 'actors', 'actor') + why = '' + if info and info.get('standalone'): + why = f'
Not part of a process: {esc(info["standalone"])}
' + actor_html = f'
{esc("; ".join(str(x) for x in actors))}
' if actors else '' + req_rows.append(('req', 'req', (_first(steps), ref), steps, + f'
' + f'
{esc(ref)} {esc(u.get("title"))}
' + + actor_html + why + f'
{step_chips(steps)} BRD detail
')) + +STATE_NOTE = { + 'UNRAISED': 'Never put to you. These block the gate.', + 'UNRECOGNISED': 'Status text no rule could map. Treated as unraised.', + 'RAISED': 'Put to you, awaiting your reply.', + 'ASSUMED': 'A position was taken and you consented to it. Recorded, not blocking.', + 'ANSWERED': 'Settled with a recorded answer.', + 'MOOT': 'Descoped, not silently dropped.', +} +STATE_ORDER = ['UNRECOGNISED', 'UNRAISED', 'RAISED', 'ASSUMED', 'ANSWERED', 'MOOT'] +BLOCKING = ('UNRAISED', 'UNRECOGNISED') + +def q_row(state, qid, origin, text, prop, steps, meta, rid): + cls = 'block' if state in BLOCKING else 'ok' + if prop: + prophtml = ('
Drafting position: confirm or overturn' + f'
{esc(prop)}
') + elif state in BLOCKING: + prophtml = ('
No proposed answer' + '
Handed over bare. The stage that raised it should propose two named options ' + 'and a recommendation before it goes to the user.
') + else: + prophtml = '' + meta_html = f'
{meta}
' if meta else '' + return ('q', state, (_first(steps), str(qid)), steps, + f'
' + f'
{esc(qid)} {esc(state)} ' + f'{esc(origin)}
{esc(text)}
{prophtml}{meta_html}' + f'
{step_chips(steps)}
') + +if oq is not None: + for q in oq.get('questions', []): + base = os.path.basename(str(q.get('origin', ''))) + bid = brd_of_base.get(base) + steps = sorted(steps_of_ref.get(f"{bid}:{q.get('id')}", [])) if bid else [] + meta = ' · '.join(x for x in [ + (f'recorded as {esc(str(q.get("rawStatus"))[:120])}' if q.get('rawStatus') else ''), + esc(q.get('reason') or ''), (f'stage {esc(q.get("stage"))}' if q.get('stage') is not None else '')] if x) + rid = f"q-{bid}-{q.get('id')}" if bid else 'q-' + re.sub('[^A-Za-z0-9_-]', '-', base) + '-' + str(q.get('id')) + q_rows.append(q_row(q.get('state', 'UNRECOGNISED'), q.get('id', '?'), q.get('origin', ''), q.get('question', ''), + (q.get('assumption') or '').strip(), steps, meta, rid)) +else: + for a in analysed: + for q in a['raw']['openQuestions']: + if not isinstance(q, dict): + continue + steps = sorted(steps_of_ref.get(f"{a['id']}:{q.get('id')}", [])) + q_rows.append(q_row('NOT COLLECTED', q.get('id', '?'), os.path.basename(a['path']), q.get('question', ''), + (q.get('assumption') or '').strip(), steps, '', f"q-{a['id']}-{q.get('id')}")) +for it in (pm_data or {}).get('items', []): + if it['kind'] != 'decision': + continue + steps = it['steps'] + q_rows.append(('q', 'ASSUMED', (_first(steps), it['id']), steps, + f'
' + f'
{esc(it["id"])} ASSUMPTION ' + f'decision register
{esc(it["text"])}
' + f'
{step_chips(steps)}
')) + +def _srank(r): + return STATE_ORDER.index(r[1]) if r[1] in STATE_ORDER else 9 + +def _pick(rows, whole): + sel = [r for r in rows if (not r[-2]) == whole] + if rows is q_rows: + sel.sort(key=(lambda r: (_srank(r), r[2])) if whole else (lambda r: (r[2], _srank(r)))) + else: + sel.sort(key=lambda r: r[2]) + return sel + +def _group(title, rows): + if not rows: + return '' + return (f'

{esc(title)} {len(rows)}

' + + ''.join(r[-1] for r in rows) + '
') + +n_blocking = (oq or {}).get('blocking', 0) +if oq is None: + q_banner = f'' +else: + _bare = [q for q in oq.get('questions', []) if q.get('state') in BLOCKING and not (q.get('assumption') or '').strip()] + q_banner = ((f'') + if n_blocking else '') + if _bare: + q_banner += (f'') + _cnt = oq.get('counts', {}) + _rows_t = ''.join(f'{s}{_cnt.get(s, 0)}' + f'{esc(STATE_NOTE[s])}' for s in STATE_ORDER if _cnt.get(s, 0)) + if _rows_t: + q_banner += ('
What was found, by state
' + '' + _rows_t + '
StateCountMeaning
') + +n_by_step = len([r for r in req_rows + q_rows if r[-2]]) +n_whole = len(req_rows) + len(q_rows) - n_by_step +by_step_html = '' +if n_by_step: + by_step_html = (f'

About the steps of the map ({n_by_step})

' + + _group('Requirements', _pick(req_rows, False)) + _group('Open questions and assumptions', _pick(q_rows, False))) +whole_html = _group('Requirements', _pick(req_rows, True)) + _group('Open questions and assumptions', _pick(q_rows, True)) + +# First paint height: the frame reports its real height once it has a width, but a screenshot or a slow +# frame must not show a clipped map in the meantime. About 105 px per lane row, plus legend and title. +pm_h0 = 70 + sum(120 + 105 * len({st.get('lane') for st in pr.get('steps', [])}) for pr in (pm_data or {}).get('processes', [])) if pm_frame else 120 +if pm_frame: + map_html = ('

Process map

' + '

One map per business process. Rows are the people and the system, cards are the steps in order. ' + 'Click a step to see the requirements and open questions about it below.

' + f'
') +else: + map_html = f'

Process map

' + +list_html = ('
' + '

Requirements and open questions

' + q_banner + by_step_html + + f'

Whole app ({n_whole})

' + '

Not tied to one step: they apply to the whole app or to one kind of data, or no map is drawn yet.

' + + (f'
Show the {n_whole} whole-app items{whole_html}
' if n_whole > 8 else whole_html) + + '
') + +PM_JS = """ +(function(){ +var META = %s; +var list = document.getElementById('pm-list'); if (!list) return; +var frame = document.getElementById('pm-frame'), bar = document.getElementById('pm-bar'), + txt = document.getElementById('pm-bar-text'), whole = document.getElementById('pm-whole'); +var current = ''; +function post(){ if (frame && frame.contentWindow) frame.contentWindow.postMessage({pm:'select', step:current}, '*'); } +function apply(id){ + current = id || ''; + var rows = list.querySelectorAll('.pmrow'), n = {req:0, q:0}; + Array.prototype.forEach.call(rows, function(r){ + var on = !current || (' ' + r.getAttribute('data-steps') + ' ').indexOf(' ' + current + ' ') >= 0; + r.hidden = !on; if (on && current) n[r.getAttribute('data-kind') === 'req' ? 'req' : 'q']++; + }); + Array.prototype.forEach.call(list.querySelectorAll('.pmgroup'), function(g){ + var v = g.querySelectorAll('.pmrow:not([hidden])').length; g.hidden = !!current && !v; + var c = g.querySelector('.cnt'); if (c) c.textContent = String(v); }); + Array.prototype.forEach.call(list.querySelectorAll('.pmh'), function(e){ e.hidden = !!current; }); + if (whole) whole.hidden = !!current; + if (bar){ bar.hidden = !current; if (current && META[current]) txt.textContent = META[current] + ': ' + n.req + ' requirement(s), ' + n.q + ' question(s) or assumption(s)'; } + post(); +} +function pick(id, scroll){ + apply(id); + try{ history.replaceState(null, '', id ? '#step=' + id : location.pathname + location.search); }catch(e){} + if (scroll) { var t = document.getElementById('pm-title'); if (t) t.scrollIntoView({behavior:'smooth', block:'start'}); } +} +window.addEventListener('message', function(e){ + if (!frame || e.source !== frame.contentWindow) return; + var d = e.data || {}; + if (d.pm === 'height') { if (d.h > 80) frame.style.height = d.h + 'px'; } + else if (d.pm === 'step') pick(d.step, true); + else if (d.pm === 'ready') post(); +}); +list.addEventListener('click', function(e){ + var a = e.target.closest ? e.target.closest('a.stepchip') : null; + if (a){ e.preventDefault(); pick(a.getAttribute('data-step'), false); var m = document.getElementById('pm-map'); if (m) m.scrollIntoView({behavior:'smooth', block:'start'}); } +}); +document.getElementById('pm-clear').addEventListener('click', function(){ pick('', false); }); +var m = /#step=([^&]+)/.exec(location.hash); if (m && META[decodeURIComponent(m[1])]) apply(decodeURIComponent(m[1])); +})(); +""" % json.dumps({k: v['label'] for k, v in step_meta.items()}) + +PM_CSS = """ + .pmmap iframe { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); min-height: 120px; display: block; } + .banner { border-radius: var(--radius); padding: 11px 15px; margin: 12px 0; font-size: 14px; } + .banner.pass { background: var(--pass-bg); color: var(--pass-ink); } + .banner.fail { background: var(--fail-bg); color: var(--fail-ink); } + .banner.warn { background: var(--warn-bg); color: var(--warn-ink); } + .pmh { font-size: 15px; margin: 26px 0 4px; } + .pmgroup h4 { margin: 18px 0 8px; } + .pmrow { background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--ink-faint); + border-radius: var(--radius); padding: 11px 14px; margin: 8px 0; overflow-wrap: anywhere; } + .pmrow[hidden], .pmgroup[hidden], .pmh[hidden], #pm-whole[hidden], .pmbar[hidden] { display: none; } + .pmrow.q.block { border-left-color: #dc2626; } + .pmrow.req { border-left-color: var(--accent); } + .pmhead { font-size: 14px; } + .pmsub, .pmrow .src { font-size: 12.5px; color: var(--ink-soft); margin-top: 3px; } + .qtext { font-size: 14.5px; margin-top: 4px; } + .prop, .gap { margin-top: 8px; padding: 8px 11px; border-radius: 8px; font-size: 13.5px; } + .prop { background: var(--warn-bg); color: var(--warn-ink); } + .gap { background: #f1f5f9; color: var(--ink-soft); } + .lbl { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; font-weight: 700; margin-bottom: 2px; } + .st { font-size: 10.5px; font-weight: 700; letter-spacing: .05em; padding: 1px 8px; border-radius: 999px; } + .st-block { background: var(--fail-bg); color: var(--fail-ink); } + .st-ok { background: #eef2f7; color: var(--ink-soft); } + .pmchips { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } + .stepchip { font-size: 11.5px; text-decoration: none; color: var(--accent); background: #eef4fb; border: 1px solid #cfe0f3; + border-radius: 999px; padding: 2px 10px; } + .stepchip:hover { background: #dde9f7; } + .whole { font-size: 11.5px; color: var(--ink-faint); border: 1px dashed var(--line); border-radius: 999px; padding: 2px 10px; } + .pmdetail { font-size: 11.5px; color: var(--ink-faint); margin-left: auto; } + .pmbar { position: sticky; top: 0; z-index: 5; display: flex; gap: 12px; align-items: center; justify-content: space-between; + background: var(--accent); color: #fff; border-radius: var(--radius); padding: 9px 14px; margin: 0 0 10px; font-size: 14px; } + .pmbar button { font: inherit; color: var(--accent); background: #fff; border: 0; border-radius: 6px; padding: 4px 12px; cursor: pointer; flex: none; } + details.more { margin: 8px 0; font-size: 13.5px; } + details.more summary { cursor: pointer; color: var(--ink-soft); } + @media (max-width: 560px) { body { padding: 20px 16px 60px; } .brd { padding: 16px 14px; } h1 { font-size: 23px; } } +""" + stat = lambda val, lbl: (f'
{esc(val)}
' f'
{esc(lbl)}
') @@ -882,15 +1216,17 @@ page = f""" .risk-medium {{ color: var(--warn-ink); font-weight: 700; }} .footer {{ margin-top: 56px; font-size: 12px; color: var(--ink-faint); border-top: 1px solid var(--line); padding-top: 14px; }} - +{PM_CSS}
-

Stage 2 — what the BRDs actually say

-

Rendered {esc(datetime.datetime.now().strftime('%Y-%m-%d'))} from - {len(analysed)} BRD(s) in {len(kb_dirs)} knowledge-base director(ies) — whatever produced - them. Every section carries a verdict, so a section that is empty because this kind of BRD - has nothing to put there reads differently from one that is empty because something broke.

+

{'Open questions, stage ' + stage_arg + ' view' if (not analysed and stage_arg) else ('Open questions' if not analysed else 'Stage 2 — what the BRDs actually say')}

+

{('Rendered ' + esc(datetime.datetime.now().strftime('%Y-%m-%d')) + '. No BRDs exist yet: these are the questions collected from analysis/sme-questions.md.') if not analysed else ('Rendered ' + esc(datetime.datetime.now().strftime('%Y-%m-%d')) + ' from ' + str(len(analysed)) + ' BRD(s) in ' + str(len(kb_dirs)) + ' knowledge-base director(ies), whatever produced them. Every section carries a verdict, so a section that is empty because this kind of BRD has nothing to put there reads differently from one that is empty because something broke.')} + {esc('Questions shown: up to stage ' + stage_arg + '.') if stage_arg else ''}

+ + {map_html} + + {list_html}
{stat(len(analysed), 'BRDs')} @@ -940,6 +1276,7 @@ page = f""" directory, not the first one.
+ """ diff --git a/bin/gate-check.sh b/bin/gate-check.sh index accd8dc6..680c911c 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -3318,14 +3318,15 @@ if [ -n "$REQUESTED_STAGE" ]; then echo "" >&2 echo "Gate BLOCKED: $blk question(s) at stage $st have never been raised with the user." >&2 echo " $counts" >&2 - # Write the triage report the user reads, alongside the batch the agent reads. - # A terminal dump scrolls away and only the agent ever sees it; the HTML is the artefact - # a human can open, sort by state, and hand to someone else. --no-html suppresses it for - # the same reason it suppresses the board: a read-only query must not dirty a tree. - QR_SCRIPT="$TOOLKIT_DIR/bin/questions-report.sh" - QR_OUT="$PROJECT_DIR/docs/open-questions.html" + # Write the report the user reads, alongside the batch the agent reads. A terminal dump + # scrolls away and only the agent ever sees it; the HTML is the artefact a human can open and + # hand to someone else. Since 2026-10-16 the questions live in the Stage 2 BRD report, under + # the process map (bin/brd-report.sh), not in docs/open-questions.html. --no-html suppresses + # it for the same reason it suppresses the board: a read-only query must not dirty a tree. + QR_SCRIPT="$TOOLKIT_DIR/bin/brd-report.sh" + QR_OUT="$PROJECT_DIR/analysis/brd-report.html" if [ "$HTML_MODE" != "never" ] && [ -x "$QR_SCRIPT" ]; then - "$QR_SCRIPT" "$PROJECT_DIR" $arg -o "$QR_OUT" --quiet >/dev/null 2>&1 + "$QR_SCRIPT" "$PROJECT_DIR" $arg --html "$QR_OUT" --quiet >/dev/null 2>&1 [ -f "$QR_OUT" ] || QR_OUT="" else QR_OUT="" @@ -3334,7 +3335,9 @@ if [ -n "$REQUESTED_STAGE" ]; then echo "" >&2 echo " See them, as a chat-ready batch:" >&2 echo " $OQ_SCRIPT $PROJECT_DIR${arg:+ $arg}" >&2 - [ -n "$QR_OUT" ] && echo " Triage report (open this, and give the user the path): $QR_OUT" >&2 + [ -n "$QR_OUT" ] && echo " BRD report, questions under the process map (open this, and give the user the path): $QR_OUT" >&2 + # Back-compat: the retired page is never read and never fails a gate; it only gets pointed away from. + [ -f "$PROJECT_DIR/docs/open-questions.html" ] && echo " (docs/open-questions.html is the retired page: its content is in analysis/brd-report.html now. Safe to delete.)" >&2 echo "" >&2 echo " Raising is mandatory; assuming is not forbidden. Anything the user hands back" >&2 echo " ('you decide') is recorded ASSUMED with consentBy/consentAt and stops blocking." >&2 diff --git a/bin/init-project.sh b/bin/init-project.sh index cabed48e..69cd4c81 100755 --- a/bin/init-project.sh +++ b/bin/init-project.sh @@ -472,7 +472,7 @@ fi # analysis/ — THE OUTPUT SIDE, and it was never scaffolded (fixed 2026-08-20). # # Every rendered surface in the pipeline lands here: triage.html (bin/triage-report.sh), -# source-sufficiency.html, brd-report.html (bin/brd-report.sh), open-questions.html. gate-check.sh +# source-sufficiency.html, brd-report.html (bin/brd-report.sh; the open questions live in it). gate-check.sh # resolves $ANALYSIS_BASE against it. sources/ was created here from day one; analysis/ was not, # on the unstated assumption that an extraction pipeline would `mkdir -p` it on its way past. # @@ -504,8 +504,7 @@ and the honest way to change one is to re-run its producer. | `source-sufficiency.html` | `bin/source-sufficiency.sh report` | 0 | | `triage.html` | `bin/triage-report.sh ` | 0 | | `extraction-report.html` | the extraction pipeline, or `kb-generation.md` for a document corpus | 1 | -| `brd-report.html` | `bin/brd-report.sh ` | 2 | -| `open-questions.html` | `bin/questions-report.sh --stage N` | any gate | +| `brd-report.html` | `bin/brd-report.sh [--stage N]`: process map, requirements and open questions | 2, any gate | **This folder exists in every entry mode.** A requirements-driven or greenfield project runs no extraction pipeline, but it still produces a sufficiency grade, a scope decision and a BRD report diff --git a/bin/install-claude-hooks.sh b/bin/install-claude-hooks.sh index 1dff2e00..758c7eac 100755 --- a/bin/install-claude-hooks.sh +++ b/bin/install-claude-hooks.sh @@ -68,7 +68,7 @@ TIER 2 --full Changes your working rhythm. Useful after a bad bill; precompact-guard Warns once before a compaction discards unfiled work. interview-gate BLOCKS the end of a turn while questions the pipeline generated have never been put to you, and writes - docs/open-questions.html for you to review. Only fires on + analysis/brd-report.html for you to review. Only fires on projects that actually have BRDs; never blocks the same question set twice, so it cannot trap a session. diff --git a/bin/lib/artifact-manifest.tsv b/bin/lib/artifact-manifest.tsv index e866a5eb..4a54214e 100644 --- a/bin/lib/artifact-manifest.tsv +++ b/bin/lib/artifact-manifest.tsv @@ -81,7 +81,7 @@ extraction-report 1 analysis/*/knowledge-base/extraction-report.html|analysis/kn source-ledger 1 analysis/source-ledger.html bin/source-ledger.sh report — every inventoried source file and what consumed it (Stage 1 owes a disposition per row; gate-check blocks 1–2 on the check) bin/gate-check.sh (LEDGER block — the check; the html is the human-facing table of the same rows) migration,requirements,existing-app-change report brds 2 analysis/*/knowledge-base/brd/*.brd.json|analysis/knowledge-base/brd/*.brd.json|knowledge-base/brd/*.brd.json pipeline generators (migration), skills/kb-generation.md (documents), skills/brd-generation.md (by hand) — the reader must not care which bin/lib/discover-brds.sh:43 (the one path authority), bin/gate-check.sh:773 (check_stage_2), bin/brd-report.sh, bin/open-questions.sh migration,requirements,existing-app-change gate brd-validation 2 analysis/*/knowledge-base/reports/validation-report.md|analysis/knowledge-base/reports/validation-report.md|knowledge-base/reports/validation-report.md skills/brd-validation.md loop, run to Clean bin/gate-check.sh:777-807 (check_stage_2 Stop-condition test) migration,requirements,existing-app-change gate -brd-surface 2 analysis/brd-report.html bin/brd-report.sh (default output, bin/brd-report.sh:113) conversion-runbook.md §2 Stage-2 Surface row, parsed at bin/gate-check.sh:1284 (stage_surface_cells) migration,requirements,existing-app-change report +brd-surface 2 analysis/brd-report.html bin/brd-report.sh (default output, bin/brd-report.sh:113); also holds the pass-1 process map and the open questions (replaced docs/open-questions.html, 2026-10-16) conversion-runbook.md §2 Stage-2 Surface row, parsed at bin/gate-check.sh:1284 (stage_surface_cells) migration,requirements,existing-app-change report module-design 3 architecture/module-design.html|module-design.html skills/modularize-domain.md:88 (the human-facing sign-off artifact) skills/modularize-domain.md:109 (gate checklist), conversion-runbook.md:478 Stage-3 Surface row (parsed at bin/gate-check.sh:1284) migration,requirements,existing-app-change report fit-gap 3 architecture/fit-gap.md skills/architecture-blueprint.md Step 4 bin/gate-check.sh:912 (check_stage_3), bin/gate-check.sh:939 (blueprint.html staleness input) migration,requirements,existing-app-change gate blueprint-md 3 architecture/blueprint.md skills/architecture-blueprint.md Steps 2-6 — the markdown is the source of truth bin/gate-check.sh:939 (staleness input), skills/architecture-blueprint.md:54 (render source for blueprint.html) migration,requirements,existing-app-change report diff --git a/bin/questions-report.sh b/bin/questions-report.sh deleted file mode 100755 index 15cee9c2..00000000 --- a/bin/questions-report.sh +++ /dev/null @@ -1,299 +0,0 @@ -#!/bin/bash -# questions-report.sh — render a project's open questions as a reviewable HTML page. -# -# WHY THIS EXISTS -# --------------- -# bin/open-questions.sh already produces a chat-ready batch. That solves "the agent has the -# questions". It does not solve the two things the user actually reported missing: -# -# 1. There is no surface showing WHAT WAS FOUND — what got examined, what is still open, -# and which open items are blocking — that a human can read away from the terminal. -# 2. Most questions arrive with no proposed answer. A question with no proposed answer is -# work handed back to the user. A question with two named options and a recommendation -# is a decision they can make in five seconds. -# -# This script renders (1), and marks every question that lacks (2) as a defect in the -# generating stage rather than quietly presenting a bare question. -# -# Usage: -# bin/questions-report.sh [--stage N] [-o ] [--quiet] -# -# Default output: /docs/open-questions.html -# -# Exit codes: 0 = report written, nothing blocking. 1 = report written, blocking questions -# exist. 2 = usage/environment error. 3 = nothing was examined (NOT a clean -# result — same meaning as open-questions.sh). - -set -uo pipefail - -# Portability: resolve a real Python 3 by EXECUTING candidates, not by looking one up on PATH. -# On Windows `python3` is usually the Microsoft Store alias stub, which `command -v` finds and -# which then opens the Store instead of running. See bin/lib/portable.sh. -# shellcheck disable=SC1091 -. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/portable.sh" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COLLECTOR="$SCRIPT_DIR/open-questions.sh" - -PROJECT_DIR="" -STAGE_ARGS=() -OUT="" -QUIET=0 - -while [ $# -gt 0 ]; do - case "$1" in - --stage) STAGE_ARGS+=(--stage "${2:-}"); shift 2 ;; - -o|--output) OUT="${2:-}"; shift 2 ;; - --quiet) QUIET=1; shift ;; - -h|--help) sed -n '2,28p' "${BASH_SOURCE[0]}"; exit 0 ;; - -*) echo "questions-report.sh: unknown option $1" >&2; exit 2 ;; - *) if [ -z "$PROJECT_DIR" ]; then PROJECT_DIR="$1"; else STAGE_ARGS+=("$1"); fi; shift ;; - esac -done - -[ -n "$PROJECT_DIR" ] || { echo "usage: questions-report.sh [--stage N] [-o out.html]" >&2; exit 2; } -[ -d "$PROJECT_DIR" ] || { echo "questions-report.sh: not a directory: $PROJECT_DIR" >&2; exit 2; } -[ -x "$COLLECTOR" ] || [ -f "$COLLECTOR" ] || { echo "questions-report.sh: collector not found: $COLLECTOR" >&2; exit 2; } - -PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)" -[ -n "$OUT" ] || OUT="$PROJECT_DIR/docs/open-questions.html" - -JSON=$(bash "$COLLECTOR" "$PROJECT_DIR" "${STAGE_ARGS[@]+"${STAGE_ARGS[@]}"}" --json 2>/dev/null) -COLLECT_RC=$? - -# rc 3 means no BRDs and no sme-questions.md were found. That is not "no open questions"; -# it means the instrument found nothing to read, and reporting it as clean is the exact -# class of false-green this toolkit keeps getting bitten by. -# Belt and braces: the flag is read from the payload as well as the exit code. rc=3 was -# unreachable in --json mode until 2026-08-12, and this script trusting it alone is how that -# went unnoticed — it rendered a clean-looking report over an empty directory. -NOTHING=$(printf '%s' "$JSON" | grep -c '"nothingExamined": *true' | head -1) -if [ "$COLLECT_RC" -eq 3 ] || [ "${NOTHING:-0}" -gt 0 ] || [ -z "$JSON" ]; then - echo "questions-report.sh: nothing was examined (no BRDs, no sme-questions.md) — refusing to" >&2 - echo " write a report that would read as 'no open questions'. Exit 3." >&2 - exit 3 -fi - -mkdir -p "$(dirname "$OUT")" || { echo "questions-report.sh: cannot create $(dirname "$OUT")" >&2; exit 2; } - -require_py - -PROJECT_NAME="$(basename "$PROJECT_DIR")" -GENERATED="$(date '+%Y-%m-%d %H:%M')" - -# The collector's JSON goes through a temp FILE, not a pipe. `"$PY" - <<'PYEOF'` takes its -# program from stdin, so a pipe into the same command is swallowed by the heredoc and json.load -# reads an empty string. Costing a temp file is cheaper than an inline-quoted program. -JSON_TMP=$(mktemp "${TMPDIR:-/tmp}/qreport.XXXXXX.json") || exit 2 -trap 'rm -f "$JSON_TMP"' EXIT -printf '%s' "$JSON" > "$JSON_TMP" - -PROJECT_NAME="$PROJECT_NAME" GENERATED="$GENERATED" JSON_TMP="$JSON_TMP" \ - PROJECT_DIR="$PROJECT_DIR" OUT="$OUT" "$PY" - <<'PYEOF' -import html, json, os, sys - -with open(os.environ["JSON_TMP"], encoding="utf-8") as fh: - d = json.load(fh) -qs = d.get("questions", []) -counts = d.get("counts", {}) -ex = d.get("examined", {}) -blocking = d.get("blocking", 0) -project = os.environ["PROJECT_NAME"] -generated = os.environ["GENERATED"] -out_path = os.environ["OUT"] - -BLOCKING_STATES = {"UNRAISED", "UNRECOGNISED"} - -# Order: what needs the user first, then what is merely on record. -ORDER = ["UNRECOGNISED", "UNRAISED", "RAISED", "ASSUMED", "ANSWERED", "MOOT"] -STATE_NOTE = { - "UNRAISED": "Never put to you. These block the gate.", - "UNRECOGNISED": "Status text no rule could map. Treated as unraised — a status nobody can parse is not an answer.", - "RAISED": "Put to you, awaiting your reply.", - "ASSUMED": "A position was taken AND you consented to it. Recorded, not blocking.", - "ANSWERED": "Settled with a recorded answer.", - "MOOT": "Descoped — genuinely dead, not silently dropped.", -} - -def esc(s): - return html.escape(str(s or "")) - -# A question the user can act on needs a proposed answer. One without is not "a question", -# it is unfinished work by the stage that produced it — so it gets called out as such. -def has_proposal(q): - return bool((q.get("assumption") or "").strip()) - -groups = {} -for q in qs: - groups.setdefault(q.get("state", "UNRECOGNISED"), []).append(q) - -actionable = [q for q in qs if q.get("state") in BLOCKING_STATES] -no_proposal = [q for q in actionable if not has_proposal(q)] - -rows = [] -for st in ORDER: - n = counts.get(st, 0) - if n: - cls = "b" if st in BLOCKING_STATES else "" - rows.append(f'{st}{n}{esc(STATE_NOTE.get(st,""))}') -tbl = "\n".join(rows) - -stage_filter = ex.get("stageFilter") -scope = f"stage ≤ {esc(stage_filter)}" if stage_filter else "all stages" - -cards = [] -for st in ORDER: - items = groups.get(st, []) - if not items: - continue - badge = "block" if st in BLOCKING_STATES else "ok" - cards.append(f'

{esc(st)} {len(items)}

') - cards.append(f'

{esc(STATE_NOTE.get(st,""))}

') - for q in items: - prop = (q.get("assumption") or "").strip() - raw = (q.get("rawStatus") or "").strip() - reason = (q.get("reason") or "").strip() - bits = [f'
'] - bits.append(f'
{esc(q.get("id","?"))} {esc(q.get("origin",""))}
') - bits.append(f'
{esc(q.get("question",""))}
') - if prop: - bits.append( - '
Drafting position — confirm or overturn' - f'
{esc(prop)}
') - elif st in BLOCKING_STATES: - bits.append( - '
No proposed answer' - '
This question was handed over bare. Before putting it to the user, the owning ' - 'stage should propose at least two named options and a recommendation — otherwise the ' - 'user is being asked to do the analysis the stage was meant to do.
') - meta = [] - if raw: - meta.append(f'recorded as: {esc(raw[:160])}') - if reason: - meta.append(esc(reason)) - if q.get("stage") is not None: - meta.append(f'stage {esc(q.get("stage"))}') - if meta: - bits.append('
' + " · ".join(meta) + "
") - bits.append("
") - cards.append("".join(bits)) - -banner_cls = "fail" if blocking else "pass" -banner_txt = (f"{blocking} question(s) have never been put to the user — this gate is blocked." - if blocking else - "No unraised questions. Every question on record has been seen by the user.") - -gapline = "" -if no_proposal: - gapline = (f'') - -doc = f""" - - -Open questions — {esc(project)} - -
-

Open questions — {esc(project)}

-

Generated {esc(generated)} · scope: {scope} · - examined {esc(ex.get("brdFiles",0))} BRD file(s) and {esc(ex.get("smeQuestionFiles",0))} SME question file(s), - {esc(ex.get("duplicatesCollapsed",0))} duplicate(s) collapsed

- - - {gapline} - -

What was found

- - {tbl} -
StateCountMeaning
- - {"".join(cards)} - - -
-""" - -with open(out_path, "w") as f: - f.write(doc) -PYEOF - -PY_RC=$? -[ "$PY_RC" -eq 0 ] || { echo "questions-report.sh: render failed (python exit $PY_RC)" >&2; exit 2; } - -if [ "$QUIET" -eq 0 ]; then - echo "Wrote: $OUT" - # Reads the temp file, not a pipe, and no 2>/dev/null: a summary that silently prints - # nothing when it breaks is worse than one that says why. - JSON_TMP="$JSON_TMP" "$PY" - <<'PYSUM' -import json, os -with open(os.environ["JSON_TMP"], encoding="utf-8") as fh: - d = json.load(fh) -c = d.get("counts", {}) -qs = d.get("questions", []) -bare = [q for q in qs - if q.get("state") in ("UNRAISED", "UNRECOGNISED") - and not (q.get("assumption") or "").strip()] -print(" blocking: %s total: %s without a proposed answer: %s" - % (d.get("blocking", 0), c.get("total", 0), len(bare))) -PYSUM -fi - -[ "$COLLECT_RC" -eq 1 ] && exit 1 -exit 0 diff --git a/claude-hooks/hooks/interview-gate.sh b/claude-hooks/hooks/interview-gate.sh index bf7a3f1f..b36c88df 100755 --- a/claude-hooks/hooks/interview-gate.sh +++ b/claude-hooks/hooks/interview-gate.sh @@ -50,7 +50,7 @@ except Exception: print("True") PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" TOOLKIT="__TOOLKIT_ROOT__" OQ="$TOOLKIT/bin/open-questions.sh" -QR="$TOOLKIT/bin/questions-report.sh" +QR="$TOOLKIT/bin/brd-report.sh" IM="$TOOLKIT/bin/interview-mode.sh" [ -x "$OQ" ] || exit 0 @@ -92,8 +92,8 @@ printf '%s' "$FP" > "$STATE" 2>/dev/null || true REPORT="" if [ -x "$QR" ]; then - "$QR" "$PROJECT_DIR" -o "$PROJECT_DIR/docs/open-questions.html" --quiet >/dev/null 2>&1 - [ -f "$PROJECT_DIR/docs/open-questions.html" ] && REPORT="$PROJECT_DIR/docs/open-questions.html" + "$QR" "$PROJECT_DIR" --html "$PROJECT_DIR/analysis/brd-report.html" --quiet >/dev/null 2>&1 + [ -f "$PROJECT_DIR/analysis/brd-report.html" ] && REPORT="$PROJECT_DIR/analysis/brd-report.html" fi # In auto mode — and when the user is remote — the report is the whole job. Refreshing it @@ -113,7 +113,7 @@ REASON="$BLOCKING question(s) in this project have never been put to the user, a Before you stop: put them in the chat. Read $TOOLKIT/skills/interview-protocol.md — every question carries two named options and your recommendation; a bare question hands your analysis back to the user. Batch them, then end the turn and wait for answers. " [ -n "$REPORT" ] && REASON="${REASON} -Triage report written for the user to review: $REPORT — give them this path. +BRD report (open questions under the process map) written for the user to review: $REPORT — give them this path. " REASON="${REASON} This project is in interview mode '$MODE'. If the user wants speed over involvement here, diff --git a/docs/pipeline-walks.html b/docs/pipeline-walks.html index dbd31540..ad08b7bb 100644 --- a/docs/pipeline-walks.html +++ b/docs/pipeline-walks.html @@ -169,7 +169,7 @@

Migration Legacy source code existsP KickoffScaffold the project. Answer every intake question. Propose the entry mode with evidence and record it CONFIRMED. Gate: every intake question answered. Owner ba-agent.bin/init-project.sh <project>
bin/init-agents.sh <session-root>
bin/gate-check.sh <project> P 0 Triage ✋Inventory every source file. Grade sufficiency (SPECIFICATION / OUTLINE / SKETCH). Decide extract-or-not per structure and fill the coverage matrix. Scan documents. Run the CAC-1 scope brainstorm, order the slices, get sign-off in triage.md. Owner ba-agent.bin/source-sufficiency.sh init <project>
bin/source-sufficiency.sh report <project>
bin/interview-mode.sh --set … (only after the user agrees)
bin/triage-report.sh <project>
bin/source-sufficiency.sh --refresh for late files
bin/gate-check.sh <project> 0 1 AnalysisPath A: run the platform pipeline (extract, knowledge base, BRD scaffolds, reports). Path B: convert documents to Markdown and build the KB. Path C: interview the SME. Every source file must name the artifact that consumed it. Gate: source ledger clean, four extraction quality checks. CAC-1b.cd pipelines/<platform>/pipeline && npm install
node run.js · node run.js 2 · node run.js 3 · npm run reports
bin/html-to-md.sh <project> · bin/images-to-md.sh
bin/source-ledger.sh report <project>
bin/source-ledger.sh check <project>
bin/extraction-report.sh <project>
bin/gate-check.sh <project> 1 - 2 RequirementsTurn scaffolds into validated BRDs, one per feature. Drive blocking open questions to zero. Lock facts. CAC-2 BRD sign-off, CAC-3 architecture entry.bin/brd-report.sh <project>
bin/open-questions.sh <project> --stage 2
bin/questions-report.sh · bin/facts-lock.sh · bin/question-kinds.sh
bin/gate-check.sh <project> 2 + 2 RequirementsTurn scaffolds into validated BRDs, one per feature. Drive blocking open questions to zero. Lock facts. CAC-2 BRD sign-off, CAC-3 architecture entry.bin/brd-report.sh <project> [--stage N]
bin/open-questions.sh <project> --stage 2
bin/facts-lock.sh · bin/question-kinds.sh
bin/gate-check.sh <project> 2 3 Design ✋Module boundaries, blueprint, fit-gap, security model, integrations. Design system and one wireframe per screen. CAC-4. Owner architect-agent, interviews by ba-agent.skills: modularize-domain.md, architecture-blueprint.md, design-artifacts.md
outputs: architecture/blueprint.html, design/design-system.html, design/wireframes/*.html
bin/gate-check.sh <project> 3 4 Build plan ✋Dependency-ordered script sequence, coverage ledger, one brief per module. CAC-5, then the build-ready wiring check.bin/coverage-check.sh
bin/gate-check.sh <project> 4
bin/gate-check.sh <project> build-ready 5 BuildWalking skeleton, then the five-stage module loop per module (see below). Coherence pass every two or three modules../mxcli check s.mdl -p app.mpr --references
bin/exec.sh s.mdl · ./mxcli lint -p app.mpr
project-bin/verify-module.sh <Module>
./mxcli graph-report -p app.mpr
bin/gate-check.sh <project> 5 diff --git a/docs/windows-test-run.md b/docs/windows-test-run.md index ba64dda7..08022bc1 100644 --- a/docs/windows-test-run.md +++ b/docs/windows-test-run.md @@ -124,7 +124,7 @@ I3 node project-bin/page-fidelity.js --no-log tests/wave2/fixtures/page-fidelit I4 node project-bin/page-fidelity.js --no-log tests/wave2/fixtures/page-fidelity-mocks/mocked-list.html DemoList tests/wave2/fixtures/page-fidelity-mocks/demo-list.mdl I5 bash bin/html-to-md.sh "tests/wave2/fixtures/html-to-md/Fleet Portal Requirements.html" > ~/mxtk-win-run/fleet.md Then: wc -l ~/mxtk-win-run/fleet.md -I6 bash bin/brd-report.sh --help ; bash bin/triage-report.sh --help ; bash bin/questions-report.sh --help +I6 bash bin/brd-report.sh --help ; bash bin/triage-report.sh --help (exit 0 and a usage text each = PASS; these are the report tools with no committed input) I7 bash bin/bug-lookup.sh CE0053 I8 bash bin/token-burn.sh --help diff --git a/process/improvement-plan-e2e-reporting.md b/process/improvement-plan-e2e-reporting.md index ddd401b6..31bc6448 100644 --- a/process/improvement-plan-e2e-reporting.md +++ b/process/improvement-plan-e2e-reporting.md @@ -270,7 +270,7 @@ These close options left open in Findings 1–4 and set the constraints for the 2. **The number stops lying instead.** The report leads with coverage, not pass-rate: *"58% of checks did not run"* above the fold, and no percentage anywhere may use a denominator that excludes faults. This blocks nobody and is the only thing that actually failed. - Precedent already in-repo: `bin/questions-report.sh:72` refuses to render a clean-looking zero. + Precedent already in-repo: `bin/brd-report.sh` (exit 3) refuses to render a clean-looking zero (it took over from the retired `questions-report.sh`). 3. **Plumbing before gating.** Findings 5–8 are fixed first, so that any later signal fires on instruments that are actually present. 4. **Four fallback levels for the ledger check**, keyed on what is present rather than declared diff --git a/project-bin/assemble-prototype.js b/project-bin/assemble-prototype.js index a4b1ae58..ca6e1fe0 100755 --- a/project-bin/assemble-prototype.js +++ b/project-bin/assemble-prototype.js @@ -283,6 +283,11 @@ const SCRIPT = ` })(); `; +// The process map (process-map.js, skills/process-map.md) is built from the same wireframes and shows +// each on its step. When the project has a process model, the prototype links to the map beside it +// and the map links back (its header). Keyed on the model, not on the built map, so the order of +// the two builds does not matter. +const hasMap = fs.existsSync(path.join(path.dirname(path.resolve(outFile)), 'process-model.json')); const index = screens.map(s => '
  • ' + esc(s.title) + ' #/' + esc(s.route) + '
  • ').join('\n'); const out = '\n' + @@ -294,7 +299,8 @@ const out = '\n' + '\n' + '\n\n' + '\n' + + '' + + (hasMap ? '
    Process map: where each screen sits in the work' : '') + '\n' + '\n' + '
    \n' + screens.map(screenSection).join('') + '
    \n' + '\n\n'; diff --git a/project-bin/process-map.js b/project-bin/process-map.js index 9957a6a1..8e4d8394 100644 --- a/project-bin/process-map.js +++ b/project-bin/process-map.js @@ -23,8 +23,17 @@ // process-map.js validate [--strict] check the model (and anchors, when present) // process-map.js anchor derive anchors mechanically, keep judged ones, write // the anchors file, list what is still unanchored +// process-map.js migrate a model whose "anchors" key points at an old path (say the +// prototype folder): move the file to design/question-anchors.json, +// keep the old one as *.bak, rewrite the key, print what carried over // process-map.js build validate, then write design/process-map.html +// process-map.js build --embed the same maps as a bare page for an