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
7 changes: 7 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,10 @@ jobs:
bash "$t" || { echo "FAILED: $t"; rc=1; }
done
exit $rc

# Lint-rule unit suite. Added 2026-10-08 with UX001/UX002: executes the Starlark rules
# under go.starlark.net at mxcli's pinned version (tests/lint-rules/starrun, built here
# with the runner's Go), so rule logic is proven before any field calibration. Not under
# the glob above because it needs Go and lives in its own directory.
- name: Lint-rule unit suite (tests/lint-rules/test-ux-rules.sh)
run: bash tests/lint-rules/test-ux-rules.sh
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are.

## Unreleased

- fix(lint): **UX001/UX002 field-calibrated on a real model; both had a vocabulary defect the unit suite could not see.** First run of the pair with mxcli 0.26.0 against a scratch copy of a 21-module Mendix 11.12 app (876 microflows, 124 pages, 1.7 s, no load errors): UX001 fired 0 times and UX002's decision leg 0 times across 728 decisions. Catalog probe showed why — the stored microflow-button action type is `Forms$MicroflowAction` (290 rows), not the `Forms$MicroflowClientAction` read from the source, so UX001 legs 2–3 could never fire for a microflow button; and `auto_generate_caption` is 0 on every `ExclusiveSplit`, a bare decision instead carries its condition expression as its caption, so UX002's test could never be true. Fixed: UX001 lists both spellings; UX002 flags a caption that is empty or equals `condition_expression` (39 of 39 custom top-level decisions on that model, while marketplace decisions read "Passwords equal?"). Fixtures now carry the probed action-type name and a new case pins the caption==condition shape (36 cases). Lesson for every rule here: a self-check that only tests "count is non-zero" passes on a wrong vocabulary — the field run is the check. — MendixMau
- fix(tests): **`test-stock-hashes.sh` no longer demands a stock hash for a toolkit-authored rule.** Its coverage check walked all of `MXTK_LINT_RULES`, but `mxcli init` only seeds three of those files, so the day UX001/UX002 joined the list every version went red (`v0.17.0 is missing: ux001… ux002…`, 3 FAIL) for a hash that cannot exist. `bin/lib/install-manifest.sh` gains `MXTK_LINT_RULES_STOCK` (the three init-seeded rules); the fixture covers, lists and live-captures against that set and asserts it is a subset of `MXTK_LINT_RULES`. Verified by replaying the fixture's awk against the real list: all three versions cover all three stock rules, and a listed non-stock name still fails. Same disease in `tests/test-lint-delivery.sh`: its fresh-install case asserted a literal 4 installed files; it now derives the count from the manifest lists and checks that no `MXTK_LINT_RULES_NOINSTALL` file landed. — MendixMau
- new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Logic is unit-tested under the real Starlark interpreter: `tests/lint-rules/test-ux-rules.sh` (35 cases incl. a synthetic 3000-flow perf run, wired into CI) — the suite caught two defects before they shipped: start/end events inflated UX002's activity count, and its version probe only fired on flows that had a decision. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — MendixMau
- perf(ui-loop): **`test-stack-up.sh` recognises a live `mxcli run --local` as this project's app, and `--restart` no longer builds twice.** The script read only Docker labels, so an app started with `./mxcli run --local --watch` (mxcli's own ~20s page loop) was found by port scan, published as `APP_OWNERSHIP=unverified`, and refused by the e2e harness unless `ALLOW_UNVERIFIED_APP=1`. It now reads mxcli's `.mxcli/run-local.json` handshake (v0.24+; beside the `.mpr` or the `-p` path on a two-tree checkout), checks its pid is alive, and publishes `verified` with a new `APP_SOURCE=run-local` line in `stack.env` (`docker` / `scan` otherwise). A model edited after boot reads as applied under `--watch` and as STALE without it — the script never restarts a loop it did not start. Docker bring-up and restart now wait on `mxcli docker run --wait` instead of a 240s poll loop, and still prove HTTP; `--restart` used to fall through into the bring-up block and run `docker run` a second time. New fixture `tests/wave2/test-stack-up-local.sh` over a redacted capture of the handshake. Field run 2026-10-07, existing app: same app, same port, `unverified` → `verified`; a touched model read `watch`; stopping the loop removed the handshake and the script fell back. — MendixMau
- fix(init): **local by default, no Dev Container.** `init-project.sh` now parks the `.devcontainer/` that `mxcli new`/`init` always writes under `.mxtk-backup/` (keep it with `MXTK_KEEP_DEVCONTAINER=1`), and the README says "say no" to VS Code's reopen prompt. Field incident 2026-10-08: a first-time user followed the docs and landed in the container with "claude: command not found" (the template installs `claude` to `~/.local/bin` and never puts it on PATH) and a fresh login prompt. Local use was always the intent. — MendixMau
- docs(lint): **Mendix quality-source review: 19 new lint-backlog rows (Batch 4) plus seven skill additions.** Recommender, QSM/AQM, ATS, Menditect and docs.mendix.com mapped against shipped rules; indexes, scheduled events, Atlas 4 tokens, accessibility floor, workflow targeting, test isolation and rollback semantics added to their skills; record in `process/quality-sources-review-2026-10-08.md`; no rule probed on a model yet — MendixMau
Expand Down
12 changes: 10 additions & 2 deletions bin/lib/install-manifest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ MXTK_PROJECT_BIN_NOINSTALL="report-disposition-check.sh" # gate-check.sh runs i
# lint-rules/README.md. conv010 (CONV010) is the top rule by
# volume everywhere it runs; the de-noised version here took PROJECT-C 399 -> 51.
#
# The toolkit ships ONLY these three of mxcli's ~29. Copying the rest would pin every project
# The toolkit repairs ONLY these three of mxcli's ~29 (MXTK_LINT_RULES_STOCK below); rules
# after them in the list are toolkit-authored, never seeded by init. Copying the rest would pin every project
# to whatever mxcli shipped the day someone copied them.
#
# MXTK_LINT_RULES_CONFIGURABLE — installed once, then owned by the project. conv020 is not an
Expand All @@ -150,7 +151,14 @@ MXTK_PROJECT_BIN_NOINSTALL="report-disposition-check.sh" # gate-check.sh runs i
# project that re-runs init loses these fixes and its lint gate goes quietly blind. Re-running
# sync-project.sh restores them; lint-rules/STOCK-HASHES.txt is what lets it tell a
# reverted-to-stock file (safe to restore) from one a project deliberately tuned (hands off).
MXTK_LINT_RULES="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star"
MXTK_LINT_RULES="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star ux001_confirm_before_destructive.star ux002_decisions_captioned.star"
# MXTK_LINT_RULES_STOCK — the subset of MXTK_LINT_RULES that `mxcli init` ALSO seeds, i.e. the
# only files lint-rules/STOCK-HASHES.txt can ever hold a hash for. The ux001/ux002 rules are
# toolkit-authored: init never writes them, so no stock hash can exist and a coverage check
# that demanded one (tests/wave2/test-stock-hashes.sh, until 2026-10-08) went red on every
# version the day a non-stock rule joined MXTK_LINT_RULES. Every name here must also be in
# MXTK_LINT_RULES; the fixture asserts it.
MXTK_LINT_RULES_STOCK="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star"
MXTK_LINT_RULES_CONFIGURABLE="conv020_action_user_feedback.star"

# Files in lint-rules/ deliberately NOT installed. Same contract as MXTK_PROJECT_BIN_NOINSTALL:
Expand Down
22 changes: 22 additions & 0 deletions lint-rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and configure the marked constants — **after** running `mxcli init`, never bef
| `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 |
| `ux001_confirm_before_destructive.star` | UX001 — a button that deletes, approves, rejects, submits or archives (direct delete action, or a flow that deletes / is named so) without "Ask confirmation" | `DESTRUCTIVE_NAME_PATTERN` if your verbs differ |
| `ux002_decisions_captioned.star` | UX002 — decisions (`ExclusiveSplit`) with an auto-generated caption instead of the question they answer; flows of 8+ activities with neither an annotation nor a documentation text | `ANNOTATE_FROM` |

`data_change_microflows` and `entity_business_key` are **repaired copies of rules `mxcli init`
already seeds**, not new ones. Both shipped comparing `entity.entity_type` to `"PERSISTENT"`
Expand All @@ -24,6 +26,26 @@ while it was dead. It now carries the same `SKIP_MODULES` list, and the same pro
are excluded at the gate (`bin/lint-gate.sh -e`, from a per-project
`.claude/lint-vendor-modules.txt`), not hardcoded in a shared rule.

`ux001` and `ux002` read widget `action_type` / `has_confirmation` and activity
`caption` / `auto_generate_caption`, which exist from **mxcli 0.25.0** (catalog schema with
widget actions and activity captions). On an older binary both rules emit a single `_rule`
finding ("needs mxcli >= 0.25.0") and nothing else — `bin/lint-gate.sh` reads that as blindness,
never as a clean pass. Their vocabulary (`Forms$DeleteClientAction`, `Forms$MicroflowAction`,
`ExclusiveSplit`, `DeleteObjectAction`) was read from the mxcli source at 0.25.0, then field-probed
on 2026-10-12 with mxcli 0.26.0 against a scratch copy of a 21-module Mendix 11.12 model (876
microflows, 124 pages). That probe found two defects the unit suite could not see: the stored
microflow-button action type is `Forms$MicroflowAction`, not the `…ClientAction` the source read
suggested, and `auto_generate_caption` is never set on a decision — a bare decision carries its
condition expression as its caption. Both fixed; after the fix UX002 reports every one of the
model's 39 top-level custom decisions as bare (all were), UX001 stays at 0 there (its 4 microflow
buttons reach no delete and match no verb). Re-probe on your own model before trusting the counts. Their *logic* is proven: `tests/lint-rules/`
runs both under the real Starlark interpreter (`go.starlark.net` at mxcli's pinned version) over
33 hand-written fixtures — self-checks, call depth both sides of the bound, cycles, the name
pattern, module skips, snippet locations, start/end events not counted — plus a synthetic
3000-flow / 20000-widget model each rule finishes in under a second. Fixtures encode the
projection as read from the mxcli source; they are not captured output, so they prove the
rule, not the vocabulary.

`conv010` here is likewise the repaired form of the rule mxcli seeds, and supersedes the
version this directory shipped on 11 Aug: one violation per microflow instead of one per
activity, plus four action types allowlisted. On PROJECT-C that is 399 rows -> 51 findings.
Expand Down
Loading
Loading