fix(cli): keep --json stdout pure across the query commands - #719
Merged
Merged
Conversation
The visitor stores DescribeFragmentFromStmt.ContainerType as "PAGE"/"SNIPPET"
while describeFragmentFrom switched on "page"/"snippet" with no default, so
neither branch ran and every widget was reported missing ("not found in page
M.P" — without even naming the widget). Same casing split as ALTER PAGE (#402)
and ALTER STYLING (#631).
Normalise with strings.ToLower (the convention of the other consumers), make
an unrecognised container type an error instead of an empty widget list, and
name the widget in the not-found message. The new tests parse the statement
and dispatch it through the registry, so they pin the visitor/executor casing
contract that a hand-built lowercase AST could not see.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`search` needs only a full catalog, which indexes string literals but not MDL source. On a project where `refresh catalog full source` had never run, the source half of every search came back empty with no word — read by agents as "no microflow/page mentions this". search now checks the build mode the catalog records (not the row count, so a built-but-empty index stays silent) and, below "source", prints a warning naming `refresh catalog full source` on a new ExecContext.Diagnostics writer (nil = stderr), keeping --format json stdout pure. The old unconditional "Tip: refresh catalog source" on stdout is replaced by it. Table format now delegates to execSearch up front instead of querying twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`show structure depth 1` (and its JSON form) filtered the catalog on MicroflowType = 'microflow' / 'nanoflow', while the catalog builder stores 'MICROFLOW' / 'NANOFLOW'. SQLite's `=` is case-sensitive, so both counts were always empty, and the summary omits zero counts, so every module appeared to have no flows at all. The builder's values are now exported constants (catalog.MicroflowTypeMicroflow/Nanoflow/Rule), used by the writer, by the structure query and by the linter's DocumentNoun switch, so reader and writer share one spelling. The test runs the real catalog builder over a MockBackend and reads the counts back through structureDepth1 / structureDepth1JSON, so it detects a casing drift between writer and reader; reverting only the reader to lower case makes it fail with the reported symptom. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and mapped entities The refs graph stopped at documents. `impact Module.Entity.Attr` answered "not referenced" for an attribute a microflow writes and a page displays (Evora: DigitalTwin.Machine.NumberOfIncidents), an enumeration had no inbound edge at all, a workflow started only by a microflow had no caller, a page navigating an association and a mapping mapping an entity were invisible. - A raw-document walk over microflows, nanoflows, rules, pages, snippets, workflows and import/export mappings matches every string value against the names the model declares: whole-string matches are structured references (MemberChange.Attribute, AttributeRef.Attribute, EntityRefStep.Association, EnumerationType.Enumeration, ObjectMappingElement.Entity); inside expressions, association paths and qualified enumeration values. New kinds: member, type, value, mapping. - XPath constraints resolve bare attribute names against their target entity (and its generalizations), association paths, and enum attributes compared to a literal (kind xpath). Page/snippet constraints had no target entity because resolveEntityRefFromBSON read a key no stored EntityRef carries. - Entities -> enumerations from attribute types (kind type). - WorkflowCallAction -> WORKFLOW (kind call). - New types ATTRIBUTE, ENUMERATION, ENUMERATION_VALUE, IMPORT_MAPPING, EXPORT_MAPPING published in the lint-rule vocabulary; members kept off the graph_god_nodes asset side; CatalogSchemaVersion 15. Not covered: a bare member named through a variable in a free-text expression ($Order/Total), whose type is not known to the catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y answer says what was checked `refs` and `impact` printed a row per edge, so a microflow with two retrieve activities over an entity appeared twice (Evora: ProductionLine_Reset on DigitalTwin.Machine); the impact summary counted those rows (MICROFLOW: 9 over six microflows) and printed the types in map order, different between runs. - select distinct, with a total order; the summary counts distinct elements per type, in type order, and the footer gives both numbers. - impact/refs on an enumeration include the edges to its values, with a Target column naming which value. - "(no impact - element is not referenced)" is gone. For an attribute or an enumeration value the message lists the sites that were checked and the ones that are not resolved (a member named through a variable in an expression; a decision branch on an enum), and says to run search first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e listed once `context DigitalTwin.Machine` said "Related Entities: (none found)" for an entity with five associations. The section read refs rows whose SOURCE is an entity, but an association edge's source is the ASSOCIATION, so only generalizations could ever match. It now reads both ends from the associations table (present in a fast catalog too), plus the generalization and the specializations. Also: Direct Callers / Shown By / workflow callers list each source once, and the enumeration context reads the same edge set as impact (type and value uses), grouped into entities, flows and pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring it Studio Pro stores expressions exactly as typed, and a trailing newline left in the expression editor is common. describe interpolated the stored text verbatim, so `change $X (Status = Mod.Enum.Val` / `);` put the closing paren or semicolon on a line of its own (297 such lines in Evora Factory Management's microflows alone). One helper, describeExpr (TrimSpace; interior newlines kept), now renders every stored expression the describer emits: change/create members, set, change-list values, aggregate/reduce, list operations, call microflow / nanoflow / java / javascript / external action arguments, show page args, log node + template params, show message params, REST/web service/DB query params, while, decision and rule arguments, and page widget Visible/Editable conditions, action arguments, datasource arguments and client template parameters. It replaces the five ad-hoc TrimSuffix/TrimRight calls that each covered one slot. The stored model is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`--json` output did not parse. refs, callers, callees, impact, structure,
show, search and `-c "select …"` printed "Connected to:", catalog
load/build progress, a header and a "Found N" count ahead of the payload;
an empty answer was a sentence ("(no references found)") instead of [];
`context` ignored --json; `check --format json` wrote its document to
stderr, one per phase; `diff --json` printed a text diff.
The executor writes the payload and its commentary through the same
ctx.Output, so the mendixlabs#904 mechanism (pick the executor's writer at the cmd
layer) cannot separate them for commands whose payload the executor
itself prints. Add ExecContext.progress(): Output in text mode (so
interactive output is unchanged), the diagnostics stream (stderr) when
Output carries JSON. The Diagnostics field is the one #716
introduces, with the same semantics.
- Status/progress in connect, catalog load/build, catalog queries and the
refs/callers/callees/impact handlers goes through progress().
- Empty results go through writeEmptyResult: [] in JSON mode.
- context --json wraps the markdown in {name, type, depth, context}.
- show catalog status / show widgets gain a JSON path; empty listings of
data transformers, import/export mappings and navigation follow the
existing `&& ctx.Format != FormatJSON` idiom.
- search: --json is the canonical spelling; --format json is kept as a
deprecated alias and now sets the executor format, so it is as clean.
- A cold-cache catalog build under -q moves its per-table lines to stderr
instead of stdout (search -q --format names).
- check: structured formats emit one document on stdout covering every
phase; the exit code is unchanged.
- diff / diff-local refuse --json (no JSON output) instead of ignoring it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… trimming expression whitespace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # mdl/executor/cmd_search.go
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptoms
Found by an agent-orientation audit on a large app (Evora Factory Management, MPR v1, 140MB).
--jsonoutput did not parse:refs,callers,callees,impact,structure,show,searchandmxcli -c "select … from CATALOG.x" --jsonprintedConnected to: …, catalog progress (Loading cached catalog…, and 40+✓ Table: Nlines on a cold cache), aReferences to Xheader and aFound N result(s)count ahead of the payload.(no references found),(no results)) instead of[].contextignored--jsonand printed markdown.searchtook both--format jsonand--json, with--format jsonstill leaking progress.check --format json|sarifwrote its document to stderr, one document per phase, so stdout held only executor chatter.show catalog statusandshow widgetshad no JSON path. Emptyshow data transformers/import mappings/export mappingsprinted a sentence.diff --jsonsilently printed a text diff. A cold-cachesearch -q --format namesprinted the build's✓lines ahead of the names.Cause
The executor has one writer,
ctx.Output, and it carries both the payload and every status line. The mendixlabs#904 fix (progressSink, used bylint/report) works because those commands print their payload themselves, so the executor's writer can be pointed at stderr. For the query commands the executor prints the payload, through the same writer as the chatter. No writer choice at the cmd layer can separate the two, so the split has to happen inside the executor, one line at a time.Fix
ExecContext.progress()returnsOutputin text mode, so interactive output is byte-for-byte unchanged. In JSON mode it returns the diagnostics stream (stderr). Connect, catalog load/build/warnings, catalog-query counts, and the refs/callers/callees/impact headers go through it.ExecContext.Diagnosticsfield anddiagnostics()helper exactly as fix(search): warn when the source index was never built #716 adds them (identical hunk, nil means stderr). Whichever PR merges second should see a clean or trivial merge there.writeEmptyResult:[]in JSON mode, with the sentence moved to stderr.context --jsonwraps the markdown in{name, type, depth, context}, the same envelope shape asdescribe --json.search:--jsonis canonical. It is the root flag, and every other query command (refs/callers/callees/impact/context/show/describe/select) already uses it.--format jsonstill works as a deprecated alias (listed as such in--help), and it now sets the executor format, so its output is identical (tested).check: structured formats now print one document on stdout that covers every phase. The exit code is unchanged. This moves the stream (stderr → stdout). No in-repo consumer read stderr (checked vscode-mdl, docs, scripts).diff/diff-localrefuse--json(stderr, exit 2) instead of ignoring it.--jsonis a root persistent flag, so every subcommand accepts it.-q, a cold catalog build sends its per-table lines to stderr. They are moved, not dropped, because an 18-minute build needs a sign of life.Tests + proof
cmd/mxcli/json_output_purity_test.goruns the realmain()in a child process (reusingTestRunMainHelper), so stdout and stderr are really separate,os.Exitpaths can be observed, and no cobra state leaks between rows:TestJSONFlagKeepsStdoutPureJSON: a table of 25 subcommand invocations, including a cold-catalog run and empty results that must be[]. The next command that prints through the executor belongs in this table.TestJSONFlagErrorsGoToStderr: context/describe of a missing element, a bad catalog table, anddiff --json. Each must exit non-zero with nothing non-JSON on stdout.TestTextModeKeepsProgressOnStdout: the control. Text mode still showsConnected to:, headers and(no … found)on stdout.TestSearchFormatJSONAliasIsAsPureAsJSONFlag,TestCheckStructuredOutputIsOneDocumentOnStdout,TestSearchQuietNamesIsOnlyNamesOnAColdCatalogmdl/executor/progress_stream_test.go: unit tests forprogress()andwriteEmptyResult.Proof by revert:
--jsonrow failed with the reported symptom, and the text control passed.progress()forced to returnOutput, all 18 then-present--jsonrows failed, plus the alias and error tests.cmd_check.goreverted, all 5 check rows failed (stdout is not JSON).Output, the names test failed on"✓ Modules: 2".Evora verification (
bin/mxcli <cmd> … | jq . >/dev/null, source-mode catalog)mxcli refs -p evora.mpr Encryption.Decrypt --jsonmxcli refs -p evora.mpr Nope.Nothing --json[])mxcli callers -p evora.mpr Encryption.Decrypt --jsonmxcli callers -p evora.mpr Encryption.Decrypt --transitive --jsonmxcli callees -p evora.mpr MxModelReflection.ACT_ShowMemberPage --jsonmxcli impact -p evora.mpr MxModelReflection.MxObjectType --jsonmxcli context -p evora.mpr MxModelReflection.MxObjectType --jsonmxcli context -p evora.mpr Nope.Nothing --jsonmxcli search -p evora.mpr Customer --jsonmxcli search -p evora.mpr Customer --format jsonmxcli search -p evora.mpr zzqqxxnomatch --json[])mxcli -p evora.mpr -c "select QualifiedName from CATALOG.microflows limit 5" --jsonmxcli -p evora.mpr -c "select … where Name = 'zz'" --json[])mxcli -p evora.mpr -c "show catalog status" --jsonmxcli -p evora.mpr -c "show catalog tables" --jsonmxcli show -p evora.mpr modules --jsonmxcli show -p evora.mpr entities --jsonmxcli show -p evora.mpr microflows MxModelReflection --jsonmxcli describe -p evora.mpr entity MxModelReflection.MxObjectType --jsonmxcli structure -p evora.mpr -d 1 --jsonmxcli check ok.mdl -p evora.mpr --jsonmxcli lint -p evora.mpr --jsonmxcli diff -p evora.mpr ok.mdl --jsonText mode on Evora (
mxcli refs … Encryption.Decrypt) still printsConnected to:/Loading cached catalog/References to …on stdout.Validation
make buildandmake lintpass.make test: the only failures are the.mpk-fixture tests incmd/mxcli,mdl/executor,modelsdk/widgets/mpkandsdk/widgets/mpk(open …/*.mpk: no such file). This worktree is a sparse checkout that omits.mpkblobs. I reproduced the same failing set on the unmodified base with these changes set aside, so they are pre-existing and unrelated.Out of scope / noticed
-c "show widgets" --jsonwrites the complete document and then exits 1 withoutput line limit exceeded.mxcli exec script.mdl --jsonstill mixes mutation messages ("Created entity …") with JSON on stdout.-cruns is a stream of documents (jqaccepts it; a single-document parser does not).check -pwithcreate association … from System.Nopepasses the reference check. This is unrelated and was noticed while writing the test.Finding appended to
.claude/skills/fix-issue/findings/cmd-mxcli.jsonl. One sentence added todocs-wiki/bug-patterns/cli-contract-defects.md.🤖 Generated with Claude Code