Skip to content

fix(lint): rename the toolkit's CONV020 rule — mxcli v0.28 ships a built-in CONV020 - #573

Merged
MendixMau merged 5 commits into
masterfrom
claude/loving-gates-hauc1n-conv020
Oct 23, 2026
Merged

MendixMau merged 5 commits into
masterfrom
claude/loving-gates-hauc1n-conv020

Conversation

@MendixMau

@MendixMau MendixMau commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Track: FIX (maintainer decision #12).

Direct lane

What changed and why (one paragraph):
mxcli v0.28.0 (2026-10-23) adds a built-in lint rule CONV020 ("Created object never persisted", mendixlabs/mxcli#1518). The toolkit's own page-action user-feedback rule was also CONV020. mxcli has no duplicate-ID guard (mdl/linter/linter.go, AddRule appends without checking), so both rules run under one ID. Collision evidence, captured verbatim on the public pedapp model (mendixlabs/mxcli testdata @ 37855fe3), committed under tests/wave2/fixtures/lint-conv020-v028/ with provenance in CAPTURE.txt:

  • v0.27.0 reports one CONV020 finding (the toolkit rule's).
  • v0.28.0 reports two CONV020 findings: Microflow 'Shop.SUB_Seed' creates $Orphan … (built-in) and 'ACT_SaveItem' is triggered from a page … (toolkit).
  • -r CONV020 runs both rules.
  • CONV020: enabled: false switches both off.
  • lint -l lists CONV020 twice (CreatedObjectNeverPersisted, ActionUserFeedback).
  • project-bin/lint-gate.sh adds the findings of both rules into one count.

The rule is now MXTK-UX003 (lint-rules/mxtk_ux003_action_user_feedback.star). The rename covers RULE_ID, the three _rule self-reports, the header and every reference.

Why that ID. The toolkit's existing rule IDs either share mxcli's own families or sit beside them, and that convention can collide. CONV020 itself and SEC014 are in mxcli's own numbered series. PERF001 appears as a test fixture ID in mxcli. The MXTK- prefix is one mxcli does not use, and the v0.28.0 -l capture contains no MXTK- ID. UX003 continues the toolkit's UX family (UX001 and UX002). Hyphenated IDs already parse in exec.sh and lint-gate.sh, as MDL-FLOW01 shows. The stock replacements (CONV009, CONV010, ARCH002, ARCH003) keep their stock IDs on purpose, because they replace the file mxcli init seeds. This is documented in lint-rules/README.md under "Rule IDs".

Migration (_mxtk_lint_migrate_renames in bin/lib/install-lint-rules.sh, driven by MXTK_LINT_RENAMES in install-manifest.sh). It runs on every sync-project.sh and init-project.sh through mxtk_install_lint_rules, and is idempotent:

  1. Unedited old copy (md5 matches one of the 4 shipped versions, with or without CRs, or matches the receipt row): the copy is deleted and the current template is installed under the new name. The receipt row moves. If the old copy was the template from before 2026-10-09 with PROJECT_MODULES = (), sync notes that the new rule will now report findings.
  2. Edited old copy: left byte-identical, with a warning that gives the manual steps. The new rule is not installed beside it, because that would run the rule twice. Once the user finishes the manual steps, the next sync rekeys the baseline.
  3. lint-baseline.json: findings whose message matches the toolkit rule's message pattern move to MXTK-UX003, and the counts move with them. Findings from the built-in keep CONV020. The regex is checked against the captured messages. A baseline that holds counts only (written before [bug] lint-gate: ratchet prints 3 arbitrary examples and baselines counts, so a rise cannot be attributed (agent re-baselined on a wrong attribution) #239) cannot be split, so sync prints a NOTE once (a marker line in .claude/.mxtk-lint-notes stops repeats) and does not guess. Python runs only when a key under CONV020 carries a toolkit-rule message (grep pre-filter), so a baselined built-in CONV020 costs nothing on later syncs.
  4. lint-rules-disabled.txt: a line naming the old file name is rewritten to the new name. A file name identifies the rule without ambiguity.
    lint-config opt-out carried over: when an unedited copy is migrated and the lint config switches CONV020 off (README's preferred opt-out), sync writes mxtk_ux003_action_user_feedback.star # reason: carried over from lint-config.yaml CONV020: enabled: false (<date>) to lint-rules-disabled.txt, so the renamed rule is not installed. The config read is the first of .claude/lint-config.yaml, lint-config.yaml, .lint-config.yaml at the project root — mxcli's FindConfigFile order. On a single-tree project that is the file mxcli reads; on a two-tree project (.mpr under app/) mxcli reads the config beside the .mpr instead, so the carry-over follows the root file the toolkit itself uses (same as lint-gate and sec014-notice; tracked as a separate toolkit-wide issue). The CONV020 key is left alone: on v0.28 it now switches off only the built-in, which the same key already had off. Accepted spellings are exactly what mxcli v0.28.0 treats as false (RuleConfigYAML.Enabled *bool decoded by gopkg.in/yaml.v3 v3.0.1): enabled: false, False, FALSE, or n, N, no, No, NO, off, Off, OFF (plain or quoted), under a bare, 'single'- or "double"-quoted CONV020 key, as its direct child or in a flow map on the key's line (CONV020: { enabled: False, severity: info }), in LF or CRLF files. Not an opt-out, as in mxcli: enabled nested under options:, fAlse/nO/0, and a quoted "false" (mxcli fails to load that config and runs every rule).
  5. lint-config.yaml CONV020 keys and PROJECT.md lines that name CONV020: one NOTE in the run that migrates (saying so when the opt-out was carried over), and no rewrite. Their text cannot say which rule was meant, and PROJECT.md decision rows have no structured waiver format.

After an mxcli upgrade (must-do 2). lint-gate.sh --update-baseline now records mxcli_version in the baseline. That costs one --version call, on update runs only, never on a check run. bin/doctor.sh compares it with the --version line it already reads for its receipt fingerprint, so there is no extra mxcli call. When they differ, doctor prints the hint bin/lint-gate.sh --update-baseline --reason "mxcli vX built-in rules". When the baseline records no version, doctor notes it and says the next --update-baseline will record it, with no command: there is no evidence of a rise yet. It is a note only, so the exit code does not change. The same hint is documented in skills/lint-that-actually-runs.md, which is an on-demand skill and not a baseline file. The gate's blocking behaviour is unchanged.

Row Answer
Process Nothing new blocks. Sync prints rename and NOTE lines once: in the run that migrates, and (counts-only baseline NOTE) once per project via a marker line. Under --strict, a project with an edited legacy conv020 copy now gets a counted warning on every sync until it migrates by hand. This is a deliberate exit-code change for that case only (pending maintainer decision #19).
Dependencies Touches exec.sh (one comment), lint-gate.sh, install-lint-rules.sh, install-manifest.sh and doctor.sh. The baseline rekey needs Python 3 through resolve_py; without it sync warns and changes nothing. New artifact read: mxcli_version in lint-baseline.json, written by lint-gate.sh --update-baseline.
Context cost baseline 63,877 → 63,877, the PR adds 0 (budget 80,000; bin/render-routing.sh --check before and after). No routed or always-loaded file grew.
Behaviour n/a for scripts. The skill text change is one short paragraph in an on-demand skill; NOT RUN as an A/B because it is a pointer to a command.
Runtime Sync: one md5 per renamed rule, plus one Python call only when a CONV020 baseline key carries the toolkit rule's message (or a counts-only baseline not yet noted). lint-gate: one --version call on --update-baseline only. doctor: no extra calls.

Fixtures. The new fixture tests/wave2/test-lint-rule-rename.sh bin/sync-project.sh has 141 cases and passes. Projects are scaffolded by the real init-project.sh and migrated by the real sync-project.sh. Baselines are written by the real lint-gate.sh from the golden captures. Every case runs on both layouts (single-tree and .mpr under app/). The cases cover:

  • the rename is complete (no toolkit CONV020 in code or docs, and all 4 shipped md5s come from git history)
  • the captures show the collision, and the migration regex classifies the two captured findings correctly
  • an unedited copy is migrated, the baseline is rekeyed, and lint-gate then PASSes on the post-rename output
  • a second sync is a no-op
  • an edited copy is left alone, the new rule is held back, and the manual path completes
  • the opt-out line is carried over
  • a counts-only baseline gets a NOTE
  • a CRLF copy and the empty-PROJECT_MODULES template are both treated as unedited
  • a baseline taken on v0.27 maps cleanly, and after the upgrade only the built-in CONV020 rises
  • doctor's hint appears when versions differ, stays silent when they match, and says so (without a command) when the baseline predates the field
  • a lint-config.yaml CONV020: enabled: false opt-out is carried over: new rule not installed, lint-gate rc 0 (T1b)
  • the counts-only NOTE prints once (T5 second run)
  • opt-out spellings, locations and a CRLF config, each checked against a v0.28.0 capture (T1c), dry run (T1d), hold path (T3b)

Scoped fixtures run and green (at 5c8adb6; later re-runs are under the Review fixes sections):

  • test-lint-delivery.sh
  • test-lint-receipt-revert, test-lint-gate-accept and test-lint-gate-new-findings. All three were updated for the new name. The first test-lint-gate-accept run caught a regression: my --version call ran after lint, which clobbered an arg-recording stub. Fixed by reading the version before lint.
  • test-exec-lint-autorestore, test-exec-gate-retry and test-exec-lock-watch
  • test-stock-hashes (with v0.28.0) and test-manifest-clean-checkout
  • test-sec014-notice and test-bug12-sync
  • test-common-root-multiapp
  • test-doctor-gate-selftest, test-doctor-docker-probe, test-doctor-mxcli-arch and test-doctor-stale-hooks
  • test-property-sets and test-progress-report-gitignore

bin/check-portability.sh and bin/check-scripts.sh are clean.

Review fixes (independent review of 5c8adb6, fixed in 9c3b891):

  • Opt-out kept (top finding): a project with rules: CONV020: enabled: false got MXTK-UX003 switched back on and lint-gate failed MXTK-UX003 0 -> 1. Now carried over (migration item 4). New fixture case T1b: baseline is the verbatim capture taken with the opt-out (lint-conv020-disabled-v0.28.0.out, no toolkit finding); it fails 4 assertions per layout on 5c8adb6 (rule installed, no disabled line, no carried-over NOTE, lint-gate rc 1) and passes now. T1 now uses a non-opt-out CONV020 setting so it still exercises the rename path.
  • Counts-only NOTE printed on every sync: now once (marker line); T5 asserts the second sync is silent.
  • Python on every sync once the built-in CONV020 is baselined: grep pre-filter on the toolkit message.
  • Doctor: no --update-baseline command for an unversioned baseline; T7 asserts it.
  • conv010 comment change reverted (no "Lint rule refreshed" diff); T0's stray check exempts that one comment line.
  • bug-logs/mxcli-bugs.md CONV020 pointer says "(now MXTK-UX003)".
  • Field run: pedapp (public) + conv020-setup.mdl, mxcli v0.28.0, scaffolded by master's init-project.sh, CONV020: enabled: false, baselined, synced with this branch: 14 -> 14 findings, "PASS — nothing got worse" (rc 1 only from the unrelated PERF002/UI001 blindness). The same steps with 5c8adb6: MXTK-UX003 0 -> 1.
  • Fixture totals at 9c3b891: test-lint-rule-rename 95/95 (83/95 on 5c8adb6), test-lint-delivery ALL GREEN, test-lint-receipt-revert 55/55, test-lint-gate-accept 24/24, test-sec014-notice 24/24, test-bug12-sync 62/62, test-doctor-docker-probe 18/18, -gate-selftest 28/28, -mxcli-arch 10/10, -stale-hooks 7/7; check-portability, check-scripts, render-routing --check clean (baseline 63,877 → 63,877, the PR adds 0).

Review fixes, round 2 (second review of 0944408, fixed in ded779c):

  • Opt-out read like mxcli (top finding): _mxtk_lint_cfg_disabled saw only a lowercase false under a bare or double-quoted key, so False, FALSE, no, a flow map and a 'CONV020': key lost the opt-out (lint-gate 0 -> 1), and an enabled: false under options: carried one over that mxcli ignores. Now the yaml.v3 false set, any key quoting, direct child or flow map only; the NOTE grep takes the same key forms.
  • Config location: mxcli's fallback order (mdl/linter/config.go FindConfigFile).
  • Hold path: the edited-copy warning gives the lint-rules-disabled.txt line that keeps an opt-out (a listed rule is not installed; a copy left in lint-rules/ would still run, because mxcli loads every *.star there, so the step replaces copying the rule, not follows it).
  • Dry run: no "Would install" for a rule it says it would carry the opt-out over for (also for a carried-over lint-rules-disabled.txt line).
  • NOTE: no move-the-key advice when the new rule is already off.
  • .claude/.mxtk-lint-notes: comment that it is committed on purpose, team-shared like .mxtk-lint-receipt (it marks a NOTE about the committed baseline as said).
  • New capture lint-config-variants-v0.28.0.out: mxcli v0.28.0 on the pedapp capture model, once per spelling/location (28 runs); fixture T1c takes its verdicts from it. New cases T1c (8 variants), T1d (dry run), T3b (hold path), T4 NOTE: test-lint-rule-rename 137/137 at ded779c, 109/137 (28 failures) at 0944408.

Review fixes, round 3 (third review of ded779c, fixed in cb50a09):

  • CRLF lint config (top finding): the round-2 reader kept the trailing \r, so a config saved with CRLF (Git Bash, core.autocrlf) lost the opt-out, although mxcli v0.28 reads it as off (the round-1 reader matched it). The awk now strips \r first, the bin/lib/change-log.sh idiom. T1c gains a CRLF row: on ded779c it fails on both layouts (opt-out lost, lint-gate rc 1); test-lint-rule-rename 141/141 now; check-portability clean.
  • Reviewer-confirmed: the false set matches yaml.v3 v3.0.1 (resolve.go, decode.go) and real v0.28 runs; precedence matches FindConfigFile; mxcli loads every *.star in lint-rules/ (mdl/linter/starlark.go), so the hold-path wording stands; the awk constructs are BSD-awk safe by inspection.
  • Left as known, unlikely edge cases (each documented in the review): CONV020 under a parent other than rules:, YAML anchors/aliases/!!bool tags, multi-line flow maps, and configs mxcli fails to load for another reason. Two-tree config location: separate toolkit-wide issue.

Field evidence / field-proof gap. The golden input is real v0.27.0 and v0.28.0 output on a public model. No real client project with the toolkit rule installed has been migrated or linted with this branch (the opt-out path was field-run on the public pedapp model, see Review fixes). The maintainer should run this before merge, on a real project on mxcli v0.28.0 that has the old rule installed:

  1. mxcli lint -p <app>.mpr --format json before the sync, to see two kinds of CONV020 finding.
  2. bin/sync-project.sh <project>.
  3. mxcli lint --format json again: toolkit findings now report as MXTK-UX003, and only the built-in reports as CONV020.
  4. bin/lint-gate.sh: the only rise should be the built-in CONV020.
  5. bin/doctor.sh <project> for the re-baseline hint.

macOS and Git Bash were not run. Checked by inspection: sed -i.b, native_path for the Python path, BSD-awk constructs; CRLF configs are covered by a fixture case.

  • No client data anywhere in the diff
  • Size cap: 12 non-test files with 380 lines changed, plus 4 test files with 289 lines (fixtures and captures excluded). This is over the 10-file cap because the rename has to touch every reference at once.
  • Test tier: scoped fixtures plus a fixture with golden captures. There is no T3 real-project run (see the gap above).
  • Golden input captured, both layouts. Both platforms were considered but only Linux was run.
  • CHANGELOG.md line appended

Open questions for the maintainer:

Review: pending re-review of cb50a09


🤖 Generated with Claude Code

https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr

….28 ships a built-in CONV020

mxcli v0.28.0 adds a built-in CONV020 ("Created object never persisted",
mendixlabs/mxcli#1518) and has no duplicate-ID guard, so the toolkit's
user-feedback CONV020 ran beside it under one id. Captured on a public
model (tests/wave2/fixtures/lint-conv020-v028/): two CONV020 findings,
`lint -l` lists CONV020 twice, `CONV020: enabled: false` switches both off.

- lint-rules/mxtk_ux003_action_user_feedback.star: RULE_ID MXTK-UX003.
- install-lint-rules.sh: _mxtk_lint_migrate_renames (MXTK_LINT_RENAMES in
  install-manifest.sh) retires an unedited old copy (md5 of any shipped
  version, CR-insensitive, or the receipt row), rekeys baseline findings
  whose message is the toolkit rule's, carries a disabled.txt line over;
  an edited copy is left alone with a warning and the new rule held back;
  counts-only baselines, lint-config keys and PROJECT.md lines get a NOTE.
- lint-gate.sh --update-baseline records mxcli_version; doctor.sh prints
  the re-baseline hint when the installed mxcli differs (no extra call).
- tests/wave2/test-lint-rule-rename.sh: 75 cases, both layouts.

Field proof: golden lint captures from the v0.28.0 test pass (pedapp
@ 37855fe3); no run against a real project with the rule installed yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
…fig opt-out, NOTE once

- install-lint-rules.sh: on the unedited-copy migration, a lint-config.yaml `CONV020: enabled:
  false` (README's preferred opt-out) is carried over as a lint-rules-disabled.txt line for
  mxtk_ux003_action_user_feedback.star, so the renamed rule is not installed. The CONV020 key is
  left alone (on v0.28 it now switches off only the built-in); the NOTE says it was carried over.
  Scoped read is lint-gate's SEC014 awk, narrowed to `enabled: false`.
- The counts-only baseline NOTE prints once (marker line in .claude/.mxtk-lint-notes).
- The baseline rekey spawns Python only when a key under the old id carries a toolkit-rule
  message (grep pre-filter), not on every sync once the built-in CONV020 is baselined.
- doctor.sh: a baseline with no recorded mxcli_version gets no --update-baseline command.
- conv010 comment change reverted (it caused a "Lint rule refreshed" diff in every project).
- bug log pointer names MXTK-UX003.
- test-lint-rule-rename.sh: T1b (opt-out baseline from the capture taken with the opt-out;
  fails 4/4 per layout on 5c8adb6: MXTK-UX003 installed, lint-gate 1 -> 2 findings), T5
  second-run assertion, T7 no-command assertion. T1 now uses a non-opt-out CONV020 setting.
  95/95 here, 83/95 on 5c8adb6.

Field run: pedapp (public, mendixlabs/mxcli testdata) + conv020-setup.mdl, mxcli v0.28.0,
scaffolded by master init-project.sh, CONV020 enabled: false, baselined, synced: this branch
14 -> 14 findings, "PASS — nothing got worse" (rc 1 only from PERF002/UI001 blindness);
5c8adb6: MXTK-UX003 0 -> 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
…G: keep both sides)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
… does (second review)

- _mxtk_lint_cfg_disabled: false is plain false/False/FALSE or n/N/no/No/NO/off/Off/OFF
  (plain or quoted), per mxcli's RuleConfigYAML.Enabled *bool decoded by gopkg.in/yaml.v3
  v3.0.1 (resolve.go resolveMapList + decode.go's YAML 1.1 fallback for typed bools). Rule key
  bare, single- or double-quoted; `enabled` only as a direct child or a top-level flow-map key,
  so `options: enabled: false` no longer carries over an opt-out mxcli ignores.
- Config read from the first of .claude/lint-config.yaml, lint-config.yaml, .lint-config.yaml
  (mxcli FindConfigFile order).
- Hold path: the warning gives the lint-rules-disabled.txt line that keeps the opt-out.
- Dry run no longer says it would install a rule it says it would carry an opt-out over for.
- NOTE drops the move-the-key advice when the new rule is already off.
- .mxtk-lint-notes: comment that it is committed on purpose (team-shared).
- Fixture: T1c (8 spellings/locations, verdicts from a new mxcli v0.28.0 capture
  lint-config-variants-v0.28.0.out), T1d dry run, T3b hold path, T4 NOTE. 137/137 here;
  28 failures at 0944408.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
Round-3 review of #573: isoff() kept the trailing \r, so a lint-config.yaml
saved with CRLF (Git Bash, core.autocrlf) lost the CONV020 opt-out; mxcli
v0.28 reads it as off. Strip \r first (the change-log.sh idiom). T1c gains a
crlf row: fails on ded779c (opt-out lost, lint-gate rc 1, both layouts),
141/0 now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
@MendixMau
MendixMau merged commit 6615869 into master Oct 23, 2026
2 checks passed
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