diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fcea574..43fa3277 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 +- fix(release-review): **the v1.0 release review: the multilingual skill loses its em and en dashes (wording only, content unchanged; its routing row keeps the trigger separator the agent stubs cut on), `doctor.sh` warns when a hook copy in `~/.claude/hooks` calls a toolkit script that no longer exists (it names the script and says to re-run `bin/install-claude-hooks.sh --full`; a warning, never a failure), and the `.gitignore` line for `.claude/progress-report-sessions.txt` now also reaches a project checked out as a git worktree (`.git` is a file there) in `doctor.sh` and `sync-project.sh`; `check-ignore --no-index` stops the line being appended again when the file is already tracked, and a `git rm --cached` hint is printed.** Tests: new `test-doctor-stale-hooks.sh`; worktree and tracked-file cases added to `test-progress-report-gitignore.sh`. CHANGELOG tidy (a stray blank line, one credit). — MendixMau +- fix(progress-report): **round 6 of the review: the session cache is machine-local and tied to its project, the first cached run is not slower than a cold one, and the build ledger decides the Stage 4 to 5 switch.** (1) `.claude/progress-report-sessions.txt` holds absolute session-log paths, so `init-project.sh`, `doctor.sh` and `sync-project.sh` now put `/.claude/progress-report-sessions.txt` in the project's `.gitignore`, the way they do for the doctor receipt and `toolkit.env`. (2) The cache header records the project's real path and the attribution rules (25% of tool calls, 20 calls); a copied or moved project, or a changed threshold, reads it as cold, so a copy never lists the original's sessions. (3) Files skipped by the 300-file cap are recorded as `no`, so a cached run never reads more files than a cold one. (4) With rows in `metrics-ledger.tsv` (else BUILD-LOG) the switch comes from the ledger; an earlier exec.sh call or build skill in a session log is ignored, the timed sign-off rule stays, and the footer says how the switch is chosen. Tests: F15 to F17 in `test-progress-report-field-fixes.sh`, new `test-progress-report-gitignore.sh` — MendixMau - fix(progress-report): **polish on a fresh and an updated demo project: a "done " plan row counts as built in the card, the plan header and the phase badge alike; a placeholder gate failure on a stage after the current one reads PENDING, not FAIL; the no-session-log notice is one calm line with the regenerate hint inside "How this was measured"; the test-moments table says unit tests run for the flows chosen at CAC-5, not "when switched on".** — field review of the #434 report - fix(progress-report): **round 6 of the review: the session cache is machine-local and tied to its project, the first cached run is not slower than a cold one, and the build ledger decides the Stage 4 to 5 switch.** (1) `.claude/progress-report-sessions.txt` holds absolute session-log paths, so `init-project.sh`, `doctor.sh` and `sync-project.sh` now put `/.claude/progress-report-sessions.txt` in the project's `.gitignore`, the way they do for the doctor receipt and `toolkit.env`. (2) The cache header records the project's real path and the attribution rules (25% of tool calls, 20 calls); a copied or moved project, or a changed threshold, reads it as cold, so a copy never lists the original's sessions. (3) Files skipped by the 300-file cap are recorded as `no`, so a cached run never reads more files than a cold one. (4) With rows in `metrics-ledger.tsv` (else BUILD-LOG) the switch comes from the ledger; an earlier exec.sh call or build skill in a session log is ignored, the timed sign-off rule stays, and the footer says how the switch is chosen. Tests: F15 to F17 in `test-progress-report-field-fixes.sh`, new `test-progress-report-gitignore.sh` — review of PR #434 - fix(progress-report): **round 5 of the field test: "building now" marks the phase of the last successful build step (its mdlsource folder, else the script name matched to the plan, else the highest active phase; blocked steps and earlier amber rows do not move it); discovery of sessions from other folders counts only tool calls that operate on the project (at least 25% of a session's calls and at least 20, gaps over 30 minutes cut, sessions that run progress-report.py excluded, text mentions never count) and lists the sessions used in a collapsed element; a Stage 4 sign-off row with a date but no time puts the Stage 5 switch at the first build activity on or after that date (marked inferred) instead of midnight; the exec log is now the primary source for build activity: `.claude/loop/metrics-ledger.tsv` (written by exec.sh, UTC shown as local time) gives the Stage 5 build facts with or without a session log (runs, applied, failed, blocked, restored, exec time, wall span, runs per phase, latest model score, share of exec time lost to failed or restored runs), the last build step, the building-now phase and the Stage 4 to 5 switch, with BUILD-LOG and then session activity as fallbacks; docs/progress/TIME-LEDGER.md is an optional hand-written extra and its reader maps columns by each table's header.** Tests F10 (existing-project fixture) and F11 (older toolkit, then sync-project.sh) added. — MendixMau @@ -26,7 +28,6 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m - new(progress-report): **`architecture/progress-report.html` replaces `architecture/build-plan.html`, and adds time, token and tips views from the session log.** `build-plan-status.sh --html` now writes one self-contained page: every phase and the built versus proven views the old page had, stages P to 6 with artifact links, a progress bar and stage nav, and, when a Claude Code log for the project folder exists, time per stage, sub-agent use, context size, an estimated token cost (rates in an optional file, placeholder defaults, labelled as estimates), a Run health strip and a speed panel. Tips and tricks sit in a narrow side column (after the title, headline and stage cards on a phone), in one collapsed box per theme (Context, Tokens and cost, Speed, Health, Sub-agents), empty themes hidden. Only patterns with a concrete action in the project are shown, ranked by time or tokens lost, each with what was seen, why it matters and one fix. Severity is calm: red only for a key script (doctor, gate-check, exec, tests) failing every run with at least 3 runs, or a hang over 30 minutes; context is never red (a Context tip is amber when the window was reached or a compaction happened). Required gates and builds on the inherent list never produce a hang or slow tip and never change Run health; tips that lose under a minute are dropped; tips name real scripts or commands only. Costs that come from how the toolkit and mxcli work (build time, required gates and reviews, waiting on builds or the user, the mandated review model) are flagged inherent in the script and never shown. The context window is 1M only when the log records it (a `[1m]` model id or a context-1m beta); otherwise 200k, and a context above 200k with auto compactions caps the window at the compaction point and is labelled inferred. Never a percentage over 100. Every threshold sits in one block at the top of `project-bin/progress-report.py`, and the footer records the project folder and session log paths used. Only numbers, stage names, script names, file paths and sub-agent short descriptions are read from the log, never message text. Without a log the project sections render and the page says time and token data are unavailable. The page refreshes after every `exec.sh` slice and reloads itself every 30 s only while the build is active. PROJECT.md `Progress report: off | ask | auto` (default `ask`) controls it, and `--open` opens it. An existing `build-plan.html` is never deleted, and gate-check and status.sh still accept it. `--json` is unchanged. Skill words fell by about 100 (Step 10 and step 16 shortened). Tests: `tests/wave2/test-progress-report.sh`, `test-build-plan-json.sh`, `test-surface-column.sh`. — MendixMau - 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 - 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/bin/doctor.sh b/bin/doctor.sh index bd888363..0205ea44 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -533,6 +533,45 @@ fi fi +# --- installed Claude hooks: copies that call a toolkit script which is gone ------------------ +# +# install-claude-hooks.sh writes COPIES into ~/.claude/hooks with the toolkit path filled in. +# When a toolkit update removes a script (bin/questions-report.sh was retired in v1.0), an old +# copy keeps calling it: the hook still runs but silently loses its output. A warning only, +# never a failure: the hooks are user-global and optional. +if [ -d "$HOME/.claude/hooks" ]; then + head_ "Installed Claude hooks (~/.claude/hooks)" + STALE_HOOKS=0 + HOOKS_SEEN=0 + for hf in "$HOME"/.claude/hooks/*.sh; do + [ -f "$hf" ] || continue + HOOKS_SEEN=$((HOOKS_SEEN + 1)) + # The toolkit root the installer wrote, e.g. TOOLKIT="/path/to/toolkit". + HOOK_ROOT="$(sed -n 's/^[A-Z_]*TOOLKIT[A-Z_]*="\(\/[^"]*\)".*/\1/p' "$hf" | head -1)" + HOOK_REFS="" + if [ -n "$HOOK_ROOT" ]; then + HOOK_REFS="$(grep -oE '[$][{]?[A-Z_]*TOOLKIT[A-Z_]*[}]?/[A-Za-z0-9_./-]+[.](sh|py|js|tsv)' "$hf" 2>/dev/null \ + | sed -e 's/^[$][{]*[A-Z_]*[}]*//' -e "s|^|$HOOK_ROOT|" | sort -u)" + fi + HOOK_GONE="" + while IFS= read -r ref; do + [ -n "$ref" ] || continue + [ -e "$ref" ] || HOOK_GONE="$HOOK_GONE ${ref##*/}" + done </dev/null; then + # -e, not -d: in a git worktree .git is a file. --no-index: a tracked file is never "ignored" otherwise, + # and the line would be appended again on every run. + if [ -e "$PROJECT_DIR/.git" ] && ! git -C "$PROJECT_DIR" check-ignore -q --no-index .claude/progress-report-sessions.txt 2>/dev/null; then printf '\n# Machine-local progress report session cache (absolute session-log paths)\n/.claude/progress-report-sessions.txt\n' \ >> "$PROJECT_DIR/.gitignore" 2>/dev/null || true note "(added /.claude/progress-report-sessions.txt to the project's .gitignore — the cache is machine-local)" fi + if [ -e "$PROJECT_DIR/.git" ] && git -C "$PROJECT_DIR" ls-files --error-unmatch .claude/progress-report-sessions.txt >/dev/null 2>&1; then + note "(.claude/progress-report-sessions.txt is already tracked by git; untrack it with: git rm --cached .claude/progress-report-sessions.txt)" + fi fi if [ "$FAIL" -gt 0 ]; then diff --git a/bin/sync-project.sh b/bin/sync-project.sh index 1e5b56fd..ba56ae24 100755 --- a/bin/sync-project.sh +++ b/bin/sync-project.sh @@ -1613,8 +1613,9 @@ fi # ── Progress report session cache: machine-local, keep it out of git ───────────────────── # It holds absolute session-log paths (user name, session ids). Same shape as doctor.sh's # receipt and toolkit.env lines; a project synced from an older toolkit has no such line. -if [ "$WIRED" -eq 1 ] && [ -d "$PROJECT_DIR/.git" ] \ - && ! git -C "$PROJECT_DIR" check-ignore -q .claude/progress-report-sessions.txt 2>/dev/null; then +# -e, not -d: in a git worktree .git is a file. --no-index: a tracked file is never "ignored" otherwise. +if [ "$WIRED" -eq 1 ] && [ -e "$PROJECT_DIR/.git" ] \ + && ! git -C "$PROJECT_DIR" check-ignore -q --no-index .claude/progress-report-sessions.txt 2>/dev/null; then if [ "$DRY_RUN" -eq 1 ]; then echo "Would add: /.claude/progress-report-sessions.txt to .gitignore (machine-local cache)" else @@ -1624,6 +1625,10 @@ if [ "$WIRED" -eq 1 ] && [ -d "$PROJECT_DIR/.git" ] \ fi CHANGES=$((CHANGES + 1)) fi +if [ "$WIRED" -eq 1 ] && [ -e "$PROJECT_DIR/.git" ] \ + && git -C "$PROJECT_DIR" ls-files --error-unmatch .claude/progress-report-sessions.txt >/dev/null 2>&1; then + echo "Note: .claude/progress-report-sessions.txt is already tracked by git; untrack it with: git rm --cached .claude/progress-report-sessions.txt" +fi # ── Starlark lint rules ────────────────────────────────────────────────────────────────── # Gated on WIRED for the same reason as the crash net: a mistyped path must not scatter diff --git a/skills/learned-multilingual-app.md b/skills/learned-multilingual-app.md index f269daa0..0618d4c1 100644 --- a/skills/learned-multilingual-app.md +++ b/skills/learned-multilingual-app.md @@ -1,4 +1,4 @@ -# A second language — the four decisions that decide whether a user ever sees the translations +# 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 @@ -8,7 +8,7 @@ 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 +failed. These were 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. --- @@ -16,7 +16,7 @@ failed — four separate failures, each green on `mxcli check`, `exec.sh` (mxbui ## 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. +> language, the user's language and the switch, and a gate looks at none of them. | What the user saw | What was actually wrong | § | |---|---|---| @@ -30,19 +30,19 @@ failed — four separate failures, each green on `mxcli check`, `exec.sh` (mxbui ## 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. +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. +statements that wrote the captions. That is 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; +English platform texts (validation messages, paging, confirmation buttons) are translated; otherwise they stay English in the Spanish UI. ```sql @@ -59,10 +59,10 @@ create or modify translations without marketplace for es_ES ( 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. +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 +This shows 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 @@ -70,13 +70,13 @@ re-applied after every caption change — this project re-applied both the same 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. +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 +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 @@ -97,13 +97,13 @@ end if; 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`* +- **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 → +- **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. @@ -111,13 +111,13 @@ What is **not** established is that a normal user can never write their own `Use 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`? +- `./mxcli -p .mpr -c 'DESCRIBE USER ROLE ""'` shows whether `ModuleRoles` includes `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. +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. @@ -148,12 +148,12 @@ One nanoflow per language, two buttons in the layout's top bar (`alter layout 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 +`mxcli lint` flags the nanoflow with 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: +Highlight the active language from **``**, which Mendix writes from the user's language. +There is no data read, so no access question at all: ```scss html[lang^="es"] .lang-switch-es, @@ -168,12 +168,12 @@ code. Measured here: `pt` after switching to `pt_PT`, with the menu reading *His `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. +sign-in; §3 and §4 do. -## 6. Verify in the running app — per language, per role +## 6. Verify in the running app, per language and 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: +A green gate proves none of §1 to §5. **Sign in as a non-admin user**, because 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). diff --git a/tests/wave2/test-doctor-stale-hooks.sh b/tests/wave2/test-doctor-stale-hooks.sh new file mode 100755 index 00000000..7a2ab150 --- /dev/null +++ b/tests/wave2/test-doctor-stale-hooks.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# test-doctor-stale-hooks.sh: bin/doctor.sh warns (never fails) when a hook COPY installed in +# ~/.claude/hooks calls a toolkit script that no longer exists (bin/questions-report.sh was retired +# in v1.0), and tells the user to re-run bin/install-claude-hooks.sh --full. Fake HOME, no real hooks touched. +# Usage: bash tests/wave2/test-doctor-stale-hooks.sh +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/doctor-stale-hooks.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT +export MXTK_NO_GUIDE=1 MXTK_LEAKGUARD_DENYFILE="$WORK/deny" +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " PASS: $1"; } +bad() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } +has() { grep -q -- "$2" "$1"; } + +mkhook() { # mkhook