Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<html lang>`; seeded users start in a visible language. `learned-mdl-cannot-express.md` gains the LanguageSelector row. — CarlesZ
Expand Down
14 changes: 11 additions & 3 deletions project-bin/process-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
48 changes: 48 additions & 0 deletions tests/wave2/test-process-map-field.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading