Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f86b8b0
fix(catalog): save the cache atomically so parallel processes do not …
ako Oct 3, 2026
54997ca
fix(lint): keep failing rules out of the report score (#952)
ako Oct 3, 2026
70cb9b1
feat(init): stamp the tooling version, guard against older binaries, …
ako Oct 3, 2026
aeaf080
fix(docker): run docker check on a temporary copy of the project (#951)
ako Oct 3, 2026
d94fcfb
fix(docker): refuse a missing project and never copy the check copy i…
ako Oct 3, 2026
8964f28
fix(check): MDL-BUTTON01 skips a grid nested in a data container
ako Oct 3, 2026
a423d46
fix(lint): MPR012 and MDL-WIDGET40 stay off native pages
ako Oct 3, 2026
c4a14bb
fix(lint): MPR002 does not call a return-only flow empty
ako Oct 3, 2026
6763b4a
fix(lint): CONV006 reports once per entity
ako Oct 3, 2026
0977b5a
feat(report): --modules scores only the selected modules
ako Oct 3, 2026
190be49
fix(check): a void Java/JavaScript action call declares no variable; …
ako Oct 3, 2026
4d53f68
fix(check): unknown call parameters are no longer hidden behind other…
ako Oct 3, 2026
3e85395
Merge remote-tracking branch 'origin/feat/952-tooling-version-guard' …
ako Oct 3, 2026
d41abe6
Merge remote-tracking branch 'origin/fix/953-void-call-duplicates' in…
ako Oct 3, 2026
119622b
style: gofmt validate_void_code_calls_test.go
ako Oct 3, 2026
05b6313
Merge remote-tracking branch 'origin/fix/953-lint-false-positives' in…
ako Oct 3, 2026
bc4cab5
test(report): --modules scoping keeps rule failures listed
ako Oct 3, 2026
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
45 changes: 31 additions & 14 deletions .claude/lint-rules/conv006_no_create_delete_rights.star
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
# create and delete operations should go through microflows that enforce
# business logic. Only READ and WRITE should be granted via access rules.
#
# One finding per entity, naming every role per right: the advice is the same
# for each role, and one row per entity x role x right buried the report (111
# findings on a mid-sized app, ako/mxcli#953).
#
# Requires FULL catalog (REFRESH CATALOG FULL).

RULE_ID = "CONV006"
Expand All @@ -12,27 +16,40 @@ DESCRIPTION = "Entity access rules should not grant CREATE or DELETE directly; u
CATEGORY = "security"
SEVERITY = "warning"

RIGHTS = ("CREATE", "DELETE")

def check():
violations = []

for entity in entities():
if entity.entity_type != "Persistent" or entity.is_external:
continue

# right -> sorted, de-duplicated roles (a role can hold several rules)
roles = {}
for perm in permissions_for(entity.qualified_name):
if perm.access_type in ("CREATE", "DELETE"):
violations.append(violation(
message="Entity '{}' grants {} to role '{}'. Use a microflow to enforce business logic.".format(
entity.qualified_name, perm.access_type, perm.module_role_name
),
location=location(
module=entity.module_name,
document_type="Entity",
document_name=entity.qualified_name,
),
suggestion="Remove the {} right and implement a microflow (ACT_) with security checks".format(
perm.access_type
),
))
if perm.access_type in RIGHTS:
held = roles.setdefault(perm.access_type, [])
if perm.module_role_name not in held:
held.append(perm.module_role_name)

granted = [r for r in RIGHTS if r in roles]
if not granted:
continue

parts = ["{} ({})".format(r, ", ".join(sorted(roles[r]))) for r in granted]
violations.append(violation(
message="Entity '{}' grants {}. Use a microflow to enforce business logic.".format(
entity.qualified_name, "; ".join(parts)
),
location=location(
module=entity.module_name,
document_type="Entity",
document_name=entity.qualified_name,
),
suggestion="Remove the {} right{} and implement a microflow (ACT_) with security checks".format(
" and ".join(granted), "s" if len(granted) > 1 else ""
),
))

return violations
4 changes: 4 additions & 0 deletions .claude/skills/fix-issue/findings/cmd-mxcli.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@
{"date": "2026-10-03", "area": "cmd/mxcli/check", "symptom": "`mxcli check -p` prints MDL067 (bare commit now WITH events) for a create-or-modify flow already stored with events, which `exec -p` no longer prints", "cause": "cmd_check ran ValidateProgram without the DropSettledCommitNotes filter exec_preflight applies; the project was only connected later, for the reference tier", "fix": "check connects to the project before the semantic report when -p is given, applies DropSettledCommitNotes and StoredTaskClaimViolations, and reuses that connection for the reference tier", "insight": "Two gates over one rule set drift whenever a post-filter lives in only one of them; grep for every caller of ValidateProgram when adding a filter. Control: a flow stored without events still notes", "issue": "ako/mxcli#943", "file": "cmd/mxcli/cmd_check.go", "test": "cmd/mxcli/check_stored_semantics_test.go"}
{"date": "2026-10-03", "area": "cmd/mxcli/test", "symptom": "every `mxcli test` run prints 2x MDL-DEPR001 and 2x MDL-V1-SLASH about a script the user never wrote", "cause": "GenerateEndpointMDL emitted a headerless mdl 0 script with `create or replace` and `/` terminators; the test-flow generators had already moved to the version-aware writeScriptHeader/createFlow/writeFlowEnd", "fix": "GenerateEndpointMDL writes mdl 1 through the same helpers (header, create or modify, `;` only); endpoint script is independent of the suite's version", "insight": "A generated script is checked like a user's one; pin it with a test that parses it and asserts ValidateProgram returns nothing. Verified end to end with `mxcli test --local` on a fresh 11.13 app", "issue": "ako/mxcli#943", "file": "cmd/mxcli/testrunner/endpoint.go", "test": "cmd/mxcli/testrunner/endpoint_clean_test.go"}
{"date": "2026-10-03", "area": "cmd/mxcli/theme", "symptom": "`theme create acme --from design.css` with `--mxt-font: \"Inter\", system-ui, sans-serif` prints nothing about Inter; the theme ships no woff2 and no @font-face for it and renders in the fallback font wherever Inter is not installed", "cause": "planFonts only decided which VENDORED families to drop; a seeded family outside the vendored set was never looked at, so the silent outcome was the default", "fix": "unvendoredSeededFamilies takes the primary (first) family of each seeded font stack, skips generic families and var() and the families the base partial loads, and CreateResult.UnvendoredFonts carries them to cmd_theme.go, which prints a note per family naming mxcli-fonts/ and the partial", "insight": "Only the first family of a stack is the design's choice; flagging the fallbacks (Helvetica, Arial) would make the note noise. The controls are a vendored family (IBM Plex Mono) and a generic stack, which must stay silent", "issue": "ako/mxcli#944", "file": "cmd/mxcli/theme/create_seeded.go (unvendoredSeededFamilies, planFonts); cmd/mxcli/cmd_theme.go", "test": "cmd/mxcli/theme/create_seeded_test.go (TestCreate_NamesSeededFontsItDoesNotVendor)"}
{"area": "cmd/mxcli/docker", "date": "2026-10-03", "symptom": "`mxcli docker check` (a check) rewrote an MPRv1 project's .mpr permanently, and on v1 and v2 alike rewrote theme-cache/web/theme.compiled.css(.map) and created deployment/sass/main.scss \u2014 with or without --no-update-widgets", "cause": "update-widgets ran on the user's project, protected only by a snapshot/restore of the v2 storage (.mpr + mprcontents/), so v1 had no protection; and `mx check` itself compiles the theme into theme-cache/ and writes deployment/sass/, which nothing guarded", "file": "`cmd/mxcli/docker/check.go` (`Check`), `cmd/mxcli/docker/check_copy.go` (`copyProjectForCheck`)", "insight": "A snapshot of the files you expect a tool to touch protects only those files; measure with a whole-tree hash+mtime diff before/after, which is what showed that plain `mx check` writes too. The fix is to run both mx steps on a temporary copy (skipping deployment/, releases/, theme-cache/, .git, node_modules) and rewrite the copy's path back in mx output. Control: a CE0117 microflow is still reported on both formats with the tree unchanged. `docker build` keeps runUpdateWidgets because it is expected to write deployment/ \u2014 but it still rewrites a v1 .mpr", "refs": ["ako/mxcli#951", "ako/mxcli#568", "ako/mxcli#646"]}
{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "A project whose CLAUDE.md, skills and lint rules were written by a newer mxcli is served by an older binary (v0.24.0 on PATH) with no warning: 'mdl 1;' is a parse error and shipped lint rules crash, all reading as project defects", "cause": "Nothing recorded which mxcli wrote the tooling; .claude/bootstrap-mxcli.sh linked whatever mxcli was on PATH; init --sync-skills refreshed only .ai-context/skills, never .claude/lint-rules or CLAUDE.md/AGENTS.md", "fix": "init and every sync write .ai-context/mxcli-tooling.json; root PersistentPreRun warns once on stderr when the binary is provably older (release by number, nightly by tag date, mixed by build date, dev never); sync refuses from an older binary; the bootstrap script carries a POSIX-sh copy of the ordering and neither links an older PATH binary nor keeps an older ./mxcli, downloading via a temp file + mv; sync also refreshes bundled lint rules by name and the CLAUDE.md/AGENTS.md section between mxcli:begin/end markers", "insight": "A binary cannot warn about a stamp it predates, so the guard for already-shipped binaries must live in the generated script, which the newer mxcli regenerates. The sh and Go comparisons share one test table so they cannot drift. curl -o ./mxcli on a symlinked ./mxcli would overwrite the PATH binary — always download to a temp name and rename", "issue": "ako/mxcli#952", "file": "cmd/mxcli/tooling_stamp.go; cmd/mxcli/init_tooling_sync.go; cmd/mxcli/init_hook.go (bootstrapScriptTemplate); cmd/mxcli/main.go; cmd/mxcli/init.go", "test": "cmd/mxcli/tooling_stamp_test.go; cmd/mxcli/init_hook_version_test.go; cmd/mxcli/init_tooling_sync_test.go"}
{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "CONV006 emits one finding per entity x role x CREATE/DELETE (111 on a mid-sized app), the same advice repeated per role, and the per-finding Security score is driven by role count rather than by entities", "cause": "The Starlark rule appended a violation inside the permissions_for() loop", "file": "`.claude/lint-rules/conv006_no_create_delete_rights.star` (synced to `cmd/mxcli/lint-rules/`)", "insight": "Group per entity and per right with de-duplicated sorted roles (a role can hold several access rules on one entity). Test both rule copies (.claude and the embedded one) like SEC008's test does", "refs": ["ako/mxcli#953"]}
{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "`mxcli report` could not score a project's own modules: `lint` has --modules, `report` had only --exclude", "cause": "Feature gap; and the LintContext module filter alone would not make the score exact, because project-level findings (CONV008 role mappings, project security) carry no module and are reported regardless", "file": "`cmd/mxcli/cmd_report.go`, `mdl/linter/report.go` (`ScopeToModules`, Report.Modules)", "insight": "Filter the scored violations to those located in a selected module, and print the selection in every format so a module score is not mistaken for the project's", "refs": ["ako/mxcli#953"]}
3 changes: 3 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -844,3 +844,6 @@
{"date": "2026-10-03", "area": "mdl/executor/drop", "symptom": "`drop microflow M.F;` in one `mxcli exec` run and `create microflow M.F …` in the next leaves M.F with no module-role grants (CE0106 on the pages calling it); the drop printed only \"Dropped microflow: M.F\"", "cause": "the grants carry only through the session cache (rememberDroppedMicroflow / consumeDroppedMicroflow), which a later process does not have; nothing told the user the carry was session-scoped. `drop page` never remembers its AllowedRoles at all", "fix": "writeDroppedGrantsNote (mdl/executor/drop_grants_note.go), called from execDropMicroflow / execDropNanoflow / execDropPage, prints the removed roles, whether a create carries them (same script or session for flows; never for a page), and the `grant` that restores them; flowRefusal's rebuild advice says drop + create in the same script", "insight": "A carry that lives in a session cache is invisible at the statement that creates it; the place to say so is the drop, which is the last moment the roles are known. Snippets have no access roles, so they need nothing", "issue": "ako/mxcli#944", "file": "mdl/executor/drop_grants_note.go; cmd_microflows_drop.go; cmd_nanoflows_drop.go; cmd_pages_builder.go; flow_verdict.go", "test": "mdl/executor/drop_grants_note_test.go; flow_verdict_test.go (TestFlowRefusalNamesTheFlowAndTheReason)"}
{"date": "2026-10-03", "area": "mdl/executor/settings", "symptom": "after `alter settings language (DefaultLanguageCode: 'de_DE')`, `docker check` fails with CE4899 \"Empty caption. [German, Germany]\" at Tab page 'tabPage2' (Administration.Account_Overview, en_US only) while `check -p --references`, `lint` and exec are silent; a page created AFTER the switch in the same script fails the same way", "cause": "nothing compared required captions with DefaultLanguageCode (QUAL005 compares languages with each other, and `mxcli lint` does not even run it); and describeDefaultLanguage cached the authoring language once per session, so the switch did not reach later creates", "fix": "translations.MissingRequiredCaptions (measured set: Forms$TabPage.Caption in pages, snippets, layouts; templates and building blocks skipped) feeds lint QUAL006, the note printed by alterSettings (defaultLanguageChanged, which also drops the cached authoring language) and check -p MDL-I18N01 (CheckDefaultLanguageCaptions simulates which documents the script writes before/after the switch)", "insight": "Measure which caption kinds the build requires before flagging: of eleven kinds written en_US-only, only the tab page caption failed; flagging the rest would have made an error rule wrong ten times out of eleven. The first lint run also flagged 22 page-template tab pages mxbuild never reported, caught only by comparing lint's count with docker check's (1 vs 1 after the fix, 3 vs 3 on the e2e script)", "issue": "ako/mxcli#944", "file": "mdl/translations/required.go; mdl/linter/rules/required_captions.go; mdl/executor/default_language_captions.go; mdl/executor/cmd_settings.go (defaultLanguageChanged)", "test": "mdl/translations/required_test.go; mdl/linter/rules/required_captions_test.go; mdl/executor/default_language_captions_test.go"}
{"area": "mdl/executor", "date": "2026-10-03", "symptom": "ako/mxcli#944: describe printed `clear $L;` and `set $L = $M;` for a Change list Clear/Set (Replace) action; `clear` did not parse, and `set` on a list variable executed to a Change variable action that passes mxcli check but mx check refuses (CE7247 \"Variable 'A' does not have a primitive type\")", "cause": "the grammar had only add/remove for Change list, and the builder routed every `set $X = …` to ChangeVariableAction regardless of the target's type", "file": "`mdl/grammar/domains/MDLMicroflow.g4` (clearListStatement), `mdl/executor/cmd_microflows_builder_graph.go` (MfSetStmt → addReplaceListAction when isListVariable)", "insight": "A describe → exec round trip that compares only MDL text passes when the action TYPE changes but prints the same — the Set revert check stayed green until the test also read the stored $Type/Type from the unit. Assert the stored shape, not just the re-described text", "refs": ["ako/mxcli#944"]}
{"area": "mdl/executor", "date": "2026-10-03", "symptom": "ako/mxcli#953 item 1: two calls to a VOID Java/JavaScript action with the same output name (Studio Pro names a JS one after the action, `$RefreshEntity`) failed `check` — MDL063 in a microflow, \"duplicate variable name … already declared in this scope (CE0111)\" in a nanoflow — while mxbuild 11.13.0 builds them clean; describe's header also called the model invalid. Alongside it, the opposite: a real duplicate output in a nanoflow's if/else branches passed check and was CE0111 in mxbuild", "cause": "every named call output counted as a declaration regardless of the action's return type; MDL063 ran for microflows only, and the nanoflow path relied on the check-time body validator (validateFlowBody), which scoped names per branch", "file": "`mdl/executor/validate_void_code_calls.go` (voidCodeActions: script declarations + lazily opened project), `validate_microflow_ce_gaps.go` (checkDuplicateVariableNames skips void calls, walks error handlers), `validate_nanoflow.go` (MDL063 for nanoflows), `cmd_microflows_builder_validate.go` (duplicatesOwnedElsewhere), `cmd_microflows_show.go` (duplicateOutputVariableWarnings)", "insight": "Measure what the name IS before deciding whether to print it: a void call's output name is inert in mxbuild (a later `declare` of the same name is clean, a use is CE0109 Undefined variable), but Studio Pro stores it with UseReturnVariable=true, and the bare `call …` form writes an empty name — so describe keeps `$X =` for the round trip and only the declaration count changes. The Java action reader returns a NIL ReturnType for Void (codeActionReturnTypeFromGen) while the JavaScript reader returns a VoidType; a mock built from the type name passed while the real project did not — run the end-to-end check on a real project. Two validators owning one rule disagreed in both directions; give the rule one owner (MDL063) and switch the other off for it.", "refs": ["ako/mxcli#953"]}
{"area": "mdl/executor", "date": "2026-10-03", "symptom": "ako/mxcli#953 item 7: `check --references` accepted `call java action M.ValidateEmail(email = …)` (the parameter is EmailAddress), which mxbuild rejects with CE1613; `call microflow` / `call nanoflow` with an argument naming no parameter of the callee passed even in isolation", "cause": "the Java/JavaScript parameter check existed, but `check` returned before the reference tier whenever a semantic rule reported an error (the repro also had a true MDL063), and validateWithContext returned a flow's body-validation errors INSTEAD of its reference errors; flow calls had no parameter-name check at all", "file": "`cmd/mxcli/cmd_check.go` (reference tier runs after semantic errors), `mdl/executor/validate.go` (flowCallRef + flowValidationError; validateCodeActionParams treats a known-empty parameter list as none)", "insight": "A check that 'accepts' an input may never have looked at it: before writing a new rule, run the existing one in isolation — here it already fired, and the bug was the early return two tiers up. mxbuild's CE1613 is itself a gate (it hides the other errors in the same run), so measure one fault per mx check.", "refs": ["ako/mxcli#953"]}
{"area": "mdl/executor", "date": "2026-10-03", "symptom": "`check` refuses (MDL-BUTTON01, an error, so `exec` refuses too) a control-bar button of a data grid nested inside a data view or a list view passing `$currentObject`, which mxbuild 11.13 builds with 0 errors; the same grid at top level is CE1571 and correctly flagged", "cause": "`checkButtonContextTree` carried only 'inside a control bar of X', never whether an ancestor data container already supplies an object; the control bar of a nested grid inherits the ENCLOSING object as `$currentObject`", "file": "`mdl/executor/validate_page_button_context.go` (`checkButtonContextTree`, `isObjectContextContainer`)", "insight": "The grid's own data source never scopes its control bar, but its ancestors' do: carry an inContext flag that a data view / list view / gallery / grid sets for its non-control-bar children, and pass the PARENT's context (not the grid's) into the control bar. Measured both containers and the top-level control on the same page, so the rule kept its true positive", "refs": ["ako/mxcli#953"]}
Loading
Loading