Next Fixture Home
+This deterministic fixture represents a static Next.js export route for repo audit tests.
+ About +diff --git a/.gitignore b/.gitignore index cb7cee5..3f79bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ node_modules/ dist/ .env tmp/ +examples/fixture-repos/next-basic/.next/ +examples/fixture-repos/next-basic/out/ +examples/fixture-repos/astro-basic/dist/ +examples/fixture-repos/vite-basic/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb380a..e291b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ - Added `--route-list` support so repository audits can constrain generated static routes and report missing, empty, non-HTML, or outside-static route-list entries deterministically. - Added repo CI gating with `audit-repo --fail-on P0|P1|P2|P3`, including early option validation before build or preview side effects. - Added static output source findings for missing generated `robots.txt` and `sitemap.xml`. +- Added Phase C repo audit framework maturity coverage with deterministic Next.js and Astro fixtures, stable Next.js route manifest evidence, and source-level route parity findings for manifest/static-output mismatches. +- Kept Astro framework coverage limited to detection and static-output audits until an explicit integration-generated route artifact is available. - Hardened repo command guardrails so restricted mode blocks local build and preview command execution before spawning, and explicit preview options take precedence over static output so callers can audit live preview servers even when a stale `dist` directory exists. - Expanded the test suite to cover repo detection, static route discovery, repo audit orchestration, preview lifecycle behavior, CLI validation, report/schema compatibility, packaging, and release-gate hardening. diff --git a/docs/prd-deterministic-audit-cli.md b/docs/prd-deterministic-audit-cli.md index feba1a2..d04b104 100644 --- a/docs/prd-deterministic-audit-cli.md +++ b/docs/prd-deterministic-audit-cli.md @@ -857,13 +857,15 @@ Delivered developer-focused repo audit completion work: - Route-list support for repository audits, including missing, empty, non-HTML, missing-entry, and outside-static findings. - Repo config support through `audit.config.json` for repeatable CI workflows. - Vite fixture coverage for deterministic build-and-static-output audits. +- Phase C framework maturity coverage for deterministic Next.js and Astro fixture audits. +- Framework route manifest evidence for stable generated artifacts, currently Next.js prerender manifests. +- Source-level route parity findings for manifest routes missing generated HTML and generated HTML routes absent from framework route manifests. - Source-level findings for generated sitemap/robots availability, static output availability, route-list issues, build failures, and preview startup failures. Remaining developer-focused repo audit work: -- Next.js and Astro fixture coverage. -- Deeper deterministic source-level findings for framework metadata usage and rendered/source mismatches where stable. -- Optional framework-specific route manifest parsing when it can be done without brittle heuristics. +- Deeper deterministic source-level findings for framework metadata usage and rendered/source mismatches where stable framework artifacts expose metadata expectations. +- Additional framework-specific route manifest parsing only when stable generated artifacts are identified and covered by fixtures. Astro route metadata should be added through an explicit integration-generated artifact rather than inferred from an undocumented default file. ## 20. Risks and Mitigations @@ -919,9 +921,9 @@ Before publishing or tagging `0.2.0`: - Confirm readiness language remains separate from measured ranking or AI-answer visibility claims. - Push and merge the guardrail branch through the repository review workflow. -Before extending developer repo audit beyond the completed build/config/route-list layer: +Before extending developer repo audit beyond the completed build/config/route-list/framework-manifest layer: -- Add Next.js and Astro fixtures only with deterministic local build scripts and no automatic dependency installation. +- Add additional framework fixtures only with deterministic local build scripts and no automatic dependency installation. - Expand deterministic source-level findings only where source evidence can be parsed without brittle framework assumptions. - Keep repo-to-audit implementation separate from external API integrations. diff --git a/docs/superpowers/plans/2026-05-18-repo-audit-framework-maturity.md b/docs/superpowers/plans/2026-05-18-repo-audit-framework-maturity.md new file mode 100644 index 0000000..cdb99db --- /dev/null +++ b/docs/superpowers/plans/2026-05-18-repo-audit-framework-maturity.md @@ -0,0 +1,1251 @@ +# Repo Audit Framework Maturity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Expand `audit-repo` from the current Vite/generic baseline into deterministic Next.js and Astro framework fixture coverage with stable route manifest evidence and source-level route parity findings. + +**Architecture:** Keep framework-specific route evidence isolated in a new manifest module instead of growing `repo-audit.mjs`. Fixture builds stay dependency-free and explicit. Repo source findings remain separate from page findings, and manifest-derived evidence is reported under repo evidence. + +**Tech Stack:** Node.js 20+, ESM modules, built-in `node:test`, fixture repositories under `examples/fixture-repos`, no new runtime dependencies. + +--- + +## Scope + +This plan implements Phase C from `docs/superpowers/specs/2026-05-18-remaining-prd-roadmap-design.md`. + +Included: + +- Next.js fixture repository using deterministic local scripts. +- Astro fixture repository using deterministic local scripts. +- Framework route manifest parser for stable generated JSON artifacts. +- Source-level findings for manifest routes missing generated HTML and generated static routes missing from a manifest. +- Markdown report coverage for framework manifest evidence. +- Changelog and PRD status updates for the Phase C work. + +Not included: + +- Automatic dependency installation. +- Running inferred framework commands. +- Deep framework source parsing. +- Authenticated route flows. +- Search Console API, SERP API, AI-answer probes, or Lighthouse execution. +- Package publish or version bump. + +## File Structure + +- Create `packages/cli/src/repo-findings.mjs`: shared source-finding helpers for repo audit modules. +- Create `packages/cli/src/repo-manifests.mjs`: framework manifest discovery, parsing, route normalization, and route parity analysis. +- Modify `packages/cli/src/repo-audit.mjs`: import shared source finding helpers, run manifest analysis for static-output audits, include `frameworkManifests` in repo evidence. +- Modify `packages/cli/src/report.mjs`: print framework route manifest evidence. +- Modify `packages/cli/test/repo-detect.test.mjs`: assert Next.js and Astro fixture detection. +- Create `packages/cli/test/repo-manifests.test.mjs`: direct tests for manifest parsing and source findings. +- Modify `packages/cli/test/repo-audit.test.mjs`: assert Next.js and Astro repo audits integrate manifest evidence. +- Modify `packages/cli/test/report.test.mjs`: assert Markdown report includes framework manifest evidence. +- Create `examples/fixture-repos/next-basic/*`: dependency-free Next.js-like static export fixture. +- Create `examples/fixture-repos/astro-basic/*`: dependency-free Astro-like static output fixture. +- Create `examples/golden/repo-framework-summary.json`: stable summary for framework fixture audits. +- Modify `packages/cli/test/golden-fixtures.test.mjs`: assert framework repo summary. +- Modify `CHANGELOG.md`: record the Phase C unreleased user-visible change. +- Modify `docs/prd-deterministic-audit-cli.md`: move Next.js and Astro fixture coverage from remaining work to delivered Phase C work after implementation. + +--- + +## Task 1: Add Next.js And Astro Fixture Repositories + +**Files:** + +- Modify: `packages/cli/test/repo-detect.test.mjs` +- Create: `examples/fixture-repos/next-basic/package.json` +- Create: `examples/fixture-repos/next-basic/build.mjs` +- Create: `examples/fixture-repos/next-basic/routes.txt` +- Create: `examples/fixture-repos/next-basic/src/index.html` +- Create: `examples/fixture-repos/next-basic/src/about.html` +- Create: `examples/fixture-repos/astro-basic/package.json` +- Create: `examples/fixture-repos/astro-basic/build.mjs` +- Create: `examples/fixture-repos/astro-basic/routes.txt` +- Create: `examples/fixture-repos/astro-basic/src/index.html` +- Create: `examples/fixture-repos/astro-basic/src/services.html` + +- [ ] **Step 1: Write the failing fixture detection test** + +Add this test to `packages/cli/test/repo-detect.test.mjs` after the existing framework-signal test: + +```js +test("detects Next.js and Astro fixture repositories without executing scripts", () => { + const next = detectRepo(fixture("next-basic")); + assert.equal(next.packageManager, "npm"); + assert.equal(next.detectedFramework, "next"); + assert.equal(next.confidence, "high"); + assert.equal(next.buildCommand, "npm run build"); + assert.equal(next.previewCommand, null); + assert.equal(next.staticDir, null); + + const astro = detectRepo(fixture("astro-basic")); + assert.equal(astro.packageManager, "npm"); + assert.equal(astro.detectedFramework, "astro"); + assert.equal(astro.confidence, "high"); + assert.equal(astro.buildCommand, "npm run build"); + assert.equal(astro.previewCommand, null); + assert.equal(astro.staticDir, null); +}); +``` + +- [ ] **Step 2: Run the focused test and verify it fails** + +Run: + +```bash +node --test packages/cli/test/repo-detect.test.mjs +``` + +Expected: FAIL because `examples/fixture-repos/next-basic` and `examples/fixture-repos/astro-basic` do not exist yet. + +- [ ] **Step 3: Create the Next.js fixture package** + +Create `examples/fixture-repos/next-basic/package.json`: + +```json +{ + "name": "openclaw-next-basic-fixture", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} +``` + +Create `examples/fixture-repos/next-basic/src/index.html`: + +```html + + +
+This deterministic fixture represents a static Next.js export route for repo audit tests.
+ About +The about route gives the audit a second generated page and an internal-link target.
+ Home +This deterministic fixture represents an Astro static output route for repo audit tests.
+ Services +The services route gives the Astro fixture a second generated static page.
+ Home +Generated fixture content.
`); +}; + +test("reads Next.js prerender manifest and reports missing generated routes", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-next-manifest-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "about", "index.html"), "About"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "prerender-manifest.json"), + JSON.stringify({ + routes: { + "/": {}, + "/about/": {}, + "/missing/": {} + } + }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/about/", path: path.join(staticDir, "about", "index.html") } + ] + }); + + assert.deepEqual(result.frameworkManifests, [ + { + type: "next_prerender_manifest", + path: path.join(manifestDir, "prerender-manifest.json"), + routes: ["/", "/about/", "/missing/"] + } + ]); + assert.deepEqual( + result.sourceFindings.map((finding) => finding.id), + ["repo.manifest_route_missing"], + ); + assert.equal(result.sourceFindings[0].evidence, "/missing/"); +}); + +test("reads Astro manifest routes when present", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-astro-manifest-")); + const staticDir = path.join(repoPath, "dist"); + const manifestDir = path.join(repoPath, ".astro"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "services", "index.html"), "Services"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "manifest.json"), + JSON.stringify({ + routes: [ + { route: "/", type: "page" }, + { route: "/services/", type: "page" } + ] + }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "astro", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/services/", path: path.join(staticDir, "services", "index.html") } + ] + }); + + assert.deepEqual(result.frameworkManifests, [ + { + type: "astro_manifest", + path: path.join(manifestDir, "manifest.json"), + routes: ["/", "/services/"] + } + ]); + assert.deepEqual(result.sourceFindings, []); +}); + +test("reports generated static routes that are not listed in a framework manifest", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-unlisted-route-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "extra", "index.html"), "Extra"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync(path.join(manifestDir, "prerender-manifest.json"), JSON.stringify({ routes: { "/": {} } })); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/extra/", path: path.join(staticDir, "extra", "index.html") } + ] + }); + + assert.deepEqual( + result.sourceFindings.map((finding) => finding.id), + ["repo.static_route_unlisted"], + ); + assert.equal(result.sourceFindings[0].evidence, "/extra/"); +}); + +test("ignores absent framework manifests", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-no-manifest-")); + const staticDir = path.join(repoPath, "out"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [{ type: "static_html", route: "/", path: path.join(staticDir, "index.html") }] + }); + + assert.deepEqual(result.frameworkManifests, []); + assert.deepEqual(result.sourceFindings, []); +}); + +test("fails closed on malformed manifest JSON", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bad-manifest-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync(path.join(manifestDir, "prerender-manifest.json"), "{bad json"); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [{ type: "static_html", route: "/", path: path.join(staticDir, "index.html") }] + }); + + assert.deepEqual(result.frameworkManifests, []); + assert.equal(result.sourceFindings[0].id, "repo.route_manifest_invalid"); +}); +``` + +- [ ] **Step 2: Run the parser tests and verify they fail** + +Run: + +```bash +node --test packages/cli/test/repo-manifests.test.mjs +``` + +Expected: FAIL because `packages/cli/src/repo-manifests.mjs` does not exist. + +- [ ] **Step 3: Create shared repo source-finding helpers** + +Create `packages/cli/src/repo-findings.mjs`: + +```js +export const sourceFinding = ({ id, severity = "P1", message, evidence, recommendation, confidence = "high", details }) => ({ + id, + severity, + message, + evidence, + recommendation, + confidence, + ...(details ? { details } : {}), +}); +``` + +- [ ] **Step 4: Create the framework manifest parser** + +Create `packages/cli/src/repo-manifests.mjs`: + +```js +import fs from "node:fs"; +import path from "node:path"; +import { sourceFinding } from "./repo-findings.mjs"; + +const ordinalCompare = (left, right) => (left < right ? -1 : left > right ? 1 : 0); + +const normalizeRoute = (route) => { + if (typeof route !== "string") return null; + const clean = route.trim(); + if (!clean || clean.startsWith("#")) return null; + const withSlash = clean.startsWith("/") ? clean : `/${clean}`; + if (withSlash === "/") return "/"; + if (path.posix.extname(withSlash)) return withSlash; + return withSlash.endsWith("/") ? withSlash : `${withSlash}/`; +}; + +const htmlPathForRoute = (staticDir, route) => { + if (route === "/") return path.join(staticDir, "index.html"); + const normalized = route.startsWith("/") ? route.slice(1) : route; + if (normalized.endsWith(".html")) return path.join(staticDir, normalized); + return path.join(staticDir, normalized, "index.html"); +}; + +const readJsonFile = (filePath) => { + try { + return { ok: true, value: JSON.parse(fs.readFileSync(filePath, "utf8")) }; + } catch (error) { + return { ok: false, error }; + } +}; + +const uniqueSortedRoutes = (routes) => + [...new Set(routes.map(normalizeRoute).filter(Boolean))].sort((left, right) => ordinalCompare(left, right)); + +const nextPrerenderRoutes = (json) => uniqueSortedRoutes(Object.keys(json?.routes || {})); + +const astroRoutes = (json) => { + const rawRoutes = Array.isArray(json?.routes) ? json.routes : Array.isArray(json?.manifest?.routes) ? json.manifest.routes : []; + return uniqueSortedRoutes( + rawRoutes.map((entry) => { + if (typeof entry === "string") return entry; + return entry?.route || entry?.pathname || entry?.pattern; + }), + ); +}; + +const manifestConfigFor = (detectedFramework) => { + if (detectedFramework === "next") { + return [ + { + type: "next_prerender_manifest", + relativePath: path.join(".next", "prerender-manifest.json"), + routesFor: nextPrerenderRoutes, + }, + ]; + } + if (detectedFramework === "astro") { + return [ + { + type: "astro_manifest", + relativePath: path.join(".astro", "manifest.json"), + routesFor: astroRoutes, + }, + ]; + } + return []; +}; + +const invalidManifestFinding = (manifestPath, error) => + sourceFinding({ + id: "repo.route_manifest_invalid", + severity: "P2", + message: "Framework route manifest could not be parsed.", + evidence: manifestPath, + recommendation: "Regenerate the framework build artifacts and rerun the repository audit.", + confidence: "high", + details: { message: error?.message || "Invalid JSON" }, + }); + +const manifestRouteMissingFinding = (route) => + sourceFinding({ + id: "repo.manifest_route_missing", + severity: "P2", + message: "Framework route manifest lists a route that is missing generated HTML output.", + evidence: route, + recommendation: "Ensure the framework build emits static HTML for this route or remove it from the prerendered route manifest.", + confidence: "high", + }); + +const staticRouteUnlistedFinding = (route) => + sourceFinding({ + id: "repo.static_route_unlisted", + severity: "P3", + message: "Static output contains a generated HTML route that is not listed in the framework route manifest.", + evidence: route, + recommendation: "Confirm the route is intentionally generated and discoverable through sitemap or internal links.", + confidence: "medium", + }); + +export const analyzeFrameworkRouteManifests = ({ repoPath, staticDir, detectedFramework, staticRoutes = [] }) => { + const configs = manifestConfigFor(detectedFramework); + const frameworkManifests = []; + const sourceFindings = []; + + for (const config of configs) { + const manifestPath = path.join(repoPath, config.relativePath); + if (!fs.existsSync(manifestPath)) continue; + + const parsed = readJsonFile(manifestPath); + if (!parsed.ok) { + sourceFindings.push(invalidManifestFinding(manifestPath, parsed.error)); + continue; + } + + const routes = config.routesFor(parsed.value); + frameworkManifests.push({ type: config.type, path: manifestPath, routes }); + + const manifestRouteSet = new Set(routes); + const staticRouteSet = new Set(staticRoutes.map((route) => normalizeRoute(route.route)).filter(Boolean)); + + for (const route of routes) { + const htmlPath = htmlPathForRoute(staticDir, route); + if (!fs.existsSync(htmlPath) || !fs.statSync(htmlPath).isFile()) { + sourceFindings.push(manifestRouteMissingFinding(route)); + } + } + + for (const route of [...staticRouteSet].sort((left, right) => ordinalCompare(left, right))) { + if (!manifestRouteSet.has(route)) sourceFindings.push(staticRouteUnlistedFinding(route)); + } + } + + return { frameworkManifests, sourceFindings }; +}; +``` + +- [ ] **Step 5: Add new source files to the validation required-file list** + +Open `scripts/validate-skill.mjs` and add these two entries to the required files array: + +```js +"packages/cli/src/repo-findings.mjs", +"packages/cli/src/repo-manifests.mjs", +``` + +- [ ] **Step 6: Re-run parser tests and validation** + +Run: + +```bash +node --test packages/cli/test/repo-manifests.test.mjs +npm run validate +``` + +Expected: both commands PASS. + +- [ ] **Step 7: Commit Task 2** + +Run: + +```bash +git add packages/cli/src/repo-findings.mjs packages/cli/src/repo-manifests.mjs packages/cli/test/repo-manifests.test.mjs scripts/validate-skill.mjs +git commit -m "feat: parse framework route manifests" +``` + +--- + +## Task 3: Integrate Manifest Evidence Into Repo Audits + +**Files:** + +- Modify: `packages/cli/src/repo-audit.mjs` +- Modify: `packages/cli/test/repo-audit.test.mjs` + +- [ ] **Step 1: Write failing repo-audit integration tests** + +Add these tests to `packages/cli/test/repo-audit.test.mjs` after the Vite build audit test: + +```js +test("Next.js static build audit records framework manifest evidence and route parity findings", async () => { + const repoPath = fixture("next-basic"); + fs.rmSync(path.join(repoPath, "out"), { recursive: true, force: true }); + fs.rmSync(path.join(repoPath, ".next"), { recursive: true, force: true }); + + const audit = await runRepoAudit({ + repoPath, + buildCommand: "npm run build", + staticDir: "out", + maxBuildMs: 5000, + }); + + assert.equal(audit.repo.detectedFramework, "next"); + assert.equal(audit.repo.staticDirRelative, "out"); + assert.equal(audit.pages.length, 2); + assert.deepEqual(audit.repo.frameworkManifests, [ + { + type: "next_prerender_manifest", + path: path.join(repoPath, ".next", "prerender-manifest.json"), + routes: ["/", "/about/", "/missing/"] + } + ]); + assert.ok(audit.repo.sourceFindings.some((finding) => finding.id === "repo.manifest_route_missing")); +}); + +test("Astro static build audit records framework manifest evidence", async () => { + const repoPath = fixture("astro-basic"); + fs.rmSync(path.join(repoPath, "dist"), { recursive: true, force: true }); + fs.rmSync(path.join(repoPath, ".astro"), { recursive: true, force: true }); + + const audit = await runRepoAudit({ + repoPath, + buildCommand: "npm run build", + staticDir: "dist", + maxBuildMs: 5000, + }); + + assert.equal(audit.repo.detectedFramework, "astro"); + assert.equal(audit.repo.staticDirRelative, "dist"); + assert.equal(audit.pages.length, 2); + assert.deepEqual(audit.repo.frameworkManifests, [ + { + type: "astro_manifest", + path: path.join(repoPath, ".astro", "manifest.json"), + routes: ["/", "/services/"] + } + ]); + assert.ok(!audit.repo.sourceFindings.some((finding) => finding.id === "repo.manifest_route_missing")); +}); +``` + +- [ ] **Step 2: Run integration tests and verify they fail** + +Run: + +```bash +node --test packages/cli/test/repo-audit.test.mjs +``` + +Expected: FAIL because `audit.repo.frameworkManifests` is not populated yet. + +- [ ] **Step 3: Update repo-audit imports** + +In `packages/cli/src/repo-audit.mjs`, replace the local `sourceFinding` helper import area with these imports: + +```js +import { sourceFinding } from "./repo-findings.mjs"; +import { analyzeFrameworkRouteManifests } from "./repo-manifests.mjs"; +``` + +Then remove the local `const sourceFinding = ...` declaration from `repo-audit.mjs`. + +- [ ] **Step 4: Add default manifest evidence to repo evidence** + +In `repoEvidence`, add `frameworkManifests: []` before `sourceFindings: []`: + +```js +const repoEvidence = (detected, overrides = {}) => ({ + path: detected.repoRoot, + detectedFramework: detected.detectedFramework, + confidence: detected.confidence, + packageManager: detected.packageManager, + buildCommand: detected.buildCommand, + previewCommand: detected.previewCommand, + staticDir: detected.staticDir, + staticDirRelative: detected.staticDirRelative, + routeSources: detected.routeSources || [], + frameworkManifests: [], + sourceFindings: [], + notes: [], + ...overrides, +}); +``` + +- [ ] **Step 5: Analyze all static routes before route-list narrowing** + +In the static-output branch of `runRepoAudit`, replace: + +```js +const routes = routeListResult ? routeListResult.routes : discoverStaticRoutes(staticDir); +``` + +with: + +```js +const staticRoutes = discoverStaticRoutes(staticDir); +const routes = routeListResult ? routeListResult.routes : staticRoutes; +``` + +- [ ] **Step 6: Add manifest analysis to successful static audits** + +In the static-output branch, replace: + +```js +const outputSourceFindings = generatedOutputFindings(staticDir); +``` + +with: + +```js +const manifestAnalysis = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: detected.detectedFramework, + staticRoutes, +}); +const outputSourceFindings = [...generatedOutputFindings(staticDir), ...manifestAnalysis.sourceFindings]; +``` + +Then add `frameworkManifests` to the `repoEvidence` override: + +```js +frameworkManifests: manifestAnalysis.frameworkManifests, +sourceFindings: outputSourceFindings, +``` + +- [ ] **Step 7: Re-run focused repo-audit tests** + +Run: + +```bash +node --test packages/cli/test/repo-audit.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 8: Commit Task 3** + +Run: + +```bash +git add packages/cli/src/repo-audit.mjs packages/cli/test/repo-audit.test.mjs +git commit -m "feat: include framework manifest evidence in repo audits" +``` + +--- + +## Task 4: Add Report Coverage For Framework Manifests + +**Files:** + +- Modify: `packages/cli/src/report.mjs` +- Modify: `packages/cli/test/report.test.mjs` + +- [ ] **Step 1: Write failing Markdown report test** + +Add this test to `packages/cli/test/report.test.mjs`: + +```js +test("includes framework manifest evidence when present", () => { + const markdown = generateMarkdownReport({ + run: { target: "repo" }, + findings: [], + scores: {}, + integrations: {}, + evidenceGaps: [], + sources: [], + repo: { + path: "/repo", + detectedFramework: "next", + packageManager: "npm", + staticDirRelative: "out", + routeSources: [{ type: "static_html", route: "/", path: "/repo/out/index.html" }], + frameworkManifests: [ + { + type: "next_prerender_manifest", + path: "/repo/.next/prerender-manifest.json", + routes: ["/", "/about/", "/missing/"] + } + ], + sourceFindings: [] + }, + }); + + assert.match(markdown, /Framework route manifests:/); + assert.match(markdown, /next_prerender_manifest: 3 routes/); + assert.match(markdown, /\/repo\/\.next\/prerender-manifest\.json/); +}); +``` + +- [ ] **Step 2: Run report tests and verify failure** + +Run: + +```bash +node --test packages/cli/test/report.test.mjs +``` + +Expected: FAIL because Markdown does not print framework manifest evidence yet. + +- [ ] **Step 3: Update Markdown report generation** + +In `packages/cli/src/report.mjs`, after the `Repository routes:` block and before `Repository source findings:`, add: + +```js + lines.push("", "Framework route manifests:"); + if (repo.frameworkManifests?.length) { + for (const manifest of repo.frameworkManifests) { + lines.push( + `- ${formatBulletValue(manifest.type)}: ${formatBulletValue((manifest.routes || []).length)} routes from ${formatBulletValue(manifest.path)}`, + ); + } + } else { + lines.push("- None recorded."); + } +``` + +- [ ] **Step 4: Re-run report tests** + +Run: + +```bash +node --test packages/cli/test/report.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 5: Commit Task 4** + +Run: + +```bash +git add packages/cli/src/report.mjs packages/cli/test/report.test.mjs +git commit -m "feat: report framework manifest evidence" +``` + +--- + +## Task 5: Add Golden Summary Coverage For Framework Repo Audits + +**Files:** + +- Modify: `packages/cli/test/golden-fixtures.test.mjs` +- Create: `examples/golden/repo-framework-summary.json` + +- [ ] **Step 1: Write failing golden summary test** + +In `packages/cli/test/golden-fixtures.test.mjs`, add these imports: + +```js +import { runRepoAudit } from "../src/repo-audit.mjs"; +``` + +Add this helper near the existing helpers: + +```js +const frameworkRepoSummary = (audit) => ({ + framework: audit.repo.detectedFramework, + staticDirRelative: audit.repo.staticDirRelative, + pageTitles: audit.pages.map((page) => page.evidence.title), + frameworkManifests: audit.repo.frameworkManifests.map((manifest) => ({ + type: manifest.type, + routes: manifest.routes, + })), + sourceFindingIds: audit.repo.sourceFindings.map((finding) => finding.id), +}); +``` + +Add this test: + +```js +test("framework repo audit golden summary matches fixtures", async () => { + const nextRepo = path.resolve("examples/fixture-repos/next-basic"); + const astroRepo = path.resolve("examples/fixture-repos/astro-basic"); + const nextAudit = await runRepoAudit({ + repoPath: nextRepo, + buildCommand: "npm run build", + staticDir: "out", + maxBuildMs: 5000, + }); + const astroAudit = await runRepoAudit({ + repoPath: astroRepo, + buildCommand: "npm run build", + staticDir: "dist", + maxBuildMs: 5000, + }); + const expected = JSON.parse(fs.readFileSync("examples/golden/repo-framework-summary.json", "utf8")); + + assert.deepEqual( + { + next: frameworkRepoSummary(nextAudit), + astro: frameworkRepoSummary(astroAudit), + }, + expected, + ); +}); +``` + +- [ ] **Step 2: Run golden test and verify failure** + +Run: + +```bash +node --test packages/cli/test/golden-fixtures.test.mjs +``` + +Expected: FAIL because `examples/golden/repo-framework-summary.json` does not exist. + +- [ ] **Step 3: Add framework golden summary** + +Create `examples/golden/repo-framework-summary.json`: + +```json +{ + "next": { + "framework": "next", + "staticDirRelative": "out", + "pageTitles": [ + "Next Fixture Home", + "Next Fixture About" + ], + "frameworkManifests": [ + { + "type": "next_prerender_manifest", + "routes": [ + "/", + "/about/", + "/missing/" + ] + } + ], + "sourceFindingIds": [ + "repo.manifest_route_missing" + ] + }, + "astro": { + "framework": "astro", + "staticDirRelative": "dist", + "pageTitles": [ + "Astro Fixture Home", + "Astro Fixture Services" + ], + "frameworkManifests": [ + { + "type": "astro_manifest", + "routes": [ + "/", + "/services/" + ] + } + ], + "sourceFindingIds": [] + } +} +``` + +- [ ] **Step 4: Re-run golden test** + +Run: + +```bash +node --test packages/cli/test/golden-fixtures.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 5: Commit Task 5** + +Run: + +```bash +git add packages/cli/test/golden-fixtures.test.mjs examples/golden/repo-framework-summary.json +git commit -m "test: add framework repo golden summary" +``` + +--- + +## Task 6: Update Docs And Changelog + +**Files:** + +- Modify: `CHANGELOG.md` +- Modify: `docs/prd-deterministic-audit-cli.md` + +- [ ] **Step 1: Update changelog** + +In `CHANGELOG.md`, under the current `Unreleased` section, add: + +```md +- Added Phase C repo audit framework maturity coverage with deterministic Next.js and Astro fixtures, framework route manifest evidence, and source-level route parity findings for manifest/static-output mismatches. +``` + +- [ ] **Step 2: Update PRD delivered/remaining status** + +In `docs/prd-deterministic-audit-cli.md`, under `Delivered developer-focused repo audit completion work:`, add: + +```md +- Phase C framework maturity coverage for deterministic Next.js and Astro fixture audits. +- Framework route manifest evidence for stable generated artifacts. +- Source-level route parity findings for manifest routes missing generated HTML and generated HTML routes absent from framework route manifests. +``` + +Under `Remaining developer-focused repo audit work:`, replace: + +```md +- Next.js and Astro fixture coverage. +- Deeper deterministic source-level findings for framework metadata usage and rendered/source mismatches where stable. +- Optional framework-specific route manifest parsing when it can be done without brittle heuristics. +``` + +with: + +```md +- Deeper deterministic source-level findings for framework metadata usage and rendered/source mismatches where stable framework artifacts expose metadata expectations. +- Additional framework-specific route manifest parsing only when stable generated artifacts are identified and covered by fixtures. +``` + +- [ ] **Step 3: Check docs wording** + +Run: + +```bash +rg -n "Next.js and Astro fixture coverage|Optional framework-specific route manifest parsing|Phase C framework maturity" docs/prd-deterministic-audit-cli.md CHANGELOG.md +``` + +Expected: output includes the new delivered Phase C language and does not include the old standalone `Next.js and Astro fixture coverage` remaining-work bullet. + +- [ ] **Step 4: Commit Task 6** + +Run: + +```bash +git add CHANGELOG.md docs/prd-deterministic-audit-cli.md +git commit -m "docs: record framework repo audit maturity" +``` + +--- + +## Task 7: Full Verification And Final Cleanup + +**Files:** + +- Validate working tree and all changed behavior. + +- [ ] **Step 1: Run focused repo test suite** + +Run: + +```bash +node --test packages/cli/test/repo-detect.test.mjs packages/cli/test/repo-manifests.test.mjs packages/cli/test/repo-audit.test.mjs packages/cli/test/report.test.mjs packages/cli/test/golden-fixtures.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 2: Run full test suite** + +Run: + +```bash +npm test +``` + +Expected: PASS. + +- [ ] **Step 3: Run skill validation** + +Run: + +```bash +npm run validate +``` + +Expected: PASS with `"ok": true`. + +- [ ] **Step 4: Run diff hygiene check** + +Run: + +```bash +git diff --check +``` + +Expected: no output and exit code 0. + +- [ ] **Step 5: Inspect final status** + +Run: + +```bash +git status --short --branch +``` + +Expected: branch shows only the expected ahead count and no uncommitted files. + +- [ ] **Step 6: Commit any verification-only documentation correction** + +If Step 1 through Step 4 expose a documentation wording mismatch only, make the smallest docs correction, then run: + +```bash +git add CHANGELOG.md docs/prd-deterministic-audit-cli.md +git commit -m "docs: clarify framework audit status" +``` + +Skip this step when no correction is needed. + +--- + +## Completion Checklist + +- [ ] Next.js fixture detection passes without executing scripts. +- [ ] Astro fixture detection passes without executing scripts. +- [ ] Next.js and Astro fixture builds are dependency-free local Node scripts. +- [ ] Framework manifest parser has direct unit coverage. +- [ ] Repo audit output includes `repo.frameworkManifests`. +- [ ] Repo source findings include manifest/static parity findings. +- [ ] Markdown report includes framework manifest evidence. +- [ ] Golden summary covers Next.js and Astro framework repo audits. +- [ ] PRD and changelog reflect Phase C implementation. +- [ ] `npm test` passes. +- [ ] `npm run validate` passes. +- [ ] `git diff --check` passes. +- [ ] Working tree is clean. diff --git a/docs/superpowers/specs/2026-05-18-remaining-prd-roadmap-design.md b/docs/superpowers/specs/2026-05-18-remaining-prd-roadmap-design.md new file mode 100644 index 0000000..460b487 --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-remaining-prd-roadmap-design.md @@ -0,0 +1,255 @@ +# Remaining PRD Roadmap Design + +Date: 2026-05-18 +Repository: openclaw-geo-seo-audit-skill +Status: Approved roadmap direction; awaiting user review before implementation planning + +## Purpose + +This spec plans the remaining work in the deterministic GEO/SEO audit PRD after release stabilization, initial repo-to-audit mode, and developer repo audit completion have landed on `main`. + +The product target remains a deterministic audit CLI plus OpenClaw skill wrapper. The CLI should be the source of evidence. The skill wrapper should interpret CLI evidence, explain priorities, cite sources, and avoid inventing findings. The product should audit websites from URLs, local apps, static output, and source repositories for SEO and GEO readiness. It should only report measured rankings, SERP visibility, or AI-answer visibility when supplied evidence or approved integrations provide those measurements. + +## Current Completion State + +The full PRD is approximately 75-80 percent complete. + +Completed or substantially complete: + +- Deterministic CLI package. +- OpenClaw skill wrapper. +- Local HTML, live URL, URL-list, sitemap-seeded, bounded crawl, static-output, and explicit preview-server audits. +- Raw and optional rendered evidence collection. +- Robots, sitemap, metadata, heading, canonical, link, structured-data, performance-import, and GEO/entity baseline rules. +- JSON and Markdown report output. +- Implementation-task metadata on findings. +- Restricted security guardrails. +- Search Console CSV, SERP JSON, AI-answer JSON, and Lighthouse JSON evidence imports. +- `detect-repo` and `audit-repo` baseline. +- Developer repo audit support for explicit build commands, route lists, repo config, CI threshold failures, and deterministic source-level findings. + +Remaining work should focus on shippable phases rather than one large implementation plan. + +## Approved Approach + +Use a release-sequenced roadmap. + +Each phase should produce working, testable behavior and should be planned separately before implementation. This preserves determinism, keeps changes reviewable, and avoids mixing source-repo execution, rule heuristics, external APIs, and package-release concerns in the same plan. + +The remaining sequence is: + +1. Phase C: Repo Audit Framework Maturity. +2. Phase D: Deterministic Rule Depth. +3. Phase E: Reporting And Skill Polish. +4. Phase F: External Evidence Integrations. +5. Phase G: Release Packaging And Version Readiness. + +## Phase C: Repo Audit Framework Maturity + +### Goal + +Make `audit-repo` credible across the next most important web framework workflows while preserving the explicit-execution safety model. + +### Scope + +Phase C should add deterministic framework fixture coverage and source-output parity findings for common developer repo audits. + +Required work: + +- Add Next.js fixture coverage with deterministic local build scripts and no automatic dependency installation. +- Add Astro fixture coverage with deterministic local build scripts and no automatic dependency installation. +- Add route manifest parsing only for stable, framework-owned generated artifacts. +- Add source-level findings for route manifest gaps, missing generated routes, static output omissions, and stable metadata/rendered-output mismatches. +- Keep explicit preview and explicit static output precedence over inferred framework paths. +- Keep restricted mode blocking local build and preview command execution. + +Out of scope: + +- Automatic `npm install`, `pnpm install`, or `yarn install`. +- Deep arbitrary framework source parsing. +- Authenticated routes. +- Hosted deployment provider integrations. +- Search Console, SERP, AI-answer, or Lighthouse execution integrations. + +### Acceptance Criteria + +- `npm test` includes passing Next.js and Astro repo-audit fixture coverage. +- Fixture audits demonstrate static output route discovery, route-list parity, and source-level repo findings. +- Manifest parsing is guarded by fixture tests and fails closed when artifacts are absent or malformed. +- Existing Vite and generic static fixture behavior remains compatible. +- Markdown and JSON output continue to separate repo source findings from rendered page findings. + +## Phase D: Deterministic Rule Depth + +### Goal + +Expand high-value SEO/GEO rule coverage where findings can be derived from observed evidence without LLM inference. + +### Scope + +Phase D should prioritize rule IDs already represented in the PRD or taxonomy but not yet fully triggered. + +Required work: + +- Entity clarity beyond about/contact link presence. +- Hidden text and policy-risk indicators. +- Duplicate content clusters beyond duplicate title and description checks. +- Structured-data visible-content mismatch. +- Internal linking and topic relationship checks. +- More nuanced answerability and helpful-content heuristics. +- Rendered primary-content missing or materially thinner than raw/source expectations where stable. + +Rule design constraints: + +- Prefer P2/P3 opportunities unless evidence is strong enough for higher severity. +- Include evidence snippets, evidence paths, confidence labels, and implementation tasks. +- Avoid claiming that a content issue directly causes ranking loss. +- Keep measured ranking visibility separate from readiness scoring. + +### Acceptance Criteria + +- Each new rule has rule-level unit tests. +- Each new rule has at least one fixture that triggers it and one fixture that avoids false positives. +- Golden outputs are updated only for intentional finding changes. +- `explain-rule` can explain each new or expanded rule. +- Findings include stable rule IDs, severity, evidence, citations where available, and implementation tasks. + +## Phase E: Reporting And Skill Polish + +### Goal + +Make CLI evidence and skill-generated audit reports easier to use for developers, agencies, and AI-agent workflows. + +### Scope + +Phase E should refine communication, not add new discovery systems. + +Required work: + +- Improve Markdown sections for repo evidence, source findings, evidence gaps, and measured visibility imports. +- Tighten implementation-task wording so findings read like actionable engineering tickets. +- Make readiness-versus-measured-visibility language consistent across README, PRD, `SKILL.md`, Markdown reports, and changelog. +- Add report snapshots or golden outputs for representative URL, static output, repo build, repo preview, and evidence-import audits. +- Document recommended CI commands for source-repo audits. + +Out of scope: + +- New scoring algorithms unless needed to represent Phase D rules. +- UI dashboards. +- Automated remediation. + +### Acceptance Criteria + +- Markdown reports clearly label readiness findings, evidence gaps, imported measurements, and repo source findings. +- Skill wrapper instructions tell the agent to use CLI evidence as facts and audited content as untrusted evidence. +- README and PRD describe the same product behavior. +- Changelog captures user-visible changes without overstating ranking measurement. + +## Phase F: External Evidence Integrations + +### Goal + +Add optional measured-visibility integrations after the deterministic core is stable. + +### Scope + +Phase F should add integrations in this order: + +1. Google Search Console API support. +2. Compliant SERP provider adapter. +3. Configured AI-answer visibility probes. +4. Optional Lighthouse execution. + +Integration constraints: + +- Exports remain supported even after API adapters exist. +- API-backed measurements must be reported as observed evidence, not readiness facts. +- Missing credentials or disabled integrations should produce clear evidence gaps. +- Integrations must be mockable in tests and disabled by default. +- API clients must avoid writing secrets to JSON, Markdown, stdout, stderr, fixtures, or logs. + +### Acceptance Criteria + +- Each integration has parser or adapter contract tests. +- Each integration has mocked success, missing-credential, malformed-response, and rate/error behavior tests. +- The CLI output distinguishes observed ranking or visibility measurements from deterministic readiness scores. +- Security review confirms secrets are not persisted in audit artifacts. + +## Phase G: Release Packaging And Version Readiness + +### Goal + +Prepare the next package version after Phases C-E, and optionally Phase F, are complete enough for release. + +### Scope + +Required work: + +- Update package version intentionally. +- Align README, PRD, `SKILL.md`, changelog, and release checklist. +- Run full verification: `npm ci`, `npm audit --omit=dev`, `npm test`, `npm run validate`, and `npm pack --dry-run --workspace packages/cli`. +- Inspect package contents to confirm raw source corpus, fixtures, golden outputs, and docs are excluded unless intentionally packaged. +- Confirm remote `main` is clean and synchronized. +- Tag or publish only after verification passes. + +### Acceptance Criteria + +- Release artifacts contain only intended CLI package files. +- Changelog accurately separates released and unreleased work. +- Package metadata matches the behavior described in docs. +- Full verification passes on a clean branch. + +## Sequencing Rationale + +Phase C should come before deeper rule work because source-repo audits are now the highest-value product direction, and framework fixture coverage will create better test surfaces for future source/render parity rules. + +Phase D should come before external integrations because it improves the deterministic product without credentials, paid APIs, or non-deterministic probes. + +Phase E should follow rule-depth work so the reports and skill language can reflect the expanded finding model. + +Phase F should wait until deterministic coverage is stable. Integrations create new operational risks, credential concerns, and wording risks around measured visibility. + +Phase G should happen only after the desired feature bundle is chosen and verified. + +## Implementation Planning Strategy + +Do not write one implementation plan for all remaining phases. + +Recommended next plan: + +- `docs/superpowers/plans/YYYY-MM-DD-repo-audit-framework-maturity.md` +- Source spec: this document, Phase C only. +- Execution style: subagent-driven development, with independent workers for fixtures, manifest parsing, findings, and report/golden updates where file ownership can be separated. + +Later plans should be written only after the prior phase is verified and committed: + +- Deterministic rule depth plan. +- Reporting and skill polish plan. +- External evidence integrations plan. +- Release packaging plan. + +## Risks And Mitigations + +Risk: Framework heuristics become brittle. + +Mitigation: Parse stable generated artifacts first, use fixtures, and fail closed when evidence is absent. + +Risk: Rule-depth work creates false positives. + +Mitigation: Use conservative severities, confidence labels, positive and negative fixtures, and evidence snippets. + +Risk: External integrations blur readiness and measurement. + +Mitigation: Keep imported or API-backed visibility in observed-evidence sections and keep readiness scores deterministic. + +Risk: Repo command execution expands the security surface. + +Mitigation: Keep command execution explicit, bounded, disabled in restricted mode, and covered by cleanup tests. + +Risk: Release documentation drifts from behavior. + +Mitigation: Treat README, PRD, `SKILL.md`, changelog, validation, tests, and package dry-run as the release gate. + +## User Review Gate + +After this spec is committed, the user should review it before implementation planning starts. Once approved, the next Superpowers step is to use the writing-plans skill for Phase C only. diff --git a/examples/fixture-repos/astro-basic/build.mjs b/examples/fixture-repos/astro-basic/build.mjs new file mode 100644 index 0000000..9197856 --- /dev/null +++ b/examples/fixture-repos/astro-basic/build.mjs @@ -0,0 +1,19 @@ +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const src = path.join(root, "src"); +const dist = path.join(root, "dist"); + +fs.rmSync(dist, { recursive: true, force: true }); +fs.mkdirSync(path.join(dist, "services"), { recursive: true }); + +fs.copyFileSync(path.join(src, "index.html"), path.join(dist, "index.html")); +fs.copyFileSync(path.join(src, "services.html"), path.join(dist, "services", "index.html")); +fs.writeFileSync(path.join(dist, "robots.txt"), "User-agent: *\nAllow: /\n"); +fs.writeFileSync( + path.join(dist, "sitemap.xml"), + 'This deterministic fixture represents an Astro static output route for repo audit tests.
+ Services +The services route gives the Astro fixture a second generated static page.
+ Home +The about route gives the audit a second generated page and an internal-link target.
+ Home +This deterministic fixture represents a static Next.js export route for repo audit tests.
+ About +Enough generated content.
", + ); + fs.chmodSync(blockedDir, 0); + + try { + const audit = await runRepoAudit({ + repoPath, + staticDir, + routeList: path.join(repoPath, "missing-routes.txt"), + }); + + assert.equal(audit.pages.length, 0); + assert.equal(audit.repo.sourceFindings[0].id, "repo.route_list_missing"); + } finally { + fs.chmodSync(blockedDir, 0o700); + fs.rmSync(repoPath, { recursive: true, force: true }); + } +}); + test("repo audit reports missing route-list entries", async () => { const repoPath = fixture("vite-basic"); const routeList = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-routes-")), "routes.txt"); diff --git a/packages/cli/test/repo-detect.test.mjs b/packages/cli/test/repo-detect.test.mjs index 8a35668..a1755e5 100644 --- a/packages/cli/test/repo-detect.test.mjs +++ b/packages/cli/test/repo-detect.test.mjs @@ -82,3 +82,21 @@ test("detects declared framework signals without executing scripts", () => { assert.equal(result.buildCommand, "npm run build"); assert.equal(result.previewCommand, "npm run preview"); }); + +test("detects Next.js and Astro fixture repositories without executing scripts", () => { + const next = detectRepo(fixture("next-basic")); + assert.equal(next.packageManager, "npm"); + assert.equal(next.detectedFramework, "next"); + assert.equal(next.confidence, "high"); + assert.equal(next.buildCommand, "npm run build"); + assert.equal(next.previewCommand, null); + assert.equal(next.staticDir, null); + + const astro = detectRepo(fixture("astro-basic")); + assert.equal(astro.packageManager, "npm"); + assert.equal(astro.detectedFramework, "astro"); + assert.equal(astro.confidence, "high"); + assert.equal(astro.buildCommand, "npm run build"); + assert.equal(astro.previewCommand, null); + assert.equal(astro.staticDir, null); +}); diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs new file mode 100644 index 0000000..965a25d --- /dev/null +++ b/packages/cli/test/repo-manifests.test.mjs @@ -0,0 +1,251 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { analyzeFrameworkRouteManifests } from "../src/repo-manifests.mjs"; + +const writeHtml = (file, title) => { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `Generated fixture content.
`); +}; + +test("reads Next.js prerender manifest and reports missing generated routes", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-next-manifest-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "about", "index.html"), "About"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "prerender-manifest.json"), + JSON.stringify({ + routes: { + "/": {}, + "/about/": {}, + "/missing/": {} + } + }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/about/", path: path.join(staticDir, "about", "index.html") } + ] + }); + + assert.deepEqual(result.frameworkManifests, [ + { + type: "next_prerender_manifest", + path: path.join(manifestDir, "prerender-manifest.json"), + routes: ["/", "/about/", "/missing/"] + } + ]); + assert.deepEqual( + result.sourceFindings.map((finding) => finding.id), + ["repo.manifest_route_missing"], + ); + assert.equal(result.sourceFindings[0].evidence, "/missing/"); +}); + +test("does not treat Astro fixture-only metadata as a framework route manifest", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-astro-unowned-manifest-")); + const staticDir = path.join(repoPath, "dist"); + const manifestDir = path.join(repoPath, ".astro"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "services", "index.html"), "Services"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "manifest.json"), + JSON.stringify({ + routes: [ + { route: "/", type: "page" }, + { route: "/services/", type: "page" } + ] + }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "astro", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/services/", path: path.join(staticDir, "services", "index.html") } + ] + }); + + assert.deepEqual(result.frameworkManifests, []); + assert.deepEqual(result.sourceFindings, []); +}); + +test("matches extensionless manifest routes to generated html files", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-extensionless-route-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "about.html"), "About"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "prerender-manifest.json"), + JSON.stringify({ routes: { "/": {}, "/about": {} } }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/about.html", path: path.join(staticDir, "about.html") } + ] + }); + + assert.deepEqual(result.frameworkManifests, [ + { + type: "next_prerender_manifest", + path: path.join(manifestDir, "prerender-manifest.json"), + routes: ["/", "/about/"] + } + ]); + assert.deepEqual(result.sourceFindings, []); +}); + +test("reports generated static routes that are not listed in a framework manifest", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-unlisted-route-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "extra", "index.html"), "Extra"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync(path.join(manifestDir, "prerender-manifest.json"), JSON.stringify({ routes: { "/": {} } })); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/extra/", path: path.join(staticDir, "extra", "index.html") } + ] + }); + + assert.deepEqual( + result.sourceFindings.map((finding) => finding.id), + ["repo.static_route_unlisted"], + ); + assert.equal(result.sourceFindings[0].evidence, "/extra/"); +}); + +test("ignores valid Next.js manifest files with unrecognized route schema", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-next-unknown-schema-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + writeHtml(path.join(staticDir, "extra", "index.html"), "Extra"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync(path.join(manifestDir, "prerender-manifest.json"), JSON.stringify({ notRoutes: {} })); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/extra/", path: path.join(staticDir, "extra", "index.html") } + ] + }); + + assert.deepEqual(result.frameworkManifests, []); + assert.deepEqual(result.sourceFindings, []); +}); + +test("keeps manifest route file checks inside the static directory", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-contained-manifest-route-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(repoPath, "outside", "index.html"), "Outside"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync(path.join(manifestDir, "prerender-manifest.json"), JSON.stringify({ routes: { "/../outside/": {} } })); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [] + }); + + assert.deepEqual( + result.sourceFindings.map((finding) => finding.id), + ["repo.manifest_route_missing"], + ); + assert.equal(result.sourceFindings[0].evidence, "/../outside/"); + assert.ok(!JSON.stringify(result).includes(path.join(repoPath, "outside"))); +}); + +test("checks manifest route presence against generated HTML files", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-missing-file-route-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "prerender-manifest.json"), + JSON.stringify({ routes: { "/": {}, "/missing/": {} } }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [ + { type: "static_html", route: "/", path: path.join(staticDir, "index.html") }, + { type: "static_html", route: "/missing/", path: path.join(staticDir, "missing", "index.html") } + ] + }); + + assert.deepEqual( + result.sourceFindings.map((finding) => finding.id), + ["repo.manifest_route_missing"], + ); + assert.equal(result.sourceFindings[0].evidence, "/missing/"); +}); + +test("ignores absent framework manifests", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-no-manifest-")); + const staticDir = path.join(repoPath, "out"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [{ type: "static_html", route: "/", path: path.join(staticDir, "index.html") }] + }); + + assert.deepEqual(result.frameworkManifests, []); + assert.deepEqual(result.sourceFindings, []); +}); + +test("fails closed on malformed manifest JSON", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bad-manifest-")); + const staticDir = path.join(repoPath, "out"); + const manifestDir = path.join(repoPath, ".next"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync(path.join(manifestDir, "prerender-manifest.json"), "{bad json"); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "next", + staticRoutes: [{ type: "static_html", route: "/", path: path.join(staticDir, "index.html") }] + }); + + assert.deepEqual(result.frameworkManifests, []); + assert.equal(result.sourceFindings[0].id, "repo.route_manifest_invalid"); +}); diff --git a/packages/cli/test/report.test.mjs b/packages/cli/test/report.test.mjs index e43b58c..c15da3f 100644 --- a/packages/cli/test/report.test.mjs +++ b/packages/cli/test/report.test.mjs @@ -114,3 +114,33 @@ test("includes repository build evidence when present", () => { assert.match(markdown, /Build exit code: 0/); assert.match(markdown, /Route list: \/repo\/routes.txt/); }); + +test("includes framework manifest evidence when present", () => { + const markdown = generateMarkdownReport({ + run: { target: "repo" }, + findings: [], + scores: {}, + integrations: {}, + evidenceGaps: [], + sources: [], + repo: { + path: "/repo", + detectedFramework: "next", + packageManager: "npm", + staticDirRelative: "out", + routeSources: [{ type: "static_html", route: "/", path: "/repo/out/index.html" }], + frameworkManifests: [ + { + type: "next_prerender_manifest", + path: "/repo/.next/prerender-manifest.json", + routes: ["/", "/about/", "/missing/"], + }, + ], + sourceFindings: [], + }, + }); + + assert.match(markdown, /Framework route manifests:/); + assert.match(markdown, /next_prerender_manifest: 3 routes/); + assert.match(markdown, /\/repo\/\.next\/prerender-manifest\.json/); +}); diff --git a/scripts/validate-skill.mjs b/scripts/validate-skill.mjs index 4e2e5d9..79e87a6 100644 --- a/scripts/validate-skill.mjs +++ b/scripts/validate-skill.mjs @@ -8,6 +8,7 @@ const requiredFiles = [ "docs/prd-deterministic-audit-cli.md", "docs/release-checklist.md", "docs/superpowers/plans/2026-05-18-developer-repo-audit-completion.md", + "docs/superpowers/plans/2026-05-18-repo-audit-framework-maturity.md", "docs/superpowers/specs/2026-05-18-developer-repo-audit-completion-design.md", "packages/cli/package.json", "packages/cli/src/index.mjs", @@ -26,6 +27,8 @@ const requiredFiles = [ "packages/cli/src/crawl.mjs", "packages/cli/src/repo-audit.mjs", "packages/cli/src/repo-detect.mjs", + "packages/cli/src/repo-findings.mjs", + "packages/cli/src/repo-manifests.mjs", "packages/cli/src/repo-process.mjs", "packages/cli/src/repo-routes.mjs", "packages/cli/src/rule-engine.mjs", @@ -58,6 +61,7 @@ const requiredFiles = [ "examples/fixture-sites/known-issues/sitemap.xml", "packages/cli/test/repo-audit.test.mjs", "packages/cli/test/repo-detect.test.mjs", + "packages/cli/test/repo-manifests.test.mjs", "packages/cli/test/repo-process.test.mjs", "packages/cli/test/repo-routes.test.mjs", "examples/fixture-repos/static-basic/dist/index.html", @@ -73,6 +77,17 @@ const requiredFiles = [ "examples/fixture-repos/vite-basic/src/index.html", "examples/fixture-repos/vite-basic/src/about.html", "examples/fixture-repos/vite-basic/routes.txt", + "examples/fixture-repos/next-basic/package.json", + "examples/fixture-repos/next-basic/build.mjs", + "examples/fixture-repos/next-basic/routes.txt", + "examples/fixture-repos/next-basic/src/index.html", + "examples/fixture-repos/next-basic/src/about.html", + "examples/fixture-repos/astro-basic/package.json", + "examples/fixture-repos/astro-basic/build.mjs", + "examples/fixture-repos/astro-basic/routes.txt", + "examples/fixture-repos/astro-basic/src/index.html", + "examples/fixture-repos/astro-basic/src/services.html", + "examples/golden/repo-framework-summary.json", "examples/golden/repo-static-summary.json", "examples/golden/known-issues-summary.json", "examples/golden/known-issues-report.md",