Skip to content
Closed
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 @@ -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-<Page>.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
Expand Down
5 changes: 3 additions & 2 deletions lint-rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
34 changes: 21 additions & 13 deletions lint-rules/conv020_action_user_feedback.star
Original file line number Diff line number Diff line change
@@ -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 ---
Expand Down Expand Up @@ -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 <project>.mpr -c "SHOW MODULES"
PROJECT_MODULES = ()
PROJECT_MODULES = "*"

# Work was actually completed and persisted.
STATE_CHANGE_ACTIONS = (
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions project-bin/lint-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion tests/test-lint-delivery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading