From 3e48cdcf46dedbe8c44ba7ff98bace9234e97f99 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Thu, 27 Aug 2026 14:03:37 +0530 Subject: [PATCH] refactor(web): derive content from repository sources --- apps/web/app/docs/markdown/page.tsx | 12 +- apps/web/app/docs/page.tsx | 105 ++++- apps/web/app/page.tsx | 234 ++++++++-- apps/web/components/benchmark-chart.tsx | 76 ++- apps/web/components/docs-copy-controls.tsx | 198 ++++++-- apps/web/components/docs-markdown.ts | 58 --- apps/web/components/efficiency-chart.tsx | 75 ++- apps/web/lib/repository-content.d.mts | 57 +++ apps/web/lib/repository-content.mjs | 439 ++++++++++++++++++ apps/web/package.json | 2 +- .../scripts/validate-content-provenance.mjs | 14 + docs/roadmap/architecture-decisions.md | 38 +- docs/roadmap/improvements-backlog.md | 8 +- 13 files changed, 1089 insertions(+), 227 deletions(-) create mode 100644 apps/web/lib/repository-content.d.mts create mode 100644 apps/web/lib/repository-content.mjs create mode 100644 apps/web/scripts/validate-content-provenance.mjs diff --git a/apps/web/app/docs/markdown/page.tsx b/apps/web/app/docs/markdown/page.tsx index 76c9dd1..d6c6971 100644 --- a/apps/web/app/docs/markdown/page.tsx +++ b/apps/web/app/docs/markdown/page.tsx @@ -1,5 +1,13 @@ -import { pageMarkdown } from "@/components/docs-markdown"; +import { loadDocumentationContent } from "@/lib/repository-content.mjs"; export default function DocsMarkdownPage() { - return
← Back to docs
{pageMarkdown}
; + const documentation = loadDocumentationContent(); + return ( +
+
+ ← Back to docs +
{documentation.markdown}
+
+
+ ); } diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx index c2254d2..6c960a4 100644 --- a/apps/web/app/docs/page.tsx +++ b/apps/web/app/docs/page.tsx @@ -2,79 +2,136 @@ import { CommandBlock, CopyPageButton } from "@/components/docs-copy-controls"; import { HeadlessMark } from "@/components/headless-mark"; import { LinkGlyph } from "@/components/link-glyph"; import { ThemeToggle } from "@/components/theme-toggle"; +import { loadDocumentationContent } from "@/lib/repository-content.mjs"; import Link from "next/link"; -const coreCommands = [ - ["Navigation", "visit, back, reload, wait", "Move through a real browser session and wait for a URL, page text, or settled state."], - ["Interaction", "inspect, click, fill, press, scroll", "Use task-ranked controls and accessibility roles instead of mouse coordinates or selectors."], - ["Evidence", "screenshot, record, visual compare, report", "Create private PNG/JPG/PDF, MP4/MOV/WebM/GIF, diff, flow, and PR-report artifacts."], - ["Diagnostics", "console, network, styles, cookies, storage", "Investigate only when something fails; sensitive values remain redacted by default."], -]; +function plainText(markdown: string) { + return markdown + .replace(/\[([^\]]+)]\([^)]+\)/g, "$1") + .replaceAll("`", "") + .replaceAll("**", ""); +} export default function DocsPage() { + const documentation = loadDocumentationContent(); + return (
-
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 7a8bac1..da0685a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -8,6 +8,7 @@ import { ThemeToggle } from "@/components/theme-toggle"; import { LinkGlyph } from "@/components/link-glyph"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { loadBenchmarkContent } from "@/lib/repository-content.mjs"; import { ArrowDownIcon, ArrowUpRightIcon } from "lucide-react"; const commands = [ @@ -17,27 +18,31 @@ const commands = [ ]; const capabilities = [ - ["Observe", "Accessible elements, rendered media, console, network, CSS, cookies, and storage—only when the agent needs them.", false], - ["Capture", "Browser-pixel MP4 recordings and private PNG artifacts built for frontend QA and PR review.", false], - ["Reproduce", "Reusable flows, visual comparisons, throttling, offline mode, and exact API mocks.", false], - ["Contain", "A private Unix socket, no exposed DevTools port, bounded commands, and safe navigation rules.", true], + [ + "Observe", + "Accessible elements, rendered media, console, network, CSS, cookies, and storage—only when the agent needs them.", + false, + ], + [ + "Capture", + "Browser-pixel MP4 recordings and private PNG artifacts built for frontend QA and PR review.", + false, + ], + [ + "Reproduce", + "Reusable flows, visual comparisons, throttling, offline mode, and exact API mocks.", + false, + ], + [ + "Contain", + "A private Unix socket, no exposed DevTools port, bounded commands, and safe navigation rules.", + true, + ], ] as const; -const proofs = [ - { metric: "Tokens", against: "Selenium + Python", value: 64, description: "fewer estimated agent tokens" }, - { metric: "Tokens", against: "Puppeteer", value: 71, description: "fewer estimated agent tokens" }, - { metric: "CPU time", against: "Selenium + Python", value: 56, description: "less CPU time per run" }, - { metric: "CPU time", against: "Puppeteer", value: 66, description: "less CPU time per run" }, -] as const; - -const benchmarks = [ - ["Headless", "warm", "147", "4.753 s", "842 ms", "276 MiB"], - ["Headless", "cold", "194", "5.002 s", "1.478 s", "279 MiB"], - ["Selenium", "Python", "410", "3.134 s", "1.900 s", "280 MiB"], - ["Puppeteer", "", "499", "2.850 s", "2.441 s", "319 MiB"], -]; - export default function Home() { + const benchmark = loadBenchmarkContent(); + return (
@@ -70,7 +75,10 @@ export default function Home() { Workflow Capabilities Docs - + GitHub @@ -84,7 +92,9 @@ export default function Home() { a real browser.

- Headless turns browser QA into a small, inspectable command surface, so an agent can navigate, test, record, and explain without screen coordinates or a brittle script stack. + Headless turns browser QA into a small, inspectable command + surface, so an agent can navigate, test, record, and explain + without screen coordinates or a brittle script stack.

-
P2 benchmark / 17 Jul 2026

Smallest agent surface.
Measured, not claimed.

-

One fresh Linux ARM64 container per case. The same dashboard visit, recording, transition, and final screenshot.

+
+
{benchmark.sectionLabel}
+

+ {benchmark.headline} +
+ Measured, not claimed. +

+
+

{benchmark.summary}

-
- {proofs.map((proof) => ( +
+ {benchmark.proofs.map((proof) => (
- {proof.metric} · vs {proof.against} - {proof.value}% + + {proof.metric} · vs {proof.against} + + + {proof.value} + % +

{proof.description}

))}
-
Agent token footprint / lower is better

Headless warm = 1×

-
Wall time vs CPU time per run

The gap is waiting, not computing

+
+
+ Agent token footprint / lower is better +

Headless warm = 1×

+
+ +
+
+
+ Wall time vs CPU time per run +

The gap is waiting, not computing

+
+ ({ + ...workflow, + label: workflow.variant || workflow.name, + }))} + /> +
- - {benchmarks.map(([name, variant, tokens, wallTime, cpuTime, memory]) => )} + + + + + + + + + + + {benchmark.workflows.map((workflow) => ( + + + + + + + + ))} +
WorkflowEstimated tokensWall timeCPU timePeak memory
{name}{variant && {variant}}{tokens}{wallTime}{cpuTime}{memory}
WorkflowEstimated tokensWall timeCPU timePeak memory
+ {workflow.name} + {workflow.variant && {workflow.variant}} + {workflow.formatted.tokens}{workflow.formatted.wallTime}{workflow.formatted.cpuTime}{workflow.formatted.memory}
-
+
+

+ + Warm starts with the Headless host and session ready.{" "} + Cold includes starting both. + + + Estimated tokens measure workflow source size, not billed LLM + tokens. Point-in-time snapshot; repeat before comparing a change. + +

+ + Read the method + +
The control loop
-

From intent to evidence—

without touching the desktop.

+
+

From intent to evidence—

+

without touching the desktop.

+
{commands.map(([number, title, description]) => (
- {number}

{title}

{description}

+ {number} +

{title}

+

{description}

+
))}
-
-

hermes-vm — headless

secure local session
+
+
+ + + +

hermes-vm — headless

+ secure local session +
-

$ headless start ✓ ready

-

$ headless session create qa

-

$ headless --session qa visit localhost:3000/designers/dashboard

-

$ headless --session qa record start --fps 10

-

$ headless --session qa click --role button --name Continue

-

$ headless --session qa report create --output pr-report.json ✓ evidence ready

+

+ $ headless start ✓ ready +

+

+ $ headless session create qa +

+

+ $ headless --session qa visit + localhost:3000/designers/dashboard +

+

+ $ headless --session qa record start --fps 10 +

+

+ $ headless --session qa click --role button --name + Continue +

+

+ $ headless --session qa report create --output + pr-report.json ✓ evidence ready +

@@ -174,14 +292,29 @@ export default function Home() {
Made for real QA
-

When the page breaks,
the agent can look closer.

-

Deep diagnostics are available on demand, not forced into every interaction.

+

+ When the page breaks, +
+ the agent can look closer. +

+

+ Deep diagnostics are available on demand, not forced into every + interaction. +

{capabilities.map(([title, description, safe], index) => ( -
+
0{index + 1} -

{title}

{description}

+
+

{title}

+

{description}

+
))} @@ -220,7 +353,8 @@ export default function Home() {

- One CLI protocol. The same commands over MCP. Purpose-built for the last mile of software QA. + One CLI protocol. The same commands over MCP. Purpose-built for + the last mile of software QA.

+
+ headless
-
headless
); diff --git a/apps/web/components/benchmark-chart.tsx b/apps/web/components/benchmark-chart.tsx index 74147d9..e111ff6 100644 --- a/apps/web/components/benchmark-chart.tsx +++ b/apps/web/components/benchmark-chart.tsx @@ -18,16 +18,15 @@ type Workflow = { color: string; }; -const data: Workflow[] = [ - { workflow: "Headless warm", surface: 1, color: "var(--amber-ink)" }, - { workflow: "Headless cold", surface: 1.32, color: "var(--teal-ink)" }, - { workflow: "Selenium + Python", surface: 2.79, color: "#8A9490" }, - { workflow: "Puppeteer", surface: 3.39, color: "#5B6469" }, -]; - const formatRatio = (value: number) => `${value.toFixed(value === 1 ? 0 : 2)}×`; -function BenchmarkTooltip({ active, payload }: { active?: boolean; payload?: ReadonlyArray<{ payload?: unknown }> }) { +function BenchmarkTooltip({ + active, + payload, +}: { + active?: boolean; + payload?: ReadonlyArray<{ payload?: unknown }>; +}) { if (!active || !payload?.length) return null; const workflow = payload[0]?.payload as Workflow | undefined; if (!workflow) return null; @@ -35,41 +34,68 @@ function BenchmarkTooltip({ active, payload }: { active?: boolean; payload?: Rea return (
{workflow.workflow} - {formatRatio(workflow.surface)} Headless warm's estimated tokens + + {formatRatio(workflow.surface)} Headless warm's estimated tokens +
); } -export function BenchmarkChart() { +export function BenchmarkChart({ data }: { data: Workflow[] }) { + const ariaLabel = `Estimated agent tokens relative to Headless warm: ${data + .map((workflow) => `${workflow.workflow} ${formatRatio(workflow.surface)}`) + .join(", ")}. Lower is better.`; + const maximum = Math.max(...data.map((workflow) => workflow.surface)); + return ( -
+
- - + + `${value}×`} axisLine={{ stroke: "var(--line)" }} tickLine={false} /> - - - + + + {data.map((workflow) => ( ))} ( - typeof value === "number" ? formatRatio(value) : String(value ?? "") - )} + formatter={(value) => + typeof value === "number" + ? formatRatio(value) + : String(value ?? "") + } fill="var(--ink-soft)" fontFamily="var(--font-mono), monospace" fontSize={10} diff --git a/apps/web/components/docs-copy-controls.tsx b/apps/web/components/docs-copy-controls.tsx index f27d36d..f2740bd 100644 --- a/apps/web/components/docs-copy-controls.tsx +++ b/apps/web/components/docs-copy-controls.tsx @@ -2,16 +2,51 @@ import { useEffect, useRef, useState } from "react"; import { siClaude, siCursor, siPerplexity } from "simple-icons/icons"; -import { cursorMcpConfig, pageMarkdown } from "@/components/docs-markdown"; +import { cursorMcpConfig } from "@/components/docs-markdown"; -const openaiBlossomPath = "M11.248 18.25q-.825 0-1.568-.314a4.3 4.3 0 0 1-1.32-.874 4 4 0 0 1-1.304.214 4 4 0 0 1-2.046-.544 4.27 4.27 0 0 1-1.518-1.485 4 4 0 0 1-.56-2.095q0-.48.131-1.04A4.4 4.4 0 0 1 2.04 10.71a4.07 4.07 0 0 1 .017-3.4 4.2 4.2 0 0 1 1.056-1.418 3.8 3.8 0 0 1 1.6-.842 3.9 3.9 0 0 1 .76-1.683q.593-.759 1.451-1.188a4.04 4.04 0 0 1 1.832-.429q.825 0 1.567.313.742.314 1.32.875a4 4 0 0 1 1.304-.215q1.106 0 2.046.545a4.14 4.14 0 0 1 1.501 1.485q.578.941.578 2.095 0 .48-.132 1.04.66.61 1.023 1.419.363.792.363 1.666 0 .892-.38 1.717a4.3 4.3 0 0 1-1.072 1.435 3.8 3.8 0 0 1-1.584.825 3.8 3.8 0 0 1-.775 1.683 4.06 4.06 0 0 1-1.436 1.188 4.04 4.04 0 0 1-1.832.429m-4.076-2.062q.825 0 1.435-.347l3.103-1.782a.36.36 0 0 0 .164-.313v-1.42L7.881 14.62a.67.67 0 0 1-.726 0l-3.118-1.798a.5.5 0 0 1-.017.115v.198q0 .841.396 1.551.413.693 1.139 1.089a3.2 3.2 0 0 0 1.617.412m.165-2.69a.4.4 0 0 0 .181.05q.083 0 .165-.05l1.238-.71-3.977-2.31a.7.7 0 0 1-.363-.643v-3.58q-.825.362-1.32 1.122a2.9 2.9 0 0 0-.495 1.65q0 .809.413 1.55.412.743 1.072 1.123zm3.91 3.663q.875 0 1.585-.396a2.96 2.96 0 0 0 1.534-2.64v-3.564a.32.32 0 0 0-.165-.297l-1.254-.726v4.604a.7.7 0 0 1-.363.643l-3.119 1.799a3 3 0 0 0 1.783.577m.627-6.039V8.878L10.01 7.822 8.129 8.878v2.244l1.881 1.056zM7.057 5.859a.7.7 0 0 1 .363-.644l3.119-1.798a3 3 0 0 0-1.782-.578q-.874 0-1.584.396A2.96 2.96 0 0 0 6.05 4.324a3.07 3.07 0 0 0-.396 1.551v3.547q0 .199.165.314l1.237.726zm8.383 7.887q.825-.364 1.303-1.123.495-.758.495-1.65a3.15 3.15 0 0 0-.412-1.55q-.413-.743-1.073-1.123l-3.086-1.782q-.099-.065-.181-.049a.3.3 0 0 0-.165.05l-1.238.692 3.993 2.327a.6.6 0 0 1 .264.264.64.64 0 0 1 .1.363zm-3.317-8.382a.63.63 0 0 1 .726 0l3.135 1.831v-.297q0-.792-.396-1.501a2.86 2.86 0 0 0-1.105-1.155q-.71-.43-1.65-.43-.825 0-1.436.347L8.294 5.941a.36.36 0 0 0-.165.314v1.418z"; +const openaiBlossomPath = + "M11.248 18.25q-.825 0-1.568-.314a4.3 4.3 0 0 1-1.32-.874 4 4 0 0 1-1.304.214 4 4 0 0 1-2.046-.544 4.27 4.27 0 0 1-1.518-1.485 4 4 0 0 1-.56-2.095q0-.48.131-1.04A4.4 4.4 0 0 1 2.04 10.71a4.07 4.07 0 0 1 .017-3.4 4.2 4.2 0 0 1 1.056-1.418 3.8 3.8 0 0 1 1.6-.842 3.9 3.9 0 0 1 .76-1.683q.593-.759 1.451-1.188a4.04 4.04 0 0 1 1.832-.429q.825 0 1.567.313.742.314 1.32.875a4 4 0 0 1 1.304-.215q1.106 0 2.046.545a4.14 4.14 0 0 1 1.501 1.485q.578.941.578 2.095 0 .48-.132 1.04.66.61 1.023 1.419.363.792.363 1.666 0 .892-.38 1.717a4.3 4.3 0 0 1-1.072 1.435 3.8 3.8 0 0 1-1.584.825 3.8 3.8 0 0 1-.775 1.683 4.06 4.06 0 0 1-1.436 1.188 4.04 4.04 0 0 1-1.832.429m-4.076-2.062q.825 0 1.435-.347l3.103-1.782a.36.36 0 0 0 .164-.313v-1.42L7.881 14.62a.67.67 0 0 1-.726 0l-3.118-1.798a.5.5 0 0 1-.017.115v.198q0 .841.396 1.551.413.693 1.139 1.089a3.2 3.2 0 0 0 1.617.412m.165-2.69a.4.4 0 0 0 .181.05q.083 0 .165-.05l1.238-.71-3.977-2.31a.7.7 0 0 1-.363-.643v-3.58q-.825.362-1.32 1.122a2.9 2.9 0 0 0-.495 1.65q0 .809.413 1.55.412.743 1.072 1.123zm3.91 3.663q.875 0 1.585-.396a2.96 2.96 0 0 0 1.534-2.64v-3.564a.32.32 0 0 0-.165-.297l-1.254-.726v4.604a.7.7 0 0 1-.363.643l-3.119 1.799a3 3 0 0 0 1.783.577m.627-6.039V8.878L10.01 7.822 8.129 8.878v2.244l1.881 1.056zM7.057 5.859a.7.7 0 0 1 .363-.644l3.119-1.798a3 3 0 0 0-1.782-.578q-.874 0-1.584.396A2.96 2.96 0 0 0 6.05 4.324a3.07 3.07 0 0 0-.396 1.551v3.547q0 .199.165.314l1.237.726zm8.383 7.887q.825-.364 1.303-1.123.495-.758.495-1.65a3.15 3.15 0 0 0-.412-1.55q-.413-.743-1.073-1.123l-3.086-1.782q-.099-.065-.181-.049a.3.3 0 0 0-.165.05l-1.238.692 3.993 2.327a.6.6 0 0 1 .264.264.64.64 0 0 1 .1.363zm-3.317-8.382a.63.63 0 0 1 .726 0l3.135 1.831v-.297q0-.792-.396-1.501a2.86 2.86 0 0 0-1.105-1.155q-.71-.43-1.65-.43-.825 0-1.436.347L8.294 5.941a.36.36 0 0 0-.165.314v1.418z"; -function BrandIcon({ name, path, viewBox = "0 0 24 24" }: { name: string; path: string; viewBox?: string }) { - return ; +function BrandIcon({ + name, + path, + viewBox = "0 0 24 24", +}: { + name: string; + path: string; + viewBox?: string; +}) { + return ( + + ); } function CopyIcon() { - return ; + return ( + + ); } function useCopy(text: string) { @@ -30,7 +65,7 @@ function useCopy(text: string) { return { copied, copy }; } -export function CopyPageButton() { +export function CopyPageButton({ pageMarkdown }: { pageMarkdown: string }) { const { copied, copy } = useCopy(pageMarkdown); const [open, setOpen] = useState(false); const [mcpCopied, setMcpCopied] = useState(false); @@ -54,7 +89,11 @@ export function CopyPageButton() { function openAssistant(baseUrl: string) { const pageUrl = `${window.location.origin}/docs`; const prompt = `Read and answer questions about this documentation: ${pageUrl}`; - window.open(`${baseUrl}${encodeURIComponent(prompt)}`, "_blank", "noopener,noreferrer"); + window.open( + `${baseUrl}${encodeURIComponent(prompt)}`, + "_blank", + "noopener,noreferrer", + ); setOpen(false); } @@ -68,29 +107,134 @@ export function CopyPageButton() { } } - return
- - {open &&
- - M↓View as Markdown ↗Read the plain-text version - - - - -
} -
; + return ( +
+ + {open && ( +
+ + + M↓ + + View as Markdown ↗ + Read the plain-text version + + + + + + +
+ )} +
+ ); } export function CommandBlock({ children }: { children: string }) { const { copied, copy } = useCopy(children); - return
-
{children}
- -
; + return ( +
+
+        {children}
+      
+ +
+ ); } diff --git a/apps/web/components/docs-markdown.ts b/apps/web/components/docs-markdown.ts index 1304667..9bf9852 100644 --- a/apps/web/components/docs-markdown.ts +++ b/apps/web/components/docs-markdown.ts @@ -1,61 +1,3 @@ import cursorConfig from "../../../.cursor/mcp.json"; -export const pageMarkdown = `# Headless documentation - -Headless gives an agent a persistent browser through a small, safe CLI. Use it to test a page, capture evidence, and inspect a failure. - -## First run - -Start the host, create a session, then visit the app. The session stays isolated until you close it. - -\`\`\`sh -headless start -headless session create qa -headless --session qa visit localhost:3000/designers/dashboard -headless --session qa inspect --context summary --task "click Continue" -\`\`\` - -On macOS, agent startup leaves your current app in front. Use \`headless config set startup-presentation foreground\` to make the old foreground behavior persistent, or set it to \`background\` to restore the built-in default. Inspect it with \`headless config get startup-presentation\`. The \`headless start --foreground\` and \`--background\` flags override the setting for one new host; they do not reorder a running host. - -## QA workflow - -Record the path you need, then stop and create a report. - -\`\`\`sh -headless --session qa record start --fps 10 --format mp4 -headless --session qa click --role button --name Continue -headless --session qa wait --url /next --settled -headless --session qa record stop --output dashboard-flow.mp4 -headless --session qa report create --output pr-report.json -\`\`\` - -## Command groups - -- Navigation: visit, back, reload, wait -- Interaction: inspect, click, fill, press, scroll -- Evidence: screenshot, record, visual compare, report -- Diagnostics: console, network, styles, cookies, storage - -## Context pruning - -Start with \`inspect --context summary --task "..."\`. On a large page, request \`--context outline\`, choose a returned region such as \`@r4\`, then ask only for \`--context text --within @r4\` or \`--context actions --within @r4\`. Use \`--limit\`, \`--budget\`, and \`--depth\` to cap responses. Each result reports omitted content and conservative estimated-token statistics. Use \`inspect --context full --text\` only when the broader raw snapshot is required. - -## Scrollable page evidence - -\`\`\`sh -headless --session qa screenshot --every-viewport --output dashboard-scroll -headless --session qa screenshot --by-section --format jpg --output dashboard-sections -\`\`\` - -Use viewport screenshots for up to 80 100vh scroll stops, always including the final bottom position; bounded results report \`truncated\` and \`totalPoints\`. Series capture restores the original scroll position and creates numbered PNG or JPG artifacts from the prefix. PDF requires \`--full-page\`; macOS image screenshots can also use clipboard output. - -## Safety - -Headless uses a private Unix socket, not a public DevTools port. Only HTTP(S) navigation is allowed. Diagnostic secrets are redacted. - -## Platforms - -- Linux: use the supplied Docker runtime or native Chromium. Ubuntu Snap Chromium is not supported for repeated navigation. -- macOS: build with Xcode Command Line Tools and use the visible WKWebView host through the same CLI.`; - export const cursorMcpConfig = JSON.stringify(cursorConfig, null, 2); diff --git a/apps/web/components/efficiency-chart.tsx b/apps/web/components/efficiency-chart.tsx index 12deaeb..cbbb60b 100644 --- a/apps/web/components/efficiency-chart.tsx +++ b/apps/web/components/efficiency-chart.tsx @@ -13,7 +13,7 @@ import { YAxis, } from "recharts"; -type WorkflowPoint = { +export type WorkflowPoint = { workflow: string; label: string; tokens: number; @@ -23,14 +23,6 @@ type WorkflowPoint = { color: string; }; -/** P2 single-run benchmark. Source: BENCHMARK.md. */ -const data: WorkflowPoint[] = [ - { workflow: "Headless warm", label: "Warm", tokens: 147, cpuMs: 842, wallMs: 4753, memoryMiB: 276, color: "var(--amber-ink)" }, - { workflow: "Headless cold", label: "Cold", tokens: 194, cpuMs: 1478, wallMs: 5002, memoryMiB: 279, color: "var(--teal-ink)" }, - { workflow: "Selenium + Python", label: "Selenium", tokens: 410, cpuMs: 1900, wallMs: 3134, memoryMiB: 280, color: "#8A9490" }, - { workflow: "Puppeteer", label: "Puppeteer", tokens: 499, cpuMs: 2441, wallMs: 2850, memoryMiB: 319, color: "#5B6469" }, -]; - function formatSeconds(value: number) { return `${(value / 1000).toFixed(3)} s`; } @@ -39,7 +31,13 @@ function cpuShare(point: WorkflowPoint) { return Math.round((point.cpuMs / point.wallMs) * 100); } -function EfficiencyTooltip({ active, payload }: { active?: boolean; payload?: ReadonlyArray<{ payload?: unknown }> }) { +function EfficiencyTooltip({ + active, + payload, +}: { + active?: boolean; + payload?: ReadonlyArray<{ payload?: unknown }>; +}) { if (!active || !payload?.length) return null; const point = payload[0]?.payload as WorkflowPoint | undefined; if (!point) return null; @@ -47,22 +45,38 @@ function EfficiencyTooltip({ active, payload }: { active?: boolean; payload?: Re return (
{point.workflow} - {formatSeconds(point.wallMs)} wall · {formatSeconds(point.cpuMs)} CPU - CPU busy {cpuShare(point)}% of the run · {point.memoryMiB} MiB peak + + {formatSeconds(point.wallMs)} wall · {formatSeconds(point.cpuMs)} CPU + + + CPU busy {cpuShare(point)}% of the run · {point.memoryMiB} MiB peak +
); } -export function EfficiencyChart() { +export function EfficiencyChart({ data }: { data: WorkflowPoint[] }) { + const ariaLabel = `Wall time versus CPU time per run. ${data + .map( + (point) => + `${point.workflow}: ${formatSeconds(point.wallMs)} wall, ${formatSeconds(point.cpuMs)} CPU, busy ${cpuShare(point)} percent`, + ) + .join(". ")}.`; + const maximum = + Math.ceil(Math.max(...data.map((point) => point.wallMs)) / 1_000) * 1_000; + return ( -
+
- - + + `${value / 1000}s`} axisLine={false} tickLine={false} width={40} /> - + @@ -112,7 +133,11 @@ export function EfficiencyChart() { dataKey="cpuMs" position="bottom" offset={10} - formatter={(value) => (typeof value === "number" ? `${(value / 1000).toFixed(2)}s` : String(value ?? ""))} + formatter={(value) => + typeof value === "number" + ? `${(value / 1000).toFixed(2)}s` + : String(value ?? "") + } fill="var(--ink-soft)" fontFamily="var(--font-mono), monospace" fontSize={9} diff --git a/apps/web/lib/repository-content.d.mts b/apps/web/lib/repository-content.d.mts new file mode 100644 index 0000000..3eeb3d8 --- /dev/null +++ b/apps/web/lib/repository-content.d.mts @@ -0,0 +1,57 @@ +export type BenchmarkWorkflow = { + case: string; + workflow: string; + name: string; + variant: string; + color: string; + tokens: number; + cpuMs: number; + wallMs: number; + memoryMiB: number; + surface: number; + formatted: { + tokens: string; + wallTime: string; + cpuTime: string; + memory: string; + }; +}; + +export type BenchmarkContent = { + sectionLabel: string; + headline: string; + summary: string; + proofs: Array<{ + metric: string; + against: string; + value: number; + description: string; + }>; + workflows: BenchmarkWorkflow[]; +}; + +export type DocumentationContent = { + introduction: string; + firstRunCommands: string; + qaWorkflowCommands: string; + startupPresentation: string; + contextPruning: string; + scrollableEvidence: string; + commandGroups: Array<{ + title: string; + commands: string; + description: string; + usage: string; + }>; + security: string[]; + platforms: string[]; + markdown: string; +}; + +export function loadBenchmarkContent(): BenchmarkContent; +export function loadDocumentationContent(): DocumentationContent; +export function validateRepositoryContent(): { + benchmarkCases: number; + commandGroups: number; + securityRules: number; +}; diff --git a/apps/web/lib/repository-content.mjs b/apps/web/lib/repository-content.mjs new file mode 100644 index 0000000..c458bc2 --- /dev/null +++ b/apps/web/lib/repository-content.mjs @@ -0,0 +1,439 @@ +import { lstatSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPOSITORY_ROOT = resolve( + dirname(fileURLToPath(import.meta.url)), + "../../..", +); +const MAX_SOURCE_BYTES = 8 * 1024 * 1024; +const BENCHMARK_CASES = ["headless", "headless-warm", "selenium", "puppeteer"]; +const PRESENTATION = { + headless: { name: "Headless", variant: "cold", color: "var(--teal-ink)" }, + "headless-warm": { + name: "Headless", + variant: "warm", + color: "var(--amber-ink)", + }, + selenium: { name: "Selenium", variant: "Python", color: "#8A9490" }, + puppeteer: { name: "Puppeteer", variant: "", color: "#5B6469" }, +}; + +let benchmarkCache; +let documentationCache; + +function fail(message) { + throw new Error(`repository content: ${message}`); +} + +function readRepositoryFile(relativePath) { + const path = resolve(REPOSITORY_ROOT, relativePath); + const metadata = lstatSync(path); + if ( + !metadata.isFile() || + metadata.size === 0 || + metadata.size > MAX_SOURCE_BYTES + ) { + fail( + `${relativePath} must be a non-empty regular file no larger than ${MAX_SOURCE_BYTES} bytes`, + ); + } + return readFileSync(path, "utf8"); +} + +function assertRecord(value, name) { + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${name} must be an object`); + } + return value; +} + +function positiveInteger(value, name) { + if (!Number.isSafeInteger(value) || value <= 0) + fail(`${name} must be a positive integer`); + return value; +} + +function parseBenchmarkDocument() { + let document; + try { + document = JSON.parse( + readRepositoryFile("packages/benchmark-results/results.json"), + ); + } catch (error) { + fail( + `benchmark JSON is invalid: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + assertRecord(document, "benchmark document"); + if (document.schemaVersion !== 1) fail("benchmark schemaVersion must be 1"); + const generatedAt = new Date(document.generatedAt); + if ( + typeof document.generatedAt !== "string" || + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(document.generatedAt) || + Number.isNaN(generatedAt.valueOf()) || + generatedAt.toISOString() !== document.generatedAt.replace("Z", ".000Z") + ) { + fail("benchmark generatedAt must be an ISO 8601 timestamp"); + } + + const provenance = assertRecord(document.provenance, "benchmark provenance"); + if (provenance.generator !== "apps/headless/benchmark.sh") + fail("unexpected benchmark generator"); + if (provenance.method !== "apps/headless/docs/BENCHMARK.md") + fail("unexpected benchmark method"); + if (!/^linux\/[a-z0-9][a-z0-9_-]{0,31}$/.test(provenance.platform)) { + fail("benchmark platform must identify a Linux architecture"); + } + positiveInteger(provenance.repeats, "benchmark repeats"); + if (provenance.aggregation !== "median") + fail("benchmark aggregation must be median"); + if (provenance.taskAwareInspection !== true) + fail("benchmark must include task-aware inspection"); + if ( + !Array.isArray(document.cases) || + document.cases.length !== BENCHMARK_CASES.length + ) { + fail(`benchmark must contain exactly ${BENCHMARK_CASES.length} cases`); + } + + const cases = new Map(); + for (const entry of document.cases) { + assertRecord(entry, "benchmark case"); + if (!BENCHMARK_CASES.includes(entry.case) || cases.has(entry.case)) { + fail(`unexpected or duplicate benchmark case: ${String(entry.case)}`); + } + const median = assertRecord(entry.median, `${entry.case} median`); + for (const metric of [ + "wallMs", + "cpuMs", + "memoryPeakBytes", + "estimatedTokens", + ]) { + positiveInteger(median[metric], `${entry.case}.${metric}`); + } + cases.set(entry.case, { ...entry, median }); + } + for (const caseName of BENCHMARK_CASES) { + if (!cases.has(caseName)) fail(`missing benchmark case: ${caseName}`); + } + + return { generatedAt, provenance, cases }; +} + +function comparisonProof( + metric, + against, + baseline, + comparison, + lowerDescription, + higherDescription, +) { + return { + metric, + against, + value: Math.round(Math.abs(1 - baseline / comparison) * 100), + description: baseline <= comparison ? lowerDescription : higherDescription, + }; +} + +function formatDuration(milliseconds) { + return milliseconds < 1_000 + ? `${milliseconds} ms` + : `${(milliseconds / 1_000).toFixed(3)} s`; +} + +function formatMemory(bytes) { + return `${Math.round(bytes / (1024 * 1024))} MiB`; +} + +function displayPlatform(platform) { + const [operatingSystem, architecture] = platform.split("/"); + return `${operatingSystem[0].toUpperCase()}${operatingSystem.slice(1)} ${architecture.toUpperCase()}`; +} + +export function loadBenchmarkContent() { + if (benchmarkCache) return benchmarkCache; + const { generatedAt, provenance, cases } = parseBenchmarkDocument(); + const warm = cases.get("headless-warm").median; + const selenium = cases.get("selenium").median; + const puppeteer = cases.get("puppeteer").median; + const date = new Intl.DateTimeFormat("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "UTC", + }).format(generatedAt); + + const workflows = BENCHMARK_CASES.map((caseName) => { + const entry = cases.get(caseName); + const presentation = PRESENTATION[caseName]; + return { + case: caseName, + workflow: entry.label.replace(",", ""), + ...presentation, + tokens: entry.median.estimatedTokens, + cpuMs: entry.median.cpuMs, + wallMs: entry.median.wallMs, + memoryMiB: Math.round(entry.median.memoryPeakBytes / (1024 * 1024)), + surface: entry.median.estimatedTokens / warm.estimatedTokens, + formatted: { + tokens: String(entry.median.estimatedTokens), + wallTime: formatDuration(entry.median.wallMs), + cpuTime: formatDuration(entry.median.cpuMs), + memory: formatMemory(entry.median.memoryPeakBytes), + }, + }; + }); + + benchmarkCache = { + sectionLabel: `P2 benchmark / ${date}`, + headline: + warm.estimatedTokens === + Math.min(...workflows.map((workflow) => workflow.tokens)) + ? "Smallest agent surface." + : "Measured agent surface.", + summary: `${provenance.repeats} fresh ${displayPlatform(provenance.platform)} containers per case. The table reports medians for the same dashboard workflow.`, + proofs: [ + comparisonProof( + "Tokens", + "Selenium + Python", + warm.estimatedTokens, + selenium.estimatedTokens, + "fewer estimated agent tokens", + "more estimated agent tokens", + ), + comparisonProof( + "Tokens", + "Puppeteer", + warm.estimatedTokens, + puppeteer.estimatedTokens, + "fewer estimated agent tokens", + "more estimated agent tokens", + ), + comparisonProof( + "CPU time", + "Selenium + Python", + warm.cpuMs, + selenium.cpuMs, + "less median CPU time", + "more median CPU time", + ), + comparisonProof( + "CPU time", + "Puppeteer", + warm.cpuMs, + puppeteer.cpuMs, + "less median CPU time", + "more median CPU time", + ), + ], + workflows, + }; + return benchmarkCache; +} + +function extractSection(markdown, heading) { + const marker = `## ${heading}`; + const start = markdown.indexOf(marker); + if (start < 0) fail(`missing Markdown section: ${heading}`); + const contentStart = start + marker.length; + const nextHeading = markdown.indexOf("\n## ", contentStart); + return markdown + .slice(contentStart, nextHeading < 0 ? markdown.length : nextHeading) + .trim(); +} + +function normalizeParagraph(value) { + return value + .split("\n") + .map((line) => line.trim()) + .join(" ") + .replace(/\s+/g, " ") + .trim(); +} + +function paragraphs(markdown) { + return markdown + .split(/\n\s*\n/) + .map(normalizeParagraph) + .filter( + (value) => value && !value.startsWith("```") && !value.startsWith("- "), + ); +} + +function paragraphStarting(markdown, prefix) { + const paragraph = paragraphs(markdown).find((value) => + value.startsWith(prefix), + ); + if (!paragraph) fail(`missing paragraph beginning with: ${prefix}`); + return paragraph; +} + +function fencedCode(markdown) { + const match = markdown.match(/```(?:sh)?\n([\s\S]*?)\n```/); + if (!match) fail("missing fenced command block"); + return match[1].trim(); +} + +function bulletItems(markdown) { + const items = []; + let current = ""; + for (const line of markdown.split("\n")) { + if (line.startsWith("- ")) { + if (current) items.push(normalizeParagraph(current)); + current = line.slice(2); + } else if (current && /^\s{2,}\S/.test(line)) { + current += ` ${line.trim()}`; + } else if (current && line.trim() === "") { + items.push(normalizeParagraph(current)); + current = ""; + } + } + if (current) items.push(normalizeParagraph(current)); + return items; +} + +function commandNames(usage) { + const names = []; + for (const line of usage.split("\n")) { + if (/^\s/.test(line)) continue; + for (const alternative of line.split(" | ")) { + const match = alternative.trim().match(/^([a-z][a-z-]*)/); + if (match && !names.includes(match[1])) names.push(match[1]); + } + } + return names.slice(0, 6).join(", "); +} + +function commandGroup(commandReference, title) { + const section = extractSection(commandReference, title); + const usage = fencedCode(section); + const description = bulletItems(section)[0]; + if (!description) fail(`missing command description: ${title}`); + return { title, commands: commandNames(usage), description, usage }; +} + +function markdownForDocumentation(content) { + const groups = content.commandGroups + .map( + (group) => + `### ${group.title}\n\n\`\`\`sh\n${group.usage}\n\`\`\`\n\n${group.description}`, + ) + .join("\n\n"); + return `# Headless documentation + +${content.introduction} + +## First run + +\`\`\`sh +${content.firstRunCommands} +\`\`\` + +${content.startupPresentation} + +## QA workflow + +\`\`\`sh +${content.qaWorkflowCommands} +\`\`\` + +## Command groups + +${groups} + +## Context pruning + +${content.contextPruning} + +## Scrollable page evidence + +${content.scrollableEvidence} + +## Safety + +${content.security.map((item) => `- ${item}`).join("\n")} + +## Platforms + +${content.platforms.map((item) => `- ${item}`).join("\n")}`; +} + +export function loadDocumentationContent() { + if (documentationCache) return documentationCache; + const readme = readRepositoryFile("README.md"); + const commandReference = readRepositoryFile("apps/headless/docs/COMMANDS.md"); + const workflowSection = extractSection(readme, "Agent workflow"); + const workflowCommands = fencedCode(workflowSection) + .split("\n") + .filter(Boolean); + const firstRunCommands = workflowCommands.slice(0, 4).join("\n"); + const qaPrefixes = [ + "record start", + "click", + "wait", + "record stop", + "qa report", + ]; + const qaWorkflowCommands = workflowCommands + .filter((line) => + qaPrefixes.some((prefix) => + line.startsWith(`headless --session qa ${prefix}`), + ), + ) + .join("\n"); + if ( + firstRunCommands.split("\n").length !== 4 || + qaWorkflowCommands.split("\n").length !== 5 + ) { + fail( + "README agent workflow no longer contains the expected first-run and QA sequence", + ); + } + + const content = { + introduction: paragraphStarting(readme, "Persistent browser control"), + firstRunCommands, + qaWorkflowCommands, + startupPresentation: paragraphStarting(workflowSection, "On macOS"), + contextPruning: paragraphStarting( + workflowSection, + "Inspection is progressively disclosed", + ), + scrollableEvidence: paragraphStarting( + workflowSection, + "For scrollable-page QA", + ), + commandGroups: [ + commandGroup(commandReference, "Host lifecycle"), + commandGroup(commandReference, "Navigation and interaction"), + commandGroup(commandReference, "Capture and evidence"), + commandGroup(commandReference, "Diagnostics"), + ], + security: bulletItems(extractSection(readme, "Security boundary")), + platforms: bulletItems( + readme.slice(0, readme.indexOf("## Computer use comparison")), + ).filter((item) => item.startsWith("macOS") || item.startsWith("Linux")), + }; + if (content.security.length < 3 || content.platforms.length !== 2) { + fail("README security or platform contract is incomplete"); + } + + documentationCache = { + ...content, + markdown: markdownForDocumentation(content), + }; + return documentationCache; +} + +export function validateRepositoryContent() { + const benchmark = loadBenchmarkContent(); + const documentation = loadDocumentationContent(); + return { + benchmarkCases: benchmark.workflows.length, + commandGroups: documentation.commandGroups.length, + securityRules: documentation.security.length, + }; +} diff --git a/apps/web/package.json b/apps/web/package.json index 57874bc..2b30b09 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "node scripts/validate-harness-onboarding.mjs && eslint .", + "lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && eslint .", "brand": "node scripts/render-brand.mjs" }, "dependencies": { diff --git a/apps/web/scripts/validate-content-provenance.mjs b/apps/web/scripts/validate-content-provenance.mjs new file mode 100644 index 0000000..cd3ee66 --- /dev/null +++ b/apps/web/scripts/validate-content-provenance.mjs @@ -0,0 +1,14 @@ +import { validateRepositoryContent } from "../lib/repository-content.mjs"; + +const result = validateRepositoryContent(); +if ( + result.benchmarkCases !== 4 || + result.commandGroups !== 4 || + result.securityRules < 3 +) { + throw new Error( + `content provenance validation failed: ${JSON.stringify(result)}`, + ); +} + +console.log("Repository content provenance is valid"); diff --git a/docs/roadmap/architecture-decisions.md b/docs/roadmap/architecture-decisions.md index ad0f6e9..fac94e6 100644 --- a/docs/roadmap/architecture-decisions.md +++ b/docs/roadmap/architecture-decisions.md @@ -279,6 +279,8 @@ product version). CHANGELOG generated per tag. ## 13. Web app: keep Next.js; move content to generated sources (Phase 5) +**Status:** implemented 2026-08-27. + **Decision:** keep `apps/web` on Next.js/Tailwind — no framework change. The architectural change is **content provenance**: benchmark numbers, command tables, and docs prose must be imported from repo artifacts (benchmark JSON @@ -288,6 +290,12 @@ places. Also: delete dead visual code (`side-rays.tsx`/`ogl`, unused assets), reconsider shipping two WebGL bundles for decoration, add deploy pipeline + CI, metadata/sitemap/404. Details: backlog §F. +The website now validates and derives its benchmark presentation from the +generated benchmark JSON. Its rendered and copyable documentation share the +README and generated command reference as build-time sources, with a web lint +gate that fails on missing or malformed provenance instead of preserving a +stale hand-written fallback. + ## 14. Testing architecture: promote the conformance suite (Phase 1–2) **Status:** implemented 2026-08-10. @@ -481,21 +489,21 @@ no TCP listener, fail closed, bounded everything. ## Decision log -| # | Decision | Status | Date | -| --- | ----------------------------------------------------------- | ----------------- | ---------- | -| 1 | Keep Swift core; Rust only via revisit trigger | Decided | 2026-08-04 | -| 3 | Extract HostCore + BrowserEngine, typed errors | Implemented | 2026-08-10 | -| 5 | Remote stays SSH-only; no cloud offering | Decided (owner) | 2026-08-04 | +| # | Decision | Status | Date | +| --- | ----------------------------------------------------------- | --------------------------------------------------------- | ---------- | +| 1 | Keep Swift core; Rust only via revisit trigger | Decided | 2026-08-04 | +| 3 | Extract HostCore + BrowserEngine, typed errors | Implemented | 2026-08-10 | +| 5 | Remote stays SSH-only; no cloud offering | Decided (owner) | 2026-08-04 | | 6 | Windows = stretch via Chromium engine; WSL2/Docker interim | Decided (owner); spike failed 2026-08-22, native deferred | 2026-08-04 | -| 8 | Real CDP input on Linux as capability upgrade | Implemented | 2026-08-13 | -| 12 | Version unification on git tag | Implemented | 2026-08-04 | -| 14 | Run one conformance scenario against every engine | Implemented | 2026-08-10 | -| 15 | Package-manager distribution set | Decided (owner) | 2026-08-04 | -| 16 | Preserve CLI value boundaries with `--` and shell quoting | Decided | 2026-08-10 | -| 17 | Keep full MCP surface; annotate its maximum risk | Decided | 2026-08-10 | -| 18 | Treat WebKit page diagnostics as bounded untrusted evidence | Decided | 2026-08-10 | -| 19 | Keep macOS agent startup behind the current app | Implemented | 2026-08-12 | -| 20 | Omit passkeys unless Apple provisions Developer ID release | Implemented | 2026-08-12 | -| 21 | Rust port of shared core, protocol layer first | In progress | 2026-08-22 | +| 8 | Real CDP input on Linux as capability upgrade | Implemented | 2026-08-13 | +| 12 | Version unification on git tag | Implemented | 2026-08-04 | +| 14 | Run one conformance scenario against every engine | Implemented | 2026-08-10 | +| 15 | Package-manager distribution set | Decided (owner) | 2026-08-04 | +| 16 | Preserve CLI value boundaries with `--` and shell quoting | Decided | 2026-08-10 | +| 17 | Keep full MCP surface; annotate its maximum risk | Decided | 2026-08-10 | +| 18 | Treat WebKit page diagnostics as bounded untrusted evidence | Decided | 2026-08-10 | +| 19 | Keep macOS agent startup behind the current app | Implemented | 2026-08-12 | +| 20 | Omit passkeys unless Apple provisions Developer ID release | Implemented | 2026-08-12 | +| 21 | Rust port of shared core, protocol layer first | In progress | 2026-08-22 | New decisions append here with the same format. diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index dbcb7d2..f9009e4 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -416,11 +416,15 @@ Owner-decided scope: package managers, no hosted service. reproducible and reviewable — check in the project config, document the hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped. -- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — benchmark numbers hand-copied in +- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in `app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`, `components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose triplicated across `app/docs/page.tsx`, `components/docs-markdown.ts`, and - `README.md`, already diverging. Import from generated artifacts (D5, B6). + `README.md`, already diverging. Import from generated artifacts (D5, B6).~~ + **Done:** the website validates and derives benchmark values from generated + results, while rendered and copyable docs share README and generated command + reference content. Web lint runs the provenance validator so malformed or + missing sources fail the build instead of falling back to copied claims. - **F3. Missing pages:** ([#49](https://github.com/LockInTime/headless/issues/49)) install (README's build/install section is absent from the site entirely), security model, MCP setup, full command reference (~30 commands; site lists 4 groups), changelog/version indicator, platform