docs: explain skills adoption measurement and collection
Build and publish Eldorado documentation / build (pull_request) Successful in 39s
Build and publish Eldorado documentation / build (pull_request) Successful in 39s
This commit is contained in:
1 parent
8bfcb3f81d
commit
8dd54f0ab6
12 files changed
+577
-398
No files matched your search
@@ -23,7 +23,7 @@ const fs = require('node:fs');
|
||||
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(), ['Target', 'Roadmap', 'Skills governance', 'Skills quality', 'Skills adoption', 'Measurement', 'Skills maintenance']);
|
||||
assert.deepEqual(await menu.allTextContents(), ['Target', 'Roadmap', 'Skills governance', 'Skills quality', 'Skills adoption', 'Skills Measurement', 'Skills maintenance']);
|
||||
assert((await menu.nth(0).getAttribute('href')).endsWith('/strategy/target-operating-model/'));
|
||||
assert((await menu.nth(1).getAttribute('href')).endsWith('/strategy/roadmap/'));
|
||||
assert.equal(await menu.filter({hasText:/^Target$/}).count(), 1);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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/measurement-${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 context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
const response = await page.goto(`${base}/eldorado-v1/documentation/strategy/measurement/`, {waitUntil:'networkidle'});
|
||||
assert.equal(response.status(), 200);
|
||||
assert.equal(await page.locator('h1').innerText(), 'Skills Measurement');
|
||||
assert((await page.title()).startsWith('Skills Measurement |'));
|
||||
assert.equal(await page.locator('.strategy-content details').count(), 0);
|
||||
assert.equal(await page.locator('.strategy-content h2').count(), 4);
|
||||
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(), ['Target','Roadmap','Skills governance','Skills quality','Skills adoption','Skills Measurement','Skills maintenance']);
|
||||
if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
|
||||
const panel = page.locator('.archify-panel--story');
|
||||
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
|
||||
await iframe.scrollIntoViewIfNeeded();
|
||||
const frame = await (await iframe.elementHandle()).contentFrame();
|
||||
await frame.locator('svg[role="img"]').waitFor();
|
||||
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => {
|
||||
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))};
|
||||
});
|
||||
assert.equal(geometry.nodes.length, 6);
|
||||
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}`);
|
||||
await frame.locator('#btn-theme').click();
|
||||
await page.waitForFunction(() => document.documentElement.dataset.theme === 'dark');
|
||||
await frame.locator('#btn-theme').click();
|
||||
await page.waitForFunction(() => document.documentElement.dataset.theme === 'light');
|
||||
for (const preset of ['blueprint','classic']) {
|
||||
await frame.locator('#btn-preset').click();
|
||||
await frame.locator(`[data-preset-value="${preset}"]`).click();
|
||||
assert.equal(await frame.locator('html').getAttribute('data-preset'), preset);
|
||||
}
|
||||
await 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}.svg`; await download.saveAs(path);
|
||||
const svg = fs.readFileSync(path, 'utf8');
|
||||
assert(svg.includes('Establish baseline') && svg.includes('Ongoing monitoring'));
|
||||
const opener = panel.getByRole('button', {name:/^Expand /});
|
||||
await opener.click();
|
||||
const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true});
|
||||
await close.waitFor({state:'visible'}); assert.equal(await close.innerText(), '×');
|
||||
await close.click(); assert(await opener.evaluate(n => n === document.activeElement));
|
||||
await opener.click(); await close.waitFor({state:'visible'}); await close.focus(); await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => !document.querySelector('.archify-panel--story dialog').open);
|
||||
assert(await opener.evaluate(n => n === document.activeElement));
|
||||
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
|
||||
await page.evaluate(() => scrollTo({top:0, behavior:'instant'}));
|
||||
await page.screenshot({path:`${output}/${width}-top.png`});
|
||||
await page.locator('.strategy-content h2').nth(1).scrollIntoViewIfNeeded();
|
||||
await page.screenshot({path:`${output}/${width}-pilot.png`});
|
||||
await page.screenshot({path:`${output}/${width}-full.png`, fullPage:true});
|
||||
const links = await page.locator('.strategy-content a[href*="measurement-contracts"]').evaluateAll(nodes => nodes.map(n => n.href));
|
||||
for (const href of [...new Set(links)]) {
|
||||
const navigation = await page.goto(href, {waitUntil:'networkidle'});
|
||||
if (navigation) assert.equal(navigation.status(), 200);
|
||||
assert.equal(page.url(), href);
|
||||
if (new URL(href).hash) await page.locator(new URL(href).hash).waitFor({state:'visible'});
|
||||
}
|
||||
assert.equal(await page.locator('.strategy-content h3').count(), 19);
|
||||
assert((await page.locator('.strategy-content').innerText()).includes('9 prototype calculations and 7 unimplemented metrics'));
|
||||
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
|
||||
assert.deepEqual(errors, []);
|
||||
results.push({width, geometry, links:links.length, title:true, sidebar:true, controls:true, export:true, reference:true});
|
||||
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
|
||||
await context.close();
|
||||
}
|
||||
console.log(`PASS ${results.length} Skills Measurement viewports: title/sidebar, open plan, diagram geometry/controls/export, all reference links and no page errors`);
|
||||
} finally {await browser.close();}
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
@@ -22,7 +22,7 @@ const fs=require('node:fs');
|
||||
assert(geometry.nodes.length>=5 && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id);
|
||||
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<=200,id);assert(!data.overflow,id);assert.equal(data.openDetails,0,id);
|
||||
assert(data.words<=(id==='measurement'?1100:200),id);assert(!data.overflow,id);assert.equal(data.openDetails,0,id);
|
||||
await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));await page.screenshot({path:`.cache/story-${label}/${width}-${id}.png`,fullPage:true});
|
||||
const opener=panel.getByRole('button',{name:/^Expand /});await opener.click();
|
||||
const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
|
||||
@@ -30,15 +30,17 @@ const fs=require('node:fs');
|
||||
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 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));
|
||||
if(id !== 'measurement') {
|
||||
const summary=page.locator('.strategy-content > details > summary').first();
|
||||
await summary.focus();await page.keyboard.press('Enter');
|
||||
await page.waitForFunction(()=>document.querySelector('.strategy-content > details').open);
|
||||
await page.keyboard.press('Enter');
|
||||
results.push({id,width,...data,geometry,modalClick:true,modalEscape:true,focusRestored:true,details:true});fs.writeFileSync(`.cache/story-${label}/results.json`,JSON.stringify(results,null,2));
|
||||
}
|
||||
results.push({id,width,...data,geometry,modalClick:true,modalEscape:true,focusRestored:true,details:id!=='measurement'});fs.writeFileSync(`.cache/story-${label}/results.json`,JSON.stringify(results,null,2));
|
||||
}
|
||||
await page.goto(`${base}/eldorado-v1/documentation/strategy/measurement/#validated-task-success`,{waitUntil:'networkidle'});
|
||||
await page.goto(`${base}/eldorado-v1/documentation/strategy/reference/measurement-contracts/#validated-task-success`,{waitUntil:'networkidle'});
|
||||
const heading=page.locator('#validated-task-success');await heading.waitFor({state:'visible'});
|
||||
assert(await heading.evaluate(n=>n.closest('details').open),'hash should open metric reference');
|
||||
assert.equal(await page.locator('.strategy-content details').count(),0,'reference is directly readable');
|
||||
await context.close();
|
||||
}
|
||||
console.log(`PASS ${results.length} desktop/mobile cases, clipping, default word count, details, modal click/Escape/focus and KPI hash navigation`);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {calculate} from './ai-kpi.mjs';
|
||||
const read = path => readFileSync(path, 'utf8');
|
||||
|
||||
test('Skills Measurement is an open adoption collection plan, not a hidden catalogue', () => {
|
||||
const page = read('docs/strategy/measurement.mdx');
|
||||
assert.match(page, /title: Skills Measurement\nsidebar_label: Skills 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);
|
||||
assert(!page.includes('<details>'));
|
||||
assert(page.includes('./reference/measurement-contracts.mdx'));
|
||||
assert(!page.includes('Is AI-assisted work worth continuing?'));
|
||||
for (const word of ['Recommended', 'not installed', 'unknown', 'review', 'CI', 'revision', 'Monthly', 'Weekly']) assert(page.includes(word), word);
|
||||
});
|
||||
|
||||
test('all sixteen metric definitions remain in the reference with explicit implementation limits', () => {
|
||||
const page = read('docs/strategy/reference/measurement-contracts.mdx');
|
||||
const model = JSON.parse(read('research/ai-adoption/strategy-model.json'));
|
||||
for (const k of model.kpis) {
|
||||
const section = page.split(`### ${k.name}\n`)[1]?.split('\n### ')[0];
|
||||
assert(section, k.name);
|
||||
for (const field of ['Numerator','Denominator','Expression','Aggregation','Unit','Eligible population','Exclusions','Events and source','Owner / cadence','Missing evidence','Gaming and limits']) assert(section.includes(`| ${field} |`));
|
||||
}
|
||||
for (const text of ['9 prototype calculations', '7 unimplemented metrics', 'no network collectors', '30 days', '12 months', 'below five people', 'Missing telemetry is unknown']) assert(page.includes(text), text);
|
||||
});
|
||||
|
||||
test('published pilot arithmetic matches the executable fixture, including partial cost', () => {
|
||||
const page = read('docs/strategy/measurement.mdx');
|
||||
const fixture = JSON.parse(read('scripts/fixtures/ai-kpi-synthetic.json'));
|
||||
const report = calculate(fixture);
|
||||
const success = report.kpis['validated-task-success'];
|
||||
assert(page.includes(`${success.numerator} / ${success.denominator} × 100 = ${success.value_or_null}%`));
|
||||
assert(page.includes(`${success.known_outcome_coverage}%`));
|
||||
const cost = report.kpis['cost-per-accepted-outcome'];
|
||||
assert.equal(cost.partial_estimate, true);
|
||||
assert(page.includes(`EUR ${cost.numerator} / ${cost.denominator} = EUR ${cost.value_or_null}`));
|
||||
assert(page.includes('partial estimate'));
|
||||
const experience = report.kpis['human-experience'];
|
||||
assert(page.includes(`${experience.numerator} / ${experience.denominator} = ${experience.value_or_null}`));
|
||||
assert(page.includes(`${experience.denominator} / ${fixture.survey.invited} × 100 = ${experience.coverage.response_percent}%`));
|
||||
});
|
||||
@@ -10,15 +10,15 @@ test('every strategy page leads with a compact diagram story and closes its refe
|
||||
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(narrative.includes(' story />'), id);
|
||||
assert(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.equal((page.match(/<details>/g) || []).length, (page.match(/<\/details>/g) || []).length, id);
|
||||
assert.equal((narrative.match(/^## /gm) || []).length, 1, id);
|
||||
assert.equal((narrative.match(/^## /gm) || []).length, id === 'measurement' ? 4 : 1, id);
|
||||
assert((narrative.match(/^- /gm) || []).length <= 3, id);
|
||||
assert(narrative.includes('**Next:**'), id);
|
||||
assert(!narrative.includes('| ---'), id);
|
||||
const words = narrative.replace(/<[^>]*>/g, '').trim().split(/\s+/).length;
|
||||
assert(words <= 200, `${id}: ${words} default narrative words`);
|
||||
assert(words <= (id === 'measurement' ? 1100 : 200), `${id}: ${words} default narrative words`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,9 +36,10 @@ test('mobile story diagrams preserve the same nodes and directed relationships',
|
||||
}
|
||||
});
|
||||
|
||||
test('all sixteen exact KPI formulas remain reachable in the closed reference', () => {
|
||||
const page = read('docs/strategy/measurement.mdx');
|
||||
const contracts = page.split('<summary>All 16 KPI contracts')[1].split('</details>')[0];
|
||||
test('all sixteen exact KPI formulas remain reachable in the linked reference', () => {
|
||||
assert(read('docs/strategy/measurement.mdx').includes('./reference/measurement-contracts.mdx'));
|
||||
const page = read('docs/strategy/reference/measurement-contracts.mdx');
|
||||
const contracts = page.split('## Metric contracts')[1].split('## Run the synthetic')[0];
|
||||
assert.equal((contracts.match(/^### /gm) || []).length, 16);
|
||||
for (const metric of model.kpis) {
|
||||
const section = contracts.split(`### ${metric.name}\n`)[1]?.split('\n### ')[0];
|
||||
|
||||
@@ -5,7 +5,7 @@ const read = p => readFileSync(p, 'utf8');
|
||||
const model = JSON.parse(read('research/ai-adoption/strategy-model.json'));
|
||||
test('seven canonical strategy pages preserve target boundary and separate evidence', () => {
|
||||
assert.equal(model.pages.length, 7);
|
||||
assert.equal(readdirSync('docs/strategy').length, 7);
|
||||
assert.equal(readdirSync('docs/strategy').filter(p => p.endsWith('.mdx')).length, 7);
|
||||
for (const {id} of model.pages) {
|
||||
const page = read(`docs/strategy/${id}.mdx`);
|
||||
assert(page.includes(`id: ${id}`));
|
||||
@@ -19,7 +19,7 @@ test('seven canonical strategy pages preserve target boundary and separate evide
|
||||
}
|
||||
});
|
||||
test('every metric has an actionable reader-facing contract', () => {
|
||||
const page = read('docs/strategy/measurement.mdx');
|
||||
const page = read('docs/strategy/reference/measurement-contracts.mdx');
|
||||
assert.equal(model.kpis.length,16);
|
||||
for (const k of model.kpis) {
|
||||
const section=page.split(`### ${k.name}\n`)[1]?.split('\n### ')[0];
|
||||
|
||||
Reference in new issue
Block a user