543 lines
29 KiB
JavaScript
543 lines
29 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import {existsSync, readFileSync, readdirSync} from 'node:fs';
|
|
import {join} from 'node:path';
|
|
import {XMLValidator} from 'fast-xml-parser';
|
|
|
|
const root = process.cwd();
|
|
const read = (path) => readFileSync(join(root, path), 'utf8');
|
|
const config = read('docusaurus.config.ts');
|
|
const packageJson = JSON.parse(read('package.json'));
|
|
const globalCss = read('src/css/custom.css');
|
|
const homepageCss = read('src/pages/index.module.css');
|
|
|
|
assert.match(config, /defaultMode:\s*'dark'/, 'dark must remain the default theme');
|
|
assert.match(config, /disableSwitch:\s*false/, 'theme switch must be enabled');
|
|
assert.match(globalCss, /\[data-theme=['"]light['"]\]/, 'global CSS needs an explicit light palette');
|
|
assert.match(homepageCss, /:global\(\[data-theme=['"]light['"]\]\)/, 'homepage needs a light palette');
|
|
assert.doesNotMatch(globalCss, /html,\s*body\s*\{\s*background:\s*#08101b/, 'document background must not be hard-coded dark');
|
|
|
|
for (const section of ['projects', 'teams', 'members', 'board', 'governance']) {
|
|
assert.match(config, new RegExp(`id:\\s*'${section}'`), `${section} docs plugin is missing`);
|
|
assert.match(config, new RegExp(`sidebarId:\\s*'${section}Sidebar'`), `${section} navbar item does not target its dedicated sidebar`);
|
|
assert.ok(existsSync(join(root, `sidebars/${section}.ts`)), `${section} sidebar file is missing`);
|
|
}
|
|
assert.match(config, /id:\s*'ways-of-working'/, 'Ways of Working docs plugin is missing');
|
|
assert.match(config, /sidebarId:\s*'waysOfWorkingSidebar'/, 'Ways of Working navbar item does not target its dedicated sidebar');
|
|
assert.ok(existsSync(join(root, 'sidebars/ways-of-working.ts')), 'Ways of Working sidebar file is missing');
|
|
assert.ok(existsSync(join(root, 'docs/ways-of-working/index.md')), 'Ways of Working entry page is missing');
|
|
assert.ok(existsSync(join(root, 'docs/ways-of-working/onboarding.md')), 'Onboarding page is missing');
|
|
assert.match(config, /id:\s*'architecture-high-level'/, 'Architecture High Level docs plugin is missing');
|
|
assert.match(config, /sidebarId:\s*'architectureHighLevelSidebar'/, 'Architecture High Level navbar item does not target its dedicated sidebar');
|
|
assert.match(config, /label:\s*'Architecture'/, 'Architecture navbar label is missing');
|
|
assert.doesNotMatch(config, /label:\s*'Architecture High Level'/, 'legacy Architecture High Level menu label must not remain');
|
|
assert.ok(existsSync(join(root, 'sidebars/architecture-high-level.ts')), 'Architecture High Level sidebar file is missing');
|
|
|
|
const expectedLeftNavLabels = ['Board', 'Projects', 'Teams', 'Members', 'Governance', 'Ways of Working', 'Architecture'];
|
|
const leftNavLabels = [...config.matchAll(/\{type:\s*'docSidebar',[^\n]+label:\s*'([^']+)',\s*position:\s*'left'\}/g)]
|
|
.map((match) => match[1]);
|
|
assert.deepEqual(leftNavLabels, expectedLeftNavLabels, 'Top-level navigation must follow the approved Board-first order');
|
|
|
|
const waysOfWorkingSidebar = read('sidebars/ways-of-working.ts');
|
|
assert.match(waysOfWorkingSidebar, /link:\s*\{type:\s*'doc',\s*id:\s*'index'\}/, 'Ways of Working must link to its entry page');
|
|
assert.match(waysOfWorkingSidebar, /items:\s*\[\{type:\s*'doc',\s*id:\s*'onboarding',\s*label:\s*'Onboarding'\}\]/, 'Onboarding must be the first child page');
|
|
const onboarding = read('docs/ways-of-working/onboarding.md');
|
|
for (const term of ['Your first 20 minutes', 'Use the seven channels correctly', 'Understand authority', 'Know what “done” means', 'Security and credentials', 'Onboarding complete']) {
|
|
assert.ok(onboarding.includes(term), `Onboarding is missing: ${term}`);
|
|
}
|
|
|
|
assert.match(config, /baseUrl:\s*'\/corp-v1\/portal\/'/, 'Pages base URL must follow the renamed repository');
|
|
assert.match(config, /projectName:\s*'portal'/, 'Docusaurus project name must be portal');
|
|
assert.doesNotMatch(config, /sterring-commitee/, 'legacy repository name must not remain in Docusaurus config');
|
|
assert.doesNotMatch(config, /Pilot Model|docs\/pilot|routeBasePath:\s*'pilot'/, 'legacy Pilot Model navigation must not remain');
|
|
|
|
const board = read('docs/board/index.md');
|
|
for (const memberId of [
|
|
'1518725627845283888',
|
|
'476287310627864587',
|
|
'773109855497093191',
|
|
'364383883778850827',
|
|
'1483820991720329227',
|
|
'1540035254746882048',
|
|
'501954115690168321',
|
|
'1526860732917088266',
|
|
]) {
|
|
assert.ok(board.includes(memberId), `board page is missing Discord member ${memberId}`);
|
|
}
|
|
assert.match(board, /exactly eight members/i);
|
|
assert.match(board, /Only the seven human Board members may approve a project kickoff or project closure/i);
|
|
assert.match(board, /Only the seven human Board members may approve changes to `corp-v1-main`/i);
|
|
assert.match(board, /Jarvis .*Board member and executing code agent/i);
|
|
|
|
for (const path of [
|
|
'docs/projects/dpvp.md',
|
|
'docs/teams/dpvp.md',
|
|
'docs/governance/decisions/jarvis-board-all-project-membership.md',
|
|
]) {
|
|
assert.ok(existsSync(join(root, path)), `${path} is missing`);
|
|
}
|
|
const jarvisMember = read('docs/members/jarvis-at-skic.md');
|
|
for (const marker of ['Board member and executing code agent', 'all 35 lifecycle channels', 'all ten project/skills teams']) {
|
|
assert.ok(jarvisMember.includes(marker), `Jarvis member record is missing: ${marker}`);
|
|
}
|
|
const dpvpProject = read('docs/projects/dpvp.md');
|
|
for (const marker of ['corp-v1-dpvp', 'dpvp-maintainers', 'dpvp-skill-maintainers', '1544621890994184224']) {
|
|
assert.ok(dpvpProject.includes(marker), `DPVP project record is missing: ${marker}`);
|
|
}
|
|
|
|
const aerosimProject = read('docs/projects/aerosim.md');
|
|
const APP_SHA = 'efbf3967548f1cbf5e06f97fa9d659535e35a7b4';
|
|
const DOC_SHA = 'f686610a8b1b21825c66dc424f484fb2672933d7';
|
|
|
|
const markdownSection = (document, heading) => {
|
|
const marker = `## ${heading}`;
|
|
const start = document.indexOf(marker);
|
|
assert.notEqual(start, -1, `AeroSim section is missing: ${heading}`);
|
|
const end = document.indexOf('\n## ', start + marker.length);
|
|
return document.slice(start, end === -1 ? document.length : end);
|
|
};
|
|
|
|
const uniqueLine = (text, predicate, description) => {
|
|
const matches = text.split('\n').filter(predicate);
|
|
assert.equal(matches.length, 1, `AeroSim must contain exactly one ${description}`);
|
|
return matches[0];
|
|
};
|
|
|
|
function verifyAeroSimProject(document) {
|
|
const requiredSectionHeadings = [
|
|
'Resources',
|
|
'Delivery progress checkpoint — 2026-09-03',
|
|
'Discord',
|
|
];
|
|
const documentLines = document.split('\n');
|
|
const requiredSectionIndices = requiredSectionHeadings.map((heading) => {
|
|
const indices = documentLines.flatMap((line, index) => line === `## ${heading}` ? [index] : []);
|
|
assert.equal(indices.length, 1, `AeroSim required section heading must appear exactly once: ${heading}`);
|
|
return indices[0];
|
|
});
|
|
assert.ok(
|
|
requiredSectionIndices.every((index, position) => position === 0 || requiredSectionIndices[position - 1] < index),
|
|
'AeroSim required sections must follow Resources, checkpoint, Discord order',
|
|
);
|
|
|
|
for (const marker of [
|
|
'Delivery progress checkpoint — 2026-09-03',
|
|
'8 / 114',
|
|
'7.0%',
|
|
'0 / 114',
|
|
'TASK-0012',
|
|
'TASK-0015',
|
|
'TASK-0073',
|
|
'TASK-0114',
|
|
'TASK-0013',
|
|
'TASK-0016',
|
|
'TASK-0074',
|
|
'TASK-0115',
|
|
'PR #143',
|
|
'1760',
|
|
'1761',
|
|
'1762',
|
|
'1763',
|
|
'efbf3967548f1cbf5e06f97fa9d659535e35a7b4',
|
|
'f686610a8b1b21825c66dc424f484fb2672933d7',
|
|
'106 ReadyForDelivery',
|
|
]) {
|
|
assert.ok(document.includes(marker), `AeroSim progress checkpoint is missing: ${marker}`);
|
|
}
|
|
|
|
const resources = markdownSection(document, 'Resources');
|
|
const applicationResource = uniqueLine(resources, (line) => line.startsWith('- [Application repository]'), 'application repository resource');
|
|
const documentationResource = uniqueLine(resources, (line) => line.startsWith('- [Documentation repository]'), 'documentation repository resource');
|
|
assert.match(applicationResource, new RegExp(`application.*\\b${APP_SHA}\\b`, 'i'), 'application repository must reference the application SHA');
|
|
assert.match(documentationResource, new RegExp(`documentation.*\\b${DOC_SHA}\\b`, 'i'), 'documentation repository must reference the documentation SHA');
|
|
|
|
const checkpoint = markdownSection(document, 'Delivery progress checkpoint — 2026-09-03');
|
|
const checkpointLines = checkpoint.split('\n');
|
|
const checkpointTableHeader = '| Measure | Progress | Meaning |';
|
|
const checkpointTableHeaderIndices = checkpointLines.flatMap(
|
|
(line, index) => line === checkpointTableHeader ? [index] : [],
|
|
);
|
|
assert.equal(checkpointTableHeaderIndices.length, 1, 'AeroSim checkpoint table header must appear exactly once');
|
|
const checkpointTableHeaderIndex = checkpointTableHeaderIndices[0];
|
|
assert.equal(
|
|
checkpointLines[checkpointTableHeaderIndex + 1],
|
|
'|---|---:|---|',
|
|
'AeroSim checkpoint table separator must exactly preserve its three columns and progress alignment',
|
|
);
|
|
assert.deepEqual(
|
|
checkpointLines.slice(checkpointTableHeaderIndex + 2, checkpointTableHeaderIndex + 5).map(
|
|
(line) => line.match(/^\| (Canonical `ToBeReleased`|Repository-verified implementation|Released) \|/)?.[1],
|
|
),
|
|
['Canonical `ToBeReleased`', 'Repository-verified implementation', 'Released'],
|
|
'AeroSim checkpoint rows must follow canonical, implementation, released order',
|
|
);
|
|
const canonicalRow = uniqueLine(checkpoint, (line) => line.startsWith('| Canonical `ToBeReleased` |'), 'canonical ToBeReleased row');
|
|
const implementationRow = uniqueLine(checkpoint, (line) => line.startsWith('| Repository-verified implementation |'), 'repository-verified implementation row');
|
|
const releasedRow = uniqueLine(checkpoint, (line) => line.startsWith('| Released |'), 'released row');
|
|
const waveStatus = uniqueLine(checkpoint, (line) => line.startsWith('Wave 2 is complete:'), 'Wave 2/Wave 3 status paragraph');
|
|
|
|
assert.match(releasedRow, /\|\s*0\s*\/\s*114\s*\(0%\)/i, 'AeroSim must distinguish released progress');
|
|
assert.match(canonicalRow, /\|\s*8\s*\/\s*114\s*\(7\.0%\)/i, 'AeroSim must expose canonical delivery completion');
|
|
assert.match(implementationRow, /\|\s*8\s*\/\s*114\s*\(7\.0%\)/i, 'AeroSim must expose repository-verified implementation separately');
|
|
assert.match(
|
|
canonicalRow,
|
|
/Wave 2 Tasks `TASK-0012`, `TASK-0015`, `TASK-0073`, and `TASK-0114`/,
|
|
'AeroSim Wave 2 task group must remain on the canonical completion row',
|
|
);
|
|
assert.match(
|
|
implementationRow,
|
|
new RegExp('application `test` SHA `' + APP_SHA + '`; integrated publish task `1760` and validate task `1761` succeeded', 'i'),
|
|
'AeroSim implementation row must bind the application SHA, publish task 1760, and validate task 1761',
|
|
);
|
|
assert.match(waveStatus, new RegExp('final application head is `' + APP_SHA + '`'), 'Wave 2 status must bind the final application head to the application SHA');
|
|
assert.match(waveStatus, new RegExp('documentation `test` SHA `' + DOC_SHA + '`'), 'Wave 2 status must bind the documentation merge to the documentation SHA');
|
|
assert.match(
|
|
waveStatus,
|
|
/Documentation exact-head build task `1762` and integrated build task `1763` succeeded/,
|
|
'Wave 2 status must bind exact-head build task 1762 and integrated build task 1763',
|
|
);
|
|
assert.match(
|
|
waveStatus,
|
|
/Wave 3 is active: `TASK-0013`, `TASK-0016`, `TASK-0074`, and `TASK-0115` are claimed/,
|
|
'AeroSim Wave 3 task group must remain in the active Wave 3 clause',
|
|
);
|
|
assert.match(waveStatus, /Wave 3[^\n]*claimed[^\n]*not (?:yet )?implemented or merged/i, 'AeroSim must distinguish active Wave 3 claims from implementation');
|
|
assert.match(waveStatus, /dated portfolio checkpoint, not a second task database/i, 'AeroSim must remain a non-authoritative portfolio checkpoint');
|
|
|
|
for (const [staleMarker, stalePattern] of [
|
|
['4 / 114', /(?<!\d)4\s*\/\s*114(?!\d)/],
|
|
['3.5%', /(?<![\d.])3\.5%(?![\d.])/],
|
|
['5 / 114', /(?<!\d)5\s*\/\s*114(?!\d)/],
|
|
['4.4%', /(?<![\d.])4\.4%(?![\d.])/],
|
|
['58d33ffbd3ca21ffdc5882a75de8fe71c25188b6', /58d33ffbd3ca21ffdc5882a75de8fe71c25188b6/],
|
|
['29d5e7810e63f3407f62730c1b2c71bc0644310b', /29d5e7810e63f3407f62730c1b2c71bc0644310b/],
|
|
]) {
|
|
assert.doesNotMatch(document, stalePattern, `stale AeroSim checkpoint marker must not remain: ${staleMarker}`);
|
|
}
|
|
assert.doesNotMatch(waveStatus, /Wave 2 is active/i, 'Wave 2 must not remain active');
|
|
}
|
|
|
|
verifyAeroSimProject(aerosimProject);
|
|
|
|
if (process.argv.includes('--self-test')) {
|
|
const replaceExactly = (document, oldText, newText) => {
|
|
assert.equal(document.split(oldText).length - 1, 1, `mutation fixture must find exactly one occurrence: ${oldText}`);
|
|
return document.replace(oldText, newText);
|
|
};
|
|
const swap = (document, left, right) => {
|
|
const placeholder = '__VERIFY_PORTAL_MUTATION_PLACEHOLDER__';
|
|
return replaceExactly(replaceExactly(replaceExactly(document, left, placeholder), right, left), placeholder, right);
|
|
};
|
|
const swapAll = (document, left, right) => {
|
|
assert.ok(document.includes(left), `mutation fixture must find: ${left}`);
|
|
assert.ok(document.includes(right), `mutation fixture must find: ${right}`);
|
|
const placeholder = '__VERIFY_PORTAL_MUTATION_PLACEHOLDER__';
|
|
return document.replaceAll(left, placeholder).replaceAll(right, left).replaceAll(placeholder, right);
|
|
};
|
|
const checkpointRow = (document, prefix) => uniqueLine(
|
|
markdownSection(document, 'Delivery progress checkpoint — 2026-09-03'),
|
|
(line) => line.startsWith(prefix),
|
|
`${prefix} mutation source row`,
|
|
);
|
|
const sectionBlock = (document, heading) => {
|
|
const section = markdownSection(document, heading);
|
|
return section.endsWith('\n') ? section : `${section}\n`;
|
|
};
|
|
const futureCountFixtures = [14, 15, 24, 25, 114].flatMap((count) => [
|
|
{
|
|
name: `accepts legitimate future count ${count} / 114`,
|
|
document: replaceExactly(
|
|
aerosimProject,
|
|
'Release `1.0.0` contains 114 Tasks.',
|
|
`Release \`1.0.0\` contains 114 Tasks. Forecast example: ${count} / 114.`,
|
|
),
|
|
accepted: true,
|
|
},
|
|
{
|
|
name: `accepts legitimate compact future count ${count}/114`,
|
|
document: replaceExactly(
|
|
aerosimProject,
|
|
'Release `1.0.0` contains 114 Tasks.',
|
|
`Release \`1.0.0\` contains 114 Tasks. Forecast example: ${count}/114.`,
|
|
),
|
|
accepted: true,
|
|
},
|
|
]);
|
|
const staleCountFixtures = [4, 5].flatMap((count) => [
|
|
{
|
|
name: `rejects stale count ${count} / 114`,
|
|
document: replaceExactly(
|
|
aerosimProject,
|
|
'Release `1.0.0` contains 114 Tasks.',
|
|
`Release \`1.0.0\` contains 114 Tasks. Superseded snapshot: ${count} / 114.`,
|
|
),
|
|
accepted: false,
|
|
message: new RegExp(`stale AeroSim checkpoint marker must not remain: ${count} / 114`),
|
|
},
|
|
{
|
|
name: `rejects compact stale count ${count}/114`,
|
|
document: replaceExactly(
|
|
aerosimProject,
|
|
'Release `1.0.0` contains 114 Tasks.',
|
|
`Release \`1.0.0\` contains 114 Tasks. Superseded snapshot: ${count}/114.`,
|
|
),
|
|
accepted: false,
|
|
message: new RegExp(`stale AeroSim checkpoint marker must not remain: ${count} / 114`),
|
|
},
|
|
]);
|
|
const canonicalRowFixture = checkpointRow(aerosimProject, '| Canonical `ToBeReleased` |');
|
|
const implementationRowFixture = checkpointRow(aerosimProject, '| Repository-verified implementation |');
|
|
const releasedRowFixture = checkpointRow(aerosimProject, '| Released |');
|
|
const resourcesSectionFixture = sectionBlock(aerosimProject, 'Resources');
|
|
const checkpointSectionFixture = sectionBlock(aerosimProject, 'Delivery progress checkpoint — 2026-09-03');
|
|
const discordSectionFixture = sectionBlock(aerosimProject, 'Discord');
|
|
const fixtures = [
|
|
...futureCountFixtures,
|
|
...staleCountFixtures,
|
|
{
|
|
name: 'rejects removed checkpoint table header',
|
|
document: replaceExactly(aerosimProject, '| Measure | Progress | Meaning |\n', ''),
|
|
accepted: false,
|
|
message: /checkpoint table header/,
|
|
},
|
|
{
|
|
name: 'rejects malformed checkpoint table header',
|
|
document: replaceExactly(aerosimProject, '| Measure | Progress | Meaning |', '| Measure | Meaning | Progress |'),
|
|
accepted: false,
|
|
message: /checkpoint table header/,
|
|
},
|
|
{
|
|
name: 'rejects removed checkpoint table separator',
|
|
document: replaceExactly(aerosimProject, '|---|---:|---|\n', ''),
|
|
accepted: false,
|
|
message: /checkpoint table separator/,
|
|
},
|
|
{
|
|
name: 'rejects malformed checkpoint table separator',
|
|
document: replaceExactly(aerosimProject, '|---|---:|---|', '|---|---|---|'),
|
|
accepted: false,
|
|
message: /checkpoint table separator/,
|
|
},
|
|
{
|
|
name: 'rejects swapped canonical and implementation rows',
|
|
document: swap(aerosimProject, canonicalRowFixture, implementationRowFixture),
|
|
accepted: false,
|
|
message: /checkpoint rows must follow canonical, implementation, released order/,
|
|
},
|
|
{
|
|
name: 'rejects swapped implementation and released rows',
|
|
document: swap(aerosimProject, implementationRowFixture, releasedRowFixture),
|
|
accepted: false,
|
|
message: /checkpoint rows must follow canonical, implementation, released order/,
|
|
},
|
|
{
|
|
name: 'rejects removed Resources heading',
|
|
document: replaceExactly(aerosimProject, '## Resources\n', ''),
|
|
accepted: false,
|
|
message: /required section heading.*Resources/i,
|
|
},
|
|
{
|
|
name: 'rejects renamed Resources heading',
|
|
document: replaceExactly(aerosimProject, '## Resources', '## Project resources'),
|
|
accepted: false,
|
|
message: /required section heading.*Resources/i,
|
|
},
|
|
{
|
|
name: 'rejects removed checkpoint heading',
|
|
document: replaceExactly(aerosimProject, '## Delivery progress checkpoint — 2026-09-03\n', ''),
|
|
accepted: false,
|
|
message: /required section heading.*Delivery progress checkpoint/i,
|
|
},
|
|
{
|
|
name: 'rejects renamed checkpoint heading',
|
|
document: replaceExactly(aerosimProject, '## Delivery progress checkpoint — 2026-09-03', '## Delivery status — 2026-09-03'),
|
|
accepted: false,
|
|
message: /required section heading.*Delivery progress checkpoint/i,
|
|
},
|
|
{
|
|
name: 'rejects removed Discord heading',
|
|
document: replaceExactly(aerosimProject, '## Discord\n', ''),
|
|
accepted: false,
|
|
message: /required section heading.*Discord/i,
|
|
},
|
|
{
|
|
name: 'rejects renamed Discord heading',
|
|
document: replaceExactly(aerosimProject, '## Discord', '## Project channels'),
|
|
accepted: false,
|
|
message: /required section heading.*Discord/i,
|
|
},
|
|
{
|
|
name: 'rejects Resources and checkpoint sections in reverse order',
|
|
document: swap(aerosimProject, resourcesSectionFixture, checkpointSectionFixture),
|
|
accepted: false,
|
|
message: /required sections must follow Resources, checkpoint, Discord order/,
|
|
},
|
|
{
|
|
name: 'rejects checkpoint and Discord sections in reverse order',
|
|
document: swap(aerosimProject, checkpointSectionFixture, discordSectionFixture),
|
|
accepted: false,
|
|
message: /required sections must follow Resources, checkpoint, Discord order/,
|
|
},
|
|
{
|
|
name: 'rejects swapped application and documentation SHA associations',
|
|
document: swapAll(aerosimProject, APP_SHA, DOC_SHA),
|
|
accepted: false,
|
|
message: /application repository.*SHA|documentation repository.*SHA/i,
|
|
},
|
|
{
|
|
name: 'rejects swapped publish and validate CI task associations',
|
|
document: replaceExactly(
|
|
aerosimProject,
|
|
'publish task `1760` and validate task `1761`',
|
|
'publish task `1761` and validate task `1760`',
|
|
),
|
|
accepted: false,
|
|
message: /publish task.*1760|validate task.*1761/i,
|
|
},
|
|
{
|
|
name: 'rejects swapped documentation CI task associations',
|
|
document: replaceExactly(
|
|
aerosimProject,
|
|
'exact-head build task `1762` and integrated build task `1763`',
|
|
'exact-head build task `1763` and integrated build task `1762`',
|
|
),
|
|
accepted: false,
|
|
message: /exact-head build task.*1762|integrated build task.*1763/i,
|
|
},
|
|
{
|
|
name: 'rejects Wave 2 and Wave 3 task-group permutations',
|
|
document: swap(
|
|
aerosimProject,
|
|
'`TASK-0012`, `TASK-0015`, `TASK-0073`, and `TASK-0114`',
|
|
'`TASK-0013`, `TASK-0016`, `TASK-0074`, and `TASK-0115`',
|
|
),
|
|
accepted: false,
|
|
message: /Wave 2 task group|Wave 3 task group/i,
|
|
},
|
|
];
|
|
const failures = [];
|
|
for (const fixture of fixtures) {
|
|
try {
|
|
verifyAeroSimProject(fixture.document);
|
|
if (!fixture.accepted) throw new assert.AssertionError({message: 'mutated document was incorrectly accepted'});
|
|
console.log(`ok - ${fixture.name}`);
|
|
} catch (error) {
|
|
if (!fixture.accepted && fixture.message?.test(error.message)) {
|
|
console.log(`ok - ${fixture.name}`);
|
|
} else {
|
|
failures.push(`${fixture.name}: ${error.message}`);
|
|
console.error(`not ok - ${fixture.name}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
assert.equal(failures.length, 0, `AeroSim verifier self-tests failed:\n${failures.join('\n')}`);
|
|
console.log('AeroSim verifier self-tests passed.');
|
|
}
|
|
|
|
const lifecycle = read('docs/governance/project-lifecycle.md');
|
|
assert.match(lifecycle, /Board approval/i, 'project lifecycle must require Board approval');
|
|
assert.match(lifecycle, /central project-channel reference skill/i, 'lifecycle must govern central channel-reference skill changes');
|
|
assert.match(lifecycle, /project-adopted channel skill/i, 'lifecycle must require project skill synchronization');
|
|
|
|
for (const section of ['projects', 'teams', 'members']) {
|
|
const sidebar = read(`sidebars/${section}.ts`);
|
|
assert.match(sidebar, /label:\s*'Registry'/, `${section} sidebar registry label must be concise`);
|
|
assert.doesNotMatch(sidebar, /label:\s*'(Projects|Teams|Members) Registry'/, `${section} sidebar must not repeat its section name`);
|
|
}
|
|
|
|
assert.ok(packageJson.dependencies?.['@easyops-cn/docusaurus-search-local'], 'local search dependency is missing');
|
|
assert.match(config, /@easyops-cn\/docusaurus-search-local/, 'local search theme is not configured');
|
|
assert.ok(packageJson.dependencies?.['docusaurus-plugin-drawio'], 'Draw.io plugin dependency is missing');
|
|
assert.ok(packageJson.devDependencies?.['raw-loader'], 'Draw.io raw-loader dependency is missing');
|
|
assert.match(config, /\['drawio',\s*\{lib:\s*'\/corp-v1\/portal\/drawio\/viewer\.min\.js'\}\]/, 'Draw.io plugin must use the self-hosted viewer');
|
|
assert.ok(existsSync(join(root, 'static/drawio/viewer.min.js')), 'self-hosted Draw.io viewer is missing');
|
|
const drawioLicense = read('static/drawio/LICENSE');
|
|
assert.match(drawioLicense, /MIT License/, 'vendored Draw.io viewer license is missing');
|
|
assert.match(drawioLicense, /Copyright \(c\) 2022 xiguaxigua/, 'vendored Draw.io viewer copyright notice is missing');
|
|
assert.match(config, /indexPages:\s*true/, 'custom pages must be indexed');
|
|
const searchRoutesMatch = config.match(/docsRouteBasePath:\s*\[([^\]]+)\]/);
|
|
assert.ok(searchRoutesMatch, 'search route configuration is missing');
|
|
const searchRoutes = searchRoutesMatch[1];
|
|
for (const route of ['/', 'architecture-high-level', 'projects', 'teams', 'members', 'ways-of-working', 'board', 'governance']) {
|
|
assert.ok(searchRoutes.includes(`'${route}'`), `search does not include route base ${route}`);
|
|
}
|
|
|
|
const architectureSidebar = read('sidebars/architecture-high-level.ts');
|
|
const expectedArchitectureSidebar = `import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
|
|
|
const sidebars: SidebarsConfig = {
|
|
architectureHighLevelSidebar: [
|
|
{type: 'doc', id: 'context', label: 'Context'},
|
|
{type: 'doc', id: 'overview', label: 'Container Overview'},
|
|
{type: 'doc', id: 'operating-model', label: 'Operating Model'},
|
|
{type: 'doc', id: 'overview-channels', label: 'Overview Channels'},
|
|
{type: 'doc', id: 'overview-skills', label: 'Overview Skills'},
|
|
{type: 'doc', id: 'overview-repositories', label: 'Overview Repositories'},
|
|
],
|
|
};
|
|
|
|
export default sidebars;`;
|
|
assert.equal(
|
|
architectureSidebar.trim(),
|
|
expectedArchitectureSidebar,
|
|
'Architecture High Level sidebar must contain exactly the six requested documents in order',
|
|
);
|
|
const architecturePages = {
|
|
context: read('docs/architecture-high-level/context.mdx'),
|
|
overview: read('docs/architecture-high-level/overview.mdx'),
|
|
operatingModel: read('docs/architecture-high-level/operating-model.mdx'),
|
|
channels: read('docs/architecture-high-level/overview-channels.mdx'),
|
|
skills: read('docs/architecture-high-level/overview-skills.mdx'),
|
|
repositories: read('docs/architecture-high-level/overview-repositories.mdx'),
|
|
};
|
|
for (const [name, page] of Object.entries(architecturePages)) {
|
|
assert.match(page, /!!raw-loader!\.\/diagrams\/.+\.drawio/, `${name} page must render an editable Draw.io source`);
|
|
}
|
|
for (const term of ['seven channel skills', 'seven engineering skills', 'Gitea Actions', 'scheduler jobs', 'corp-v1-board', 'corp-v1-guild', 'corp-v1-guild', 'UI/UX', 'one AI agent', 'Hermes Agent', 'MicroK8s', 'Penpot', 'Argo CD', 'Harbor']) {
|
|
assert.ok(Object.values(architecturePages).some((page) => page.includes(term)), `architecture explanation is missing: ${term}`);
|
|
}
|
|
assert.match(architecturePages.channels, /two central portfolio workspaces/i, 'channels overview must distinguish Board and Guild as central workspaces');
|
|
assert.match(architecturePages.channels, /neither is an eighth project channel/i, 'Guild must not be classified as an eighth project channel');
|
|
assert.match(architecturePages.skills, /not copied as a fifteenth project skill/i, 'Guild must remain central rather than project-adopted');
|
|
assert.doesNotMatch(architecturePages.channels, /one portfolio-governance channel/i, 'superseded single-central-workspace wording must not remain');
|
|
for (const term of ['Corp v1 Board members', 'Project leads and contributors', 'Prospective project sponsors and contributors', 'Solution users and reviewers']) {
|
|
assert.ok(architecturePages.context.includes(term), `system context is missing actor: ${term}`);
|
|
}
|
|
for (const term of ['only two external software systems', 'LLM Provider', 'ICA or GPT Codex', 'Communication Service — Discord']) {
|
|
assert.ok(architecturePages.context.includes(term), `system context is missing boundary rule: ${term}`);
|
|
}
|
|
for (const term of ['Gitea Actions runners', 'Penpot', 'LEGO Cloud Pages', 'Harbor', 'Argo CD', 'Identity and access', 'Ingress edge', 'Gondor v1 MicroK8s']) {
|
|
assert.ok(architecturePages.overview.includes(term), `container overview is missing internal container: ${term}`);
|
|
}
|
|
for (const term of ['corp-v1-main', 'corp-v1-channel-ui-ux', 'development-gitops-argo-cd', 'drawio-main', 'systematic-debugging', 'gondor-v1-nodes']) {
|
|
assert.ok(architecturePages.skills.includes(term), `skills overview is missing: ${term}`);
|
|
}
|
|
for (const [name, page] of Object.entries(architecturePages)) {
|
|
assert.doesNotMatch(page, /exactly six|six project channels|six channel skills|thirteen adopted skill/i, `${name} retains the superseded six-channel model`);
|
|
}
|
|
for (const [name, markers] of Object.entries({
|
|
'context.drawio': ['CORP V1 — HOME-LAB SYSTEM CONTEXT', 'HUMAN ACTORS', 'TARGET SYSTEM', 'EXTERNAL SYSTEMS · ONLY TWO', 'DISCORD', 'ICA OR GPT CODEX'],
|
|
'architecture-overview.drawio': ['CORP V1 — HOME-LAB CONTAINER OVERVIEW', 'EXTERNAL · DISCORD', 'EXTERNAL · LLM PROVIDER', 'INTERNAL CONTAINERS', 'GITEA ACTIONS RUNNERS', 'HARBOR', 'ARGO CD', 'GONDOR MICROK8S'],
|
|
'operating-model.drawio': ['CORP V1 — OPERATING MODEL FLOW', 'CORP-V1-BOARD', 'BOARD APPROVAL', 'UI/UX', 'KANBAN / FOCUS', 'VERIFY & RECORD'],
|
|
'channels.drawio': ['CORP V1 — BOARD, GUILD & PROJECT CHANNELS', 'CORP-V1-BOARD', 'CORP-V1-GUILD', 'GENERAL', 'ARCHITECTURE', 'UI/UX', 'RELEASES', 'MULTIPLE FOCUSED SESSIONS'],
|
|
'skills.drawio': ['CORP V1 — SKILLS MODEL', 'CENTRAL SOURCES', 'CORP-V1--GUILD', 'DELIBERATE ADOPTION', 'PROJECT COPIES', 'FOCUSED SESSION'],
|
|
'repositories.drawio': ['CORP V1 — ORGANIZATIONS, REPOSITORIES & DELIVERY', 'PROJECT ORGANIZATION', 'SKILLS ORGANIZATION', '7 adopted channel-skill repos', 'GITEA ACTIONS'],
|
|
})) {
|
|
const diagram = read(`docs/architecture-high-level/diagrams/${name}`);
|
|
const xmlResult = XMLValidator.validate(diagram);
|
|
assert.equal(xmlResult, true, `${name} is malformed XML: ${JSON.stringify(xmlResult)}`);
|
|
assert.match(diagram, /<mxfile[\s>]/, `${name} is not a Draw.io source file`);
|
|
for (const marker of markers) assert.ok(diagram.includes(marker), `${name} is missing: ${marker}`);
|
|
assert.doesNotMatch(diagram, /Six purpose-specific|6 channel skills|6 adopted channel-skill repos/i, `${name} retains the superseded six-channel model`);
|
|
}
|
|
|
|
if (existsSync(join(root, 'build'))) {
|
|
const filename = readdirSync(join(root, 'build')).find((name) => /^search-index(?:-[a-f0-9]+)?\.json$/.test(name));
|
|
assert.ok(filename, 'production search index is missing');
|
|
const search = readFileSync(join(root, 'build', filename), 'utf8').toLowerCase();
|
|
for (const term of ['projects registry', 'teams registry', 'members registry', 'board members', 'project lifecycle', 'corp v1 board', 'ways of working', 'your first 20 minutes', 'onboarding complete', 'container overview', 'operating model', 'overview channels', 'overview skills', 'overview repositories']) {
|
|
assert.ok(search.includes(term), `search index is missing: ${term}`);
|
|
}
|
|
}
|
|
|
|
console.log('Portal behavior checks passed.');
|