From 5c872d02cf7c69fb7a977e23098fcf1b32fefd35 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Mon, 18 May 2026 13:28:39 -0500 Subject: [PATCH 01/15] docs: plan remaining PRD roadmap --- ...2026-05-18-remaining-prd-roadmap-design.md | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-18-remaining-prd-roadmap-design.md 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. From 2993ae1bca32484ae333c22e4723ea89381656cc Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Mon, 18 May 2026 13:35:29 -0500 Subject: [PATCH 02/15] docs: plan repo audit framework maturity --- ...026-05-18-repo-audit-framework-maturity.md | 1251 +++++++++++++++++ 1 file changed, 1251 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-18-repo-audit-framework-maturity.md 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 + + + + Next Fixture Home + + + + +
+

Next Fixture Home

+

This deterministic fixture represents a static Next.js export route for repo audit tests.

+ About +
+ + +``` + +Create `examples/fixture-repos/next-basic/src/about.html`: + +```html + + + + Next Fixture About + + + + +
+

Next Fixture About

+

The about route gives the audit a second generated page and an internal-link target.

+ Home +
+ + +``` + +Create `examples/fixture-repos/next-basic/routes.txt`: + +```text +/ +/about/ +``` + +Create `examples/fixture-repos/next-basic/build.mjs`: + +```js +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const src = path.join(root, "src"); +const out = path.join(root, "out"); +const nextDir = path.join(root, ".next"); + +fs.rmSync(out, { recursive: true, force: true }); +fs.rmSync(nextDir, { recursive: true, force: true }); +fs.mkdirSync(path.join(out, "about"), { recursive: true }); +fs.mkdirSync(nextDir, { recursive: true }); + +fs.copyFileSync(path.join(src, "index.html"), path.join(out, "index.html")); +fs.copyFileSync(path.join(src, "about.html"), path.join(out, "about", "index.html")); +fs.writeFileSync(path.join(out, "robots.txt"), "User-agent: *\nAllow: /\n"); +fs.writeFileSync( + path.join(out, "sitemap.xml"), + 'https://example.test/https://example.test/about/\n', +); +fs.writeFileSync( + path.join(nextDir, "prerender-manifest.json"), + `${JSON.stringify( + { + version: 4, + routes: { + "/": { initialRevalidateSeconds: false, srcRoute: null, dataRoute: null }, + "/about/": { initialRevalidateSeconds: false, srcRoute: null, dataRoute: null }, + "/missing/": { initialRevalidateSeconds: false, srcRoute: null, dataRoute: null } + }, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: { previewModeId: "fixture", previewModeSigningKey: "fixture", previewModeEncryptionKey: "fixture" } + }, + null, + 2, + )}\n`, +); + +console.log("next fixture build complete"); +``` + +- [ ] **Step 4: Create the Astro fixture package** + +Create `examples/fixture-repos/astro-basic/package.json`: + +```json +{ + "name": "openclaw-astro-basic-fixture", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs" + }, + "dependencies": { + "astro": "^5.0.0" + } +} +``` + +Create `examples/fixture-repos/astro-basic/src/index.html`: + +```html + + + + Astro Fixture Home + + + + +
+

Astro Fixture Home

+

This deterministic fixture represents an Astro static output route for repo audit tests.

+ Services +
+ + +``` + +Create `examples/fixture-repos/astro-basic/src/services.html`: + +```html + + + + Astro Fixture Services + + + + +
+

Astro Fixture Services

+

The services route gives the Astro fixture a second generated static page.

+ Home +
+ + +``` + +Create `examples/fixture-repos/astro-basic/routes.txt`: + +```text +/ +/services/ +``` + +Create `examples/fixture-repos/astro-basic/build.mjs`: + +```js +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"); +const astroDir = path.join(root, ".astro"); + +fs.rmSync(dist, { recursive: true, force: true }); +fs.rmSync(astroDir, { recursive: true, force: true }); +fs.mkdirSync(path.join(dist, "services"), { recursive: true }); +fs.mkdirSync(astroDir, { 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"), + 'https://example.test/https://example.test/services/\n', +); +fs.writeFileSync( + path.join(astroDir, "manifest.json"), + `${JSON.stringify( + { + routes: [ + { route: "/", type: "page" }, + { route: "/services/", type: "page" } + ], + assets: [] + }, + null, + 2, + )}\n`, +); + +console.log("astro fixture build complete"); +``` + +- [ ] **Step 5: Re-run detection test and verify it passes** + +Run: + +```bash +node --test packages/cli/test/repo-detect.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 6: Commit Task 1** + +Run: + +```bash +git add packages/cli/test/repo-detect.test.mjs examples/fixture-repos/next-basic examples/fixture-repos/astro-basic +git commit -m "test: add framework repo fixtures" +``` + +--- + +## Task 2: Add Framework Route Manifest Parser + +**Files:** + +- Create: `packages/cli/test/repo-manifests.test.mjs` +- Create: `packages/cli/src/repo-findings.mjs` +- Create: `packages/cli/src/repo-manifests.mjs` +- Modify: `scripts/validate-skill.mjs` + +- [ ] **Step 1: Write failing manifest parser tests** + +Create `packages/cli/test/repo-manifests.test.mjs`: + +```js +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, `${title}

${title}

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. From bdf8d3d41034cefed7a034280fd0a078f32a2eb3 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:33:14 -0500 Subject: [PATCH 03/15] test: add framework repo fixtures --- examples/fixture-repos/astro-basic/build.mjs | 36 +++++++++++++++++ .../fixture-repos/astro-basic/package.json | 11 +++++ examples/fixture-repos/astro-basic/routes.txt | 2 + .../fixture-repos/astro-basic/src/index.html | 15 +++++++ .../astro-basic/src/services.html | 15 +++++++ examples/fixture-repos/next-basic/build.mjs | 40 +++++++++++++++++++ .../fixture-repos/next-basic/package.json | 13 ++++++ examples/fixture-repos/next-basic/routes.txt | 2 + .../fixture-repos/next-basic/src/about.html | 15 +++++++ .../fixture-repos/next-basic/src/index.html | 15 +++++++ packages/cli/test/repo-detect.test.mjs | 18 +++++++++ 11 files changed, 182 insertions(+) create mode 100644 examples/fixture-repos/astro-basic/build.mjs create mode 100644 examples/fixture-repos/astro-basic/package.json create mode 100644 examples/fixture-repos/astro-basic/routes.txt create mode 100644 examples/fixture-repos/astro-basic/src/index.html create mode 100644 examples/fixture-repos/astro-basic/src/services.html create mode 100644 examples/fixture-repos/next-basic/build.mjs create mode 100644 examples/fixture-repos/next-basic/package.json create mode 100644 examples/fixture-repos/next-basic/routes.txt create mode 100644 examples/fixture-repos/next-basic/src/about.html create mode 100644 examples/fixture-repos/next-basic/src/index.html diff --git a/examples/fixture-repos/astro-basic/build.mjs b/examples/fixture-repos/astro-basic/build.mjs new file mode 100644 index 0000000..4616ed5 --- /dev/null +++ b/examples/fixture-repos/astro-basic/build.mjs @@ -0,0 +1,36 @@ +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"); +const astroDir = path.join(root, ".astro"); + +fs.rmSync(dist, { recursive: true, force: true }); +fs.rmSync(astroDir, { recursive: true, force: true }); +fs.mkdirSync(path.join(dist, "services"), { recursive: true }); +fs.mkdirSync(astroDir, { 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"), + 'https://example.test/https://example.test/services/\n', +); +fs.writeFileSync( + path.join(astroDir, "manifest.json"), + `${JSON.stringify( + { + routes: [ + { route: "/", type: "page" }, + { route: "/services/", type: "page" } + ], + assets: [] + }, + null, + 2, + )}\n`, +); + +console.log("astro fixture build complete"); diff --git a/examples/fixture-repos/astro-basic/package.json b/examples/fixture-repos/astro-basic/package.json new file mode 100644 index 0000000..9eb50f2 --- /dev/null +++ b/examples/fixture-repos/astro-basic/package.json @@ -0,0 +1,11 @@ +{ + "name": "openclaw-astro-basic-fixture", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs" + }, + "dependencies": { + "astro": "^5.0.0" + } +} diff --git a/examples/fixture-repos/astro-basic/routes.txt b/examples/fixture-repos/astro-basic/routes.txt new file mode 100644 index 0000000..f5c97de --- /dev/null +++ b/examples/fixture-repos/astro-basic/routes.txt @@ -0,0 +1,2 @@ +/ +/services/ diff --git a/examples/fixture-repos/astro-basic/src/index.html b/examples/fixture-repos/astro-basic/src/index.html new file mode 100644 index 0000000..d0e04c3 --- /dev/null +++ b/examples/fixture-repos/astro-basic/src/index.html @@ -0,0 +1,15 @@ + + + + Astro Fixture Home + + + + +
+

Astro Fixture Home

+

This deterministic fixture represents an Astro static output route for repo audit tests.

+ Services +
+ + diff --git a/examples/fixture-repos/astro-basic/src/services.html b/examples/fixture-repos/astro-basic/src/services.html new file mode 100644 index 0000000..418e3f3 --- /dev/null +++ b/examples/fixture-repos/astro-basic/src/services.html @@ -0,0 +1,15 @@ + + + + Astro Fixture Services + + + + +
+

Astro Fixture Services

+

The services route gives the Astro fixture a second generated static page.

+ Home +
+ + diff --git a/examples/fixture-repos/next-basic/build.mjs b/examples/fixture-repos/next-basic/build.mjs new file mode 100644 index 0000000..874932f --- /dev/null +++ b/examples/fixture-repos/next-basic/build.mjs @@ -0,0 +1,40 @@ +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const src = path.join(root, "src"); +const out = path.join(root, "out"); +const nextDir = path.join(root, ".next"); + +fs.rmSync(out, { recursive: true, force: true }); +fs.rmSync(nextDir, { recursive: true, force: true }); +fs.mkdirSync(path.join(out, "about"), { recursive: true }); +fs.mkdirSync(nextDir, { recursive: true }); + +fs.copyFileSync(path.join(src, "index.html"), path.join(out, "index.html")); +fs.copyFileSync(path.join(src, "about.html"), path.join(out, "about", "index.html")); +fs.writeFileSync(path.join(out, "robots.txt"), "User-agent: *\nAllow: /\n"); +fs.writeFileSync( + path.join(out, "sitemap.xml"), + 'https://example.test/https://example.test/about/\n', +); +fs.writeFileSync( + path.join(nextDir, "prerender-manifest.json"), + `${JSON.stringify( + { + version: 4, + routes: { + "/": { initialRevalidateSeconds: false, srcRoute: null, dataRoute: null }, + "/about/": { initialRevalidateSeconds: false, srcRoute: null, dataRoute: null }, + "/missing/": { initialRevalidateSeconds: false, srcRoute: null, dataRoute: null } + }, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: { previewModeId: "fixture", previewModeSigningKey: "fixture", previewModeEncryptionKey: "fixture" } + }, + null, + 2, + )}\n`, +); + +console.log("next fixture build complete"); diff --git a/examples/fixture-repos/next-basic/package.json b/examples/fixture-repos/next-basic/package.json new file mode 100644 index 0000000..c53396f --- /dev/null +++ b/examples/fixture-repos/next-basic/package.json @@ -0,0 +1,13 @@ +{ + "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" + } +} diff --git a/examples/fixture-repos/next-basic/routes.txt b/examples/fixture-repos/next-basic/routes.txt new file mode 100644 index 0000000..fd2abb2 --- /dev/null +++ b/examples/fixture-repos/next-basic/routes.txt @@ -0,0 +1,2 @@ +/ +/about/ diff --git a/examples/fixture-repos/next-basic/src/about.html b/examples/fixture-repos/next-basic/src/about.html new file mode 100644 index 0000000..e72d1ba --- /dev/null +++ b/examples/fixture-repos/next-basic/src/about.html @@ -0,0 +1,15 @@ + + + + Next Fixture About + + + + +
+

Next Fixture About

+

The about route gives the audit a second generated page and an internal-link target.

+ Home +
+ + diff --git a/examples/fixture-repos/next-basic/src/index.html b/examples/fixture-repos/next-basic/src/index.html new file mode 100644 index 0000000..c086164 --- /dev/null +++ b/examples/fixture-repos/next-basic/src/index.html @@ -0,0 +1,15 @@ + + + + Next Fixture Home + + + + +
+

Next Fixture Home

+

This deterministic fixture represents a static Next.js export route for repo audit tests.

+ About +
+ + 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); +}); From 69a37db749fd5ea4cec384b8912dfabdd86d8ec8 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:37:58 -0500 Subject: [PATCH 04/15] feat: parse framework route manifests --- packages/cli/src/repo-findings.mjs | 9 ++ packages/cli/src/repo-manifests.mjs | 105 +++++++++++++++ packages/cli/test/repo-manifests.test.mjs | 151 ++++++++++++++++++++++ scripts/validate-skill.mjs | 2 + 4 files changed, 267 insertions(+) create mode 100644 packages/cli/src/repo-findings.mjs create mode 100644 packages/cli/src/repo-manifests.mjs create mode 100644 packages/cli/test/repo-manifests.test.mjs diff --git a/packages/cli/src/repo-findings.mjs b/packages/cli/src/repo-findings.mjs new file mode 100644 index 0000000..5e06c87 --- /dev/null +++ b/packages/cli/src/repo-findings.mjs @@ -0,0 +1,9 @@ +export const sourceFinding = ({ id, severity = "P1", message, evidence, recommendation, confidence = "high", details }) => ({ + id, + severity, + message, + evidence, + recommendation, + confidence, + ...(details ? { details } : {}), +}); diff --git a/packages/cli/src/repo-manifests.mjs b/packages/cli/src/repo-manifests.mjs new file mode 100644 index 0000000..047bafe --- /dev/null +++ b/packages/cli/src/repo-manifests.mjs @@ -0,0 +1,105 @@ +import fs from "node:fs"; +import path from "node:path"; +import { sourceFinding } from "./repo-findings.mjs"; + +const manifestConfigs = { + next: { + type: "next_prerender_manifest", + relativePath: path.join(".next", "prerender-manifest.json"), + routesFor: (json) => (json?.routes && typeof json.routes === "object" && !Array.isArray(json.routes) ? Object.keys(json.routes) : []), + }, + astro: { + type: "astro_manifest", + relativePath: path.join(".astro", "manifest.json"), + routesFor: (json) => { + const routes = Array.isArray(json?.routes) ? json.routes : Array.isArray(json?.manifest?.routes) ? json.manifest.routes : []; + return routes.map((route) => (typeof route === "string" ? route : route?.route)).filter(Boolean); + }, + }, +}; + +const normalizeRoute = (route) => { + if (typeof route !== "string") return null; + const cleanRoute = route.trim(); + if (!cleanRoute) return null; + const withLeadingSlash = cleanRoute.startsWith("/") ? cleanRoute : `/${cleanRoute}`; + if (withLeadingSlash === "/") return "/"; + if (withLeadingSlash.endsWith("/") || withLeadingSlash.endsWith(".html")) return withLeadingSlash; + return `${withLeadingSlash}/`; +}; + +const uniqueNormalizedRoutes = (routes) => { + const normalized = []; + const seen = new Set(); + for (const route of routes) { + const normalizedRoute = normalizeRoute(route); + if (!normalizedRoute || seen.has(normalizedRoute)) continue; + seen.add(normalizedRoute); + normalized.push(normalizedRoute); + } + return normalized; +}; + +const invalidManifestFinding = (manifestPath, error) => + sourceFinding({ + id: "repo.route_manifest_invalid", + message: "Framework route manifest could not be parsed.", + evidence: manifestPath, + recommendation: "Regenerate the framework build output and rerun the repository audit.", + details: { message: error?.message }, + }); + +const manifestRouteMissingFinding = (route) => + sourceFinding({ + id: "repo.manifest_route_missing", + message: "Framework route manifest lists a route that is missing from static output.", + evidence: route, + recommendation: "Ensure the framework build emits this route or remove it from generated route metadata.", + }); + +const staticRouteUnlistedFinding = (route) => + sourceFinding({ + id: "repo.static_route_unlisted", + message: "Static output includes a route that is not listed in the framework route manifest.", + evidence: route, + recommendation: "Confirm the generated route is intentional and represented in framework route metadata.", + }); + +export const analyzeFrameworkRouteManifests = ({ repoPath, detectedFramework, staticRoutes = [] }) => { + const config = manifestConfigs[detectedFramework]; + const frameworkManifests = []; + const sourceFindings = []; + if (!config) return { frameworkManifests, sourceFindings }; + + const manifestPath = path.join(repoPath, config.relativePath); + if (!fs.existsSync(manifestPath)) return { frameworkManifests, sourceFindings }; + + let json; + try { + json = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + } catch (error) { + return { + frameworkManifests, + sourceFindings: [invalidManifestFinding(manifestPath, error)], + }; + } + + const manifestRoutes = uniqueNormalizedRoutes(config.routesFor(json)); + frameworkManifests.push({ + type: config.type, + path: manifestPath, + routes: manifestRoutes, + }); + + const manifestRouteSet = new Set(manifestRoutes); + const staticRouteSet = new Set(uniqueNormalizedRoutes(staticRoutes.map((route) => route.route))); + + for (const route of manifestRoutes) { + if (!staticRouteSet.has(route)) sourceFindings.push(manifestRouteMissingFinding(route)); + } + for (const route of staticRouteSet) { + if (!manifestRouteSet.has(route)) sourceFindings.push(staticRouteUnlistedFinding(route)); + } + + return { frameworkManifests, sourceFindings }; +}; diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs new file mode 100644 index 0000000..ba5150b --- /dev/null +++ b/packages/cli/test/repo-manifests.test.mjs @@ -0,0 +1,151 @@ +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, `${title}

${title}

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"); +}); diff --git a/scripts/validate-skill.mjs b/scripts/validate-skill.mjs index 4e2e5d9..05dd12f 100644 --- a/scripts/validate-skill.mjs +++ b/scripts/validate-skill.mjs @@ -26,6 +26,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", From 34d2ef45a6bcfdb4a679e884b8684acd0cd323fe Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:42:06 -0500 Subject: [PATCH 05/15] fix: verify manifest routes against static output --- packages/cli/src/repo-manifests.mjs | 19 +++++++++++++-- packages/cli/test/repo-manifests.test.mjs | 28 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/repo-manifests.mjs b/packages/cli/src/repo-manifests.mjs index 047bafe..2c14172 100644 --- a/packages/cli/src/repo-manifests.mjs +++ b/packages/cli/src/repo-manifests.mjs @@ -40,6 +40,21 @@ const uniqueNormalizedRoutes = (routes) => { return normalized; }; +const htmlPathForRoute = (staticDir, route) => { + if (!staticDir) return null; + if (route === "/") return path.join(staticDir, "index.html"); + + const relativeRoute = route.startsWith("/") ? route.slice(1) : route; + if (relativeRoute.endsWith(".html")) return path.join(staticDir, relativeRoute); + if (relativeRoute.endsWith("/")) return path.join(staticDir, relativeRoute, "index.html"); + return path.join(staticDir, relativeRoute, "index.html"); +}; + +const hasGeneratedHtmlForRoute = (staticDir, route) => { + const htmlPath = htmlPathForRoute(staticDir, route); + return Boolean(htmlPath && fs.existsSync(htmlPath) && fs.statSync(htmlPath).isFile()); +}; + const invalidManifestFinding = (manifestPath, error) => sourceFinding({ id: "repo.route_manifest_invalid", @@ -65,7 +80,7 @@ const staticRouteUnlistedFinding = (route) => recommendation: "Confirm the generated route is intentional and represented in framework route metadata.", }); -export const analyzeFrameworkRouteManifests = ({ repoPath, detectedFramework, staticRoutes = [] }) => { +export const analyzeFrameworkRouteManifests = ({ repoPath, staticDir, detectedFramework, staticRoutes = [] }) => { const config = manifestConfigs[detectedFramework]; const frameworkManifests = []; const sourceFindings = []; @@ -95,7 +110,7 @@ export const analyzeFrameworkRouteManifests = ({ repoPath, detectedFramework, st const staticRouteSet = new Set(uniqueNormalizedRoutes(staticRoutes.map((route) => route.route))); for (const route of manifestRoutes) { - if (!staticRouteSet.has(route)) sourceFindings.push(manifestRouteMissingFinding(route)); + if (!hasGeneratedHtmlForRoute(staticDir, route)) sourceFindings.push(manifestRouteMissingFinding(route)); } for (const route of staticRouteSet) { if (!manifestRouteSet.has(route)) sourceFindings.push(staticRouteUnlistedFinding(route)); diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs index ba5150b..77383eb 100644 --- a/packages/cli/test/repo-manifests.test.mjs +++ b/packages/cli/test/repo-manifests.test.mjs @@ -115,6 +115,34 @@ test("reports generated static routes that are not listed in a framework manifes assert.equal(result.sourceFindings[0].evidence, "/extra/"); }); +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"); From 0506743956cffa1525024579eb8f328319cecee0 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:45:32 -0500 Subject: [PATCH 06/15] fix: match extensionless manifest routes to html files --- packages/cli/src/repo-manifests.mjs | 77 ++++++++++++++++++----- packages/cli/test/repo-manifests.test.mjs | 32 ++++++++++ 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/repo-manifests.mjs b/packages/cli/src/repo-manifests.mjs index 2c14172..c6a85ae 100644 --- a/packages/cli/src/repo-manifests.mjs +++ b/packages/cli/src/repo-manifests.mjs @@ -28,7 +28,45 @@ const normalizeRoute = (route) => { return `${withLeadingSlash}/`; }; +const routeEntry = (route) => { + if (typeof route !== "string") return null; + const cleanRoute = route.trim(); + const normalizedRoute = normalizeRoute(cleanRoute); + if (!normalizedRoute) return null; + + const withLeadingSlash = cleanRoute.startsWith("/") ? cleanRoute : `/${cleanRoute}`; + return { + route: normalizedRoute, + extensionless: withLeadingSlash !== "/" && !withLeadingSlash.endsWith("/") && !withLeadingSlash.endsWith(".html"), + }; +}; + +const uniqueRouteEntries = (routes) => { + const entries = []; + const seen = new Set(); + for (const route of routes) { + const entry = routeEntry(route); + if (!entry) continue; + const key = `${entry.route}:${entry.extensionless ? "extensionless" : "explicit"}`; + if (seen.has(key)) continue; + seen.add(key); + entries.push(entry); + } + return entries; +}; + const uniqueNormalizedRoutes = (routes) => { + const normalized = []; + const seen = new Set(); + for (const { route } of routes) { + if (seen.has(route)) continue; + seen.add(route); + normalized.push(route); + } + return normalized; +}; + +const uniqueNormalizedRouteStrings = (routes) => { const normalized = []; const seen = new Set(); for (const route of routes) { @@ -40,19 +78,28 @@ const uniqueNormalizedRoutes = (routes) => { return normalized; }; -const htmlPathForRoute = (staticDir, route) => { - if (!staticDir) return null; - if (route === "/") return path.join(staticDir, "index.html"); +const htmlPathsForRoute = (staticDir, { route, extensionless = false }) => { + if (!staticDir) return []; + if (route === "/") return [path.join(staticDir, "index.html")]; const relativeRoute = route.startsWith("/") ? route.slice(1) : route; - if (relativeRoute.endsWith(".html")) return path.join(staticDir, relativeRoute); - if (relativeRoute.endsWith("/")) return path.join(staticDir, relativeRoute, "index.html"); - return path.join(staticDir, relativeRoute, "index.html"); + if (relativeRoute.endsWith(".html")) return [path.join(staticDir, relativeRoute)]; + const directoryHtmlPath = path.join(staticDir, relativeRoute, "index.html"); + if (!extensionless) return [directoryHtmlPath]; + return [directoryHtmlPath, path.join(staticDir, relativeRoute.replace(/\/$/, ".html"))]; }; -const hasGeneratedHtmlForRoute = (staticDir, route) => { - const htmlPath = htmlPathForRoute(staticDir, route); - return Boolean(htmlPath && fs.existsSync(htmlPath) && fs.statSync(htmlPath).isFile()); +const hasGeneratedHtmlForRoute = (staticDir, entry) => + htmlPathsForRoute(staticDir, entry).some((htmlPath) => fs.existsSync(htmlPath) && fs.statSync(htmlPath).isFile()); + +const extensionlessHtmlRoute = (route) => (route !== "/" && route.endsWith("/") ? `${route.slice(0, -1)}.html` : null); + +const isStaticRouteListed = (staticRoute, manifestEntries) => { + for (const entry of manifestEntries) { + if (entry.route === staticRoute) return true; + if (entry.extensionless && extensionlessHtmlRoute(entry.route) === staticRoute) return true; + } + return false; }; const invalidManifestFinding = (manifestPath, error) => @@ -99,21 +146,21 @@ export const analyzeFrameworkRouteManifests = ({ repoPath, staticDir, detectedFr }; } - const manifestRoutes = uniqueNormalizedRoutes(config.routesFor(json)); + const manifestEntries = uniqueRouteEntries(config.routesFor(json)); + const manifestRoutes = uniqueNormalizedRoutes(manifestEntries); frameworkManifests.push({ type: config.type, path: manifestPath, routes: manifestRoutes, }); - const manifestRouteSet = new Set(manifestRoutes); - const staticRouteSet = new Set(uniqueNormalizedRoutes(staticRoutes.map((route) => route.route))); + const staticRouteSet = new Set(uniqueNormalizedRouteStrings(staticRoutes.map((route) => route.route))); - for (const route of manifestRoutes) { - if (!hasGeneratedHtmlForRoute(staticDir, route)) sourceFindings.push(manifestRouteMissingFinding(route)); + for (const entry of manifestEntries) { + if (!hasGeneratedHtmlForRoute(staticDir, entry)) sourceFindings.push(manifestRouteMissingFinding(entry.route)); } for (const route of staticRouteSet) { - if (!manifestRouteSet.has(route)) sourceFindings.push(staticRouteUnlistedFinding(route)); + if (!isStaticRouteListed(route, manifestEntries)) sourceFindings.push(staticRouteUnlistedFinding(route)); } return { frameworkManifests, sourceFindings }; diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs index 77383eb..9fe2109 100644 --- a/packages/cli/test/repo-manifests.test.mjs +++ b/packages/cli/test/repo-manifests.test.mjs @@ -89,6 +89,38 @@ test("reads Astro manifest routes when present", () => { 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"); From fa03702362431baf81bd0ef51ce922f7f0341f51 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:48:41 -0500 Subject: [PATCH 07/15] fix: filter astro manifest page routes --- packages/cli/src/repo-manifests.mjs | 5 +++- packages/cli/test/repo-manifests.test.mjs | 34 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/repo-manifests.mjs b/packages/cli/src/repo-manifests.mjs index c6a85ae..792c2c7 100644 --- a/packages/cli/src/repo-manifests.mjs +++ b/packages/cli/src/repo-manifests.mjs @@ -13,7 +13,10 @@ const manifestConfigs = { relativePath: path.join(".astro", "manifest.json"), routesFor: (json) => { const routes = Array.isArray(json?.routes) ? json.routes : Array.isArray(json?.manifest?.routes) ? json.manifest.routes : []; - return routes.map((route) => (typeof route === "string" ? route : route?.route)).filter(Boolean); + return routes + .filter((route) => typeof route === "string" || route?.type === "page") + .map((route) => (typeof route === "string" ? route : route?.route)) + .filter(Boolean); }, }, }; diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs index 9fe2109..2280692 100644 --- a/packages/cli/test/repo-manifests.test.mjs +++ b/packages/cli/test/repo-manifests.test.mjs @@ -89,6 +89,40 @@ test("reads Astro manifest routes when present", () => { assert.deepEqual(result.sourceFindings, []); }); +test("ignores Astro endpoint routes when checking generated HTML", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-astro-endpoint-manifest-")); + const staticDir = path.join(repoPath, "dist"); + const manifestDir = path.join(repoPath, ".astro"); + writeHtml(path.join(staticDir, "index.html"), "Home"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "manifest.json"), + JSON.stringify({ + routes: [ + { route: "/", type: "page" }, + { route: "/rss.xml", type: "endpoint" }, + { route: "/api/data.json", type: "endpoint" } + ] + }), + ); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "astro", + staticRoutes: [{ type: "static_html", route: "/", path: path.join(staticDir, "index.html") }] + }); + + assert.deepEqual(result.frameworkManifests, [ + { + type: "astro_manifest", + path: path.join(manifestDir, "manifest.json"), + routes: ["/"] + } + ]); + 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"); From 8ddd82f3f5c2d1c008a93bc4c98b483c6e6fdd5a Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:52:57 -0500 Subject: [PATCH 08/15] feat: include framework manifest evidence in repo audits --- packages/cli/src/repo-audit.mjs | 25 +++++++------- packages/cli/test/repo-audit.test.mjs | 50 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/repo-audit.mjs b/packages/cli/src/repo-audit.mjs index d3bf13d..81484b8 100644 --- a/packages/cli/src/repo-audit.mjs +++ b/packages/cli/src/repo-audit.mjs @@ -2,21 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { runAudit } from "./audit.mjs"; import { detectRepo } from "./repo-detect.mjs"; +import { sourceFinding } from "./repo-findings.mjs"; +import { analyzeFrameworkRouteManifests } from "./repo-manifests.mjs"; import { runCommand, startPreview, stopPreview } from "./repo-process.mjs"; import { discoverStaticRoutes } from "./repo-routes.mjs"; const toolVersion = "0.2.0"; -const sourceFinding = ({ id, severity = "P1", message, evidence, recommendation, confidence = "high", details }) => ({ - id, - severity, - message, - evidence, - recommendation, - confidence, - ...(details ? { details } : {}), -}); - const relativePath = (repoPath, targetPath) => { if (!targetPath) return null; const relative = path.relative(repoPath, targetPath); @@ -98,6 +90,7 @@ const repoEvidence = (detected, overrides = {}) => ({ staticDir: detected.staticDir, staticDirRelative: detected.staticDirRelative, routeSources: detected.routeSources || [], + frameworkManifests: [], sourceFindings: [], notes: [], ...overrides, @@ -308,7 +301,8 @@ export const runRepoAudit = async (options = {}) => { const routeList = options.routeList ? path.resolve(repoPath, options.routeList) : null; const routeListResult = routeList ? readRouteListRoutes(routeList, staticDir) : null; - const routes = routeListResult ? routeListResult.routes : discoverStaticRoutes(staticDir); + const staticRoutes = discoverStaticRoutes(staticDir); + const routes = routeListResult ? routeListResult.routes : staticRoutes; const routeSourceFindings = routeListResult?.sourceFindings || []; if (routeSourceFindings.length) { return emptyAudit(detected, { @@ -334,7 +328,13 @@ export const runRepoAudit = async (options = {}) => { }); } - const outputSourceFindings = generatedOutputFindings(staticDir); + const manifestAnalysis = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: detected.detectedFramework, + staticRoutes, + }); + const outputSourceFindings = [...generatedOutputFindings(staticDir), ...manifestAnalysis.sourceFindings]; const audit = await runAudit({ ...options, target: routes[0].path, @@ -349,6 +349,7 @@ export const runRepoAudit = async (options = {}) => { staticDirRelative, routeList, routeSources: routes, + frameworkManifests: manifestAnalysis.frameworkManifests, sourceFindings: outputSourceFindings, notes: ["Audited static output directory."], }); diff --git a/packages/cli/test/repo-audit.test.mjs b/packages/cli/test/repo-audit.test.mjs index 13f91fb..1a97bfd 100644 --- a/packages/cli/test/repo-audit.test.mjs +++ b/packages/cli/test/repo-audit.test.mjs @@ -208,6 +208,56 @@ test("repo audit runs explicit build command before static output audit", async assert.ok(audit.pages.some((page) => page.evidence.title === "Vite Fixture Home")); }); +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")); +}); + test("repo audit reports build failures as source findings", async () => { const audit = await runRepoAudit({ repoPath: fixture("vite-basic"), From a45b6a5d78c058b4c936fb6d94792f7a069286db Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:57:05 -0500 Subject: [PATCH 09/15] fix: preserve route-list repo audit handling --- packages/cli/src/repo-audit.mjs | 5 +- packages/cli/test/repo-audit.test.mjs | 108 +++++++++++++++++--------- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/packages/cli/src/repo-audit.mjs b/packages/cli/src/repo-audit.mjs index 81484b8..ad01587 100644 --- a/packages/cli/src/repo-audit.mjs +++ b/packages/cli/src/repo-audit.mjs @@ -301,8 +301,6 @@ export const runRepoAudit = async (options = {}) => { const routeList = options.routeList ? path.resolve(repoPath, options.routeList) : null; const routeListResult = routeList ? readRouteListRoutes(routeList, staticDir) : null; - const staticRoutes = discoverStaticRoutes(staticDir); - const routes = routeListResult ? routeListResult.routes : staticRoutes; const routeSourceFindings = routeListResult?.sourceFindings || []; if (routeSourceFindings.length) { return emptyAudit(detected, { @@ -314,6 +312,9 @@ export const runRepoAudit = async (options = {}) => { }); } + const staticRoutes = discoverStaticRoutes(staticDir); + const routes = routeListResult ? routeListResult.routes : staticRoutes; + if (!routes.length) { return emptyAudit(detected, { ...staticRepoFields, diff --git a/packages/cli/test/repo-audit.test.mjs b/packages/cli/test/repo-audit.test.mjs index 1a97bfd..990b99f 100644 --- a/packages/cli/test/repo-audit.test.mjs +++ b/packages/cli/test/repo-audit.test.mjs @@ -213,24 +213,29 @@ test("Next.js static build audit records framework manifest evidence and route p 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")); + try { + 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")); + } finally { + fs.rmSync(path.join(repoPath, "out"), { recursive: true, force: true }); + fs.rmSync(path.join(repoPath, ".next"), { recursive: true, force: true }); + } }); test("Astro static build audit records framework manifest evidence", async () => { @@ -238,24 +243,29 @@ test("Astro static build audit records framework manifest evidence", async () => 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")); + try { + 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")); + } finally { + fs.rmSync(path.join(repoPath, "dist"), { recursive: true, force: true }); + fs.rmSync(path.join(repoPath, ".astro"), { recursive: true, force: true }); + } }); test("repo audit reports build failures as source findings", async () => { @@ -355,6 +365,32 @@ test("repo audit reports missing route-list files", async () => { assert.equal(audit.repo.sourceFindings[0].id, "repo.route_list_missing"); }); +test("repo audit reports missing route-list files before full static route traversal", async () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-missing-route-list-")); + const staticDir = path.join(repoPath, "site-output"); + const blockedDir = path.join(staticDir, "blocked"); + fs.mkdirSync(blockedDir, { recursive: true }); + fs.writeFileSync( + path.join(staticDir, "index.html"), + "Home

Home

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"); From 0eec1da6f785769a4efcdeed818521f626e4085d Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Tue, 19 May 2026 23:59:22 -0500 Subject: [PATCH 10/15] feat: report framework manifest evidence --- packages/cli/src/report.mjs | 11 +++++++++++ packages/cli/test/report.test.mjs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/packages/cli/src/report.mjs b/packages/cli/src/report.mjs index 99f00a8..40a17bb 100644 --- a/packages/cli/src/report.mjs +++ b/packages/cli/src/report.mjs @@ -40,6 +40,17 @@ const appendRepositoryEvidence = (lines, repo) => { lines.push("- None recorded."); } + 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."); + } + lines.push("", "Repository source findings:"); if (repo.sourceFindings?.length) { for (const finding of repo.sourceFindings) { 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/); +}); From ff8499435efd4a2104084f7ea15169df590d317d Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Wed, 20 May 2026 00:02:08 -0500 Subject: [PATCH 11/15] test: add framework repo golden summary --- examples/golden/repo-framework-summary.json | 41 ++++++++++++++++ packages/cli/test/golden-fixtures.test.mjs | 54 +++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 examples/golden/repo-framework-summary.json diff --git a/examples/golden/repo-framework-summary.json b/examples/golden/repo-framework-summary.json new file mode 100644 index 0000000..85379fa --- /dev/null +++ b/examples/golden/repo-framework-summary.json @@ -0,0 +1,41 @@ +{ + "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": [] + } +} diff --git a/packages/cli/test/golden-fixtures.test.mjs b/packages/cli/test/golden-fixtures.test.mjs index ff91b71..7e07491 100644 --- a/packages/cli/test/golden-fixtures.test.mjs +++ b/packages/cli/test/golden-fixtures.test.mjs @@ -5,6 +5,7 @@ import http from "node:http"; import path from "node:path"; import { runAudit } from "../src/audit.mjs"; import { generateMarkdownReport } from "../src/report.mjs"; +import { runRepoAudit } from "../src/repo-audit.mjs"; import { normalizeAuditForGolden, normalizeMarkdownForGolden } from "./helpers/golden.mjs"; const rootDir = path.resolve("examples/fixture-sites/known-issues"); @@ -43,6 +44,28 @@ const withFixtureServer = async (fn) => { } }; +const cleanupFrameworkRepoOutputs = () => { + for (const target of [ + "examples/fixture-repos/next-basic/out", + "examples/fixture-repos/next-basic/.next", + "examples/fixture-repos/astro-basic/dist", + "examples/fixture-repos/astro-basic/.astro", + ]) { + fs.rmSync(target, { recursive: true, force: true }); + } +}; + +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), +}); + test("known-issues fixture audit matches golden JSON and Markdown", async () => { await withFixtureServer(async (origin) => { const audit = await runAudit({ @@ -62,3 +85,34 @@ test("known-issues fixture audit matches golden JSON and Markdown", async () => assert.equal(markdown, expectedMarkdown); }); }); + +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"); + + try { + 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, + ); + } finally { + cleanupFrameworkRepoOutputs(); + } +}); From 9ec52eb98fc4b3e4f09c640dd53e4699135282aa Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Wed, 20 May 2026 00:04:47 -0500 Subject: [PATCH 12/15] docs: record framework repo audit maturity --- CHANGELOG.md | 1 + docs/prd-deterministic-audit-cli.md | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb380a..b2565ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - 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, framework route manifest evidence, and source-level route parity findings for manifest/static-output mismatches. - 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..e466c21 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. +- 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. ## 20. Risks and Mitigations From 3d3a7e0473b60d9088fcf0d993be0a9cb62a77b6 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Wed, 20 May 2026 00:06:42 -0500 Subject: [PATCH 13/15] docs: refresh repo audit extension checklist --- docs/prd-deterministic-audit-cli.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/prd-deterministic-audit-cli.md b/docs/prd-deterministic-audit-cli.md index e466c21..e1ebcdc 100644 --- a/docs/prd-deterministic-audit-cli.md +++ b/docs/prd-deterministic-audit-cli.md @@ -921,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. From 057b86bdf8635e3fa9fafd20cda54581a6a96d4d Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Wed, 20 May 2026 00:13:15 -0500 Subject: [PATCH 14/15] fix: harden framework manifest audits --- packages/cli/src/repo-manifests.mjs | 25 +++++--- packages/cli/test/golden-fixtures.test.mjs | 26 ++++---- packages/cli/test/repo-audit.test.mjs | 21 +++---- packages/cli/test/repo-manifests.test.mjs | 69 ++++++++++++++++++++++ 4 files changed, 110 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/repo-manifests.mjs b/packages/cli/src/repo-manifests.mjs index 792c2c7..d9f3905 100644 --- a/packages/cli/src/repo-manifests.mjs +++ b/packages/cli/src/repo-manifests.mjs @@ -6,13 +6,15 @@ const manifestConfigs = { next: { type: "next_prerender_manifest", relativePath: path.join(".next", "prerender-manifest.json"), - routesFor: (json) => (json?.routes && typeof json.routes === "object" && !Array.isArray(json.routes) ? Object.keys(json.routes) : []), + routesFor: (json) => + json?.routes && typeof json.routes === "object" && !Array.isArray(json.routes) ? Object.keys(json.routes) : null, }, astro: { type: "astro_manifest", relativePath: path.join(".astro", "manifest.json"), routesFor: (json) => { - const routes = Array.isArray(json?.routes) ? json.routes : Array.isArray(json?.manifest?.routes) ? json.manifest.routes : []; + const routes = Array.isArray(json?.routes) ? json.routes : Array.isArray(json?.manifest?.routes) ? json.manifest.routes : null; + if (!routes) return null; return routes .filter((route) => typeof route === "string" || route?.type === "page") .map((route) => (typeof route === "string" ? route : route?.route)) @@ -81,15 +83,21 @@ const uniqueNormalizedRouteStrings = (routes) => { return normalized; }; +const isPathInside = (root, candidate) => { + const relative = path.relative(path.resolve(root), path.resolve(candidate)); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +}; + const htmlPathsForRoute = (staticDir, { route, extensionless = false }) => { if (!staticDir) return []; - if (route === "/") return [path.join(staticDir, "index.html")]; + const safePaths = (candidates) => candidates.filter((candidate) => isPathInside(staticDir, candidate)); + if (route === "/") return safePaths([path.join(staticDir, "index.html")]); const relativeRoute = route.startsWith("/") ? route.slice(1) : route; - if (relativeRoute.endsWith(".html")) return [path.join(staticDir, relativeRoute)]; + if (relativeRoute.endsWith(".html")) return safePaths([path.join(staticDir, relativeRoute)]); const directoryHtmlPath = path.join(staticDir, relativeRoute, "index.html"); - if (!extensionless) return [directoryHtmlPath]; - return [directoryHtmlPath, path.join(staticDir, relativeRoute.replace(/\/$/, ".html"))]; + if (!extensionless) return safePaths([directoryHtmlPath]); + return safePaths([directoryHtmlPath, path.join(staticDir, relativeRoute.replace(/\/$/, ".html"))]); }; const hasGeneratedHtmlForRoute = (staticDir, entry) => @@ -149,7 +157,10 @@ export const analyzeFrameworkRouteManifests = ({ repoPath, staticDir, detectedFr }; } - const manifestEntries = uniqueRouteEntries(config.routesFor(json)); + const routes = config.routesFor(json); + if (!routes) return { frameworkManifests, sourceFindings }; + + const manifestEntries = uniqueRouteEntries(routes); const manifestRoutes = uniqueNormalizedRoutes(manifestEntries); frameworkManifests.push({ type: config.type, diff --git a/packages/cli/test/golden-fixtures.test.mjs b/packages/cli/test/golden-fixtures.test.mjs index 7e07491..4bb4aef 100644 --- a/packages/cli/test/golden-fixtures.test.mjs +++ b/packages/cli/test/golden-fixtures.test.mjs @@ -2,6 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import http from "node:http"; +import os from "node:os"; import path from "node:path"; import { runAudit } from "../src/audit.mjs"; import { generateMarkdownReport } from "../src/report.mjs"; @@ -44,15 +45,11 @@ const withFixtureServer = async (fn) => { } }; -const cleanupFrameworkRepoOutputs = () => { - for (const target of [ - "examples/fixture-repos/next-basic/out", - "examples/fixture-repos/next-basic/.next", - "examples/fixture-repos/astro-basic/dist", - "examples/fixture-repos/astro-basic/.astro", - ]) { - fs.rmSync(target, { recursive: true, force: true }); - } +const copyFixtureRepo = (name) => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-${name}-`)); + const repoPath = path.join(tempRoot, name); + fs.cpSync(path.resolve("examples/fixture-repos", name), repoPath, { recursive: true }); + return { repoPath, tempRoot }; }; const frameworkRepoSummary = (audit) => ({ @@ -87,18 +84,18 @@ test("known-issues fixture audit matches golden JSON and Markdown", async () => }); 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 nextFixture = copyFixtureRepo("next-basic"); + const astroFixture = copyFixtureRepo("astro-basic"); try { const nextAudit = await runRepoAudit({ - repoPath: nextRepo, + repoPath: nextFixture.repoPath, buildCommand: "npm run build", staticDir: "out", maxBuildMs: 5000, }); const astroAudit = await runRepoAudit({ - repoPath: astroRepo, + repoPath: astroFixture.repoPath, buildCommand: "npm run build", staticDir: "dist", maxBuildMs: 5000, @@ -113,6 +110,7 @@ test("framework repo audit golden summary matches fixtures", async () => { expected, ); } finally { - cleanupFrameworkRepoOutputs(); + fs.rmSync(nextFixture.tempRoot, { recursive: true, force: true }); + fs.rmSync(astroFixture.tempRoot, { recursive: true, force: true }); } }); diff --git a/packages/cli/test/repo-audit.test.mjs b/packages/cli/test/repo-audit.test.mjs index 990b99f..7c8376a 100644 --- a/packages/cli/test/repo-audit.test.mjs +++ b/packages/cli/test/repo-audit.test.mjs @@ -10,6 +10,13 @@ import { waitForHttp } from "../src/repo-process.mjs"; const fixture = (name) => path.resolve("examples/fixture-repos", name); +const copyFixtureRepo = (name) => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-${name}-`)); + const repoPath = path.join(tempRoot, name); + fs.cpSync(fixture(name), repoPath, { recursive: true }); + return { repoPath, tempRoot }; +}; + const freePort = async () => { const server = net.createServer(); server.listen(0, "127.0.0.1"); @@ -209,9 +216,7 @@ test("repo audit runs explicit build command before static output audit", async }); 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 { repoPath, tempRoot } = copyFixtureRepo("next-basic"); try { const audit = await runRepoAudit({ @@ -233,15 +238,12 @@ test("Next.js static build audit records framework manifest evidence and route p ]); assert.ok(audit.repo.sourceFindings.some((finding) => finding.id === "repo.manifest_route_missing")); } finally { - fs.rmSync(path.join(repoPath, "out"), { recursive: true, force: true }); - fs.rmSync(path.join(repoPath, ".next"), { recursive: true, force: true }); + fs.rmSync(tempRoot, { recursive: true, force: true }); } }); 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 { repoPath, tempRoot } = copyFixtureRepo("astro-basic"); try { const audit = await runRepoAudit({ @@ -263,8 +265,7 @@ test("Astro static build audit records framework manifest evidence", async () => ]); assert.ok(!audit.repo.sourceFindings.some((finding) => finding.id === "repo.manifest_route_missing")); } finally { - fs.rmSync(path.join(repoPath, "dist"), { recursive: true, force: true }); - fs.rmSync(path.join(repoPath, ".astro"), { recursive: true, force: true }); + fs.rmSync(tempRoot, { recursive: true, force: true }); } }); diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs index 2280692..2818d1a 100644 --- a/packages/cli/test/repo-manifests.test.mjs +++ b/packages/cli/test/repo-manifests.test.mjs @@ -181,6 +181,75 @@ test("reports generated static routes that are not listed in a framework manifes 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("ignores valid Astro manifest files with unrecognized route schema", () => { + const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-astro-unknown-schema-")); + const staticDir = path.join(repoPath, "dist"); + const manifestDir = path.join(repoPath, ".astro"); + 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, "manifest.json"), JSON.stringify({ assets: [] })); + + const result = analyzeFrameworkRouteManifests({ + repoPath, + staticDir, + detectedFramework: "astro", + 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"); From a696c8ec6f70f0880467bf66b4cc983d2f32b964 Mon Sep 17 00:00:00 2001 From: PSkinnerTech Date: Wed, 20 May 2026 00:22:15 -0500 Subject: [PATCH 15/15] fix: align framework manifest scope --- .gitignore | 4 ++ CHANGELOG.md | 3 +- docs/prd-deterministic-audit-cli.md | 4 +- examples/fixture-repos/astro-basic/build.mjs | 17 ----- examples/golden/repo-framework-summary.json | 10 +-- packages/cli/src/repo-manifests.mjs | 12 ---- packages/cli/test/repo-audit.test.mjs | 10 +-- packages/cli/test/repo-manifests.test.mjs | 69 +------------------- scripts/validate-skill.mjs | 13 ++++ 9 files changed, 27 insertions(+), 115 deletions(-) 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 b2565ed..e291b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +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, framework route manifest evidence, and source-level route parity findings for manifest/static-output mismatches. +- 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 e1ebcdc..d04b104 100644 --- a/docs/prd-deterministic-audit-cli.md +++ b/docs/prd-deterministic-audit-cli.md @@ -858,14 +858,14 @@ Delivered developer-focused repo audit completion work: - 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. +- 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: - 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. +- 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 diff --git a/examples/fixture-repos/astro-basic/build.mjs b/examples/fixture-repos/astro-basic/build.mjs index 4616ed5..9197856 100644 --- a/examples/fixture-repos/astro-basic/build.mjs +++ b/examples/fixture-repos/astro-basic/build.mjs @@ -4,12 +4,9 @@ import path from "node:path"; const root = process.cwd(); const src = path.join(root, "src"); const dist = path.join(root, "dist"); -const astroDir = path.join(root, ".astro"); fs.rmSync(dist, { recursive: true, force: true }); -fs.rmSync(astroDir, { recursive: true, force: true }); fs.mkdirSync(path.join(dist, "services"), { recursive: true }); -fs.mkdirSync(astroDir, { 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")); @@ -18,19 +15,5 @@ fs.writeFileSync( path.join(dist, "sitemap.xml"), 'https://example.test/https://example.test/services/\n', ); -fs.writeFileSync( - path.join(astroDir, "manifest.json"), - `${JSON.stringify( - { - routes: [ - { route: "/", type: "page" }, - { route: "/services/", type: "page" } - ], - assets: [] - }, - null, - 2, - )}\n`, -); console.log("astro fixture build complete"); diff --git a/examples/golden/repo-framework-summary.json b/examples/golden/repo-framework-summary.json index 85379fa..3c15df1 100644 --- a/examples/golden/repo-framework-summary.json +++ b/examples/golden/repo-framework-summary.json @@ -27,15 +27,7 @@ "Astro Fixture Home", "Astro Fixture Services" ], - "frameworkManifests": [ - { - "type": "astro_manifest", - "routes": [ - "/", - "/services/" - ] - } - ], + "frameworkManifests": [], "sourceFindingIds": [] } } diff --git a/packages/cli/src/repo-manifests.mjs b/packages/cli/src/repo-manifests.mjs index d9f3905..be74228 100644 --- a/packages/cli/src/repo-manifests.mjs +++ b/packages/cli/src/repo-manifests.mjs @@ -9,18 +9,6 @@ const manifestConfigs = { routesFor: (json) => json?.routes && typeof json.routes === "object" && !Array.isArray(json.routes) ? Object.keys(json.routes) : null, }, - astro: { - type: "astro_manifest", - relativePath: path.join(".astro", "manifest.json"), - routesFor: (json) => { - const routes = Array.isArray(json?.routes) ? json.routes : Array.isArray(json?.manifest?.routes) ? json.manifest.routes : null; - if (!routes) return null; - return routes - .filter((route) => typeof route === "string" || route?.type === "page") - .map((route) => (typeof route === "string" ? route : route?.route)) - .filter(Boolean); - }, - }, }; const normalizeRoute = (route) => { diff --git a/packages/cli/test/repo-audit.test.mjs b/packages/cli/test/repo-audit.test.mjs index 7c8376a..64c6bed 100644 --- a/packages/cli/test/repo-audit.test.mjs +++ b/packages/cli/test/repo-audit.test.mjs @@ -242,7 +242,7 @@ test("Next.js static build audit records framework manifest evidence and route p } }); -test("Astro static build audit records framework manifest evidence", async () => { +test("Astro static build audit records framework detection without fixture-only manifest evidence", async () => { const { repoPath, tempRoot } = copyFixtureRepo("astro-basic"); try { @@ -256,13 +256,7 @@ test("Astro static build audit records framework manifest evidence", async () => 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.deepEqual(audit.repo.frameworkManifests, []); assert.ok(!audit.repo.sourceFindings.some((finding) => finding.id === "repo.manifest_route_missing")); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); diff --git a/packages/cli/test/repo-manifests.test.mjs b/packages/cli/test/repo-manifests.test.mjs index 2818d1a..965a25d 100644 --- a/packages/cli/test/repo-manifests.test.mjs +++ b/packages/cli/test/repo-manifests.test.mjs @@ -52,8 +52,8 @@ test("reads Next.js prerender manifest and reports missing generated routes", () 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-")); +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"); @@ -79,47 +79,7 @@ test("reads Astro manifest routes when present", () => { ] }); - assert.deepEqual(result.frameworkManifests, [ - { - type: "astro_manifest", - path: path.join(manifestDir, "manifest.json"), - routes: ["/", "/services/"] - } - ]); - assert.deepEqual(result.sourceFindings, []); -}); - -test("ignores Astro endpoint routes when checking generated HTML", () => { - const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-astro-endpoint-manifest-")); - const staticDir = path.join(repoPath, "dist"); - const manifestDir = path.join(repoPath, ".astro"); - writeHtml(path.join(staticDir, "index.html"), "Home"); - fs.mkdirSync(manifestDir, { recursive: true }); - fs.writeFileSync( - path.join(manifestDir, "manifest.json"), - JSON.stringify({ - routes: [ - { route: "/", type: "page" }, - { route: "/rss.xml", type: "endpoint" }, - { route: "/api/data.json", type: "endpoint" } - ] - }), - ); - - const result = analyzeFrameworkRouteManifests({ - repoPath, - staticDir, - detectedFramework: "astro", - staticRoutes: [{ type: "static_html", route: "/", path: path.join(staticDir, "index.html") }] - }); - - assert.deepEqual(result.frameworkManifests, [ - { - type: "astro_manifest", - path: path.join(manifestDir, "manifest.json"), - routes: ["/"] - } - ]); + assert.deepEqual(result.frameworkManifests, []); assert.deepEqual(result.sourceFindings, []); }); @@ -204,29 +164,6 @@ test("ignores valid Next.js manifest files with unrecognized route schema", () = assert.deepEqual(result.sourceFindings, []); }); -test("ignores valid Astro manifest files with unrecognized route schema", () => { - const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-astro-unknown-schema-")); - const staticDir = path.join(repoPath, "dist"); - const manifestDir = path.join(repoPath, ".astro"); - 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, "manifest.json"), JSON.stringify({ assets: [] })); - - const result = analyzeFrameworkRouteManifests({ - repoPath, - staticDir, - detectedFramework: "astro", - 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"); diff --git a/scripts/validate-skill.mjs b/scripts/validate-skill.mjs index 05dd12f..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", @@ -60,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", @@ -75,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",