From e087adab1e38ec602da3ce9513f5829a686a35e4 Mon Sep 17 00:00:00 2001 From: Jarvis Jr Hermes Date: Tue, 6 Oct 2026 20:00:08 +0000 Subject: [PATCH 1/2] fix: [verified] expose all documentation in reachable sidebars --- docs/core/maintain.md | 3 + package.json | 2 +- scripts/browser/sidebar-coverage.cjs | 60 +++++++++++++ scripts/browser/strategy-navigation.cjs | 18 ++-- scripts/sidebar-coverage.mjs | 111 ++++++++++++++++++++++++ scripts/sidebar-coverage.test.mjs | 51 +++++++++++ scripts/strategy-navigation.test.mjs | 1 + scripts/verify.mjs | 2 + sidebars/contribution.ts | 11 ++- sidebars/strategy.ts | 4 + 10 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 scripts/browser/sidebar-coverage.cjs create mode 100644 scripts/sidebar-coverage.mjs create mode 100644 scripts/sidebar-coverage.test.mjs diff --git a/docs/core/maintain.md b/docs/core/maintain.md index 042861c..bda4d55 100644 --- a/docs/core/maintain.md +++ b/docs/core/maintain.md @@ -29,6 +29,9 @@ Include `title` and `description` frontmatter. Place each page in its owning sec | 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. +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 diff --git a/package.json b/package.json index d8447de..c9ad7cb 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "verify:build": "node scripts/verify.mjs --build", "diagrams:build": "node scripts/build-diagrams.mjs", "test": "node --test scripts/*.test.mjs", - "test:browser": "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", + "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" }, "dependencies": { diff --git a/scripts/browser/sidebar-coverage.cjs b/scripts/browser/sidebar-coverage.cjs new file mode 100644 index 0000000..1a8825a --- /dev/null +++ b/scripts/browser/sidebar-coverage.cjs @@ -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;}); diff --git a/scripts/browser/strategy-navigation.cjs b/scripts/browser/strategy-navigation.cjs index e4d679e..91cd980 100644 --- a/scripts/browser/strategy-navigation.cjs +++ b/scripts/browser/strategy-navigation.cjs @@ -8,9 +8,9 @@ const fs = require('node:fs'); 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']; - const ids = ['target-operating-model','roadmap','stage-01-skills-adoption','skills-governance','skills-quality','skills-adoption','measurement','skills-maintenance','stage-02-autonomous-agent']; - const titles = ['Target','Roadmap',first,'Skills governance','Skills quality','Skills adoption','Skills Measurement','Skills maintenance',second]; + 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 { @@ -34,11 +34,17 @@ const fs = require('node:fs'); 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('button.menu__caret'); + 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').allTextContents(), topics); + 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'}); @@ -78,6 +84,6 @@ const fs = require('node:fs'); } finally {await context.close();} } assert.equal(results.length, 4); - console.log('PASS: 4 desktop/mobile viewports; all 9 routes; linked category, expand/collapse, unique nested labels, breadcrumbs, next/previous and no overflow/errors'); + 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;}); diff --git a/scripts/sidebar-coverage.mjs b/scripts/sidebar-coverage.mjs new file mode 100644 index 0000000..4723008 --- /dev/null +++ b/scripts/sidebar-coverage.mjs @@ -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(' ${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}; +} diff --git a/scripts/sidebar-coverage.test.mjs b/scripts/sidebar-coverage.test.mjs new file mode 100644 index 0000000..420d0d8 --- /dev/null +++ b/scripts/sidebar-coverage.test.mjs @@ -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); +}); diff --git a/scripts/strategy-navigation.test.mjs b/scripts/strategy-navigation.test.mjs index 9a68aea..b6fa67e 100644 --- a/scripts/strategy-navigation.test.mjs +++ b/scripts/strategy-navigation.test.mjs @@ -19,6 +19,7 @@ test('Strategy has Target, Roadmap, a linked Skills Adoption category and Autono {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); diff --git a/scripts/verify.mjs b/scripts/verify.mjs index ec20c5e..d3b3824 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import {readFileSync, readdirSync, existsSync} from 'node:fs'; import {join} from 'node:path'; +import {verifySidebarCoverage} from './sidebar-coverage.mjs'; const read = p => readFileSync(p, 'utf8'); const config = read('docusaurus.config.ts'); assert(config.includes("baseUrl: '/eldorado-v1/documentation/'")); @@ -16,6 +17,7 @@ const workflow = read('.gitea/workflows/pages.yml'); assert(workflow.includes('mountpoint -q')); assert(workflow.includes("gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'")); if (process.argv.includes('--build')) { + await verifySidebarCoverage(); 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 redirect = read('build/strategy/index.html'); diff --git a/sidebars/contribution.ts b/sidebars/contribution.ts index 8fc9619..ea4e8cc 100644 --- a/sidebars/contribution.ts +++ b/sidebars/contribution.ts @@ -1,2 +1,11 @@ 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; diff --git a/sidebars/strategy.ts b/sidebars/strategy.ts index 687ca25..bbcd96c 100644 --- a/sidebars/strategy.ts +++ b/sidebars/strategy.ts @@ -14,6 +14,10 @@ export default { {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', -- 2.54.0 From e0afa78eb24d33c84b36c25386809425b8664d6a Mon Sep 17 00:00:00 2001 From: Jarvis Jr Hermes Date: Tue, 6 Oct 2026 20:14:19 +0000 Subject: [PATCH 2/2] test: [verified] include Reference in measurement navigation --- scripts/browser/skills-measurement.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/browser/skills-measurement.cjs b/scripts/browser/skills-measurement.cjs index 62a4747..400e323 100644 --- a/scripts/browser/skills-measurement.cjs +++ b/scripts/browser/skills-measurement.cjs @@ -22,7 +22,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','Stage - 01 - Skills Adoption','Governance','Quality','Adoption','Measurement','Maintenance','Stage - 02 - Autonomous Agent']); + 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(); const panel = page.locator('.archify-panel--story'); const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile'); -- 2.54.0