Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -728,3 +728,5 @@
{"area": "mdl/executor", "date": "2026-09-26", "symptom": "`describe fragment from page M.P widget w` (and `from snippet`) fails for every container and every widget — including ones `describe page` prints — with `not found in page M.P`, a message that does not even name the widget", "cause": "Visitor stores DescribeFragmentFromStmt.ContainerType as \"PAGE\"/\"SNIPPET\"; describeFragmentFrom switched on \"page\"/\"snippet\" with no default, so neither branch ran, the widget list stayed empty, and the fall-through reported the widget missing. Third instance of this split: ALTER PAGE (#402), DESCRIBE/ALTER STYLING (#631)", "file": "`mdl/executor/cmd_fragments.go` (`describeFragmentFrom`)", "insight": "The mismatch hid behind a plausible error because a switch on the discriminator had no default: an unmatched container type looked like an empty container, and an empty container looks like a missing widget. Normalise with strings.ToLower where the discriminator is consumed (the house convention — cmd_styling, cmd_alter_page, validate_alter_* all do) AND make the default an error, so the next casing drift fails loudly instead of reporting the wrong thing. The existing mock tests hand-built the AST and so agreed with the handler; only a test that goes visitor.Build → NewRegistry().Dispatch pins the contract between the two layers (cmd_fragments_from_test.go). Verified on Evora: Administration.Account_Edit/textBox6 and AgentCommons.Snippet_Agent_Details/dataView7 now describe", "refs": ["#402", "#631"]}
{"area": "mdl/executor", "date": "2026-09-26", "symptom": "describe output that does not re-parse or loses data (ako/mxcli#707): an entity string default or validation message containing ' was emitted unescaped; so were module-role descriptions, published OData/REST Path/Version/Namespace/Summary/Folder, and REST client BaseUrl/Path/header values; an agent `mcp service` block with a Description lacked the comma after `Enabled`; workflow decision / parallel split captions came back only as `-- caption` comments (replay reset them to 'Decision' / 'Parallel split'); `describe demo user` emitted `password '***'`, which replay stored as the password; `describe settings` printed `DatabasePassword = '<plaintext>'`.", "cause": "Hand-rolled `'%s'` emit sites that put the quotes and the escaping in different places (the #1006 source-scan guard covered only cmd_workflows.go); a block emitter with no separator logic, unlike its sibling; captions treated as commentary although the grammar has `comment '…'` for both activities; secrets printed as data, with a placeholder the writer took literally.", "file": "mdl/executor/cmd_entities_describe.go, cmd_security.go, cmd_security_write.go, cmd_odata.go, cmd_published_rest.go, cmd_rest_clients.go, cmd_agenteditor_agents.go, cmd_workflows.go, cmd_settings.go", "fix": "Every emit site uses mdlQuoted; TestDescribers_HaveNoHandRolledStringLiterals now scans all seven describer files. MCP block writes the comma like the tool block. workflowCaptionClauses emits `comment '…'` for a non-default caption and computes the name clause against the caption the writer will store. DatabasePassword is omitted with a comment (create or modify is a patch, so replay keeps it). Demo users are described as `create or modify … password '***'`, and the executor treats '***' as 'keep the stored password', refusing it for a user that does not exist.", "insight": "Assert round trips by reparsing describe output with the real visitor and comparing the AST value to the stored one, not by substring. For secrets the right placeholder is one the WRITER understands: omission works where the create is a patch (configuration); where the grammar requires the value (demo user) give the placeholder a meaning (keep stored) and refuse it where that meaning is empty, so a replay can neither leak nor silently set a credential.", "test": "mdl/executor/issue707_describe_roundtrip_test.go"}
{"area": "mdl/executor", "date": "2026-09-26", "symptom": "describe microflow \u2026 with handles (ako/mxcli#713) printed no handle for an activity inside an `on error { \u2026 }` block, and the alter-target resolver counted such activities after the whole main flow: on SUB_Feedback_PostToAppInsights `return * @1` picked `return $Response`, although describe prints the handler's `return empty` first. Also `$Response` did not address a REST call whose output is on its result handling (and cast, create list, web service, workflow, XML/JSON, database-query outputs).", "cause": "Error-handler bodies are rendered by collectErrorHandlerStatements, a second describer that returned bare strings and never wrote the source map, so those nodes had no line to rank or print a handle at; unranked candidates were appended last. The output-variable switch was copied from actionOutputVariableName, which had drifted from the formatter.", "file": "mdl/executor/cmd_microflows_show_helpers.go; mdl/backend/mfmutator/target.go", "fix": "collectErrorHandlerStatementSpans reports each handler-body object's statement span; emitActivityStatement and emitCommentedErrorHandler record them in the source map (additive entries in ELK sourceMap too). mfmutator.OutputVariable reads the variable where the formatter does, for every action it prints as `$X = \u2026`. A comment rendering (`-- Unsupported \u2026`) is no statement, so it never becomes a handle.", "insight": "Any node the describer prints through a side path must enter the source map, or everything keyed on print order (ordinals, handles, ELK highlighting) silently disagrees with the text. Check ranking against a Studio Pro flow that has a handler body, not just VAL_Feedback.", "test": "TestDescribeWithHandles_ErrorHandlerBody, TestMicroflowTargets_PedAppEveryFlowRanksAndResolves, TestOutputVariable_EveryActionDescribePrintsAnAssignmentFor, TestCandidate_CommentRenderingIsNoStatement"}
{"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe microflow \u2026 with handles printed `-- handle: commit $Order on error {` for an activity with a custom error handler (TestApp Services.SaveOrder). The handle could not be used: an `alter microflow` target ends at the `{` that opens a fragment, so `insert after commit $Order on error { \u2026 }` parsed the handler brace as the fragment.", "cause": "printedStatement ended an action's statement at a line ending in `;` or `{` and kept the `{`, which belongs to the error-handler block describe opens, not to the statement.", "file": "mdl/executor/cmd_microflows_handles.go", "fix": "printedStatement strips the trailing `{` of an error-handler block opener, so the handle is `commit $Order on error`, which parses as a target and still matches the activity.", "insight": "A handle is only useful if it can be written back as a target in the grammar that consumes it; test a printed handle by parsing it, not only by resolving it.", "test": "TestPrintedStatement_ErrorHandlerBlockOpenerIsNotPartOfTheStatement"}
{"area": "mdl/executor", "date": "2026-09-27", "symptom": "alter microflow (ako/mxcli#736) reported \"Altered microflow\" for statements mx check then rejected: two fragments in one statement declaring the same variable gave CE0111 Duplicate variable name; a fragment reading $X in the same statement as `drop $X` (either order) gave CE0109 Undefined variable. A loop fragment was refused as reading its own iterator, and drop/replace of a Studio Pro loop with more than one body activity was refused by the dangling-reference guard.", "cause": "The scope checks compared each operation with the flow as stored, never with what earlier operations of the same statement had declared, read or removed. The iterator of a fragment's loop is on its LoopSource, not an action output. Loop body flows are stored in the unit's Flows list, not in the loop, so removing the loop left them pointing at removed objects.", "file": "mdl/executor/cmd_alter_flow.go; mdl/backend/mfmutator/splice.go", "fix": "alterFlowContext tracks declaredByOps / readByOps / removedByOps across the statement's operations and checks each later operation against them; checkFragmentScope counts a fragment loop's iterator as its own; Drop and Replace remove graph.bodyFlows of a loop with it.", "insight": "A per-operation check against the stored document is only sound for a one-operation statement; every multi-operation test needs a case where operation 2 depends on operation 1. mx check on a copied TestApp is the cheap oracle: the CE numbers appear the moment a hygiene hole is hit.", "test": "TestAlterMicroflow_PedApp_ScopeSpansTheStatement; TestAlterMicroflow_PedApp_LoopFragmentDeclaresItsIterator; TestSplice_DropOrReplaceALoopTakesItsBodyFlows"}
35 changes: 35 additions & 0 deletions cmd/mxcli/syntax/features_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,41 @@ func init() {
Example: "LOG INFO NODE 'OrderService' 'Order created successfully';\nLOG WARNING 'Customer not found';\nLOG ERROR 'Failed to process {1}' WITH (\n {1} = $OrderNumber\n);",
})

Register(SyntaxFeature{
Path: "microflow.alter",
Summary: "Patch a stored microflow or nanoflow: insert, replace or drop activities in place",
Keywords: []string{
"alter microflow", "alter nanoflow", "insert after", "insert before",
"replace", "drop activity", "patch microflow", "splice", "handle",
},
Syntax: "ALTER MICROFLOW|NANOFLOW Module.Name {\n" +
" INSERT AFTER|BEFORE <target> { <statements> }\n" +
" REPLACE <target> WITH { <statements> }\n" +
" DROP <target>;\n" +
"};\n\n" +
"-- <target> addresses one activity by content, as `describe microflow ... with handles` prints it:\n" +
"-- $Var the activity that outputs $Var\n" +
"-- 'Caption' a decision or an activity with a custom caption\n" +
"-- <statement> a statement pattern; * matches any run of tokens\n" +
"-- followed by @n when it matches more than one. Targets are resolved against the\n" +
"-- stored flow before any operation runs; an ambiguous or unknown target is an error.\n" +
"-- Only the new activities, the rewired flows and the objects moved to make room change;\n" +
"-- every other element keeps its $ID, position and curve.\n" +
"-- Refused: insert after a decision, insert before an activity several flows enter,\n" +
"-- drop/replace of a decision or of an activity with an error handler, anything inside\n" +
"-- a loop body, a fragment that returns, and a fragment variable that clashes with one\n" +
"-- the flow has or reads one not declared on the path. Over --mcp only insert is supported.",
Example: "alter microflow FeedbackModule.VAL_Feedback {\n" +
" insert after $IsValidEmail { log info node 'Feedback' 'Email checked'; }\n" +
" replace set $ValidFeedback = false @3 with {\n" +
" set $ValidFeedback = false;\n" +
" log warning node 'Feedback' 'Email rejected';\n" +
" }\n" +
" drop log debug node 'Feedback' *;\n" +
"};",
SeeAlso: []string{"microflow"},
})

Register(SyntaxFeature{
Path: "microflow.show-page",
Summary: "Open and close pages from microflows",
Expand Down
2 changes: 2 additions & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ rather than updating the first.
| Describe microflow | `describe microflow Module.Name;` | Full MDL with activities |
| Describe microflow (normalized) | `describe microflow Module.Name normalized;` | Folds crossed branches into one condition instead of flattening them. Opt-in: the output re-executes to an equivalent graph with fewer nodes and a different layout |
| Describe microflow (with handles) | `describe microflow Module.Name with handles;` | Prints `-- handle: <target>` above each activity: its content address for `alter microflow` — output `$Var`, `'Caption'`, or a statement pattern with `*` wildcards (anchored at both ends), plus `@n` when several match. Comments only; cannot be combined with `normalized` |
| Insert into a stored microflow | `alter microflow Module.Name { insert after <target> { <statements> } };` | Also `insert before`, and `alter nanoflow`. A graph splice into the stored flow, not a rebuild: only the new activities, the two rewired flows and the objects moved to make room change; every other element keeps its `$ID`, position and curve. `<target>` is a handle from `describe … with handles`, resolved before any operation runs. Refused: after a decision, before an activity several flows enter, inside a loop body, a fragment that returns, a variable the flow already has or one not declared on the path |
| Replace or drop an activity | `alter microflow Module.Name { replace <target> with { <statements> } drop <target>; };` | Flows into the activity are re-pointed at the replacement (or at its successor, for `drop`). Refused for a decision, an end event, an activity with an error handler, and an activity whose output variable is still read. Over `--mcp` only `insert` is supported |
| Describe nanoflow | `describe nanoflow Module.Name;` | Full MDL with activities |
| Rename microflow | `rename microflow Module.Old to New;` | Updates all references |
| Rename nanoflow | `rename nanoflow Module.Old to New;` | Updates all references |
Expand Down
54 changes: 54 additions & 0 deletions mdl/ast/ast_alter_flow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0

package ast

// ============================================================================
// ALTER MICROFLOW / ALTER NANOFLOW — a graph splice into the stored flow
// ============================================================================

// AlterFlowStmt represents:
//
// alter microflow|nanoflow Module.Name {
// insert after|before <target> { <statements> }
// replace <target> with { <statements> }
// drop <target>;
// }
//
// (ADR-0012 decision 3). Targets are content addresses, resolved against the
// flow as stored before any operation applies.
type AlterFlowStmt struct {
Nanoflow bool
Name QualifiedName
Operations []*AlterFlowOperation
}

func (s *AlterFlowStmt) isStatement() {}

// Kind is "microflow" or "nanoflow".
func (s *AlterFlowStmt) Kind() string {
if s.Nanoflow {
return "nanoflow"
}
return "microflow"
}

// AlterFlowOpKind names an operation of an AlterFlowStmt.
type AlterFlowOpKind string

const (
AlterFlowInsertAfter AlterFlowOpKind = "insert after"
AlterFlowInsertBefore AlterFlowOpKind = "insert before"
AlterFlowReplace AlterFlowOpKind = "replace"
AlterFlowDrop AlterFlowOpKind = "drop"
)

// AlterFlowOperation is one operation of an AlterFlowStmt.
type AlterFlowOperation struct {
Op AlterFlowOpKind
// Target is the content address as written (`$IsValidEmail`,
// `'Email is Valid?'`, `log * node 'Debug' *`, with an optional `@n`).
// mfmutator.ParseTarget reads it.
Target string
// Body is the fragment, for insert and replace.
Body []MicroflowStatement
}
1 change: 1 addition & 0 deletions mdl/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ type FullBackend interface {
AgentEditorBackend
PageMutationBackend
WorkflowMutationBackend
MicroflowMutationBackend
WidgetBuilderBackend
}
Loading
Loading