Skip to content

Commit 4efb48d

Browse files
committed
fix(web): keep command directory in sync with COMMANDS.md
Extract the session contract from the current shared-profile wording, include the Credential vault section, drive the on-page TOC from the filter, and announce result counts to assistive technology.
1 parent f1c2adf commit 4efb48d

8 files changed

Lines changed: 119 additions & 47 deletions

File tree

apps/web/app/docs/commands/page.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ export default function CommandsPage() {
1414
return (
1515
<DocsShell
1616
activePath="/docs/commands"
17-
sections={commands.groups.map((group) => ({
18-
id: group.id,
19-
label: group.title,
20-
}))}
2117
kicker={`${commands.count} command forms`}
2218
title={<>The complete agent surface.</>}
2319
lede="These usage lines come from the generated command reference, which protocol tests keep in sync with the CLI help output. Unknown parameters fail closed."

apps/web/app/globals.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,9 @@ h3,
13091309
.docs-toc {
13101310
margin-top: 30px;
13111311
}
1312+
.docs-toc-inline {
1313+
margin: 0 0 42px;
1314+
}
13121315
.docs-sidebar p {
13131316
margin: 0 0 6px;
13141317
color: var(--amber-ink);
@@ -1989,7 +1992,7 @@ h3,
19891992
overflow-x: auto;
19901993
}
19911994
.docs-sidebar p,
1992-
.docs-toc {
1995+
.docs-sidebar .docs-toc {
19931996
display: none;
19941997
}
19951998
.docs-nav-group {

apps/web/components/command-directory.tsx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export function CommandDirectory({ groups }: { groups: CommandGroup[] }) {
2323
);
2424
}, [groups, normalized]);
2525

26+
const countLabel =
27+
visible.length === 0
28+
? `No commands match “${query}”.`
29+
: `${visible.length} of ${groups.length} groups`;
30+
2631
return (
2732
<>
2833
<div className="command-filter">
@@ -32,28 +37,34 @@ export function CommandDirectory({ groups }: { groups: CommandGroup[] }) {
3237
type="search"
3338
value={query}
3439
onChange={(event) => setQuery(event.target.value)}
35-
placeholder="visit, inspect, record…"
40+
placeholder="visit, inspect, credentials…"
3641
autoComplete="off"
3742
spellCheck={false}
3843
/>
39-
<p>
40-
{visible.length} of {groups.length} groups
44+
<p role="status" aria-live="polite">
45+
{countLabel}
4146
</p>
4247
</div>
43-
{visible.length === 0 ? (
44-
<p role="status">No commands match “{query}”.</p>
45-
) : (
46-
visible.map((group, index) => (
47-
<section id={group.id} key={group.id}>
48-
<p className="docs-label">
49-
{String(index + 1).padStart(2, "0")} / {group.title}
50-
</p>
51-
<h2>{group.title}</h2>
52-
<p>{plainText(group.description)}</p>
53-
<CommandBlock>{group.usage}</CommandBlock>
54-
</section>
55-
))
56-
)}
48+
{visible.length > 0 ? (
49+
<nav className="docs-toc docs-toc-inline" aria-label="On this page">
50+
<p>On this page</p>
51+
{visible.map((group) => (
52+
<a href={`#${group.id}`} key={group.id}>
53+
{group.title}
54+
</a>
55+
))}
56+
</nav>
57+
) : null}
58+
{visible.map((group, index) => (
59+
<section id={group.id} key={group.id}>
60+
<p className="docs-label">
61+
{String(index + 1).padStart(2, "0")} / {group.title}
62+
</p>
63+
<h2>{group.title}</h2>
64+
<p>{plainText(group.description)}</p>
65+
<CommandBlock>{group.usage}</CommandBlock>
66+
</section>
67+
))}
5768
</>
5869
);
5970
}

apps/web/lib/repository-content.d.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const PRODUCT_DOC_ROUTES: Array<{
9696
category: string;
9797
}>;
9898

99+
export function sessionModelFromCommands(commandReference: string): string;
99100
export function loadBenchmarkContent(): BenchmarkContent;
100101
export function loadDocumentationContent(): DocumentationContent;
101102
export function loadProductDocsContent(): ProductDocsContent;

apps/web/lib/repository-content.mjs

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -389,17 +389,43 @@ function commandNames(usage) {
389389
function commandGroup(commandReference, title) {
390390
const section = extractSection(commandReference, title);
391391
const usage = fencedCode(section);
392-
const description = bulletItems(section)[0];
392+
if (!usage) fail(`missing command usage fence: ${title}`);
393+
const description = bulletItems(section)[0] || paragraphs(section)[0];
393394
if (!description) fail(`missing command description: ${title}`);
394395
return {
395396
title,
396-
id: title.toLowerCase().replaceAll(" ", "-"),
397+
id: title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
397398
commands: commandNames(usage),
398399
description,
399400
usage,
400401
};
401402
}
402403

404+
function commandGroupsFromReference(commandReference) {
405+
const titles = [...commandReference.matchAll(/^## (.+)$/gm)].map(
406+
(match) => match[1],
407+
);
408+
return titles.flatMap((title) => {
409+
if (title === "Where to go next") return [];
410+
const usage = fencedCodeBlocks(extractSection(commandReference, title))[0];
411+
return usage ? [commandGroup(commandReference, title)] : [];
412+
});
413+
}
414+
415+
export function sessionModelFromCommands(commandReference) {
416+
const lifecycle = extractSection(commandReference, "Host lifecycle");
417+
const shared = bulletItems(lifecycle).find((item) =>
418+
/one browser profile/i.test(item),
419+
);
420+
if (!shared) {
421+
fail("COMMANDS.md is missing the shared-profile session contract");
422+
}
423+
if (/stays isolated until you close it/i.test(shared)) {
424+
fail("session contract must not claim sessions stay isolated");
425+
}
426+
return shared;
427+
}
428+
403429
function listCommandForms(groups) {
404430
return groups.flatMap((group) =>
405431
group.usage
@@ -469,15 +495,7 @@ export function loadDocumentationContent() {
469495
if (documentationCache) return documentationCache;
470496
const readme = readRepositoryFile("README.md");
471497
const commandReference = readRepositoryFile("apps/headless/docs/COMMANDS.md");
472-
const sessionModel = bulletItems(
473-
extractSection(commandReference, "Host lifecycle"),
474-
).find(
475-
(item) =>
476-
item.includes("one browser profile") && item.includes("not an isolation"),
477-
);
478-
if (!sessionModel) {
479-
fail("COMMANDS.md is missing the shared-profile session contract");
480-
}
498+
const sessionModel = sessionModelFromCommands(commandReference);
481499
const workflowSection = extractSection(readme, "Agent workflow");
482500
const workflowCommands = fencedCode(workflowSection)
483501
.split("\n")
@@ -520,12 +538,7 @@ export function loadDocumentationContent() {
520538
workflowSection,
521539
"For scrollable-page QA",
522540
),
523-
commandGroups: [
524-
commandGroup(commandReference, "Host lifecycle"),
525-
commandGroup(commandReference, "Navigation and interaction"),
526-
commandGroup(commandReference, "Capture and evidence"),
527-
commandGroup(commandReference, "Diagnostics"),
528-
],
541+
commandGroups: commandGroupsFromReference(commandReference),
529542
security: bulletItems(extractSection(readme, "Security boundary")),
530543
platforms: bulletItems(
531544
readme.slice(0, readme.indexOf("## Computer use comparison")),
@@ -586,12 +599,7 @@ export function loadProductDocsContent() {
586599
fail("Headless MCP configuration is malformed");
587600
}
588601

589-
const commandGroups = [
590-
commandGroup(commandReference, "Host lifecycle"),
591-
commandGroup(commandReference, "Navigation and interaction"),
592-
commandGroup(commandReference, "Capture and evidence"),
593-
commandGroup(commandReference, "Diagnostics"),
594-
];
602+
const commandGroups = commandGroupsFromReference(commandReference);
595603
const commandForms = commandFormCount(commandGroups);
596604
if (commandForms < 30) fail("command reference contains fewer than 30 forms");
597605

@@ -780,6 +788,18 @@ export function validateRepositoryContent() {
780788
if (productDocs.commands.forms.length !== productDocs.commands.count) {
781789
fail("command directory forms drifted from the generated command count");
782790
}
791+
sessionModelFromCommands(readRepositoryFile("apps/headless/docs/COMMANDS.md"));
792+
if (
793+
!productDocs.commands.groups.some((group) => group.title === "Credential vault")
794+
) {
795+
fail("command directory omitted the Credential vault section");
796+
}
797+
const directory = readRepositoryFile(
798+
"apps/web/components/command-directory.tsx",
799+
);
800+
if (!directory.includes('role="status"')) {
801+
fail("command directory must announce filter result counts");
802+
}
783803
for (const convention of ["not-found.tsx", "robots.ts", "sitemap.ts"]) {
784804
readRepositoryFile(`apps/web/app/${convention}`);
785805
}

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
9+
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-command-directory.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
1010
"brand": "node scripts/render-brand.mjs"
1111
},
1212
"dependencies": {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import assert from "node:assert/strict";
2+
import { readFile } from "node:fs/promises";
3+
import { dirname, join } from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
import {
6+
loadProductDocsContent,
7+
sessionModelFromCommands,
8+
} from "../lib/repository-content.mjs";
9+
10+
const root = join(dirname(fileURLToPath(import.meta.url)), "../../..");
11+
const commands = await readFile(
12+
join(root, "apps/headless/docs/COMMANDS.md"),
13+
"utf8",
14+
);
15+
const directory = await readFile(
16+
join(root, "apps/web/components/command-directory.tsx"),
17+
"utf8",
18+
);
19+
20+
const sessionModel = sessionModelFromCommands(commands);
21+
assert.match(sessionModel, /one browser profile/i);
22+
assert.doesNotMatch(sessionModel, /stays isolated until you close it/i);
23+
24+
const { commands: surface } = loadProductDocsContent();
25+
assert.ok(
26+
surface.groups.some((group) => group.title === "Credential vault"),
27+
"command directory must include Credential vault",
28+
);
29+
assert.ok(
30+
surface.groups.some((group) => /auth login/.test(group.usage)),
31+
"command directory must include auth login",
32+
);
33+
assert.match(directory, /role="status"/);
34+
assert.match(directory, /aria-live="polite"/);
35+
assert.match(directory, /docs-toc-inline/);
36+
assert.doesNotMatch(
37+
await readFile(join(root, "apps/web/app/docs/commands/page.tsx"), "utf8"),
38+
/sections=\{commands\.groups/,
39+
);
40+
41+
console.log("command directory search and accessibility checks passed");

apps/web/scripts/validate-content-provenance.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { validateRepositoryContent } from "../lib/repository-content.mjs";
33
const result = validateRepositoryContent();
44
if (
55
result.benchmarkCases !== 4 ||
6-
result.commandGroups !== 4 ||
6+
result.commandGroups < 5 ||
77
result.securityRules < 3 ||
88
result.productDocRoutes !== 7 ||
99
result.commandForms < 30 ||

0 commit comments

Comments
 (0)