diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f107cf6..f472375c 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(process-map): migrate no longer trusts a regex for the model key: it checks the top-level `anchors` changed and otherwise re-writes the model, and says so instead of claiming "only that key was changed"; a string `targets` in an old file counts as a one-item list instead of crashing; unfilled entries already in the current anchors file are listed, not dropped silently (follow-up to #438 review) — MendixMau - 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 diff --git a/project-bin/process-map.js b/project-bin/process-map.js index 8e4d8394..9f069643 100644 --- a/project-bin/process-map.js +++ b/project-bin/process-map.js @@ -86,6 +86,7 @@ function anchorEntries(j) { for (const e of list) { if (!e || !e.id) { res.noId++; continue; } let targets = e.targets || (e.target ? [e.target] : null); + if (targets && !Array.isArray(targets)) targets = [targets]; if (targets) targets = targets.map(t => typeof t === 'string' ? { type: 'step', step: t } : t); if (!targets) { targets = []; @@ -139,8 +140,14 @@ if (cmd === 'migrate') { writeAtomic(target, JSON.stringify(out, null, 2) + '\n'); const rawText = fs.readFileSync(modelFile, 'utf8'); const keyRe = /("anchors"\s*:\s*)"[^"]*"/; - if (keyRe.test(rawText)) writeAtomic(modelFile, rawText.replace(keyRe, (m, k) => k + JSON.stringify(want))); - else { const m = readJson(modelFile, 'process model'); m.anchors = want; writeAtomic(modelFile, JSON.stringify(m, null, 2) + '\n'); } + // The regex hits the first "anchors" string at any depth; keep its result only when the TOP-LEVEL key changed. + let keyOnly = false; + if (keyRe.test(rawText)) { + const patched = rawText.replace(keyRe, (m, k) => k + JSON.stringify(want)); + let top = null; try { top = JSON.parse(patched).anchors; } catch (e) { /* fall through */ } + if (top === want) { writeAtomic(modelFile, patched); keyOnly = true; } + } + if (!keyOnly) { const m = readJson(modelFile, 'process model'); m.anchors = want; writeAtomic(modelFile, JSON.stringify(m, null, 2) + '\n'); } let bak = old + '.bak', n = 1; while (fs.existsSync(bak)) bak = old + '.bak.' + (n++); fs.renameSync(old, bak); @@ -149,8 +156,9 @@ if (cmd === 'migrate') { path.relative(root, old) + ' to ' + want + (haveE.entries.length ? ' (' + haveE.entries.length + ' already there)' : '')); for (const m of notes) console.log('migrate: ' + m); if (oldE.noTarget.length) console.log('migrate: ' + oldE.noTarget.length + ' old entr' + (oldE.noTarget.length === 1 ? 'y' : 'ies') + ' placed nothing and ' + (oldE.noTarget.length === 1 ? 'was' : 'were') + ' left out: ' + oldE.noTarget.join(', ')); + if (haveE.noTarget.length) console.log('migrate: ' + haveE.noTarget.length + ' entr' + (haveE.noTarget.length === 1 ? 'y' : 'ies') + ' in the current anchors file placed nothing and ' + (haveE.noTarget.length === 1 ? 'was' : 'were') + ' left out: ' + haveE.noTarget.join(', ')); if (oldE.noId) console.log('migrate: ' + oldE.noId + ' old entr' + (oldE.noId === 1 ? 'y' : 'ies') + ' had no id and ' + (oldE.noId === 1 ? 'was' : 'were') + ' left out'); - console.log('migrate: old file kept as ' + path.relative(root, bak) + '; model "anchors" is now ' + want + ' (only that key was changed)'); + console.log('migrate: old file kept as ' + path.relative(root, bak) + '; model "anchors" is now ' + want + ' (' + (keyOnly ? 'only that key was changed' : 'the model was re-written with that key set') + ')'); console.log('migrate: next, run anchor to place anything new and list what is still unanchored'); process.exit(0); } diff --git a/tests/wave2/test-process-map-field.sh b/tests/wave2/test-process-map-field.sh index c8fe53b0..8fc7d884 100755 --- a/tests/wave2/test-process-map-field.sh +++ b/tests/wave2/test-process-map-field.sh @@ -182,6 +182,54 @@ cp "$FIX/design/question-anchors.json" "$P/design/question-anchors.json" OUT=$(node "$PM" migrate --project "$P" 2>&1) has "two judged placements: the current one is kept and the loss is printed" "$OUT" "the current placement is kept" +echo "== migrate: the top-level key is patched, a nested look-alike is not ==" +P=$(fresh nest); mkdir -p "$P/design/old" +node "$PM" anchor --project "$P" >/dev/null 2>&1 +cp "$P/design/question-anchors.json" "$P/design/old/a.json"; rm "$P/design/question-anchors.json" +"$PY" - "$P/design/process-model.json" <<'PY' +import json,sys +p=sys.argv[1]; m=json.load(open(p)); d={'meta':{'anchors':'notes.txt'}}; d.update(m); d['anchors']='design/old/a.json'; json.dump(d,open(p,'w'),indent=2) +PY +OUT=$(node "$PM" migrate --project "$P" 2>&1) +R=$("$PY" -c " +import json;m=json.load(open('$P/design/process-model.json')) +print(m['anchors'], m['meta']['anchors'])") +[ "$R" = "design/question-anchors.json notes.txt" ] && ok "top-level key changed, nested one untouched" || bad "top-level key changed" "$R" +hasnt "output does not claim only that key changed" "$OUT" "only that key was changed" +has "output says the model was re-written" "$OUT" "re-written with that key set" +P=$(fresh plain); mkdir -p "$P/design/old" +node "$PM" anchor --project "$P" >/dev/null 2>&1 +cp "$P/design/question-anchors.json" "$P/design/old/a.json"; rm "$P/design/question-anchors.json" +setanch "$P" design/old/a.json +has "a plain model still claims only the key changed" "$(node "$PM" migrate --project "$P" 2>&1)" "only that key was changed" + +echo "== migrate: a string target in an old file is a one-item list ==" +P=$(fresh strt); mkdir -p "$P/design/old"; rm -f "$P/design/question-anchors.json" +cat > "$P/design/old/a.json" <<'J' +{"anchors":[{"id":"F001:Q01","targets":"P1-02"}]} +J +setanch "$P" design/old/a.json +OUT=$(node "$PM" migrate --project "$P" 2>&1); RC=$? +[ "$RC" -eq 0 ] && ok "string targets do not crash migrate" || bad "string targets crash migrate" "$OUT" +R=$("$PY" -c " +import json;e=json.load(open('$P/design/question-anchors.json'))['anchors'][0] +print(e['targets'])") +[ "$R" = "[{'type': 'step', 'step': 'P1-02'}]" ] && ok "string target became one step target" || bad "string target became one step target" "$R" + +echo "== migrate: unfilled entries in the current file are listed ==" +P=$(fresh cur0); mkdir -p "$P/design/old" +node "$PM" anchor --project "$P" >/dev/null 2>&1 +"$PY" - "$P/design/question-anchors.json" <<'PY' +import json,sys +p=sys.argv[1]; d=json.load(open(p)); d['anchors'].append({'id':'F001:Q77','kind':'question','targets':[]}); json.dump(d,open(p,'w')) +PY +cat > "$P/design/old/a.json" <<'J' +{"anchors":[{"id":"F001:Q78","step":"P1-02"}]} +J +setanch "$P" design/old/a.json +OUT=$(node "$PM" migrate --project "$P" 2>&1) +has "current-file entry with no targets is named" "$OUT" "in the current anchors file placed nothing and was left out: F001:Q77" + echo "== anchor: a judged entry with empty targets is unanchored, not global ==" P=$(fresh emp) "$PY" - "$P/design/question-anchors.json" <<'PY'