Compare commits

...
Author SHA1 Message Date
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
jarvis-at-skic 1de7d16d09 docs: start roadmap with code and process assessment
Build and publish Eldorado documentation / build (pull_request) Successful in 40s
2026-10-05 18:50:28 +00:00
jarvis-at-skic b679e6b2b5 Merge pull request 'docs: make Skills Measurement an adoption collection plan' (#22) from docs/skills-measurement-plan into main
Build and publish Eldorado documentation / build (push) Successful in 40s
2026-10-05 11:36:20 -07:00
16 changed files with 630 additions and 272 deletions

No files matched your search

@@ -1,110 +0,0 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "TO-BE: Evidence-gated rollout",
"output": "static/diagrams/strategy-roadmap-mobile/index.html",
"quality_profile": "showcase",
"legend": {
"mode": "hidden"
}
},
"components": [
{
"id": "foundation",
"type": "backend",
"label": "1 \u00b7 Foundation",
"sublabel": "People + data + baseline",
"pos": [
20,
20
],
"size": [
180,
60
]
},
{
"id": "pilot",
"type": "backend",
"label": "2 \u00b7 Skills pilot",
"sublabel": "Qualified, useful work",
"pos": [
20,
140
],
"size": [
180,
60
]
},
{
"id": "distribution",
"type": "backend",
"label": "3 \u00b7 Distribution",
"sublabel": "Revocation + recovery",
"pos": [
20,
260
],
"size": [
180,
60
]
},
{
"id": "coordination",
"type": "backend",
"label": "4 \u00b7 Coordination",
"sublabel": "Human takeover proven",
"pos": [
20,
380
],
"size": [
180,
60
]
},
{
"id": "expansion",
"type": "backend",
"label": "5 \u00b7 Departments",
"sublabel": "Outcomes + cost + risk",
"pos": [
20,
500
],
"size": [
180,
60
]
}
],
"connections": [
{
"id": "flow-0",
"from": "foundation",
"to": "pilot",
"label": "Charter"
},
{
"id": "flow-1",
"from": "pilot",
"to": "distribution",
"label": "quality + coverage"
},
{
"id": "flow-2",
"from": "distribution",
"to": "coordination",
"label": "Controls"
},
{
"id": "flow-3",
"from": "coordination",
"to": "expansion",
"label": "authority drills pass"
}
]
}
@@ -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": "TO-BE: Evidence-gated rollout",
"output": "static/diagrams/strategy-roadmap/index.html",
"quality_profile": "showcase",
"legend": {
"mode": "hidden"
}
},
"components": [
{
"id": "foundation",
"type": "backend",
"label": "1 \u00b7 Foundation",
"sublabel": "People + data + baseline",
"pos": [
20,
20
],
"size": [
180,
60
]
},
{
"id": "pilot",
"type": "backend",
"label": "2 \u00b7 Skills pilot",
"sublabel": "Qualified, useful work",
"pos": [
260,
20
],
"size": [
180,
60
]
},
{
"id": "distribution",
"type": "backend",
"label": "3 \u00b7 Distribution",
"sublabel": "Revocation + recovery",
"pos": [
260,
150
],
"size": [
180,
60
]
},
{
"id": "coordination",
"type": "backend",
"label": "4 \u00b7 Coordination",
"sublabel": "Human takeover proven",
"pos": [
20,
150
],
"size": [
180,
60
]
},
{
"id": "expansion",
"type": "backend",
"label": "5 \u00b7 Departments",
"sublabel": "Outcomes + cost + risk",
"pos": [
20,
280
],
"size": [
180,
60
]
}
],
"connections": [
{
"id": "flow-0",
"from": "foundation",
"to": "pilot",
"label": "Charter"
},
{
"id": "flow-1",
"from": "pilot",
"to": "distribution",
"label": "quality + coverage"
},
{
"id": "flow-2",
"from": "distribution",
"to": "coordination",
"label": "Controls"
},
{
"id": "flow-3",
"from": "coordination",
"to": "expansion",
"label": "authority drills pass"
}
]
}
+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"
}
]
}
+3 -1
View File
@@ -15,7 +15,7 @@ description: Author typed JSON diagrams and embed their local interactive viewer
| Dataflow | Sources, processing and destinations | | Dataflow | Sources, processing and destinations |
| Lifecycle | States and transitions | | 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 ## 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. 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 ```sh
pnpm diagrams:build pnpm diagrams:build
pnpm test pnpm test
+45 -41
View File
@@ -1,7 +1,7 @@
--- ---
id: roadmap id: roadmap
title: Roadmap title: Roadmap
description: Evidence-gated phases, no promised dates or budgets. description: Assess current code quality and delivery processes, close critical gaps, then pilot, expand and sustain AI adoption.
hide_table_of_contents: true hide_table_of_contents: true
--- ---
@@ -9,64 +9,68 @@ import ArchifyDiagram from '@site/src/components/ArchifyDiagram';
<div className="strategy-content"> <div className="strategy-content">
**TO-BE · Target operating model — not installed controls or company results.** 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.
## What must be true before widening the rollout? <ArchifyDiagram name="strategy-roadmap" title="Understand today → Prepare foundations → Pilot → Expand → Sustain" story />
Advance by exit evidence, not promised dates or usage volume. Each stage can hold, narrow or stop; changed client, data or authority boundaries reopen earlier gates. **Recommended sequence · Target operating model, not an approved schedule or deployed governance.**
<ArchifyDiagram name="strategy-roadmap" title="Foundation, pilot, reliable distribution, coordination and departmental expansion have exit gates" story /> ## 1. Understand code quality and delivery
- **Start bounded:** named people, lawful data handling and a reproducible baseline precede the pilot. Inspect representative repositories across important services and change types: tests and CI reliability, maintainability, technical debt and security findings. Trace recent changes through the actual SDLC with the people doing the work—from request to production and feedback. Check ownership, review and release practices, handoffs, rework and bottlenecks; do not assume the documented process is the real one.
- **Prove recovery before coordination:** verified versions, revocation and human takeover are prerequisites.
- **Expand by department:** accepted outcomes, guardrails, total cost and experience justify continuation; sensitive decisions remain human. **Output:** baseline findings, a process map and prioritized gaps. Use these to identify a low-risk, repeatable workflow with observable acceptance—not simply the easiest repository.
## 2. Fix critical readiness gaps
Resolve gaps that threaten safe execution or a trustworthy comparison: for example, exposed secrets, unclear approval authority or unreliable checks needed to accept the pilot’s changes. Establish accountable people, data boundaries and recovery for the selected scope. Broader refactoring and non-critical debt can run alongside the pilot with a separate improvement backlog.
**Output:** a readiness decision and bounded pilot charter. Proceed only when the critical gaps for that workflow are closed; otherwise narrow the scope or hold.
## 3. Run a bounded pilot
Try a small set of reviewed skills in the selected workflow, with human review and no new privileged autonomous actions. Compare equivalent work with the baseline: accepted outcomes, escaped defects, full cost and verification effort. [Skills Measurement](./measurement.mdx) explains what to collect, calculate and report; agree the comparison rules before starting.
**Output:** a pilot report and a continue, adjust or stop decision. Missing evidence is a reason to investigate, not proof of improvement.
## 4. Expand only on evidence
Extend to another workflow or team only when quality, usefulness, cost and support capacity justify it. Prove reliable skill distribution, revocation and recovery before expanding authority. Introduce autonomous SDLC execution progressively under human direction; new departmental workflows need their own data boundaries and acceptance evidence.
**Output:** an evidenced expansion decision with verified controls for the next scope—not organization-wide rollout by default.
## 5. Sustain and improve
Review outcomes, drift, incidents and maintenance effort. Retire ineffective skills, revisit process bottlenecks and repeat assessment when repositories, clients, data or authority change. Work toward [Target](./target-operating-model.mdx): an autonomous agent executes and governs the repeating SDLC while humans direct, guide and review.
**Output:** maintained skills, a refreshed improvement backlog and evidence for the next cycle. Keep human takeover available; narrow or stop workflows when evidence no longer supports them.
<details> <details>
<summary>Stage-by-stage entry evidence, work, exit gates and hold conditions</summary> <summary>Supporting gate evidence and hold conditions</summary>
**TO-BE · Target operating model.** Introduction depends on the [roadmap gates](./roadmap.mdx); these pages do not describe installed controls or measured company results. These are recommended implementation gates, not assigned human owners, approved thresholds or installed integrations. Agree acceptance criteria for each selected workflow before evaluating it.
### Foundation ### Assessment → readiness
**Entry evidence** — Strategy scope accepted for planning; existing teams identified. Retain the sampled repository revisions, test/CI results, debt and security findings, and traced changes behind the process map. Record coverage and unknowns. Distinguish critical pilot blockers from parallel improvements; hold selection if the sample cannot explain how the candidate workflow is delivered and accepted.
**Work** — Inventory actual client hosts/builds, provider/data terms, repositories, authority, owners and event availability; select low-risk workflows and metric definitions. ### Readiness → pilot
**Exit evidence and hold condition** — Named accountable people and independent reviewers, bounded pilot charter, forbidden actions, data/retention decision and reproducible baseline plan. Hold if accountability or lawful collection is unresolved. Inventory actual client hosts/builds, provider/data terms, authority and event availability. Confirm accountable people and independent reviewers, forbidden actions, data/retention decisions, recovery and a reproducible baseline. Hold if accountability, lawful collection or critical acceptance checks remain unresolved.
### Governed skills pilot ### Pilot → expansion
**Entry evidence** — Foundation evidence approved; no privileged autonomous effects. Use a small registry, immutable skill contracts, qualified client adapters, evaluations and coaching. Verify the measurement calculations with synthetic fixtures before interpreting real outcomes. Require mandatory safety checks, independent task acceptance, reconciled source coverage and user feedback. Hold or narrow scope if quality or coverage is inadequate; usage alone does not justify expansion.
**Work** — Build a small registry, immutable contract, client adapters, evaluations, coaching and synthetic measurement verification. ### Expansion → wider authority
**Exit evidence and hold condition** — Qualified client/skill tuples, mandatory safety checks, independent task acceptance, reconciled coverage and user feedback. Hold or narrow scope if quality or data coverage is inadequate. Prove canary updates, effective-version readback, version locking, exception expiry, stale/offline handling, rollback and revocation. Hold if effective versions or controls cannot be verified or the catalog cannot be supported.
### Reliable distribution For autonomous execution, begin with observation and evidence assembly, then proposing and routing work, before granting scoped execution authority. Recommended controls include exact-artifact approvals, idempotency, duplicate/outage recovery, separation of duties, audit and kill-switch drills. Prove human takeover; never infer permission from missing evidence. The agent ultimately executes and governs the cycle, not only routes tasks.
**Entry evidence** — Pilot quality and usefulness support continuing. ### Sustained operation
**Work** — Canary updates, effective-version readback, version locking, exception expiry, stale/offline handling and rollback. Operations, Marketing, Finance & Accounting and HR & Administration workflows each need a workflow owner, outcome, data boundary, capacity and domain-specific review; Product and Technology continue quality improvement. Keep sensitive decisions human. Use accepted outcomes, guardrails, total cost and experience to continue, narrow, stop or roll back. Changed client, provider, data or authority boundaries reopen the relevant gates.
**Exit evidence and hold condition** — Tested revocation and recovery, no bypass of central privileged gates, owners can support the catalog. Hold if effective versions or controls cannot be verified.
### Logical SDLC coordination
**Entry evidence** — Distribution, authority and measurement evidence stable.
**Work** — Observe and assemble evidence first, then propose and route work within scoped identities and budgets.
**Exit evidence and hold condition** — Exact-artifact approvals, idempotency, duplicate/outage recovery, separation of duties, audit and kill-switch drills pass; humans can take over. No authority expansion based on usage alone.
### Departmental expansion
**Entry evidence** — Each department supplies a workflow owner, outcome, data boundary and capacity.
**Work** — Add Operations, Marketing, Finance & Accounting and HR & Administration workflows with domain-specific review; Product and Technology continue quality improvement.
**Exit evidence and hold condition** — Departmental accepted outcomes, guardrails, total cost and experience justify continuation; sensitive decisions stay human. Stop or roll back workflows without adequate evidence.
There are no invented launch dates, budgets, staffing commitments or guaranteed gains. Phase gates permit narrowing or stopping. Re-evaluate earlier phases when the underlying client, provider, data scope or authority changes.
</details> </details>
+1 -1
View File
@@ -15,7 +15,7 @@
"verify:build": "node scripts/verify.mjs --build", "verify:build": "node scripts/verify.mjs --build",
"diagrams:build": "node scripts/build-diagrams.mjs", "diagrams:build": "node scripts/build-diagrams.mjs",
"test": "node --test scripts/*.test.mjs", "test": "node --test scripts/*.test.mjs",
"test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/skills-measurement.cjs", "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",
"kpi:example": "node scripts/ai-kpi.mjs" "kpi:example": "node scripts/ai-kpi.mjs"
}, },
"dependencies": { "dependencies": {
+1 -1
View File
@@ -61,7 +61,7 @@
{ {
"id": "roadmap", "id": "roadmap",
"title": "Roadmap", "title": "Roadmap",
"purpose": "Evidence-gated phases, no promised dates or budgets." "purpose": "Assess current code quality and delivery processes, close critical gaps, then pilot, expand and sustain AI adoption."
} }
], ],
"diagram": { "diagram": {
+164
View File
@@ -0,0 +1,164 @@
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/roadmap-${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 {
// 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();
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);
assert.equal(await page.locator('h1').innerText(), 'Roadmap');
const headings = page.locator('.strategy-content > h2');
assert.deepEqual((await headings.allTextContents()).map(s => s.replace(/\u200b/g, '')), ['1. Understand code quality and delivery','2. Fix critical readiness gaps','3. Run a bounded pilot','4. Expand only on evidence','5. Sustain and improve']);
const text = await page.locator('.strategy-content').innerText();
for (const marker of ['baseline findings', 'process map', 'prioritized gaps', 'alongside the pilot', 'Recommended sequence']) assert(text.includes(marker), marker);
assert(!text.includes('Supporting gate evidence and hold conditions\n\nThese are'));
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true});
await menu.first().waitFor({state:'visible'});
assert.deepEqual((await menu.allTextContents()).slice(0,2), ['Target','Roadmap']);
if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
const panel = page.locator('.archify-panel--story');
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
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()})).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('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});
await close.waitFor({state:'visible'}); assert.equal(await close.innerText(), '×');
await close.click(); 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));
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
await page.evaluate(() => scrollTo({top:0, behavior:'instant'}));
await page.screenshot({path:`${output}/${width}-top.png`});
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').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));
for (const href of [...new Set(links)]) {
const r = await page.goto(href, {waitUntil:'networkidle'}); assert.equal(r.status(), 200);
assert.equal(page.url(), href);
}
assert.deepEqual(errors, []);
results.push({width, geometry, links:links.length, controls:true, export:true, sidebar:true, visibleAssessment:true});
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
await context.close();
}
console.log(`PASS ${results.length} Roadmap viewports: visible stages/outputs, sidebar, diagram geometry, controls, export and links`);
} finally {await browser.close();}
})().catch(error => {console.error(error); process.exitCode = 1;});
+1 -1
View File
@@ -22,7 +22,7 @@ const fs=require('node:fs');
assert(geometry.nodes.length>=5 && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id); assert(geometry.nodes.length>=5 && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id);
for(const n of geometry.nodes) assert(n.x>=0 && n.y>=0 && n.right<=geometry.width+1 && n.bottom<=geometry.height+1,`${id} ${width} clipped ${JSON.stringify(n)} / ${geometry.height}`); for(const n of geometry.nodes) assert(n.x>=0 && n.y>=0 && n.right<=geometry.width+1 && n.bottom<=geometry.height+1,`${id} ${width} clipped ${JSON.stringify(n)} / ${geometry.height}`);
const data=await page.locator('.strategy-content').evaluate(el=>({words:el.innerText.trim().split(/\s+/).length,overflow:document.documentElement.scrollWidth>innerWidth,openDetails:el.querySelectorAll('details[open]').length})); const data=await page.locator('.strategy-content').evaluate(el=>({words:el.innerText.trim().split(/\s+/).length,overflow:document.documentElement.scrollWidth>innerWidth,openDetails:el.querySelectorAll('details[open]').length}));
assert(data.words<=(id==='measurement'?1100:200),id);assert(!data.overflow,id);assert.equal(data.openDetails,0,id); assert(data.words<=(id==='measurement'?1100:id==='roadmap'?600:200),id);assert(!data.overflow,id);assert.equal(data.openDetails,0,id);
await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));await page.screenshot({path:`.cache/story-${label}/${width}-${id}.png`,fullPage:true}); await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));await page.screenshot({path:`.cache/story-${label}/${width}-${id}.png`,fullPage:true});
const opener=panel.getByRole('button',{name:/^Expand /});await opener.click(); const opener=panel.getByRole('button',{name:/^Expand /});await opener.click();
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'});
+22 -4
View File
@@ -1,6 +1,8 @@
import {readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, rmSync} from 'node:fs'; import {readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, rmSync} from 'node:fs';
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 {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']);
const inlineStyles = readFileSync(new URL('./inline-viewer.css', import.meta.url), 'utf8'); 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}); mkdirSync(`static/diagrams/${name}`, {recursive: true});
if (spec.diagram_type !== type || spec.meta.output !== output) throw new Error(`Diagram type/output mismatch: ${file}`); 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'}}); 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']); const roadmap = ['strategy-roadmap', 'strategy-roadmap-mobile'].includes(name);
run(['deliver',type,input,output,'--quality','showcase','--json']); if (roadmap) {
run(['check',output,'--json','--require-provenance']); // 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. // Verify the upstream artifact first, then add the scoped portal integration.
let viewer = readFileSync(output, 'utf8'); let viewer = readFileSync(output, 'utf8');
// Upstream intentionally disables preset selection in gallery embeds. Enable // 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>`) 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>`)); .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/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');
+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});}
});
+36
View File
@@ -0,0 +1,36 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
const read = path => readFileSync(path, 'utf8');
test('Roadmap visibly assesses code and real delivery before adoption', () => {
const page = read('docs/strategy/roadmap.mdx');
const visible = page.split('<details>')[0];
const headings = [...visible.matchAll(/^## (.+)$/gm)].map(match => match[1]);
assert.deepEqual(headings, ['1. Understand code quality and delivery', '2. Fix critical readiness gaps', '3. Run a bounded pilot', '4. Expand only on evidence', '5. Sustain and improve']);
assert(visible.indexOf(' story />') < visible.indexOf('## 1.'));
for (const marker of ['representative repositories', 'tests and CI', 'maintainability', 'technical debt', 'security findings', 'actual SDLC', 'ownership', 'review and release', 'bottlenecks', 'baseline findings', 'process map', 'prioritized gaps', 'alongside the pilot', './measurement.mdx', './target-operating-model.mdx']) assert(visible.includes(marker), marker);
assert.equal((visible.match(/\*\*Output:/g) || []).length, 5);
assert.match(visible, /Recommended sequence/);
assert(!page.includes('### Foundation'));
for (const marker of ['revocation', 'exact-artifact approvals', 'idempotency', 'human takeover', 'sensitive decisions']) assert(page.includes(marker), marker);
});
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}.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);
}
});
+9 -2
View File
@@ -13,12 +13,12 @@ test('every strategy page leads with a compact diagram story and closes its refe
assert(!page.includes('<details>') || page.indexOf(' story />') < page.indexOf('<details>'), id); assert(!page.includes('<details>') || page.indexOf(' story />') < page.indexOf('<details>'), id);
assert(!/<details\s+open/.test(page), id); assert(!/<details\s+open/.test(page), id);
assert.equal((page.match(/<details>/g) || []).length, (page.match(/<\/details>/g) || []).length, id); assert.equal((page.match(/<details>/g) || []).length, (page.match(/<\/details>/g) || []).length, id);
assert.equal((narrative.match(/^## /gm) || []).length, id === 'measurement' ? 4 : 1, id); assert.equal((narrative.match(/^## /gm) || []).length, id === 'measurement' ? 4 : id === 'roadmap' ? 5 : 1, id);
assert((narrative.match(/^- /gm) || []).length <= 3, id); assert((narrative.match(/^- /gm) || []).length <= 3, id);
assert(narrative.includes('**Next:**'), id); assert(narrative.includes('**Next:**'), id);
assert(!narrative.includes('| ---'), id); assert(!narrative.includes('| ---'), id);
const words = narrative.replace(/<[^>]*>/g, '').trim().split(/\s+/).length; const words = narrative.replace(/<[^>]*>/g, '').trim().split(/\s+/).length;
assert(words <= (id === 'measurement' ? 1100 : 200), `${id}: ${words} default narrative words`); assert(words <= (id === 'measurement' ? 1100 : id === 'roadmap' ? 600 : 200), `${id}: ${words} default narrative words`);
} }
}); });
@@ -26,6 +26,13 @@ test('mobile story diagrams preserve the same nodes and directed relationships',
for (const {id} of model.pages) { for (const {id} of model.pages) {
const page = read(`docs/strategy/${id}.mdx`); const page = read(`docs/strategy/${id}.mdx`);
const name = /<ArchifyDiagram name="([^"]+)"[^>]+ story/.exec(page)[1]; 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 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);
+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 {max-width: 740px;}
.archify-panel--story > iframe {height: auto; aspect-ratio: 480 / 408;} .archify-panel--story > iframe {height: auto; aspect-ratio: 480 / 408;}
.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;}
.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-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;}
@@ -121,11 +124,16 @@ 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: 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-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;}
} }
/* 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 {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);}
.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);} .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);}