fix: [verified] expose all documentation in reachable sidebars
Build and publish Eldorado documentation / build (pull_request) Successful in 47s
Build and publish Eldorado documentation / build (pull_request) Successful in 47s
This commit is contained in:
1 parent
325363d89c
commit
e087adab1e
10 files changed
+255
-8
No files matched your search
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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;});
|
||||
@@ -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;});
|
||||
@@ -0,0 +1,111 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync, readdirSync, mkdirSync, writeFileSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
|
||||
const normalize = path => path.replace(/\/$/, '');
|
||||
const read = path => JSON.parse(readFileSync(path, 'utf8'));
|
||||
|
||||
// Use Docusaurus's resolved IDs/routes and sidebar trees, not filename guesses.
|
||||
export function auditSidebars(instances, entrySidebars) {
|
||||
const docs = new Map();
|
||||
const menus = new Map();
|
||||
for (const {plugin, version, documents, sidebars} of instances) {
|
||||
const ids = new Set();
|
||||
for (const doc of documents) {
|
||||
assert(!ids.has(doc.id), `Duplicate document ID: ${plugin}:${version}:${doc.id}`);
|
||||
ids.add(doc.id);
|
||||
const route = normalize(doc.path);
|
||||
assert(!docs.has(route), `Duplicate document route: ${route}`);
|
||||
docs.set(route, {...doc, plugin, version, menu: `${plugin}:${version}:${doc.sidebar}`});
|
||||
}
|
||||
for (const [name, items] of Object.entries(sidebars)) menus.set(`${plugin}:${version}:${name}`, items);
|
||||
}
|
||||
const links = new Map();
|
||||
for (const [menu, items] of menus) {
|
||||
const found = new Set();
|
||||
const walk = items => {
|
||||
for (const item of items) {
|
||||
assert(['category', 'link', 'html'].includes(item.type), `Unresolved sidebar type: ${item.type}`);
|
||||
assert(!item.unlisted, `Hidden sidebar item: ${item.label}`);
|
||||
if (item.href && !/^(https?:|mailto:)/.test(item.href)) {
|
||||
const route = normalize(item.href);
|
||||
if (item.docId) assert.equal(docs.get(route)?.id, item.docId, `Sidebar ID/route mismatch: ${item.docId}`);
|
||||
assert(!found.has(route), `Duplicate sidebar route in ${menu}: ${route}`);
|
||||
found.add(route);
|
||||
}
|
||||
if (item.type === 'category') walk(item.items);
|
||||
}
|
||||
};
|
||||
walk(items);
|
||||
links.set(menu, found);
|
||||
}
|
||||
const reachable = new Set(entrySidebars);
|
||||
for (const menu of reachable) {
|
||||
assert(menus.has(menu), `Unknown navigation sidebar: ${menu}`);
|
||||
for (const route of links.get(menu)) {
|
||||
const doc = docs.get(route);
|
||||
if (doc) reachable.add(doc.menu);
|
||||
}
|
||||
}
|
||||
for (const [route, doc] of docs) {
|
||||
assert(reachable.has(doc.menu), `Unreachable sidebar for ${doc.plugin}:${doc.id}`);
|
||||
assert(links.get(doc.menu)?.has(route), `Missing sidebar document: ${doc.plugin}:${doc.id}`);
|
||||
}
|
||||
return [...docs.entries()].map(([route, doc]) => ({route, id:doc.id, plugin:doc.plugin, sidebar:doc.sidebar}));
|
||||
}
|
||||
|
||||
export async function verifySidebarCoverage() {
|
||||
const global = read('.docusaurus/globalData.json')['docusaurus-plugin-content-docs'];
|
||||
assert(global && Object.keys(global).length, 'No generated document inventory');
|
||||
const instances = [];
|
||||
for (const [plugin, data] of Object.entries(global)) {
|
||||
const folder = `.docusaurus/docusaurus-plugin-content-docs/${plugin}/p`;
|
||||
const resolved = readdirSync(folder).filter(f => f.endsWith('.json')).map(f => read(join(folder, f)).version);
|
||||
for (const version of data.versions) {
|
||||
const matches = resolved.filter(v => v.version === version.name);
|
||||
assert.equal(matches.length, 1, `Missing/duplicate resolved version: ${plugin}:${version.name}`);
|
||||
instances.push({plugin, version:version.name, documents:version.docs, sidebars:matches[0].docsSidebars});
|
||||
}
|
||||
}
|
||||
const {default:config} = await import('../.docusaurus/docusaurus.config.mjs');
|
||||
const entries = config.themeConfig.navbar.items.filter(i => i.type === 'docSidebar').map(i => `${i.docsPluginId || 'default'}:current:${i.sidebarId}`);
|
||||
const documents = auditSidebars(instances, entries);
|
||||
const routes = new Set(documents.map(d => `${d.route.replace(config.baseUrl, '')}/index.html`));
|
||||
const exclusions = [];
|
||||
const html = readdirSync('build', {recursive:true}).filter(f => f.endsWith('.html')).sort();
|
||||
for (const file of html) {
|
||||
if (routes.delete(file)) continue;
|
||||
let reason;
|
||||
if (file === 'index.html') reason = 'Dedicated React homepage, not a documentation page';
|
||||
else if (file === '404.html') reason = 'Docusaurus not-found system page';
|
||||
else if (file === 'search/index.html') reason = 'Local-search results UI, not a documentation page';
|
||||
else if (file === 'strategy/index.html') {
|
||||
assert(readFileSync(join('build',file),'utf8').includes('http-equiv="refresh"'), 'Strategy alias must remain a redirect');
|
||||
reason = 'Legacy Strategy redirect alias to Target';
|
||||
} else if (file.startsWith('diagrams/') && readFileSync(join('build',file),'utf8').includes('<svg')) {
|
||||
assert.equal(readFileSync(join('build',file),'utf8'), readFileSync(join('static',file),'utf8'), `Viewer must match generated static asset: ${file}`);
|
||||
reason = 'Standalone diagram viewer asset, not documentation';
|
||||
}
|
||||
assert(reason, `Unclassified published HTML route: ${file}`);
|
||||
exclusions.push({file, reason});
|
||||
}
|
||||
assert.equal(routes.size, 0, `Missing published docs: ${[...routes]}`);
|
||||
// Check all local links and anchors on every document, not only selected Strategy pages.
|
||||
for (const {route} of documents) {
|
||||
const file = `build/${route.replace(config.baseUrl, '')}/index.html`;
|
||||
const body = readFileSync(file, 'utf8');
|
||||
for (const [, href] of body.matchAll(/href="([^" ]+)"/g)) {
|
||||
if (!href.startsWith(config.baseUrl) && !href.startsWith('#')) continue;
|
||||
const url = new URL(href, `https://docs.invalid${route}/`);
|
||||
let target = url.pathname.slice(config.baseUrl.length);
|
||||
if (!target || target.endsWith('/')) target += 'index.html';
|
||||
else if (!target.split('/').at(-1).includes('.')) target += '/index.html';
|
||||
const targetBody = readFileSync(join('build', decodeURIComponent(target)), 'utf8');
|
||||
if (url.hash) assert(targetBody.includes(`id="${decodeURIComponent(url.hash.slice(1))}"`), `Broken anchor: ${route} -> ${href}`);
|
||||
}
|
||||
}
|
||||
mkdirSync('.cache', {recursive:true});
|
||||
writeFileSync('.cache/sidebar-coverage.json', JSON.stringify({documents, exclusions}, null, 2));
|
||||
console.log(`PASS: ${documents.length} published docs reachable in resolved sidebars; ${exclusions.length} classified non-doc HTML routes; all doc links/anchors checked`);
|
||||
return {documents, exclusions};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user