docs: [verified] connect skills-first Roadmap to autonomous Target
Build and publish Eldorado documentation / build (pull_request) Successful in 49s

This commit is contained in:
jarvis-at-skic committed 2026-10-06 11:34:39 +00:00
1 parent 733d6f2ece
commit e10b03dff4
17 files changed
+621 -331

No files matched your search

+102 -95
View File
@@ -1,129 +1,136 @@
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const ids = ['assessment','readiness','pilot','expansion','sustain'];
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, vertical) => {
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,
numberColor:getComputedStyle(node.querySelector('[data-stage-number]')).fill,
titleColor:getComputedStyle(node.querySelector('[data-node-label]')).fill,
text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a}))
}))
}));
geometry.cards.sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x);
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), ['1','2','3','4','5']);
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, 'number, title and one outcome only');
assert.equal(card.numberColor,card.titleColor,'stage number must inherit readable theme color');
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `outside iframe: ${card.id}`);
for (const text of card.text) {
assert(text.size >= 13, `small text: ${card.id} ${text.size}`);
assert(!/OUTPUT|Exit:/.test(text.value));
assert(text.box.left >= card.box.left && text.box.right <= card.box.right + 1 && text.box.top >= card.box.top && text.box.bottom <= card.box.bottom + 1, `clipped: ${card.id}`);
}
for (let t=1; t<card.text.length; t++) assert(card.text[t-1].box.bottom < card.text[t].box.top, `overlapping text: ${card.id}`);
if (index) {
const previous = geometry.cards[index-1].box;
if (vertical) {assert.equal(previous.x,card.box.x); assert(previous.bottom < card.box.top);}
else {assert.equal(previous.y,card.box.y); assert(previous.right < card.box.left);}
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'});
for(const width of [1440,1024,390,320]) {
const context=await browser.newContext({viewport:{width,height:1000},colorScheme:'light'});
try {
const page = await context.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'})).status(),200);
const panel = page.locator('[data-diagram="strategy-roadmap"]');
const iframe = panel.locator(width < 1360 ? '.story-mobile' : '.story-desktop');
const frame = await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const opener = panel.getByRole('button', {name:/^Expand /});
for (const theme of ['light','dark']) {
if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click();
await page.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
const inline = await inspect(frame,width < 1360);
await panel.screenshot({path:`${output}/${width}-${theme}-inline.png`});
await opener.focus(); await page.keyboard.press('Enter');
const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true});
await close.waitFor({state:'visible'});
const fullscreen = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await fullscreen.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
const expanded = await inspect(fullscreen,width < 1000);
await page.screenshot({path:`${output}/${width}-${theme}-fullscreen.png`});
await page.keyboard.press('Escape');
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
assert(await opener.evaluate(n => n === document.activeElement));
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
results.push({width,theme,inline,expanded});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
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.deepEqual(errors,[]);
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));assert.deepEqual(errors,[]);
} finally {await context.close();}
}
assert.equal(results.length,8);
for (const viewport of [{width:390,height:667},{width:320,height:568}]) {
const context = await browser.newContext({viewport});
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'});
const panel = page.locator('[data-diagram="strategy-roadmap"]');
const opener = panel.getByRole('button', {name:/^Expand /});
await opener.focus(); await page.keyboard.press('Enter');
const modal = panel.locator('dialog');
const close = modal.getByRole('button', {name:'Close full-screen diagram', exact:true});
await close.waitFor({state:'visible'});
const iframe = modal.locator('iframe');
const frame = await (await iframe.elementHandle()).contentFrame();
const geometry = await inspect(frame,true);
assert(await modal.evaluate(n => n.scrollHeight > n.clientHeight), 'short fullscreen must scroll');
for (const card of geometry.cards) {
await modal.evaluate((n, y) => {n.scrollTop = y;}, card.box.top);
const frameBox = await iframe.boundingBox();
const toolbarBox = await modal.locator('.archify-modal-toolbar').boundingBox();
const closeBox = await close.boundingBox();
assert(closeBox.y >= 0 && closeBox.y + closeBox.height <= viewport.height, 'close remains visible');
assert(frameBox.y + card.box.top >= toolbarBox.y + toolbarBox.height - 1 && frameBox.y + card.box.bottom <= viewport.height + 1, `stage unreachable: ${card.id}`);
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));
}
// Controls stay reachable at the beginning of the scrollable viewer.
await modal.evaluate(n => {n.scrollTop = 0;});
for (const selector of ['#btn-theme','#btn-preset','#btn-export']) {
const control = frame.locator(selector);
const box = await control.boundingBox();
assert(box.x >= 0 && box.x + box.width <= viewport.width && box.y >= 0 && box.y + box.height <= viewport.height, `control outside viewport: ${selector}`);
await control.click();
if (selector !== '#btn-theme') await control.click();
}
await close.focus(); await page.keyboard.press('Escape');
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
await iframe.waitFor({state:'detached'});
assert(await opener.evaluate(n => n === document.activeElement), 'Escape restores opener focus');
await page.keyboard.press('Enter');
await close.waitFor({state:'visible'});
await close.focus(); await page.keyboard.press('Enter');
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
assert(await opener.evaluate(n => n === document.activeElement), 'close button restores opener focus');
results.push({viewport,scrollable:geometry});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
} finally {await context.close();}
}
assert.equal(results.length,10);
console.log('PASS 16 timeline contexts: 4 widths × 2 themes × inline/fullscreen; ordered readable text, no clipping, keyboard exit.');
console.log('PASS short fullscreen 390×667 and 320×568: readable stages reachable by scroll, sticky close, accessible controls, keyboard close and focus restoration.');
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;});
})().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 results = [];
try {
// Intermediate widths must use the readable vertical Roadmap, not shrink five cards.
for (const width of [768, 1024, 1280]) {
// Intermediate widths must use the readable vertical Roadmap, not shrink the loop.
for (const width of [768, 1024]) {
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
try {
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}))
})).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()) {
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}`);
@@ -61,16 +61,16 @@ const fs = require('node:fs');
assert.equal(response.status(), 200);
assert.equal(await page.locator('h1').innerText(), 'Roadmap');
const headings = page.locator('.strategy-content > h2');
assert.deepEqual((await headings.allTextContents()).map(s => s.replace(/\u200b/g, '')), ['1. Understand code quality and delivery','2. Fix critical readiness gaps','3. Run a bounded pilot','4. Expand only on evidence','5. Sustain and improve']);
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();
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'));
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true});
await menu.first().waitFor({state:'visible'});
assert.deepEqual((await menu.allTextContents()).slice(0,2), ['Target','Roadmap']);
if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
const panel = page.locator('.archify-panel--story');
const panel = page.locator('.archify-panel--story').first();
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
@@ -92,7 +92,7 @@ const fs = require('node:fs');
width:innerWidth, height:innerHeight,
minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))
}));
assert.deepEqual(geometry.nodes.map(n => n.id), ['assessment','readiness','pilot','expansion','sustain']);
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);
assert(geometry.minLabel >= 10, `readable node labels: ${geometry.minLabel}`);
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()]);
assert.equal(await download.failure(), null);
await download.saveAs(`${output}/${width}.svg`);
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Understand today'));
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Adopt skills'));
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`});
}
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));
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();}
})().catch(error => {console.error(error); process.exitCode = 1;});
+2 -2
View File
@@ -12,14 +12,14 @@ const fs=require('node:fs');
const page=await context.newPage();
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 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 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('[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))};
});
assert(geometry.nodes.length>=5 && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id);
assert(geometry.nodes.length>=(id==='roadmap'?2:5) && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id);
for(const n of geometry.nodes) assert(n.x>=0 && n.y>=0 && n.right<=geometry.width+1 && n.bottom<=geometry.height+1,`${id} ${width} clipped ${JSON.stringify(n)} / ${geometry.height}`);
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);