Skip to content

Sudoku findings: OQL column union, Docker-free mxcli test --local, loop-scope check - #105

Merged
ako merged 5 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h
Aug 6, 2026
Merged

Sudoku findings: OQL column union, Docker-free mxcli test --local, loop-scope check#105
ako merged 5 commits into
mainfrom
claude/mxbuild-diagnostics-spike-emta6h

Conversation

@ako

@ako ako commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Findings from the sudoku test project, plus one lint-config commit that was pushed to this branch after #100 merged and so was never in a PR.

mxcli oql dropped a whole column (finding 39, first half)

The runtime omits a null-valued column from a row's JSON object entirely, and parseOQLFeedback took the column list from rows[0]. A column that happened to be null in row 1 disappeared from the entire result — no error, no empty column, a well-formed table answering a narrower query than the one asked. The reporter's harness declared 40 of 40 games broken on that basis.

The column set is now the union of all rows' keys. New keys are inserted after the last already-known key rather than appended, so a column absent from early rows keeps its SELECT position ([A,C][A,B,C][A,B,C], not [A,C,B]). Rows are only re-scanned for key order when they carry an unseen key.

Proven by stubbing the union back to first-row-only: the three sparse cases fail with exactly the reported symptom.

mxcli test --local — microflow tests without Docker (finding 41)

mxcli test got through parsing, runner generation, model injection, after-startup wiring and the entire mxbuild build natively, then died on docker up. Microflow tests were unavailable in exactly the environments mxcli targets — containers that ship /usr/bin/docker with no daemon.

Only the last step needed a container. docker.StartLocalApp performs the run --local boot (cache mxbuild + runtime → ensure database → mxbuild --serve build → standalone runtime); the runner reads its output from the runtime log. The Docker path moved to runDockerAndCapture; both modes share parse, inject, parse-results and cleanup. Local runs use their own ports (8081/8091) and a <project>_test database, so a warm run --local loop can keep serving the same project and test fixtures never land in the database the developer is watching.

Two things only running it could have taught:

  • The runner reports through an after-startup microflow, so its LOG output happens during the start action, before the runtime attaches its log subscriber. Registering the subscriber early is not possible — the runtime answers LoggingException pre-start. The JVM console tee, live from spawn, is what carries it; confirmed by A/B running with the early attach removed, and the broken option was deleted rather than shipped.
  • A failing test makes the runner return false → after-startup fails → start returns an error. The first version reported that as a broken run and printed a stack trace instead of the test report.

Verified end-to-end in a daemon-less container: 2 passed (exit 0); then 1 passed / 1 failed (exit 1), with the project restored both times.

Loop variable used outside its loop → MDL053

Referencing a loop's variable after end loop; passed mxcli check and then failed the build. Both flavours reproduce against mxbuild 11.12.1 — the iterator itself, and anything the body introduces:

[error] [CE0108] "Variable 'Inner' is defined but not in scope at this location."
[error] [CE0108] "Variable 'item' is defined but not in scope at this location."

Each loop-scoped name is mapped to the loop whose own body introduces it (nested loops keep their own), then the flow is walked tracking which loops enclose the current position. A name claimed by two loops is deliberately not reported — that is the MDL052/CE0111 duplicate-name case, and without the guard the existing MDL052 negative example failed for the wrong reason. A sweep over all MDL examples shows zero MDL053 hits.

MDL052 is the sibling rule: names are unique across the whole microflow, but visibility stops at the loop body. The write-microflows skill now states both and shows the carry-out idiom.

Attribution note: this was originally filed against sudoku finding 40, which is actually an app bug in that project and explicitly marked "not an mxcli bug". The check is real and verified, but the repro filename and symptom row said otherwise; one commit here corrects that.

Also included

feat(init): seed a lint config that excludes the System module — reviewed and approved during #100, pushed to this branch after that PR merged, so it never landed. Rebased onto current main and carried here.

Not addressed

Finding 39's second half — ORDER BY on a DateTime attribute being ignored. ExecuteOQL forwards the query text verbatim, which points at the runtime's preview engine rather than mxcli, but that is a hypothesis and wants a reproduction against a running app with DateTime data before anything is claimed.

Validation

  • make build, go vet, go test ./mdl/... ./sdk/... ./cmd/mxcli/... green
  • make check-mdl: 275 examples pass, including the new negative test
  • Every fix A/B'd against a pre-fix binary; the two runtime-behaviour claims verified against real mxbuild 11.12.1 / a real booted runtime

🤖 Generated with Claude Code

https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4


Generated by Claude Code

claude added 5 commits August 6, 2026 21:59
`mxcli lint` on a freshly initialised app reported ~106 issues, of which ~102
came from the System module — Mendix's own platform module, whose entities you
cannot document, give access rules, or rename members of. Every one of those
findings is un-actionable, and they buried the handful about the developer's
own code. Measured on a real project here: 170 issues, 102 of them System,
across QUAL002 (50), SEC001 (38), CONV001 (8), DESIGN001 (4), MPR003 and
SEC006.

`mxcli lint` already had -e/--exclude and a lint-config.yaml with
excludeModules, so this was solvable — but only by a user who already knew to
look. The default is what people see. `mxcli init` now writes
.claude/lint-config.yaml with System excluded, which drops that project from
170 findings to 68.

The file is written only when the project has no lint config in any location
FindConfigFile searches, so re-running init never discards edits. It is written
outside the per-tool branches because `mxcli lint` reads it regardless of which
AI tool was selected.

Also warns when --modules names a config-excluded module. LintContext
.IsExcluded checks the exclude set before the include set, so `lint -m System`
against the new default would otherwise report zero findings with no
explanation — a trap this change would have introduced.

Addresses issuetracker finding #9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The Mendix runtime omits a column from a row's JSON object when its value
is null. parseOQLFeedback took the column list from the first row only, so
a column that happened to be null in row 1 was dropped from the entire
result — `mxcli oql` rendered a narrower table than the query asked for,
with no error and no empty column to hint at the loss.

The column set is now the union of every row's keys. New keys are inserted
directly after the last key already known rather than appended, so a
column absent from earlier rows keeps its SELECT position: merging
[A, C] with [A, B, C] gives [A, B, C], not [A, C, B]. Rows are re-scanned
for key order only when they carry a key not seen yet, so the uniform case
still costs one length check.

Verified by stubbing the union back to first-row-only and watching the
reported symptom return.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Referencing a loop's variable after `end loop;` passed `mxcli check` and
then failed the build with

  [error] [CE0108] "Variable 'item' is defined but not in scope at this
  location."

Both flavours reproduce against mxbuild 11.12.1: the iterator itself, and
anything the loop body introduces (a retrieve, a `$X = create …`, a call
output).

MDL053 maps each loop-scoped name to the loop whose own body introduces it
(a nested loop keeps its own names), then walks the flow tracking which
loops enclose the current position and flags any reference from outside
the owner. A name claimed by two loops is deliberately not reported — that
is the MDL052/CE0111 duplicate-name case, and without the guard the
existing MDL052 negative example started failing for the wrong reason.

MDL052 is the sibling rule: names are unique across the whole microflow,
but visibility stops at the loop body. The write-microflows skill now
states both and shows the carry-out idiom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The loop-variable-scope check (MDL053) was filed against sudoku finding
#40, which is actually an app bug in that project ("roughly one dealt
board in twenty is not solvable by forced logic") and explicitly marked
"not an mxcli bug". The check is a real, mxbuild-verified check-parity
gap, so it stays — but it is not that finding, and the repro file and
symptom row said otherwise.

The OQL column fix does match sudoku #39, but only its first half; the
second (ORDER BY on a DateTime attribute being ignored) is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mxcli test` got all the way through parsing, runner generation, model
injection, after-startup wiring and the full mxbuild build with no
container, then died on `docker up` — so microflow tests were unavailable
in exactly the environments mxcli targets, where /usr/bin/docker exists
with no daemon behind it.

Only the last step needed a container. --local boots the same standalone
runtime `mxcli run --local` uses (new docker.StartLocalApp: cache mxbuild
+ runtime, ensure the database, mxbuild --serve build, boot) and reads the
runner's output from the runtime log. The Docker path moves to
runDockerAndCapture; both share parse, inject, parse-results and cleanup.

Local runs use their own ports (8081/8091) and a <project>_test database,
so a warm `run --local` loop can keep serving the same project and test
data never lands in the database the developer is looking at.

Two things only running it could have taught:

  - The runner reports through an after-startup microflow, so its LOG
    output happens during the start action, before the runtime attaches
    its log subscriber. Registering the subscriber early is not possible —
    the runtime answers LoggingException pre-start. The JVM console tee,
    live from spawn, is what carries it; confirmed by A/B running with the
    early attach removed, and it is documented where it matters.
  - A failing test makes the runner return false, which fails the
    after-startup action, which makes `start` return an error. The first
    version reported that as a broken run and printed a stack trace
    instead of the test report.

Verified end-to-end in a container with no Docker daemon: two tests
passing, then one passing and one failing with exit 1, project restored
both times.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
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