Skip to content

Bind GED worker for generated diagram plugins (partial fix for #7) - #27

Merged
nk-coding merged 2 commits into
mde-optimiser:mainfrom
amirrza777:amir/fix-edge-reroute
Jul 27, 2026
Merged

Bind GED worker for generated diagram plugins (partial fix for #7)#27
nk-coding merged 2 commits into
mde-optimiser:mainfrom
amirrza777:amir/fix-edge-reroute

Conversation

@amirrza777

@amirrza777 amirrza777 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Relates to #7

What this fixes

Traced the reported "reroute changes reset immediately" behavior to the metadata manager's graph edit distance (GED) matching, shared by every diagram type. When a textual edit changes an element's ID (even one not conceptually related to the edit), the next validation pass can no longer match old and new metadata by ID, and falls back to a naive ID-equality match. Anything that doesn't match gets reset to default layout, including routing points.

Found one confirmed, concrete gap in this: generatedModelPlugin.ts and generatedModelTransformationPlugin.ts (the newer .m_gen/.mt_gen pipeline) construct their diagram module with only the services argument, dropping the languageJsUrl that every other language plugin already receives and forwards. Without it, GedWorkerBaseUrl never gets bound, so GED always returns undefined regardless of how small the mismatch is. That means the generated pipeline has zero chance of correct old-to-new remapping, not just a reduced chance on large diffs.

Fixed by converting both plugin definitions into functions that accept and forward languageJsUrl, mirroring the existing pattern in the classic model-transformation, classic model, and metamodel plugins. The browser-side worker that drives live diagram editing already passes languageJsUrl uniformly to every plugin's create() call, so this takes effect without further changes.

What this does not fix

The underlying source of ID churn is unchanged: model-transformation statement IDs are derived from their position/index within their containing block (ModelTransformationModelIdProvider.getHierarchicalStatementName), so inserting or reordering one statement can shift the IDs of everything after it, cascading into new IDs for patterns, pattern links, and control-flow edges that didn't conceptually change. On the classic pipeline this can still exceed GED's node-difference threshold on large diagrams and fall back to the same naive matching. Making statement identity stable across edits would be the real fix, but that's a bigger, riskier change to the ID generation scheme and felt out of scope here.

Verification

Reproduced end to end: created a minimal .mt_gen file with a match pattern (p : Person, c : Company, linked), rerouted the link edge to give it a custom bend, then edited the underlying content to insert a new empty match statement before it, exactly the kind of edit that shifts the pattern's IDs. Before this fix the reroute reset to a straight line on the next render; after it, the custom route was preserved.

Also included a small unrelated fix: bumped the workbench Docker build's Node heap limit, since a fresh build now OOMs given how much the bundle has grown.

…rams

Both generatedModelPlugin.ts and generatedModelTransformationPlugin.ts
constructed their diagram module with only the services argument,
dropping the languageJsUrl that every other language plugin (classic
model, classic model-transformation, metamodel) already receives and
forwards. Without it, BaseDiagramModule never binds GedWorkerBaseUrl,
so MetadataManager.computeGED() always returns undefined regardless of
how small the metadata mismatch is, forcing every edit that changes an
element's ID onto the naive ID-equality fallback, which drops routing
points, positions, and other layout data for anything that doesn't
match by identical ID.

Converts both plugin definitions into functions that accept
languageJsUrl and pass it through, mirroring the existing pattern in
plugin.ts (model-transformation), modelPlugin.ts, and the metamodel
plugin. The browser-side worker that actually drives live diagram
editing already passes languageJsUrl uniformly to every plugin's
create() call, so this fix takes effect without further changes.

This directly affects issue mde-optimiser#7 (edges not reroutable): once a textual
edit shifts a node or edge's ID, the classic pipeline can still recover
via GED as long as the affected region is small; the generated
pipeline previously had no chance to recover at all. The underlying
ID-churn source (position-index-derived IDs for model-transformation
statements) is unchanged and can still defeat GED on large diagrams,
but this closes the one confirmed gap where recovery was impossible
by construction.
The workbench bundle has grown enough (generated model-transformation
support, etc.) that a fresh npm run build now hits Node's default
heap limit and crashes with SIGABRT. Matches the memory bump already
used elsewhere for similarly large builds in this repo.
@amirrza777
amirrza777 marked this pull request as ready for review July 22, 2026 19:13
@amirrza777

amirrza777 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Traced this to the metadata manager's graph edit distance (GED) matching, which every diagram type shares. When a textual edit shifts an element's ID, even one not conceptually related to the edit, the next validation pass can't match old and new metadata by ID anymore and falls back to naive ID-equality matching. Anything that doesn't match gets reset to default layout, including routing points, and since the reset gets written straight to the metadata file, reloading does not bring it back.

Found one concrete, confirmed gap: the generated pipeline (.m_gen/.mt_gen) constructs its diagram module without the languageJsUrl argument that every other language plugin already receives and forwards. Without it, the GED worker never gets bound, so GED always returns undefined no matter how small the mismatch is. The generated pipeline had zero chance of correct old-to-new remapping, not just a reduced chance on large diffs.

Fixed by threading languageJsUrl through both generated plugin definitions, matching the classic pipelines exactly.

What this does not fix: the underlying source of ID churn is still there. Model-transformation statement IDs are derived from their position in their containing block, so inserting or reordering one statement can shift the IDs of everything after it. On the classic pipeline this can still exceed GED's node-difference threshold on large diagrams and fall back to the same naive matching. Making statement identity stable across edits would be the real fix, but that's a bigger, riskier change to the ID generation scheme and felt out of scope here.

Verified end to end: created a .mt_gen file with a match pattern, rerouted the link edge to give it a custom bend, then edited the content to insert a new empty match statement before it, the kind of edit that shifts the pattern's IDs. Before this fix the reroute reset to a straight line. After, it kept the custom route.

Also included a small unrelated fix: bumped the workbench Docker build's Node heap limit, since a fresh build now OOMs given how much the bundle has grown.

For the underlying ID churn, a few questions before I take a pass at it (@nk-coding @szschaler):

  1. Would you rather have stable statement identity be explicit in the DSL (e.g. an optional id you can attach to a match/pattern statement, defaulting to the current index-derived name when omitted), or computed implicitly (e.g. a content-based hash of the statement), so hand-written files don't need to change at all?

  2. Is there a reason statement IDs are index-derived today rather than stable from the start, something I should know before touching ModelTransformationModelIdProvider?

  3. Should this extend to the generated .mt_gen/.m_gen pipeline as well, or is content there always machine-generated in a way that makes this less of a concern there?

  4. As a smaller stop-gap while a real fix is designed, is raising or removing GED_MAX_NODE_DIFFERENCE (currently 10) acceptable, or does that risk real performance problems given GED already runs in a worker with a timeout?

Thanks beforehand @nk-coding @szschaler

@nk-coding

Copy link
Copy Markdown
Collaborator

these are good catches, but do not solve what #7 was originally about
regarding your questions:

  1. no, I cannot imagine users want to define lots of manual ids just so that the layout is a bit more stable
  2. I did not find a better derived id
  3. I mean the original concern is that in some cases, something goes wrong internally resulting in a broken state where no layout changes can be made any more, if this happens there too then it is a concern there too
  4. not sure, needs evaluation, but regardless this should not be the fix

@amirrza777

Copy link
Copy Markdown
Contributor Author

@nk-coding @szschaler now that the lockup part of #7 is resolved separately (see #28), I would like to nail down the ID churn problem enough to actually start on it, so I do not need to keep coming back with more questions mid-implementation. Looked closer at ModelTransformationModelIdProvider to ground these:

getHierarchicalStatementName builds a purely positional ID: the statement's index within its containing array, chained through its parent's name as "{parentName}_{index}". This is used for every statement kind (MatchStatement, IfMatchStatement, WhileMatchStatement, UntilMatchStatement, ForMatchStatement, IfExpressionStatement, WhileExpressionStatement, StopStatement), plus WhereClause and ElseIfBranch through their own index-based methods. Pattern is not itself index-based, but it inherits its name from the containing statement, so the churn cascades into every pattern, pattern link, and pattern object instance nested under a statement whose index shifted, even though PatternLink/PatternObjectInstance names are otherwise based on stable, user-given names.

Questions, roughly in the order I would need answers to start:

  1. Scope: is stability needed across all eight statement kinds plus WhereClause/ElseIfBranch, or is there a subset that matters most in practice? I would rather fix all of them in one pass for consistency, but want to check there is not a reason to treat some differently.

  2. Collisions: a content/structural hash instead of an index would stop stability from breaking on reorder or insertion, but two structurally identical sibling statements would then hash to the same ID (for example two empty match{} blocks, which is literally what is in the test file I used for Recover metadata saves after a failed write instead of locking up forever #28's verification). How should ties be broken? My instinct is to keep index-based disambiguation only within a collision group (first-seen order among identical siblings), so unique statements never move and only true duplicates fall back to today's behavior.

  3. Given Pattern/PatternLink/PatternObjectInstance already avoid raw array indices themselves, would stabilizing just the statement-level and WhereClause/ElseIfBranch naming be enough to stop the cascade, or is there other array-index-based ID logic elsewhere (model, metamodel, config languages) with the same problem that should be addressed at the same time for consistency?

  4. Migration: changing the ID format means every existing .mt file's saved metadata, keyed by the current index-based IDs, would fail to match on first load after this ships, causing a one-time full layout reset across all existing diagrams. Is that acceptable, or does this need a fallback (accepting the old ID format as a secondary lookup key for some transition period)?

  5. Interaction with GED: does stabilizing these IDs make GED's fallback path rarely triggered for this specific kind of edit, or should GED stay exactly as is as a safety net for actual renames and other cases it already handles? I am assuming this is additive to GED, not a replacement for it.

  6. Does this apply equally to the generated pipeline (.mt_gen) now that Bind GED worker for generated diagram plugins (partial fix for #7) #27 gave it a working GED worker, or does machine-generated content there make ID churn a non-issue in practice, per your original point 3 answer?

  7. Testing: there is no test harness in this repo yet (noted in Recover metadata saves after a failed write instead of locking up forever #28 too). Would it be worth adding one now, scoped to ModelTransformationModelIdProvider.getName, with before/after edit fixtures, so future regressions here get caught automatically instead of needing a live manual repro each time?

  8. Priority: should I start on this now, or is it behind other work? And separately from whatever the real fix ends up being, is there still no appetite for bumping GED_MAX_NODE_DIFFERENCE as a temporary, independent mitigation while this is designed, or is that off the table entirely per your last answer?

Thanks beforehand.

@nk-coding

Copy link
Copy Markdown
Collaborator

without answering all the detailed questions:
why do we even want to improve that? is this really an issue that users typically face?
I don't think you change the control flow that often, and even if you typically just append stuff
regarding all the structural stuff, it is important that editing the content of a statement should not change the id as these changes are way more common
regarding priority, imho really low as this should not be the cause of #7

@amirrza777

Copy link
Copy Markdown
Contributor Author

That's a good catch on the hash idea specifically: a content hash would make a statement's own ID change the moment you edit its content, which is exactly the common case you're describing, so it would trade a rare problem (reorder/insert shifting siblings) for a worse, constant one (any content edit changing the statement's own ID). That alone is enough to drop that approach.

Given that plus your priority read, I'll leave this alone for now rather than pursue a redesign. Won't touch ModelTransformationModelIdProvider unless this actually comes up as a real user complaint later.

@szschaler

Copy link
Copy Markdown
Member

I agree, this isn't what #7 was about -- where the edges "changed on their own" without changes to the textual model. If you current implementation fixes that part, then that's what we're after. The rest is of lower priority: if I change the text and the layout changes as a result, I can live with that.

@szschaler

Copy link
Copy Markdown
Member

While this fixes a bug (and as such should be merged assuming the checks run through), I'm not sure I understand how it would fix the bug in #7. Can you explain and add a short screenshot showing that this now works?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an identified gap where the generated diagram language plugins (.m_gen / .mt_gen) were not forwarding languageJsUrl into their diagram modules, preventing GedWorkerBaseUrl from being bound and causing the metadata manager’s GED-based remapping to be unavailable (leading to layout/routing resets after textual edits). It also adjusts the workbench Docker build to avoid Node heap OOM during npm run build.

Changes:

  • Update generatedModelPluginProvider and generatedModelTransformationPluginProvider to accept languageJsUrl and pass it into their respective *DiagramModule constructors.
  • Refactor the generated plugin definitions into factory functions to mirror the existing plugin pattern used by other languages in the repo.
  • Increase Node heap size during the workbench Docker build via NODE_OPTIONS.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
infra/docker/workbench/Dockerfile Increases Node heap for the build stage to prevent OOM during npm run build.
app/packages/language-model/src/generatedModelPlugin.ts Forwards languageJsUrl into GeneratedModelDiagramModule so GedWorkerBaseUrl can be bound for GED remapping.
app/packages/language-model-transformation/src/generatedModelTransformationPlugin.ts Forwards languageJsUrl into GeneratedModelTransformationDiagramModule for the same GED worker binding/remapping behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@amirrza777

Copy link
Copy Markdown
Contributor Author

Went back to Niklas's original report word for word rather than my own paraphrase of it: "this primarily seems to happen after editing the content beforehand" — so the original report describes text edit, then reroute, then immediate reset. That's a different trigger than "edges changed with no textual change at all," which is how I'd been characterizing it. Worth flagging since those are two different bugs and I want to make sure this PR is actually being judged against the right one.

Why this fix addresses that: text edits shift the position-derived IDs of unrelated statements (that part is unavoidable given how IDs are computed today, separate topic). When the next render sees a mismatch between the old and new IDs, MetadataManager falls back to GED to figure out which old element corresponds to which new one, so custom layout (like a reroute) can carry over despite the ID change. GED only runs at all if GedWorkerBaseUrl is bound, and that requires languageJsUrl to be threaded through to the diagram module constructor. The generated pipeline's plugin definitions were never passing it, so GED always returned undefined there and every mismatch fell straight through to the naive reset path, regardless of how small the actual difference was. This PR fixes that wiring so GED gets a real chance to run.

Re-tested the literal original scenario, on the classic .mt pipeline this time (not .mt_gen), with this PR's fix in place:

Opened a .mt file with a match pattern (p : Person, c : Company, c -- p), rerouted the link to give it a custom bend.
Added a new empty match{} block before it (a text edit that shifts the position-derived IDs of everything after it, exactly what Niklas described as the precursor).
The reroute did not reset. It stayed bent.
Reloaded the page with no further edits, per Niklas's other open question ("not sure whether reloading fixes the issue"): the custom route was still there after reload too.
So for the literal scenario in the original report, this fix (plus #28's WebSocket fix) does resolve it, verified live just now on the classic pipeline, not just the generated one.

Separately, and not something either of you described, so flagging as new information rather than assuming it's related: I also tested what happens across repeated executions of the same unchanged .mt file (re-running rather than editing). Each Run creates a brand new, separately-identified result file rather than updating an existing one, so there is no persisted metadata for a subsequent run to inherit regardless of GED. Custom layout never carries over between separate runs of the same transformation. That's a real, verified behavior, but distinct from what's described in this issue, since neither of you mentioned re-running an execution. Happy to open it separately if it's worth tracking, or drop it if it's expected behavior.

@nk-coding

nk-coding commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Why this fix addresses that

No it does not. The original issue is about a rarely occuring bug in the model transformation graphical editor.
This PR only deals with the generated langauges, so it does not change anything there.
Also you cannot reproduce this issue by testing it once or twice, it is a rarely occuring bug, potentially dependent on the previous user actions, and not easy to reproduce

But this is fine, this PR is an improvement regardless and can be merged, but there is still an underlying issue to be identified and solved

@amirrza777

amirrza777 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@nk-coding Fair - not reproducing it in a couple of tries doesn't prove anything for a rare, action-dependent bug. I overreached calling it "fixed." Agreed this PR stands on its own merits, but the real #7 bug in the classic editor is still open and unidentified.

Two things that might actually help, since live testing won't:

Any memory of what you were doing right before it happened would help more than another blind attempt from me.
I could add logging to the classic editor's metadata validation path (e.g. whenever GED falls back to reset), so next time it happens there's something concrete to look at.
Let me know if either's worth doing.

@szschaler

Copy link
Copy Markdown
Member

For me, this literally happens when I edit in any of the blended editors: I create a model (using text or diagram, doesn't seem to matter which), then note that an edge is in a bad place, so move it in the diagram. It's then sometimes enough for me to move the mouse without any specific action for the edge to go back to where it was first.

@nk-coding

Copy link
Copy Markdown
Collaborator

I merge this one as this is definitely a a partial fix, but the whole fix of the core issue (as soon as we identify the root cause) should be in a separate PR

@nk-coding
nk-coding merged commit 59e1cb5 into mde-optimiser:main Jul 27, 2026
14 checks passed
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.

4 participants