From ab346f723cbd88ac2949483bfd969ba01c614a4b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:11:58 +0000 Subject: [PATCH] fix(lint): CONV020 default scope "*" and page actions count as feedback (#229, #245) PROJECT_MODULES defaults to "*" (all modules the lint run includes, vendor list excluded by lint-gate) so a fresh install is no longer inert. ShowPage/ClosePage/ShowHomePage count as feedback (presence only; action_type strings verified against the mxcli catalog builder). lint-gate --update-baseline drops _rule findings. Install-time module derivation not done: the existing vendor-exclusion mechanism makes it unnecessary. Checks: bash -n lint-gate.sh, tests/test-lint-delivery.sh (ALL GREEN, updated for the new default), test-stock-hashes.sh (16/0), leak guard, portability, check-scripts. Starlark behaviour fixture NOT added/run: no mxcli binary or rule runner in this repo. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + lint-rules/README.md | 5 +-- lint-rules/conv020_action_user_feedback.star | 34 ++++++++++++-------- project-bin/lint-gate.sh | 7 ++++ tests/test-lint-delivery.sh | 2 +- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3d67a9..56e9c2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- fix(lint): **CONV020 works out of the box and counts page navigation as feedback.** `PROJECT_MODULES` now defaults to `"*"` (every module the lint run includes, minus `.claude/lint-vendor-modules.txt`) instead of an empty tuple that made a fresh install inert; ShowPage/ClosePage/ShowHomePage actions count as user feedback (presence only; refresh-in-client is not exposed by mxcli, noted in the rule header). `lint-gate.sh --update-baseline` no longer baselines `_rule` (self-reported blindness) findings. Starlark fixture not run: no mxcli binary or rule runner in this repo; installer fixture `test-lint-delivery.sh` green (#229, #245) — MendixMau - perf(routing): **CLAUDE.local.md routing carries triggers only.** The `baseline` view (the copy loaded into every session of every project) now cuts each row's "when" cell at its first " — " and replaces the three-paragraph preamble with two lines pointing at `ROUTING.md` for the full text; `readme-baseline` (README, runbook) is unchanged. Fresh scaffold: CLAUDE.local.md 17.5 KB → 11.7 KB (−33%). Field run 2026-10-07 (existing app, one main session per run, 2 runs per side, an entity + 2 microflows + 2 pages): 10/10 steps applied each side, new mx errors 5 vs 5 (the same CE2421 page-binding slip in both), cost $1.39 vs $1.38; the slim lanes opened the MDL pre-flight skill as often or more. Also drops two stray merge-artifact lines from this section. — MendixMau - perf(ui-loop): **fewer mxbuilds, no fixed sleeps, legible screenshots.** Field run 2026-10-07 (a requirements-driven build): `exec.sh` was 22% of tool time at two mxbuilds per exec, journeys and page waits slept a fixed time, and screenshots were 11.5% of re-read tokens. `exec.sh` now skips the pre-flight baseline mxbuild when the model is byte-identical to its last *clean* pass: the verification stamp gains an `errors:` field (`model-stamp.sh check --clean` accepts only `errors: 0`, so a delta-gate pass over pre-existing errors never counts; `SKIP_BASELINE=0` is unchanged). New `tests/e2e/settle.js` waits for the page's own state (no visible Mendix progress/loading indicator, DOM size stable) instead of a fixed time; `journey-runner.js` uses it after each action (an explicit `settleMs` still wins) and `page-audit.js` per page. Probed in a real browser: on a page that loads in 1.8 s the old 1200 ms sleep and networkidle+600 both captured it half-loaded, settle waited for it; on a fast page settle took 775 ms vs 1200. `SETTLE_MODE=fixed` restores the old waits for an A/B run. `page-audit.js` also saves a viewport shot `page-audit-.top.png` beside the full-page one, and `ui-loop.md` says to look at viewport shots: a tall full-page shot reaches the model at ~1024 px high, text unreadable. `shrink-image-read.sh` now works off macOS (ImageMagick or Pillow, python when jq is absent; it was a silent no-op without `sips`). — MendixMau - perf(ui-loop): **a stale app reloads instead of restarting, journeys stop sleeping, worst pages are looked at first.** Field run 2026-10-07, existing app. `test-stack-up.sh` records which model the running app was built from (`.claude/loop/served-model`); when the model has changed since, it hot-reloads with `mxcli docker reload` (adding `--skip-check` when the exec.sh gate already passed this model) and restarts only if that fails: 93 s instead of 179 s. `ui-loop.md` and the script header carry the measured table and point at `mxcli run --local --watch` as the fast loop (~18 s per page change). `journey-runner.js` waits on page state after a combobox action instead of fixed pauses, and a combobox step now opens the widget it names: it used to open the page's first combobox, so on a form with three every step but the first failed. `page-audit.js` clicks the first copy of a nav item a user could actually click, not just the first match: with the sidebar collapsed, every in-nav page faulted while the same item sat in the top menu bar. It also writes `page-audit-look-order.txt`, all pages worst first (`module-review.md` §4a: order, not selection, every page is still looked at). `design-audit.js` drops its fixed 1.2 s wait. Same 7-step journey on the same app: 56 s with `SETTLE_MODE=fixed`, 24 s with settle, 11/11 pass both ways, 3 runs each. — MendixMau diff --git a/lint-rules/README.md b/lint-rules/README.md index c6182c00..3cca40ca 100644 --- a/lint-rules/README.md +++ b/lint-rules/README.md @@ -7,7 +7,7 @@ and configure the marked constants — **after** running `mxcli init`, never bef | Rule | What it catches | Configure? | |---|---|---| | `conv010_act_microflow_content.star` | `ACT_` page-action microflows holding business logic instead of delegating to `SUB_` | if your prefixes differ — `ACTION_PREFIX`, `DELEGATE_PREFIX` | -| `conv020_action_user_feedback.star` | Page-triggered microflows that commit / import / delete but tell the user nothing | **yes** — `PROJECT_MODULES`, and `ACTION_PREFIX` if your prefixes differ | +| `conv020_action_user_feedback.star` | Page-triggered microflows that commit / import / delete but tell the user nothing | no — `PROJECT_MODULES` defaults to `"*"` (all modules the lint run includes, minus `.claude/lint-vendor-modules.txt`); `ACTION_PREFIX` if your prefixes differ | | `data_change_microflows.star` | ARCH002 — persistent entities written from pages instead of microflows | no | | `entity_business_key.star` | ARCH003 — persistent entities with no UNIQUE NOT NULL business key | no | @@ -37,7 +37,7 @@ convention: on a project that names things differently both rules matched nothin nothing, and reported a clean pass. Both now emit a `_rule` finding when the prefix matches zero microflows (CONV010 guards on the -project having any microflows at all; CONV020 on `PROJECT_MODULES` containing any). Verified by +project having any microflows at all; CONV020 on `PROJECT_MODULES` being non-empty and containing any). Verified by fixture on TestCLIApp — setting `ACTION_PREFIX` to an unused string produces the finding, and the real prefix produces none. `bin/lint-gate.sh` treats `module == "_rule"` as blindness, so this turns a silent pass into a gate failure rather than a line in a report nobody reads. @@ -149,6 +149,7 @@ If yes, it needs a self-check before it is worth installing. `CONV020` deliberately ignores retrieves, changes without commit, non-page-reachable microflows, and validation-only paths. It flags roughly what a reviewer would flag by hand. +Show-page, close-page and show-home-page actions count as feedback (presence only, not order). Refresh-in-client is not visible to the Starlark API, so a flow that only refreshes is still flagged until mxcli exposes it. A rule that fires on everything gets switched off, and a switched-off rule catches nothing — which is how `CONV010` and the lint gate both ended up idle. diff --git a/lint-rules/conv020_action_user_feedback.star b/lint-rules/conv020_action_user_feedback.star index 5281b454..01d881b5 100644 --- a/lint-rules/conv020_action_user_feedback.star +++ b/lint-rules/conv020_action_user_feedback.star @@ -1,7 +1,8 @@ # mxtk-lint-rule: conv020_action_user_feedback -# Shipped by mxcli-project-toolkit. UNLIKE the other rules here this one is MEANT to be -# edited: PROJECT_MODULES must name the modules this project owns, or the rule inspects -# nothing and says so. sync-project.sh therefore installs it once and never refreshes it; +# Shipped by mxcli-project-toolkit. UNLIKE the other rules here this one MAY be edited: +# PROJECT_MODULES defaults to "*" (every module the lint run includes, i.e. all but those +# in .claude/lint-vendor-modules.txt); narrow it to a tuple to scope the rule down. +# sync-project.sh installs it once and never refreshes it; # when the template changes it reports the change and leaves your configured copy alone. # See lint-rules/README.md. # --- end mxtk-lint-rule header --- @@ -41,14 +42,14 @@ SEVERITY = "warning" REQUIRES = ["full"] -# CONFIGURE ME. Only lint modules this project owns -- everything else is Marketplace -# or vendor content you do not author and cannot fix. Leaving this empty does not make -# the rule lint everything; it makes the rule refuse to run (see the guard in check()), -# because a rule that silently inspects nothing is worse than no rule. +# "*" (default) = every module this lint run includes. project-bin/lint-gate.sh passes +# .claude/lint-vendor-modules.txt as an exclusion, so Marketplace/vendor content is already +# out of scope there. Set a tuple of names to narrow it, e.g. ("Sales", "Orders"); an empty +# tuple makes the rule refuse to run (see the guard in check()) rather than pass silently. # -# List them with: +# List modules with: # ./mxcli -p .mpr -c "SHOW MODULES" -PROJECT_MODULES = () +PROJECT_MODULES = "*" # Work was actually completed and persisted. STATE_CHANGE_ACTIONS = ( @@ -61,6 +62,13 @@ STATE_CHANGE_ACTIONS = ( FEEDBACK_ACTIONS = ( "ShowMessageAction", "ValidationFeedbackAction", + # Navigating the user to a page, or closing the current one, is visible feedback. + # Presence only: flow order is not visible to this API, so a page opened BEFORE the + # commit also counts. Refresh-in-client is NOT detectable (not exposed by mxcli as an + # activity field) -- upstream ask; until then a flow that only refreshes is flagged. + "ShowPageAction", + "ClosePageAction", + "ShowHomePageAction", ) MAX_CALL_DEPTH = 3 @@ -102,17 +110,17 @@ def check(): # UNCONFIGURED GUARD. Ships from the toolkit with an empty module list. Without # this, an uncustomised copy inspects zero microflows and reports a clean pass -- # the exact silent-false-pass shape this rule's self-check exists to prevent. - if not PROJECT_MODULES: + if not PROJECT_MODULES: # empty tuple/string only; "*" is truthy return [violation( - message="CONV020 is unconfigured: PROJECT_MODULES is empty, so this rule inspected NOTHING. It was installed from the toolkit template and never customised for this project.", + message="CONV020 is unconfigured: PROJECT_MODULES is empty, so this rule inspected NOTHING.", location=location(module="_rule", document_type="Microflow", document_name="CONV020"), - suggestion="Edit PROJECT_MODULES at the top of this rule to list the modules this project owns (`SHOW MODULES`), excluding Marketplace and vendor modules.", + suggestion="Set PROJECT_MODULES at the top of this rule to "*" (all non-vendor modules) or to a tuple of module names.", )] # Which microflows are called directly from a page, and which from a nanoflow. # Starlark has no recursion, so the call graph is walked with a bounded frontier. for mf in microflows(): - if mf.module_name not in PROJECT_MODULES: + if PROJECT_MODULES != "*" and mf.module_name not in PROJECT_MODULES: continue in_scope += 1 diff --git a/project-bin/lint-gate.sh b/project-bin/lint-gate.sh index a2e7069c..e11b2009 100755 --- a/project-bin/lint-gate.sh +++ b/project-bin/lint-gate.sh @@ -189,6 +189,13 @@ if crashed: viol = [v for v in viol if v["ruleId"] not in crashed] counts = collections.Counter(v["ruleId"] for v in viol) +# Self-reported blindness (module "_rule") is likewise never baselined: a rule that said it +# inspected nothing would otherwise be ratcheted as accepted debt (#229). Later runs still +# report it as PASS-BLIND via `blind` above, which was computed before this filter. +if update: + viol = [v for v in viol if v.get("module") != "_rule"] + counts = collections.Counter(v["ruleId"] for v in viol) + if update: json.dump({"counts": dict(sorted(counts.items())), "total": len(viol), diff --git a/tests/test-lint-delivery.sh b/tests/test-lint-delivery.sh index 2e0c457d..ec558926 100755 --- a/tests/test-lint-delivery.sh +++ b/tests/test-lint-delivery.sh @@ -89,7 +89,7 @@ ls "$R"/*.local-* >/dev/null 2>&1 && grep -q "LOCAL TUNING" "$R"/*.local-* \ rm -f "$R"/*.local-* echo "== 8 configured conv020 is left alone, silently ==" -sed 's/^PROJECT_MODULES = ()/PROJECT_MODULES = ("MyModule",)/' "$C" > "$C.t" && mv "$C.t" "$C" +sed 's/^PROJECT_MODULES = "\*"/PROJECT_MODULES = ("MyModule",)/' "$C" > "$C.t" && mv "$C.t" "$C" out=$(mxtk_install_lint_rules "$W/tk" "$P" 0 "" 2>&1) [ -z "$out" ] && ok "silent" || no "noisy about the expected state" grep -q 'PROJECT_MODULES = ("MyModule",)' "$C" && ok "config survived" || no "CONFIG WIPED"