Arrange graph nodes for the agent and with a button (#429)

An agent that built a graph through MCP left every new node in one
row. In the first real test that put nine nodes on a line 2080 px
long, with the Owner's links running straight through all the others.
Placing nodes by hand cost nine move_node calls and guessed heights.
The agent now gives no coordinates, and the editor lays the nodes out.

- arrange.ts is a pure function. It puts the flow into columns from
  left to right and sets an Owner or Get right before the first node
  it feeds. It orders each column for the fewest crossings and lines
  nodes up by their linked ports without overlaps.
- Node sizes come from the drawn NodeView in the browser and from an
  estimate in unit tests. Both share the constants in nodeBox.ts.
- run_commands arranges the nodes that came without x and y when the
  call ends, inside the same undo step. Nodes that were there stay.
- New command arrange_graph and new tool lint_graph. The lint counts
  overlaps, crossings, links through foreign nodes, backward links
  and the longest links, with example ids.
- The relay instructions tell the agent not to give coordinates and
  to fetch far values with a second Get or Owner node.
- An Arrange button next to the snap magnet in all eight graph tabs.
  It arranges the selected nodes, or all of them, as one undo step.

Found while testing on a real project:

- lint_graph missed a crossing where two links swap rows exactly
  mirrored. Both curves then share a sample point in the middle, and
  only a crossing between two sample points was counted. Links that
  meet on a point count now (segmentsMeet).
- The Arrow-key graph with one Owner for four Moves cannot get below
  8 crossings. The Owner feeds object in row 2 of Move, between exec
  and dt of the key, so it always crosses one of them.

Tests:
- graph-arrange, agent-edit, snap-toggle: 33/33. The swapped-rows
  test was red before the fix (0 instead of 1).
- Four counter-checks that bend arrange or the edit plan, all red.
- e2e 71 on melektaus-atx: 18/18. Lint after the build shows 0
  overlaps, 0 links through nodes, 0 backward. Arrange takes a
  tangled graph from 3 backward links and 2 crossings to 0, and one
  Ctrl+Z restores it.
- Gate on melektaus-atx: CHECK EXIT 0, 318 files, 4917 tests,
  measured before the lint fix.
- Live through MCP on the user's project: lint_graph reports 8
  crossings after Arrange, matching the screenshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-18 12:09:52 +02:00
1 parent 25af064249
commit b9d1602b94
25 files changed
+1268 -35

No files matched your search

+42
View File
@@ -8,6 +8,7 @@ const TOOLS = [
'list_assets',
'get_asset',
'get_graph',
'lint_graph',
'catalog',
'run_commands',
'open',
@@ -260,6 +261,47 @@ export async function mcp(t) {
JSON.stringify({ built: built?.content?.[0]?.text?.slice(0, 300), graph: graph?.edges?.length }),
);
// 71g2. The nodes the agent gave no place come out arranged, as the drawn nodes measure.
const lint = walkId ? (await callTool('lint_graph', { asset: walkId }))?.structuredContent : null;
check(
'the agent’s nodes are arranged: no overlaps, no link over a node, none backwards',
lint?.nodes === 3 && lint.overlaps === 0 && lint.throughNodes === 0 && lint.backwards === 0,
JSON.stringify(lint),
);
// 71g3. Arrange in the graph's header puts a tangle right again, as one step of the user's own.
await callTool('open', { asset: walkId });
const [keyNode, moveNode] = graph.nodes;
await callTool('run_commands', {
label: 'tangle',
steps: [
{
command: 'move_node',
args: { asset: walkId, node: moveNode.id, x: keyNode.x - 400, y: keyNode.y + 300 },
},
],
});
await settle();
const tangled = (await callTool('lint_graph', { asset: walkId }))?.structuredContent;
await clickText('Arrange');
await settle();
const tidied = (await callTool('lint_graph', { asset: walkId }))?.structuredContent;
const arrangedLabel = await json('__gm.state.commands.undoLabel');
await press('z', { ctrl: true });
await settle();
const backTo = (await callTool('lint_graph', { asset: walkId }))?.structuredContent;
await callTool('undo');
check(
'Arrange in the header turns the links forward again in one undo step, and Ctrl+Z brings the tangle back',
tangled?.backwards > 0 &&
tidied?.backwards === 0 &&
tidied.overlaps === 0 &&
arrangedLabel === 'Arrange nodes' &&
backTo?.backwards === tangled.backwards &&
(await json('__gm.state.commands.undoLabel')) === 'Agent: walk left',
JSON.stringify({ tangled, tidied, arrangedLabel, backTo }),
);
// 71h. Played by the agent, the object walks left while the user holds the arrow.
const started = await callTool('play', { action: 'play' });
await waitUntil('__gm.state.mode === "play"', { timeout: 10000, label: 'the game runs' });
+3
View File
@@ -38,6 +38,9 @@ const INSTRUCTIONS = [
'are commands from catalog topic "commands", run as one undo step, and a step can name what an',
'earlier one made ("as": "x", then "$x"). Nodes for a graph come from catalog topic "nodes".',
'A behavior acts on the object it is placed under: place it with parent set to that object.',
'Give new nodes no x and y: the editor arranges them when the call ends, and leaves the nodes',
'already there where they are. Keep a graph readable: a value needed far away comes from a second',
'Get or Owner node next to its user, not over a long link; check with lint_graph.',
'When a tool answers that no editor is connected, ask the user to open the editor and switch on',
'"Agent" in its account menu.',
].join(' ');