Skip to content

alter microflow/nanoflow: graph splice, placement, insert/replace/drop (#736) - #739

Merged
ako merged 7 commits into
mainfrom
feature/736-alter-microflow-splice
Sep 27, 2026
Merged

ako merged 7 commits into
mainfrom
feature/736-alter-microflow-splice

Conversation

@ako

@ako ako commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #736 (plan items 4.2b–4.2e of PROPOSAL_mdl_beta_syntax_freeze.md; ADR-0012 decision 3). Part of #714.

What

alter microflow FeedbackModule.VAL_Feedback {
  insert after $IsValidEmail { log info node 'Feedback' 'Email checked'; }
};

alter microflow|nanoflow M.F { insert after|before <target> { … } replace <target> with { … } drop <target>; }

  • Splice engine (mdl/backend/mfmutator/splice.go) on the stored unit's BSON — never UpdateMicroflow:
    • appends only the fragment's objects and flows (so no stored element moves in its list);
    • rewires the one flow around the target by its pointer, connection side and control vector at the rewired end only; its $ID, case value, error-handler flag and other end are kept; a new flow carries on to the old destination on the side the old flow entered;
    • replace re-points incoming flows at the fragment and the outgoing one from it (annotation lines move to the fragment); drop joins incoming flows to the successor;
    • no $ID is ever rewritten. Save refuses a unit in which any 16-byte binary still names a removed element, or two elements share an $ID (CLAUDE.md rule 1).
  • Placement (4.2c): the fragment is centred in the gap on the flow; if the gap is too narrow, every node past the gap's midpoint moves along the flow's axis by the deficit (a sweep, so relative geometry on each side and every curve survive). A placement that would still overlap an unmoved object is refused.
  • Refusals, before anything is written: insert after a decision (which branch?), insert before an activity several flows enter, anything inside a loop body (coordinates are loop-relative — follow-up), drop/replace of a decision, end event or an activity with an error handler, a fragment that returns.
  • Hygiene (4.2d): a fragment output that clashes with a variable the flow has is an error; a fragment reading a variable not declared upstream of the insertion point is an error; dropping/replacing an activity whose output is still read is an error.
  • Grammar/executor: targets are the alter microflow: content-addressed target resolver (output variable, caption, statement pattern) #713 content addresses ($Var, 'Caption', statement pattern, @n), all resolved against the stored flow before any operation runs. Fragments are built by the create microflow builder seeded with the flow's variables, then cut from their start/end events (several paths into the end get a merge).
  • Write path: Writer.UpdateRawUnitPatch → canon.Reconcile with a new canon.ContentsOwnElementIDs(). Without it, TransplantIDs re-pairs the flows after a dropped one onto their neighbours' $IDs (measured on VAL_Feedback, see test plan). Elision and the storage-GUID guard still run.
  • MCP backend (4.2f, insert only): the same splice on the stored flow, sent as ped_update_document path ops in one update, after checking that the live document still matches the .mpr (PED addresses by index). drop/replace over --mcp are refused: measured live, PED does not roll back removals when an update fails (a failed update left an activity and its flows removed).
  • describe … with handles: the error-handler block's { is no longer part of a handle (it could not be written as a target).

Test plan (what I ran)

  • make build; make lint (Go + TS) pass.
  • go test ./mdl/visitor/ ./mdl/grammar/... ./mdl/backend/... ./modelsdk/canon/ ./modelsdk/mpr/ ./cmd/mxcli/... ./mdl/ast/ ./mdl/executor/ pass.
  • go test -tags integration ./mdl/roundtrip/ pass (no allowlist change; this adds a statement, it does not change describe/exec of existing ones).
  • Acceptance on the Studio Pro-authored PedApp VAL_Feedback (TestAlterMicroflow_PedApp_InsertAfterChangesOnlyTheSplice, 3-line script): every stored object keeps its $ID and list position; exactly one new object (a log activity); objects that changed differ only in RelativeMiddlePoint, all past the insertion point, only along x; exactly one new flow (log → 'Email is Valid?', entering on the old side); exactly one changed flow (out of $IsValidEmail), changed only in DestinationPointer, DestinationConnectionIndex, Line.DestinationControlVector; microflow and collection properties byte-identical; the new activity overlaps nothing; describe shows it in place.
  • Control: alter microflow … { }; leaves the unit byte-identical (TestAlterMicroflow_PedApp_EmptyAlterChangesNothing; TestSplice_UntouchedUnitRoundTripsExactly for the decode/encode round trip).
  • Drop, replace (two-activity fragment), nanoflow insert-before, and 9 refusals on PedApp, each asserting a refused alter leaves the unit unchanged; synthetic tests for error-handler flows, vertical flows, loops, joins, and the dangling-reference guard (with its control).
  • Revert checks (each made the named test fail, then restored):
    • skip TransplantIDs bypass → TestReconcile_PatchOwnsElementIDs fails, and TestAlterMicroflow_PedApp_Drop fails with 8 flows showing neighbours' pointers under their $IDs;
    • remove the shift → acceptance test fails (overlap refusal on 'Email is Valid?');
    • rewire the wrong end of the flow → acceptance test fails;
    • disable the integrity guard → TestSplice_DropRefusesADanglingReference fails;
    • disable the MCP live-match check → TestFlowPatchOps_StaleLiveDocumentIsRefused fails;
    • handle fix: TestPrintedStatement_ErrorHandlerBlockOpenerIsNotPartOfTheStatement failed before the change.
  • mx check (scripts/mx-check.sh --version 11.13.0) on PedApp: baseline 1078 errors (theme/widgets stripped from the fixture); after the acceptance insert, after an if/else fragment insert-before, and after replace + drop + insert in one statement: the identical sorted error list each time; nothing about the microflow.
  • Studio Pro (MCP tunnel, TestApp 11.14): FeedbackModule is a marketplace module and PED refuses writes to it ("Module 'FeedbackModule' is not writeable"), so the live check ran on Studio Pro-authored flows in writable modules: alter microflow Microflows.MicroflowReduce { insert after $CarList2 { log … } } and alter microflow Services.SaveOrder { insert after commit $Order on error { log … } } (an activity with a custom error handler) via --mcp: ped_check_errors → "No errors found." for both; read back, the rewired flow kept its origin curve and the error-handler flow was untouched. A second alter on the now-stale .mpr was refused by the live-match check; drop over --mcp refused. I restored both microflows afterwards (unsaved in Studio Pro; SaveOrder's log activity and its two flows were recreated during an earlier failed two-phase attempt, so their in-memory element IDs differ from the saved file — same content). A probe microflow MyFirstModule.Zz736_SpliceProbe is left in the unsaved Studio Pro model (no delete tool without Concord).

Design choices the ADRs did not settle

  • Targets resolve against the stored flow before any operation; an operation cannot address what an earlier one in the same statement inserted.
  • "Midpoint" placement: centred on the gap along the flow's dominant axis (from the centres), aligned across it with the midpoint of the two ends; the room is made by a sweep past the gap's midpoint rather than by walking reachable nodes (safer for merges/back-edges, never overlaps what moved).
  • New elements are appended to their lists; removed ones leave the list. The patch write bypasses TransplantIDs via a new explicit option rather than relying on it to be a no-op.
  • MCP supports insert only; the diff-to-path-ops code emits removals but SaveUnit refuses to send them.
  • No mdl 1 gating: this is new syntax, not a change of meaning (ADR-0011).

Not in this PR (follow-ups)

🤖 Generated with Claude Code

ako and others added 7 commits September 27, 2026 08:49
…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>
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>
@ako

ako commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Independent review: pushed 534a595 (drop/replace of a Studio Pro loop with 2+ body activities was refused by the dangling guard because body flows live in the unit's Flows list; now removed with the loop) and 7abe76b (scope checks spanned only one operation: two fragments declaring the same var gave CE0111, and insert-reader + drop-producer in one statement gave CE0109, both after "Altered microflow"; loop fragments were refused over their own iterator). Tests written first, each fix revert-checked. mx check (11.13 PedApp / 11.14 TestApp copies) matches baseline for: insert before an error-handler path's first activity, drop in an error-handler path, inserts around a rejoin merge, insert before a nested decision, if/else and loop fragments, loop drop/replace. make lint, roundtrip integration, and package tests pass.

🤖 Generated with Claude Code

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.

alter microflow/nanoflow: graph splice, placement and insert/replace/drop statements

1 participant