diff --git a/archify.zip b/archify.zip index 59b525489..655422061 100644 Binary files a/archify.zip and b/archify.zip differ diff --git a/archify/examples/lifecycle-agent-run.html b/archify/examples/lifecycle-agent-run.html index 56abd2881..e867db846 100644 --- a/archify/examples/lifecycle-agent-run.html +++ b/archify/examples/lifecycle-agent-run.html @@ -5019,12 +5019,12 @@

Agent Run Lifecycle

- - + + - - + + @@ -5198,12 +5198,12 @@

Agent Run Lifecycle

retry
- - needs input + + needs input - - input given + + input given diff --git a/archify/renderers/lifecycle/render-lifecycle.mjs b/archify/renderers/lifecycle/render-lifecycle.mjs index 6d1b26d2f..596a0e873 100644 --- a/archify/renderers/lifecycle/render-lifecycle.mjs +++ b/archify/renderers/lifecycle/render-lifecycle.mjs @@ -672,6 +672,17 @@ function snapPorts(ports, mode) { if (!slot.endsWith('|top')) continue; const lowerId = slot.split('|')[0]; if (onSpine(lowerId)) continue; + // Validate the final group, since neighboring ports may move together. + const group = [...entries.values()].sort((a, b) => a.x - b.x); + const state = states.get(lowerId); + const targets = group.map((port) => port.key?.kind === 'single' + ? ports.get(`${port.key.upperId}|bottom`)?.get(port.key)?.x : undefined); + if (targets.every((x, index) => Number.isFinite(x) + && x >= state.x + PORT_PAD - 8 && x <= state.x + state.width - PORT_PAD + 8 + && (index === 0 || x - targets[index - 1] >= 12))) { + group.forEach((port, index) => { port.x = targets[index]; }); + continue; + } for (const port of entries.values()) { const connector = port.key; if (connector?.kind === 'single') { diff --git a/docs/assets/archify-lifecycle.png b/docs/assets/archify-lifecycle.png index 0f562112c..450e4a5eb 100644 Binary files a/docs/assets/archify-lifecycle.png and b/docs/assets/archify-lifecycle.png differ diff --git a/docs/gallery.html b/docs/gallery.html index ca7feb10c..cc8309907 100644 --- a/docs/gallery.html +++ b/docs/gallery.html @@ -535,7 +535,7 @@

Artifact9/9 pass
CompositionSHOWCASE · PASS
Graph10N · 12E
-
SHA-256e363fdb54f2a
+
SHA-256cb2f4315b7db
Explore focus ↗ @@ -1868,8 +1868,8 @@

Agent Run Lifecycle

- - + + - - + + @@ -5198,12 +5198,12 @@

Agent Run Lifecycle

retry
- - needs input + + needs input - - input given + + input given diff --git a/docs/gallery/manifest.json b/docs/gallery/manifest.json index 1379eb8eb..2270eabe9 100644 --- a/docs/gallery/manifest.json +++ b/docs/gallery/manifest.json @@ -1186,8 +1186,8 @@ "engineeringProfile": null, "nodeCount": 10, "edgeCount": 12, - "artifactBytes": 785682, - "artifactSha256": "e363fdb54f2a6cb3ef3262b081515212524f4b24a0cb5103f3dd301fe8764c5d", + "artifactBytes": 785426, + "artifactSha256": "cb2f4315b7db9ccd006232053499bc93294afac8877c02182654df0cc8f07284", "sourceBytes": 3260, "sourceSha256": "5ba36f5ce91ed428cb48b599e275424916d82b619b9a3231cbd1bbc3966fc54b", "checks": [ @@ -1253,7 +1253,7 @@ "maxStretch": 1, "routesOverSuggestedStretch": 0, "minSegmentPx": 34, - "minInteriorSegmentPx": 40, + "minInteriorSegmentPx": 64, "shortSegmentCount": 0, "shortEndpointSegmentCount": 0, "shortInteriorSegmentCount": 0, diff --git a/examples/lifecycle-agent-run.html b/examples/lifecycle-agent-run.html index 56abd2881..e867db846 100644 --- a/examples/lifecycle-agent-run.html +++ b/examples/lifecycle-agent-run.html @@ -5019,12 +5019,12 @@

Agent Run Lifecycle

- - + + - - + + @@ -5198,12 +5198,12 @@

Agent Run Lifecycle

retry
- - needs input + + needs input - - input given + + input given diff --git a/test/lifecycle-layout.test.mjs b/test/lifecycle-layout.test.mjs index 37b3c431f..3f81b9bd3 100644 --- a/test/lifecycle-layout.test.mjs +++ b/test/lifecycle-layout.test.mjs @@ -68,6 +68,25 @@ test('bundled examples pass the showcase artifact check without crossings', () = } }); +test('a lower state aligns its complete return-port group without changing mixed-fit branches', () => { + const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', 'agent-run.lifecycle.json'), 'utf8')); + const result = render(doc); + assert.equal(result.code, 0, result.stderr); + assert.equal(result.check.ok, true, JSON.stringify(result.check.composition.issues)); + for (const [from, to] of [['reviewing', 'blocked'], ['blocked', 'reviewing']]) { + const route = points(result.svg, from, to); + assert.ok(route.every(([x]) => x === route[0][0]), `${from} -> ${to} is straight`); + } + for (const [from, to, expected] of [ + ['executing', 'approval', [[537, 92], [537, 126], [473, 126], [473, 202]]], + ['approval', 'executing', [[499, 202], [499, 144], [563, 144], [563, 92]]], + ['executing', 'failed', [[589, 92], [589, 144], [657, 144], [657, 202]]], + ['failed', 'executing', [[683, 202], [683, 126], [615, 126], [615, 92]]], + ]) { + assert.deepEqual(points(result.svg, from, to), expected, `${from} -> ${to} retains its mixed-fit route`); + } +}); + test('main path is one row, loops arc above it and other states sit one row per step below', () => { const result = render(base({ mainPath: ['draft', 'review', 'done'],