Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4e058684 | ||
|
|
63f38597a6 | ||
|
|
0248baa97b | ||
|
|
7e3696d2d7 | ||
|
|
15c5dcb31d | ||
|
|
64b2d6dae8 | ||
|
|
20e6f4899c | ||
|
|
2f68e60cec |
No files matched your search
@@ -30,11 +30,10 @@
|
|||||||
"col": 0,
|
"col": 0,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Understand today",
|
"label": "Understand today",
|
||||||
"sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.",
|
"sublabel": "Code quality and delivery processes understood.",
|
||||||
"tag": "Exit: Workflow understood",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460,
|
"height": 240,
|
||||||
"yOffset": -1000
|
"yOffset": -560
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "readiness",
|
"id": "readiness",
|
||||||
@@ -42,11 +41,10 @@
|
|||||||
"col": 0,
|
"col": 0,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Prepare foundations",
|
"label": "Prepare foundations",
|
||||||
"sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.",
|
"sublabel": "Critical pilot gaps closed.",
|
||||||
"tag": "Exit: Critical gaps closed",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460,
|
"height": 240,
|
||||||
"yOffset": -500
|
"yOffset": -280
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pilot",
|
"id": "pilot",
|
||||||
@@ -54,10 +52,9 @@
|
|||||||
"col": 0,
|
"col": 0,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Pilot",
|
"label": "Pilot",
|
||||||
"sublabel": "Try reviewed skills.\nOutput: results compared with the baseline.",
|
"sublabel": "AI skills tested against the baseline.",
|
||||||
"tag": "Exit: Continue, adjust or stop",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460,
|
"height": 240,
|
||||||
"yOffset": 0
|
"yOffset": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -66,11 +63,10 @@
|
|||||||
"col": 0,
|
"col": 0,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Expand",
|
"label": "Expand",
|
||||||
"sublabel": "Add a workflow or team.\nOutput: evidence for the next scope.",
|
"sublabel": "Next workflow justified by evidence.",
|
||||||
"tag": "Exit: Controls proven",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460,
|
"height": 240,
|
||||||
"yOffset": 500
|
"yOffset": 280
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sustain",
|
"id": "sustain",
|
||||||
@@ -78,11 +74,10 @@
|
|||||||
"col": 0,
|
"col": 0,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Sustain",
|
"label": "Sustain",
|
||||||
"sublabel": "Review and improve.\nOutput: maintained skills and refreshed backlog.",
|
"sublabel": "Skills reviewed and improved.",
|
||||||
"tag": "Exit: Reassess when changed",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460,
|
"height": 240,
|
||||||
"yOffset": 1000
|
"yOffset": 560
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"edges": [
|
"edges": [
|
||||||
|
|||||||
@@ -30,10 +30,9 @@
|
|||||||
"col": 0,
|
"col": 0,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Understand today",
|
"label": "Understand today",
|
||||||
"sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.",
|
"sublabel": "Code quality and delivery processes understood.",
|
||||||
"tag": "Exit: Workflow understood",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460
|
"height": 240
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "readiness",
|
"id": "readiness",
|
||||||
@@ -41,10 +40,9 @@
|
|||||||
"col": 1,
|
"col": 1,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Prepare foundations",
|
"label": "Prepare foundations",
|
||||||
"sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.",
|
"sublabel": "Critical pilot gaps closed.",
|
||||||
"tag": "Exit: Critical gaps closed",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460
|
"height": 240
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pilot",
|
"id": "pilot",
|
||||||
@@ -52,10 +50,9 @@
|
|||||||
"col": 2,
|
"col": 2,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Pilot",
|
"label": "Pilot",
|
||||||
"sublabel": "Try reviewed skills.\nOutput: results compared with the baseline.",
|
"sublabel": "AI skills tested against the baseline.",
|
||||||
"tag": "Exit: Continue, adjust or stop",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460
|
"height": 240
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "expansion",
|
"id": "expansion",
|
||||||
@@ -63,10 +60,9 @@
|
|||||||
"col": 3,
|
"col": 3,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Expand",
|
"label": "Expand",
|
||||||
"sublabel": "Add a workflow or team.\nOutput: evidence for the next scope.",
|
"sublabel": "Next workflow justified by evidence.",
|
||||||
"tag": "Exit: Controls proven",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460
|
"height": 240
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sustain",
|
"id": "sustain",
|
||||||
@@ -74,10 +70,9 @@
|
|||||||
"col": 4,
|
"col": 4,
|
||||||
"type": "backend",
|
"type": "backend",
|
||||||
"label": "Sustain",
|
"label": "Sustain",
|
||||||
"sublabel": "Review and improve.\nOutput: maintained skills and refreshed backlog.",
|
"sublabel": "Skills reviewed and improved.",
|
||||||
"tag": "Exit: Reassess when changed",
|
|
||||||
"width": 340,
|
"width": 340,
|
||||||
"height": 460
|
"height": 240
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"edges": [
|
"edges": [
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -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
@@ -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 && node scripts/browser/roadmap-timeline.cjs",
|
||||||
"kpi:example": "node scripts/ai-kpi.mjs"
|
"kpi:example": "node scripts/ai-kpi.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
const {chromium} = require('playwright');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const ids = ['assessment','readiness','pilot','expansion','sustain'];
|
||||||
|
(async () => {
|
||||||
|
const base = process.argv[2] || 'http://127.0.0.1:4173';
|
||||||
|
const output = `.cache/roadmap-timeline-${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 = [];
|
||||||
|
const inspect = async (frame, vertical) => {
|
||||||
|
await frame.locator('svg[role="img"]').waitFor();
|
||||||
|
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({
|
||||||
|
width:innerWidth, height:innerHeight,
|
||||||
|
cards:[...svg.querySelectorAll('[data-node-id]')].map(node => ({
|
||||||
|
id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(),
|
||||||
|
number:node.querySelector('[data-stage-number]').textContent,
|
||||||
|
numberColor:getComputedStyle(node.querySelector('[data-stage-number]')).fill,
|
||||||
|
titleColor:getComputedStyle(node.querySelector('[data-node-label]')).fill,
|
||||||
|
text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a}))
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
geometry.cards.sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x);
|
||||||
|
assert.deepEqual(geometry.cards.map(n => n.id), ids);
|
||||||
|
assert.deepEqual(geometry.cards.map(n => n.number), ['1','2','3','4','5']);
|
||||||
|
for (const [index, card] of geometry.cards.entries()) {
|
||||||
|
assert.equal(card.text.length, 3, 'number, title and one outcome only');
|
||||||
|
assert.equal(card.numberColor,card.titleColor,'stage number must inherit readable theme color');
|
||||||
|
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `outside iframe: ${card.id}`);
|
||||||
|
for (const text of card.text) {
|
||||||
|
assert(text.size >= 13, `small text: ${card.id} ${text.size}`);
|
||||||
|
assert(!/OUTPUT|Exit:/.test(text.value));
|
||||||
|
assert(text.box.left >= card.box.left && text.box.right <= card.box.right + 1 && text.box.top >= card.box.top && text.box.bottom <= card.box.bottom + 1, `clipped: ${card.id}`);
|
||||||
|
}
|
||||||
|
for (let t=1; t<card.text.length; t++) assert(card.text[t-1].box.bottom < card.text[t].box.top, `overlapping text: ${card.id}`);
|
||||||
|
if (index) {
|
||||||
|
const previous = geometry.cards[index-1].box;
|
||||||
|
if (vertical) {assert.equal(previous.x,card.box.x); assert(previous.bottom < card.box.top);}
|
||||||
|
else {assert.equal(previous.y,card.box.y); assert(previous.right < card.box.left);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return geometry;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
for (const width of [1440,1024,390,320]) {
|
||||||
|
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
|
||||||
|
try {
|
||||||
|
const page = await context.newPage();
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', error => errors.push(error.message));
|
||||||
|
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'})).status(),200);
|
||||||
|
const panel = page.locator('[data-diagram="strategy-roadmap"]');
|
||||||
|
const iframe = panel.locator(width < 1360 ? '.story-mobile' : '.story-desktop');
|
||||||
|
const frame = await (await iframe.elementHandle()).contentFrame();
|
||||||
|
await frame.locator('svg[role="img"]').waitFor();
|
||||||
|
const opener = panel.getByRole('button', {name:/^Expand /});
|
||||||
|
for (const theme of ['light','dark']) {
|
||||||
|
if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click();
|
||||||
|
await page.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
|
||||||
|
const inline = await inspect(frame,width < 1360);
|
||||||
|
await panel.screenshot({path:`${output}/${width}-${theme}-inline.png`});
|
||||||
|
await opener.focus(); await page.keyboard.press('Enter');
|
||||||
|
const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true});
|
||||||
|
await close.waitFor({state:'visible'});
|
||||||
|
const fullscreen = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
|
||||||
|
await fullscreen.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
|
||||||
|
const expanded = await inspect(fullscreen,width < 1000);
|
||||||
|
await page.screenshot({path:`${output}/${width}-${theme}-fullscreen.png`});
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
|
||||||
|
assert(await opener.evaluate(n => n === document.activeElement));
|
||||||
|
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
|
||||||
|
results.push({width,theme,inline,expanded});
|
||||||
|
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors,[]);
|
||||||
|
} finally {await context.close();}
|
||||||
|
}
|
||||||
|
assert.equal(results.length,8);
|
||||||
|
for (const viewport of [{width:390,height:667},{width:320,height:568}]) {
|
||||||
|
const context = await browser.newContext({viewport});
|
||||||
|
try {
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'});
|
||||||
|
const panel = page.locator('[data-diagram="strategy-roadmap"]');
|
||||||
|
const opener = panel.getByRole('button', {name:/^Expand /});
|
||||||
|
await opener.focus(); await page.keyboard.press('Enter');
|
||||||
|
const modal = panel.locator('dialog');
|
||||||
|
const close = modal.getByRole('button', {name:'Close full-screen diagram', exact:true});
|
||||||
|
await close.waitFor({state:'visible'});
|
||||||
|
const iframe = modal.locator('iframe');
|
||||||
|
const frame = await (await iframe.elementHandle()).contentFrame();
|
||||||
|
const geometry = await inspect(frame,true);
|
||||||
|
assert(await modal.evaluate(n => n.scrollHeight > n.clientHeight), 'short fullscreen must scroll');
|
||||||
|
for (const card of geometry.cards) {
|
||||||
|
await modal.evaluate((n, y) => {n.scrollTop = y;}, card.box.top);
|
||||||
|
const frameBox = await iframe.boundingBox();
|
||||||
|
const toolbarBox = await modal.locator('.archify-modal-toolbar').boundingBox();
|
||||||
|
const closeBox = await close.boundingBox();
|
||||||
|
assert(closeBox.y >= 0 && closeBox.y + closeBox.height <= viewport.height, 'close remains visible');
|
||||||
|
assert(frameBox.y + card.box.top >= toolbarBox.y + toolbarBox.height - 1 && frameBox.y + card.box.bottom <= viewport.height + 1, `stage unreachable: ${card.id}`);
|
||||||
|
}
|
||||||
|
// Controls stay reachable at the beginning of the scrollable viewer.
|
||||||
|
await modal.evaluate(n => {n.scrollTop = 0;});
|
||||||
|
for (const selector of ['#btn-theme','#btn-preset','#btn-export']) {
|
||||||
|
const control = frame.locator(selector);
|
||||||
|
const box = await control.boundingBox();
|
||||||
|
assert(box.x >= 0 && box.x + box.width <= viewport.width && box.y >= 0 && box.y + box.height <= viewport.height, `control outside viewport: ${selector}`);
|
||||||
|
await control.click();
|
||||||
|
if (selector !== '#btn-theme') await control.click();
|
||||||
|
}
|
||||||
|
await close.focus(); await page.keyboard.press('Escape');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
|
||||||
|
await iframe.waitFor({state:'detached'});
|
||||||
|
assert(await opener.evaluate(n => n === document.activeElement), 'Escape restores opener focus');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await close.waitFor({state:'visible'});
|
||||||
|
await close.focus(); await page.keyboard.press('Enter');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
|
||||||
|
assert(await opener.evaluate(n => n === document.activeElement), 'close button restores opener focus');
|
||||||
|
results.push({viewport,scrollable:geometry});
|
||||||
|
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
|
||||||
|
} finally {await context.close();}
|
||||||
|
}
|
||||||
|
assert.equal(results.length,10);
|
||||||
|
console.log('PASS 16 timeline contexts: 4 widths × 2 themes × inline/fullscreen; ordered readable text, no clipping, keyboard exit.');
|
||||||
|
console.log('PASS short fullscreen 390×667 and 320×568: readable stages reachable by scroll, sticky close, accessible controls, keyboard close and focus restoration.');
|
||||||
|
} finally {await browser.close();}
|
||||||
|
})().catch(error => {console.error(error); process.exitCode=1;});
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ const fs=require('node:fs');
|
|||||||
const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
|
const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
|
||||||
assert.equal(await close.innerText(),'×');assert(await close.evaluate(n=>n===document.activeElement));
|
assert.equal(await close.innerText(),'×');assert(await close.evaluate(n=>n===document.activeElement));
|
||||||
const box=await close.boundingBox();assert.equal(box.width,44);assert.equal(box.height,44);
|
const box=await close.boundingBox();assert.equal(box.width,44);assert.equal(box.height,44);
|
||||||
await close.click();assert(await opener.evaluate(n=>n===document.activeElement));
|
await close.click();await panel.locator('dialog iframe').waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
|
||||||
await opener.click();await close.waitFor({state:'visible'});await page.keyboard.press('Escape');await page.waitForFunction(()=>!document.querySelector('.archify-panel--story dialog').open);assert(await opener.evaluate(n=>n===document.activeElement));
|
await opener.click();await close.waitFor({state:'visible'});await page.keyboard.press('Escape');await page.waitForFunction(()=>!document.querySelector('.archify-panel--story dialog').open);
|
||||||
|
// Native close clears open before the queued React onClose restores focus.
|
||||||
|
// Wait for its unmount before focusing the next keyboard interaction target.
|
||||||
|
await panel.locator('dialog iframe').waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
|
||||||
if(id !== 'measurement') {
|
if(id !== 'measurement') {
|
||||||
const summary=page.locator('.strategy-content > details > summary').first();
|
const summary=page.locator('.strategy-content > details > summary').first();
|
||||||
await summary.focus();await page.keyboard.press('Enter');
|
await summary.focus();await page.keyboard.press('Enter');
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
const {chromium} = require('playwright');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const {pathToFileURL} = require('node:url');
|
||||||
|
const {resolve} = require('node:path');
|
||||||
|
|
||||||
|
// Inspect the rendered paths, not just the authored JSON. Also runs on downloaded SVGs.
|
||||||
|
function inspect({mobile}) {
|
||||||
|
const svg = document.querySelector('svg[role="img"]');
|
||||||
|
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 phases = ['scope','architecture','ui-ux','planning','delivery','uat','security','releases'];
|
||||||
|
const edges = [...svg.querySelectorAll('path[data-edge-id]')];
|
||||||
|
if (edges.length !== 24) errors.push(`expected 24 graph edges, got ${edges.length}`);
|
||||||
|
phases.forEach((phase, index) => {
|
||||||
|
for (const [id, from, to] of [
|
||||||
|
[`govern-${phase}`,'agent',phase],
|
||||||
|
[`stage-${index+1}`,phase,phases[(index+1)%phases.length]],
|
||||||
|
[`contribute-${phase}`,`code-${phase}`,phase],
|
||||||
|
]) {
|
||||||
|
const matches = edges.filter(p => p.dataset.edgeId === id);
|
||||||
|
if (matches.length !== 1 || matches[0].dataset.edgeFrom !== from || matches[0].dataset.edgeTo !== to || !matches[0].getAttribute('marker-end') || matches[0].getAttribute('marker-start')) errors.push(`${id}: graph or direction changed`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// CTMs put boxes and paths in the same viewport coordinate system.
|
||||||
|
// 0.01 covers floating-point error against bounds, NOT source spreading:
|
||||||
|
// shared endpoints below must be exactly identical.
|
||||||
|
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();
|
||||||
|
const target = boxes.find(n => n.id === id);
|
||||||
|
const destination = mobile ? [target.x,target.cy]
|
||||||
|
: side === 'top' ? [target.cx,target.bottom] : side === 'bottom' ? [target.cx,target.y]
|
||||||
|
: side === 'left' ? [target.right,target.cy] : [target.x,target.cy];
|
||||||
|
const end = point(path,path.getPointAtLength(length));
|
||||||
|
if (Math.hypot(end.x-destination[0],end.y-destination[1]) > 0.01) errors.push(`${id}: destination midpoint changed`);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const group of mobile ? [phases] : [['scope','architecture','releases'],['planning','delivery','uat']]) {
|
||||||
|
const first = endpoints[group[0]];
|
||||||
|
if (group.some(id => !endpoints[id] || endpoints[id][0] !== first?.[0] || endpoints[id][1] !== first?.[1])) errors.push(`source-port spreading: ${group.join(',')}`);
|
||||||
|
}
|
||||||
|
const lifecycle = [];
|
||||||
|
if (!mobile) {
|
||||||
|
for (const [id, x, y1, y2, peer] of [
|
||||||
|
['stage-2',850,262.5,457.5,'architecture'], ['stage-3',850,532.5,727.5,'planning'],
|
||||||
|
['stage-6',350,727.5,532.5,'uat'], ['stage-7',350,457.5,262.5,'releases'],
|
||||||
|
]) {
|
||||||
|
const path = svg.querySelector(`path[data-edge-id="${id}"]`);
|
||||||
|
if (!path) {errors.push(`${id}: missing lifecycle path`); continue;}
|
||||||
|
const length = path.getTotalLength();
|
||||||
|
const a = path.getPointAtLength(0), b = path.getPointAtLength(length);
|
||||||
|
// Local SVG coordinates test exact offsets; transformed coordinates test
|
||||||
|
// node clearance at any inline/fullscreen/export scale.
|
||||||
|
if (Math.hypot(a.x-x,a.y-y1) > 0.01 || Math.hypot(b.x-x,b.y-y2) > 0.01) errors.push(`${id}: wrong shifted endpoints`);
|
||||||
|
if (!path.getAttribute('marker-end')) errors.push(`${id}: missing direction`);
|
||||||
|
const governance = paths.find(p => p.dataset.edgeTo === peer);
|
||||||
|
if (!governance) {errors.push(`${id}: missing governance peer ${peer}`); continue;}
|
||||||
|
const end = governance.getPointAtLength(governance.getTotalLength());
|
||||||
|
if (Math.abs(Math.abs(a.x-end.x)-20) > 0.01) errors.push(`${id}: wrong attachment clearance`);
|
||||||
|
for (let distance = 1; distance < length; distance += 2) {
|
||||||
|
const local = path.getPointAtLength(distance), p = point(path,local);
|
||||||
|
if (Math.abs(local.x-x) > 0.01) errors.push(`${id}: channel not straight`);
|
||||||
|
for (const box of boxes) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No positive-length collinear overlap with ANY governance segment.
|
||||||
|
// Perpendicular crossings remain legitimate in this fixed radial layout.
|
||||||
|
for (const govern of paths) {
|
||||||
|
const total = govern.getTotalLength();
|
||||||
|
for (let d = 0; d < total - 1; d += 1) {
|
||||||
|
const p = govern.getPointAtLength(d), q = govern.getPointAtLength(d+1);
|
||||||
|
if (Math.abs(p.x-x) < 0.01 && Math.abs(q.x-x) < 0.01 && Math.min(Math.max(p.y,q.y),Math.max(y1,y2)) - Math.max(Math.min(p.y,q.y),Math.min(y1,y2)) > 0.01) errors.push(`${id}: overlaps ${govern.dataset.edgeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycle.push({id,start:[a.x,a.y],end:[b.x,b.y],attachmentClearance:Math.abs(a.x-end.x)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {errors:[...new Set(errors)],endpoints,lifecycle};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectTypography({mobile}) {
|
||||||
|
const errors = [];
|
||||||
|
const nodes = [...document.querySelectorAll('[data-node-id]')].map(node => {
|
||||||
|
const box = node.querySelector('rect').getBoundingClientRect();
|
||||||
|
const texts = [...node.querySelectorAll(':scope > text')];
|
||||||
|
const bounds = texts.map(text => text.getBoundingClientRect());
|
||||||
|
texts.forEach((text, index) => {
|
||||||
|
const expected = text.hasAttribute('data-node-label') ? (mobile ? 38 : 27) : (mobile ? 28 : 21);
|
||||||
|
if (parseFloat(getComputedStyle(text).fontSize) !== expected) errors.push(`${node.id}: wrong text size`);
|
||||||
|
const b = bounds[index];
|
||||||
|
if (b.width <= 0 || b.height <= 0 || b.x < box.x - 1 || b.right > box.right + 1 || b.y < box.y - 1 || b.bottom > box.bottom + 1) errors.push(`${node.id}: text outside node`);
|
||||||
|
});
|
||||||
|
if (bounds.length === 2 && bounds[0].bottom > bounds[1].top) errors.push(`${node.id}: title/subtitle overlap`);
|
||||||
|
return {id:node.id, text:texts.map(t => t.textContent), sizes:texts.map(t => getComputedStyle(t).fontSize)};
|
||||||
|
});
|
||||||
|
if (nodes.length !== 17) errors.push(`expected 17 nodes, got ${nodes.length}`);
|
||||||
|
return {errors, nodes};
|
||||||
|
}
|
||||||
|
|
||||||
|
(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});
|
||||||
|
const exported = [];
|
||||||
|
for (const theme of ['light','dark']) {
|
||||||
|
if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click();
|
||||||
|
await frame.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
|
||||||
|
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}-${theme}.svg`;
|
||||||
|
await download.saveAs(path);
|
||||||
|
// A separate SVG document has no viewer CSS to hide export regressions.
|
||||||
|
const standalone = await browser.newPage({viewport:{width:1440,height:1100},colorScheme:theme});
|
||||||
|
await standalone.goto(pathToFileURL(resolve(path)).href);
|
||||||
|
assert.equal(await standalone.evaluate(() => document.contentType),'image/svg+xml');
|
||||||
|
await standalone.evaluate(() => document.fonts.ready);
|
||||||
|
const geometry = await standalone.evaluate(inspect,{mobile:width < 700});
|
||||||
|
const typography = await standalone.evaluate(inspectTypography,{mobile:width < 700});
|
||||||
|
assert.deepEqual(geometry.errors,[]);
|
||||||
|
assert.deepEqual(typography.errors,[]);
|
||||||
|
const background = await standalone.locator('rect.c-bg-rect').evaluate(n => getComputedStyle(n).fill);
|
||||||
|
if (exported.length) assert.notEqual(background,exported[0].background,'standalone theme must change');
|
||||||
|
// CDP captures SVG documents without Playwright's HTML-body screenshot wait.
|
||||||
|
const client = await standalone.context().newCDPSession(standalone);
|
||||||
|
const size = await standalone.evaluate(() => ({width:document.documentElement.width.baseVal.value,height:document.documentElement.height.baseVal.value}));
|
||||||
|
const image = await client.send('Page.captureScreenshot',{format:'png',captureBeyondViewport:true,clip:{x:0,y:0,...size,scale:1}});
|
||||||
|
fs.writeFileSync(`${output}/${width}-${theme}-standalone.png`,Buffer.from(image.data,'base64'));
|
||||||
|
exported.push({theme,background,geometry,typography});
|
||||||
|
await standalone.close();
|
||||||
|
}
|
||||||
|
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: 3 viewports × inline/fullscreen; standalone SVG geometry and typography in light/dark');
|
||||||
|
} finally {await browser.close();}
|
||||||
|
})().catch(error => {console.error(error);process.exitCode=1;});
|
||||||
@@ -2,6 +2,7 @@ import {readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renam
|
|||||||
import {join} from 'node:path';
|
import {join} from 'node:path';
|
||||||
import {execFileSync} from 'node:child_process';
|
import {execFileSync} from 'node:child_process';
|
||||||
import {roadmapViewer} from './roadmap-viewer.mjs';
|
import {roadmapViewer} from './roadmap-viewer.mjs';
|
||||||
|
import {targetRouting} from './target-routing.mjs';
|
||||||
import {createHash} from 'node:crypto';
|
import {createHash} from 'node:crypto';
|
||||||
const cli = 'vendor/archify/bin/archify.mjs';
|
const cli = 'vendor/archify/bin/archify.mjs';
|
||||||
const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']);
|
const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']);
|
||||||
@@ -39,6 +40,7 @@ for (const file of readdirSync('diagrams').sort()) {
|
|||||||
}
|
}
|
||||||
// Verify the upstream artifact first, then add the scoped portal integration.
|
// Verify the upstream artifact first, then add the scoped portal integration.
|
||||||
let viewer = readFileSync(output, 'utf8');
|
let viewer = readFileSync(output, 'utf8');
|
||||||
|
if (name === 'strategy-target') viewer = targetRouting(viewer);
|
||||||
// Upstream intentionally disables preset selection in gallery embeds. Enable
|
// Upstream intentionally disables preset selection in gallery embeds. Enable
|
||||||
// only those two guards; fail closed if a future vendor update changes them.
|
// only those two guards; fail closed if a future vendor update changes them.
|
||||||
const start = viewer.indexOf('Archify.preset = (function () {');
|
const start = viewer.indexOf('Archify.preset = (function () {');
|
||||||
@@ -66,7 +68,16 @@ for (const file of readdirSync('diagrams').sort()) {
|
|||||||
const checked = JSON.parse(execFileSync(process.execPath, [cli,'check',output,'--json'], {encoding:'utf8', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}));
|
const checked = JSON.parse(execFileSync(process.execPath, [cli,'check',output,'--json'], {encoding:'utf8', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}));
|
||||||
if (!checked.ok || checked.composition?.status !== 'pass') throw new Error('Roadmap final artifact failed');
|
if (!checked.ok || checked.composition?.status !== 'pass') throw new Error('Roadmap final artifact failed');
|
||||||
writeFileSync(`.cache/archify/${name}.portal.json`, JSON.stringify({source:input, sourceSha256:hash(input), adapterSha256:hash('scripts/roadmap-viewer.mjs'), artifactSha256:hash(output), checker:checked}, null, 2));
|
writeFileSync(`.cache/archify/${name}.portal.json`, JSON.stringify({source:input, sourceSha256:hash(input), adapterSha256:hash('scripts/roadmap-viewer.mjs'), artifactSha256:hash(output), checker:checked}, null, 2));
|
||||||
} else renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`);
|
} else {
|
||||||
|
renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`);
|
||||||
|
if (name === 'strategy-target') {
|
||||||
|
// The native receipt describes the PRE-integration artifact only.
|
||||||
|
const hash = path => createHash('sha256').update(readFileSync(path)).digest('hex');
|
||||||
|
const checked = JSON.parse(execFileSync(process.execPath, [cli,'check',output,'--json'], {encoding:'utf8', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}));
|
||||||
|
if (!checked.ok || checked.composition?.status !== 'pass') throw new Error('Target final artifact failed');
|
||||||
|
writeFileSync(`.cache/archify/${name}.portal.json`, JSON.stringify({source:input, sourceSha256:hash(input), adapterSha256:hash('scripts/target-routing.mjs'), artifactSha256:hash(output), nativeReceiptScope:'before portal integration', checker:checked}, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt');
|
copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt');
|
||||||
copyFileSync('vendor/archify/THIRD_PARTY_NOTICES.md','static/diagrams/THIRD-PARTY-NOTICES.md');
|
copyFileSync('vendor/archify/THIRD_PARTY_NOTICES.md','static/diagrams/THIRD-PARTY-NOTICES.md');
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
font-size: var(--cycle-label);
|
font-size: var(--cycle-label);
|
||||||
transform: translateY(-6px);
|
transform: translateY(-6px);
|
||||||
}
|
}
|
||||||
.diagram-container > svg text[data-detail="context"] {
|
/* Export strips data-detail; node ownership and the muted class survive. */
|
||||||
|
.diagram-container > svg [data-node-id] > text.t-muted {
|
||||||
font-size: var(--cycle-detail);
|
font-size: var(--cycle-detail);
|
||||||
transform: translateY(16px);
|
transform: translateY(16px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,23 +20,19 @@ export function roadmapViewer(html, spec) {
|
|||||||
if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`);
|
if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`);
|
||||||
const original = html.slice(start, end);
|
const original = html.slice(start, end);
|
||||||
const rect = /<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.]+)" height="([\d.]+)"/.exec(original);
|
const rect = /<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.]+)" height="([\d.]+)"/.exec(original);
|
||||||
if (!rect || (original.match(/<text /g) || []).length !== 3) throw new Error(`Roadmap text contract drift: ${node.id}`);
|
if (!rect || (original.match(/<text /g) || []).length !== 2) throw new Error(`Roadmap text contract drift: ${node.id}`);
|
||||||
const [x,y,w,h] = rect.slice(1).map(Number);
|
const [x,y,w,h] = rect.slice(1).map(Number);
|
||||||
boxes.push({x,y,w,h});
|
boxes.push({x,y,w,h});
|
||||||
const text = (value, top, size, cls, attrs = 'data-detail="context"') => {
|
const text = (value, top, size, cls, attrs = 'data-detail="context"') => {
|
||||||
const lines = wrap(value, size === 30 ? 18 : 21);
|
const lines = wrap(value, size === 30 ? 18 : 21);
|
||||||
const maxLines = top === 166 || top === 310 ? 3 : 2;
|
const maxLines = top === 160 ? 3 : 2;
|
||||||
if (lines.length > maxLines || lines.some(line => line.length > 21)) throw new Error(`Roadmap copy exceeds card capacity: ${node.id}`);
|
if (lines.length > maxLines || lines.some(line => line.length > 21)) throw new Error(`Roadmap copy exceeds card capacity: ${node.id}`);
|
||||||
return `<text x="${x+24}" y="${y+top}" font-size="${size}" class="${cls}" ${attrs}>${lines.map((line,i) => `<tspan x="${x+24}" dy="${i ? 34 : 0}">${escape(line)}</tspan>`).join('')}</text>`;
|
return `<text x="${x+24}" y="${y+top}" font-size="${size}" class="${cls}" ${attrs}>${lines.map((line,i) => `<tspan x="${x+24}" dy="${i ? 34 : 0}">${escape(line)}</tspan>`).join('')}</text>`;
|
||||||
};
|
};
|
||||||
const [action, output] = node.sublabel.split('\nOutput: ');
|
if (!node.sublabel || node.tag || /Output:|Exit:|\n/.test(node.sublabel)) throw new Error(`Roadmap content contract drift: ${node.id}`);
|
||||||
if (!action || !output || !node.tag.startsWith('Exit: ')) throw new Error(`Roadmap content contract drift: ${node.id}`);
|
const content = text(String(index+1), 40, 26, 't-primary', 'data-stage-number="" font-weight="700"')
|
||||||
const content = text(String(index+1).padStart(2,'0'), 40, 26, 't-muted')
|
|
||||||
+ text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"')
|
+ text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"')
|
||||||
+ text(action, 166, 26, 't-primary')
|
+ text(node.sublabel, 160, 26, 't-muted');
|
||||||
+ text('OUTPUT', 274, 20, 't-muted', 'font-weight="700"')
|
|
||||||
+ text(output, 310, 26, 't-primary')
|
|
||||||
+ text(node.tag, 416, 24, 't-primary');
|
|
||||||
// Preserve native node title/metadata and rectangles; replace sigil/text only.
|
// Preserve native node title/metadata and rectangles; replace sigil/text only.
|
||||||
// Sigils are nested SVG groups. Anchor on the two native node rectangles.
|
// Sigils are nested SVG groups. Anchor on the two native node rectangles.
|
||||||
const rectangles = [...original.matchAll(/<rect\b[^>]*\/>/g)];
|
const rectangles = [...original.matchAll(/<rect\b[^>]*\/>/g)];
|
||||||
@@ -58,7 +54,8 @@ export function roadmapViewer(html, spec) {
|
|||||||
[data-node-id] text {font-family: Arial, sans-serif;}
|
[data-node-id] text {font-family: Arial, sans-serif;}
|
||||||
svg[data-preset="classic"] [data-node-id] > rect.c-backend,
|
svg[data-preset="classic"] [data-node-id] > rect.c-backend,
|
||||||
svg[data-preset="editorial"] [data-node-id] > rect.c-backend {fill:var(--panel); stroke:var(--border);}
|
svg[data-preset="editorial"] [data-node-id] > rect.c-backend {fill:var(--panel); stroke:var(--border);}
|
||||||
svg[data-preset="classic"] .c-grid {opacity:0;}
|
.c-grid {opacity:0;}
|
||||||
|
|
||||||
</style>`;
|
</style>`;
|
||||||
html = html.replace(roots[0][0], root + styles);
|
html = html.replace(roots[0][0], root + styles);
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ for (const suffix of ['', '-mobile']) test(`native Workflow${suffix} retains gra
|
|||||||
assert.equal(checked.composition.metrics.labelCanvasOverflowIssues, 0);
|
assert.equal(checked.composition.metrics.labelCanvasOverflowIssues, 0);
|
||||||
assert.throws(() => roadmapViewer('', spec), /drift/);
|
assert.throws(() => roadmapViewer('', spec), /drift/);
|
||||||
const oversized = structuredClone(spec);
|
const oversized = structuredClone(spec);
|
||||||
oversized.nodes[0].sublabel = 'Too much copy '.repeat(20) + '\nOutput: baseline';
|
oversized.nodes[0].sublabel = 'Too much copy '.repeat(20);
|
||||||
assert.throws(() => roadmapViewer(native, oversized), /capacity/);
|
assert.throws(() => roadmapViewer(native, oversized), /capacity/);
|
||||||
assert.throws(() => roadmapViewer(native, {...spec,diagram_type:'architecture'}), /contract drift/);
|
assert.throws(() => roadmapViewer(native, {...spec,diagram_type:'architecture'}), /contract drift/);
|
||||||
} finally {rmSync(dir, {recursive:true, force:true});}
|
} finally {rmSync(dir, {recursive:true, force:true});}
|
||||||
|
|||||||
@@ -26,10 +26,9 @@ test('both roadmap diagrams make assessment and readiness prerequisites to the p
|
|||||||
assert.deepEqual(diagram.nodes.map(n => n.id), ids);
|
assert.deepEqual(diagram.nodes.map(n => n.id), ids);
|
||||||
assert.deepEqual(diagram.nodes.map(n => n.label), ['Understand today', 'Prepare foundations', 'Pilot', 'Expand', 'Sustain']);
|
assert.deepEqual(diagram.nodes.map(n => n.label), ['Understand today', 'Prepare foundations', 'Pilot', 'Expand', 'Sustain']);
|
||||||
assert.deepEqual(diagram.edges.map(e => [e.from,e.to]), ids.slice(1).map((id, i) => [ids[i],id]));
|
assert.deepEqual(diagram.edges.map(e => [e.from,e.to]), ids.slice(1).map((id, i) => [ids[i],id]));
|
||||||
assert.match(diagram.nodes[0].sublabel, /code quality and real delivery/);
|
assert.equal(diagram.nodes[0].sublabel, 'Code quality and delivery processes understood.');
|
||||||
assert.match(diagram.nodes[0].sublabel, /baseline, process map, prioritized gaps/);
|
assert.equal(diagram.nodes[1].sublabel, 'Critical pilot gaps closed.');
|
||||||
assert.match(diagram.nodes[1].tag, /Critical gaps closed/);
|
assert(diagram.nodes.every(n => !n.sublabel.includes('Output:') && !n.tag && n.height === 240));
|
||||||
assert(diagram.nodes.every(n => n.sublabel.includes('Output:') && n.tag.startsWith('Exit:')));
|
|
||||||
assert.deepEqual(diagram.nodes.map(n => n.col), suffix ? [0,0,0,0,0] : [0,1,2,3,4]);
|
assert.deepEqual(diagram.nodes.map(n => n.col), suffix ? [0,0,0,0,0] : [0,1,2,3,4]);
|
||||||
assert.equal(diagram.lanes.length, 1);
|
assert.equal(diagram.lanes.length, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Owned desktop-only integration, applied AFTER native artifact verification.
|
||||||
|
// Native architecture JSON has no per-edge attachment offset. Keep the central
|
||||||
|
// governance ports native; shift only these lifecycle channels on the same sides.
|
||||||
|
export const targetLifecycleOffsets = [
|
||||||
|
{id:'stage-2', from:'architecture', to:'ui-ux', x:870, shiftedX:850, y1:262.5, y2:457.5},
|
||||||
|
{id:'stage-3', from:'ui-ux', to:'planning', x:870, shiftedX:850, y1:532.5, y2:727.5},
|
||||||
|
{id:'stage-6', from:'uat', to:'security', x:330, shiftedX:350, y1:727.5, y2:532.5},
|
||||||
|
{id:'stage-7', from:'security', to:'releases', x:330, shiftedX:350, y1:457.5, y2:262.5},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function targetRouting(viewer) {
|
||||||
|
for (const {id, from, to, x, shiftedX, y1, y2} of targetLifecycleOffsets) {
|
||||||
|
const oldPath = `M ${x} ${y1} L ${x} ${y2}`;
|
||||||
|
const newPath = `M ${shiftedX} ${y1} L ${shiftedX} ${y2}`;
|
||||||
|
const oldPoints = `${x},${y1};${x},${y2}`;
|
||||||
|
const newPoints = `${shiftedX},${y1};${shiftedX},${y2}`;
|
||||||
|
// Match the whole native crossover group: its mask must move with the edge.
|
||||||
|
// Trace/export use this actual path; composition checks use its point list.
|
||||||
|
const groups = [...viewer.matchAll(/<g data-graph-role="automatic-crossover"[^>]*>\s*<path\b[^>]*\/>\s*<path\b[^>]*\/>\s*<\/g>/g)]
|
||||||
|
.filter(([group]) => group.includes(`data-edge-id="${id}"`));
|
||||||
|
if (groups.length !== 1) throw new Error(`Target routing drift: ${id} group`);
|
||||||
|
const group = groups[0][0];
|
||||||
|
for (const attribute of [`data-edge-from="${from}"`, `data-edge-to="${to}"`, `data-composition-points="${oldPoints}"`, 'data-graph-role="automatic-crossover-underlay"']) {
|
||||||
|
if (!group.includes(attribute)) throw new Error(`Target routing drift: ${id} ${attribute}`);
|
||||||
|
}
|
||||||
|
if (group.split(`d="${oldPath}"`).length !== 3) throw new Error(`Target routing drift: ${id} paths`);
|
||||||
|
const shifted = group.replaceAll(`d="${oldPath}"`, `d="${newPath}"`)
|
||||||
|
.replace(`data-composition-points="${oldPoints}"`, `data-composition-points="${newPoints}"`);
|
||||||
|
viewer = viewer.replace(group, shifted);
|
||||||
|
}
|
||||||
|
return viewer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {test} from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {targetRouting, targetLifecycleOffsets} from './target-routing.mjs';
|
||||||
|
|
||||||
|
const fixture = targetLifecycleOffsets.map(({id, from, to, x, y1, y2}) => `<g data-graph-role="automatic-crossover" style="--step:9">
|
||||||
|
<path data-graph-role="automatic-crossover-underlay" d="M ${x} ${y1} L ${x} ${y2}"/>
|
||||||
|
<path data-edge-from="${from}" data-edge-to="${to}" data-edge-id="${id}" data-composition-points="${x},${y1};${x},${y2}" d="M ${x} ${y1} L ${x} ${y2}" marker-end="url(#arrowhead-emphasis)"/>
|
||||||
|
</g>`).join('\n');
|
||||||
|
|
||||||
|
test('Target adapter shifts precisely four paths, masks and composition points', () => {
|
||||||
|
const unrelated = '<path data-edge-id="govern-architecture" d="M 600 450 L 870 262.5"/><g data-node-id="agent"/>';
|
||||||
|
const output = targetRouting(fixture + unrelated);
|
||||||
|
assert(output.endsWith(unrelated));
|
||||||
|
let restored = output;
|
||||||
|
for (const {x, shiftedX, y1, y2} of targetLifecycleOffsets) {
|
||||||
|
assert.equal(output.split(`d="M ${shiftedX} ${y1} L ${shiftedX} ${y2}"`).length, 3);
|
||||||
|
assert(output.includes(`data-composition-points="${shiftedX},${y1};${shiftedX},${y2}"`));
|
||||||
|
restored = restored.replaceAll(`d="M ${shiftedX} ${y1} L ${shiftedX} ${y2}"`, `d="M ${x} ${y1} L ${x} ${y2}"`)
|
||||||
|
.replace(`data-composition-points="${shiftedX},${y1};${shiftedX},${y2}"`, `data-composition-points="${x},${y1};${x},${y2}"`);
|
||||||
|
}
|
||||||
|
assert.equal(restored, fixture + unrelated, 'all other bytes preserved');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Target adapter fails closed on missing, duplicate, moved or changed native edges', () => {
|
||||||
|
for (const changed of [fixture.replace('stage-2', 'missing'), fixture + fixture,
|
||||||
|
fixture.replace('870 262.5', '871 262.5'), fixture.replace('870,262.5', '871,262.5'),
|
||||||
|
fixture.replace('data-edge-from="architecture"', 'data-edge-from="scope"'),
|
||||||
|
fixture.replace('automatic-crossover-underlay', 'unknown')]) {
|
||||||
|
assert.throws(() => targetRouting(changed), /Target routing drift/);
|
||||||
|
}
|
||||||
|
assert.throws(() => targetRouting(targetRouting(fixture)), /Target routing drift/);
|
||||||
|
});
|
||||||
@@ -11,6 +11,15 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
|
|||||||
const opener = useRef<HTMLButtonElement>(null);
|
const opener = useRef<HTMLButtonElement>(null);
|
||||||
const headingId = useId();
|
const headingId = useId();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [narrowRoadmap, setNarrowRoadmap] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (name !== 'strategy-roadmap') return;
|
||||||
|
const media = window.matchMedia('(max-width: 999px)');
|
||||||
|
const update = () => setNarrowRoadmap(media.matches);
|
||||||
|
update();
|
||||||
|
media.addEventListener('change', update);
|
||||||
|
return () => media.removeEventListener('change', update);
|
||||||
|
}, [name]);
|
||||||
const {colorMode, setColorMode} = useColorMode();
|
const {colorMode, setColorMode} = useColorMode();
|
||||||
const initialTheme = useRef(colorMode);
|
const initialTheme = useRef(colorMode);
|
||||||
const desktop = useRef<HTMLIFrameElement>(null);
|
const desktop = useRef<HTMLIFrameElement>(null);
|
||||||
@@ -65,7 +74,7 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
|
|||||||
</> : <iframe ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />}
|
</> : <iframe ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />}
|
||||||
<dialog ref={dialog} className="archify-modal" aria-labelledby={headingId} onClose={() => {setExpanded(false); opener.current?.focus();}}>
|
<dialog ref={dialog} className="archify-modal" aria-labelledby={headingId} onClose={() => {setExpanded(false); opener.current?.focus();}}>
|
||||||
<div className="archify-modal-toolbar"><strong id={headingId}>{title}</strong><button type="button" onClick={close} autoFocus aria-label="Close full-screen diagram"><span aria-hidden="true">×</span></button></div>
|
<div className="archify-modal-toolbar"><strong id={headingId}>{title}</strong><button type="button" onClick={close} autoFocus aria-label="Close full-screen diagram"><span aria-hidden="true">×</span></button></div>
|
||||||
{expanded && <iframe ref={fullscreen} onLoad={() => syncTheme(fullscreen.current)} src={`${source}?theme=${initialTheme.current}`} title={`${title} — expanded`} sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" />}
|
{expanded && <iframe ref={fullscreen} onLoad={() => syncTheme(fullscreen.current)} src={`${narrowRoadmap ? mobileSource : source}?${name === 'strategy-roadmap' ? 'embed=1&' : ''}theme=${initialTheme.current}`} title={`${title} — expanded`} sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" />}
|
||||||
</dialog>
|
</dialog>
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
+10
-3
@@ -112,7 +112,7 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
|
|||||||
.archify-panel--story > .story-mobile {display: none;}
|
.archify-panel--story > .story-mobile {display: none;}
|
||||||
.theme-doc-markdown:has([data-diagram="strategy-roadmap"]) {max-width: none;}
|
.theme-doc-markdown:has([data-diagram="strategy-roadmap"]) {max-width: none;}
|
||||||
.archify-panel--story[data-diagram="strategy-roadmap"] {max-width: none;}
|
.archify-panel--story[data-diagram="strategy-roadmap"] {max-width: none;}
|
||||||
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-desktop {aspect-ratio: auto; height: 360px;}
|
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-desktop {aspect-ratio: auto; height: 240px;}
|
||||||
.archify-panel--story[data-diagram="strategy-target"] > iframe {aspect-ratio: 1620 / 1430;}
|
.archify-panel--story[data-diagram="strategy-target"] > iframe {aspect-ratio: 1620 / 1430;}
|
||||||
.archify-panel--story[data-diagram="strategy-adoption"] > iframe {aspect-ratio: 545 / 408;}
|
.archify-panel--story[data-diagram="strategy-adoption"] > iframe {aspect-ratio: 545 / 408;}
|
||||||
.archify-panel--story[data-diagram="strategy-maintenance"] > iframe {aspect-ratio: 524 / 408;}
|
.archify-panel--story[data-diagram="strategy-maintenance"] > iframe {aspect-ratio: 524 / 408;}
|
||||||
@@ -124,7 +124,7 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
|
|||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.archify-panel--story > .story-desktop {display: none;}
|
.archify-panel--story > .story-desktop {display: none;}
|
||||||
.archify-panel--story > .story-mobile {display: block; height: auto; aspect-ratio: 240 / 748;}
|
.archify-panel--story > .story-mobile {display: block; height: auto; aspect-ratio: 240 / 748;}
|
||||||
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {aspect-ratio: 364 / 2600;}
|
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {aspect-ratio: 364 / 1480;}
|
||||||
.archify-panel--story[data-diagram="strategy-adoption"] > .story-mobile {aspect-ratio: 305 / 748;}
|
.archify-panel--story[data-diagram="strategy-adoption"] > .story-mobile {aspect-ratio: 305 / 748;}
|
||||||
.archify-panel--story[data-diagram="strategy-maintenance"] > .story-mobile {aspect-ratio: 284 / 748;}
|
.archify-panel--story[data-diagram="strategy-maintenance"] > .story-mobile {aspect-ratio: 284 / 748;}
|
||||||
.archify-panel--story[data-diagram="strategy-target"] > .story-mobile {aspect-ratio: 680 / 2340;}
|
.archify-panel--story[data-diagram="strategy-target"] > .story-mobile {aspect-ratio: 680 / 2340;}
|
||||||
@@ -132,7 +132,7 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
|
|||||||
/* Five Roadmap cards need desktop space, including room for the sidebar. */
|
/* Five Roadmap cards need desktop space, including room for the sidebar. */
|
||||||
@media (width < 1360px) {
|
@media (width < 1360px) {
|
||||||
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-desktop {display: none;}
|
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-desktop {display: none;}
|
||||||
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {display: block; height: auto; aspect-ratio: 364 / 2600;}
|
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {display: block; height: auto; width: 100%; max-width: 364px; margin-inline: auto; aspect-ratio: 364 / 1480;}
|
||||||
}
|
}
|
||||||
.archify-expand {display: grid; place-items: center; width: 44px; height: 44px; border: 0; padding: 0; background: transparent; color: var(--ifm-link-color); cursor: pointer;}
|
.archify-expand {display: grid; place-items: center; width: 44px; height: 44px; border: 0; padding: 0; background: transparent; color: var(--ifm-link-color); cursor: pointer;}
|
||||||
.archify-expand:hover {background: var(--ifm-hover-overlay);}
|
.archify-expand:hover {background: var(--ifm-hover-overlay);}
|
||||||
@@ -142,4 +142,11 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
|
|||||||
.archify-modal-toolbar {display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 20px; border-bottom: 1px solid var(--line);}
|
.archify-modal-toolbar {display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 20px; border-bottom: 1px solid var(--line);}
|
||||||
.archify-modal-toolbar button {display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 44px; height: 44px; padding: 0; background: var(--gold); color: #14131a; font: inherit; font-size: 24px; font-weight: 700; border: 0; cursor: pointer;}
|
.archify-modal-toolbar button {display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 44px; height: 44px; padding: 0; background: var(--gold); color: #14131a; font: inherit; font-size: 24px; font-weight: 700; border: 0; cursor: pointer;}
|
||||||
.archify-panel .archify-modal iframe {flex: 1; min-height: 0; height: 100%; width: 100%;}
|
.archify-panel .archify-modal iframe {flex: 1; min-height: 0; height: 100%; width: 100%;}
|
||||||
|
/* Read the five-stage narrow Roadmap by scrolling, never by fitting its
|
||||||
|
entire height into a short phone screen. Other diagram viewers are unchanged. */
|
||||||
|
@media (max-width: 999px) {
|
||||||
|
[data-diagram="strategy-roadmap"] .archify-modal {overflow-y: auto; overscroll-behavior: contain;}
|
||||||
|
[data-diagram="strategy-roadmap"] .archify-modal-toolbar {position: sticky; top: 0; z-index: 1; flex-shrink: 0; background: var(--surface);}
|
||||||
|
[data-diagram="strategy-roadmap"] .archify-modal iframe {flex: none; height: 1480px; min-height: 1480px;}
|
||||||
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {.start-card {transition: none;} .start-card:hover {transform: none;}}
|
@media (prefers-reduced-motion: reduce) {.start-card {transition: none;} .start-card:hover {transform: none;}}
|
||||||
Reference in new issue
Block a user