Compare commits

..
Author SHA1 Message Date
jarvis-at-skic a35cade49c feat: [verified] compose Skills Adoption diagrams around their stories
Build and publish Eldorado documentation / build (pull_request) Successful in 40s
2026-10-06 22:35:48 +00:00
jarvis-at-skic ef169cec58 Merge pull request 'fix: expose every documentation page in navigation' (#32) from fix/all-docs-sidebar into main
Build and publish Eldorado documentation / build (push) Successful in 41s
2026-10-06 13:15:15 -07:00
jarvis-at-skic e0afa78eb2 test: [verified] include Reference in measurement navigation
Build and publish Eldorado documentation / build (pull_request) Successful in 43s
2026-10-06 20:14:19 +00:00
jarvis-at-skic e087adab1e fix: [verified] expose all documentation in reachable sidebars
Build and publish Eldorado documentation / build (pull_request) Successful in 47s
2026-10-06 20:00:08 +00:00
jarvis-at-skic 325363d89c Merge pull request 'feat: wider, richer Skills Adoption diagrams' (#31) from feat/skills-adoption-stories into main
Build and publish Eldorado documentation / build (push) Successful in 44s
2026-10-06 12:41:28 -07:00
jarvis-at-skic ad7312b65a feat: [verified] widen Skills Adoption diagram stories
Build and publish Eldorado documentation / build (pull_request) Successful in 39s
2026-10-06 19:37:49 +00:00
jarvis-at-skic 3238e1d809 Merge pull request 'docs: group Strategy into Skills Adoption and Autonomous Agent stages' (#30) from feat/strategy-stage-navigation into main
Build and publish Eldorado documentation / build (push) Successful in 42s
2026-10-06 12:07:57 -07:00
jarvis-at-skic fe4c3509d1 docs: [verified] group Strategy into adoption and autonomy stages
Build and publish Eldorado documentation / build (pull_request) Successful in 40s
2026-10-06 19:07:00 +00:00
jarvis-at-skic 8a7dbea852 Merge pull request 'docs: connect skills-first Roadmap to autonomous Target' (#29) from feat/roadmap-skills-to-target into main
Build and publish Eldorado documentation / build (push) Successful in 39s
2026-10-06 04:36:20 -07:00
jarvis-at-skic e10b03dff4 docs: [verified] connect skills-first Roadmap to autonomous Target
Build and publish Eldorado documentation / build (pull_request) Successful in 49s
2026-10-06 11:34:39 +00:00
jarvis-at-skic 733d6f2ece Merge pull request 'fix: replace Roadmap with minimal adoption timeline' (#28) from fix/roadmap-minimal-timeline into main
Build and publish Eldorado documentation / build (push) Successful in 41s
2026-10-06 00:35:15 -07:00
jarvis-at-skic 2a4e058684 fix: [verified] simplify Roadmap adoption timeline
Build and publish Eldorado documentation / build (pull_request) Successful in 54s
2026-10-06 07:25:53 +00:00
jarvis-at-skic 63f38597a6 Merge pull request 'fix: separate Target lifecycle channels and standalone typography' (#27) from fix/standalone-svg-typography into main
Build and publish Eldorado documentation / build (push) Successful in 55s
2026-10-05 14:13:27 -07:00
jarvis-at-skic 0248baa97b fix: [verified] separate Target lifecycle channels and export typography
Build and publish Eldorado documentation / build (pull_request) Successful in 50s
2026-10-05 21:11:29 +00:00
jarvis-at-skic 7e3696d2d7 Merge pull request 'fix: share Target central-agent edge midpoints' (#26) from fix/target-midpoint-connectors into main
Build and publish Eldorado documentation / build (push) Successful in 40s
2026-10-05 13:14:33 -07:00
jarvis-at-skic 15c5dcb31d fix: align Target governance links to shared edge midpoints
Build and publish Eldorado documentation / build (pull_request) Successful in 38s
2026-10-05 20:12:20 +00:00
jarvis-at-skic 64b2d6dae8 Merge pull request 'docs: rename Target contributors to AI Agent' (#25) from docs/target-ai-agent into main
Build and publish Eldorado documentation / build (push) Successful in 45s
2026-10-05 12:54:21 -07:00
jarvis-at-skic 20e6f4899c docs: rename Target contributors to AI Agent
Build and publish Eldorado documentation / build (pull_request) Successful in 50s
2026-10-05 19:51:04 +00:00
jarvis-at-skic 2f68e60cec Merge pull request 'docs: redesign Roadmap as a native five-stage workflow' (#24) from docs/roadmap-workflow into main
Build and publish Eldorado documentation / build (push) Successful in 39s
2026-10-05 12:36:42 -07:00
59 changed files with 4018 additions and 1119 deletions

No files matched your search

@@ -2,138 +2,244 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Learn before expanding", "title": "TO-BE: Adoption",
"output": "static/diagrams/strategy-adoption-mobile/index.html", "output": "static/diagrams/strategy-adoption-mobile/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
880,
2040
]
}, },
"components": [ "components": [
{ {
"id": "choose", "id": "delivery",
"type": "backend", "type": "external",
"label": "Choose workflow", "label": "Delivery first",
"sublabel": "Department outcome", "tag": "Start with a real workflow",
"sublabel": "Baseline + bounded canary",
"pos": [ "pos": [
20, 60,
20 120
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "coach", "id": "human",
"type": "backend", "type": "backend",
"label": "Coach people", "label": "Human",
"sublabel": "Verify + recover", "tag": "Direct + guide + review",
"sublabel": "Define outcome; inspect artifacts",
"pos": [ "pos": [
20, 60,
140 1280
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "canary", "id": "agent",
"type": "backend", "type": "external",
"label": "Run bounded canary", "label": "AI Agent",
"sublabel": "Qualified skill + client", "tag": "Supported client + approved skills",
"sublabel": "Produce work within the boundary",
"pos": [ "pos": [
20, 60,
260 910
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "accept", "id": "learn",
"type": "backend", "type": "external",
"label": "Accept real work", "label": "Learn from real work",
"sublabel": "Independent review", "tag": "Ongoing, reviewed improvement",
"sublabel": "Feedback + friction + measures\nRetain a manual path\nNo quotas or rankings",
"pos": [ "pos": [
20, 60,
380 490
], ],
"size": [ "size": [
180, 740,
60 270
]
},
{
"id": "feedback",
"type": "backend",
"label": "Review feedback",
"sublabel": "Usefulness + non-use",
"pos": [
20,
500
], ],
"size": [ "icon": "none"
180,
60
]
}, },
{ {
"id": "expand", "id": "expand",
"type": "backend", "type": "backend",
"label": "Expand or revise", "label": "Expand on evidence",
"sublabel": "Keep a manual path", "tag": "Other SDLC phases",
"sublabel": "Later order undecided\nDelivery keeps improving",
"pos": [ "pos": [
20, 60,
620 1650
], ],
"size": [ "size": [
180, 740,
60 240
] ],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "delivery-agent",
"from": "choose", "from": "delivery",
"to": "coach", "to": "agent",
"label": "Prepare" "label": "Practice",
},
{
"id": "flow-1",
"from": "coach",
"to": "canary",
"label": "support pilot"
},
{
"id": "flow-2",
"from": "canary",
"to": "accept",
"label": "Verify"
},
{
"id": "flow-3",
"from": "accept",
"to": "feedback",
"label": "learn"
},
{
"id": "flow-4",
"from": "feedback",
"to": "expand",
"label": "decide"
},
{
"id": "feedback-loop",
"from": "expand",
"to": "coach",
"label": "Review revision",
"variant": "dashed",
"fromSide": "right", "fromSide": "right",
"toSide": "right" "toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
230.0
],
[
850,
1020.0
]
],
"labelAt": [
780,
870
]
},
{
"id": "human-agent",
"from": "human",
"to": "agent",
"label": "Direction",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
824,
1390.0
],
[
824,
1020.0
]
],
"labelAt": [
770,
1240
]
},
{
"id": "agent-human",
"from": "agent",
"to": "human",
"label": "Work to inspect",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1185
]
},
{
"id": "agent-learn",
"from": "agent",
"to": "learn",
"label": "Observe",
"fromSide": "top",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
815
]
},
{
"id": "learn-delivery",
"from": "learn",
"to": "delivery",
"label": "Improve",
"fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
42,
625.0
],
[
42,
230.0
]
],
"labelAt": [
620,
80
]
},
{
"id": "human-expand",
"from": "human",
"to": "expand",
"label": "Accepted work",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1555
]
},
{
"id": "learn-expand",
"from": "learn",
"to": "expand",
"label": "Feedback",
"fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
20,
625.0
],
[
20,
1770.0
]
],
"labelAt": [
620,
1610
]
} }
] ]
} }
+197 -93
View File
@@ -2,138 +2,242 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Learn before expanding", "title": "TO-BE: Adoption",
"output": "static/diagrams/strategy-adoption/index.html", "output": "static/diagrams/strategy-adoption/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
2040,
1160
]
}, },
"components": [ "components": [
{ {
"id": "choose", "id": "delivery",
"type": "backend", "type": "external",
"label": "Choose workflow", "label": "Delivery first",
"sublabel": "Department outcome", "tag": "Start with a real workflow",
"sublabel": "Baseline + bounded canary",
"pos": [ "pos": [
20, 60,
20 350
], ],
"size": [ "size": [
180, 470,
60 210
] ],
"icon": "none"
}, },
{ {
"id": "coach", "id": "human",
"type": "backend", "type": "backend",
"label": "Coach people", "label": "Human",
"sublabel": "Verify + recover", "tag": "Direct + guide + review",
"sublabel": "Define outcome; inspect artifacts",
"pos": [ "pos": [
260, 700,
20 90
], ],
"size": [ "size": [
180, 570,
60 190
] ],
"icon": "none"
}, },
{ {
"id": "canary", "id": "agent",
"type": "backend", "type": "external",
"label": "Run bounded canary", "label": "AI Agent",
"sublabel": "Qualified skill + client", "tag": "Supported client + approved skills",
"sublabel": "Produce work within the boundary",
"pos": [ "pos": [
260, 700,
150 440
], ],
"size": [ "size": [
180, 570,
60 190
] ],
"icon": "none"
}, },
{ {
"id": "accept", "id": "learn",
"type": "backend", "type": "external",
"label": "Accept real work", "label": "Learn from real work",
"sublabel": "Independent review", "tag": "Ongoing, reviewed improvement",
"sublabel": "Feedback + friction + measures\nRetain a manual path\nNo quotas or rankings",
"pos": [ "pos": [
20, 700,
150 800
], ],
"size": [ "size": [
180, 570,
60 270
]
},
{
"id": "feedback",
"type": "backend",
"label": "Review feedback",
"sublabel": "Usefulness + non-use",
"pos": [
20,
280
], ],
"size": [ "icon": "none"
180,
60
]
}, },
{ {
"id": "expand", "id": "expand",
"type": "backend", "type": "backend",
"label": "Expand or revise", "label": "Expand on evidence",
"sublabel": "Keep a manual path", "tag": "Other SDLC phases",
"sublabel": "Later order undecided\nDelivery keeps improving",
"pos": [ "pos": [
260, 1460,
280 360
], ],
"size": [ "size": [
180, 520,
60 240
] ],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "delivery-agent",
"from": "choose", "from": "delivery",
"to": "coach", "to": "agent",
"label": "Prepare" "label": "Practice",
},
{
"id": "flow-1",
"from": "coach",
"to": "canary",
"label": "support pilot"
},
{
"id": "flow-2",
"from": "canary",
"to": "accept",
"label": "Verify"
},
{
"id": "flow-3",
"from": "accept",
"to": "feedback",
"label": "learn"
},
{
"id": "flow-4",
"from": "feedback",
"to": "expand",
"label": "decide"
},
{
"id": "feedback-loop",
"from": "expand",
"to": "coach",
"label": "Review revision",
"variant": "dashed",
"fromSide": "right", "fromSide": "right",
"toSide": "right" "toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
585,
455
],
[
585,
535
]
],
"labelAt": [
570,
600
]
},
{
"id": "human-agent",
"from": "human",
"to": "agent",
"label": "Direction",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
1100,
360
]
},
{
"id": "agent-human",
"from": "agent",
"to": "human",
"label": "Work to inspect",
"fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
630,
535
],
[
630,
185
]
],
"labelAt": [
470,
240
]
},
{
"id": "agent-learn",
"from": "agent",
"to": "learn",
"label": "Observe",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
1100,
720
]
},
{
"id": "learn-delivery",
"from": "learn",
"to": "delivery",
"label": "Improve",
"fromSide": "left",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
295,
935
]
],
"labelAt": [
450,
900
]
},
{
"id": "human-expand",
"from": "human",
"to": "expand",
"label": "Accepted work",
"fromSide": "right",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1720,
185
]
],
"labelAt": [
1550,
140
]
},
{
"id": "learn-expand",
"from": "learn",
"to": "expand",
"label": "Feedback",
"fromSide": "right",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1720,
935
]
],
"labelAt": [
1480,
895
]
} }
] ]
} }
@@ -2,129 +2,246 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Independent approval", "title": "TO-BE: Governance",
"output": "static/diagrams/strategy-governance-mobile/index.html", "output": "static/diagrams/strategy-governance-mobile/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
880,
2710
]
}, },
"components": [ "components": [
{ {
"id": "owner", "id": "owner",
"type": "security", "type": "external",
"label": "Owner proposes", "label": "01 Propose",
"sublabel": "Existing accountable team", "tag": "Owning team",
"sublabel": "Scope + testable outcome",
"pos": [ "pos": [
20, 60,
20 120
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "review", "id": "review",
"type": "security", "type": "external",
"label": "Independent review", "label": "02 Review independently",
"sublabel": "Quality + risk evidence", "tag": "Quality + Security",
"sublabel": "Author is not sole approver",
"pos": [ "pos": [
20, 60,
140 490
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "canary", "id": "canary",
"type": "security", "type": "external",
"label": "Bounded canary", "label": "03 Qualify real use",
"sublabel": "Qualified client path", "tag": "Human + AI Agent",
"sublabel": "Exact client + bounded canary",
"pos": [ "pos": [
20, 60,
260 860
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "approve", "id": "approve",
"type": "security", "type": "backend",
"label": "Approve revision", "label": "04 Approve exact scope",
"sublabel": "Exact digest + scope", "tag": "Accountable approvers",
"sublabel": "Digest + permitted uses + expiry",
"pos": [ "pos": [
20, 60,
380 1230
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "publish", "id": "publish",
"type": "security", "type": "backend",
"label": "Publish bundle", "label": "05 Publish the bundle",
"sublabel": "Data & AI catalog", "tag": "Data & AI catalog",
"sublabel": "Pin skill, adapter + dependencies",
"pos": [ "pos": [
20, 60,
500 1600
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "distribute", "id": "distribute",
"type": "security", "type": "backend",
"label": "Verify effective state", "label": "06 Verify loaded state",
"sublabel": "Platform & IT", "tag": "Platform & IT",
"sublabel": "Read revision; verify reload",
"pos": [ "pos": [
20, 60,
620 1970
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
},
{
"id": "withdraw",
"type": "security",
"label": "Withdraw / roll back",
"tag": "Unsafe revision",
"sublabel": "Revoke; restore last-known-good",
"pos": [
60,
2340
],
"size": [
740,
220
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "owner-review",
"from": "owner", "from": "owner",
"to": "review", "to": "review",
"label": "submit" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
395
]
}, },
{ {
"id": "flow-1", "id": "review-canary",
"from": "review", "from": "review",
"to": "canary", "to": "canary",
"label": "qualify" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
765
]
}, },
{ {
"id": "flow-2", "id": "canary-approve",
"from": "canary", "from": "canary",
"to": "approve", "to": "approve",
"label": "Accept" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1135
]
}, },
{ {
"id": "flow-3", "id": "approve-publish",
"from": "approve", "from": "approve",
"to": "publish", "to": "publish",
"label": "record approval" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1505
]
}, },
{ {
"id": "flow-4", "id": "publish-distribute",
"from": "publish", "from": "publish",
"to": "distribute", "to": "distribute",
"label": "Deliver" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1875
]
},
{
"id": "approve-owner",
"from": "approve",
"to": "owner",
"label": "Rework",
"fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "security",
"via": [
[
20,
1340.0
],
[
20,
230.0
]
],
"labelAt": [
620,
80
]
},
{
"id": "distribute-withdraw",
"from": "distribute",
"to": "withdraw",
"label": "Unsafe",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "security",
"labelAt": [
610,
2245
]
} }
] ]
} }
+207 -60
View File
@@ -2,129 +2,276 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Independent approval", "title": "TO-BE: Governance",
"output": "static/diagrams/strategy-governance/index.html", "output": "static/diagrams/strategy-governance/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
1760,
1420
]
}, },
"components": [ "components": [
{ {
"id": "owner", "id": "owner",
"type": "security", "type": "external",
"label": "Owner proposes", "label": "01 Propose",
"sublabel": "Existing accountable team", "tag": "Owning team",
"sublabel": "Scope + testable outcome",
"pos": [ "pos": [
20, 80,
20 60
], ],
"size": [ "size": [
180, 500,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "review", "id": "review",
"type": "security", "type": "external",
"label": "Independent review", "label": "02 Review independently",
"sublabel": "Quality + risk evidence", "tag": "Quality + Security",
"sublabel": "Author is not sole approver",
"pos": [ "pos": [
260, 300,
20 280
], ],
"size": [ "size": [
180, 500,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "canary", "id": "canary",
"type": "security", "type": "external",
"label": "Bounded canary", "label": "03 Qualify real use",
"sublabel": "Qualified client path", "tag": "Human + AI Agent",
"sublabel": "Exact client + bounded canary",
"pos": [ "pos": [
260, 520,
150 500
], ],
"size": [ "size": [
180, 500,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "approve", "id": "approve",
"type": "security", "type": "backend",
"label": "Approve revision", "label": "04 Approve exact scope",
"sublabel": "Exact digest + scope", "tag": "Accountable approvers",
"sublabel": "Digest + permitted uses + expiry",
"pos": [ "pos": [
20, 740,
150 720
], ],
"size": [ "size": [
180, 500,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "publish", "id": "publish",
"type": "security", "type": "backend",
"label": "Publish bundle", "label": "05 Publish the bundle",
"sublabel": "Data & AI catalog", "tag": "Data & AI catalog",
"sublabel": "Pin skill, adapter + dependencies",
"pos": [ "pos": [
20, 960,
280 940
], ],
"size": [ "size": [
180, 500,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "distribute", "id": "distribute",
"type": "security", "type": "backend",
"label": "Verify effective state", "label": "06 Verify loaded state",
"sublabel": "Platform & IT", "tag": "Platform & IT",
"sublabel": "Read revision; verify reload",
"pos": [ "pos": [
260, 1180,
280 1160
], ],
"size": [ "size": [
180, 500,
60 170
] ],
"icon": "none"
},
{
"id": "withdraw",
"type": "security",
"label": "Withdraw / roll back",
"tag": "Unsafe revision",
"sublabel": "Revoke; restore last-known-good",
"pos": [
80,
1160
],
"size": [
660,
170
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "owner-review",
"from": "owner", "from": "owner",
"to": "review", "to": "review",
"label": "submit" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
330,
255
],
[
550,
255
]
]
}, },
{ {
"id": "flow-1", "id": "review-canary",
"from": "review", "from": "review",
"to": "canary", "to": "canary",
"label": "qualify" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
550,
475
],
[
770,
475
]
]
}, },
{ {
"id": "flow-2", "id": "canary-approve",
"from": "canary", "from": "canary",
"to": "approve", "to": "approve",
"label": "Accept" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
770,
695
],
[
990,
695
]
]
}, },
{ {
"id": "flow-3", "id": "approve-publish",
"from": "approve", "from": "approve",
"to": "publish", "to": "publish",
"label": "record approval" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
990,
915
],
[
1210,
915
]
]
}, },
{ {
"id": "flow-4", "id": "publish-distribute",
"from": "publish", "from": "publish",
"to": "distribute", "to": "distribute",
"label": "Deliver" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1210,
1135
],
[
1430,
1135
]
]
},
{
"id": "approve-owner",
"from": "approve",
"to": "owner",
"label": "Rework",
"fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "security",
"via": [
[
40,
805
],
[
40,
145
]
],
"labelAt": [
180,
680
]
},
{
"id": "distribute-withdraw",
"from": "distribute",
"to": "withdraw",
"label": "Unsafe",
"fromSide": "left",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "security",
"labelAt": [
950,
1220
]
} }
] ]
} }
@@ -2,140 +2,337 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Maintain trust over time", "title": "TO-BE: Maintenance",
"output": "static/diagrams/strategy-maintenance-mobile/index.html", "output": "static/diagrams/strategy-maintenance-mobile/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
880,
3080
]
}, },
"components": [ "components": [
{ {
"id": "monitor", "id": "observe",
"type": "backend", "type": "external",
"label": "Monitor skills", "label": "Observe use",
"sublabel": "Usefulness + safety", "tag": "Owner + Data & AI",
"sublabel": "Feedback, drift + review due",
"pos": [ "pos": [
20, 60,
20 120
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "triage", "id": "triage",
"type": "backend", "type": "external",
"label": "Triage changes", "label": "Triage the signal",
"sublabel": "Owner + risk review", "tag": "Owning team",
"sublabel": "Useful? Safe? Still needed?",
"pos": [ "pos": [
20, 60,
140 1970
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "correct", "id": "change",
"type": "backend", "type": "external",
"label": "Correct and test", "label": "Change the skill",
"sublabel": "Independent regression", "tag": "Reviewed correction",
"sublabel": "New immutable revision",
"pos": [ "pos": [
20, 60,
260 1230
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "decision", "id": "validate",
"type": "backend", "type": "external",
"label": "Approve or hold", "label": "Revalidate",
"sublabel": "Exact revised bundle", "tag": "Independent reviewers",
"sublabel": "Regression + client evidence",
"pos": [ "pos": [
20, 60,
380 860
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "promote", "id": "release",
"type": "backend", "type": "backend",
"label": "Promote safely", "label": "Approve + version",
"sublabel": "Verify state + monitor", "tag": "Accountable human authority",
"sublabel": "Verify distribution and recovery",
"pos": [ "pos": [
20, 60,
500 490
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "observe", "id": "safe",
"type": "backend", "type": "backend",
"label": "Quarantine / rollback", "label": "Continue safe use",
"sublabel": "Revoke unsafe revision", "tag": "Substantive review passes",
"sublabel": "Keep owner + next review date",
"pos": [ "pos": [
20, 60,
620 2710
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
},
{
"id": "retire",
"type": "external",
"label": "Retire deliberately",
"tag": "No longer useful",
"sublabel": "Reconcile consumers; no new use",
"pos": [
60,
2340
],
"size": [
740,
220
],
"icon": "none"
},
{
"id": "contain",
"type": "security",
"label": "Contain unsafe use",
"tag": "Security + Platform & IT",
"sublabel": "Revoke + quarantine + roll back\nNo automatic reinstatement",
"pos": [
60,
1600
],
"size": [
740,
220
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "observe-triage",
"from": "monitor",
"to": "triage",
"label": "Detect"
},
{
"id": "flow-1",
"from": "triage",
"to": "correct",
"label": "scope correction"
},
{
"id": "flow-2",
"from": "correct",
"to": "decision",
"label": "Prove"
},
{
"id": "flow-3",
"from": "decision",
"to": "promote",
"label": "authorize"
},
{
"id": "unsafe",
"from": "triage",
"to": "observe",
"label": "Unsafe",
"variant": "security",
"fromSide": "right",
"toSide": "right"
},
{
"id": "review-loop",
"from": "observe", "from": "observe",
"to": "correct", "to": "triage",
"label": "Requalify", "label": "",
"variant": "dashed" "fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
20,
230.0
],
[
20,
2080.0
]
],
"labelAt": [
620,
1930
]
},
{
"id": "triage-change",
"from": "triage",
"to": "change",
"label": "Correct",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
2080.0
],
[
850,
1340.0
]
],
"labelAt": [
780,
1560
]
},
{
"id": "change-validate",
"from": "change",
"to": "validate",
"label": "",
"fromSide": "top",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1135
]
},
{
"id": "validate-release",
"from": "validate",
"to": "release",
"label": "",
"fromSide": "top",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
765
]
},
{
"id": "release-observe",
"from": "release",
"to": "observe",
"label": "",
"fromSide": "top",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
395
]
},
{
"id": "triage-safe",
"from": "triage",
"to": "safe",
"label": "Safe",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
824,
2080.0
],
[
824,
2820.0
]
],
"labelAt": [
620,
2670
]
},
{
"id": "triage-retire",
"from": "triage",
"to": "retire",
"label": "Retire",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
2245
]
},
{
"id": "triage-contain",
"from": "triage",
"to": "contain",
"label": "Unsafe",
"fromSide": "top",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "security",
"labelAt": [
610,
1875
]
},
{
"id": "contain-change",
"from": "contain",
"to": "change",
"label": "Reviewed fix",
"fromSide": "top",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "security",
"labelAt": [
610,
1505
]
},
{
"id": "safe-observe",
"from": "safe",
"to": "observe",
"label": "Keep watching",
"fromSide": "left",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
42,
2820.0
],
[
42,
230.0
]
],
"labelAt": [
620,
80
]
} }
] ]
} }
+320 -91
View File
@@ -2,140 +2,369 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Maintain trust over time", "title": "TO-BE: Maintenance",
"output": "static/diagrams/strategy-maintenance/index.html", "output": "static/diagrams/strategy-maintenance/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
2630,
1300
]
}, },
"components": [ "components": [
{ {
"id": "monitor", "id": "observe",
"type": "backend", "type": "external",
"label": "Monitor skills", "label": "Observe use",
"sublabel": "Usefulness + safety", "tag": "Owner + Data & AI",
"sublabel": "Feedback, drift + review due",
"pos": [ "pos": [
20, 670,
20 60
], ],
"size": [ "size": [
180, 560,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "triage", "id": "triage",
"type": "backend", "type": "external",
"label": "Triage changes", "label": "Triage the signal",
"sublabel": "Owner + risk review", "tag": "Owning team",
"sublabel": "Useful? Safe? Still needed?",
"pos": [ "pos": [
260, 1340,
20 330
], ],
"size": [ "size": [
180, 500,
60 190
] ],
"icon": "none"
}, },
{ {
"id": "correct", "id": "change",
"type": "backend", "type": "external",
"label": "Correct and test", "label": "Change the skill",
"sublabel": "Independent regression", "tag": "Reviewed correction",
"sublabel": "New immutable revision",
"pos": [ "pos": [
260, 1240,
150 750
], ],
"size": [ "size": [
180, 520,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "decision", "id": "validate",
"type": "backend", "type": "external",
"label": "Approve or hold", "label": "Revalidate",
"sublabel": "Exact revised bundle", "tag": "Independent reviewers",
"sublabel": "Regression + client evidence",
"pos": [ "pos": [
20, 560,
150 970
], ],
"size": [ "size": [
180, 560,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "promote", "id": "release",
"type": "backend", "type": "backend",
"label": "Promote safely", "label": "Approve + version",
"sublabel": "Verify state + monitor", "tag": "Accountable human authority",
"sublabel": "Verify distribution and recovery",
"pos": [ "pos": [
20, 70,
280 570
], ],
"size": [ "size": [
180, 540,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "observe", "id": "safe",
"type": "backend", "type": "backend",
"label": "Quarantine / rollback", "label": "Continue safe use",
"sublabel": "Revoke unsafe revision", "tag": "Substantive review passes",
"sublabel": "Keep owner + next review date",
"pos": [ "pos": [
260, 2010,
280 80
], ],
"size": [ "size": [
180, 550,
60 180
] ],
"icon": "none"
},
{
"id": "retire",
"type": "external",
"label": "Retire deliberately",
"tag": "No longer useful",
"sublabel": "Reconcile consumers; no new use",
"pos": [
2010,
430
],
"size": [
550,
180
],
"icon": "none"
},
{
"id": "contain",
"type": "security",
"label": "Contain unsafe use",
"tag": "Security + Platform & IT",
"sublabel": "Revoke + quarantine + roll back\nNo automatic reinstatement",
"pos": [
2010,
800
],
"size": [
550,
220
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "observe-triage",
"from": "monitor",
"to": "triage",
"label": "Detect"
},
{
"id": "flow-1",
"from": "triage",
"to": "correct",
"label": "scope correction"
},
{
"id": "flow-2",
"from": "correct",
"to": "decision",
"label": "Prove"
},
{
"id": "flow-3",
"from": "decision",
"to": "promote",
"label": "authorize"
},
{
"id": "unsafe",
"from": "triage",
"to": "observe",
"label": "Unsafe",
"variant": "security",
"fromSide": "right",
"toSide": "right"
},
{
"id": "review-loop",
"from": "observe", "from": "observe",
"to": "correct", "to": "triage",
"label": "Requalify", "label": "",
"variant": "dashed" "fromSide": "right",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1590,
145
]
]
},
{
"id": "triage-change",
"from": "triage",
"to": "change",
"label": "Correct",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1590,
620
],
[
1500,
620
]
],
"labelAt": [
1450,
680
]
},
{
"id": "change-validate",
"from": "change",
"to": "validate",
"label": "",
"fromSide": "bottom",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1500,
1060
]
]
},
{
"id": "validate-release",
"from": "validate",
"to": "release",
"label": "",
"fromSide": "left",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
340,
1060
]
]
},
{
"id": "release-observe",
"from": "release",
"to": "observe",
"label": "",
"fromSide": "top",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
340,
145
]
]
},
{
"id": "triage-safe",
"from": "triage",
"to": "safe",
"label": "Safe",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1920,
425
],
[
1920,
170
]
],
"labelAt": [
1930,
210
]
},
{
"id": "triage-retire",
"from": "triage",
"to": "retire",
"label": "Retire",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1950,
425
],
[
1950,
520
]
],
"labelAt": [
1900,
490
]
},
{
"id": "triage-contain",
"from": "triage",
"to": "contain",
"label": "Unsafe",
"fromSide": "bottom",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "security",
"via": [
[
1590,
580
],
[
1850,
580
],
[
1850,
910
]
],
"labelAt": [
1900,
720
]
},
{
"id": "contain-change",
"from": "contain",
"to": "change",
"label": "Reviewed fix",
"fromSide": "bottom",
"toSide": "bottom",
"route": "straight",
"width": 4,
"variant": "security",
"via": [
[
2285,
1230
],
[
1500,
1230
]
],
"labelAt": [
1900,
1190
]
},
{
"id": "safe-observe",
"from": "safe",
"to": "observe",
"label": "Keep watching",
"fromSide": "top",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
2285,
20
],
[
950,
20
]
],
"labelAt": [
1640,
45
]
} }
] ]
} }
@@ -2,129 +2,313 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Measure skills adoption", "title": "TO-BE: Measurement",
"output": "static/diagrams/strategy-measurement-mobile/index.html", "output": "static/diagrams/strategy-measurement-mobile/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
880,
3630
]
}, },
"components": [ "components": [
{ {
"id": "collect", "id": "success-source",
"type": "backend", "type": "external",
"label": "Establish baseline", "label": "Tracker + acceptance",
"sublabel": "Current workflow", "tag": "Matured task starts",
"sublabel": "Join evidence to the workflow",
"pos": [ "pos": [
20, 60,
20 120
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "validate", "id": "success",
"type": "backend", "type": "frontend",
"label": "Join evidence", "label": "Accepted / eligible starts \u00d7 100",
"sublabel": "Tasks / versions / costs", "tag": "Task success (%)",
"sublabel": "Failures + unresolved retained",
"pos": [ "pos": [
20, 60,
140 490
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "calculate", "id": "quality-source",
"type": "backend", "type": "external",
"label": "Measure the pilot", "label": "Changes + defects",
"sublabel": "Outcomes / quality", "tag": "Completed follow-up",
"sublabel": "Join evidence to the workflow",
"pos": [ "pos": [
20, 60,
260 860
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "balance", "id": "quality",
"type": "backend", "type": "frontend",
"label": "Report together", "label": "Affected / accepted changes \u00d7 100",
"sublabel": "Cost / confidence / gaps", "tag": "Escaped quality (%)",
"sublabel": "Count each affected change once",
"pos": [ "pos": [
20, 60,
380 1230
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "weigh", "id": "cost-source",
"type": "backend", "type": "external",
"label": "Broader adoption", "label": "Invoices + review minutes",
"sublabel": "Availability + use", "tag": "Include failed attempts",
"sublabel": "Join evidence to the workflow",
"pos": [ "pos": [
20, 60,
500 1600
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "decide", "id": "cost",
"type": "backend", "type": "frontend",
"label": "Ongoing monitoring", "label": "Allocated cost / accepted tasks",
"sublabel": "Review + maintenance", "tag": "Cost per accepted outcome",
"sublabel": "Missing buckets = partial",
"pos": [ "pos": [
20, 60,
620 1970
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
},
{
"id": "confidence-source",
"type": "external",
"label": "Anonymous 1\u20135 survey",
"tag": "Protect small groups",
"sublabel": "Join evidence to the workflow",
"pos": [
60,
2340
],
"size": [
740,
220
],
"icon": "none"
},
{
"id": "confidence",
"type": "frontend",
"label": "Confidence sum / valid responses",
"tag": "Verification confidence (1\u20135)",
"sublabel": "Show distribution + response rate",
"pos": [
60,
2710
],
"size": [
740,
220
],
"icon": "none"
},
{
"id": "interpret",
"type": "backend",
"label": "Compare with baseline",
"tag": "Interpret, then act",
"sublabel": "Like workflows + source coverage\nZero denominator = N/A\nTrends are not causal ROI\nFix gaps; improve the workflow",
"pos": [
60,
3080
],
"size": [
740,
400
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "success-source-success",
"from": "collect", "from": "success-source",
"to": "validate", "to": "success",
"label": "prepare" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
395
]
}, },
{ {
"id": "flow-1", "id": "quality-source-quality",
"from": "validate", "from": "quality-source",
"to": "calculate", "to": "quality",
"label": "collect" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1135
]
}, },
{ {
"id": "flow-2", "id": "cost-source-cost",
"from": "calculate", "from": "cost-source",
"to": "balance", "to": "cost",
"label": "calculate" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1875
]
}, },
{ {
"id": "flow-3", "id": "confidence-source-confidence",
"from": "balance", "from": "confidence-source",
"to": "weigh", "to": "confidence",
"label": "extend" "label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
2615
]
}, },
{ {
"id": "flow-4", "id": "success-interpret",
"from": "weigh", "from": "success",
"to": "decide", "to": "interpret",
"label": "repeat" "label": "",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
600.0
],
[
850,
3280.0
]
],
"labelAt": [
620,
3040
]
},
{
"id": "quality-interpret",
"from": "quality",
"to": "interpret",
"label": "",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
1340.0
],
[
850,
3280.0
]
],
"labelAt": [
620,
3040
]
},
{
"id": "cost-interpret",
"from": "cost",
"to": "interpret",
"label": "",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
2080.0
],
[
850,
3280.0
]
],
"labelAt": [
620,
3040
]
},
{
"id": "confidence-interpret",
"from": "confidence",
"to": "interpret",
"label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
2985
]
} }
] ]
} }
+238 -76
View File
@@ -2,129 +2,291 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Measure skills adoption", "title": "TO-BE: Measurement",
"output": "static/diagrams/strategy-measurement/index.html", "output": "static/diagrams/strategy-measurement/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
2400,
1120
]
}, },
"components": [ "components": [
{ {
"id": "collect", "id": "success-source",
"type": "backend", "type": "external",
"label": "Establish baseline", "label": "Tracker + acceptance",
"sublabel": "Current workflow", "tag": "Matured task starts",
"sublabel": "Join evidence to the workflow",
"pos": [ "pos": [
20, 50,
20 70
], ],
"size": [ "size": [
180, 580,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "validate", "id": "success",
"type": "backend", "type": "frontend",
"label": "Join evidence", "label": "Accepted / eligible starts \u00d7 100",
"sublabel": "Tasks / versions / costs", "tag": "Task success (%)",
"sublabel": "Failures + unresolved retained",
"pos": [ "pos": [
260, 770,
20 70
], ],
"size": [ "size": [
180, 700,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "calculate", "id": "quality-source",
"type": "backend", "type": "external",
"label": "Measure the pilot", "label": "Changes + defects",
"sublabel": "Outcomes / quality", "tag": "Completed follow-up",
"sublabel": "Join evidence to the workflow",
"pos": [ "pos": [
260, 50,
150 320
], ],
"size": [ "size": [
180, 580,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "balance", "id": "quality",
"type": "backend", "type": "frontend",
"label": "Report together", "label": "Affected / accepted changes \u00d7 100",
"sublabel": "Cost / confidence / gaps", "tag": "Escaped quality (%)",
"sublabel": "Count each affected change once",
"pos": [ "pos": [
20, 770,
150 320
], ],
"size": [ "size": [
180, 700,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "weigh", "id": "cost-source",
"type": "backend", "type": "external",
"label": "Broader adoption", "label": "Invoices + review minutes",
"sublabel": "Availability + use", "tag": "Include failed attempts",
"sublabel": "Join evidence to the workflow",
"pos": [ "pos": [
20, 50,
280 570
], ],
"size": [ "size": [
180, 580,
60 180
] ],
"icon": "none"
}, },
{ {
"id": "decide", "id": "cost",
"type": "backend", "type": "frontend",
"label": "Ongoing monitoring", "label": "Allocated cost / accepted tasks",
"sublabel": "Review + maintenance", "tag": "Cost per accepted outcome",
"sublabel": "Missing buckets = partial",
"pos": [ "pos": [
260, 770,
280 570
], ],
"size": [ "size": [
180, 700,
60 180
] ],
"icon": "none"
},
{
"id": "confidence-source",
"type": "external",
"label": "Anonymous 1\u20135 survey",
"tag": "Protect small groups",
"sublabel": "Join evidence to the workflow",
"pos": [
50,
820
],
"size": [
580,
180
],
"icon": "none"
},
{
"id": "confidence",
"type": "frontend",
"label": "Confidence sum / valid responses",
"tag": "Verification confidence (1\u20135)",
"sublabel": "Show distribution + response rate",
"pos": [
770,
820
],
"size": [
700,
180
],
"icon": "none"
},
{
"id": "interpret",
"type": "backend",
"label": "Compare with baseline",
"tag": "Interpret, then act",
"sublabel": "Like workflows + source coverage\nZero denominator = N/A\nTrends are not causal ROI\nFix gaps; improve the workflow",
"pos": [
1650,
325
],
"size": [
680,
400
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "success-source-success",
"from": "collect", "from": "success-source",
"to": "validate", "to": "success",
"label": "prepare" "label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default"
}, },
{ {
"id": "flow-1", "id": "quality-source-quality",
"from": "validate", "from": "quality-source",
"to": "calculate", "to": "quality",
"label": "collect" "label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default"
}, },
{ {
"id": "flow-2", "id": "cost-source-cost",
"from": "calculate", "from": "cost-source",
"to": "balance", "to": "cost",
"label": "calculate" "label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default"
}, },
{ {
"id": "flow-3", "id": "confidence-source-confidence",
"from": "balance", "from": "confidence-source",
"to": "weigh", "to": "confidence",
"label": "extend" "label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default"
}, },
{ {
"id": "flow-4", "id": "success-interpret",
"from": "weigh", "from": "success",
"to": "decide", "to": "interpret",
"label": "repeat" "label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1550,
160
],
[
1550,
525
]
]
},
{
"id": "quality-interpret",
"from": "quality",
"to": "interpret",
"label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1550,
410
],
[
1550,
525
]
]
},
{
"id": "cost-interpret",
"from": "cost",
"to": "interpret",
"label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1550,
660
],
[
1550,
525
]
]
},
{
"id": "confidence-interpret",
"from": "confidence",
"to": "interpret",
"label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1550,
910
],
[
1550,
525
]
]
} }
] ]
} }
@@ -2,129 +2,261 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Risk-tiered qualification", "title": "TO-BE: Quality",
"output": "static/diagrams/strategy-quality-mobile/index.html", "output": "static/diagrams/strategy-quality-mobile/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
880,
2800
]
}, },
"components": [ "components": [
{ {
"id": "risk", "id": "selection",
"type": "security", "type": "external",
"label": "Classify risk", "label": "Right skill?",
"sublabel": "R1 / R2 / R3", "tag": "Selection evidence",
"sublabel": "Triggers, near misses, negatives",
"pos": [ "pos": [
20, 60,
20 120
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "plan", "id": "safety",
"type": "security", "type": "external",
"label": "Agree evaluation", "label": "Safe execution?",
"sublabel": "Task mix + thresholds", "tag": "Safety evidence",
"sublabel": "Permissions + adversarial trials",
"pos": [ "pos": [
20, 60,
140 490
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "trial", "id": "artifact",
"type": "security", "type": "external",
"label": "Repeat trials", "label": "Valid artifact?",
"sublabel": "Candidate vs approved", "tag": "Independent inspection",
"sublabel": "Tests + human assessment",
"pos": [ "pos": [
20, 60,
260 860
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "verify", "id": "outcome",
"type": "security", "type": "external",
"label": "Verify artifacts", "label": "Useful outcome?",
"sublabel": "Independent checks", "tag": "Workflow evidence",
"sublabel": "Baseline + repeated trials",
"pos": [ "pos": [
20, 60,
380 1230
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
}, },
{ {
"id": "gate", "id": "gate",
"type": "security", "type": "security",
"label": "All gates pass?", "label": "Every gate must pass",
"sublabel": "Safety cannot be traded", "tag": "Risk sets evaluation depth",
"sublabel": "No averaging away harm\nCritical effect blocks promotion\nMissing coverage = incomplete",
"pos": [ "pos": [
20, 60,
500 1600
], ],
"size": [ "size": [
180, 740,
60 310
] ],
"icon": "none"
}, },
{ {
"id": "promote", "id": "accept",
"type": "security", "type": "backend",
"label": "Promote or hold", "label": "Accept evidence",
"sublabel": "Owner decision", "tag": "All mandatory gates passed",
"sublabel": "Proceed to scoped approval",
"pos": [ "pos": [
20, 60,
620 2060
], ],
"size": [ "size": [
180, 740,
60 220
] ],
"icon": "none"
},
{
"id": "rework",
"type": "security",
"label": "Rework / retest",
"tag": "Any failure or missing evidence",
"sublabel": "Keep failures in the record",
"pos": [
60,
2430
],
"size": [
740,
220
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "selection-gate",
"from": "risk", "from": "selection",
"to": "plan",
"label": "Scope"
},
{
"id": "flow-1",
"from": "plan",
"to": "trial",
"label": "freeze plan"
},
{
"id": "flow-2",
"from": "trial",
"to": "verify",
"label": "Assess"
},
{
"id": "flow-3",
"from": "verify",
"to": "gate", "to": "gate",
"label": "review evidence" "label": "",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
230.0
],
[
850,
1755.0
]
],
"labelAt": [
620,
1560
]
}, },
{ {
"id": "flow-4", "id": "safety-gate",
"from": "safety",
"to": "gate",
"label": "",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
600.0
],
[
850,
1755.0
]
],
"labelAt": [
620,
1560
]
},
{
"id": "artifact-gate",
"from": "artifact",
"to": "gate",
"label": "",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
850,
970.0
],
[
850,
1755.0
]
],
"labelAt": [
620,
1560
]
},
{
"id": "outcome-gate",
"from": "outcome",
"to": "gate",
"label": "",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1505
]
},
{
"id": "gate-accept",
"from": "gate", "from": "gate",
"to": "promote", "to": "accept",
"label": "decide" "label": "Pass",
"fromSide": "bottom",
"toSide": "top",
"route": "straight",
"width": 4,
"variant": "default",
"labelAt": [
610,
1965
]
},
{
"id": "gate-rework",
"from": "gate",
"to": "rework",
"label": "Fail",
"fromSide": "right",
"toSide": "right",
"route": "straight",
"width": 4,
"variant": "security",
"via": [
[
824,
1755.0
],
[
824,
2540.0
]
],
"labelAt": [
620,
2390
]
} }
] ]
} }
+214 -78
View File
@@ -2,129 +2,265 @@
"schema_version": 1, "schema_version": 1,
"diagram_type": "architecture", "diagram_type": "architecture",
"meta": { "meta": {
"title": "TO-BE: Risk-tiered qualification", "title": "TO-BE: Quality",
"output": "static/diagrams/strategy-quality/index.html", "output": "static/diagrams/strategy-quality/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"legend": { "legend": {
"mode": "hidden" "mode": "hidden"
} },
"viewBox": [
2000,
1050
]
}, },
"components": [ "components": [
{ {
"id": "risk", "id": "selection",
"type": "security", "type": "external",
"label": "Classify risk", "label": "Right skill?",
"sublabel": "R1 / R2 / R3", "tag": "Selection evidence",
"sublabel": "Triggers, near misses, negatives",
"pos": [ "pos": [
20, 60,
20 70
], ],
"size": [ "size": [
180, 510,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "plan", "id": "safety",
"type": "security", "type": "external",
"label": "Agree evaluation", "label": "Safe execution?",
"sublabel": "Task mix + thresholds", "tag": "Safety evidence",
"sublabel": "Permissions + adversarial trials",
"pos": [ "pos": [
260, 60,
20 310
], ],
"size": [ "size": [
180, 510,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "trial", "id": "artifact",
"type": "security", "type": "external",
"label": "Repeat trials", "label": "Valid artifact?",
"sublabel": "Candidate vs approved", "tag": "Independent inspection",
"sublabel": "Tests + human assessment",
"pos": [ "pos": [
260, 60,
150 550
], ],
"size": [ "size": [
180, 510,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "verify", "id": "outcome",
"type": "security", "type": "external",
"label": "Verify artifacts", "label": "Useful outcome?",
"sublabel": "Independent checks", "tag": "Workflow evidence",
"sublabel": "Baseline + repeated trials",
"pos": [ "pos": [
20, 60,
150 790
], ],
"size": [ "size": [
180, 510,
60 170
] ],
"icon": "none"
}, },
{ {
"id": "gate", "id": "gate",
"type": "security", "type": "security",
"label": "All gates pass?", "label": "Every gate must pass",
"sublabel": "Safety cannot be traded", "tag": "Risk sets evaluation depth",
"sublabel": "No averaging away harm\nCritical effect blocks promotion\nMissing coverage = incomplete",
"pos": [ "pos": [
20, 740,
280 350
], ],
"size": [ "size": [
180, 520,
60 310
] ],
"icon": "none"
}, },
{ {
"id": "promote", "id": "accept",
"type": "security", "type": "backend",
"label": "Promote or hold", "label": "Accept evidence",
"sublabel": "Owner decision", "tag": "All mandatory gates passed",
"sublabel": "Proceed to scoped approval",
"pos": [ "pos": [
260, 1430,
280 160
], ],
"size": [ "size": [
180, 510,
60 190
] ],
"icon": "none"
},
{
"id": "rework",
"type": "security",
"label": "Rework / retest",
"tag": "Any failure or missing evidence",
"sublabel": "Keep failures in the record",
"pos": [
1430,
760
],
"size": [
510,
190
],
"icon": "none"
} }
], ],
"connections": [ "connections": [
{ {
"id": "flow-0", "id": "selection-gate",
"from": "risk", "from": "selection",
"to": "plan",
"label": "Scope"
},
{
"id": "flow-1",
"from": "plan",
"to": "trial",
"label": "freeze plan"
},
{
"id": "flow-2",
"from": "trial",
"to": "verify",
"label": "Assess"
},
{
"id": "flow-3",
"from": "verify",
"to": "gate", "to": "gate",
"label": "review evidence" "label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
650,
155
],
[
650,
505
]
]
}, },
{ {
"id": "flow-4", "id": "safety-gate",
"from": "safety",
"to": "gate",
"label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
650,
395
],
[
650,
505
]
]
},
{
"id": "artifact-gate",
"from": "artifact",
"to": "gate",
"label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
650,
635
],
[
650,
505
]
]
},
{
"id": "outcome-gate",
"from": "outcome",
"to": "gate",
"label": "",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
650,
875
],
[
650,
505
]
]
},
{
"id": "gate-accept",
"from": "gate", "from": "gate",
"to": "promote", "to": "accept",
"label": "decide" "label": "Pass",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "default",
"via": [
[
1340,
505
],
[
1340,
255
]
],
"labelAt": [
1330,
210
]
},
{
"id": "gate-rework",
"from": "gate",
"to": "rework",
"label": "Fail",
"fromSide": "right",
"toSide": "left",
"route": "straight",
"width": 4,
"variant": "security",
"via": [
[
1340,
505
],
[
1340,
855
]
],
"labelAt": [
1325,
900
]
} }
] ]
} }
@@ -0,0 +1,57 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Stage 1: Delivery first",
"output": "static/diagrams/strategy-roadmap-adoption-mobile/index.html",
"quality_profile": "showcase",
"animation": "none",
"legend": {
"mode": "hidden"
}
},
"lanes": [
{
"id": "adoption",
"label": "Stage 1: Delivery first"
}
],
"mainPath": [
"delivery",
"remaining"
],
"nodes": [
{
"id": "delivery",
"lane": "adoption",
"col": 0,
"yOffset": -140.0,
"type": "backend",
"label": "Delivery first",
"sublabel": "Human + AI Agent; keep improving.",
"width": 340,
"height": 240
},
{
"id": "remaining",
"lane": "adoption",
"col": 0,
"yOffset": 140.0,
"type": "backend",
"label": "Other SDLC phases",
"sublabel": "Expand adoption; order not yet decided.",
"width": 340,
"height": 240
}
],
"edges": [
{
"id": "flow-0",
"from": "delivery",
"to": "remaining",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
}
]
}
@@ -0,0 +1,57 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Stage 1: Delivery first",
"output": "static/diagrams/strategy-roadmap-adoption/index.html",
"quality_profile": "showcase",
"animation": "none",
"legend": {
"mode": "hidden"
}
},
"lanes": [
{
"id": "adoption",
"label": "Stage 1: Delivery first"
}
],
"mainPath": [
"delivery",
"remaining"
],
"nodes": [
{
"id": "delivery",
"lane": "adoption",
"col": 0,
"yOffset": 0,
"type": "backend",
"label": "Delivery first",
"sublabel": "Human + AI Agent; keep improving.",
"width": 340,
"height": 240
},
{
"id": "remaining",
"lane": "adoption",
"col": 1,
"yOffset": 0,
"type": "backend",
"label": "Other SDLC phases",
"sublabel": "Expand adoption; order not yet decided.",
"width": 340,
"height": 240
}
],
"edges": [
{
"id": "flow-0",
"from": "delivery",
"to": "remaining",
"role": "main",
"fromSide": "right",
"toSide": "left"
}
]
}
@@ -0,0 +1,143 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Improve within each phase",
"output": "static/diagrams/strategy-roadmap-loop-mobile/index.html",
"quality_profile": "showcase",
"animation": "none",
"legend": {
"mode": "hidden"
}
},
"lanes": [
{
"id": "adoption",
"label": "Improve within each phase"
}
],
"mainPath": [
"understand",
"baseline",
"topology"
],
"nodes": [
{
"id": "understand",
"lane": "adoption",
"col": 0,
"yOffset": -700.0,
"type": "backend",
"label": "Understand today",
"sublabel": "Actual practices, tools and problems.",
"width": 340,
"height": 240
},
{
"id": "baseline",
"lane": "adoption",
"col": 0,
"yOffset": -420.0,
"type": "backend",
"label": "Set metrics / KPIs",
"sublabel": "Establish a baseline before changes.",
"width": 340,
"height": 240
},
{
"id": "topology",
"lane": "adoption",
"col": 0,
"yOffset": -140.0,
"type": "backend",
"label": "Engineer topology",
"sublabel": "Connect skills, triggers and handoffs.",
"width": 340,
"height": 240
},
{
"id": "use",
"lane": "adoption",
"col": 0,
"yOffset": 140.0,
"type": "backend",
"label": "Use skills",
"sublabel": "Real work with a human + AI Agent.",
"width": 340,
"height": 240
},
{
"id": "collect",
"lane": "adoption",
"col": 0,
"yOffset": 420.0,
"type": "backend",
"label": "Collect results",
"sublabel": "Metrics, friction and human feedback.",
"width": 340,
"height": 240
},
{
"id": "reassess",
"lane": "adoption",
"col": 0,
"yOffset": 700.0,
"type": "backend",
"label": "Understand anew",
"sublabel": "Compare with baseline; repeat the loop.",
"width": 340,
"height": 240
}
],
"edges": [
{
"id": "flow-0",
"from": "understand",
"to": "baseline",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-1",
"from": "baseline",
"to": "topology",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-2",
"from": "topology",
"to": "use",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-3",
"from": "use",
"to": "collect",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-4",
"from": "collect",
"to": "reassess",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "repeat",
"from": "reassess",
"to": "understand",
"role": "return",
"fromSide": "left",
"toSide": "left",
"route": "return-left"
}
]
}
@@ -0,0 +1,143 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "Improve within each phase",
"output": "static/diagrams/strategy-roadmap-loop/index.html",
"quality_profile": "showcase",
"animation": "none",
"legend": {
"mode": "hidden"
}
},
"lanes": [
{
"id": "adoption",
"label": "Improve within each phase"
}
],
"mainPath": [
"understand",
"baseline",
"topology"
],
"nodes": [
{
"id": "understand",
"lane": "adoption",
"col": 0,
"yOffset": -160,
"type": "backend",
"label": "Understand today",
"sublabel": "Actual practices, tools and problems.",
"width": 340,
"height": 240
},
{
"id": "baseline",
"lane": "adoption",
"col": 1,
"yOffset": -160,
"type": "backend",
"label": "Set metrics / KPIs",
"sublabel": "Establish a baseline before changes.",
"width": 340,
"height": 240
},
{
"id": "topology",
"lane": "adoption",
"col": 2,
"yOffset": -160,
"type": "backend",
"label": "Engineer topology",
"sublabel": "Connect skills, triggers and handoffs.",
"width": 340,
"height": 240
},
{
"id": "use",
"lane": "adoption",
"col": 2,
"yOffset": 160,
"type": "backend",
"label": "Use skills",
"sublabel": "Real work with a human + AI Agent.",
"width": 340,
"height": 240
},
{
"id": "collect",
"lane": "adoption",
"col": 1,
"yOffset": 160,
"type": "backend",
"label": "Collect results",
"sublabel": "Metrics, friction and human feedback.",
"width": 340,
"height": 240
},
{
"id": "reassess",
"lane": "adoption",
"col": 0,
"yOffset": 160,
"type": "backend",
"label": "Understand anew",
"sublabel": "Compare with baseline; repeat the loop.",
"width": 340,
"height": 240
}
],
"edges": [
{
"id": "flow-0",
"from": "understand",
"to": "baseline",
"role": "main",
"fromSide": "right",
"toSide": "left"
},
{
"id": "flow-1",
"from": "baseline",
"to": "topology",
"role": "main",
"fromSide": "right",
"toSide": "left"
},
{
"id": "flow-2",
"from": "topology",
"to": "use",
"role": "main",
"fromSide": "bottom",
"toSide": "top"
},
{
"id": "flow-3",
"from": "use",
"to": "collect",
"role": "main",
"fromSide": "left",
"toSide": "right"
},
{
"id": "flow-4",
"from": "collect",
"to": "reassess",
"role": "main",
"fromSide": "left",
"toSide": "right"
},
{
"id": "repeat",
"from": "reassess",
"to": "understand",
"role": "return",
"fromSide": "top",
"toSide": "bottom",
"route": "straight"
}
]
}
+16 -81
View File
@@ -2,7 +2,7 @@
"schema_version": 2, "schema_version": 2,
"diagram_type": "workflow", "diagram_type": "workflow",
"meta": { "meta": {
"title": "Recommended adoption roadmap", "title": "Two stages toward Target",
"output": "static/diagrams/strategy-roadmap-mobile/index.html", "output": "static/diagrams/strategy-roadmap-mobile/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"animation": "none", "animation": "none",
@@ -13,107 +13,42 @@
"lanes": [ "lanes": [
{ {
"id": "adoption", "id": "adoption",
"label": "Recommended sequence" "label": "Two stages toward Target"
} }
], ],
"mainPath": [ "mainPath": [
"assessment", "skills",
"readiness", "autonomy"
"pilot",
"expansion",
"sustain"
], ],
"nodes": [ "nodes": [
{ {
"id": "assessment", "id": "skills",
"lane": "adoption", "lane": "adoption",
"col": 0, "col": 0,
"yOffset": -140.0,
"type": "backend", "type": "backend",
"label": "Understand today", "label": "Adopt skills",
"sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.", "sublabel": "Human + AI Agent in each SDLC phase.",
"tag": "Exit: Workflow understood",
"width": 340, "width": 340,
"height": 460, "height": 240
"yOffset": -1000
}, },
{ {
"id": "readiness", "id": "autonomy",
"lane": "adoption", "lane": "adoption",
"col": 0, "col": 0,
"yOffset": 140.0,
"type": "backend", "type": "backend",
"label": "Prepare foundations", "label": "Autonomous Agent",
"sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.", "sublabel": "Future: use proven skills and practices.",
"tag": "Exit: Critical gaps closed",
"width": 340, "width": 340,
"height": 460, "height": 240
"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": [ "edges": [
{ {
"id": "flow-0", "id": "flow-0",
"from": "assessment", "from": "skills",
"to": "readiness", "to": "autonomy",
"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", "role": "main",
"fromSide": "bottom", "fromSide": "bottom",
"toSide": "top" "toSide": "top"
+16 -76
View File
@@ -2,7 +2,7 @@
"schema_version": 2, "schema_version": 2,
"diagram_type": "workflow", "diagram_type": "workflow",
"meta": { "meta": {
"title": "Recommended adoption roadmap", "title": "Two stages toward Target",
"output": "static/diagrams/strategy-roadmap/index.html", "output": "static/diagrams/strategy-roadmap/index.html",
"quality_profile": "showcase", "quality_profile": "showcase",
"animation": "none", "animation": "none",
@@ -13,102 +13,42 @@
"lanes": [ "lanes": [
{ {
"id": "adoption", "id": "adoption",
"label": "Recommended sequence" "label": "Two stages toward Target"
} }
], ],
"mainPath": [ "mainPath": [
"assessment", "skills",
"readiness", "autonomy"
"pilot",
"expansion",
"sustain"
], ],
"nodes": [ "nodes": [
{ {
"id": "assessment", "id": "skills",
"lane": "adoption", "lane": "adoption",
"col": 0, "col": 0,
"yOffset": 0,
"type": "backend", "type": "backend",
"label": "Understand today", "label": "Adopt skills",
"sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.", "sublabel": "Human + AI Agent in each SDLC phase.",
"tag": "Exit: Workflow understood",
"width": 340, "width": 340,
"height": 460 "height": 240
}, },
{ {
"id": "readiness", "id": "autonomy",
"lane": "adoption", "lane": "adoption",
"col": 1, "col": 1,
"yOffset": 0,
"type": "backend", "type": "backend",
"label": "Prepare foundations", "label": "Autonomous Agent",
"sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.", "sublabel": "Future: use proven skills and practices.",
"tag": "Exit: Critical gaps closed",
"width": 340, "width": 340,
"height": 460 "height": 240
},
{
"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": [ "edges": [
{ {
"id": "flow-0", "id": "flow-0",
"from": "assessment", "from": "skills",
"to": "readiness", "to": "autonomy",
"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", "role": "main",
"fromSide": "right", "fromSide": "right",
"toSide": "left" "toSide": "left"
@@ -12,14 +12,14 @@
{"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[100,1540],"size":[240,100]}, {"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[100,1540],"size":[240,100]},
{"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[100,1760],"size":[240,100]}, {"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[100,1760],"size":[240,100]},
{"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[100,1980],"size":[240,100]}, {"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[100,1980],"size":[240,100]},
{"id":"code-scope","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,440],"size":[240,100]}, {"id":"code-scope","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,440],"size":[240,100]},
{"id":"code-architecture","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,660],"size":[240,100]}, {"id":"code-architecture","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,660],"size":[240,100]},
{"id":"code-ui-ux","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,880],"size":[240,100]}, {"id":"code-ui-ux","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,880],"size":[240,100]},
{"id":"code-planning","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1100],"size":[240,100]}, {"id":"code-planning","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1100],"size":[240,100]},
{"id":"code-delivery","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1320],"size":[240,100]}, {"id":"code-delivery","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1320],"size":[240,100]},
{"id":"code-uat","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1540],"size":[240,100]}, {"id":"code-uat","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1540],"size":[240,100]},
{"id":"code-security","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1760],"size":[240,100]}, {"id":"code-security","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1760],"size":[240,100]},
{"id":"code-releases","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[400,1980],"size":[240,100]} {"id":"code-releases","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[400,1980],"size":[240,100]}
], ],
"connections": [ "connections": [
+16 -16
View File
@@ -12,25 +12,25 @@
{"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[240,727.5],"size":[180,75]}, {"id":"uat","type":"backend","label":"UAT","sublabel":"06","pos":[240,727.5],"size":[180,75]},
{"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[240,457.5],"size":[180,75]}, {"id":"security","type":"backend","label":"Security","sublabel":"07","pos":[240,457.5],"size":[180,75]},
{"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[240,187.5],"size":[180,75]}, {"id":"releases","type":"backend","label":"Releases","sublabel":"08","pos":[240,187.5],"size":[180,75]},
{"id":"code-scope","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[510,37.5],"size":[180,75]}, {"id":"code-scope","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[510,37.5],"size":[180,75]},
{"id":"code-architecture","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[780,37.5],"size":[180,75]}, {"id":"code-architecture","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[780,37.5],"size":[180,75]},
{"id":"code-ui-ux","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[1005,457.5],"size":[180,75]}, {"id":"code-ui-ux","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[1005,457.5],"size":[180,75]},
{"id":"code-planning","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[780,877.5],"size":[180,75]}, {"id":"code-planning","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[780,877.5],"size":[180,75]},
{"id":"code-delivery","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[510,877.5],"size":[180,75]}, {"id":"code-delivery","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[510,877.5],"size":[180,75]},
{"id":"code-uat","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[240,877.5],"size":[180,75]}, {"id":"code-uat","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[240,877.5],"size":[180,75]},
{"id":"code-security","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[15,457.5],"size":[180,75]}, {"id":"code-security","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[15,457.5],"size":[180,75]},
{"id":"code-releases","type":"frontend","label":"Code agent","sublabel":"Human-controlled","pos":[240,37.5],"size":[180,75]} {"id":"code-releases","type":"frontend","label":"AI Agent","sublabel":"Human-controlled","pos":[240,37.5],"size":[180,75]}
], ],
"connections": [ "connections": [
{"id":"govern-scope","from":"agent","to":"scope","label":"","variant":"security","fromSide":"right","toSide":"bottom","via":[[750,495],[750,292.5],[600,292.5]]}, {"id":"govern-scope","from":"agent","to":"scope","label":"","variant":"security","fromSide":"top","toSide":"bottom","route":"straight"},
{"id":"govern-architecture","from":"agent","to":"architecture","label":"","variant":"security","fromSide":"right","toSide":"bottom","via":[[765,495],[765,345],[870,345]]}, {"id":"govern-architecture","from":"agent","to":"architecture","label":"","variant":"security","fromSide":"top","toSide":"bottom","via":[[600,345],[870,345]]},
{"id":"govern-ui-ux","from":"agent","to":"ui-ux","label":"","variant":"security","fromSide":"right","toSide":"left"}, {"id":"govern-ui-ux","from":"agent","to":"ui-ux","label":"","variant":"security","fromSide":"right","toSide":"left","route":"straight"},
{"id":"govern-planning","from":"agent","to":"planning","label":"","variant":"security","fromSide":"right","toSide":"top","via":[[765,495],[765,690],[870,690]]}, {"id":"govern-planning","from":"agent","to":"planning","label":"","variant":"security","fromSide":"bottom","toSide":"top","via":[[600,645],[870,645]]},
{"id":"govern-delivery","from":"agent","to":"delivery","label":"","variant":"security","fromSide":"bottom","toSide":"top"}, {"id":"govern-delivery","from":"agent","to":"delivery","label":"","variant":"security","fromSide":"bottom","toSide":"top","route":"straight"},
{"id":"govern-uat","from":"agent","to":"uat","label":"","variant":"security","fromSide":"left","toSide":"top","via":[[435,495],[435,690],[330,690]]}, {"id":"govern-uat","from":"agent","to":"uat","label":"","variant":"security","fromSide":"bottom","toSide":"top","via":[[600,645],[330,645]]},
{"id":"govern-security","from":"agent","to":"security","label":"","variant":"security","fromSide":"left","toSide":"right"}, {"id":"govern-security","from":"agent","to":"security","label":"","variant":"security","fromSide":"left","toSide":"right","route":"straight"},
{"id":"govern-releases","from":"agent","to":"releases","label":"","variant":"security","fromSide":"left","toSide":"bottom","via":[[435,495],[435,345],[330,345]]}, {"id":"govern-releases","from":"agent","to":"releases","label":"","variant":"security","fromSide":"top","toSide":"bottom","via":[[600,345],[330,345]]},
{"id":"stage-1","from":"scope","to":"architecture","label":"Then","variant":"emphasis","fromSide":"right","toSide":"left"}, {"id":"stage-1","from":"scope","to":"architecture","label":"Then","variant":"emphasis","fromSide":"right","toSide":"left"},
{"id":"stage-2","from":"architecture","to":"ui-ux","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"}, {"id":"stage-2","from":"architecture","to":"ui-ux","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"},
{"id":"stage-3","from":"ui-ux","to":"planning","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"}, {"id":"stage-3","from":"ui-ux","to":"planning","label":"Then","variant":"emphasis","fromSide":"bottom","toSide":"top"},
+3
View File
@@ -29,6 +29,9 @@ Include `title` and `description` frontmatter. Place each page in its owning sec
| Portal maintenance and reference | `docs/core/` | `sidebars.ts` | | Portal maintenance and reference | `docs/core/` | `sidebars.ts` |
Keep these content roots disjoint. Preserve existing URLs when moving pages. Specific AI initiatives and organizational reporting lines remain subject to agreement. Keep these content roots disjoint. Preserve existing URLs when moving pages. Specific AI initiatives and organizational reporting lines remain subject to agreement.
Every published documentation page must have a navigable sidebar entry. Nested references are welcome; hidden documentation is not. Linked category overviews count as entries, but an in-page anchor is not a separate document. Portal-maintenance pages keep their existing `/docs/` routes and dedicated sidebar, reachable from Contribution → Portal maintenance.
`pnpm verify:build` compares all Docusaurus-generated document IDs/routes with resolved sidebar trees (including linked category overviews), checks reachability from the navbar, rejects missing/duplicate entries and unclassified HTML routes, and checks every document’s local links and anchors. The inventory is written to `.cache/sidebar-coverage.json`. Only the homepage, not-found page, search-results UI, legacy Strategy redirect and standalone diagram viewers are non-document HTML routes; no documentation page is exempt.
## Validate the change ## Validate the change
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: measurement id: measurement
title: Skills Measurement title: Skills Measurement
sidebar_label: Skills Measurement sidebar_label: Measurement
description: Which KPIs to collect during skills adoption, where the evidence comes from, how to calculate them and when to report. description: Which KPIs to collect during skills adoption, where the evidence comes from, how to calculate them and when to report.
hide_table_of_contents: true hide_table_of_contents: true
--- ---
+26 -32
View File
@@ -1,7 +1,7 @@
--- ---
id: roadmap id: roadmap
title: Roadmap title: Roadmap
description: Assess current code quality and delivery processes, close critical gaps, then pilot, expand and sustain AI adoption. description: Build proven skills in human and AI Agent work, starting with Delivery, as the foundation for the future autonomous SDLC.
hide_table_of_contents: true hide_table_of_contents: true
--- ---
@@ -9,71 +9,65 @@ import ArchifyDiagram from '@site/src/components/ArchifyDiagram';
<div className="strategy-content"> <div className="strategy-content">
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. ## Two stages toward Target
<ArchifyDiagram name="strategy-roadmap" title="Understand today → Prepare foundations → Pilot → Expand → Sustain" story /> <ArchifyDiagram name="strategy-roadmap" title="Adopt skills → Future Autonomous Agent" story />
**Recommended sequence · Target operating model, not an approved schedule or deployed governance.** **Humans direct, guide and review throughout.** Stage 1 standardizes skills in human + AI Agent work across the SDLC. Proven skills, measurements and working practices become the foundation for Stage 2: the future Autonomous Agent in [Target](./target-operating-model.mdx).
## 1. Understand code quality and delivery Target operating model—not deployed autonomy, an agreed schedule or approved KPI thresholds.
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. ## Stage 1: Delivery first
**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. <ArchifyDiagram name="strategy-roadmap-adoption" title="Delivery first → Other SDLC phases, order undecided" story />
## 2. Fix critical readiness gaps Start with Delivery, then extend skills adoption to the remaining SDLC phases. Their order is not yet decided. **Delivery keeps improving as adoption expands.**
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. ## Improve within each phase
**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. <ArchifyDiagram name="strategy-roadmap-loop" title="Each phase: understand → baseline → topology → use → collect → reassess → repeat" story />
## 3. Run a bounded pilot Repeat this loop within every phase—not as six global roadmap stages. Skills topology defines **what skills exist, how they relate, when they are used and how they work together**, including handoffs—not just a catalogue.
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. ## Delivery in practice
**Output:** a pilot report and a continue, adjust or stop decision. Missing evidence is a reason to investigate, not proof of improvement. **Understand the engineering path first:** codebase structure and existing principles → system-logic source → tests → builds and packaging → deployments. Inspect actual practices, tooling, controls and problems with the people doing the work, rather than assuming the documented process is the real one.
## 4. Expand only on evidence **Establish the comparison, then engineer skills.** Define metrics/KPIs and a baseline for comparable work before changing it. [Skills Measurement](./measurement.mdx) explains collection and calculation. Connect reviewed skills to the engineering path, with clear triggers, dependencies and acceptance checks.
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. **Use, collect, compare, repeat.** A human works with an AI Agent on real changes. Collect results, friction and feedback alongside quality, cost and verification effort. Compare with the baseline to understand the new current situation; revise the skills and working practices, then run the loop again.
**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>Supporting gate evidence and hold conditions</summary> <summary>Supporting gate evidence and hold conditions</summary>
These are recommended implementation gates, not assigned human owners, approved thresholds or installed integrations. Agree acceptance criteria for each selected workflow before evaluating it. These are recommended implementation gates, not assigned human owners, approved thresholds or installed integrations. Agree acceptance criteria for each selected workflow before evaluating it.
### Assessment → readiness ### Understand the current situation
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. Sample representative repositories and changes. Retain repository revisions, tests and CI reliability, maintainability, technical debt and security findings. Trace ownership, review and release practices, handoffs, rework and bottlenecks. Keep baseline findings, a process map and prioritized gaps; hold selection if the sample cannot explain how work is delivered and accepted.
### Readiness → pilot ### Prepare safe, measurable human + AI work
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. 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.
### Pilot → expansion Close critical gaps such as exposed secrets, unclear approval authority or unreliable acceptance checks. Broader refactoring and non-critical debt can run alongside the selected work. Stage 1 introduces no new privileged autonomous actions.
### Engineer and evaluate connected skills
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. 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.
### Expansion → wider authority ### Extend adoption without stopping improvement
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. 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.
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. Use evidence to choose the next SDLC phase; no second or third phase is assigned here. Continue the improvement loop in Delivery and every phase already using skills.
### Sustained operation ### Prepare for the future Autonomous Agent
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. Stage 2 builds on proven skills, measurements and working practices—not permission inferred from adoption alone. Before granting scoped execution authority, prove observation and evidence assembly, then proposing and routing work. 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, while humans retain direction and review. Keep sensitive decisions human. Changed client, provider, data or authority boundaries reopen the relevant checks.
</details> </details>
**Next:** [Return to the strategy destination →](./target-operating-model.mdx) **Next:** [Stage - 01 - Skills Adoption →](./stage-01-skills-adoption.mdx)
</div> </div>
+1 -1
View File
@@ -44,6 +44,6 @@ Owner ratification is required before adoption. Review earlier after incidents o
</details> </details>
**Next:** [Use exit evidence to choose the next rollout stage →](./roadmap.mdx) **Next:** [Stage - 02 - Autonomous Agent →](./stage-02-autonomous-agent.mdx)
</div> </div>
@@ -0,0 +1,36 @@
---
id: stage-01-skills-adoption
title: Stage - 01 - Skills Adoption
description: Build proven skills and measurable working practices through human and AI Agent collaboration, starting with Delivery.
hide_table_of_contents: true
---
<div className="strategy-content">
**Human + AI Agent → proven skills and working practices → future Autonomous Agent.**
This first stage of the [Roadmap](./roadmap.mdx) standardizes skills in real human + AI Agent work. It builds the foundation for [Target](./target-operating-model.mdx), while people direct the work, verify outcomes and improve the skills.
## Delivery first
Start with the engineering path: codebase structure and existing principles → system-logic source → tests → builds and packaging → deployments. Understand actual practices, tooling, controls and problems before changing them.
Adoption then extends to other SDLC phases; their order is not yet decided. Delivery keeps improving as adoption expands.
## Repeat within each phase
**Understand the current situation → establish KPIs and a baseline → engineer skills topology → use skills in real work → collect metrics and feedback → understand the new current situation → repeat.**
Skills topology connects what skills exist, how they relate, when they are used and how they work together. Compare accepted outcomes, quality, cost and review effort with the baseline; use friction and feedback to revise the skills and working practices. These are repeating steps within each phase, not additional roadmap stages.
## Put the foundation into practice
- [Governance](./skills-governance.mdx) — define ownership, authority and approval of skills.
- [Quality](./skills-quality.mdx) — qualify skills and verify their outcomes before wider use.
- [Adoption](./skills-adoption.mdx) — coach bounded workflows with a supported client and a manual path.
- [Measurement](./measurement.mdx) — establish KPIs, collect evidence and compare outcomes with the baseline.
- [Maintenance](./skills-maintenance.mdx) — review feedback, control versions and recover from unsafe updates.
Proven skills, measurements and working practices support [Stage - 02 - Autonomous Agent](./stage-02-autonomous-agent.mdx); adoption alone does not grant autonomous execution authority.
</div>
@@ -0,0 +1,24 @@
---
id: stage-02-autonomous-agent
title: Stage - 02 - Autonomous Agent
description: Introduce the future Autonomous Agent on proven skills and measured practices while humans retain direction and review.
hide_table_of_contents: true
---
<div className="strategy-content">
**Proven skills and measured practices → future Autonomous Agent → human-directed SDLC.**
This future stage of the [Roadmap](./roadmap.mdx) builds on [Stage - 01 - Skills Adoption](./stage-01-skills-adoption.mdx). The destination is [Target](./target-operating-model.mdx): an Autonomous Agent that executes and governs the SDLC cycle, with humans retaining direction and review.
## Build on what has been proven
Carry forward proven skills, measurements and working practices from real human + AI Agent work. The skills foundation provides [governance](./skills-governance.mdx), [quality checks](./skills-quality.mdx), [adoption feedback](./skills-adoption.mdx), [measurement](./measurement.mdx) and [maintenance](./skills-maintenance.mdx); it is not permission to expand authority automatically.
## Introduce authority deliberately
Establish reliable observation and evidence assembly, then proposing and routing work, before granting scoped execution authority. The Roadmap describes recommended controls and hold conditions, including exact-artifact approvals, independent review, recovery and human takeover. Sensitive decisions remain human; missing evidence never means permission.
This is future direction, not deployed autonomy, agreed delivery dates or approved KPI thresholds. The transition depends on reviewed evidence and explicit authority decisions, not adoption volume alone.
</div>
+2 -2
View File
@@ -16,7 +16,7 @@ import ArchifyDiagram from '@site/src/components/ArchifyDiagram';
<ArchifyDiagram name="strategy-target" title="Autonomous agent executes and governs the SDLC; humans direct, guide and review" story /> <ArchifyDiagram name="strategy-target" title="Autonomous agent executes and governs the SDLC; humans direct, guide and review" story />
An autonomous agent executes and governs every phase. Humans direct, guide and review it; a separate human-controlled code agent contributes to each phase from outside the cycle. An autonomous agent executes and governs every phase. Humans direct, guide and review it; a separate human-controlled AI Agent contributes to each phase from outside the cycle.
**Solid arrows:** lifecycle · **Dotted spokes:** execution and governance · **Dashed arrows:** contributions. **Solid arrows:** lifecycle · **Dotted spokes:** execution and governance · **Dashed arrows:** contributions.
@@ -162,6 +162,6 @@ Success means accepted outcomes with maintained quality, controlled risk and und
</details> </details>
**Next:** [Decide who owns and approves each skill →](./skills-governance.mdx) **Next:** [Follow the Roadmap toward Target →](./roadmap.mdx)
</div> </div>
+1 -1
View File
@@ -15,7 +15,7 @@
"verify:build": "node scripts/verify.mjs --build", "verify:build": "node scripts/verify.mjs --build",
"diagrams:build": "node scripts/build-diagrams.mjs", "diagrams:build": "node scripts/build-diagrams.mjs",
"test": "node --test scripts/*.test.mjs", "test": "node --test scripts/*.test.mjs",
"test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/skills-measurement.cjs && node scripts/browser/roadmap.cjs", "test:browser": "node scripts/browser/sidebar-coverage.cjs && node scripts/browser/skills-stories.cjs && node scripts/browser/strategy-navigation.cjs && node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/target-midpoints.cjs && node scripts/browser/skills-measurement.cjs && node scripts/browser/roadmap.cjs && node scripts/browser/roadmap-timeline.cjs",
"kpi:example": "node scripts/ai-kpi.mjs" "kpi:example": "node scripts/ai-kpi.mjs"
}, },
"dependencies": { "dependencies": {
+2 -2
View File
@@ -125,9 +125,9 @@ const fs = require('node:fs');
await page.screenshot({path:`${output}/${width}.png`, fullPage:true}); await page.screenshot({path:`${output}/${width}.png`, fullPage:true});
await frame.locator('#btn-theme').click(); await theme('dark'); await frame.locator('#btn-theme').click(); await theme('dark');
// Exercise keyboard navigation without racing the portal's smooth scroll. // Exercise keyboard navigation without racing the portal's smooth scroll.
await page.locator('.strategy-content a[href$="/strategy/skills-governance/"]').filter({visible:true}).focus(); await page.locator('.strategy-content a[href$="/strategy/roadmap/"]').filter({visible:true}).focus();
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await page.waitForURL('**/strategy/skills-governance/'); await page.waitForURL('**/strategy/roadmap/');
await page.goBack({waitUntil:'domcontentloaded'}); await page.goBack({waitUntil:'domcontentloaded'});
await page.waitForURL('**/strategy/target-operating-model/'); await page.waitForURL('**/strategy/target-operating-model/');
await page.getByRole('heading', {name:'Target operating model', exact:true, level:1}).waitFor(); await page.getByRole('heading', {name:'Target operating model', exact:true, level:1}).waitFor();
+136
View File
@@ -0,0 +1,136 @@
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const views = {
'strategy-roadmap': ['skills','autonomy'],
'strategy-roadmap-adoption': ['delivery','remaining'],
'strategy-roadmap-loop': ['understand','baseline','topology','use','collect','reassess'],
};
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/roadmap-timeline-${process.argv[3] || 'local'}`;
fs.mkdirSync(output, {recursive:true});
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
const results = [];
const inspect = async (frame, name, vertical) => {
await frame.locator('svg[role="img"]').waitFor();
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({
width:innerWidth, height:innerHeight,
edges:[...svg.querySelectorAll('path[data-edge-id]')].map(p => ({id:p.dataset.edgeId,box:p.getBoundingClientRect().toJSON()})),
cards:[...svg.querySelectorAll('[data-node-id]')].map(node => ({
id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(),
number:node.querySelector('[data-stage-number]').textContent,
text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a}))
}))
}));
const ids=views[name];
geometry.cards.sort((a,b)=>ids.indexOf(a.id)-ids.indexOf(b.id));
assert.deepEqual(geometry.cards.map(n => n.id), ids);
assert.deepEqual(geometry.cards.map(n => n.number), ids.length === 6 ? ids.map((_,i)=>`Step ${i+1}`) : name === 'strategy-roadmap' ? ['Stage 1','Stage 2'] : ['First','Then']);
assert.equal(geometry.edges.length,ids.length===6?6:1);
if(ids.length===6) assert(geometry.edges.some(e=>e.id==='repeat'));
for(const edge of geometry.edges) assert(edge.box.left>=0 && edge.box.right<=geometry.width+1 && edge.box.top>=0 && edge.box.bottom<=geometry.height+1, `clipped edge ${name} ${edge.id}`);
for (const [index, card] of geometry.cards.entries()) {
assert.equal(card.text.length,3);
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width+1 && card.box.bottom <= geometry.height+1, `outside iframe ${name} ${card.id}`);
for(const text of card.text) {
assert(text.size>=13, `small text ${name} ${card.id}: ${text.size}`);
assert(text.box.left>=card.box.left && text.box.right<=card.box.right+1 && text.box.top>=card.box.top && text.box.bottom<=card.box.bottom+1, `clipped text ${name} ${card.id}`);
}
for(let t=1;t<card.text.length;t++) assert(card.text[t-1].box.bottom<card.text[t].box.top,`text overlap ${card.id}`);
if(index && vertical) {assert.equal(geometry.cards[index-1].box.x,card.box.x);assert(geometry.cards[index-1].box.bottom<card.box.top);}
}
return geometry;
};
try {
for(const width of [1440,1024,390,320]) {
const context=await browser.newContext({viewport:{width,height:1000},colorScheme:'light'});
try {
const page=await context.newPage(); const errors=[]; page.on('pageerror',e=>errors.push(e.message));
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`,{waitUntil:'networkidle'})).status(),200);
await page.addStyleTag({content:'html {scroll-behavior:auto !important;}'});
assert.equal(await page.locator('.archify-panel--story').count(),3);
for(const name of Object.keys(views)) {
const panel=page.locator(`[data-diagram="${name}"]`);
const iframe=panel.locator(width<1100?'.story-mobile':'.story-desktop');
await iframe.scrollIntoViewIfNeeded();
const frame=await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const opener=panel.getByRole('button',{name:/^Expand /});
for(const theme of ['light','dark']) {
const frameTop=await iframe.evaluate(n=>n.getBoundingClientRect().top+scrollY);
await page.evaluate(y=>scrollTo({top:y-120,behavior:'instant'}),frameTop);
if(await frame.locator('html').getAttribute('data-theme')!==theme) await frame.locator('#btn-theme').click();
await page.waitForFunction(t=>document.documentElement.dataset.theme===t,theme);
const inline=await inspect(frame,name,width<1100);
await frame.locator('#btn-preset').focus();await page.keyboard.press('Enter');
await frame.locator('[data-preset-value="blueprint"]').focus();await page.keyboard.press('Enter');
await frame.waitForFunction(()=>document.documentElement.dataset.preset==='blueprint');
await inspect(frame,name,width<1100);
await frame.locator('#btn-preset').focus();await page.keyboard.press('Enter');
await frame.locator('[data-preset-value="classic"]').focus();await page.keyboard.press('Enter');
await frame.waitForFunction(()=>document.documentElement.dataset.preset==='classic');
await frame.locator('#btn-export').focus();await page.keyboard.press('Enter');
await frame.locator('[data-format="svg"]').focus();
const [download]=await Promise.all([page.waitForEvent('download'),page.keyboard.press('Enter')]);
assert.equal(await download.failure(),null);
const exported=`${output}/${width}-${theme}-${name}.svg`;
await download.saveAs(exported);
const svg=fs.readFileSync(exported,'utf8');
for(const id of views[name]) assert(svg.includes(`data-node-id="${id}"`));
assert(svg.includes('eldorado-roadmap-typography'));
await panel.screenshot({path:`${output}/${width}-${theme}-${name}-inline.png`});
// Scroll each card into the real viewport: tall iframe captures alone
// may contain compositor blanks even when DOM geometry is correct.
for(const card of inline.cards) {
const top=await iframe.evaluate(n=>n.getBoundingClientRect().top+scrollY);
await page.evaluate(y=>scrollTo({top:y,behavior:'instant'}),top+card.box.top-100);
await page.screenshot({path:`${output}/${width}-${theme}-${name}-${card.id}.png`});
}
await opener.focus();await page.keyboard.press('Enter');
const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
const fullIframe=panel.locator('dialog iframe');
const fullscreen=await (await fullIframe.elementHandle()).contentFrame();
await fullscreen.waitForFunction(t=>document.documentElement.dataset.theme===t,theme);
const expanded=await inspect(fullscreen,name,width<1000);
await page.screenshot({path:`${output}/${width}-${theme}-${name}-fullscreen.png`});
await close.focus();await page.keyboard.press('Escape');
await fullIframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
results.push({width,theme,name,inline,expanded});fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
}
}
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));assert.deepEqual(errors,[]);
} finally {await context.close();}
}
assert.equal(results.length,24);
for(const viewport of [{width:390,height:667},{width:320,height:568}]) {
const context=await browser.newContext({viewport});
try {
const page=await context.newPage();await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`,{waitUntil:'networkidle'});
await page.addStyleTag({content:'html {scroll-behavior:auto !important;}'});
for(const name of Object.keys(views)) {
const panel=page.locator(`[data-diagram="${name}"]`), opener=panel.getByRole('button',{name:/^Expand /});
await opener.focus();await page.keyboard.press('Enter');const modal=panel.locator('dialog'), close=modal.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
const iframe=modal.locator('iframe'), frame=await (await iframe.elementHandle()).contentFrame();
const geometry=await inspect(frame,name,true);
assert(await modal.evaluate(n=>n.scrollHeight>n.clientHeight),'short fullscreen must scroll');
for(const card of geometry.cards) {
await modal.evaluate((n,y)=>{n.scrollTop=y;},card.box.top);
const b=await iframe.boundingBox(), t=await modal.locator('.archify-modal-toolbar').boundingBox(), c=await close.boundingBox();
assert(c.y>=0 && c.y+c.height<=viewport.height);
assert(b.y+card.box.top>=t.y+t.height-1 && b.y+card.box.bottom<=viewport.height+1,`unreachable ${name} ${card.id}`);
}
await modal.evaluate(n=>{n.scrollTop=0;});
for(const selector of ['#btn-theme','#btn-preset','#btn-export']) {
const control=frame.locator(selector), b=await control.boundingBox();assert(b.x>=0 && b.x+b.width<=viewport.width && b.y>=0 && b.y+b.height<=viewport.height);await control.click();if(selector!=='#btn-theme')await control.click();
}
await close.focus();await page.keyboard.press('Escape');await iframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
await page.keyboard.press('Enter');await close.waitFor({state:'visible'});await close.click();await iframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
results.push({viewport,name,scrollable:geometry});fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
}
} finally {await context.close();}
}
assert.equal(results.length,30);
console.log('PASS 48 diagram contexts: three views × four widths × light/dark × inline/fullscreen; plus six short-phone scrolling/keyboard contexts.');
} finally {await browser.close();}
})().catch(error=>{console.error(error);process.exitCode=1;});
+10 -10
View File
@@ -8,8 +8,8 @@ const fs = require('node:fs');
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']}); const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
const results = []; const results = [];
try { try {
// Intermediate widths must use the readable vertical Roadmap, not shrink five cards. // Intermediate widths must use the readable vertical Roadmap, not shrink the loop.
for (const width of [768, 1024, 1280]) { for (const width of [768, 1024]) {
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'}); const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
try { try {
const page = await context.newPage(); const page = await context.newPage();
@@ -31,7 +31,7 @@ const fs = require('node:fs');
text:[...node.querySelectorAll('text')].map(t => ({text:t.textContent, box:t.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a})) 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) })).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']); assert.deepEqual(geometry.nodes.map(n => n.id), ['skills','autonomy']);
for (const [index, card] of geometry.nodes.entries()) { for (const [index, card] of geometry.nodes.entries()) {
assert(card.text.length > 0, `${width}: missing text in ${card.id}`); 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}`); 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}`);
@@ -61,16 +61,16 @@ const fs = require('node:fs');
assert.equal(response.status(), 200); assert.equal(response.status(), 200);
assert.equal(await page.locator('h1').innerText(), 'Roadmap'); assert.equal(await page.locator('h1').innerText(), 'Roadmap');
const headings = page.locator('.strategy-content > h2'); 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']); assert.deepEqual((await headings.allTextContents()).map(s => s.replace(/\u200b/g, '')), ['Two stages toward Target','Stage 1: Delivery first','Improve within each phase','Delivery in practice']);
const text = await page.locator('.strategy-content').innerText(); 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); for (const marker of ['Humans direct, guide and review throughout', 'Delivery keeps improving', 'system-logic source', 'baseline', 'Target operating model']) assert(text.includes(marker), marker);
assert(!text.includes('Supporting gate evidence and hold conditions\n\nThese are')); assert(!text.includes('Supporting gate evidence and hold conditions\n\nThese are'));
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click(); 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}); const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true});
await menu.first().waitFor({state:'visible'}); await menu.first().waitFor({state:'visible'});
assert.deepEqual((await menu.allTextContents()).slice(0,2), ['Target','Roadmap']); assert.deepEqual((await menu.allTextContents()).slice(0,2), ['Target','Roadmap']);
if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click(); if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
const panel = page.locator('.archify-panel--story'); const panel = page.locator('.archify-panel--story').first();
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile'); const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
await iframe.scrollIntoViewIfNeeded(); await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame(); const frame = await (await iframe.elementHandle()).contentFrame();
@@ -92,7 +92,7 @@ const fs = require('node:fs');
width:innerWidth, height:innerHeight, width:innerWidth, height:innerHeight,
minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a)) 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']); assert.deepEqual(geometry.nodes.map(n => n.id), ['skills','autonomy']);
for (const n of geometry.nodes) assert(n.x >= 0 && n.y >= 0 && n.right <= geometry.width + 1 && n.bottom <= geometry.height + 1); 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}`); assert(geometry.minLabel >= 10, `readable node labels: ${geometry.minLabel}`);
for (let i=1; i<geometry.nodes.length; i++) { for (let i=1; i<geometry.nodes.length; i++) {
@@ -125,9 +125,9 @@ const fs = require('node:fs');
const [download] = await Promise.all([page.waitForEvent('download'), frame.locator('[data-format="svg"]').click()]); const [download] = await Promise.all([page.waitForEvent('download'), frame.locator('[data-format="svg"]').click()]);
assert.equal(await download.failure(), null); assert.equal(await download.failure(), null);
await download.saveAs(`${output}/${width}.svg`); await download.saveAs(`${output}/${width}.svg`);
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Understand today')); assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Adopt skills'));
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('eldorado-roadmap-typography')); assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('eldorado-roadmap-typography'));
for (const id of ['assessment','readiness','pilot','expansion','sustain']) { for (const id of ['skills','autonomy']) {
await frame.locator(`svg[role="img"] [data-node-id="${id}"]`).screenshot({path:`${output}/${width}-${id}-card.png`}); await frame.locator(`svg[role="img"] [data-node-id="${id}"]`).screenshot({path:`${output}/${width}-${id}-card.png`});
} }
const opener = panel.getByRole('button', {name:/^Expand /}); const opener = panel.getByRole('button', {name:/^Expand /});
@@ -159,6 +159,6 @@ const fs = require('node:fs');
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2)); fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
await context.close(); await context.close();
} }
console.log(`PASS ${results.length} Roadmap viewports: visible stages/outputs, sidebar, diagram geometry, controls, export and links`); console.log(`PASS ${results.length} Roadmap viewports: three-level story, sidebar, diagram geometry, controls, export and links`);
} finally {await browser.close();} } finally {await browser.close();}
})().catch(error => {console.error(error); process.exitCode = 1;}); })().catch(error => {console.error(error); process.exitCode = 1;});
+13 -1
View File
@@ -23,7 +23,7 @@ const fs = require('node:fs');
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click(); 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}); const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true});
await menu.first().waitFor({state:'visible'}); await menu.first().waitFor({state:'visible'});
assert.deepEqual(await menu.allTextContents(), ['Target', 'Roadmap', 'Skills governance', 'Skills quality', 'Skills adoption', 'Skills Measurement', 'Skills maintenance']); assert.deepEqual(await menu.allTextContents(), ['Target', 'Roadmap', 'Stage - 01 - Skills Adoption', 'Stage - 02 - Autonomous Agent']);
assert((await menu.nth(0).getAttribute('href')).endsWith('/strategy/target-operating-model/')); assert((await menu.nth(0).getAttribute('href')).endsWith('/strategy/target-operating-model/'));
assert((await menu.nth(1).getAttribute('href')).endsWith('/strategy/roadmap/')); assert((await menu.nth(1).getAttribute('href')).endsWith('/strategy/roadmap/'));
assert.equal(await menu.filter({hasText:/^Target$/}).count(), 1); assert.equal(await menu.filter({hasText:/^Target$/}).count(), 1);
@@ -53,6 +53,9 @@ const fs = require('node:fs');
assert.deepEqual(metrics.edges.map(e=>e.slice(0,2)), ids.map((id,i)=>[id,ids[(i+1)%ids.length]])); assert.deepEqual(metrics.edges.map(e=>e.slice(0,2)), ids.map((id,i)=>[id,ids[(i+1)%ids.length]]));
assert(metrics.edges.every(e=>e[2]?.startsWith('url(')), 'all stages must have arrowheads'); assert(metrics.edges.every(e=>e[2]?.startsWith('url(')), 'all stages must have arrowheads');
assert.equal(metrics.nodes.length, 17); assert.equal(metrics.nodes.length, 17);
assert.equal(metrics.labels.filter(n => n.text === 'AI Agent').length, 8);
assert(!metrics.labels.some(n => /code[ -]agent/i.test(n.text)));
assert((await page.locator('.strategy-content').innerText()).includes('human-controlled AI Agent'));
assert(!metrics.nodes.some(n=>n.id==='humans')); assert(!metrics.nodes.some(n=>n.id==='humans'));
assert.equal(await frame.locator('[data-node-id="agent"] text').allTextContents().then(texts=>texts.includes('Human Controlled')), true); assert.equal(await frame.locator('[data-node-id="agent"] text').allTextContents().then(texts=>texts.includes('Human Controlled')), true);
assert.equal(metrics.relationships.length, 24); assert.equal(metrics.relationships.length, 24);
@@ -74,6 +77,15 @@ const fs = require('node:fs');
} }
assert(metrics.labels.every(n=>n.px>=14), JSON.stringify(metrics.labels)); assert(metrics.labels.every(n=>n.px>=14), JSON.stringify(metrics.labels));
assert.deepEqual(metrics.overflow, []); assert.deepEqual(metrics.overflow, []);
await frame.locator('#btn-export').click();
const [download] = await Promise.all([page.waitForEvent('download'), frame.locator('[data-format="svg"]').click()]);
assert.equal(await download.failure(), null);
const exportPath = `${output}/${width}-${route ? 'target' : 'landing'}.svg`;
await download.saveAs(exportPath);
const exported = fs.readFileSync(exportPath, 'utf8');
assert.equal((exported.match(/>AI Agent<\/text>/g) || []).length, 8);
assert(!/code[ -]agent/i.test(exported));
assert(exported.includes('Autonomous agent') && exported.includes('Human Controlled'));
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth)); assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));
await page.evaluate(()=>scrollTo({top:0,behavior:'instant'})); await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));
await page.waitForFunction(()=>scrollY===0); await page.waitForFunction(()=>scrollY===0);
+60
View File
@@ -0,0 +1,60 @@
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/sidebar-browser-${process.argv[3] || 'local'}`;
const {documents} = JSON.parse(fs.readFileSync('.cache/sidebar-coverage.json', 'utf8'));
assert(documents.length > 0);
fs.mkdirSync(output, {recursive:true});
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
const results = [];
try {
for (const width of [1440,390,320]) {
const context = await browser.newContext({viewport:{width,height:900}});
try {
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push(e.message));
const menu = page.locator('.theme-doc-sidebar-menu').filter({visible:true});
const open = async () => {
if (width < 997) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
await menu.waitFor();
};
for (const doc of documents) {
assert.equal((await page.goto(`${base}${doc.route}/`, {waitUntil:'networkidle'})).status(), 200);
await page.locator('article h1').waitFor();
await open();
const links = menu.locator('a[href]:not([role="button"])').filter({visible:true});
const active = await links.evaluateAll((nodes, route) => nodes.filter(n => n.pathname.replace(/\/$/,'') === route).map(n => n.textContent), doc.route);
assert.equal(active.length, 1, `${width}: visible unique owning-sidebar entry for ${doc.route}`);
results.push({width, route:doc.route, visibleEntry:active[0]});
}
// Contribution exposes all legacy maintenance pages without changing routes.
for (const [label, route] of [['Starting point','overview'],['Update and publish','maintain'],['Site configuration','reference']]) {
await page.goto(`${base}/eldorado-v1/documentation/contribution/`, {waitUntil:'networkidle'});
await open();
const toggle = menu.getByRole('button', {name:'Portal maintenance', exact:true});
await toggle.focus();
await page.keyboard.press('Enter');
await menu.getByRole('link', {name:label, exact:true}).click();
await page.waitForURL(`${base}/eldorado-v1/documentation/docs/${route}/`);
await page.locator('article h1').waitFor();
}
const reference = '/eldorado-v1/documentation/strategy/reference/measurement-contracts/';
await page.goto(`${base}${reference}#supported-bundle-coverage`, {waitUntil:'networkidle'});
await page.locator('#supported-bundle-coverage').waitFor();
assert((await page.locator('article').innerText()).includes('Supported bundle coverage'));
assert.equal(await page.locator('.breadcrumbs a').filter({hasText:'Stage - 01 - Skills Adoption'}).getAttribute('href'), '/eldorado-v1/documentation/strategy/stage-01-skills-adoption/');
assert((await page.locator('.breadcrumbs').innerText()).includes('Reference'));
await open();
await menu.getByRole('link', {name:'Measurement Contracts', exact:true}).waitFor({state:'visible'});
await page.screenshot({path:`${output}/${width}-reference.png`, animations:'disabled'});
assert.deepEqual(errors, []);
} finally {await context.close();}
}
assert.equal(results.length, documents.length * 3);
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
console.log(`PASS: all ${documents.length} docs have visible unique sidebar entries at 3 viewports; Contribution links, Reference breadcrumbs and reported anchor work`);
} finally {await browser.close();}
})().catch(e => {console.error(e); process.exitCode = 1;});
+42
View File
@@ -0,0 +1,42 @@
const {chromium}=require('playwright');
const fs=require('node:fs');
const path=require('node:path');
const assert=require('node:assert/strict');
(async()=>{
const dir=process.argv[2]||'.cache/skills-candidate';
const browser=await chromium.launch({executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,args:['--no-sandbox']});
const results=[];
try {
for(const topic of ['governance','quality','adoption','measurement','maintenance']) for(const width of [1440,1024,390,320]){
const spec=JSON.parse(fs.readFileSync(`diagrams/strategy-${topic}${width<=1100?'-mobile':''}.architecture.json`));
const page=await browser.newPage({viewport:{width:1200,height:1000}});
await page.goto('file://'+path.resolve(`${dir}/${topic}-${width}.svg`));
const measured=await page.locator('svg').evaluate(svg=>{
const v=svg.viewBox.baseVal;const issues=[];
for(const el of svg.querySelectorAll('[data-node-id],path[data-edge-id],g[data-edge-id]')){
const b=el.getBBox();if(b.x<v.x-1||b.y<v.y-1||b.x+b.width>v.x+v.width+1||b.y+b.height>v.y+v.height+1)issues.push(el.getAttribute('data-node-id')||el.getAttribute('data-edge-id'));
}
// Measure the whole rendered label group, including its opaque mask:
// text presence alone cannot detect one label hiding another.
const labels=[...svg.querySelectorAll('g[data-edge-id]')].filter(el=>el.querySelector('text')).map(el=>({id:el.dataset.edgeId,box:el.getBoundingClientRect()}));
const labelOverlaps=[];
for(let i=0;i<labels.length;i++)for(let j=i+1;j<labels.length;j++){
const a=labels[i],b=labels[j];
if(Math.min(a.box.right,b.box.right)>Math.max(a.box.left,b.box.left)&&Math.min(a.box.bottom,b.box.bottom)>Math.max(a.box.top,b.box.top))labelOverlaps.push([a.id,b.id]);
}
return {labelOverlaps,viewBox:[v.width,v.height],nodes:[...svg.querySelectorAll('[data-node-id]')].map(n=>n.dataset.nodeId),edges:[...svg.querySelectorAll('path[data-edge-id]')].map(n=>n.dataset.edgeId),text:svg.textContent,issues};
});
assert.deepEqual(measured.viewBox,spec.meta.viewBox);
assert.deepEqual(measured.nodes.sort(),spec.components.map(n=>n.id).sort());
assert.deepEqual(measured.edges.sort(),spec.connections.map(e=>e.id).sort());
assert.deepEqual(measured.issues,[]);
assert.deepEqual(measured.labelOverlaps,[],`${topic}-${width}: rendered edge labels overlap`);
for(const n of spec.components){assert(measured.text.includes(n.label));assert(measured.text.includes(n.tag));for(const line of n.sublabel.split('\n'))assert(measured.text.includes(line));}
for(const e of spec.connections)if(e.label)assert(measured.text.includes(e.label));
if(width===1440)await page.screenshot({path:`${dir}/${topic}-standalone-export.png`});
results.push({topic,width,nodes:measured.nodes.length,edges:measured.edges.length,complete:true});await page.close();
}
fs.writeFileSync(`${dir}/export-results.json`,JSON.stringify(results,null,2));
console.log(`PASS ${results.length} standalone SVGs: exact graph, text, full-canvas containment and non-overlapping edge labels`);
}finally{await browser.close();}
})().catch(e=>{console.error(e);process.exitCode=1});
+3 -3
View File
@@ -22,7 +22,7 @@ const fs = require('node:fs');
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click(); 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}); const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true});
await menu.first().waitFor({state:'visible'}); await menu.first().waitFor({state:'visible'});
assert.deepEqual(await menu.allTextContents(), ['Target','Roadmap','Skills governance','Skills quality','Skills adoption','Skills Measurement','Skills maintenance']); assert.deepEqual(await menu.allTextContents(), ['Target','Roadmap','Stage - 01 - Skills Adoption','Governance','Quality','Adoption','Measurement','Maintenance','Reference','Stage - 02 - Autonomous Agent']);
if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click(); if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
const panel = page.locator('.archify-panel--story'); const panel = page.locator('.archify-panel--story');
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile'); const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
@@ -33,7 +33,7 @@ const fs = require('node:fs');
const nodes = [...svg.querySelectorAll('[data-node-id]')].map(n => n.getBoundingClientRect().toJSON()); const nodes = [...svg.querySelectorAll('[data-node-id]')].map(n => n.getBoundingClientRect().toJSON());
return {nodes, width:innerWidth, height:innerHeight, minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))}; return {nodes, width:innerWidth, height:innerHeight, minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))};
}); });
assert.equal(geometry.nodes.length, 6); assert.equal(geometry.nodes.length, JSON.parse(fs.readFileSync('diagrams/strategy-measurement.architecture.json')).components.length);
for (const n of geometry.nodes) assert(n.x >= 0 && n.y >= 0 && n.right <= geometry.width + 1 && n.bottom <= geometry.height + 1); 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}`); assert(geometry.minLabel >= 10, `readable node labels: ${geometry.minLabel}`);
await frame.locator('#btn-theme').click(); await frame.locator('#btn-theme').click();
@@ -50,7 +50,7 @@ const fs = require('node:fs');
assert.equal(await download.failure(), null); assert.equal(await download.failure(), null);
const path = `${output}/${width}.svg`; await download.saveAs(path); const path = `${output}/${width}.svg`; await download.saveAs(path);
const svg = fs.readFileSync(path, 'utf8'); const svg = fs.readFileSync(path, 'utf8');
assert(svg.includes('Establish baseline') && svg.includes('Ongoing monitoring')); assert(svg.includes('Compare with baseline') && svg.includes('Zero denominator = N/A'));
const opener = panel.getByRole('button', {name:/^Expand /}); const opener = panel.getByRole('button', {name:/^Expand /});
await opener.click(); await opener.click();
const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true}); const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true});
+46
View File
@@ -0,0 +1,46 @@
const {chromium}=require('playwright');
const fs=require('node:fs');
const assert=require('node:assert/strict');
(async()=>{
const base=process.argv[2]||'http://127.0.0.1:4173';
const out=`.cache/skills-${process.argv[3]||'candidate'}`;fs.mkdirSync(out,{recursive:true});
const browser=await chromium.launch({executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,args:['--no-sandbox']}); const results=[];
try {
for(const topic of ['governance','quality','adoption','measurement','maintenance']) for(const width of [1440,1024,390,320]) {
const page=await browser.newPage({viewport:{width,height:900},colorScheme:'light'});const errors=[];page.on('pageerror',e=>errors.push(e.message));
const route=topic==='measurement'?topic:`skills-${topic}`;
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/${route}/`,{waitUntil:'networkidle'})).status(),200);
const panel=page.locator('.archify-panel--skills');assert.equal(await panel.count(),1);
const iframe=panel.locator(width>1100?'.story-desktop':'.story-mobile');await iframe.scrollIntoViewIfNeeded();
const frame=await(await iframe.elementHandle()).contentFrame();await frame.locator('svg[role="img"]').waitFor();
const geometry=await frame.locator('svg[role="img"]').evaluate(svg=>{
const nodes=[...svg.querySelectorAll('g[data-node-id]')];const issues=[];let min=Infinity;
for(const n of nodes){const rect=n.querySelector('rect').getBBox();for(const t of n.querySelectorAll('text')){
const b=t.getBBox();min=Math.min(min,parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a);
if(b.x<rect.x||b.y<rect.y||b.x+b.width>rect.x+rect.width||b.y+b.height>rect.y+rect.height)issues.push(n.id+':'+t.textContent);
}}
return {nodes:nodes.length,min,issues,width:innerWidth,svgWidth:svg.getBoundingClientRect().width};
});
const source=JSON.parse(fs.readFileSync(`diagrams/strategy-${topic}.architecture.json`));
assert.equal(geometry.nodes,source.components.length);assert.deepEqual(geometry.issues,[]);assert(geometry.min>=10,JSON.stringify(geometry));
for(const theme of ['light','dark']){
if(await frame.locator('html').getAttribute('data-theme')!==theme)await frame.locator('#btn-theme').click();
await page.waitForFunction(t=>document.documentElement.dataset.theme===t,theme);
await panel.scrollIntoViewIfNeeded();await page.screenshot({path:`${out}/${topic}-${width}-${theme}-inline.png`});
const open=panel.getByRole('button',{name:/^Expand /});await open.click();const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
const expanded=await(await panel.locator('dialog iframe').elementHandle()).contentFrame();await expanded.locator('svg[role="img"]').waitFor();
assert((await expanded.url()).includes(width<=1100?`${topic}-mobile/`:`${topic}/`));
await page.screenshot({path:`${out}/${topic}-${width}-${theme}-fullscreen.png`});
if(width<=700){await page.setViewportSize({width,height:480});assert(await panel.locator('dialog').evaluate(n=>n.scrollHeight>n.clientHeight));await panel.locator('dialog').evaluate(n=>n.scrollTop=n.scrollHeight);await page.screenshot({path:`${out}/${topic}-${width}-${theme}-short-bottom.png`});await page.setViewportSize({width,height:900});}
await close.focus();await page.keyboard.press('Escape');assert(await open.evaluate(n=>n===document.activeElement));
}
await frame.locator('#btn-preset').click();await frame.locator('[data-preset-value="blueprint"]').click();
await frame.locator('#btn-preset').click();await frame.locator('[data-preset-value="classic"]').click();
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(`${out}/${topic}-${width}.svg`);assert(fs.readFileSync(`${out}/${topic}-${width}.svg`,'utf8').includes('eldorado-skills-typography'));
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));assert.deepEqual(errors,[]);
results.push({topic,width,geometry,controls:true,export:true,themes:true,fullscreen:true});fs.writeFileSync(`${out}/results.json`,JSON.stringify(results,null,2));await page.close();
}
console.log(`PASS ${results.length} Skills story contexts: geometry, themes, controls, exports, fullscreen and short-phone scrolling`);
}finally{await browser.close();}
})().catch(e=>{console.error(e);process.exitCode=1});
+89
View File
@@ -0,0 +1,89 @@
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/strategy-navigation-${process.argv[3] || 'local'}`;
fs.mkdirSync(output, {recursive:true});
const prefix = '/eldorado-v1/documentation/strategy/';
const first = 'Stage - 01 - Skills Adoption';
const second = 'Stage - 02 - Autonomous Agent';
const topics = ['Governance','Quality','Adoption','Measurement','Maintenance','Reference'];
const ids = ['target-operating-model','roadmap','stage-01-skills-adoption','skills-governance','skills-quality','skills-adoption','measurement','skills-maintenance','reference/measurement-contracts','stage-02-autonomous-agent'];
const titles = ['Target','Roadmap',first,'Skills governance','Skills quality','Skills adoption','Skills Measurement','Skills maintenance','Skills KPI reference',second];
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
const results = [];
try {
for (const width of [1440,1024,390,320]) {
const context = await browser.newContext({viewport:{width,height:900}, colorScheme:'light'});
try {
const page = await context.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
const openMenu = async () => {
if (width < 997) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
const menu = page.locator('.theme-doc-sidebar-menu').filter({visible:true});
await menu.locator('.menu__link').first().waitFor({state:'visible'});
return menu;
};
const closeMenu = async () => {
if (width < 997) await page.getByRole('button', {name:'Close navigation bar'}).click();
};
assert.equal((await page.goto(`${base}${prefix}roadmap/`, {waitUntil:'networkidle'})).status(), 200);
let menu = await openMenu();
const visibleLabels = () => menu.locator('.menu__link').filter({visible:true}).allTextContents();
assert.deepEqual(await visibleLabels(), ['Target','Roadmap',first,second]);
const category = menu.locator('.theme-doc-sidebar-item-category').filter({has:page.getByRole('link', {name:first, exact:true})});
const caret = category.locator(':scope > .menu__list-item-collapsible > button.menu__caret');
await caret.click();
await category.getByRole('link', {name:'Governance', exact:true}).waitFor({state:'visible'});
assert.deepEqual(await visibleLabels(), ['Target','Roadmap',first,...topics,second]);
assert.deepEqual(await category.locator('ul .menu__link').filter({visible:true}).allTextContents(), topics);
const reference = category.getByRole('button', {name:'Reference', exact:true});
await reference.focus();
await page.keyboard.press('Enter');
await category.getByRole('link', {name:'Measurement Contracts', exact:true}).waitFor({state:'visible'});
assert.deepEqual(await visibleLabels(), ['Target','Roadmap',first,...topics,'Measurement Contracts',second]);
await reference.click();
await page.screenshot({path:`${output}/${width}-expanded.png`, animations:'disabled'});
await caret.click();
await category.getByRole('link', {name:'Governance', exact:true}).waitFor({state:'hidden'});
assert.deepEqual(await visibleLabels(), ['Target','Roadmap',first,second]);
await menu.getByRole('link', {name:first, exact:true}).click();
await page.waitForURL(`${base}${prefix}stage-01-skills-adoption/`);
await page.getByRole('heading', {level:1, name:first, exact:true}).waitFor();
assert.equal(await page.locator('h1').innerText(), first);
assert((await page.locator('article').innerText()).includes('Delivery first'));
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
await page.screenshot({path:`${output}/${width}-stage-01.png`, fullPage:true, animations:'disabled'});
for (const [index, id] of ids.entries()) {
assert.equal((await page.goto(`${base}${prefix}${id}/`, {waitUntil:'networkidle'})).status(), 200);
assert.equal(new URL(page.url()).pathname, `${prefix}${id}/`);
// Target has a longer canonical page heading but keeps its short navigation label.
if (index) assert.equal(await page.locator('h1').innerText(), titles[index]);
else assert((await page.locator('h1').innerText()).includes('Target'));
const prev = page.locator('.pagination-nav__link--prev');
const next = page.locator('.pagination-nav__link--next');
if (index) assert.equal(await prev.getAttribute('href'), `${prefix}${ids[index-1]}/`);
else assert.equal(await prev.count(), 0);
if (index < ids.length-1) assert.equal(await next.getAttribute('href'), `${prefix}${ids[index+1]}/`);
else assert.equal(await next.count(), 0);
if (index >= 3 && index <= 7) {
assert.equal(await page.locator('.breadcrumbs a').filter({hasText:first}).getAttribute('href'), `${prefix}stage-01-skills-adoption/`);
menu = await openMenu();
assert.deepEqual(await visibleLabels(), ['Target','Roadmap',first,...topics,second]);
await closeMenu();
}
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), `${width}: overflow on ${id}`);
}
assert((await page.locator('article').innerText()).includes('not deployed autonomy'));
await page.screenshot({path:`${output}/${width}-stage-02.png`, fullPage:true, animations:'disabled'});
assert.deepEqual(errors, []);
results.push({width, routes:ids.length, linkedCategory:true, expandCollapse:true, nesting:true, breadcrumbs:true, pagination:true, overflow:false, errors});
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
} finally {await context.close();}
}
assert.equal(results.length, 4);
console.log('PASS: 4 desktop/mobile viewports; all 10 routes; linked category, keyboard expansion, nested Reference, breadcrumbs, next/previous and no overflow/errors');
} finally {await browser.close();}
})().catch(error => {console.error(error); process.exitCode = 1;});
+9 -4
View File
@@ -12,14 +12,16 @@ const fs=require('node:fs');
const page=await context.newPage(); const page=await context.newPage();
for(const id of ['index','target-operating-model','skills-governance','skills-quality','skills-adoption','measurement','skills-maintenance','roadmap']){ for(const id of ['index','target-operating-model','skills-governance','skills-quality','skills-adoption','measurement','skills-maintenance','roadmap']){
const response=await page.goto(`${base}/eldorado-v1/documentation/strategy/${id==='index'?'':id+'/'}`,{waitUntil:'networkidle'});assert.equal(response.status(),200); const response=await page.goto(`${base}/eldorado-v1/documentation/strategy/${id==='index'?'':id+'/'}`,{waitUntil:'networkidle'});assert.equal(response.status(),200);
const panel=page.locator('.archify-panel--story'); const panel=page.locator('.archify-panel--story').first();
const iframe=panel.locator(width===390?'.story-mobile':'.story-desktop');await iframe.scrollIntoViewIfNeeded(); const iframe=panel.locator(width===390?'.story-mobile':'.story-desktop');await iframe.scrollIntoViewIfNeeded();
const frame=await (await iframe.elementHandle()).contentFrame();await frame.locator('svg[role="img"]').waitFor(); const frame=await (await iframe.elementHandle()).contentFrame();await frame.locator('svg[role="img"]').waitFor();
const geometry=await frame.locator('svg[role="img"]').evaluate(svg=>{ const geometry=await frame.locator('svg[role="img"]').evaluate(svg=>{
const nodes=[...svg.querySelectorAll('[data-node-id]')].map(n=>{const r=n.getBoundingClientRect();return {id:n.id,x:r.x,y:r.y,right:r.right,bottom:r.bottom};}); const nodes=[...svg.querySelectorAll('[data-node-id]')].map(n=>{const r=n.getBoundingClientRect();return {id:n.id,x:r.x,y:r.y,right:r.right,bottom:r.bottom};});
return {width:innerWidth,height:innerHeight,nodes,svg:svg.getBoundingClientRect().toJSON(),minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n=>parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))}; return {width:innerWidth,height:innerHeight,nodes,svg:svg.getBoundingClientRect().toJSON(),minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n=>parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))};
}); });
assert(geometry.nodes.length>=5 && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id); const name=await panel.getAttribute('data-diagram');
const source=JSON.parse(fs.readFileSync(`diagrams/${name}.${name==='strategy-roadmap'?'workflow':'architecture'}.json`));
assert.equal(geometry.nodes.length,(source.components||source.nodes).length,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:id==='roadmap'?600: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);
@@ -28,8 +30,11 @@ const fs=require('node:fs');
const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'}); const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
assert.equal(await close.innerText(),'×');assert(await close.evaluate(n=>n===document.activeElement)); assert.equal(await close.innerText(),'×');assert(await close.evaluate(n=>n===document.activeElement));
const box=await close.boundingBox();assert.equal(box.width,44);assert.equal(box.height,44); const box=await close.boundingBox();assert.equal(box.width,44);assert.equal(box.height,44);
await close.click();assert(await opener.evaluate(n=>n===document.activeElement)); await close.click();await panel.locator('dialog iframe').waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
await opener.click();await close.waitFor({state:'visible'});await page.keyboard.press('Escape');await page.waitForFunction(()=>!document.querySelector('.archify-panel--story dialog').open);assert(await opener.evaluate(n=>n===document.activeElement)); await opener.click();await close.waitFor({state:'visible'});await page.keyboard.press('Escape');await page.waitForFunction(()=>!document.querySelector('.archify-panel--story dialog').open);
// Native close clears open before the queued React onClose restores focus.
// Wait for its unmount before focusing the next keyboard interaction target.
await panel.locator('dialog iframe').waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
if(id !== 'measurement') { if(id !== 'measurement') {
const summary=page.locator('.strategy-content > details > summary').first(); const summary=page.locator('.strategy-content > details > summary').first();
await summary.focus();await page.keyboard.press('Enter'); await summary.focus();await page.keyboard.press('Enter');
+185
View File
@@ -0,0 +1,185 @@
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const {pathToFileURL} = require('node:url');
const {resolve} = require('node:path');
// Inspect the rendered paths, not just the authored JSON. Also runs on downloaded SVGs.
function inspect({mobile}) {
const svg = document.querySelector('svg[role="img"]');
const point = (element, p) => new DOMPoint(p.x, p.y).matrixTransform(element.getCTM());
const boxes = [...svg.querySelectorAll('[data-node-id]')].map(node => {
const rect = node.querySelector('rect'), box = rect.getBBox();
const a = point(rect, box), b = point(rect, {x:box.x + box.width, y:box.y + box.height});
return {id:node.dataset.nodeId, x:a.x, y:a.y, right:b.x, bottom:b.y, cx:(a.x+b.x)/2, cy:(a.y+b.y)/2};
});
const agent = boxes.find(n => n.id === 'agent');
const errors = [], endpoints = {};
const phases = ['scope','architecture','ui-ux','planning','delivery','uat','security','releases'];
const edges = [...svg.querySelectorAll('path[data-edge-id]')];
if (edges.length !== 24) errors.push(`expected 24 graph edges, got ${edges.length}`);
phases.forEach((phase, index) => {
for (const [id, from, to] of [
[`govern-${phase}`,'agent',phase],
[`stage-${index+1}`,phase,phases[(index+1)%phases.length]],
[`contribute-${phase}`,`code-${phase}`,phase],
]) {
const matches = edges.filter(p => p.dataset.edgeId === id);
if (matches.length !== 1 || matches[0].dataset.edgeFrom !== from || matches[0].dataset.edgeTo !== to || !matches[0].getAttribute('marker-end') || matches[0].getAttribute('marker-start')) errors.push(`${id}: graph or direction changed`);
}
});
// CTMs put boxes and paths in the same viewport coordinate system.
// 0.01 covers floating-point error against bounds, NOT source spreading:
// shared endpoints below must be exactly identical.
const paths = [...svg.querySelectorAll('path[data-edge-id^="govern-"]')];
if (paths.length !== 8) errors.push(`expected eight governance paths, got ${paths.length}`);
for (const path of paths) {
const id = path.dataset.edgeTo;
const side = mobile ? 'left' : ['scope','architecture','releases'].includes(id) ? 'top'
: ['planning','delivery','uat'].includes(id) ? 'bottom' : id === 'security' ? 'left' : 'right';
const expected = {top:[agent.cx,agent.y],bottom:[agent.cx,agent.bottom],left:[agent.x,agent.cy],right:[agent.right,agent.cy]}[side];
const start = point(path,path.getPointAtLength(0));
endpoints[id] = [start.x,start.y];
if (Math.hypot(start.x-expected[0],start.y-expected[1]) > 0.01) errors.push(`${id}: wrong ${side} midpoint`);
const length = path.getTotalLength();
const target = boxes.find(n => n.id === id);
const destination = mobile ? [target.x,target.cy]
: side === 'top' ? [target.cx,target.bottom] : side === 'bottom' ? [target.cx,target.y]
: side === 'left' ? [target.right,target.cy] : [target.x,target.cy];
const end = point(path,path.getPointAtLength(length));
if (Math.hypot(end.x-destination[0],end.y-destination[1]) > 0.01) errors.push(`${id}: destination midpoint changed`);
for (let distance = 1; distance < length; distance += 2) {
const p = point(path,path.getPointAtLength(distance));
for (const box of boxes) {
if (box.id === id) continue;
if (p.x > box.x+0.1 && p.x < box.right-0.1 && p.y > box.y+0.1 && p.y < box.bottom-0.1) {
errors.push(`${id}: crosses ${box.id}`); break;
}
}
}
}
for (const group of mobile ? [phases] : [['scope','architecture','releases'],['planning','delivery','uat']]) {
const first = endpoints[group[0]];
if (group.some(id => !endpoints[id] || endpoints[id][0] !== first?.[0] || endpoints[id][1] !== first?.[1])) errors.push(`source-port spreading: ${group.join(',')}`);
}
const lifecycle = [];
if (!mobile) {
for (const [id, x, y1, y2, peer] of [
['stage-2',850,262.5,457.5,'architecture'], ['stage-3',850,532.5,727.5,'planning'],
['stage-6',350,727.5,532.5,'uat'], ['stage-7',350,457.5,262.5,'releases'],
]) {
const path = svg.querySelector(`path[data-edge-id="${id}"]`);
if (!path) {errors.push(`${id}: missing lifecycle path`); continue;}
const length = path.getTotalLength();
const a = path.getPointAtLength(0), b = path.getPointAtLength(length);
// Local SVG coordinates test exact offsets; transformed coordinates test
// node clearance at any inline/fullscreen/export scale.
if (Math.hypot(a.x-x,a.y-y1) > 0.01 || Math.hypot(b.x-x,b.y-y2) > 0.01) errors.push(`${id}: wrong shifted endpoints`);
if (!path.getAttribute('marker-end')) errors.push(`${id}: missing direction`);
const governance = paths.find(p => p.dataset.edgeTo === peer);
if (!governance) {errors.push(`${id}: missing governance peer ${peer}`); continue;}
const end = governance.getPointAtLength(governance.getTotalLength());
if (Math.abs(Math.abs(a.x-end.x)-20) > 0.01) errors.push(`${id}: wrong attachment clearance`);
for (let distance = 1; distance < length; distance += 2) {
const local = path.getPointAtLength(distance), p = point(path,local);
if (Math.abs(local.x-x) > 0.01) errors.push(`${id}: channel not straight`);
for (const box of boxes) {
if (p.x > box.x+0.1 && p.x < box.right-0.1 && p.y > box.y+0.1 && p.y < box.bottom-0.1) errors.push(`${id}: crosses ${box.id}`);
}
}
// No positive-length collinear overlap with ANY governance segment.
// Perpendicular crossings remain legitimate in this fixed radial layout.
for (const govern of paths) {
const total = govern.getTotalLength();
for (let d = 0; d < total - 1; d += 1) {
const p = govern.getPointAtLength(d), q = govern.getPointAtLength(d+1);
if (Math.abs(p.x-x) < 0.01 && Math.abs(q.x-x) < 0.01 && Math.min(Math.max(p.y,q.y),Math.max(y1,y2)) - Math.max(Math.min(p.y,q.y),Math.min(y1,y2)) > 0.01) errors.push(`${id}: overlaps ${govern.dataset.edgeId}`);
}
}
lifecycle.push({id,start:[a.x,a.y],end:[b.x,b.y],attachmentClearance:Math.abs(a.x-end.x)});
}
}
return {errors:[...new Set(errors)],endpoints,lifecycle};
}
function inspectTypography({mobile}) {
const errors = [];
const nodes = [...document.querySelectorAll('[data-node-id]')].map(node => {
const box = node.querySelector('rect').getBoundingClientRect();
const texts = [...node.querySelectorAll(':scope > text')];
const bounds = texts.map(text => text.getBoundingClientRect());
texts.forEach((text, index) => {
const expected = text.hasAttribute('data-node-label') ? (mobile ? 38 : 27) : (mobile ? 28 : 21);
if (parseFloat(getComputedStyle(text).fontSize) !== expected) errors.push(`${node.id}: wrong text size`);
const b = bounds[index];
if (b.width <= 0 || b.height <= 0 || b.x < box.x - 1 || b.right > box.right + 1 || b.y < box.y - 1 || b.bottom > box.bottom + 1) errors.push(`${node.id}: text outside node`);
});
if (bounds.length === 2 && bounds[0].bottom > bounds[1].top) errors.push(`${node.id}: title/subtitle overlap`);
return {id:node.id, text:texts.map(t => t.textContent), sizes:texts.map(t => getComputedStyle(t).fontSize)};
});
if (nodes.length !== 17) errors.push(`expected 17 nodes, got ${nodes.length}`);
return {errors, nodes};
}
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/midpoints-${process.argv[3] || 'local'}`;
fs.mkdirSync(output,{recursive:true});
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,args:['--no-sandbox']});
const results = [];
try {
for (const width of [1440,390,320]) {
const page = await browser.newPage({viewport:{width,height:1000}});
await page.goto(`${base}/eldorado-v1/documentation/strategy/target-operating-model/`,{waitUntil:'networkidle'});
const panel = page.locator('[data-diagram="strategy-target"]');
const iframe = panel.locator(`:scope > .story-${width > 700 ? 'desktop' : 'mobile'}`);
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const inline = await frame.evaluate(inspect,{mobile:width < 700});
assert.deepEqual(inline.errors,[]);
await page.screenshot({path:`${output}/${width}-inline.png`,fullPage:true});
const exported = [];
for (const theme of ['light','dark']) {
if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click();
await frame.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
await frame.locator('#btn-export').click();
const [download] = await Promise.all([page.waitForEvent('download'),frame.locator('[data-format="svg"]').click()]);
assert.equal(await download.failure(),null);
const path = `${output}/${width}-${theme}.svg`;
await download.saveAs(path);
// A separate SVG document has no viewer CSS to hide export regressions.
const standalone = await browser.newPage({viewport:{width:1440,height:1100},colorScheme:theme});
await standalone.goto(pathToFileURL(resolve(path)).href);
assert.equal(await standalone.evaluate(() => document.contentType),'image/svg+xml');
await standalone.evaluate(() => document.fonts.ready);
const geometry = await standalone.evaluate(inspect,{mobile:width < 700});
const typography = await standalone.evaluate(inspectTypography,{mobile:width < 700});
assert.deepEqual(geometry.errors,[]);
assert.deepEqual(typography.errors,[]);
const background = await standalone.locator('rect.c-bg-rect').evaluate(n => getComputedStyle(n).fill);
if (exported.length) assert.notEqual(background,exported[0].background,'standalone theme must change');
// CDP captures SVG documents without Playwright's HTML-body screenshot wait.
const client = await standalone.context().newCDPSession(standalone);
const size = await standalone.evaluate(() => ({width:document.documentElement.width.baseVal.value,height:document.documentElement.height.baseVal.value}));
const image = await client.send('Page.captureScreenshot',{format:'png',captureBeyondViewport:true,clip:{x:0,y:0,...size,scale:1}});
fs.writeFileSync(`${output}/${width}-${theme}-standalone.png`,Buffer.from(image.data,'base64'));
exported.push({theme,background,geometry,typography});
await standalone.close();
}
await panel.getByRole('button',{name:/^Expand /}).click();
const full = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await full.locator('svg[role="img"]').waitFor();
// The existing fullscreen viewer always opens the radial desktop source.
const fullscreen = await full.evaluate(inspect,{mobile:false});
assert.deepEqual(fullscreen.errors,[]);
await page.screenshot({path:`${output}/${width}-fullscreen.png`});
await panel.getByRole('button',{name:'Close full-screen diagram'}).click();
results.push({width,inline,exported,fullscreen});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
await page.close();
}
assert.equal(results.length,3);
console.log('PASS midpoint geometry: 3 viewports × inline/fullscreen; standalone SVG geometry and typography in light/dark');
} finally {await browser.close();}
})().catch(error => {console.error(error);process.exitCode=1;});
+21 -8
View File
@@ -1,7 +1,9 @@
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 {skillsViewer, isSkillsStory} from './skills-viewer.mjs';
import {roadmapViewer} from './roadmap-viewer.mjs'; import {roadmapViewer} from './roadmap-viewer.mjs';
import {targetRouting} from './target-routing.mjs';
import {createHash} from 'node:crypto'; import {createHash} from 'node:crypto';
const cli = 'vendor/archify/bin/archify.mjs'; const cli = 'vendor/archify/bin/archify.mjs';
const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']); const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']);
@@ -25,12 +27,13 @@ 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'}});
const roadmap = ['strategy-roadmap', 'strategy-roadmap-mobile'].includes(name); const roadmap = /^strategy-roadmap(?:-(?:adoption|loop))?(?:-mobile)?$/.test(name);
if (roadmap) { const skills = isSkillsStory(name);
if (roadmap || skills) {
// Native renderer validates schema and compiles layout. Check readability // Native renderer validates schema and compiles layout. Check readability
// after multiline typography, not upstream's single-line preview. // 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'}}); execFileSync(process.execPath, [`vendor/archify/renderers/${type}/render-${type}.mjs`, input, output], {stdio:'inherit', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}});
writeFileSync(output, roadmapViewer(readFileSync(output, 'utf8'), spec)); writeFileSync(output, (skills ? skillsViewer : roadmapViewer)(readFileSync(output, 'utf8'), spec));
run(['check',output,'--json']); run(['check',output,'--json']);
} else { } else {
run(['validate',type,input,'--quality','showcase','--json']); run(['validate',type,input,'--quality','showcase','--json']);
@@ -39,6 +42,7 @@ for (const file of readdirSync('diagrams').sort()) {
} }
// Verify the upstream artifact first, then add the scoped portal integration. // Verify the upstream artifact first, then add the scoped portal integration.
let viewer = readFileSync(output, 'utf8'); let viewer = readFileSync(output, 'utf8');
if (name === 'strategy-target') viewer = targetRouting(viewer);
// Upstream intentionally disables preset selection in gallery embeds. Enable // Upstream intentionally disables preset selection in gallery embeds. Enable
// only those two guards; fail closed if a future vendor update changes them. // only those two guards; fail closed if a future vendor update changes them.
const start = viewer.indexOf('Archify.preset = (function () {'); const start = viewer.indexOf('Archify.preset = (function () {');
@@ -59,14 +63,23 @@ 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>`));
if (roadmap) { if (roadmap || skills) {
// This is a portal receipt, not upstream deliver provenance: formatting // This is a portal receipt, not upstream deliver provenance: formatting
// happens between the native schema/layout compiler and artifact checker. // happens between the native schema/layout compiler and artifact checker.
const hash = path => createHash('sha256').update(readFileSync(path)).digest('hex'); 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'}})); const checked = JSON.parse(execFileSync(process.execPath, [cli,'check',output,'--json'], {encoding:'utf8', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}));
if (!checked.ok || checked.composition?.status !== 'pass') throw new Error('Roadmap final artifact failed'); if (!checked.ok || checked.composition?.status !== 'pass') throw new Error('Story final artifact failed');
writeFileSync(`.cache/archify/${name}.portal.json`, JSON.stringify({source:input, sourceSha256:hash(input), adapterSha256:hash('scripts/roadmap-viewer.mjs'), artifactSha256:hash(output), checker:checked}, null, 2)); writeFileSync(`.cache/archify/${name}.portal.json`, JSON.stringify({source:input, sourceSha256:hash(input), adapterSha256:hash(skills ? 'scripts/skills-viewer.mjs' : 'scripts/roadmap-viewer.mjs'), artifactSha256:hash(output), checker:checked}, null, 2));
} else renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`); } else {
renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`);
if (name === 'strategy-target') {
// The native receipt describes the PRE-integration artifact only.
const hash = path => createHash('sha256').update(readFileSync(path)).digest('hex');
const checked = JSON.parse(execFileSync(process.execPath, [cli,'check',output,'--json'], {encoding:'utf8', env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}));
if (!checked.ok || checked.composition?.status !== 'pass') throw new Error('Target final artifact failed');
writeFileSync(`.cache/archify/${name}.portal.json`, JSON.stringify({source:input, sourceSha256:hash(input), adapterSha256:hash('scripts/target-routing.mjs'), artifactSha256:hash(output), nativeReceiptScope:'before portal integration', checker:checked}, null, 2));
}
}
} }
copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt'); copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt');
copyFileSync('vendor/archify/THIRD_PARTY_NOTICES.md','static/diagrams/THIRD-PARTY-NOTICES.md'); copyFileSync('vendor/archify/THIRD_PARTY_NOTICES.md','static/diagrams/THIRD-PARTY-NOTICES.md');
+2 -1
View File
@@ -8,7 +8,8 @@
font-size: var(--cycle-label); font-size: var(--cycle-label);
transform: translateY(-6px); transform: translateY(-6px);
} }
.diagram-container > svg text[data-detail="context"] { /* Export strips data-detail; node ownership and the muted class survive. */
.diagram-container > svg [data-node-id] > text.t-muted {
font-size: var(--cycle-detail); font-size: var(--cycle-detail);
transform: translateY(16px); transform: translateY(16px);
} }
+13 -13
View File
@@ -1,6 +1,6 @@
// Native Workflow v2 owns node geometry, mainPath and routing. The portal adds // Native Workflow v2 owns node geometry, mainPath and routing. The portal adds
// multiline typography: upstream's three single-line labels are too small for // 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. // a Roadmap reading view. Do not change the pinned vendor or graph semantics.
const escape = text => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;'); const escape = text => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
const wrap = (text, limit = 21) => { const wrap = (text, limit = 21) => {
const lines = []; const lines = [];
@@ -11,7 +11,7 @@ const wrap = (text, limit = 21) => {
return lines; return lines;
}; };
export function roadmapViewer(html, spec) { 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'); if (spec.diagram_type !== 'workflow' || spec.schema_version !== 2 || ![2, 6].includes(spec.nodes.length)) throw new Error('Roadmap workflow contract drift');
const boxes = []; const boxes = [];
for (const [index, node] of spec.nodes.entries()) { for (const [index, node] of spec.nodes.entries()) {
const start = html.indexOf(`<g id="node-${node.id}"`); const start = html.indexOf(`<g id="node-${node.id}"`);
@@ -20,23 +20,20 @@ export function roadmapViewer(html, spec) {
if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`); if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`);
const original = html.slice(start, end); const original = html.slice(start, end);
const rect = /<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.]+)" height="([\d.]+)"/.exec(original); const rect = /<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.]+)" height="([\d.]+)"/.exec(original);
if (!rect || (original.match(/<text /g) || []).length !== 3) throw new Error(`Roadmap text contract drift: ${node.id}`); if (!rect || (original.match(/<text /g) || []).length !== 2) throw new Error(`Roadmap text contract drift: ${node.id}`);
const [x,y,w,h] = rect.slice(1).map(Number); const [x,y,w,h] = rect.slice(1).map(Number);
boxes.push({x,y,w,h}); boxes.push({x,y,w,h});
const text = (value, top, size, cls, attrs = 'data-detail="context"') => { const text = (value, top, size, cls, attrs = 'data-detail="context"') => {
const lines = wrap(value, size === 30 ? 18 : 21); const lines = wrap(value, size === 30 ? 18 : 21);
const maxLines = top === 166 || top === 310 ? 3 : 2; const maxLines = top === 160 ? 3 : 2;
if (lines.length > maxLines || lines.some(line => line.length > 21)) throw new Error(`Roadmap copy exceeds card capacity: ${node.id}`); if (lines.length > maxLines || lines.some(line => line.length > 21)) throw new Error(`Roadmap copy exceeds card capacity: ${node.id}`);
return `<text x="${x+24}" y="${y+top}" font-size="${size}" class="${cls}" ${attrs}>${lines.map((line,i) => `<tspan x="${x+24}" dy="${i ? 34 : 0}">${escape(line)}</tspan>`).join('')}</text>`; return `<text x="${x+24}" y="${y+top}" font-size="${size}" class="${cls}" ${attrs}>${lines.map((line,i) => `<tspan x="${x+24}" dy="${i ? 34 : 0}">${escape(line)}</tspan>`).join('')}</text>`;
}; };
const [action, output] = node.sublabel.split('\nOutput: '); if (!node.sublabel || node.tag || /Output:|Exit:|\n/.test(node.sublabel)) throw new Error(`Roadmap content contract drift: ${node.id}`);
if (!action || !output || !node.tag.startsWith('Exit: ')) throw new Error(`Roadmap content contract drift: ${node.id}`); const number = spec.nodes.length === 6 ? `Step ${index+1}` : spec.nodes[0].id === 'skills' ? `Stage ${index+1}` : index === 0 ? 'First' : 'Then';
const content = text(String(index+1).padStart(2,'0'), 40, 26, 't-muted') const content = text(number, 40, 26, 't-primary', 'data-stage-number="" font-weight="700"')
+ text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"') + text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"')
+ text(action, 166, 26, 't-primary') + text(node.sublabel, 160, 26, 't-muted');
+ text('OUTPUT', 274, 20, 't-muted', 'font-weight="700"')
+ text(output, 310, 26, 't-primary')
+ text(node.tag, 416, 24, 't-primary');
// Preserve native node title/metadata and rectangles; replace sigil/text only. // Preserve native node title/metadata and rectangles; replace sigil/text only.
// Sigils are nested SVG groups. Anchor on the two native node rectangles. // Sigils are nested SVG groups. Anchor on the two native node rectangles.
const rectangles = [...original.matchAll(/<rect\b[^>]*\/>/g)]; const rectangles = [...original.matchAll(/<rect\b[^>]*\/>/g)];
@@ -45,7 +42,8 @@ export function roadmapViewer(html, spec) {
const replacement = original.slice(0,prefixEnd).replaceAll('rx="6"', 'rx="0"') + content + '</g>'; const replacement = original.slice(0,prefixEnd).replaceAll('rx="6"', 'rx="0"') + content + '</g>';
html = html.slice(0,start) + replacement + html.slice(end); html = html.slice(0,start) + replacement + html.slice(end);
} }
const left = Math.min(...boxes.map(b => b.x)) - 12; // The native mobile return channel sits outside the card column.
const left = Math.min(...boxes.map(b => b.x)) - (spec.edges.some(e => e.route === 'return-left') ? 80 : 12);
const top = Math.min(...boxes.map(b => b.y)) - 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 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; const height = Math.max(...boxes.map(b => b.y+b.h)) - top + 12;
@@ -55,10 +53,12 @@ export function roadmapViewer(html, spec) {
if (roots.length !== 1) throw new Error('Roadmap SVG contract drift'); if (roots.length !== 1) throw new Error('Roadmap SVG contract drift');
const root = roots[0][0].replace(/viewBox="[^"]*"/, `viewBox="${left} ${top} ${width} ${height}"`); const root = roots[0][0].replace(/viewBox="[^"]*"/, `viewBox="${left} ${top} ${width} ${height}"`);
const styles = `<style id="eldorado-roadmap-typography"> const styles = `<style id="eldorado-roadmap-typography">
html[data-embed="true"] .diagram-container > svg {max-height:calc(100vh - 84px);}
[data-node-id] text {font-family: Arial, sans-serif;} [data-node-id] text {font-family: Arial, sans-serif;}
svg[data-preset="classic"] [data-node-id] > rect.c-backend, svg[data-preset="classic"] [data-node-id] > rect.c-backend,
svg[data-preset="editorial"] [data-node-id] > rect.c-backend {fill:var(--panel); stroke:var(--border);} svg[data-preset="editorial"] [data-node-id] > rect.c-backend {fill:var(--panel); stroke:var(--border);}
svg[data-preset="classic"] .c-grid {opacity:0;} .c-grid {opacity:0;}
</style>`; </style>`;
html = html.replace(roots[0][0], root + styles); html = html.replace(roots[0][0], root + styles);
return html; return html;
+4 -4
View File
@@ -4,18 +4,18 @@ import {readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync} from 'node:
import {execFileSync} from 'node:child_process'; import {execFileSync} from 'node:child_process';
import {roadmapViewer} from './roadmap-viewer.mjs'; import {roadmapViewer} from './roadmap-viewer.mjs';
for (const suffix of ['', '-mobile']) test(`native Workflow${suffix} retains graph and passes final composition`, () => { for (const name of ['strategy-roadmap', 'strategy-roadmap-adoption', 'strategy-roadmap-loop']) for (const suffix of ['', '-mobile']) test(`${name}${suffix} retains graph and passes final composition`, () => {
mkdirSync('.cache', {recursive:true}); mkdirSync('.cache', {recursive:true});
const dir = mkdtempSync('.cache/roadmap-test-'); const dir = mkdtempSync('.cache/roadmap-test-');
try { try {
const source = `diagrams/strategy-roadmap${suffix}.workflow.json`; const source = `diagrams/${name}${suffix}.workflow.json`;
const spec = JSON.parse(readFileSync(source, 'utf8')); const spec = JSON.parse(readFileSync(source, 'utf8'));
const output = `${dir}/index.html`; 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'}}); 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 native = readFileSync(output, 'utf8');
const styled = roadmapViewer(native, spec); const styled = roadmapViewer(native, spec);
const graph = html => [...html.matchAll(/<path\b[^>]*data-edge-id="[^"]+"[^>]*>/g)].map(m => m[0]); const graph = html => [...html.matchAll(/<path\b[^>]*data-edge-id="[^"]+"[^>]*>/g)].map(m => m[0]);
assert.equal(graph(native).length, 4); assert.equal(graph(native).length, spec.edges.length);
assert.deepEqual(graph(styled), graph(native), 'native paths must not be rewritten'); 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}"`)); 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.equal((styled.match(/<text data-node-label=/g) || []).length, 0, 'single-line preview replaced');
@@ -28,7 +28,7 @@ for (const suffix of ['', '-mobile']) test(`native Workflow${suffix} retains gra
assert.equal(checked.composition.metrics.labelCanvasOverflowIssues, 0); assert.equal(checked.composition.metrics.labelCanvasOverflowIssues, 0);
assert.throws(() => roadmapViewer('', spec), /drift/); assert.throws(() => roadmapViewer('', spec), /drift/);
const oversized = structuredClone(spec); const oversized = structuredClone(spec);
oversized.nodes[0].sublabel = 'Too much copy '.repeat(20) + '\nOutput: baseline'; oversized.nodes[0].sublabel = 'Too much copy '.repeat(20);
assert.throws(() => roadmapViewer(native, oversized), /capacity/); assert.throws(() => roadmapViewer(native, oversized), /capacity/);
assert.throws(() => roadmapViewer(native, {...spec,diagram_type:'architecture'}), /contract drift/); assert.throws(() => roadmapViewer(native, {...spec,diagram_type:'architecture'}), /contract drift/);
} finally {rmSync(dir, {recursive:true, force:true});} } finally {rmSync(dir, {recursive:true, force:true});}
+24 -26
View File
@@ -3,34 +3,32 @@ import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs'; import {readFileSync} from 'node:fs';
const read = path => readFileSync(path, 'utf8'); const read = path => readFileSync(path, 'utf8');
test('Roadmap visibly assesses code and real delivery before adoption', () => { export const views = {
'strategy-roadmap': ['skills', 'autonomy'],
'strategy-roadmap-adoption': ['delivery', 'remaining'],
'strategy-roadmap-loop': ['understand', 'baseline', 'topology', 'use', 'collect', 'reassess'],
};
test('Roadmap has three visual levels and preserves human direction and Delivery discovery', () => {
const page = read('docs/strategy/roadmap.mdx'); const page = read('docs/strategy/roadmap.mdx');
const visible = page.split('<details>')[0]; const visible = page.split('<details>')[0];
const headings = [...visible.matchAll(/^## (.+)$/gm)].map(match => match[1]); assert.deepEqual([...visible.matchAll(/<ArchifyDiagram name="([^"]+)"/g)].map(m => m[1]), Object.keys(views));
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.deepEqual([...visible.matchAll(/^## (.+)$/gm)].map(m => m[1]), ['Two stages toward Target', 'Stage 1: Delivery first', 'Improve within each phase', 'Delivery in practice']);
assert(visible.indexOf(' story />') < visible.indexOf('## 1.')); for (const marker of ['Humans direct, guide and review throughout', 'standardizes skills', 'foundation for Stage 2', 'future Autonomous Agent', 'not deployed autonomy', 'order is not yet decided', 'Delivery keeps improving', 'not as six global roadmap stages', 'what skills exist, how they relate, when they are used and how they work together', 'codebase structure', 'existing principles', 'system-logic source', 'tests', 'builds and packaging', 'deployments', 'practices, tooling, controls and problems', 'metrics/KPIs and a baseline', 'friction and feedback', './measurement.mdx', './target-operating-model.mdx']) assert(visible.includes(marker), marker);
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); for (const marker of ['revocation','exact-artifact approvals','idempotency','human takeover','sensitive decisions','security findings','reproducible baseline']) assert(page.includes(marker), marker);
assert.equal((visible.match(/\*\*Output:/g) || []).length, 5); assert(!/## [1-5]\.|Prepare foundations|Sustain and improve/.test(page));
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);
}); });
for (const [name, ids] of Object.entries(views)) test(`${name}: native graph, mobile parity and loop semantics`, () => {
test('both roadmap diagrams make assessment and readiness prerequisites to the pilot', () => { const variants = ['', '-mobile'].map(suffix => JSON.parse(read(`diagrams/${name}${suffix}.workflow.json`)));
const ids = ['assessment', 'readiness', 'pilot', 'expansion', 'sustain']; for (const [index, d] of variants.entries()) {
for (const suffix of ['', '-mobile']) { assert.equal(d.schema_version, 2);
const diagram = JSON.parse(read(`diagrams/strategy-roadmap${suffix}.workflow.json`)); assert.equal(d.diagram_type, 'workflow');
assert.equal(diagram.schema_version, 2); assert.deepEqual(d.mainPath, ids.length === 6 ? ids.slice(0,3) : ids);
assert.equal(diagram.diagram_type, 'workflow'); assert.deepEqual(d.nodes.map(n => n.id), ids);
assert.deepEqual(diagram.mainPath, ids); assert.deepEqual(d.edges.filter(e => e.role === 'main').map(e => [e.from,e.to]), ids.slice(1).map((id,i) => [ids[i],id]));
assert.deepEqual(diagram.nodes.map(n => n.id), ids); assert.deepEqual(d.edges.filter(e => e.role === 'return').map(e => [e.from,e.to]), ids.length === 6 ? [['reassess','understand']] : []);
assert.deepEqual(diagram.nodes.map(n => n.label), ['Understand today', 'Prepare foundations', 'Pilot', 'Expand', 'Sustain']); if (index) assert(d.nodes.every(n => n.col === 0));
assert.deepEqual(diagram.edges.map(e => [e.from,e.to]), ids.slice(1).map((id, i) => [ids[i],id])); assert(d.nodes.every(n => n.sublabel && !n.tag && n.height === 240));
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);
} }
const semantics = d => ({nodes:d.nodes.map(({col,yOffset,...n}) => n), edges:d.edges.map(({fromSide,toSide,route,...e}) => e)});
assert.deepEqual(semantics(variants[0]),semantics(variants[1]));
}); });
+4 -3
View File
@@ -5,7 +5,7 @@ const read = p => readFileSync(p, 'utf8');
const stages = ['Scope', 'Architecture', 'UI/UX', 'Planning', 'Delivery', 'UAT', 'Security', 'Releases']; const stages = ['Scope', 'Architecture', 'UI/UX', 'Planning', 'Delivery', 'UAT', 'Security', 'Releases'];
test('Strategy opens at the canonical autonomous lifecycle', () => { test('Strategy opens at the canonical autonomous lifecycle', () => {
assert.match(read('sidebars/strategy.ts'), /strategySidebar: \['target-operating-model', 'roadmap'/); assert.match(read('sidebars/strategy.ts'), /strategySidebar:\s*\[\s*'target-operating-model',\s*'roadmap'/);
assert.match(read('docs/strategy/target-operating-model.mdx'), /^sidebar_label: Target$/m); assert.match(read('docs/strategy/target-operating-model.mdx'), /^sidebar_label: Target$/m);
for (const id of ['target-operating-model']) { for (const id of ['target-operating-model']) {
const page = read(`docs/strategy/${id}.mdx`); const page = read(`docs/strategy/${id}.mdx`);
@@ -14,7 +14,8 @@ test('Strategy opens at the canonical autonomous lifecycle', () => {
assert(primary.includes('autonomous agent')); assert(primary.includes('autonomous agent'));
assert(primary.includes('direct, guide and review')); assert(primary.includes('direct, guide and review'));
assert(primary.includes('Scope → Architecture → UI/UX → Planning → Delivery → UAT → Security → Releases → Scope')); assert(primary.includes('Scope → Architecture → UI/UX → Planning → Delivery → UAT → Security → Releases → Scope'));
assert(primary.includes('human-controlled code agent')); assert(primary.includes('human-controlled AI Agent'));
assert(!/code[ -]agent/i.test(page));
assert(primary.includes('TO-BE')); assert(primary.includes('TO-BE'));
assert(!/only routes|People authorize|independent gates protect release/.test(primary)); assert(!/only routes|People authorize|independent gates protect release/.test(primary));
} }
@@ -37,7 +38,7 @@ test('both target diagrams have eight stages, eight governed phases and eight hu
assert.deepEqual(d.connections.filter(e => e.from === 'agent').map(e => e.to), ids); assert.deepEqual(d.connections.filter(e => e.from === 'agent').map(e => e.to), ids);
const contributors = d.components.filter(n => n.id.startsWith('code-')); const contributors = d.components.filter(n => n.id.startsWith('code-'));
assert.equal(contributors.length, 8); assert.equal(contributors.length, 8);
assert(contributors.every(n => n.label === 'Code agent' && n.sublabel === 'Human-controlled')); assert(contributors.every(n => n.label === 'AI Agent' && n.sublabel === 'Human-controlled'));
for (const id of ids) { for (const id of ids) {
assert(d.connections.some(e => e.from === `code-${id}` && e.to === id && e.variant === 'dashed')); assert(d.connections.some(e => e.from === `code-${id}` && e.to === id && e.variant === 'dashed'));
assert(d.connections.some(e => e.from === 'agent' && e.to === id && e.variant === 'security')); assert(d.connections.some(e => e.from === 'agent' && e.to === id && e.variant === 'security'));
+111
View File
@@ -0,0 +1,111 @@
import assert from 'node:assert/strict';
import {readFileSync, readdirSync, mkdirSync, writeFileSync} from 'node:fs';
import {join} from 'node:path';
const normalize = path => path.replace(/\/$/, '');
const read = path => JSON.parse(readFileSync(path, 'utf8'));
// Use Docusaurus's resolved IDs/routes and sidebar trees, not filename guesses.
export function auditSidebars(instances, entrySidebars) {
const docs = new Map();
const menus = new Map();
for (const {plugin, version, documents, sidebars} of instances) {
const ids = new Set();
for (const doc of documents) {
assert(!ids.has(doc.id), `Duplicate document ID: ${plugin}:${version}:${doc.id}`);
ids.add(doc.id);
const route = normalize(doc.path);
assert(!docs.has(route), `Duplicate document route: ${route}`);
docs.set(route, {...doc, plugin, version, menu: `${plugin}:${version}:${doc.sidebar}`});
}
for (const [name, items] of Object.entries(sidebars)) menus.set(`${plugin}:${version}:${name}`, items);
}
const links = new Map();
for (const [menu, items] of menus) {
const found = new Set();
const walk = items => {
for (const item of items) {
assert(['category', 'link', 'html'].includes(item.type), `Unresolved sidebar type: ${item.type}`);
assert(!item.unlisted, `Hidden sidebar item: ${item.label}`);
if (item.href && !/^(https?:|mailto:)/.test(item.href)) {
const route = normalize(item.href);
if (item.docId) assert.equal(docs.get(route)?.id, item.docId, `Sidebar ID/route mismatch: ${item.docId}`);
assert(!found.has(route), `Duplicate sidebar route in ${menu}: ${route}`);
found.add(route);
}
if (item.type === 'category') walk(item.items);
}
};
walk(items);
links.set(menu, found);
}
const reachable = new Set(entrySidebars);
for (const menu of reachable) {
assert(menus.has(menu), `Unknown navigation sidebar: ${menu}`);
for (const route of links.get(menu)) {
const doc = docs.get(route);
if (doc) reachable.add(doc.menu);
}
}
for (const [route, doc] of docs) {
assert(reachable.has(doc.menu), `Unreachable sidebar for ${doc.plugin}:${doc.id}`);
assert(links.get(doc.menu)?.has(route), `Missing sidebar document: ${doc.plugin}:${doc.id}`);
}
return [...docs.entries()].map(([route, doc]) => ({route, id:doc.id, plugin:doc.plugin, sidebar:doc.sidebar}));
}
export async function verifySidebarCoverage() {
const global = read('.docusaurus/globalData.json')['docusaurus-plugin-content-docs'];
assert(global && Object.keys(global).length, 'No generated document inventory');
const instances = [];
for (const [plugin, data] of Object.entries(global)) {
const folder = `.docusaurus/docusaurus-plugin-content-docs/${plugin}/p`;
const resolved = readdirSync(folder).filter(f => f.endsWith('.json')).map(f => read(join(folder, f)).version);
for (const version of data.versions) {
const matches = resolved.filter(v => v.version === version.name);
assert.equal(matches.length, 1, `Missing/duplicate resolved version: ${plugin}:${version.name}`);
instances.push({plugin, version:version.name, documents:version.docs, sidebars:matches[0].docsSidebars});
}
}
const {default:config} = await import('../.docusaurus/docusaurus.config.mjs');
const entries = config.themeConfig.navbar.items.filter(i => i.type === 'docSidebar').map(i => `${i.docsPluginId || 'default'}:current:${i.sidebarId}`);
const documents = auditSidebars(instances, entries);
const routes = new Set(documents.map(d => `${d.route.replace(config.baseUrl, '')}/index.html`));
const exclusions = [];
const html = readdirSync('build', {recursive:true}).filter(f => f.endsWith('.html')).sort();
for (const file of html) {
if (routes.delete(file)) continue;
let reason;
if (file === 'index.html') reason = 'Dedicated React homepage, not a documentation page';
else if (file === '404.html') reason = 'Docusaurus not-found system page';
else if (file === 'search/index.html') reason = 'Local-search results UI, not a documentation page';
else if (file === 'strategy/index.html') {
assert(readFileSync(join('build',file),'utf8').includes('http-equiv="refresh"'), 'Strategy alias must remain a redirect');
reason = 'Legacy Strategy redirect alias to Target';
} else if (file.startsWith('diagrams/') && readFileSync(join('build',file),'utf8').includes('<svg')) {
assert.equal(readFileSync(join('build',file),'utf8'), readFileSync(join('static',file),'utf8'), `Viewer must match generated static asset: ${file}`);
reason = 'Standalone diagram viewer asset, not documentation';
}
assert(reason, `Unclassified published HTML route: ${file}`);
exclusions.push({file, reason});
}
assert.equal(routes.size, 0, `Missing published docs: ${[...routes]}`);
// Check all local links and anchors on every document, not only selected Strategy pages.
for (const {route} of documents) {
const file = `build/${route.replace(config.baseUrl, '')}/index.html`;
const body = readFileSync(file, 'utf8');
for (const [, href] of body.matchAll(/href="([^" ]+)"/g)) {
if (!href.startsWith(config.baseUrl) && !href.startsWith('#')) continue;
const url = new URL(href, `https://docs.invalid${route}/`);
let target = url.pathname.slice(config.baseUrl.length);
if (!target || target.endsWith('/')) target += 'index.html';
else if (!target.split('/').at(-1).includes('.')) target += '/index.html';
const targetBody = readFileSync(join('build', decodeURIComponent(target)), 'utf8');
if (url.hash) assert(targetBody.includes(`id="${decodeURIComponent(url.hash.slice(1))}"`), `Broken anchor: ${route} -> ${href}`);
}
}
mkdirSync('.cache', {recursive:true});
writeFileSync('.cache/sidebar-coverage.json', JSON.stringify({documents, exclusions}, null, 2));
console.log(`PASS: ${documents.length} published docs reachable in resolved sidebars; ${exclusions.length} classified non-doc HTML routes; all doc links/anchors checked`);
return {documents, exclusions};
}
+51
View File
@@ -0,0 +1,51 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {auditSidebars} from './sidebar-coverage.mjs';
const fixture = () => [{plugin:'strategy', version:'current', documents:[
{id:'overview', path:'/strategy/overview/', sidebar:'main'},
{id:'nested/reference', path:'/strategy/reference/', sidebar:'main'},
], sidebars:{main:[{type:'category', label:'Stage', href:'/strategy/overview', items:[
{type:'category', label:'Reference', items:[{type:'link', docId:'nested/reference', href:'/strategy/reference'}]},
]}]}}];
const entries = ['strategy:current:main'];
test('resolved nested entries and linked category overviews both count', () => {
assert.equal(auditSidebars(fixture(), entries).length, 2);
});
test('missing docs fail even if Docusaurus assigns a displayed sidebar', () => {
const data = fixture();
data[0].sidebars.main[0].items = [];
assert.throws(() => auditSidebars(data, entries), /Missing sidebar document/);
});
test('new publishable docs and absent category overview links fail closed', () => {
const data = fixture();
data[0].documents.push({id:'new', path:'/new'});
assert.throws(() => auditSidebars(data, entries), /Unreachable sidebar/);
const withoutLink = fixture();
delete withoutLink[0].sidebars.main[0].href;
assert.throws(() => auditSidebars(withoutLink, entries), /Missing sidebar document/);
});
test('duplicate normalized routes and duplicate sidebar entries fail', () => {
const data = fixture();
data[0].documents.push({...data[0].documents[0], path:'/strategy/overview'});
assert.throws(() => auditSidebars(data, entries), /Duplicate document ID/);
data[0].documents.at(-1).id = 'different';
assert.throws(() => auditSidebars(data, entries), /Duplicate document route/);
const duplicate = fixture();
duplicate[0].sidebars.main.push({type:'link', href:'/strategy/overview/'});
assert.throws(() => auditSidebars(duplicate, entries), /Duplicate sidebar route/);
});
test('unlisted entries and unresolved sidebar types fail', () => {
const data = fixture();
data[0].sidebars.main[0].unlisted = true;
assert.throws(() => auditSidebars(data, entries), /Hidden sidebar item/);
data[0].sidebars.main[0] = {type:'autogenerated'};
assert.throws(() => auditSidebars(data, entries), /Unresolved sidebar type/);
});
test('disconnected plugin menus fail, cross-plugin sidebar links make them reachable', () => {
const data = fixture();
data.push({plugin:'default', version:'current', documents:[{id:'help', path:'/docs/help', sidebar:'docs'}], sidebars:{docs:[{type:'link', href:'/docs/help'}]}});
assert.throws(() => auditSidebars(data, entries), /Unreachable sidebar/);
data[0].sidebars.main.push({type:'link', href:'/docs/help/'});
assert.equal(auditSidebars(data, entries).length, 3);
});
+1 -1
View File
@@ -6,7 +6,7 @@ const read = path => readFileSync(path, 'utf8');
test('Skills Measurement is an open adoption collection plan, not a hidden catalogue', () => { test('Skills Measurement is an open adoption collection plan, not a hidden catalogue', () => {
const page = read('docs/strategy/measurement.mdx'); const page = read('docs/strategy/measurement.mdx');
assert.match(page, /title: Skills Measurement\nsidebar_label: Skills Measurement/); assert.match(page, /title: Skills Measurement\nsidebar_label: Measurement/);
for (const heading of ['Establish the baseline', 'Measure the pilot', 'Extend collection as adoption grows', 'Keep the collection running']) assert(page.includes(heading), heading); for (const heading of ['Establish the baseline', 'Measure the pilot', 'Extend collection as adoption grows', 'Keep the collection running']) assert(page.includes(heading), heading);
assert(!page.includes('<details>')); assert(!page.includes('<details>'));
assert(page.includes('./reference/measurement-contracts.mdx')); assert(page.includes('./reference/measurement-contracts.mdx'));
+60
View File
@@ -0,0 +1,60 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync, mkdtempSync, rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {execFileSync} from 'node:child_process';
import {skillsViewer,isSkillsStory} from './skills-viewer.mjs';
const read = p => readFileSync(p,'utf8');
const topics = ['governance','quality','adoption','measurement','maintenance'];
test('all six Skills Adoption pages account for exactly five diagrams and ten sources',()=>{
let count=0;
for(const route of ['stage-01-skills-adoption',...topics.map(t=>t==='measurement'?t:`skills-${t}`)]){
const diagrams=[...read(`docs/strategy/${route}.mdx`).matchAll(/<ArchifyDiagram name="([^"]+)"/g)];
assert.equal(diagrams.length,route.startsWith('stage-')?0:1);
for(const [,name] of diagrams){assert(isSkillsStory(name));for(const suffix of ['','-mobile'])JSON.parse(read(`diagrams/${name}${suffix}.architecture.json`));count++;}
}
assert.equal(count,5);assert(!isSkillsStory('strategy-target'));assert(!isSkillsStory('strategy-roadmap'));
});
for(const topic of topics) test(`${topic}: rich native source, wide desktop, mobile parity and checked adapter`,()=>{
const name=`strategy-${topic}`;const desktop=JSON.parse(read(`diagrams/${name}.architecture.json`));
const mobile=JSON.parse(read(`diagrams/${name}-mobile.architecture.json`));
const semantic = d=>d.components.map(({pos,size,...n})=>n);
assert.deepEqual(semantic(desktop),semantic(mobile));
assert(new Set(desktop.components.map(n=>n.pos[0])).size>1);
assert.equal(new Set(mobile.components.map(n=>n.pos[0])).size,1);
for(const n of desktop.components){assert(n.tag);assert(n.sublabel);assert(n.sublabel.split('\n').length<=4);}
const dir=mkdtempSync(join(tmpdir(),'skills-viewer-'));
try{
const path=join(dir,'native.html');
execFileSync(process.execPath,['vendor/archify/renderers/architecture/render-architecture.mjs',`diagrams/${name}.architecture.json`,path]);
const native=read(path);const html=skillsViewer(native,desktop);
assert.equal((html.match(/<g\b[^>]*data-node-id=/g)||[]).length,desktop.components.length);
assert.equal((html.match(/<text data-node-label=/g)||[]).length,0); // Adapter uses explicit position before semantic attributes.
assert.equal((html.match(/data-node-label=""/g)||[]).length,desktop.components.length);
assert(html.includes('eldorado-skills-typography'));
assert.throws(()=>skillsViewer(native.replace('data-node-id="'+desktop.components[0].id+'"','data-node-id="missing"'),desktop));
const bad=structuredClone(desktop);bad.components[0].sublabel='';assert.throws(()=>skillsViewer(native,bad));
}finally{rmSync(dir,{recursive:true,force:true});}
});
test('domain contracts retain human authority, honest measures and Delivery-first rollout',()=>{
const text=t=>JSON.stringify(JSON.parse(read(`diagrams/strategy-${t}.architecture.json`)));
assert(text('governance').includes('Author is not sole approver'));
assert(text('quality').includes('Critical effect blocks'));
assert(text('adoption').includes('Delivery first')&&text('adoption').includes('Later order undecided'));
assert(text('adoption').includes('No quotas or rankings'));
for(const marker of ['Tracker + acceptance','Invoices + review minutes','Changes + defects','Anonymous 1–5 survey','Missing buckets = partial','Zero denominator = N/A'])assert(text('measurement').includes(marker),marker);
assert(text('maintenance').includes('No automatic reinstatement'));
});
test('topic compositions explain distinct relationships rather than a fixed card count',()=>{
const spec=t=>JSON.parse(read(`diagrams/strategy-${t}.architecture.json`));
const has=(d,a,b)=>assert(d.connections.some(e=>e.from===a&&e.to===b),`${a} → ${b}`);
const g=spec('governance');const steps=g.components.filter(n=>/^0[1-6]/.test(n.label));
assert.equal(steps.length,6);
for(let i=1;i<steps.length;i++){assert(steps[i].pos[0]>steps[i-1].pos[0]);assert(steps[i].pos[1]>steps[i-1].pos[1]);has(g,steps[i-1].id,steps[i].id);}
has(g,'approve','owner');has(g,'distribute','withdraw');
const q=spec('quality');for(const id of ['selection','safety','artifact','outcome'])has(q,id,'gate');has(q,'gate','accept');has(q,'gate','rework');
const a=spec('adoption');for(const [x,y] of [['human','agent'],['agent','human'],['agent','learn'],['learn','delivery'],['learn','expand'],['human','expand']])has(a,x,y);
const m=spec('measurement');for(const id of ['success','quality','cost','confidence']){has(m,id+'-source',id);has(m,id,'interpret');}
const c=spec('maintenance');for(const [x,y] of [['observe','triage'],['triage','change'],['change','validate'],['validate','release'],['release','observe'],['triage','safe'],['safe','observe'],['triage','retire'],['triage','contain'],['contain','change']])has(c,x,y);
});
+44
View File
@@ -0,0 +1,44 @@
// Native editable geometry and edges; scoped editorial typography for Stage 01.
export const isSkillsStory = name => /^strategy-(governance|quality|adoption|measurement|maintenance)(-mobile)?$/.test(name);
const escape = text => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
export function skillsViewer(html, spec) {
if (spec.diagram_type !== 'architecture' || !spec.components.length) throw new Error('Skills story contract drift');
const detailSize = spec.meta.output.includes('-mobile/') ? 34 : 32;
for (const node of spec.components) {
const pattern = new RegExp(`<g\\b[^>]*data-node-id="${node.id}"[^>]*>[\\s\\S]*?<text data-node-label=[\\s\\S]*?</g>`);
const match = html.match(pattern);
if (!match) throw new Error(`Missing Skills node: ${node.id}`);
const original = match[0];
const rectangles = [...original.matchAll(/<rect\b[^>]*class="c-[^"]+"[^>]*\/>/g)];
if (rectangles.length !== 2) throw new Error('Skills shape contract drift');
const [x,y] = node.pos;
const lines = node.sublabel.split('\n');
if (!node.tag || !node.sublabel || lines.length > 4) throw new Error(`Skills copy exceeds capacity: ${node.id}`);
const text = (value, top, size, attrs) => `<text x="${x+28}" y="${y+top}" font-size="${size}" ${attrs}>${escape(value)}</text>`;
const content = text(node.tag, 40, detailSize, 'class="t-muted" data-detail="context"')
+ text(node.label, 89, 36, 'class="t-primary" data-node-label="" font-weight="700"')
+ lines.map((line,i) => text(line, 138+i*42, detailSize, 'class="t-primary" data-detail="context"')).join('');
const prefix = original.slice(0,rectangles[1].index+rectangles[1][0].length).replaceAll('rx="6"','rx="0"');
html = html.replace(original,prefix+content+'</g>');
}
const roots = [...html.matchAll(/<svg\b[^>]*role="img"[^>]*>/g)];
if (roots.length !== 1) throw new Error('Skills SVG contract drift');
const [width,height] = spec.meta.viewBox;
const root = roots[0][0].replace(/viewBox="[^"]*"/,`viewBox="0 0 ${width} ${height}"`);
// Embedded in SVG so downloaded files retain typography without the viewer.
html = html.replace(roots[0][0],root+`<style id="eldorado-skills-typography">
[data-node-id] text {font-family:Arial,sans-serif;}
rect[fill="url(#grid)"] {opacity:0;}
[data-node-id] > rect:not(.c-mask) {stroke-width:3;}
[data-edge-id] {opacity:1;}
</style>`);
// Native labels use tiny technical annotations. Enlarge both text and mask,
// retaining the native edge identity, routing and explicit label position.
html = html.replace(/(<g data-detail="context"[^>]*data-edge-id[^>]*>)([\s\S]*?)(<\/g>)/g, (all,open,body,close) => {
const text = body.match(/<text x="([^"]+)" y="([^"]+)"([^>]*)>([\s\S]*?)<\/text>/);
if (!text) throw new Error('Skills edge label contract drift');
const x=Number(text[1]), y=Number(text[2]), width=text[4].replace(/&[^;]+;/g,'x').length*17+24;
return open+`<rect x="${x-width/2}" y="${y-34}" width="${width}" height="44" class="c-mask"/>`+text[0].replace('font-size="8"',`font-size="${detailSize}"`)+close;
});
return html;
}
+35 -5
View File
@@ -1,17 +1,47 @@
import {test} from 'node:test'; import {test} from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import {readFileSync, existsSync} from 'node:fs'; import {readFileSync, existsSync} from 'node:fs';
import {runInNewContext} from 'node:vm';
import ts from 'typescript';
const read = p => readFileSync(p, 'utf8'); const read = p => readFileSync(p, 'utf8');
test('Strategy has one canonical Target, then Roadmap, without a landing menu duplicate', () => { test('Strategy has Target, Roadmap, a linked Skills Adoption category and Autonomous Agent', () => {
const sidebar = read('sidebars/strategy.ts').split('strategySidebar: [')[1].split(']')[0]; const compiled = ts.transpileModule(read('sidebars/strategy.ts'), {compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText;
const ids = [...sidebar.matchAll(/'([^']+)'/g)].map(match => match[1]); const sandbox = {exports:{}};
assert.deepEqual(ids, ['target-operating-model', 'roadmap', 'skills-governance', 'skills-quality', 'skills-adoption', 'measurement', 'skills-maintenance']); runInNewContext(compiled, sandbox);
assert.equal(ids.filter(id => id === 'target-operating-model').length, 1); const sidebar = JSON.parse(JSON.stringify(sandbox.exports.default.strategySidebar));
assert.deepEqual(sidebar, ['target-operating-model', 'roadmap', {
type:'category', label:'Stage - 01 - Skills Adoption',
link:{type:'doc', id:'stage-01-skills-adoption'}, collapsed:true,
items:[
{type:'doc', id:'skills-governance', label:'Governance'},
{type:'doc', id:'skills-quality', label:'Quality'},
{type:'doc', id:'skills-adoption', label:'Adoption'},
{type:'doc', id:'measurement', label:'Measurement'},
{type:'doc', id:'skills-maintenance', label:'Maintenance'},
{type:'category', label:'Reference', collapsed:true, items:[{type:'doc', id:'reference/measurement-contracts', label:'Measurement Contracts'}]},
],
}, 'stage-02-autonomous-agent']);
assert.match(read('docs/strategy/target-operating-model.mdx'), /^sidebar_label: Target$/m); assert.match(read('docs/strategy/target-operating-model.mdx'), /^sidebar_label: Target$/m);
assert.match(read('docusaurus.config.ts'), /type: 'docSidebar', docsPluginId: 'strategy', sidebarId: 'strategySidebar', label: 'Strategy'/); assert.match(read('docusaurus.config.ts'), /type: 'docSidebar', docsPluginId: 'strategy', sidebarId: 'strategySidebar', label: 'Strategy'/);
}); });
test('stage overviews explain the foundation and future without replacing topic content', () => {
const first = read('docs/strategy/stage-01-skills-adoption.mdx');
const second = read('docs/strategy/stage-02-autonomous-agent.mdx');
for (const [id, title, page] of [
['stage-01-skills-adoption', 'Stage - 01 - Skills Adoption', first],
['stage-02-autonomous-agent', 'Stage - 02 - Autonomous Agent', second],
]) {
assert(page.includes(`id: ${id}\ntitle: ${title}`));
for (const link of ['./roadmap.mdx', './target-operating-model.mdx']) assert(page.includes(link));
}
for (const id of ['skills-governance','skills-quality','skills-adoption','measurement','skills-maintenance']) assert(first.includes(`./${id}.mdx`));
for (const marker of ['human + AI Agent','Delivery first','order is not yet decided','KPIs','baseline','topology','feedback','new current situation']) assert(first.includes(marker), marker);
for (const marker of ['future','proven skills','measurements','working practices','direction and review','not deployed autonomy','approved KPI thresholds']) assert(second.includes(marker), marker);
assert.match(read('docs/strategy/measurement.mdx'), /^title: Skills Measurement\nsidebar_label: Measurement$/m);
});
test('old Strategy entry redirects to Target instead of maintaining a second document', () => { test('old Strategy entry redirects to Target instead of maintaining a second document', () => {
assert(!existsSync('docs/strategy/index.mdx')); assert(!existsSync('docs/strategy/index.mdx'));
assert.match(read('docusaurus.config.ts'), /'@docusaurus\/plugin-client-redirects'/); assert.match(read('docusaurus.config.ts'), /'@docusaurus\/plugin-client-redirects'/);
+5 -5
View File
@@ -8,12 +8,12 @@ test('every strategy page leads with a compact diagram story and closes its refe
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 narrative = page.split('<div className="strategy-content">')[1].replace(/<details>[\s\S]*?<\/details>/g, ''); const narrative = page.split('<div className="strategy-content">')[1].replace(/<details>[\s\S]*?<\/details>/g, '');
assert.equal((narrative.match(/<ArchifyDiagram /g) || []).length, 1, id); assert.equal((narrative.match(/<ArchifyDiagram /g) || []).length, id === 'roadmap' ? 3 : 1, id);
assert(narrative.includes(' story />'), id); assert(narrative.includes(' story />'), id);
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 : id === 'roadmap' ? 5 : 1, id); assert.equal((narrative.match(/^## /gm) || []).length, id === 'measurement' ? 4 : id === 'roadmap' ? 4 : 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);
@@ -35,10 +35,10 @@ test('mobile story diagrams preserve the same nodes and directed relationships',
} }
const desktop = JSON.parse(read(`diagrams/${name}.architecture.json`)); const desktop = JSON.parse(read(`diagrams/${name}.architecture.json`));
const mobile = JSON.parse(read(`diagrams/${name}-mobile.architecture.json`)); const mobile = JSON.parse(read(`diagrams/${name}-mobile.architecture.json`));
assert(desktop.components.length >= 5 && desktop.components.length <= (name === 'strategy-target' ? 18 : 7), name); assert(desktop.components.length >= 5, name);
const semantic = d => ({nodes:d.components.map(({pos,size,...n})=>n),edges:d.connections.map(({fromSide,toSide,via,labelAt,...e})=>e)}); const semantic = d => ({nodes:d.components.map(({pos,size,...n})=>n),edges:d.connections.map(({fromSide,toSide,route,via,labelAt,...e})=>e)});
assert.deepEqual(semantic(desktop), semantic(mobile), name); assert.deepEqual(semantic(desktop), semantic(mobile), name);
assert(desktop.connections.every(e => (e.label || e.id.startsWith('govern-')) && e.from !== e.to), name); assert(desktop.connections.every(e => (e.label || e.id.startsWith('govern-') || /^strategy-(governance|quality|adoption|measurement|maintenance)$/.test(name)) && e.from !== e.to), name);
assert.equal(desktop.meta.legend.mode, 'hidden'); assert.equal(desktop.meta.legend.mode, 'hidden');
} }
}); });
+3 -2
View File
@@ -3,9 +3,10 @@ import assert from 'node:assert/strict';
import {readFileSync, readdirSync} from 'node:fs'; import {readFileSync, readdirSync} from 'node:fs';
const read = p => readFileSync(p, 'utf8'); const read = p => readFileSync(p, 'utf8');
const model = JSON.parse(read('research/ai-adoption/strategy-model.json')); const model = JSON.parse(read('research/ai-adoption/strategy-model.json'));
test('seven canonical strategy pages preserve target boundary and separate evidence', () => { test('seven existing strategy pages and two stage overviews preserve canonical routes', () => {
assert.equal(model.pages.length, 7); assert.equal(model.pages.length, 7);
assert.equal(readdirSync('docs/strategy').filter(p => p.endsWith('.mdx')).length, 7); assert.deepEqual(readdirSync('docs/strategy').filter(p => p.endsWith('.mdx')).sort(),
[...model.pages.map(({id}) => `${id}.mdx`), 'stage-01-skills-adoption.mdx', 'stage-02-autonomous-agent.mdx'].sort());
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`);
assert(page.includes(`id: ${id}`)); assert(page.includes(`id: ${id}`));
+31
View File
@@ -0,0 +1,31 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createRouter} from '../vendor/archify/renderers/architecture/routing.mjs';
import {segmentIntersectsRect} from '../vendor/archify/renderers/shared/geometry.mjs';
for (const suffix of ['', '-mobile']) {
test(`Target${suffix} uses shared native midpoint ports without node crossings`, () => {
const diagram = JSON.parse(readFileSync(`diagrams/strategy-target${suffix}.architecture.json`, 'utf8'));
const boxes = new Map(diagram.components.map(({id, pos: [x, y], size: [width, height]}) =>
[id, {id, x, y, width, height, cx: x + width / 2, cy: y + height / 2}]));
const router = createRouter(boxes, diagram.connections, {distinctAutomaticPorts: true});
const agent = boxes.get('agent');
for (const edge of diagram.connections.filter(edge => edge.from === 'agent')) {
const side = suffix ? 'left' : ['scope', 'architecture', 'releases'].includes(edge.to) ? 'top'
: ['planning', 'delivery', 'uat'].includes(edge.to) ? 'bottom' : edge.to === 'security' ? 'left' : 'right';
assert.equal(edge.fromSide, side, edge.id);
assert(edge.via || (edge.route && edge.route !== 'auto'), `${edge.id} must opt out of automatic port spreading`);
const expected = {top: [agent.cx, agent.y], bottom: [agent.cx, agent.y + agent.height],
left: [agent.x, agent.cy], right: [agent.x + agent.width, agent.cy]}[side];
const points = router.pathFor(edge).points;
assert.deepEqual(points[0], expected, edge.id);
for (let i = 1; i < points.length; i++) {
for (const box of boxes.values()) {
if ([edge.from, edge.to].includes(box.id)) continue;
assert(!segmentIntersectsRect({start: points[i - 1], end: points[i]}, box), `${edge.id} crosses ${box.id}`);
}
}
}
});
}
+32
View File
@@ -0,0 +1,32 @@
// Owned desktop-only integration, applied AFTER native artifact verification.
// Native architecture JSON has no per-edge attachment offset. Keep the central
// governance ports native; shift only these lifecycle channels on the same sides.
export const targetLifecycleOffsets = [
{id:'stage-2', from:'architecture', to:'ui-ux', x:870, shiftedX:850, y1:262.5, y2:457.5},
{id:'stage-3', from:'ui-ux', to:'planning', x:870, shiftedX:850, y1:532.5, y2:727.5},
{id:'stage-6', from:'uat', to:'security', x:330, shiftedX:350, y1:727.5, y2:532.5},
{id:'stage-7', from:'security', to:'releases', x:330, shiftedX:350, y1:457.5, y2:262.5},
];
export function targetRouting(viewer) {
for (const {id, from, to, x, shiftedX, y1, y2} of targetLifecycleOffsets) {
const oldPath = `M ${x} ${y1} L ${x} ${y2}`;
const newPath = `M ${shiftedX} ${y1} L ${shiftedX} ${y2}`;
const oldPoints = `${x},${y1};${x},${y2}`;
const newPoints = `${shiftedX},${y1};${shiftedX},${y2}`;
// Match the whole native crossover group: its mask must move with the edge.
// Trace/export use this actual path; composition checks use its point list.
const groups = [...viewer.matchAll(/<g data-graph-role="automatic-crossover"[^>]*>\s*<path\b[^>]*\/>\s*<path\b[^>]*\/>\s*<\/g>/g)]
.filter(([group]) => group.includes(`data-edge-id="${id}"`));
if (groups.length !== 1) throw new Error(`Target routing drift: ${id} group`);
const group = groups[0][0];
for (const attribute of [`data-edge-from="${from}"`, `data-edge-to="${to}"`, `data-composition-points="${oldPoints}"`, 'data-graph-role="automatic-crossover-underlay"']) {
if (!group.includes(attribute)) throw new Error(`Target routing drift: ${id} ${attribute}`);
}
if (group.split(`d="${oldPath}"`).length !== 3) throw new Error(`Target routing drift: ${id} paths`);
const shifted = group.replaceAll(`d="${oldPath}"`, `d="${newPath}"`)
.replace(`data-composition-points="${oldPoints}"`, `data-composition-points="${newPoints}"`);
viewer = viewer.replace(group, shifted);
}
return viewer;
}
+32
View File
@@ -0,0 +1,32 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {targetRouting, targetLifecycleOffsets} from './target-routing.mjs';
const fixture = targetLifecycleOffsets.map(({id, from, to, x, y1, y2}) => `<g data-graph-role="automatic-crossover" style="--step:9">
<path data-graph-role="automatic-crossover-underlay" d="M ${x} ${y1} L ${x} ${y2}"/>
<path data-edge-from="${from}" data-edge-to="${to}" data-edge-id="${id}" data-composition-points="${x},${y1};${x},${y2}" d="M ${x} ${y1} L ${x} ${y2}" marker-end="url(#arrowhead-emphasis)"/>
</g>`).join('\n');
test('Target adapter shifts precisely four paths, masks and composition points', () => {
const unrelated = '<path data-edge-id="govern-architecture" d="M 600 450 L 870 262.5"/><g data-node-id="agent"/>';
const output = targetRouting(fixture + unrelated);
assert(output.endsWith(unrelated));
let restored = output;
for (const {x, shiftedX, y1, y2} of targetLifecycleOffsets) {
assert.equal(output.split(`d="M ${shiftedX} ${y1} L ${shiftedX} ${y2}"`).length, 3);
assert(output.includes(`data-composition-points="${shiftedX},${y1};${shiftedX},${y2}"`));
restored = restored.replaceAll(`d="M ${shiftedX} ${y1} L ${shiftedX} ${y2}"`, `d="M ${x} ${y1} L ${x} ${y2}"`)
.replace(`data-composition-points="${shiftedX},${y1};${shiftedX},${y2}"`, `data-composition-points="${x},${y1};${x},${y2}"`);
}
assert.equal(restored, fixture + unrelated, 'all other bytes preserved');
});
test('Target adapter fails closed on missing, duplicate, moved or changed native edges', () => {
for (const changed of [fixture.replace('stage-2', 'missing'), fixture + fixture,
fixture.replace('870 262.5', '871 262.5'), fixture.replace('870,262.5', '871,262.5'),
fixture.replace('data-edge-from="architecture"', 'data-edge-from="scope"'),
fixture.replace('automatic-crossover-underlay', 'unknown')]) {
assert.throws(() => targetRouting(changed), /Target routing drift/);
}
assert.throws(() => targetRouting(targetRouting(fixture)), /Target routing drift/);
});
+9 -2
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import {readFileSync, readdirSync, existsSync} from 'node:fs'; import {readFileSync, readdirSync, existsSync} from 'node:fs';
import {join} from 'node:path'; import {join} from 'node:path';
import {verifySidebarCoverage} from './sidebar-coverage.mjs';
const read = p => readFileSync(p, 'utf8'); const read = p => readFileSync(p, 'utf8');
const config = read('docusaurus.config.ts'); const config = read('docusaurus.config.ts');
assert(config.includes("baseUrl: '/eldorado-v1/documentation/'")); assert(config.includes("baseUrl: '/eldorado-v1/documentation/'"));
@@ -16,15 +17,21 @@ const workflow = read('.gitea/workflows/pages.yml');
assert(workflow.includes('mountpoint -q')); assert(workflow.includes('mountpoint -q'));
assert(workflow.includes("gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'")); assert(workflow.includes("gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'"));
if (process.argv.includes('--build')) { if (process.argv.includes('--build')) {
await verifySidebarCoverage();
const model = JSON.parse(read('research/ai-adoption/strategy-model.json')); const model = JSON.parse(read('research/ai-adoption/strategy-model.json'));
const indexes = readdirSync('build').filter(f => /^search-index.*\.json$/.test(f)).map(f => read(join('build',f))).join('\n'); const indexes = readdirSync('build').filter(f => /^search-index.*\.json$/.test(f)).map(f => read(join('build',f))).join('\n');
const redirect = read('build/strategy/index.html'); const redirect = read('build/strategy/index.html');
assert(redirect.includes('/eldorado-v1/documentation/strategy/target-operating-model/')); assert(redirect.includes('/eldorado-v1/documentation/strategy/target-operating-model/'));
assert(redirect.includes('window.location.href') && redirect.includes('http-equiv="refresh"') && !redirect.includes('archify-panel'), 'Landing must redirect, not duplicate Target'); assert(redirect.includes('window.location.href') && redirect.includes('http-equiv="refresh"') && !redirect.includes('archify-panel'), 'Landing must redirect, not duplicate Target');
for (const {id, title} of model.pages) { const strategyPages = [...model.pages,
{id:'stage-01-skills-adoption', title:'Stage - 01 - Skills Adoption'},
{id:'stage-02-autonomous-agent', title:'Stage - 02 - Autonomous Agent'},
];
for (const {id, title} of strategyPages) {
const route = `strategy/${id === 'index' ? '' : `${id}/`}index.html`; const route = `strategy/${id === 'index' ? '' : `${id}/`}index.html`;
const page = read(join('build',route)); const page = read(join('build',route));
assert(page.includes(title) && page.includes('Target operating model'), route); assert(page.includes(title), route);
if (!id.startsWith('stage-')) assert(page.includes('Target operating model'), route);
assert(indexes.includes(title), `Missing indexed Strategy page: ${id}`); assert(indexes.includes(title), `Missing indexed Strategy page: ${id}`);
for (const match of page.matchAll(/href="(\/eldorado-v1\/documentation\/[^"?#]*)/g)) { for (const match of page.matchAll(/href="(\/eldorado-v1\/documentation\/[^"?#]*)/g)) {
const local = decodeURIComponent(match[1].replace('/eldorado-v1/documentation/','')); const local = decodeURIComponent(match[1].replace('/eldorado-v1/documentation/',''));
+10 -1
View File
@@ -1,2 +1,11 @@
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
export default {contributionSidebar: ['contribute', 'diagrams']} satisfies SidebarsConfig; export default {
contributionSidebar: ['contribute', 'diagrams', {
type: 'category', label: 'Portal maintenance', collapsed: true,
items: [
{type: 'link', label: 'Starting point', href: '/eldorado-v1/documentation/docs/overview/'},
{type: 'link', label: 'Update and publish', href: '/eldorado-v1/documentation/docs/maintain/'},
{type: 'link', label: 'Site configuration', href: '/eldorado-v1/documentation/docs/reference/'},
],
}],
} satisfies SidebarsConfig;
+24 -1
View File
@@ -1,2 +1,25 @@
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
export default {strategySidebar: ['target-operating-model', 'roadmap', 'skills-governance', 'skills-quality', 'skills-adoption', 'measurement', 'skills-maintenance']} satisfies SidebarsConfig; export default {
strategySidebar: [
'target-operating-model',
'roadmap',
{
type: 'category',
label: 'Stage - 01 - Skills Adoption',
link: {type: 'doc', id: 'stage-01-skills-adoption'},
collapsed: true,
items: [
{type: 'doc', id: 'skills-governance', label: 'Governance'},
{type: 'doc', id: 'skills-quality', label: 'Quality'},
{type: 'doc', id: 'skills-adoption', label: 'Adoption'},
{type: 'doc', id: 'measurement', label: 'Measurement'},
{type: 'doc', id: 'skills-maintenance', label: 'Maintenance'},
{
type: 'category', label: 'Reference', collapsed: true,
items: [{type: 'doc', id: 'reference/measurement-contracts', label: 'Measurement Contracts'}],
},
],
},
'stage-02-autonomous-agent',
],
} satisfies SidebarsConfig;
+12 -2
View File
@@ -11,6 +11,16 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
const opener = useRef<HTMLButtonElement>(null); const opener = useRef<HTMLButtonElement>(null);
const headingId = useId(); const headingId = useId();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [narrowRoadmap, setNarrowRoadmap] = useState(false);
const skillsStory = /^strategy-(governance|quality|adoption|measurement|maintenance)$/.test(name);
useEffect(() => {
if (!name.startsWith('strategy-roadmap') && !skillsStory) return;
const media = window.matchMedia(skillsStory ? '(max-width: 1100px)' : '(max-width: 999px)');
const update = () => setNarrowRoadmap(media.matches);
update();
media.addEventListener('change', update);
return () => media.removeEventListener('change', update);
}, [name, skillsStory]);
const {colorMode, setColorMode} = useColorMode(); const {colorMode, setColorMode} = useColorMode();
const initialTheme = useRef(colorMode); const initialTheme = useRef(colorMode);
const desktop = useRef<HTMLIFrameElement>(null); const desktop = useRef<HTMLIFrameElement>(null);
@@ -57,7 +67,7 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
return () => {document.body.style.overflow = previousOverflow;}; return () => {document.body.style.overflow = previousOverflow;};
}, [expanded]); }, [expanded]);
const close = () => {dialog.current?.close();}; const close = () => {dialog.current?.close();};
return <section className={`archify-panel${compact ? ' archify-panel--compact' : ''}${story ? ' archify-panel--story' : ''}`} data-diagram={name} aria-label={title}> return <section className={`archify-panel${compact ? ' archify-panel--compact' : ''}${story ? ' archify-panel--story' : ''}${skillsStory ? ' archify-panel--skills' : ''}`} data-diagram={name} aria-label={title}>
<div className="archify-toolbar"><button ref={opener} type="button" className="archify-expand" aria-label={`Expand ${title}`} title="Expand diagram" onClick={() => setExpanded(true)}><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M21 16v5h-5M8 21H3v-5" /></svg></button></div> <div className="archify-toolbar"><button ref={opener} type="button" className="archify-expand" aria-label={`Expand ${title}`} title="Expand diagram" onClick={() => setExpanded(true)}><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M21 16v5h-5M8 21H3v-5" /></svg></button></div>
{story ? <> {story ? <>
<iframe className="story-desktop" ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?embed=1&theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" /> <iframe className="story-desktop" ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?embed=1&theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />
@@ -65,7 +75,7 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
</> : <iframe ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />} </> : <iframe ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />}
<dialog ref={dialog} className="archify-modal" aria-labelledby={headingId} onClose={() => {setExpanded(false); opener.current?.focus();}}> <dialog ref={dialog} className="archify-modal" aria-labelledby={headingId} onClose={() => {setExpanded(false); opener.current?.focus();}}>
<div className="archify-modal-toolbar"><strong id={headingId}>{title}</strong><button type="button" onClick={close} autoFocus aria-label="Close full-screen diagram"><span aria-hidden="true">×</span></button></div> <div className="archify-modal-toolbar"><strong id={headingId}>{title}</strong><button type="button" onClick={close} autoFocus aria-label="Close full-screen diagram"><span aria-hidden="true">×</span></button></div>
{expanded && <iframe ref={fullscreen} onLoad={() => syncTheme(fullscreen.current)} src={`${source}?theme=${initialTheme.current}`} title={`${title} — expanded`} sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" />} {expanded && <iframe ref={fullscreen} onLoad={() => syncTheme(fullscreen.current)} src={`${narrowRoadmap ? mobileSource : source}?${name.startsWith('strategy-roadmap') || skillsStory ? 'embed=1&' : ''}theme=${initialTheme.current}`} title={`${title} — expanded`} sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" />}
</dialog> </dialog>
</section>; </section>;
} }
+35 -11
View File
@@ -111,11 +111,12 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
.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;} .theme-doc-markdown:has([data-diagram="strategy-roadmap"]) {max-width: none;}
.archify-panel--story[data-diagram="strategy-roadmap"] {max-width: none;} .archify-panel--story[data-diagram^="strategy-roadmap"] {max-width: none; --roadmap-height: 640px;}
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-desktop {aspect-ratio: auto; height: 360px;} .archify-panel--story[data-diagram^="strategy-roadmap"] > .story-desktop {aspect-ratio: auto; height: 300px;}
.archify-panel--story[data-diagram="strategy-roadmap-loop"] {--roadmap-height: 1840px;}
.archify-panel--story[data-diagram="strategy-roadmap-loop"] > .story-desktop {height: 460px;}
.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-maintenance"] > iframe {aspect-ratio: 524 / 408;}
.strategy-content > details {margin: .65rem 0; border: 1px solid var(--line); padding: .8rem 1rem; background: var(--surface);} .strategy-content > details {margin: .65rem 0; border: 1px solid var(--line); padding: .8rem 1rem; background: var(--surface);}
.strategy-content summary {cursor: pointer; font-weight: 700;} .strategy-content summary {cursor: pointer; font-weight: 700;}
.strategy-content summary:focus-visible {outline: 3px solid #b88719; outline-offset: 4px;} .strategy-content summary:focus-visible {outline: 3px solid #b88719; outline-offset: 4px;}
@@ -124,15 +125,14 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
@media (max-width: 700px) { @media (max-width: 700px) {
.archify-panel--story > .story-desktop {display: none;} .archify-panel--story > .story-desktop {display: none;}
.archify-panel--story > .story-mobile {display: block; height: auto; aspect-ratio: 240 / 748;} .archify-panel--story > .story-mobile {display: block; height: auto; aspect-ratio: 240 / 748;}
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {aspect-ratio: 364 / 2600;}
.archify-panel--story[data-diagram="strategy-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;} .archify-panel--story[data-diagram="strategy-target"] > .story-mobile {aspect-ratio: 680 / 2340;}
} }
/* Five Roadmap cards need desktop space, including room for the sidebar. */ /* Keep the three-card loop readable beside the documentation sidebar. */
@media (width < 1360px) { @media (width < 1100px) {
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-desktop {display: none;} .archify-panel--story[data-diagram^="strategy-roadmap"] > .story-desktop {display: none;}
.archify-panel--story[data-diagram="strategy-roadmap"] > .story-mobile {display: block; height: auto; aspect-ratio: 364 / 2600;} .archify-panel--story[data-diagram^="strategy-roadmap"] > .story-mobile {display: block; height: var(--roadmap-height); width: 100%; max-width: 432px; margin-inline: auto; aspect-ratio: auto;}
} }
.archify-expand {display: grid; place-items: center; width: 44px; height: 44px; border: 0; padding: 0; background: transparent; color: var(--ifm-link-color); cursor: pointer;} .archify-expand {display: grid; place-items: center; width: 44px; height: 44px; border: 0; padding: 0; background: transparent; color: var(--ifm-link-color); cursor: pointer;}
.archify-expand:hover {background: var(--ifm-hover-overlay);} .archify-expand:hover {background: var(--ifm-hover-overlay);}
@@ -142,4 +142,28 @@ a:focus-visible, button:focus-visible {outline: 3px solid #b88719; outline-offse
.archify-modal-toolbar {display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 20px; border-bottom: 1px solid var(--line);} .archify-modal-toolbar {display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 20px; border-bottom: 1px solid var(--line);}
.archify-modal-toolbar button {display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 44px; height: 44px; padding: 0; background: var(--gold); color: #14131a; font: inherit; font-size: 24px; font-weight: 700; border: 0; cursor: pointer;} .archify-modal-toolbar button {display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 44px; height: 44px; padding: 0; background: var(--gold); color: #14131a; font: inherit; font-size: 24px; font-weight: 700; border: 0; cursor: pointer;}
.archify-panel .archify-modal iframe {flex: 1; min-height: 0; height: 100%; width: 100%;} .archify-panel .archify-modal iframe {flex: 1; min-height: 0; height: 100%; width: 100%;}
/* Read narrow Roadmap views by scrolling, never by fitting their
entire height into a short phone screen. Other diagram viewers are unchanged. */
@media (max-width: 999px) {
[data-diagram^="strategy-roadmap"] .archify-modal {overflow-y: auto; overscroll-behavior: contain;}
[data-diagram^="strategy-roadmap"] .archify-modal-toolbar {position: sticky; top: 0; z-index: 1; flex-shrink: 0; background: var(--surface);}
[data-diagram^="strategy-roadmap"] .archify-modal iframe {flex: none; height: var(--roadmap-height); min-height: var(--roadmap-height);}
}
@media (prefers-reduced-motion: reduce) {.start-card {transition: none;} .start-card:hover {transform: none;}} @media (prefers-reduced-motion: reduce) {.start-card {transition: none;} .start-card:hover {transform: none;}}
/* Skills Adoption alone uses the full reading column, not a narrow preview. */
.theme-doc-markdown:has(.archify-panel--skills) {max-width: none;}
.archify-panel--story.archify-panel--skills {max-width: none;}
.archify-panel--skills {--skills-wide: 2000 / 1200; --skills-narrow: 880 / 2900;}
.archify-panel--skills[data-diagram="strategy-governance"] {--skills-wide: 1760 / 1550;}
.archify-panel--skills[data-diagram="strategy-adoption"] {--skills-wide: 2040 / 1300; --skills-narrow: 880 / 2200;}
.archify-panel--skills[data-diagram="strategy-measurement"] {--skills-wide: 2400 / 1280; --skills-narrow: 880 / 3900;}
.archify-panel--skills[data-diagram="strategy-maintenance"] {--skills-wide: 2630 / 1480; --skills-narrow: 880 / 3250;}
.archify-panel--story.archify-panel--skills > .story-desktop {aspect-ratio: var(--skills-wide); height: auto;}
@media (max-width: 1100px) {
.archify-panel--story.archify-panel--skills > .story-desktop {display: none;}
.archify-panel--story.archify-panel--skills > .story-mobile {display: block; aspect-ratio: var(--skills-narrow); height: auto; max-width: 440px; margin-inline: auto;}
.archify-panel--skills .archify-modal {overflow-y: auto; overscroll-behavior: contain;}
.archify-panel--skills .archify-modal-toolbar {position: sticky; top: 0; z-index: 1; flex-shrink: 0; background: var(--surface);}
.archify-panel--skills .archify-modal iframe {flex: none; height: auto; min-height: 0; aspect-ratio: var(--skills-narrow); max-width: 600px; margin-inline: auto;}
}