Let a Start input or a variable name the state machine it carries

A sub-behavior that gets its machine through a Start input had no
state ports on Is in state, Set state and the other state nodes.
Their ports came only from an inline pick or a State machine literal,
so a machine handed in from outside left them empty.

- A statemachine port of a Start node can now name a machine
  (DeclaredPort.machine). The inspector shows the pick next to the
  port name. SetPortMachineCommand sets it with undo.
- machineIdOf follows an edge from such a port to that machine, so
  the state nodes it feeds offer its states.
- A Run behavior node has the machine picked in advance on that input.
- A different pick there is allowed, but the node warns, because the
  called graph asks for the states of its own machine.
- A statemachine variable of a Variables node can name a machine
  the same way, picked next to the variable's row.
- The inspector signature of a node now includes the picked machine.
  Before, the pick changed the ports but the dropdown kept its text
  until the node was selected again.
- A local machine handed in still starts in no state every call.
  That rule from #229 is unchanged.

Tests:
- tests/core/start-machine.test.ts (7): ports with and without a
  machine and undo, save and load, the preset value moving a shared
  machine through the call, the warning, a port of another kind,
  the inspector signature, a Variables node.
- Counter checks: without the new branch in machineIdOf 2 tests
  fail, without the machine in the signature 1 test fails.
- Gate on melektaus-atx: 239 files, 3599 tests, CHECK EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-04 18:15:56 +02:00
1 parent 3f12a77aea
commit d80d5cce26
8 files changed
+284 -10

No files matched your search

+16 -1
View File
@@ -156,8 +156,9 @@ export function isFlowPort(port: DeclaredPort): boolean {
*/
export function portDefOf(port: DeclaredPort): PortDef {
const def: PortDef = { key: port.key, label: port.key, type: declaredPortType(port.kind) };
// The machine a start node's input carries is what the field offers before anyone picks.
return isTaggedKind(port.kind) && port.kind !== 'object'
? { ...def, field: flatDef(port.key, port.kind) }
? { ...def, field: flatDef(port.key, port.kind, { value: port.machine }) }
: def;
}
@@ -360,6 +361,20 @@ export function callProblem(node: GraphNode, lookup: GraphLookup, ownerId = ''):
const program = lookup.behavior(id);
if (!program) return 'That behavior is gone';
if (callEntries(program.graph).length === 0) return 'No start or On start node – does nothing';
return otherMachineProblem(node, program.graph, lookup);
}
/**
* A machine picked here other than the one the start node's input carries: the called graph asks for
* the states of its own, so every one of them would read as never entered.
*/
function otherMachineProblem(node: GraphNode, graph: Graph, lookup: GraphLookup): string | null {
for (const port of terminalNode(graph, BEHAVIOR_START)?.ports ?? []) {
const picked = textOf(node.params[port.key] ?? '');
if (port.kind !== 'statemachine' || !port.machine || !picked || picked === port.machine) continue;
const machine = lookup.machine(port.machine);
return `"${port.key}" expects ${machine ? `"${machine.name}"` : 'a machine that is gone'}`;
}
return null;
}