Skip to content

new(lint): UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned - #238

Draft
MendixMau wants to merge 5 commits into
masterfrom
feat/ux-lint-rules
Draft

MendixMau wants to merge 5 commits into
masterfrom
feat/ux-lint-rules

Conversation

@MendixMau

@MendixMau MendixMau commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Direct lane

What changed and why (one paragraph):

Two Starlark lint rules in lint-rules/, delivered through the existing install/refresh path (MXTK_LINT_RULES in bin/lib/install-manifest.sh, one README row each, delivery fixture count 4 → 6). UX001 flags a button that deletes directly (Forms$DeleteClientAction, which has no confirmation setting at all) or calls a microflow/nanoflow without "Ask confirmation" when that flow deletes objects (bounded call-graph walk, depth 3) or is named as an irreversible step (Approve / Reject / Submit / Archive / …, CamelCase-strict so ShowApprovedOrders does not match). UX002 flags decisions left on their auto-generated caption and flows of 8+ top-level activities (start/end events and annotations not counted) with neither an annotation nor a documentation text. These are the house rules that so far lived only as prose in a project CLAUDE.md ("pop-up when approving", "annotate your flows") and therefore held no session to anything. Scope was deliberately narrowed: commit-without-feedback is already CONV020; popup-vs-page and close-before-show are not visible to Starlark (no page layout, no activity ordering). Both rules need mxcli ≥ 0.25.0 (widget action_type / has_confirmation, activity caption / auto_generate_caption) and emit a _rule blindness finding on anything older, so lint-gate.sh never reads them as a clean pass.

Second commit — unit suite. tests/lint-rules/ executes both rules under the real Starlark interpreter (go.starlark.net at mxcli's pinned version) through a ~270-line Go harness that rebuilds the mxcli projection structs from JSON fixtures and exits on any field the projection does not have. 33 hand-written fixtures + a generated 3000-flow / 20000-widget perf model: 35 cases, all passing, wired into CI as its own step. The suite caught two defects before they shipped: activities_for() returns StartEvent/EndEvent objects, which inflated UX002's activity count; and UX002's "needs ≥ 0.25.0" probe only fired on flows that had a decision. Both fixed in the same commit.

Field evidence:

Logic proven, vocabulary not yet — still a draft for that reason. No mxcli binary was reachable in the authoring session (release assets out of scope, source build not permitted), so the rules were written from the mxcli catalog-builder source at 0.25.0 (widgetPrimaryAction stores the action $Type verbatim; getMicroflowObjectType yields ExclusiveSplit / Annotation; Microflows$ prefix stripped from action types). The fixtures encode that same reading, so they cannot catch a wrong reading. Perf under the harness: UX001 0.70 s, UX002 0.73 s on 3000 flows / 20000 widgets. The README note says the calibration is owed. To do it on a laptop, on a scratch copy of a real model with mxcli ≥ 0.25.0:

cp lint-rules/ux00*.star <copy>/.claude/lint-rules/
./mxcli lint -p <copy>/<App>.mpr --rules UX001,UX002
sqlite3 <copy>/.mxcli/catalog.db "SELECT DISTINCT ActionType FROM widgets ORDER BY 1;"
sqlite3 <copy>/.mxcli/catalog.db "SELECT DISTINCT ActionType, ActivityType FROM activities ORDER BY 1;"

If the probe shows a vocabulary mismatch, the constants at the top of each rule are the only thing to change (and the fixtures follow). Counts go into the README note, then the draft flips to ready.

  • No client data anywhere in the diff (leak guard passed, denylist grep over added lines clean, fixture module names neutral)
  • Size cap: 9 files, ~560 lines excluding fixtures and CHANGELOG — over the 400 cap; ~440 of that is the test harness and the test script, which ship with the rules they prove
  • Test tier reached: T2 — tests/lint-rules/test-ux-rules.sh 35/35 under the real interpreter (CI step added); T0 on top: bin/check-scripts.sh, bash -n, py_compile. tests/test-lint-delivery.sh updated for the new count.
  • For a new/changed instrument: golden input captured — pending, see Field evidence (fixtures are hand-written by design, the header says so). Both layouts / both platforms: n/a, the rules run inside mxcli lint, delivery path unchanged; the suite needs Go and python3 and fails loudly without them.
  • For a new skill: n/a (no skill)
  • CHANGELOG.md line appended in this PR, crediting the source project or person
  • New bug entries: none

🤖 Generated with Claude Code

https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw

claude added 3 commits October 8, 2026 07:04
Two Starlark rules delivered through the existing lint-rules install/refresh
path (install-manifest MXTK_LINT_RULES, README row each, delivery fixture
count 4 -> 6).

UX001 flags a button that deletes directly (Forms$DeleteClientAction has no
confirmation setting) or calls a microflow/nanoflow without "Ask confirmation"
when that flow deletes objects (bounded call-graph walk) or is named as an
irreversible step (Approve/Reject/Submit/Archive/...). UX002 flags decisions
left on their auto-generated caption and flows of 8+ top-level activities
with neither an annotation nor a documentation text.

Both need mxcli >= 0.25.0 (widget action_type/has_confirmation, activity
caption/auto_generate_caption) and emit a `_rule` blindness finding on
anything older, so lint-gate.sh never reads them as a clean pass. Vocabulary
was read from the mxcli catalog builder source, not from the bundled API
guide. No mxcli binary was reachable in the authoring session, so the first
field calibration on a scratch copy of a real model is still owed; the README
note says so and names the probe queries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…reter

tests/lint-rules/ runs both rules through go.starlark.net at mxcli's pinned
version via a small Go harness (starrun) that rebuilds the mxcli projection
structs from JSON fixtures and rejects any field the projection does not have.
33 hand-written fixtures cover self-checks, call depth on both sides of the
bound, cycles, the destructive-name pattern, module skips, snippet locations
and the version probe; a generated 3000-flow / 20000-widget model times each
rule (~0.7 s). 35 cases, wired into CI as its own step.

The suite caught two defects in UX002 before they shipped: StartEvent/EndEvent
objects returned by activities_for() inflated the activity count, and the
"needs mxcli >= 0.25.0" probe only fired on flows that had a decision. Both
fixed here; UX001's depth comment now states what the bound means.

Fixtures encode the projection as read from the mxcli source, not captured
output: they prove the rule logic, not the catalog vocabulary. Field
calibration on a real model is still owed and the README still says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
check-portability.sh (in CI) rejects a hard-coded `python3`: on Windows a
name on PATH may be the Store alias stub, not an interpreter. The suite now
sources bin/lib/portable.sh, calls require_py and runs "$PY", like every
other script here. 35/35 cases still pass; check-portability clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
claude added 2 commits October 8, 2026 14:15
test-stock-hashes.sh T2 demanded a STOCK-HASHES.txt entry per version for
every file in MXTK_LINT_RULES. mxcli init seeds only three of those; the
toolkit-authored ux001/ux002 rules can never have a stock hash, so the
fixture went red on every version (3 FAIL) the moment they joined the list.

install-manifest.sh gains MXTK_LINT_RULES_STOCK (the three init-seeded
rules). The fixture's coverage, listed-name and live-capture checks walk
that set, and a new check asserts it is a subset of MXTK_LINT_RULES.
Replayed T2's awk against the real list: all three versions cover all
three stock rules; a listed non-stock name still fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…e manifest

Case 1 asserted a literal 4 files in .claude/lint-rules/ after a fresh
install. Two toolkit-authored rules joined MXTK_LINT_RULES, six landed,
and the fixture reported them as a NOINSTALL leak. The expected count is
now the size of MXTK_LINT_RULES + MXTK_LINT_RULES_CONFIGURABLE, with an
explicit check that no MXTK_LINT_RULES_NOINSTALL file was installed.
CI's own output on 40f2c00 listed exactly the six manifest rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants