Compare commits

...
Author SHA1 Message Date
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
jarvis-at-skic e149131307 docs: redesign Roadmap as a native five-stage workflow
Build and publish Eldorado documentation / build (pull_request) Successful in 39s
2026-10-05 19:35:14 +00:00
jarvis-at-skic 2d63313180 Merge pull request 'docs: start Roadmap with code-quality and process assessment' (#23) from docs/assessment-first-roadmap into main
Build and publish Eldorado documentation / build (push) Successful in 41s
2026-10-05 11:51:54 -07:00
18 changed files with 508 additions and 256 deletions

No files matched your search

@@ -1,110 +0,0 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "Recommended roadmap: assess before adoption",
"output": "static/diagrams/strategy-roadmap-mobile/index.html",
"quality_profile": "showcase",
"legend": {
"mode": "hidden"
}
},
"components": [
{
"id": "assessment",
"type": "backend",
"label": "1 · Assess current state",
"sublabel": "Code quality + delivery flow",
"pos": [
20,
20
],
"size": [
180,
60
]
},
{
"id": "readiness",
"type": "backend",
"label": "2 · Improve readiness",
"sublabel": "Critical gaps + safe scope",
"pos": [
20,
140
],
"size": [
180,
60
]
},
{
"id": "pilot",
"type": "backend",
"label": "3 · Bounded pilot",
"sublabel": "Compare accepted outcomes",
"pos": [
20,
260
],
"size": [
180,
60
]
},
{
"id": "expansion",
"type": "backend",
"label": "4 · Expand on evidence",
"sublabel": "Controls + support proven",
"pos": [
20,
380
],
"size": [
180,
60
]
},
{
"id": "sustain",
"type": "backend",
"label": "5 · Sustain & improve",
"sublabel": "Monitor + reassess",
"pos": [
20,
500
],
"size": [
180,
60
]
}
],
"connections": [
{
"id": "flow-0",
"from": "assessment",
"to": "readiness",
"label": "Gaps"
},
{
"id": "flow-1",
"from": "readiness",
"to": "pilot",
"label": "Ready"
},
{
"id": "flow-2",
"from": "pilot",
"to": "expansion",
"label": "Evidence"
},
{
"id": "flow-3",
"from": "expansion",
"to": "sustain",
"label": "Review"
}
]
}
@@ -0,0 +1,122 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Recommended adoption roadmap",
"output": "static/diagrams/strategy-roadmap-mobile/index.html",
"quality_profile": "showcase",
"animation": "none",
"legend": {
"mode": "hidden"
}
},
"lanes": [
{
"id": "adoption",
"label": "Recommended sequence"
}
],
"mainPath": [
"assessment",
"readiness",
"pilot",
"expansion",
"sustain"
],
"nodes": [
{
"id": "assessment",
"lane": "adoption",
"col": 0,
"type": "backend",
"label": "Understand today",
"sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.",
"tag": "Exit: Workflow understood",
"width": 340,
"height": 460,
"yOffset": -1000
},
{
"id": "readiness",
"lane": "adoption",
"col": 0,
"type": "backend",
"label": "Prepare foundations",
"sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.",
"tag": "Exit: Critical gaps closed",
"width": 340,
"height": 460,
"yOffset": -500
},
{
"id": "pilot",
"lane": "adoption",
"col": 0,
"type": "backend",
"label": "Pilot",
"sublabel": "Try reviewed skills.\nOutput: results compared with the baseline.",
"tag": "Exit: Continue, adjust or stop",
"width": 340,
"height": 460,
"yOffset": 0
},
{
"id": "expansion",
"lane": "adoption",
"col": 0,
"type": "backend",
"label": "Expand",
"sublabel": "Add a workflow or team.\nOutput: evidence for the next scope.",
"tag": "Exit: Controls proven",
"width": 340,
"height": 460,
"yOffset": 500
},
{
"id": "sustain",
"lane": "adoption",
"col": 0,
"type": "backend",
"label": "Sustain",
"sublabel": "Review and improve.\nOutput: maintained skills and refreshed backlog.",
"tag": "Exit: Reassess when changed",
"width": 340,
"height": 460,
"yOffset": 1000
}
],
"edges": [
{
"id": "flow-0",
"from": "assessment",
"to": "readiness",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-1",
"from": "readiness",
"to": "pilot",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-2",
"from": "pilot",
"to": "expansion",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-3",
"from": "expansion",
"to": "sustain",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
}
]
}
-110
View File
@@ -1,110 +0,0 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "Recommended roadmap: assess before adoption",
"output": "static/diagrams/strategy-roadmap/index.html",
"quality_profile": "showcase",
"legend": {
"mode": "hidden"
}
},
"components": [
{
"id": "assessment",
"type": "backend",
"label": "1 · Assess current state",
"sublabel": "Code quality + delivery flow",
"pos": [
20,
20
],
"size": [
180,
60
]
},
{
"id": "readiness",
"type": "backend",
"label": "2 · Improve readiness",
"sublabel": "Critical gaps + safe scope",
"pos": [
260,
20
],
"size": [
180,
60
]
},
{
"id": "pilot",
"type": "backend",
"label": "3 · Bounded pilot",
"sublabel": "Compare accepted outcomes",
"pos": [
260,
150
],
"size": [
180,
60
]
},
{
"id": "expansion",
"type": "backend",
"label": "4 · Expand on evidence",
"sublabel": "Controls + support proven",
"pos": [
20,
150
],
"size": [
180,
60
]
},
{
"id": "sustain",
"type": "backend",
"label": "5 · Sustain & improve",
"sublabel": "Monitor + reassess",
"pos": [
20,
280
],
"size": [
180,
60
]
}
],
"connections": [
{
"id": "flow-0",
"from": "assessment",
"to": "readiness",
"label": "Gaps"
},
{
"id": "flow-1",
"from": "readiness",
"to": "pilot",
"label": "Ready"
},
{
"id": "flow-2",
"from": "pilot",
"to": "expansion",
"label": "Evidence"
},
{
"id": "flow-3",
"from": "expansion",
"to": "sustain",
"label": "Review"
}
]
}
+117
View File
@@ -0,0 +1,117 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Recommended adoption roadmap",
"output": "static/diagrams/strategy-roadmap/index.html",
"quality_profile": "showcase",
"animation": "none",
"legend": {
"mode": "hidden"
}
},
"lanes": [
{
"id": "adoption",
"label": "Recommended sequence"
}
],
"mainPath": [
"assessment",
"readiness",
"pilot",
"expansion",
"sustain"
],
"nodes": [
{
"id": "assessment",
"lane": "adoption",
"col": 0,
"type": "backend",
"label": "Understand today",
"sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.",
"tag": "Exit: Workflow understood",
"width": 340,
"height": 460
},
{
"id": "readiness",
"lane": "adoption",
"col": 1,
"type": "backend",
"label": "Prepare foundations",
"sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.",
"tag": "Exit: Critical gaps closed",
"width": 340,
"height": 460
},
{
"id": "pilot",
"lane": "adoption",
"col": 2,
"type": "backend",
"label": "Pilot",
"sublabel": "Try reviewed skills.\nOutput: results compared with the baseline.",
"tag": "Exit: Continue, adjust or stop",
"width": 340,
"height": 460
},
{
"id": "expansion",
"lane": "adoption",
"col": 3,
"type": "backend",
"label": "Expand",
"sublabel": "Add a workflow or team.\nOutput: evidence for the next scope.",
"tag": "Exit: Controls proven",
"width": 340,
"height": 460
},
{
"id": "sustain",
"lane": "adoption",
"col": 4,
"type": "backend",
"label": "Sustain",
"sublabel": "Review and improve.\nOutput: maintained skills and refreshed backlog.",
"tag": "Exit: Reassess when changed",
"width": 340,
"height": 460
}
],
"edges": [
{
"id": "flow-0",
"from": "assessment",
"to": "readiness",
"role": "main",
"fromSide": "right",
"toSide": "left"
},
{
"id": "flow-1",
"from": "readiness",
"to": "pilot",
"role": "main",
"fromSide": "right",
"toSide": "left"
},
{
"id": "flow-2",
"from": "pilot",
"to": "expansion",
"role": "main",
"fromSide": "right",
"toSide": "left"
},
{
"id": "flow-3",
"from": "expansion",
"to": "sustain",
"role": "main",
"fromSide": "right",
"toSide": "left"
}
]
}
@@ -12,14 +12,14 @@
{"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":"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-architecture","type":"frontend","label":"Code 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-planning","type":"frontend","label":"Code 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-uat","type":"frontend","label":"Code 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-releases","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1980],"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":"AI Agent","sublabel":"Human-controlled","pos":[400,660],"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":"AI Agent","sublabel":"Human-controlled","pos":[400,1100],"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":"AI Agent","sublabel":"Human-controlled","pos":[400,1540],"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":"AI Agent","sublabel":"Human-controlled","pos":[400,1980],"size":[240,100]}
],
"connections": [
+8 -8
View File
@@ -12,14 +12,14 @@
{"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":"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-architecture","type":"frontend","label":"Code 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-planning","type":"frontend","label":"Code 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-uat","type":"frontend","label":"Code 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-releases","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[240,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":"AI Agent","sublabel":"Human-controlled","pos":[780,37.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":"AI Agent","sublabel":"Human-controlled","pos":[780,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":"AI Agent","sublabel":"Human-controlled","pos":[240,877.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":"AI Agent","sublabel":"Human-controlled","pos":[240,37.5],"size":[180,75]}
],
"connections": [
+3 -1
View File
@@ -15,7 +15,7 @@ description: Author typed JSON diagrams and embed their local interactive viewer
| Dataflow | Sources, processing and destinations |
| Lifecycle | States and transitions |
The standalone viewer provides pan/zoom, node inspection, search/focus, relationship tracing, light/dark themes, and an export menu. Motion is opt-in. The Organization views exercise architecture mode; other diagram types use the same build interface but do not yet have portal-specific examples. Browser-dependent exports such as video are not acceptance-tested by this integration.
The standalone viewer provides pan/zoom, node inspection, search/focus, relationship tracing, light/dark themes, and an export menu. Motion is opt-in. The Organization views exercise architecture mode; Roadmap uses native Workflow v2 with a horizontal desktop path and the same stages stacked on mobile. Other diagram types use the same build interface but do not yet have portal-specific examples. Browser-dependent exports such as video are not acceptance-tested by this integration.
## Author a diagram
@@ -23,6 +23,8 @@ Create `diagrams/<name>.<type>.json` using the matching schema and authoring gui
Set `meta.output` to `static/diagrams/<name>/index.html`. The build validates the typed source with the upstream showcase profile, renders locally, checks the artifact, and publishes the standalone HTML. Editable JSON stays in the repository; the portal does not publish source-download files.
Roadmap has a scoped multiline typography adapter in `scripts/roadmap-viewer.mjs`. Its two `.workflow.json` sources use the native v2 schema, one lane, `mainPath`, logical columns and native routes. The native renderer validates the schema and solves geometry; the adapter wraps the existing action/output/exit copy, removes the redundant lane frame and trims empty canvas. The final artifact then passes the upstream showcase composition checker. This path records source, adapter and final artifact hashes in private `.cache/archify/*.portal.json` receipts, not upstream `deliver` provenance. Keep the vendor unchanged and run the Roadmap browser check at 1440, 390 and 320 pixels after any copy or sizing change. Typography is embedded in the SVG so exports retain it.
```sh
pnpm diagrams:build
pnpm test
+1 -1
View File
@@ -11,7 +11,7 @@ import ArchifyDiagram from '@site/src/components/ArchifyDiagram';
Understand today’s code and how work really reaches production before introducing AI into the workflow. Fix what would make a pilot unsafe or impossible to evaluate—not every item of technical debt.
<ArchifyDiagram name="strategy-roadmap" title="Assess code and delivery, improve readiness, pilot, expand on evidence, then sustain" story />
<ArchifyDiagram name="strategy-roadmap" title="Understand today → Prepare foundations → Pilot → Expand → Sustain" story />
**Recommended sequence · Target operating model, not an approved schedule or deployed governance.**
+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 />
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.
+82 -4
View File
@@ -8,6 +8,50 @@ const fs = require('node:fs');
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
const results = [];
try {
// Intermediate widths must use the readable vertical Roadmap, not shrink five cards.
for (const width of [768, 1024, 1280]) {
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));
const response = await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'});
assert.equal(response.status(), 200);
const panel = page.locator('.archify-panel--story[data-diagram="strategy-roadmap"]');
const iframe = panel.locator('.story-mobile');
assert(await iframe.isVisible(), `${width}: vertical Roadmap iframe must be visible`);
assert(!await panel.locator('.story-desktop').isVisible(), `${width}: horizontal Roadmap must be hidden`);
const frame = await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({
width:innerWidth, height:innerHeight,
overflow:document.documentElement.scrollWidth > innerWidth || document.documentElement.scrollHeight > innerHeight,
nodes:[...svg.querySelectorAll('[data-node-id]')].map(node => ({
id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(),
text:[...node.querySelectorAll('text')].map(t => ({text:t.textContent, box:t.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a}))
})).sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x)
}));
assert.deepEqual(geometry.nodes.map(n => n.id), ['assessment','readiness','pilot','expansion','sustain']);
for (const [index, card] of geometry.nodes.entries()) {
assert(card.text.length > 0, `${width}: missing text in ${card.id}`);
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `${width}: card outside iframe: ${card.id}`);
for (const t of card.text) {
assert(t.box.left >= card.box.left && t.box.right <= card.box.right + 1 && t.box.top >= card.box.top && t.box.bottom <= card.box.bottom + 1, `${width}: clipped ${card.id} ${t.text}`);
assert(t.size >= 11, `${width}: small ${card.id} ${t.text}: ${t.size}`);
}
if (index) {
const previous = geometry.nodes[index - 1].box;
assert.equal(previous.x, card.box.x);
assert(previous.bottom < card.box.top, `${width}: stages must flow vertically`);
}
}
assert(!geometry.overflow, `${width}: iframe overflow`);
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), `${width}: page overflow`);
assert.deepEqual(errors, []);
results.push({width, geometry, responsiveReadability:true});
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
} finally {await context.close();}
}
for (const width of [1440, 390, 320]) {
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
const page = await context.newPage();
@@ -31,28 +75,61 @@ const fs = require('node:fs');
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const checkCards = async () => {
const cards = await frame.locator('svg[role="img"] [data-node-id]').evaluateAll(nodes => nodes.map(node => {
const box = node.querySelector('rect').getBoundingClientRect();
return {id:node.dataset.nodeId, box:box.toJSON(), text:[...node.querySelectorAll('text')].map(t => ({text:t.textContent, box:t.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a}))};
}));
for (const card of cards) for (const t of card.text) {
assert(t.box.left >= card.box.left && t.box.right <= card.box.right + 1 && t.box.top >= card.box.top && t.box.bottom <= card.box.bottom + 1, `${width}: clipped ${card.id} ${t.text}`);
assert(t.size >= 11, `${width}: small ${card.id} ${t.text}: ${t.size}`);
}
return cards;
};
await checkCards();
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({
nodes:[...svg.querySelectorAll('[data-node-id]')].map(n => ({id:n.dataset.nodeId,...n.getBoundingClientRect().toJSON()})),
nodes:[...svg.querySelectorAll('[data-node-id]')].map(n => ({id:n.dataset.nodeId,...n.getBoundingClientRect().toJSON()})).sort((a,b) => a.y-b.y || a.x-b.x),
width:innerWidth, height:innerHeight,
minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))
}));
assert.deepEqual(geometry.nodes.map(n => n.id), ['assessment','readiness','pilot','expansion','sustain']);
for (const n of geometry.nodes) assert(n.x >= 0 && n.y >= 0 && n.right <= geometry.width + 1 && n.bottom <= geometry.height + 1);
assert(geometry.minLabel >= 10, `readable node labels: ${geometry.minLabel}`);
for (let i=1; i<geometry.nodes.length; i++) {
const a=geometry.nodes[i-1], b=geometry.nodes[i];
if(width > 700) {assert.equal(a.y,b.y); assert(a.right < b.x);}
else {assert.equal(a.x,b.x); assert(a.bottom < b.y);}
}
// Portal -> diagram and diagram -> portal are both part of the contract.
if (width <= 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
for (let attempt=0; attempt<3; attempt++) {
await page.getByRole('button', {name:/Switch between dark and light mode/}).filter({visible:true}).click();
if (await page.locator('html').getAttribute('data-theme') === 'dark') break;
}
if (width <= 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
await frame.waitForFunction(() => document.documentElement.dataset.theme === 'dark');
await frame.locator('#btn-theme').click();
await page.waitForFunction(() => document.documentElement.dataset.theme === 'light');
await frame.locator('#btn-theme').click();
await page.waitForFunction(() => document.documentElement.dataset.theme === 'dark');
await checkCards();
await frame.locator('#btn-theme').click();
await page.waitForFunction(() => document.documentElement.dataset.theme === 'light');
for (const preset of ['blueprint','classic']) {
await frame.locator('#btn-preset').click();
await frame.locator(`[data-preset-value="${preset}"]`).click();
assert.equal(await frame.locator('html').getAttribute('data-preset'), preset);
await checkCards();
}
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);
await download.saveAs(`${output}/${width}.svg`);
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Assess current state'));
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Understand today'));
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('eldorado-roadmap-typography'));
for (const id of ['assessment','readiness','pilot','expansion','sustain']) {
await frame.locator(`svg[role="img"] [data-node-id="${id}"]`).screenshot({path:`${output}/${width}-${id}-card.png`});
}
const opener = panel.getByRole('button', {name:/^Expand /});
await opener.click();
const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true});
@@ -67,8 +144,9 @@ const fs = require('node:fs');
await headings.first().scrollIntoViewIfNeeded();
await page.screenshot({path:`${output}/${width}-assessment.png`});
await page.screenshot({path:`${output}/${width}-full.png`, fullPage:true});
await page.locator('.strategy-content summary').click();
assert(await page.locator('.strategy-content details').evaluate(n => n.open));
await page.locator('.strategy-content summary').focus();
await page.keyboard.press('Enter');
await page.waitForFunction(() => document.querySelector('.strategy-content details').open);
await page.locator('.strategy-content details p').last().waitFor({state:'visible'});
assert((await page.locator('.strategy-content details').innerText()).includes('exact-artifact approvals'));
const links = await page.locator('.strategy-content a').evaluateAll(nodes => nodes.filter(n => !n.classList.contains('hash-link')).map(n => n.href));
+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(metrics.edges.every(e=>e[2]?.startsWith('url(')), 'all stages must have arrowheads');
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.equal(await frame.locator('[data-node-id="agent"] text').allTextContents().then(texts=>texts.includes('Human Controlled')), true);
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.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));
await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));
await page.waitForFunction(()=>scrollY===0);
+22 -4
View File
@@ -1,6 +1,8 @@
import {readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, rmSync} from 'node:fs';
import {join} from 'node:path';
import {execFileSync} from 'node:child_process';
import {roadmapViewer} from './roadmap-viewer.mjs';
import {createHash} from 'node:crypto';
const cli = 'vendor/archify/bin/archify.mjs';
const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']);
const inlineStyles = readFileSync(new URL('./inline-viewer.css', import.meta.url), 'utf8');
@@ -23,9 +25,18 @@ for (const file of readdirSync('diagrams').sort()) {
mkdirSync(`static/diagrams/${name}`, {recursive: true});
if (spec.diagram_type !== type || spec.meta.output !== output) throw new Error(`Diagram type/output mismatch: ${file}`);
const run = args => execFileSync(process.execPath,[cli,...args],{stdio:'inherit',env:{...process.env,ARCHIFY_UPDATE_CHECK_DISABLED:'1'}});
run(['validate',type,input,'--quality','showcase','--json']);
run(['deliver',type,input,output,'--quality','showcase','--json']);
run(['check',output,'--json','--require-provenance']);
const roadmap = ['strategy-roadmap', 'strategy-roadmap-mobile'].includes(name);
if (roadmap) {
// Native renderer validates schema and compiles layout. Check readability
// after multiline typography, not upstream's single-line preview.
execFileSync(process.execPath, ['vendor/archify/renderers/workflow/render-workflow.mjs', input, output], {stdio:'inherit', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}});
writeFileSync(output, roadmapViewer(readFileSync(output, 'utf8'), spec));
run(['check',output,'--json']);
} else {
run(['validate',type,input,'--quality','showcase','--json']);
run(['deliver',type,input,output,'--quality','showcase','--json']);
run(['check',output,'--json','--require-provenance']);
}
// Verify the upstream artifact first, then add the scoped portal integration.
let viewer = readFileSync(output, 'utf8');
// Upstream intentionally disables preset selection in gallery embeds. Enable
@@ -48,7 +59,14 @@ for (const file of readdirSync('diagrams').sort()) {
}
writeFileSync(output, viewer.replace('</head>', `<style id="eldorado-inline-controls">${inlineStyles}</style><style id="eldorado-cycle-labels">${storyStyles}</style></head>`)
.replace('</body>', `<script>${themeBridge}</script></body>`));
renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`);
if (roadmap) {
// This is a portal receipt, not upstream deliver provenance: formatting
// happens between the native schema/layout compiler and artifact checker.
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('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));
} else renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`);
}
copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt');
copyFileSync('vendor/archify/THIRD_PARTY_NOTICES.md','static/diagrams/THIRD-PARTY-NOTICES.md');
+65
View File
@@ -0,0 +1,65 @@
// Native Workflow v2 owns node geometry, mainPath and routing. The portal adds
// multiline typography: upstream's three single-line labels are too small for
// a five-stage reading view. Do not change the pinned vendor or graph semantics.
const escape = text => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
const wrap = (text, limit = 21) => {
const lines = [];
for (const word of text.split(/\s+/)) {
if (!lines.length || `${lines.at(-1)} ${word}`.length > limit) lines.push(word);
else lines[lines.length - 1] += ` ${word}`;
}
return lines;
};
export function roadmapViewer(html, spec) {
if (spec.diagram_type !== 'workflow' || spec.schema_version !== 2 || spec.nodes.length !== 5) throw new Error('Roadmap workflow contract drift');
const boxes = [];
for (const [index, node] of spec.nodes.entries()) {
const start = html.indexOf(`<g id="node-${node.id}"`);
const next = html.indexOf('<text data-node-label=', start);
const end = html.indexOf('</g>', next) + 4;
if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`);
const original = html.slice(start, end);
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}`);
const [x,y,w,h] = rect.slice(1).map(Number);
boxes.push({x,y,w,h});
const text = (value, top, size, cls, attrs = 'data-detail="context"') => {
const lines = wrap(value, size === 30 ? 18 : 21);
const maxLines = top === 166 || top === 310 ? 3 : 2;
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>`;
};
const [action, output] = node.sublabel.split('\nOutput: ');
if (!action || !output || !node.tag.startsWith('Exit: ')) throw new Error(`Roadmap content contract drift: ${node.id}`);
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(action, 166, 26, 't-primary')
+ 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.
// Sigils are nested SVG groups. Anchor on the two native node rectangles.
const rectangles = [...original.matchAll(/<rect\b[^>]*\/>/g)];
if (rectangles.length !== 2) throw new Error('Roadmap shape contract drift');
const prefixEnd = rectangles[1].index + rectangles[1][0].length;
const replacement = original.slice(0,prefixEnd).replaceAll('rx="6"', 'rx="0"') + content + '</g>';
html = html.slice(0,start) + replacement + html.slice(end);
}
const left = Math.min(...boxes.map(b => b.x)) - 12;
const top = Math.min(...boxes.map(b => b.y)) - 12;
const width = Math.max(...boxes.map(b => b.x+b.w)) - left + 12;
const height = Math.max(...boxes.map(b => b.y+b.h)) - top + 12;
// One editorial lane; its title/frame would repeat the page's framing.
html = html.replace(/<rect\b[^>]*data-composition-frame-kind="lane"[^>]*\/>\s*<text\b[^>]*>[\s\S]*?<\/text>/g, '');
const roots = [...html.matchAll(/<svg\b[^>]*role="img"[^>]*>/g)];
if (roots.length !== 1) throw new Error('Roadmap SVG contract drift');
const root = roots[0][0].replace(/viewBox="[^"]*"/, `viewBox="${left} ${top} ${width} ${height}"`);
const styles = `<style id="eldorado-roadmap-typography">
[data-node-id] text {font-family: Arial, sans-serif;}
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="classic"] .c-grid {opacity:0;}
</style>`;
html = html.replace(roots[0][0], root + styles);
return html;
}
+35
View File
@@ -0,0 +1,35 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync} from 'node:fs';
import {execFileSync} from 'node:child_process';
import {roadmapViewer} from './roadmap-viewer.mjs';
for (const suffix of ['', '-mobile']) test(`native Workflow${suffix} retains graph and passes final composition`, () => {
mkdirSync('.cache', {recursive:true});
const dir = mkdtempSync('.cache/roadmap-test-');
try {
const source = `diagrams/strategy-roadmap${suffix}.workflow.json`;
const spec = JSON.parse(readFileSync(source, 'utf8'));
const output = `${dir}/index.html`;
execFileSync(process.execPath, ['vendor/archify/renderers/workflow/render-workflow.mjs', source, output], {env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}});
const native = readFileSync(output, 'utf8');
const styled = roadmapViewer(native, spec);
const graph = html => [...html.matchAll(/<path\b[^>]*data-edge-id="[^"]+"[^>]*>/g)].map(m => m[0]);
assert.equal(graph(native).length, 4);
assert.deepEqual(graph(styled), graph(native), 'native paths must not be rewritten');
for (const node of spec.nodes) assert(styled.includes(`data-node-id="${node.id}"`));
assert.equal((styled.match(/<text data-node-label=/g) || []).length, 0, 'single-line preview replaced');
assert(styled.includes('eldorado-roadmap-typography'));
writeFileSync(output, styled);
const checked = JSON.parse(execFileSync(process.execPath, ['vendor/archify/bin/archify.mjs','check',output,'--json'], {encoding:'utf8', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}));
assert(checked.ok);
assert.equal(checked.composition.status, 'pass');
assert.equal(checked.composition.metrics.properCrossings, 0);
assert.equal(checked.composition.metrics.labelCanvasOverflowIssues, 0);
assert.throws(() => roadmapViewer('', spec), /drift/);
const oversized = structuredClone(spec);
oversized.nodes[0].sublabel = 'Too much copy '.repeat(20) + '\nOutput: baseline';
assert.throws(() => roadmapViewer(native, oversized), /capacity/);
assert.throws(() => roadmapViewer(native, {...spec,diagram_type:'architecture'}), /contract drift/);
} finally {rmSync(dir, {recursive:true, force:true});}
});
+13 -6
View File
@@ -19,11 +19,18 @@ test('Roadmap visibly assesses code and real delivery before adoption', () => {
test('both roadmap diagrams make assessment and readiness prerequisites to the pilot', () => {
const ids = ['assessment', 'readiness', 'pilot', 'expansion', 'sustain'];
for (const suffix of ['', '-mobile']) {
const diagram = JSON.parse(read(`diagrams/strategy-roadmap${suffix}.architecture.json`));
assert.deepEqual(diagram.components.map(n => n.id), ids);
assert.deepEqual(diagram.connections.map(e => [e.from,e.to]), ids.slice(1).map((id, i) => [ids[i],id]));
assert.match(diagram.components[0].sublabel, /Code quality \+ delivery flow/);
assert.match(diagram.components[1].sublabel, /Critical gaps/);
assert.match(diagram.components[4].label, /Sustain/);
const diagram = JSON.parse(read(`diagrams/strategy-roadmap${suffix}.workflow.json`));
assert.equal(diagram.schema_version, 2);
assert.equal(diagram.diagram_type, 'workflow');
assert.deepEqual(diagram.mainPath, 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.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.match(diagram.nodes[0].sublabel, /baseline, process map, prioritized gaps/);
assert.match(diagram.nodes[1].tag, /Critical gaps closed/);
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.equal(diagram.lanes.length, 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('direct, guide and review'));
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(!/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);
const contributors = d.components.filter(n => n.id.startsWith('code-'));
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) {
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'));
+7
View File
@@ -26,6 +26,13 @@ test('mobile story diagrams preserve the same nodes and directed relationships',
for (const {id} of model.pages) {
const page = read(`docs/strategy/${id}.mdx`);
const name = /<ArchifyDiagram name="([^"]+)"[^>]+ story/.exec(page)[1];
if (name === 'strategy-roadmap') {
const desktop = JSON.parse(read(`diagrams/${name}.workflow.json`));
const mobile = JSON.parse(read(`diagrams/${name}-mobile.workflow.json`));
const semantic = d => ({nodes:d.nodes.map(({col,yOffset,...n}) => n), edges:d.edges.map(({fromSide,toSide,...e}) => e)});
assert.deepEqual(semantic(desktop), semantic(mobile));
continue;
}
const desktop = JSON.parse(read(`diagrams/${name}.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);
+9 -1
View File
@@ -110,6 +110,9 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
.archify-panel--story {max-width: 740px;}
.archify-panel--story > iframe {height: auto; aspect-ratio: 480 / 408;}
.archify-panel--story > .story-mobile {display: 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"] > .story-desktop {aspect-ratio: auto; height: 360px;}
.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-maintenance"] > iframe {aspect-ratio: 524 / 408;}
@@ -121,11 +124,16 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
@media (max-width: 700px) {
.archify-panel--story > .story-desktop {display: none;}
.archify-panel--story > .story-mobile {display: block; height: auto; aspect-ratio: 240 / 748;}
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {aspect-ratio: 240 / 628;}
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {aspect-ratio: 364 / 2600;}
.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-target"] > .story-mobile {aspect-ratio: 680 / 2340;}
}
/* Five Roadmap cards need desktop space, including room for the sidebar. */
@media (width < 1360px) {
.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-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-modal {position: fixed; inset: 0; margin: 0; padding: 0; width: 100vw; height: 100dvh; max-width: none; max-height: none; border: 0; background: var(--surface); color: var(--ifm-color-content);}