Skip to content

mxcli fmt --upgrade: registry rewrites, header-gated hook, execute-both property test - #742

Merged
ako merged 42 commits into
mainfrom
feature/735-fmt-upgrade
Sep 27, 2026
Merged

ako merged 42 commits into
mainfrom
feature/735-fmt-upgrade

Conversation

@ako

@ako ako commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Closes #735 (plan item 1.3, ADR-0011 decision 1).

Merges on top of the step-2 chain (#740, #745, #741, #738 via feature/731-create-or-modify). The branch contains that merge; the only conflict was docs-site/src/language/basics.md, where both sections are kept (the mdl 1 strictness table first, then fmt --upgrade).

What

  • mdl/upgrade, the rewrite engine.
    • Aliases: every use the visitor records on Program.Deprecations becomes its canonical spelling. A keyword swap (DEPR001 create or replace → create or modify, DEPR002 show → list) happens in place, in the token's letter case. A structural rewrite (DEPR003/004, list call form → statement form) applies the visitor's ast.Fix. A use without a rewrite goes to Result.Unrewritten, with its reason.
    • Header-gated constructs: with AddHeader, every LanguageNote is rewritten before mdl 1; is added. gatedRewriters holds the codes that have a rewrite, unrewritable the ones that don't, with the reason. A construct with no rewrite blocks the header with HeaderBlockedError, which names every such construct and why. Nothing is written.
    • How rewrites are computed: the parse tree is the only place the tokens and their positions still exist, so the visitor that records a construct also computes its rewrite, as rune-offset edits (ast.Fix, mdl/visitor/visitor_upgrade_fixes.go). A Fix edits only the tokens that differ, so comments, layout and nested fixes survive. An occurrence that can't be rewritten carries NoFix, the reason.
    • Self-check: the output is re-parsed. It must parse, keep the statement count, and record no rewritable deprecation, so a second run is a no-op.
  • mxcli fmt --upgrade [--header]. --header is opt-in while mdl 1 is a preview. Unrewritten uses are printed with their reason, and a refused header is an error that lists the constructs. This is documented in docs-site (language basics, with the table below) and in mxcli syntax language-header.

Rewrites (with --header)

Code Rewrite
MDL-V1-SEMI adds the missing ; right after the statement's last token, before any trailing comment
MDL-V1-SLASH deletes the / line, or only the / token when it shares a line with the statement
MDL-V1-ESCAPE writes the literal's mdl 0 value with '' as the only escape (see the cases below)
MDL-V1-LIMIT1 retrieve … limit 1 (one object) → retrieve … first
MDL-V1-SET $x = e → set $x = e
MDL-V1-LIST a call mdl 0 turns into an activity becomes the statement form (set $T = head($L) → $T = head $L, $F = find($L, Name = 'x') → $F = find $L by Name = 'x'). For find/contains on a variable declared String (a parameter or declare), mdl 0 builds the string function, so the call stays as it is and gains set
MDL-DEPR003 / MDL-DEPR004 (no header needed) call form → statement form for every list operation and aggregate, including subtract($A,$B) → subtract $B from $A, sum($L.Attr) → sum $L by Attr, reduce(…, initial:, returns:) → reduce $L from … as … using …; by vs where exactly as the call form decided
MDL-V1-REPLACE02 create or replace user role / demo user (a plain create under mdl 0) → create …

MDL-V1-ESCAPE depends on where the literal sits:

  • Plain literal, or one inside a re-rendered expression: rewritten to its mdl 0 value.
  • Inside an expression stored as written (one that spans lines): no edit. mdl 0 already passes the backslash through to Mendix, so it means the same under mdl 1.
  • Escaped line break inside a re-rendered expression: reported. Writing the break into the literal makes the builder store the expression as written. The execute-both test caught this: a log message stops being the template text and becomes a {1} parameter (bug-tests/264-log-node-expression-roundtrip.mdl).

Known-unrewritable

These are reported by fmt --upgrade --header, which then refuses the header. Tests list them explicitly, and each list may only shrink.

  • Whole codes (unrewritable in mdl/upgrade/gated.go; TestGatedRegistryIsComplete holds every visitor.LanguageChanges() code to exactly one of gatedRewriters / unrewritable, and fails if a code is added to unrewritable):
    • MDL-V1-PROP: an unknown property key is ignored under mdl 0. Which key was meant can't be guessed.
    • MDL-V1-PROPVALUE: a mis-shaped value is ignored, or read by its shape. What was meant can't be guessed.
    • MDL-V1-REPLACE01: create or replace view entity drops and recreates under mdl 0, and no mdl 1 statement does that. create or modify keeps the identity the old meaning discards, and a drop in front would fail where the old statement created the entity.
  • Occurrences of codes that otherwise have a rewrite (each carries its reason):
    • a nested list operation or non-variable operand (count(filter(…))), which needs a variable name nobody chose;
    • find/contains on a variable whose type the script does not state, e.g. a call's result;
    • an escaped line break inside a re-rendered expression;
    • a reduce whose seed (which moves) holds a backslash-escaped string;
    • a sum($L/Path/Attr) whose attribute is not a plain name.
  • Corpus scripts that keep mdl 0 (keepsItsVersion in mdl/upgrade/examples_test.go): bug-tests/1101-nested-list-operand-dropped.fail.mdl (MDL-V1-LIST, nested calls) and bug-tests/264-log-node-expression-roundtrip.mdl (MDL-V1-ESCAPE, line break in a log message).
  • Different AST, same model (buildsTheSameModelNotTheSameAST): ledger-53-string-contains.mdl and ledger-63-string-find.mdl. Under mdl 0, a List operation on a String input is turned into the string function by the flow builder. Under mdl 1 it is set $x = contains(…) directly. The execute-both test proves both write the same model.

Design choices the ADRs did not settle

  1. --upgrade does not run the heuristic formatter. It upper-cases identifiers (Issue64.User → Issue64.USER) and changes what 374 of 701 example scripts build. TestFormatterChangesMeaning pins this.
  2. The rewrite is textual, at parser positions, not an AST re-print. Keyword swaps keep the replaced token's case, and inserted keywords follow the operation keyword's case (FIND(…) → FIND $L WHERE …).
  3. The header goes on line 1. A construct without a rewrite makes --header fail rather than add the header and carry on.
  4. The property test runs the upgrade with the header. A script whose header is refused is upgraded without it, so its alias rewrites are still proven.
  5. The default property run skips terminator-only upgrades. It does not execute scripts whose only edits are the header, ; and / lines: terminators never reach the AST, and the examples test proves all 701 scripts build the same statements. MXCLI_UPGRADE_ALL=1 executes everything (run locally, see below). This keeps the integration step within budget.

Test plan (what I ran, on the merged tree)

  • make build and make lint pass.
  • go test ./mdl/upgrade ./mdl/visitor ./mdl/deprecation ./mdl/langver ./cmd/mxcli passes.
    • Examples test: 715 scripts. 701 parse and 233 need an alias rewrite; 2 keep mdl 0. For each script, with and without the header: the output parses, records zero deprecations, builds the same statements, and fmt --upgrade is idempotent.
    • New unit tests: TestUpgrade_GatedRewrites (21 cases), TestUpgrade_UnrewritableBlocksTheHeader (5), TestUpgrade_ListCallFormToStatementForm (19, AST-equal), TestGatedRegistryIsComplete, TestLanguageChangesListsEveryDeclaredChange (reads the visitor's sources, so a new langver.Change can't escape the registry test), and TestFmtUpgrade_ReportsWhatItCannotRewrite.
  • go test -tags integration ./mdl/upgrade/ ./mdl/roundtrip/ passes. TestUpgradeExecutesToTheSameModel takes about 6 min: 224 scripts execute to the same model and 25 are out of scope.
  • MXCLI_UPGRADE_ALL=1 (all scripts, 15 min): 587 execute on PedApp and upgrade to the same model, 114 are out of scope (the original does not execute on PedApp), 2 keep mdl 0, 14 do not parse.
  • Revert checks. Each mutation below made the named test fail; I restored the code after each.
    • SEMI fix removed → GatedRewrites/missing_semicolon, examples.
    • LIMIT1 fix removed → GatedRewrites/limit_1…, examples.
    • set not deleted in the set-call rewrite → GatedRewrites/set_with_a_list_call, examples.
    • subtract operands not swapped → ListCallForm/subtract (AST differs), examples.
    • by/where choice ignored → ListCallForm/filter…and…, GatedRewrites/find…by_expression, examples.
    • String operand treated as a list → GatedRewrites/find_on_a_String…, examples (stale buildsTheSameModelNotTheSameAST entry).
    • Escape rewritten inside a stored-as-written expression → GatedRewrites/escape_in_an_expression_stored_as_written, examples.
    • Line-break escape allowed in an expression → UnrewritableBlocksTheHeader/escaped_line_break…, examples. Before that guard existed, the execute-both test failed on 264-log-node-expression-roundtrip.mdl (Microflows$Microflow MF_LogMultilineMessage written differently).
    • Code added to unrewritable → TestGatedRegistryIsComplete.
    • Change dropped from LanguageChanges() → TestLanguageChangesListsEveryDeclaredChange.
  • Existing controls (TestUpgradeExecuteBoth_Controls) pass unchanged.
  • No Studio Pro verification: the change rewrites script text and writes no BSON. The execute-both test is the model-level proof.

Followups (not in this PR)

  • find/contains on an operand whose type the script does not state could also be rewritten to the statement form. Today's flow builder still turns a String input into the string function under mdl 1, so the model would be the same, but the mdl 1 text would read as a List operation on a String. It is reported instead.
  • The visitor stores a multi-line string literal as a source-preserved expression, so under mdl 1 a log message with a line break can't be a plain template literal. That's why 264-log-node-expression-roundtrip.mdl keeps mdl 0.
  • Register MDL065 (legacy case/else in split type) in mdl/deprecation; plan item 3.5 (show <single thing> → describe); plan item 3.2 (fix formatter.Format's identifier upper-casing, then let fmt --upgrade format).

🤖 Generated with Claude Code

ako and others added 30 commits September 27, 2026 08:21
Rewriting a user role or demo user re-encoded Security$ProjectSecurity's
UserRoles/DemoUsers lists with marker 3; Studio Pro writes 2 (PedApp,
TestApp and expr-checker agree). Running unchanged describe user role
output therefore wrote the unit (#731).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With describe emitting create or modify, its output re-executes. Two
things it cannot express were lost on that rewrite, as the Java twin
already handled:
- the placeholder body printed when the .js source is unreadable was
  written as the action's source;
- parameter Description/Category (printed as a comment) were dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

A /** */ comment cannot spell blank lines, trailing whitespace or CRLF, so
describe output of Studio Pro prose restated it in normal form and the
rewrite overwrote the stored bytes with nothing changed. When the stated
text is exactly the stored text's normal form, keep the stored bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
describe printed a plain create for JavaScript actions, user roles,
layouts, REST/OData clients, OData services, external entities, database
connections, data transformers, agent-editor documents, workflows,
validation rules and modules, and create or replace (the deprecated
spelling) for navigation. A plain create fails with "already exists" on
the document it describes, so describe output did not run unchanged.

Strikes the #721 F class (45 JavaScript actions, 2 user roles) from the
round-trip allowlist, plus the putget law of
Administration.NewWebServiceAccount, fixed by the doc-comment carry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…731)

Under mdl 1, create or replace on a view entity is the identity-carrying
rewrite create or modify uses, instead of delete and recreate, and on a
user role or demo user it is create or modify instead of a plain create.
Each is then a plain MDL-DEPR001 alias. Under mdl 0 (no header) the alpha
meaning is kept and MDL-V1-REPLACE01/02 warns (ADR-0011).

The GUID test runs on testdata/testapp-views, a trimmed copy of ako/TestApp
whose view entity MyFirstModule.VCar was authored in Studio Pro (GUID !=
$ID); mdl 0 create or replace is the control that re-mints the GUID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dule role if exists (#731)

create module role was its own securityStatement rule with only an
or modify prefix, so it took none of the prefixes other document types
get: or replace was a parse error and a doc comment did not attach. It is
now a createStatement kind (or replace is the MDL-DEPR001 alias there),
and drop module role takes if exists like drop user role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t mandatory under mdl 1

Each Studio Pro List operation / Aggregate list activity is one statement
whose operand is a variable, so nesting cannot be written:

  $Open = filter $Orders by Status = M.Status.Open;
  $Big  = filter $Orders where $currentObject/Total > 1000;
  $N    = count $Open;
  $Csv  = reduce $Orders from '' as String using $currentResult + $currentObject/Name;

The statement forms parse under every version. The call forms keep
parsing: a respelling everywhere except find/contains, registered as
MDL-DEPR003 (list operations) and MDL-DEPR004 (aggregates), both building
the same AST. find(...)/contains(...) clash with the string functions, so
they, a list call after `set`, and a nested call are version-gated
(MDL-V1-LIST): kept and warned under mdl 0, refused under mdl 1, where
`set $x = find(...)` is always the string function. A reassignment without
`set` is refused under mdl 1 and warned under mdl 0 (MDL-V1-SET).

`where` is always Find/Filter by expression; `by` and the call form keep
the by-member reading when the condition is `Member = value`
(ast.IsMemberEquality, shared by visitor and flow builder).

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
describe writes the language of the newest frozen version, or of the
script it runs in when that is newer. While mdl 1 is a preview a plain
describe keeps the call form; inside an `mdl 1;` script it prints the
statement form, which executes back to the same microflow (PedApp test).

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…forms

Part of #733.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lementIDs, Writer.UpdateRawUnitPatch)

TransplantIDs pairs elements by type and position, which re-pairs the
surviving flows of a patched microflow onto their neighbours' $IDs after a
drop. A write that started from the stored bytes skips it; elision and the
storage-GUID guard still apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, replace, drop)

Splices a fragment into the raw stored unit: appends only the fragment's
objects and flows, rewires the flow around the target by its pointers and
the rewired end, moves nodes past the insertion point to make room, and
never rewrites an $ID. Save refuses a unit in which anything still points at
a removed element or two elements share an $ID. Refuses what it cannot do
safely: after a decision, before a join, inside a loop body, drop/replace of
a decision or of an activity with an error handler, and a placement that
would overlap an object. The modelsdk backend writes through
UpdateRawUnitPatch (canon.Reconcile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#736)

alter microflow|nanoflow M.F { insert after|before <target> { … }
replace <target> with { … } drop <target>; }. Targets are the #713 content
addresses, resolved against the stored flow before any operation runs.
Fragments are built with the create-microflow builder, seeded with the
flow's variables, and cut out of their start and end events. A fragment
variable that clashes with one the flow has, or one it reads that is not
declared upstream of the insertion point, is an error; so is dropping an
activity whose output is still read. Acceptance on PedApp VAL_Feedback:
only the new log, the two flows around it and the shifted positions differ;
an empty alter writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… handle

A handle has to parse as an alter target, and a target ends at the { of a
fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs the shared splice on the stored flow and sends the difference as
ped_update_document path operations in one update, after checking the live
document still matches the .mpr (PED addresses entries by index). Drop and
replace are refused: PED does not roll back a removal when an update fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce alone

describe now emits create or modify javascript action (#731), so its output
reaches the rewrite, which regenerated the .js file from the statement and
dropped the import list and the EXTRA CODE section. On TestApp,
NanoflowCommons.GetStraightLineDistance lost import { Big } and the deg2rad
helpers its user code calls. When the statement restates the stored user code
and parameter names, the file is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0010 R11, additive, so under every language version. One lexer rule
drops a comma whose next significant character closes a (), {} or [],
instead of a COMMA? in each of ~70 list rules: that kept every list's
error messages LL(1). Written into the parser, an unknown item after a
comma is reported as 'no viable alternative at input ,X' rather than
naming what the list expects, which lost e.g. the annotation-property
hint (mendixlabs#1014).

A comma still needs an item before it: (,) and (a,,) stay errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loop body flows are stored in the unit's Flows list, not in the loop, so
removing a Studio Pro loop with two or more body activities left them
pointing at removed objects and Save refused the unit
(TestApp ACT_ConflictedWorkflowHelper_ApplyJumpTo). mx check 11.14 on the
dropped and replaced copies gives the baseline error list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each operation was checked against the flow as stored only. Measured
with mx check 11.14 on TestApp: two fragments declaring the same
variable gave CE0111, and a fragment reading a variable a drop in the
same statement removed gave CE0109, after "Altered microflow". The
context now tracks what earlier operations declared, read and removed.
Also count a fragment loop's iterator as the fragment's own, so loop
fragments are no longer refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0010 R11. Both are new rejections, so they apply only under the
`mdl 1` header (ADR-0011): a headerless script parses as before and
check warns MDL-V1-SEMI / MDL-V1-SLASH for each occurrence.

A statement rule that consumes its own `;` (create java action … as
$$…$$;) counts as terminated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cter (#732)

ADR-0010 R11. Under mdl 0 \n, \t, \r, \\ and \' in a string literal
are escapes, which contradicts Mendix expressions and makes 'C:\temp' a
tab. It changes what text means, so it is tied to the header
(ADR-0011): a headerless script keeps the old value, and check warns
MDL-V1-ESCAPE for each literal whose value would change.

The rule decides where a literal ends ('C:\' is complete under mdl 1,
unterminated under mdl 0), so it is fixed before lexing:
langver.ScanHeader reads the header from the source, and an mdl 1
script is lexed from a StrictEscapeStream, which a lexer predicate on
STRING_LITERAL checks. Every token keeps its stream, so the visitor
reads each literal under the rule it was lexed with: the 242
unquoteString(x.GetText()) calls become unquoteStringLit(x), and a test
keeps a new call from reading token text with mdl 0's escapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The visitor needs the same suggestion for unknown property keys (#732)
and cannot import the executor. No change in behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0010 R11. The REST client (service, basic auth and operation
lists), published REST service, business event service, and the agent
editor's model, knowledge base, consumed MCP service, agent and agent
body blocks accepted any `Key: value` and read only the keys they knew,
so `pathh: '/users'` parsed, checked and executed with the path
missing. A value was also read by its shape rather than its key:
`Response: json from $X` set the request body.

Each list now has a schema (the keys its visitor reads, the value
shapes each takes, and what describe writes). Under mdl 1 a property
outside it is an error naming the key, the list and the nearest known
key; under mdl 0 the list is read as before and check warns
MDL-V1-PROP / MDL-V1-PROPVALUE.

suggest.Closest now also tries two edits, counting a neighbour swap as
one, for names of five letters or more (Verison, Passwd), which the
OData did-you-mean gets too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dl 1

`first` is Mendix's "First object" range in every language version. Under
the mdl 1 header a bare `limit 1` is a Custom range, a list of one; without
it the alpha meaning (the object) is kept and warns MDL-V1-LIMIT1. The
visitor resolves the meaning into RetrieveStmt.First, so the writer, the
variable typing and MDL-RETRIEVE01 no longer read limit text. describe
prints the object range as `first`. `first` on an association retrieve
is refused (that source has no range).

MDL-RETRIEVE01 keys on the object range and names CE0100 for a loop, as
measured with mx check 11.13.

Tests: visitor per version, builder range per version, check per version,
describe spelling, and a PedApp round trip of both forms with a headerless
control.

Part of #734.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nder mdl 1

CLAUDE.md idiom 5, the quick reference, the syntax topic, the skills and
docs-site pages that taught `limit 1` for an object now write `first`
and say what `limit 1` means per language version. CHANGELOG entry.

Part of #734.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The microflow, nanoflow and workflow rules end in `SEMICOLON? SLASH?`
themselves, so `end;` followed by `/` left the statement's own SLASH
empty: the `/` was not reported, and the missing `;` was reported at
the `/`. Found by checking PedApp's describe output under mdl 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Statement termination no longer recommends the / terminator, and the
string-literal section no longer claims backslash escapes are
unsupported: they are, until mdl 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…732)

Under mdl 1 'it\'s' ends at the quote, and the errors that follow
point at the rest of the line. Name the cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed constructs

Upgrade rewrites every use the visitor records on Program.Deprecations to its
registry entry's canonical spelling, in place and in the token's letter case,
leaving comments and layout untouched. An entry without a rewrite is reported,
not guessed at. With AddHeader it also adds mdl 1; after rewriting each
header-gated construct through gatedRewriters; a construct with no rewrite
blocks the header rather than change the script's meaning.

The output is re-parsed: it must parse, keep every statement and record no
rewritable deprecation, so a second run is a no-op.

Part of #735.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--upgrade applies only the upgrade rewrites and not the heuristic keyword
formatter, which changes what 374 of the 701 parseable mdl-examples scripts
build (it upper-cases identifiers such as M.User). --header adds mdl 1; and is
opt-in while mdl 1 is a preview; DefaultOptions turns it on once
langver.Frozen reaches it.

Part of #735.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ako and others added 2 commits September 27, 2026 09:49
Every mdl-examples script and its upgrade (with the header) run on two copies
of the PedApp fixture; the models are compared unit by unit as canonical BSON,
with units matched by their place in the project tree and freshly minted
identities (GUID, StableId, string and embedded UUIDs) masked only when the
fixture does not contain them, so a re-minted Studio Pro identity still
registers. Controls: the same script twice is equal, a changed caption and a
meaning-changing translations rewrite are not, and a re-minted fixture GUID is
visible.

587 scripts execute on PedApp and upgrade to the same model; 114 whose
original does not execute cleanly are listed as out of scope.

Closes #735.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The execute-both property test ran all 701 parseable mdl-examples scripts
on two PedApp copies, about 15 minutes locally. On CI it pushed the
Integration tests step past its 30-minute timeout (PR #742, run
36310586491), where main's roundtrip package takes 85s.

478 of those scripts only gain the header: both runs execute the same
statements, so they test langver's gating rather than any rewrite. They
are now listed and skipped unless MXCLI_UPGRADE_ALL=1. The 223 rewritten
scripts still execute (202 same model, 21 out of scope), in about 5
minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ako

ako commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Independent review: CI's Integration tests step timed out at 30 min (run 36310586491) because the execute-both test ran the whole corpus (~15 min locally; main's roundtrip package takes 85s on CI). Pushed f35637a: by default only the 223 scripts the upgrade rewrites beyond the header execute (202 same model, 21 out of scope, ~5 min locally); the 478 header-only scripts are listed and run with MXCLI_UPGRADE_ALL=1. Verified locally: unit tests, full roundtrip integration suite, make lint, and five revert checks on mdl/upgrade (case matching, re-parse check, gated-rewrite refusal, no-rewrite reporting, header already present) each fail the named tests.

🤖 Generated with Claude Code

ako and others added 9 commits September 27, 2026 11:15
…nt/service (ADR-0012)

Their create or modify rewrites do not yet carry what describe cannot
print (review of #738; details in #743), so re-running describe output on
Studio Pro content would silently lose it. A plain create refuses instead.
Tests pin the verb until #743 proves each carry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-0010 R11/R12: describe output must mean the same under mdl 0 and
mdl 1, and mdl 1 makes `;` the only terminator. About twenty document
types printed a SQL*Plus `/` line after their statement, and pages,
snippets, layouts and some OData statements ended without `;`.

- Drop every `/` line from describe (agents, knowledge bases, MCP
  services, models, constants, contracts, associations, DB connections,
  entities, enumerations, image collections, modules, OData, published
  REST, security, microflows, nanoflows, rules).
- Pages, snippets and layouts end with `};`; a workflow with
  `end workflow;`.
- A published OData service ends with `;` after its property list,
  authentication clause or entity block, whichever is last; an external
  entity without attributes after its property list.
- A published REST service with no resources prints an empty `{ };`
  block: the block is mandatory in the grammar, so the bare `;` it
  printed before did not parse.

Tests: TestPedAppDescribeIsValidMdl1 checks the terminators of every
PedApp document type (plus each module, with and without `with all`,
navigation and settings) on the parse tree and re-parses the output under
an `mdl 1;` header; executor tests cover the types PedApp does not
contain, and every Describe*_Mock / #707 re-parse test now asserts the
same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`diff` and `diff-local` render MDL for the compared side; with describe
no longer printing `/`, keeping it here would show a spurious line on
every statement and render text that is invalid under mdl 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	mdl/executor/cmd_agenteditor_mock_test.go
#	mdl/executor/cmd_security_mock_test.go
… (valid under mdl 0 and mdl 1, required by #741)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to HEAD

# Conflicts:
#	docs-site/src/language/basics.md
…forms

On top of #740/#745/#741/#738, `fmt --upgrade --header` refused almost every
example script: those PRs gated new constructs on the header without a
rewrite. Each construct with a mechanical, meaning-preserving rewrite now has
one, computed from the parse tree by the visitor that records it (ast.Fix,
rune-offset edits, mdl/visitor/visitor_upgrade_fixes.go):

- MDL-V1-SEMI: add the missing `;`. MDL-V1-SLASH: delete the `/` line.
- MDL-V1-ESCAPE: write the literal's mdl 0 value with '' as the only escape;
  no edit inside an expression stored as written (mdl 0 already passed the
  backslash through); an escaped line break in a re-rendered expression is
  reported, since writing it into the literal changes what the builder
  stores (measured: a log message becomes a `{1}` template parameter).
- MDL-V1-LIMIT1: `limit 1` -> `first`. MDL-V1-SET: add `set`.
- MDL-DEPR003/004 and MDL-V1-LIST: call form -> statement form; find and
  contains on a declared String keep the call and gain `set`; a nested call,
  or an operand whose type the script does not state, is reported.
- MDL-V1-REPLACE02: `create or replace user role|demo user` -> `create`.

MDL-V1-PROP, MDL-V1-PROPVALUE and MDL-V1-REPLACE01 have no mechanical
rewrite; they are listed in `unrewritable` (may only shrink) and block the
header with HeaderBlockedError, which names each construct and why.
TestGatedRegistryIsComplete holds every visitor.LanguageChanges() code to
exactly one of the two lists.

The examples test lists the two corpus scripts that keep mdl 0
(keepsItsVersion) and the two whose AST differs but whose model is the same
(buildsTheSameModelNotTheSameAST), both may only shrink. The execute-both
property test upgrades a blocked script without the header, and by default
skips scripts whose only edits are the header and terminators, which never
reach the AST: 249 scripts execute (6 min); MXCLI_UPGRADE_ALL=1 runs all 701,
587 of them executing to the same model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

mxcli fmt --upgrade: rewrite deprecated aliases and header-gated constructs, proven by an execute-both property test

1 participant