feat: @anchor annotation for microflow sequence flow endpoints#276
Conversation
f58e6c2 to
732913b
Compare
AI Code ReviewCritical IssuesNone found. Moderate IssuesNone found. Minor Issues
What Looks Good
RecommendationApprove. The PR successfully implements the Automated review via OpenRouter (Nemotron Super 120B) — workflow source |
) Addresses the two minor comments from the mendixlabs#276 AI review and tidies the two fields the original PR reserved "for a follow-up on LOOP/WHILE internal flows": - Grammar: accept @anchor(iterator: (...), tail: (...)) on LOOP/WHILE statements via a new annotation param name (TAIL). Parser + visitor route the sides into the existing IteratorAnchor / BodyTailAnchor fields on ActivityAnnotations, so forward-compatible scripts parse cleanly today. - Builder: deliberately does not serialise iterator/tail edges. Studio Pro rejects loop→body and body→loop SequenceFlows with CE0709 "Sequence flow is not accepted by origin or destination" because the iterator icon is drawn implicitly from the LoopedActivity geometry. The annotation parses, but its payload is discarded at build time. A comment at the call sites explains why. - Describer: emitAnchorAnnotation now has a LoopedActivity arm (emitLoopAnchorAnnotation) that emits the combined form @anchor(from, to, iterator, tail) when iterator/tail flows exist. On current Mendix projects those flows never exist, so the output is unchanged for real roundtrips — but the code is ready if a future Mendix version starts allowing those edges. - Minor review fix: the misplaced "// @anchor (emit whenever attached flows exist, for roundtrip fidelity)" comment in emitObjectAnnotations now sits directly above the call it describes. - Tests: seven new unit tests pin the behaviour — iterator/tail are accepted on LOOP and WHILE without emitting invalid flows, and the describer's loop emitter renders the combined form correctly when synthetic iterator/tail flows are provided. `mxcli docker check` still reports 0 errors on a fresh 11.9 project after exec'ing the updated bug-test script. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Follow-up commit 3bb8126 addresses both minor notes from the AI review:
Verified against a fresh Mendix 11.9 project: |
Code Review — feat: @anchor annotation for microflow sequence flow endpointsOverviewSolid, well-motivated feature. The E2E coverage (5 real-world microflows, 0 anchor drifts, Issues1. Package-level global state for flow maps (design concern) var (
currentFlowsByOrigin map[model.ID][]*microflows.SequenceFlow
currentFlowsByDest map[model.ID][]*microflows.SequenceFlow
)The comment says "Not ideal, but keeps the blast radius confined." This is honest, but the two globals are a data race hazard: 2. Duplicate assertion in // Lines 368–375 — identical predicate, different error messages
if !hasFlow(oc.Flows, AnchorBottom, AnchorTop) {
t.Errorf("expected split→log flow with Bottom→Top...")
}
if !hasFlow(oc.Flows, AnchorBottom, AnchorTop) {
t.Errorf("expected log→return flow with Bottom→Top inside else branch...")
}Both checks test the same condition. The second check would need 3. Dead code in if ec, ok := f.CaseValue.(ast.Expression); ok {
_ = ec
}
switch cv := f.CaseValue.(type) {
case nil:
// skip
default:
_ = cv
}These twelve lines do nothing and should be removed. The actual verification proceeds via the 4. LSP completion labels are misleading {Label: "TOP", Kind: ..., Detail: "Query keyword"},
{Label: "BOTTOM", Kind: ..., Detail: "Query keyword"},
{Label: "ANCHOR", Kind: ..., Detail: "Query keyword"},
5. In the inner default path of the backslash handler: b.WriteByte('\\')
continueThe Minor observations
SummaryThe core implementation is correct and well-tested. Two actionable items before merging:
The dead test code and LSP label mismatch are minor, easy to fix before merge. |
Addresses the substantive and minor notes from both reviews on PR mendixlabs#276. ako review: - **[substantive] Remove package-level currentFlowsByOrigin/currentFlowsByDest globals**. These were a data race under captureDescribeParallel, which spawns concurrent describe goroutines. flowsByOrigin/flowsByDest are now explicit parameters on traverseFlow / traverseFlowUntilMerge / traverseLoopBody / emitLoopBody / emitActivityStatement / emitObjectAnnotations. The legacy Executor.traverseFlow wrapper passes nil for flowsByDest (preserves prior @anchor-suppressed behaviour for pre-existing callers and tests). Added TestFormatMicroflowActivities_ Concurrent_NoRace: 24 goroutines run formatMicroflowActivities in parallel on two distinct microflows and each worker's output is checked for correct per-flow @anchor content. Passes under -race. - **Duplicate assertion in TestBuilder_AnchorInsideElseBranch**. The second hasFlow(AnchorBottom, AnchorTop) duplicated the first and trivially passed once any match existed. Replaced with countFlows() to assert two distinct Bottom→Top flows (split→log and log→return). - **Dead code in TestBuilder_IfBranchAnchorOverrides**. The 12-line no-op type-assert block always fell through to a GetValue-based interface assertion that fails (EnumerationCase has no GetValue method). Replaced the whole post-build inspection with findBranchFlows — the describer's own helper that handles every CaseValue variant. - **LSP completion labels "Query keyword"** on TOP/BOTTOM/ANCHOR. Moved the anchor tokens out of the OQL/QUERY section into a new "ANCHOR ANNOTATION KEYWORDS" section in MDLLexer.g4 and taught gen-completions to map it to "Flow annotation keyword". - **quoteExpressionLiteral doc comment clarity**. Reworded the note about backslash followers that are not recognised escape letters: the backslash is emitted as-is and the follower is processed by the next loop iteration via the default arm — byte-identical to passthrough but walked separately. codex review: - **Parser/visitor regression tests for @anchor(iterator: ..., tail: ...)**. visitor_anchor_test.go gains three tests that feed real MDL text through Build() and assert IteratorAnchor / BodyTailAnchor fields: loop with both iterator+tail, while with both, loop with iterator-only (tail stays nil). The existing executor-side tests only used synthetic AST values. - **Clarify bug-test file wording**. The usage note said describe output "must include the @anchor(...) lines below verbatim" but the LOOP/WHILE section immediately below says those are parse-only. Restructured into two labeled sections: Section A = roundtrip-preserving (flat @anchor + IF split), Section B = parse-only forward-compatibility (LOOP/WHILE iterator/tail). Usage text now describes each section's guarantee explicitly. Validation: - go test ./... passes. - go test -race ./mdl/executor/ passes (including the new concurrent test). - go build -tags integration ./... builds clean. - Go lint passes. - On a fresh Mendix 11.9 project: mxcli exec of the updated bug-test script followed by mxcli docker check reports 0 errors. - describe → exec → describe is bit-exact for Section A microflows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@ako thanks for the thorough review. Pushed 0279dde addressing every item: 1. Package-level flow-map globals (data race hazard) — removed. 2. Duplicate assertion in 3. Dead code in 4. LSP completion labels — fixed. Moved 5. Also picked up the codex review:
Validation: |
Studio Pro renders each SequenceFlow against one of four sides of the
origin and destination activity boxes (top, right, bottom, left). Until
now the builder always derived the anchor side from the flow's visual
direction and the describer dropped the information entirely, so a
DESCRIBE → re-execute round-trip lost every manual side-tweak. Agent-
generated microflow patches therefore reshuffled the arrow layout in
the Studio Pro diagram even when they were semantically identical to
the original — the flow diagram had to be re-tidied by hand after each
iteration.
New optional annotation on microflow statements:
@anchor(from: right, to: left)
log info node 'App' 'hello';
Each side is independently optional. Missing sides fall back to the
builder's default for the visual flow direction — existing MDL scripts
are unchanged.
IF statements support a combined form for the incoming flow plus the
per-branch outgoing flows:
@anchor(to: top, true: (from: right, to: left), false: (from: bottom, to: top))
if condition then
...
else
...
end if;
top (0), right (1), bottom (2), left (3) — matching the indices Mendix
stores in OriginConnectionIndex / DestinationConnectionIndex on the
SequenceFlow BSON.
@anchor is emitted for every activity whenever flows are attached, so
describe → exec → describe is bit-exact on the anchor indices. For
splits the combined form is used when any of the three groups (incoming
to, true-branch, false-branch) has a non-default value.
- AST: ast.AnchorSide, ast.FlowAnchors; ActivityAnnotations gains
Anchor / TrueBranchAnchor / FalseBranchAnchor / IteratorAnchor /
BodyTailAnchor (all optional, AST-only for iterator/tail).
- Grammar: ANCHOR / TOP / BOTTOM lexer tokens; `@anchor(key: value)`
parser rule with a nested-params form for the split-branch shape.
- Visitor: populates anchor fields from annotation contexts.
- Builder: applyUserAnchors helper; mergeStatementAnnotations carries
anchor fields into pendingAnnotations; buildFlowGraph and
addIfStatement apply anchor indices per the user spec. Guard-pattern
IFs (no else, then returns) use the new nextFlowAnchor plumbing so
the deferred split→nextActivity flow also honours
@anchor(false: ...).
- Describer: emitAnchorAnnotation outputs `@anchor(from: X, to: Y)`
for non-split activities and a split-form variant for ExclusiveSplit
/ InheritanceSplit. Branch detection reuses findBranchFlows so all
CaseValue variants (ExpressionCase, EnumerationCase, BooleanCase,
value + pointer) are covered.
Tightening @anchor roundtrips exposed two pre-existing bugs in
expressionToString used for `if cond then`, regex literals, etc.:
- Using mdlQuote doubled every backslash, breaking regex escape
sequences like \d that the Mendix expression engine consumes
literally.
- Using only apostrophe-doubling emitted raw control characters (0x0A,
0x0D, 0x09) inside single-quoted literals, which STRING_LITERAL
rejects.
Fix: new quoteExpressionLiteral that escapes control chars and
backslashes followed by an MDL-significant letter (n/r/t/\/'), but
passes other backslash sequences through verbatim. Trailing backslash
at EOF is doubled so the lexer doesn't consume the closing quote as an
escape pair.
Loop describe output previously omitted the `begin` keyword the MDL
grammar requires after `loop $X in $Y`. It parsed by accident
because every body statement's leading keyword was accepted after the
header. With @anchor landing before the first body statement, the
parser started seeing `@position(...)` immediately after the loop
header and failed. Fix: emit `begin` unconditionally between the
loop header and body in every LoopedActivity call-site.
- visitor_anchor_test.go: syntax simple / partial / 4 sides / split
form / absence → nil.
- cmd_microflows_builder_anchor_test.go: override applied; default
kept when omitted; partial override preserves other side;
per-branch IF anchors.
- cmd_microflows_describe_anchor_test.go: @anchor emission;
no-flows case; roundtrip via builder.
- cmd_microflows_split_incoming_anchor_test.go: split incoming to;
EnumerationCase, ExpressionCase, BooleanCase branch forms.
- cmd_microflows_expr_literal_escape_test.go: regex passthrough; raw
control char escaping; apostrophe doubling; backslash-before-escape
letter doubling; trailing backslash doubling; idempotency.
- cmd_microflows_anchor_if_test.go: anchor preservation inside ELSE
branch (real mendixlabs#35 pattern); anchor(to: top) on return inside else.
- cmd_microflows_annotation_escape_test.go: mdlQuote control-char
escaping; apostrophe doubling.
- cmd_microflows_guard_pattern_test.go: guard-pattern IF carries
false-branch anchor through the deferred flow.
- cmd_microflows_loop_begin_test.go: loop describe output opens with
`begin`.
Tested against the Control Centre project (Mendix 9.24, ~200
microflows). On 5 representative cases including the attempt-mendixlabs#35 repro
(Apps.GetOrCreateMendixVersionFromString, MxAdmin.CreateMendixVersion
FromString, AcademyIntegration.GetOrCreateCertificate), the describe →
exec → describe round-trip preserves every anchor (0 drifts) and the
output parses through `mxcli check`.
CHANGELOG updated under [Unreleased] → Added.
) Addresses the two minor comments from the mendixlabs#276 AI review and tidies the two fields the original PR reserved "for a follow-up on LOOP/WHILE internal flows": - Grammar: accept @anchor(iterator: (...), tail: (...)) on LOOP/WHILE statements via a new annotation param name (TAIL). Parser + visitor route the sides into the existing IteratorAnchor / BodyTailAnchor fields on ActivityAnnotations, so forward-compatible scripts parse cleanly today. - Builder: deliberately does not serialise iterator/tail edges. Studio Pro rejects loop→body and body→loop SequenceFlows with CE0709 "Sequence flow is not accepted by origin or destination" because the iterator icon is drawn implicitly from the LoopedActivity geometry. The annotation parses, but its payload is discarded at build time. A comment at the call sites explains why. - Describer: emitAnchorAnnotation now has a LoopedActivity arm (emitLoopAnchorAnnotation) that emits the combined form @anchor(from, to, iterator, tail) when iterator/tail flows exist. On current Mendix projects those flows never exist, so the output is unchanged for real roundtrips — but the code is ready if a future Mendix version starts allowing those edges. - Minor review fix: the misplaced "// @anchor (emit whenever attached flows exist, for roundtrip fidelity)" comment in emitObjectAnnotations now sits directly above the call it describes. - Tests: seven new unit tests pin the behaviour — iterator/tail are accepted on LOOP and WHILE without emitting invalid flows, and the describer's loop emitter renders the combined form correctly when synthetic iterator/tail flows are provided. `mxcli docker check` still reports 0 errors on a fresh 11.9 project after exec'ing the updated bug-test script. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses the substantive and minor notes from both reviews on PR mendixlabs#276. ako review: - **[substantive] Remove package-level currentFlowsByOrigin/currentFlowsByDest globals**. These were a data race under captureDescribeParallel, which spawns concurrent describe goroutines. flowsByOrigin/flowsByDest are now explicit parameters on traverseFlow / traverseFlowUntilMerge / traverseLoopBody / emitLoopBody / emitActivityStatement / emitObjectAnnotations. The legacy Executor.traverseFlow wrapper passes nil for flowsByDest (preserves prior @anchor-suppressed behaviour for pre-existing callers and tests). Added TestFormatMicroflowActivities_ Concurrent_NoRace: 24 goroutines run formatMicroflowActivities in parallel on two distinct microflows and each worker's output is checked for correct per-flow @anchor content. Passes under -race. - **Duplicate assertion in TestBuilder_AnchorInsideElseBranch**. The second hasFlow(AnchorBottom, AnchorTop) duplicated the first and trivially passed once any match existed. Replaced with countFlows() to assert two distinct Bottom→Top flows (split→log and log→return). - **Dead code in TestBuilder_IfBranchAnchorOverrides**. The 12-line no-op type-assert block always fell through to a GetValue-based interface assertion that fails (EnumerationCase has no GetValue method). Replaced the whole post-build inspection with findBranchFlows — the describer's own helper that handles every CaseValue variant. - **LSP completion labels "Query keyword"** on TOP/BOTTOM/ANCHOR. Moved the anchor tokens out of the OQL/QUERY section into a new "ANCHOR ANNOTATION KEYWORDS" section in MDLLexer.g4 and taught gen-completions to map it to "Flow annotation keyword". - **quoteExpressionLiteral doc comment clarity**. Reworded the note about backslash followers that are not recognised escape letters: the backslash is emitted as-is and the follower is processed by the next loop iteration via the default arm — byte-identical to passthrough but walked separately. codex review: - **Parser/visitor regression tests for @anchor(iterator: ..., tail: ...)**. visitor_anchor_test.go gains three tests that feed real MDL text through Build() and assert IteratorAnchor / BodyTailAnchor fields: loop with both iterator+tail, while with both, loop with iterator-only (tail stays nil). The existing executor-side tests only used synthetic AST values. - **Clarify bug-test file wording**. The usage note said describe output "must include the @anchor(...) lines below verbatim" but the LOOP/WHILE section immediately below says those are parse-only. Restructured into two labeled sections: Section A = roundtrip-preserving (flat @anchor + IF split), Section B = parse-only forward-compatibility (LOOP/WHILE iterator/tail). Usage text now describes each section's guarantee explicitly. Validation: - go test ./... passes. - go test -race ./mdl/executor/ passes (including the new concurrent test). - go build -tags integration ./... builds clean. - Go lint passes. - On a fresh Mendix 11.9 project: mxcli exec of the updated bug-test script followed by mxcli docker check reports 0 errors. - describe → exec → describe is bit-exact for Section A microflows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0279dde to
5fb2719
Compare
Summary
Implements the
@anchor(from: X, to: Y)annotation proposed in #275 so thatDESCRIBE MICROFLOWoutput andmxcli execroundtrips preserve each SequenceFlow's connection side — previously dropped on describe and always re-derived from the visual direction on exec.Syntax
Simple form — for non-split statements:
Each side is independently optional; missing sides fall back to the builder default so existing scripts are unchanged.
Combined form — for IF, carries the incoming side plus per-branch outgoing sides:
Values:
top(0),right(1),bottom(2),left(3) — matching the BSONOriginConnectionIndex/DestinationConnectionIndex.Ancillary fixes
Enforcing the roundtrip invariant exposed three pre-existing bugs in the describer that would have blocked the annotation from surviving
describe → check:isMatch($x, '^\d+$')round-tripped as'^\\d+$', then'^\\\\d+$', etc. NewquoteExpressionLiteralescapes only what the lexer requires (\n,\r,\t, trailing\, backslash followed by an escape letter, and apostrophe) and passes regex-escape sequences through verbatim.begin. Grammar requiresloop $X in $Y begin ... end loop;but the describer wrote the header directly before the body. It happened to parse before because every body statement's leading keyword was accepted. With@anchorlanding before the first body statement, the parser saw@position(...)right after the loop header and failed with cascade errors.@annotation/@captionemitted raw control characters. Free annotations used a home-grown escape that only doubled apostrophes; multiline@annotationtext (as inMxAdmin.CreateMendixVersionFromString) was then rejected by the parser on re-execute. Switched tomdlQuote.Implementation highlights
ast.AnchorSide,ast.FlowAnchors;ActivityAnnotationsgainsAnchor,TrueBranchAnchor,FalseBranchAnchor(plusIteratorAnchor/BodyTailAnchorreserved for a follow-up on LOOP/WHILE internal flows).ANCHOR,TOP,BOTTOMlexer tokens;anchorSideproduction + nestedannotationParenValuefor the split form.applyUserAnchorshelper;mergeStatementAnnotationscarries anchor fields intopendingAnnotations;buildFlowGraphandaddIfStatementapply them per the user's spec. Guard-pattern IFs (no else, then returns) use newnextFlowAnchorplumbing so the deferredsplit→nextActivityflow also honours@anchor(false: ...).emitAnchorAnnotationoutputs the simple form for non-split activities; split activities delegate toemitSplitAnchorAnnotation. Branch detection reusesfindBranchFlowsso allCaseValuevariants (ExpressionCase,EnumerationCase,BooleanCase, value + pointer) are covered.Test coverage
CaseValuevariant, no-flows skip, expression literal escape, loop begin emission).cmd_microflows_anchor_if_test.gopins the attempt-Fix batch of reported issues (#18, #19, #23, #25, #26, #27, #28) #35 scenario (anchor inside ELSE branch + return withto: top).All tests run with
go test ./...green (29 packages, 0 failures).E2E verification
Control Centre (Mendix 9.24, ~200 microflows). 5 representative microflows including the attempt-#35 repro:
Apps.GetOrCreateMendixVersionFromString— 0 anchor driftsRepositoryServiceIntegration.GetDisplayVersion— 0 driftsMxAdmin.CreateMendixVersionFromString— 0 drifts; also validates multiline@annotationwith''and\r\nsurvives roundtripAcademyIntegration.GetOrCreateCertificate— guard-pattern IF, 0 driftsAdministration.ChangePassword— 0 driftsAll 5 pass
mxcli checkon the describe output AND round-trip describe → exec → describe with byte-exact anchor preservation.Closes
#275