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'],