Compare commits

...
Author SHA1 Message Date
jarvis-at-skic 15c5dcb31d fix: align Target governance links to shared edge midpoints
Build and publish Eldorado documentation / build (pull_request) Successful in 38s
2026-10-05 20:12:20 +00:00
jarvis-at-skic 64b2d6dae8 Merge pull request 'docs: rename Target contributors to AI Agent' (#25) from docs/target-ai-agent into main
Build and publish Eldorado documentation / build (push) Successful in 45s
2026-10-05 12:54:21 -07:00
jarvis-at-skic 20e6f4899c docs: rename Target contributors to AI Agent
Build and publish Eldorado documentation / build (pull_request) Successful in 50s
2026-10-05 19:51:04 +00:00
jarvis-at-skic 2f68e60cec Merge pull request 'docs: redesign Roadmap as a native five-stage workflow' (#24) from docs/roadmap-workflow into main
Build and publish Eldorado documentation / build (push) Successful in 39s
2026-10-05 12:36:42 -07:00
9 changed files with 158 additions and 29 deletions

No files matched your search

@@ -12,14 +12,14 @@
{"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[100,1540],"size":[240,100]}, {"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[100,1540],"size":[240,100]},
{"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[100,1760],"size":[240,100]}, {"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[100,1760],"size":[240,100]},
{"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[100,1980],"size":[240,100]}, {"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[100,1980],"size":[240,100]},
{"id":"code-scope","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,440],"size":[240,100]}, {"id":"code-scope","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,440],"size":[240,100]},
{"id":"code-architecture","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,660],"size":[240,100]}, {"id":"code-architecture","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,660],"size":[240,100]},
{"id":"code-ui-ux","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,880],"size":[240,100]}, {"id":"code-ui-ux","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,880],"size":[240,100]},
{"id":"code-planning","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1100],"size":[240,100]}, {"id":"code-planning","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1100],"size":[240,100]},
{"id":"code-delivery","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1320],"size":[240,100]}, {"id":"code-delivery","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1320],"size":[240,100]},
{"id":"code-uat","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1540],"size":[240,100]}, {"id":"code-uat","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1540],"size":[240,100]},
{"id":"code-security","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1760],"size":[240,100]}, {"id":"code-security","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1760],"size":[240,100]},
{"id":"code-releases","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1980],"size":[240,100]} {"id":"code-releases","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1980],"size":[240,100]}
], ],
"connections": [ "connections": [
+16 -16
View File
@@ -12,25 +12,25 @@
{"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[240,727.5],"size":[180,75]}, {"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[240,727.5],"size":[180,75]},
{"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[240,457.5],"size":[180,75]}, {"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[240,457.5],"size":[180,75]},
{"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[240,187.5],"size":[180,75]}, {"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[240,187.5],"size":[180,75]},
{"id":"code-scope","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[510,37.5],"size":[180,75]}, {"id":"code-scope","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[510,37.5],"size":[180,75]},
{"id":"code-architecture","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[780,37.5],"size":[180,75]}, {"id":"code-architecture","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[780,37.5],"size":[180,75]},
{"id":"code-ui-ux","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[1005,457.5],"size":[180,75]}, {"id":"code-ui-ux","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[1005,457.5],"size":[180,75]},
{"id":"code-planning","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[780,877.5],"size":[180,75]}, {"id":"code-planning","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[780,877.5],"size":[180,75]},
{"id":"code-delivery","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[510,877.5],"size":[180,75]}, {"id":"code-delivery","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[510,877.5],"size":[180,75]},
{"id":"code-uat","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[240,877.5],"size":[180,75]}, {"id":"code-uat","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[240,877.5],"size":[180,75]},
{"id":"code-security","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[15,457.5],"size":[180,75]}, {"id":"code-security","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[15,457.5],"size":[180,75]},
{"id":"code-releases","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[240,37.5],"size":[180,75]} {"id":"code-releases","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[240,37.5],"size":[180,75]}
], ],
"connections": [ "connections": [
{"id":"govern-scope","from":"agent","to":"scope","label":"","variant":"security","fromSide":"right","toSide":"bottom","via":[[750,495],[750,292.5],[600,292.5]]}, {"id":"govern-scope","from":"agent","to":"scope","label":"","variant":"security","fromSide":"top","toSide":"bottom","route":"straight"},
{"id":"govern-architecture","from":"agent","to":"architecture","label":"","variant":"security","fromSide":"right","toSide":"bottom","via":[[765,495],[765,345],[870,345]]}, {"id":"govern-architecture","from":"agent","to":"architecture","label":"","variant":"security","fromSide":"top","toSide":"bottom","via":[[600,345],[870,345]]},
{"id":"govern-ui-ux","from":"agent","to":"ui-ux","label":"","variant":"security","fromSide":"right","toSide":"left"}, {"id":"govern-ui-ux","from":"agent","to":"ui-ux","label":"","variant":"security","fromSide":"right","toSide":"left","route":"straight"},
{"id":"govern-planning","from":"agent","to":"planning","label":"","variant":"security","fromSide":"right","toSide":"top","via":[[765,495],[765,690],[870,690]]}, {"id":"govern-planning","from":"agent","to":"planning","label":"","variant":"security","fromSide":"bottom","toSide":"top","via":[[600,645],[870,645]]},
{"id":"govern-delivery","from":"agent","to":"delivery","label":"","variant":"security","fromSide":"bottom","toSide":"top"}, {"id":"govern-delivery","from":"agent","to":"delivery","label":"","variant":"security","fromSide":"bottom","toSide":"top","route":"straight"},
{"id":"govern-uat","from":"agent","to":"uat","label":"","variant":"security","fromSide":"left","toSide":"top","via":[[435,495],[435,690],[330,690]]}, {"id":"govern-uat","from":"agent","to":"uat","label":"","variant":"security","fromSide":"bottom","toSide":"top","via":[[600,645],[330,645]]},
{"id":"govern-security","from":"agent","to":"security","label":"","variant":"security","fromSide":"left","toSide":"right"}, {"id":"govern-security","from":"agent","to":"security","label":"","variant":"security","fromSide":"left","toSide":"right","route":"straight"},
{"id":"govern-releases","from":"agent","to":"releases","label":"","variant":"security","fromSide":"left","toSide":"bottom","via":[[435,495],[435,345],[330,345]]}, {"id":"govern-releases","from":"agent","to":"releases","label":"","variant":"security","fromSide":"top","toSide":"bottom","via":[[600,345],[330,345]]},
{"id":"stage-1","from":"scope","to":"architecture","label":"Then","variant":"emphasis","fromSide":"right","toSide":"left"}, {"id":"stage-1","from":"scope","to":"architecture","label":"Then","variant":"emphasis","fromSide":"right","toSide":"left"},
{"id":"stage-2","from":"architecture","to":"ui-ux","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"}, {"id":"stage-2","from":"architecture","to":"ui-ux","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"},
{"id":"stage-3","from":"ui-ux","to":"planning","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"}, {"id":"stage-3","from":"ui-ux","to":"planning","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"},
+1 -1
View File
@@ -16,7 +16,7 @@ import ArchifyDiagram from '@site/src/components/ArchifyDiagram';
<ArchifyDiagram name="strategy-target" title="Autonomous agent executes and governs the SDLC; humans direct, guide and review" story /> <ArchifyDiagram name="strategy-target" title="Autonomous agent executes and governs the SDLC; humans direct, guide and review" story />
An autonomous agent executes and governs every phase. Humans direct, guide and review it; a separate human-controlled code agent contributes to each phase from outside the cycle. An autonomous agent executes and governs every phase. Humans direct, guide and review it; a separate human-controlled AI Agent contributes to each phase from outside the cycle.
**Solid arrows:** lifecycle · **Dotted spokes:** execution and governance · **Dashed arrows:** contributions. **Solid arrows:** lifecycle · **Dotted spokes:** execution and governance · **Dashed arrows:** contributions.
+1 -1
View File
@@ -15,7 +15,7 @@
"verify:build": "node scripts/verify.mjs --build", "verify:build": "node scripts/verify.mjs --build",
"diagrams:build": "node scripts/build-diagrams.mjs", "diagrams:build": "node scripts/build-diagrams.mjs",
"test": "node --test scripts/*.test.mjs", "test": "node --test scripts/*.test.mjs",
"test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/skills-measurement.cjs && node scripts/browser/roadmap.cjs", "test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/target-midpoints.cjs && node scripts/browser/skills-measurement.cjs && node scripts/browser/roadmap.cjs",
"kpi:example": "node scripts/ai-kpi.mjs" "kpi:example": "node scripts/ai-kpi.mjs"
}, },
"dependencies": { "dependencies": {
+12
View File
@@ -53,6 +53,9 @@ const fs = require('node:fs');
assert.deepEqual(metrics.edges.map(e=>e.slice(0,2)), ids.map((id,i)=>[id,ids[(i+1)%ids.length]])); assert.deepEqual(metrics.edges.map(e=>e.slice(0,2)), ids.map((id,i)=>[id,ids[(i+1)%ids.length]]));
assert(metrics.edges.every(e=>e[2]?.startsWith('url(')), 'all stages must have arrowheads'); assert(metrics.edges.every(e=>e[2]?.startsWith('url(')), 'all stages must have arrowheads');
assert.equal(metrics.nodes.length, 17); assert.equal(metrics.nodes.length, 17);
assert.equal(metrics.labels.filter(n => n.text === 'AI Agent').length, 8);
assert(!metrics.labels.some(n => /code[ -]agent/i.test(n.text)));
assert((await page.locator('.strategy-content').innerText()).includes('human-controlled AI Agent'));
assert(!metrics.nodes.some(n=>n.id==='humans')); assert(!metrics.nodes.some(n=>n.id==='humans'));
assert.equal(await frame.locator('[data-node-id="agent"] text').allTextContents().then(texts=>texts.includes('Human Controlled')), true); assert.equal(await frame.locator('[data-node-id="agent"] text').allTextContents().then(texts=>texts.includes('Human Controlled')), true);
assert.equal(metrics.relationships.length, 24); assert.equal(metrics.relationships.length, 24);
@@ -74,6 +77,15 @@ const fs = require('node:fs');
} }
assert(metrics.labels.every(n=>n.px>=14), JSON.stringify(metrics.labels)); assert(metrics.labels.every(n=>n.px>=14), JSON.stringify(metrics.labels));
assert.deepEqual(metrics.overflow, []); assert.deepEqual(metrics.overflow, []);
await frame.locator('#btn-export').click();
const [download] = await Promise.all([page.waitForEvent('download'), frame.locator('[data-format="svg"]').click()]);
assert.equal(await download.failure(), null);
const exportPath = `${output}/${width}-${route ? 'target' : 'landing'}.svg`;
await download.saveAs(exportPath);
const exported = fs.readFileSync(exportPath, 'utf8');
assert.equal((exported.match(/>AI Agent<\/text>/g) || []).length, 8);
assert(!/code[ -]agent/i.test(exported));
assert(exported.includes('Autonomous agent') && exported.includes('Human Controlled'));
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth)); assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));
await page.evaluate(()=>scrollTo({top:0,behavior:'instant'})); await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));
await page.waitForFunction(()=>scrollY===0); await page.waitForFunction(()=>scrollY===0);
+85
View File
@@ -0,0 +1,85 @@
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
// Inspect the rendered paths, not just the authored JSON. Also runs on downloaded SVGs.
function inspect({mobile, xml}) {
let svg = document.querySelector('svg[role="img"]');
if (xml) {
svg = new DOMParser().parseFromString(xml, 'image/svg+xml').documentElement;
document.body.appendChild(svg);
}
const point = (element, p) => new DOMPoint(p.x, p.y).matrixTransform(element.getCTM());
const boxes = [...svg.querySelectorAll('[data-node-id]')].map(node => {
const rect = node.querySelector('rect'), box = rect.getBBox();
const a = point(rect, box), b = point(rect, {x:box.x + box.width, y:box.y + box.height});
return {id:node.dataset.nodeId, x:a.x, y:a.y, right:b.x, bottom:b.y, cx:(a.x+b.x)/2, cy:(a.y+b.y)/2};
});
const agent = boxes.find(n => n.id === 'agent');
const errors = [], endpoints = {};
const paths = [...svg.querySelectorAll('path[data-edge-id^="govern-"]')];
if (paths.length !== 8) errors.push(`expected eight governance paths, got ${paths.length}`);
for (const path of paths) {
const id = path.dataset.edgeTo;
const side = mobile ? 'left' : ['scope','architecture','releases'].includes(id) ? 'top'
: ['planning','delivery','uat'].includes(id) ? 'bottom' : id === 'security' ? 'left' : 'right';
const expected = {top:[agent.cx,agent.y],bottom:[agent.cx,agent.bottom],left:[agent.x,agent.cy],right:[agent.right,agent.cy]}[side];
const start = point(path,path.getPointAtLength(0));
endpoints[id] = [start.x,start.y];
if (Math.hypot(start.x-expected[0],start.y-expected[1]) > 0.01) errors.push(`${id}: wrong ${side} midpoint`);
const length = path.getTotalLength();
for (let distance = 1; distance < length; distance += 2) {
const p = point(path,path.getPointAtLength(distance));
for (const box of boxes) {
if (box.id === id) continue;
if (p.x > box.x+0.1 && p.x < box.right-0.1 && p.y > box.y+0.1 && p.y < box.bottom-0.1) {
errors.push(`${id}: crosses ${box.id}`); break;
}
}
}
}
if (xml) svg.remove();
return {errors:[...new Set(errors)],endpoints};
}
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/midpoints-${process.argv[3] || 'local'}`;
fs.mkdirSync(output,{recursive:true});
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,args:['--no-sandbox']});
const results = [];
try {
for (const width of [1440,390,320]) {
const page = await browser.newPage({viewport:{width,height:1000}});
await page.goto(`${base}/eldorado-v1/documentation/strategy/target-operating-model/`,{waitUntil:'networkidle'});
const panel = page.locator('[data-diagram="strategy-target"]');
const iframe = panel.locator(`:scope > .story-${width > 700 ? 'desktop' : 'mobile'}`);
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const inline = await frame.evaluate(inspect,{mobile:width < 700});
assert.deepEqual(inline.errors,[]);
await page.screenshot({path:`${output}/${width}-inline.png`,fullPage:true});
await frame.locator('#btn-export').click();
const [download] = await Promise.all([page.waitForEvent('download'),frame.locator('[data-format="svg"]').click()]);
assert.equal(await download.failure(),null);
const path = `${output}/${width}.svg`;
await download.saveAs(path);
const exported = await frame.evaluate(inspect,{mobile:width < 700,xml:fs.readFileSync(path,'utf8')});
assert.deepEqual(exported.errors,[]);
await panel.getByRole('button',{name:/^Expand /}).click();
const full = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await full.locator('svg[role="img"]').waitFor();
// The existing fullscreen viewer always opens the radial desktop source.
const fullscreen = await full.evaluate(inspect,{mobile:false});
assert.deepEqual(fullscreen.errors,[]);
await page.screenshot({path:`${output}/${width}-fullscreen.png`});
await panel.getByRole('button',{name:'Close full-screen diagram'}).click();
results.push({width,inline,exported,fullscreen});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
await page.close();
}
assert.equal(results.length,3);
console.log('PASS midpoint geometry and no node crossings: 3 viewports × inline/fullscreen/exported SVG');
} finally {await browser.close();}
})().catch(error => {console.error(error);process.exitCode=1;});
+3 -2
View File
@@ -14,7 +14,8 @@ test('Strategy opens at the canonical autonomous lifecycle', () => {
assert(primary.includes('autonomous agent')); assert(primary.includes('autonomous agent'));
assert(primary.includes('direct, guide and review')); assert(primary.includes('direct, guide and review'));
assert(primary.includes('Scope → Architecture → UI/UX → Planning → Delivery → UAT → Security → Releases → Scope')); assert(primary.includes('Scope → Architecture → UI/UX → Planning → Delivery → UAT → Security → Releases → Scope'));
assert(primary.includes('human-controlled code agent')); assert(primary.includes('human-controlled AI Agent'));
assert(!/code[ -]agent/i.test(page));
assert(primary.includes('TO-BE')); assert(primary.includes('TO-BE'));
assert(!/only routes|People authorize|independent gates protect release/.test(primary)); assert(!/only routes|People authorize|independent gates protect release/.test(primary));
} }
@@ -37,7 +38,7 @@ test('both target diagrams have eight stages, eight governed phases and eight hu
assert.deepEqual(d.connections.filter(e => e.from === 'agent').map(e => e.to), ids); assert.deepEqual(d.connections.filter(e => e.from === 'agent').map(e => e.to), ids);
const contributors = d.components.filter(n => n.id.startsWith('code-')); const contributors = d.components.filter(n => n.id.startsWith('code-'));
assert.equal(contributors.length, 8); assert.equal(contributors.length, 8);
assert(contributors.every(n => n.label === 'Code agent' && n.sublabel === 'Human-controlled')); assert(contributors.every(n => n.label === 'AI Agent' && n.sublabel === 'Human-controlled'));
for (const id of ids) { for (const id of ids) {
assert(d.connections.some(e => e.from === `code-${id}` && e.to === id && e.variant === 'dashed')); assert(d.connections.some(e => e.from === `code-${id}` && e.to === id && e.variant === 'dashed'));
assert(d.connections.some(e => e.from === 'agent' && e.to === id && e.variant === 'security')); assert(d.connections.some(e => e.from === 'agent' && e.to === id && e.variant === 'security'));
+1 -1
View File
@@ -36,7 +36,7 @@ test('mobile story diagrams preserve the same nodes and directed relationships',
const desktop = JSON.parse(read(`diagrams/${name}.architecture.json`)); const desktop = JSON.parse(read(`diagrams/${name}.architecture.json`));
const mobile = JSON.parse(read(`diagrams/${name}-mobile.architecture.json`)); const mobile = JSON.parse(read(`diagrams/${name}-mobile.architecture.json`));
assert(desktop.components.length >= 5 && desktop.components.length <= (name === 'strategy-target' ? 18 : 7), name); assert(desktop.components.length >= 5 && desktop.components.length <= (name === 'strategy-target' ? 18 : 7), name);
const semantic = d => ({nodes:d.components.map(({pos,size,...n})=>n),edges:d.connections.map(({fromSide,toSide,via,labelAt,...e})=>e)}); const semantic = d => ({nodes:d.components.map(({pos,size,...n})=>n),edges:d.connections.map(({fromSide,toSide,route,via,labelAt,...e})=>e)});
assert.deepEqual(semantic(desktop), semantic(mobile), name); assert.deepEqual(semantic(desktop), semantic(mobile), name);
assert(desktop.connections.every(e => (e.label || e.id.startsWith('govern-')) && e.from !== e.to), name); assert(desktop.connections.every(e => (e.label || e.id.startsWith('govern-')) && e.from !== e.to), name);
assert.equal(desktop.meta.legend.mode, 'hidden'); assert.equal(desktop.meta.legend.mode, 'hidden');
+31
View File
@@ -0,0 +1,31 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createRouter} from '../vendor/archify/renderers/architecture/routing.mjs';
import {segmentIntersectsRect} from '../vendor/archify/renderers/shared/geometry.mjs';
for (const suffix of ['', '-mobile']) {
test(`Target${suffix} uses shared native midpoint ports without node crossings`, () => {
const diagram = JSON.parse(readFileSync(`diagrams/strategy-target${suffix}.architecture.json`, 'utf8'));
const boxes = new Map(diagram.components.map(({id, pos: [x, y], size: [width, height]}) =>
[id, {id, x, y, width, height, cx: x + width / 2, cy: y + height / 2}]));
const router = createRouter(boxes, diagram.connections, {distinctAutomaticPorts: true});
const agent = boxes.get('agent');
for (const edge of diagram.connections.filter(edge => edge.from === 'agent')) {
const side = suffix ? 'left' : ['scope', 'architecture', 'releases'].includes(edge.to) ? 'top'
: ['planning', 'delivery', 'uat'].includes(edge.to) ? 'bottom' : edge.to === 'security' ? 'left' : 'right';
assert.equal(edge.fromSide, side, edge.id);
assert(edge.via || (edge.route && edge.route !== 'auto'), `${edge.id} must opt out of automatic port spreading`);
const expected = {top: [agent.cx, agent.y], bottom: [agent.cx, agent.y + agent.height],
left: [agent.x, agent.cy], right: [agent.x + agent.width, agent.cy]}[side];
const points = router.pathFor(edge).points;
assert.deepEqual(points[0], expected, edge.id);
for (let i = 1; i < points.length; i++) {
for (const box of boxes.values()) {
if ([edge.from, edge.to].includes(box.id)) continue;
assert(!segmentIntersectsRect({start: points[i - 1], end: points[i]}, box), `${edge.id} crosses ${box.id}`);
}
}
}
});
}