/** * VALIDATE stage runner. * * Runs every sub-check the harness installed: * - lint (delegates to the configured tool via platform.exec; structured pass/fail), * - structural test (placeholder — emitter-supplied; harness only records the result), * - eval (placeholder), * - cross-link check (every artifact referenced from AGENTS.md / docs/ exists), * - schema check (Discover / Design / Plan artifacts validate), * - discover-drift (re-run Discover and diff against the saved artifact), * - anti-slop scan (selected backend audit), * - score computation, * - synthetic edit test (hooks fire). * * Returns a `HarnessValidateReport` and persists it. The command handler owns the * user-accept gate. */ import * as fs from "node:fs"; import * as path from "node:path"; import { parse as parseYaml } from "yaml"; import type { Platform } from "../../platform/types.js"; import type { HarnessAntiSlopBackend, HarnessLayerRule, HarnessSlopQueueEntry, HarnessValidateCheck, HarnessValidateFinding, HarnessValidateReport, } from "../../types.js"; import { parseArchitectureMarkdown } from "../anti_slop/architecture-parser.js"; import type { SlopBackend, SlopFinding } from "../anti_slop/backend.js"; import { computeQueueEntryId } from "../anti_slop/queue.js"; import { computeScore, scoreFloorPassed } from "../anti_slop/score.js"; import { runSyntheticEditTest } from "../anti_slop/synthetic-edit-test.js"; import { type HarnessStageRunResult, type HarnessStageRunner, type HarnessStageRunnerContext, nowIso, } from "../stage-runner.js"; import { appendScoreHistory, appendSlopQueueEntry, loadHarnessDesignSpecJson, loadHarnessDiscover, saveHarnessRepoScore, saveHarnessValidateReport, readSlopQueue, } from "../storage.js"; import { buildDiscoverArtifact } from "./discover.js"; import { getHarnessAgentsMdPath, getHarnessArchitectureDocPath, getHarnessGoldenPrinciplesPath, getHarnessSessionDir, } from "../project-paths.js"; import { resolveDocsConfig } from "../docs/config.js"; import { matchesLayerGlob } from "../docs/glob-match.js"; import { selectRepresentativeFiles } from "../docs/representative-files.js"; import { computeLayerSourceHash, sha256 as sha256Hash } from "../docs/source-hash.js"; import { validateLayerDocMarkdown } from "../docs/validator.js"; import { computeLayerAddendum } from "../hooks/layer-context-inject.js"; export interface ValidateStageInput { /** Selected backend (from the design spec). */ backend: HarnessAntiSlopBackend; /** Backend adapter (only consulted when `backend !== "supi-native"`). */ adapter?: SlopBackend; /** Score floor configuration. */ scoreFloor: { strict: number; lenient: number; release_blocking: boolean }; /** Hook config snapshot for the synthetic-edit test. */ hooks: { pre_edit_dupe_probe: { enabled: boolean }; post_session_sweep: { enabled: boolean }; layer_context_inject: { enabled: boolean; addendum_max_chars: number }; }; } interface CheckResult { name: string; passed: boolean; summary: string; findings: HarnessValidateFinding[]; durationMs?: number; } type CheckContract = Pick; const CHECK_CONTRACTS: Readonly> = { "cross-link-check": { invariant: "Agent-facing harness docs must exist and point maintainers to the architecture and golden-principles contracts.", proves: "AGENTS.md, docs/architecture.md, and docs/golden-principles.md exist, and AGENTS.md references the core docs.", doesNotProve: "The docs are semantically complete, current, or sufficient for every agent client.", artifact: "validate-report.json findings plus filesystem state", failSafe: "Missing or unreadable docs produce error findings and block validation.", }, "schema-check": { invariant: "Persisted pipeline artifacts must remain readable before later stages claim continuity.", proves: "discover.json can be loaded for the active harness session.", doesNotProve: "The discovered facts are fresh or semantically exhaustive.", artifact: "validate-report.json schema-check entry", failSafe: "Missing or invalid JSON produces an error finding and blocks validation.", }, "discover-drift": { invariant: "The saved discovery artifact should not silently diverge from the current repository profile.", proves: "A fresh discovery pass did not reveal language-level drift that invalidates the saved artifact.", doesNotProve: "Every framework, script, dependency, or architecture convention is unchanged.", artifact: "validate-report.json discover-drift entry", failSafe: "Missing discovery data blocks; detected drift is recorded as warning/info with remediation.", }, "anti-slop-scan": { invariant: "New duplicate, dead-code, and layer-drift findings must be visible in the persistent slop queue.", proves: "The selected backend either ran and returned findings, or an explicitly soft-failed optional backend state was recorded.", doesNotProve: "The selected backend can detect semantic bugs, product regressions, or unsupported language patterns.", artifact: "validate-report.json, queue.jsonl, score.json", failSafe: "Missing optional backend configuration warns; backend runtime errors become blocking findings.", }, "synthetic-edit-test": { invariant: "Installed anti-slop hooks must fire against a controlled representative edit before the harness claims runtime guardrails work.", proves: "The enabled in-process hook handlers respond to the synthetic edit fixture without reported failures.", doesNotProve: "Hook latency, all real editor events, or every repository-specific layer edge case.", artifact: "validate-report.json synthetic-edit-test entry", failSafe: "Hook failures are emitted as error findings and block validation.", }, "docs-validation": { invariant: "Per-layer docs must remain valid, complete, indexed, and integrated with the layer-context-inject hook.", proves: "Every docs/layers/*.md passes the validator, docs/README.md ↔ filesystem are consistent, the hook prefers the per-layer doc, and drift between layer inputs and recorded sourceHash is surfaced.", doesNotProve: "The doc content is high quality, or that the layer rules themselves match the current codebase.", artifact: "validate-report.json docs-validation entry plus docs/layers/*.md + docs/README.md", failSafe: "Missing docs/layers/ short-circuits the check as a no-op; structural failures emit warnings and surface in the queue.", }, "ci-local-wiring": { invariant: "Every harness validation gate must have one local command and CI must invoke that command instead of relying on human memory.", proves: "The configured local command exists and the configured CI workflow calls it on the selected PR trigger.", doesNotProve: "The command's inner toolchain is installed, every gate is semantically sufficient, or CI provider secrets/runners are available.", artifact: "validate-report.json plus package.json and CI workflow contents", failSafe: "Missing package scripts or workflow files emit error findings and block validation.", }, }; function attachCheckContract(check: CheckResult): HarnessValidateCheck { const contract = CHECK_CONTRACTS[check.name] ?? { invariant: `The ${check.name} gate must state the rule it protects.`, proves: check.summary, doesNotProve: "No explicit blind spot was registered for this check.", artifact: "validate-report.json", failSafe: "Unknown checks should be given an explicit contract before release.", }; return { ...check, ...contract }; } async function checkCrossLinks(cwd: string): Promise { const startedAt = Date.now(); const findings: HarnessValidateFinding[] = []; const agentsPath = path.join(cwd, "AGENTS.md"); const architecturePath = path.join(cwd, "docs", "architecture.md"); const goldenPath = path.join(cwd, "docs", "golden-principles.md"); const expected = [ { file: "AGENTS.md", path: agentsPath }, { file: "docs/architecture.md", path: architecturePath }, { file: "docs/golden-principles.md", path: goldenPath }, ]; for (const target of expected) { if (!fs.existsSync(target.path)) { findings.push({ severity: "error", file: target.file, message: `${target.file} is missing`, remediation: `Run /supi:harness implement (or rebuild) to regenerate ${target.file}.`, source: "cross-link-check", }); } } // AGENTS.md must reference both core docs. if (fs.existsSync(agentsPath)) { try { const agents = fs.readFileSync(agentsPath, "utf8"); if (!agents.includes("docs/architecture.md")) { findings.push({ severity: "warning", file: "AGENTS.md", message: "AGENTS.md does not reference docs/architecture.md", remediation: "Add a link to docs/architecture.md from AGENTS.md.", source: "cross-link-check", }); } if (!agents.includes("docs/golden-principles.md")) { findings.push({ severity: "warning", file: "AGENTS.md", message: "AGENTS.md does not reference docs/golden-principles.md", remediation: "Add a link to docs/golden-principles.md from AGENTS.md.", source: "cross-link-check", }); } } catch { findings.push({ severity: "error", file: "AGENTS.md", message: "AGENTS.md exists but is unreadable", remediation: "Inspect filesystem permissions on AGENTS.md.", source: "cross-link-check", }); } } const passed = !findings.some((f) => f.severity === "error"); return { name: "cross-link-check", passed, summary: passed ? "All required artifacts exist and AGENTS.md references core docs." : `${findings.length} issue(s) detected.`, findings, durationMs: Date.now() - startedAt, }; } async function checkSchema( paths: HarnessStageRunnerContext["paths"], cwd: string, sessionId: string, ): Promise { const startedAt = Date.now(); const findings: HarnessValidateFinding[] = []; const discover = loadHarnessDiscover(paths, cwd, sessionId); if (!discover.ok) { findings.push({ severity: "error", file: "discover.json", message: `discover.json is missing or invalid: ${discover.error.message}`, remediation: "Re-run /supi:harness discover.", source: "schema-check", }); } return { name: "schema-check", passed: findings.length === 0, summary: findings.length === 0 ? "All schemas valid." : "Schema validation failed.", findings, durationMs: Date.now() - startedAt, }; } function scriptNameFromLocalCommand(command: string): string | null { const trimmed = command.trim(); const runMatch = /^(?:bun|npm|pnpm|yarn)\s+run\s+([^\s]+)$/.exec(trimmed); if (runMatch) return runMatch[1]; const pnpmOrYarnMatch = /^(?:pnpm|yarn)\s+([^\s]+)$/.exec(trimmed); if (pnpmOrYarnMatch) return pnpmOrYarnMatch[1]; return null; } /** * Conservative check that a GitHub Actions workflow grants the write scope needed for * the harness PR comment. Only inspects the `permissions:` block — if a user has wired * a deploy token or `secrets.GITHUB_TOKEN` with a custom scope, this returns false and * the warning is a no-op false positive. That's deliberate: a false-positive warning is * cheaper than a silent 403 the first time CI tries to post. * * Caveats this regex deliberately does not handle: * - Job-scoped `permissions:` blocks that grant `pull-requests: write` to a job other * than the one running `/supi:harness pr-comment`. Detecting that requires real YAML * parsing; a false-positive warning is again cheaper than guessing wrong. */ function workflowGrantsPrCommentPermission(workflow: string): boolean { // Strip whole-line YAML comments before matching so a commented-out // `# pull-requests: write` does not falsely register as a grant. const stripped = workflow.replace(/^[ \t]*#.*$/gm, ""); // Match either inline mapping `permissions: { pull-requests: write }`, the block form // with `pull-requests: write` on its own line, or the broad `permissions: write-all`. if (/permissions:\s*write-all\b/.test(stripped)) return true; if (/\bpull-requests:\s*write\b/.test(stripped)) return true; return false; } /** * Structurally inspect the rendered workflow for a healthy `verify-pr-source` job. * * Returns `null` when the job is present, gated on the configured `mainBranch`, and * its shell guard names the configured `devBranch`. Returns a short description of * the first mismatch otherwise. * * Parsing the YAML (rather than substring-matching the source) is what catches: * - a comment mentioning `verify-pr-source` (the substring lives, the job doesn't), * - a stale job referencing a previous mainBranch/devBranch pair after the spec * was updated and the workflow was not re-rendered, * - a structurally wrong job (no `if`, no `run` block, etc.). * * Unparseable YAML falls back to a substring sanity check so a malformed workflow * still reports *something* useful instead of silently passing. */ function inspectPrSourceGuardrailJob( workflow: string, mainBranch: string, devBranch: string, ): string | null { let doc: unknown; try { doc = parseYaml(workflow); } catch { return workflow.includes("verify-pr-source") ? "workflow YAML is unparseable; cannot confirm guardrail is correct" : "workflow YAML is unparseable and contains no verify-pr-source job"; } if (!doc || typeof doc !== "object") return "workflow root is not a mapping"; const jobs = (doc as { jobs?: unknown }).jobs; if (!jobs || typeof jobs !== "object") return "workflow has no `jobs:` mapping"; const job = (jobs as Record)["verify-pr-source"]; if (!job || typeof job !== "object") return "job `verify-pr-source` is not defined"; const ifExpr = (job as { if?: unknown }).if; if (typeof ifExpr !== "string" || !ifExpr.includes(`'${mainBranch}'`)) { return `job \`verify-pr-source\` is not gated on mainBranch '${mainBranch}'`; } // The shell guard lives in steps[*].run; concatenate every run block we find so we // do not depend on the exact step order or composition. const steps = (job as { steps?: unknown }).steps; const runBlocks: string[] = []; if (Array.isArray(steps)) { for (const step of steps) { if (step && typeof step === "object") { const run = (step as { run?: unknown }).run; if (typeof run === "string") runBlocks.push(run); } } } const combinedRun = runBlocks.join("\n"); if (!combinedRun.includes(`"${devBranch}"`)) { return `job \`verify-pr-source\` does not guard against devBranch '${devBranch}'`; } return null; } async function checkCiLocalWiring( paths: HarnessStageRunnerContext["paths"], cwd: string, sessionId: string, ): Promise { const startedAt = Date.now(); const findings: HarnessValidateFinding[] = []; const spec = loadHarnessDesignSpecJson(paths, cwd, sessionId); if (!spec.ok) { return { name: "ci-local-wiring", passed: true, summary: "Skipped: no design-spec.json available for CI wiring validation.", findings, durationMs: Date.now() - startedAt, }; } if (!spec.value.ci) { return { name: "ci-local-wiring", passed: false, summary: "design-spec.json does not contain CI/local wiring configuration.", findings: [{ severity: "error", file: "design-spec.json", message: "Design spec is missing ci configuration.", remediation: "Re-run /supi:harness design so CI trigger and local quality command are recorded.", source: "ci-local-wiring", }], durationMs: Date.now() - startedAt, }; } const scriptName = scriptNameFromLocalCommand(spec.value.ci.localCommand); const packageJsonPath = path.join(cwd, "package.json"); if (scriptName) { if (!fs.existsSync(packageJsonPath)) { findings.push({ severity: "error", file: "package.json", message: `Local quality command ${spec.value.ci.localCommand} requires package.json, but package.json is missing.`, remediation: `Create package.json with a scripts.${scriptName} entry or choose a non-package local command.`, source: "ci-local-wiring", }); } else { try { const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { scripts?: Record }; if (typeof parsed.scripts?.[scriptName] !== "string") { findings.push({ severity: "error", file: "package.json", message: `package.json does not define scripts.${scriptName} for ${spec.value.ci.localCommand}.`, remediation: `Add scripts.${scriptName} and make it run the validation gates from design-spec.json.`, source: "ci-local-wiring", }); } } catch (error) { findings.push({ severity: "error", file: "package.json", message: `package.json is unreadable or invalid JSON: ${error instanceof Error ? error.message : String(error)}`, remediation: "Fix package.json before validating CI/local quality wiring.", source: "ci-local-wiring", }); } } } const workflowPath = path.join(cwd, spec.value.ci.workflowPath); if (!fs.existsSync(workflowPath)) { findings.push({ severity: "error", file: spec.value.ci.workflowPath, message: `CI workflow ${spec.value.ci.workflowPath} is missing.`, remediation: `Create the workflow and have it run ${spec.value.ci.localCommand}.`, source: "ci-local-wiring", }); } else { try { const workflow = fs.readFileSync(workflowPath, "utf8"); if (!workflow.includes(spec.value.ci.localCommand)) { findings.push({ severity: "error", file: spec.value.ci.workflowPath, message: `CI workflow does not invoke ${spec.value.ci.localCommand}.`, remediation: "Call the local harness quality command from CI instead of duplicating gate commands inline.", source: "ci-local-wiring", }); } if (spec.value.ci.trigger.mode === "branches") { for (const branch of spec.value.ci.trigger.branches) { if (!workflow.includes(branch)) { findings.push({ severity: "error", file: spec.value.ci.workflowPath, message: `CI workflow does not mention configured PR target branch ${branch}.`, remediation: `Add ${branch} to the pull_request.branches trigger or update the design spec.`, source: "ci-local-wiring", }); } } } // Informational: when prComment is enabled but the workflow lacks // `pull-requests: write`, the `gh api` upsert will fail with 403. Surface a warning // so the user notices before the first failed PR run. if (spec.value.ci.prComment?.enabled && !workflowGrantsPrCommentPermission(workflow)) { findings.push({ severity: "warning", file: spec.value.ci.workflowPath, message: "CI workflow does not grant `pull-requests: write` but prComment.enabled is true.", remediation: "Add `permissions: { pull-requests: write }` to the workflow so /supi:harness pr-comment can post.", source: "ci-local-wiring", }); } // When the design recorded git verification with `enforceMainFromDevOnly: true`, // confirm the workflow actually contains the `verify-pr-source` job that the // implement stage is supposed to render, with an `if:` that names the current // mainBranch and a shell guard that names the current devBranch. A missing or // stale job means CI-side enforcement is silently absent — surface it as an error // so the user notices. Substring search is intentionally avoided: a comment // containing "verify-pr-source", or a stale job from a previous main/dev pairing, // would pass a `workflow.includes(...)` check but provide no real enforcement. const git = spec.value.ci.git; if (git && git.enforceMainFromDevOnly && git.devBranch) { const issue = inspectPrSourceGuardrailJob(workflow, git.mainBranch, git.devBranch); if (issue) { findings.push({ severity: "error", file: spec.value.ci.workflowPath, message: `CI workflow's verify-pr-source job is missing or stale: ${issue}.`, remediation: `Re-run /supi:harness so the workflow re-renders with the dev/main guardrail (dev=${git.devBranch}, main=${git.mainBranch}).`, source: "ci-local-wiring", }); } } } catch (error) { findings.push({ severity: "error", file: spec.value.ci.workflowPath, message: `CI workflow is unreadable: ${error instanceof Error ? error.message : String(error)}`, remediation: "Fix workflow file permissions or contents.", source: "ci-local-wiring", }); } } // Bubble git-verification findings recorded by the interactive QA step into the // validate report. The QA helper records non-fatal issues (gh missing, no permission) // so the user sees them in the validation output even if the workflow itself is fine. // When a bubbled finding has no remediation of its own, fall back to the manual // instructions doc — but only when one was actually written (`manualInstructionsPath` // is set). The previous literal-`` placeholder was never substituted and // pointed at a file that was never written for declined / completed verifications. const gitVerification = spec.value.ci.git?.verification; if (gitVerification) { const manualPath = gitVerification.manualInstructionsPath ? path.join(getHarnessSessionDir(paths, cwd, sessionId), gitVerification.manualInstructionsPath) : null; const fallbackRemediation = manualPath ? `See ${manualPath} for manual steps.` : "Re-run /supi:harness to retry git verification."; for (const finding of gitVerification.findings) { findings.push({ severity: finding.severity, file: spec.value.ci.workflowPath, message: `git-verify: ${finding.message}`, remediation: finding.remediation ?? fallbackRemediation, source: "ci-local-wiring", }); } } return { name: "ci-local-wiring", passed: !findings.some((finding) => finding.severity === "error"), summary: findings.length === 0 ? "CI workflow invokes the local harness quality command." : `${findings.length} CI/local wiring issue(s) detected.`, findings, durationMs: Date.now() - startedAt, }; } async function checkDiscoverDrift( paths: HarnessStageRunnerContext["paths"], cwd: string, sessionId: string, now: string, ): Promise { const startedAt = Date.now(); const findings: HarnessValidateFinding[] = []; const saved = loadHarnessDiscover(paths, cwd, sessionId); if (!saved.ok) { findings.push({ severity: "error", file: "discover.json", message: "Cannot drift-check: saved discover artifact missing", remediation: "Re-run /supi:harness discover.", source: "discover-drift", }); return { name: "discover-drift", passed: false, summary: "discover.json missing", findings, durationMs: Date.now() - startedAt, }; } const fresh = buildDiscoverArtifact({ cwd, sessionId, now }); const savedLangs = new Set(saved.value.languages); const freshLangs = new Set(fresh.languages); for (const lang of freshLangs) { if (!savedLangs.has(lang)) { findings.push({ severity: "warning", file: "discover.json", message: `New language detected since saved discover: ${lang}`, remediation: "Re-run /supi:harness discover to refresh the artifact.", source: "discover-drift", }); } } for (const lang of savedLangs) { if (!freshLangs.has(lang)) { findings.push({ severity: "info", file: "discover.json", message: `Language no longer present: ${lang}`, remediation: "Re-run /supi:harness discover to refresh the artifact.", source: "discover-drift", }); } } const passed = !findings.some((f) => f.severity === "error"); return { name: "discover-drift", passed, summary: passed ? "Discover artifact reflects current repo state." : "Discover artifact is stale.", findings, durationMs: Date.now() - startedAt, }; } async function checkAntiSlopScan( platform: Platform, cwd: string, input: ValidateStageInput, ): Promise<{ result: CheckResult; scanFindings: SlopFinding[]; slopCounts: { duplicates: number; deadCode: number; layerViolations: number; other: number }; }> { const startedAt = Date.now(); const findings: HarnessValidateFinding[] = []; const slopCounts = { duplicates: 0, deadCode: 0, layerViolations: 0, other: 0 }; if (input.backend === "supi-native" || !input.adapter) { return { result: { name: "anti-slop-scan", passed: true, summary: "Skipped (supi-native backend has no external scan).", findings, durationMs: Date.now() - startedAt, }, scanFindings: [], slopCounts, }; } const scan = await input.adapter.audit(platform, { cwd }); if (!scan.ok) { findings.push({ severity: scan.reason === "not-installed" || scan.reason === "config-missing" ? "warning" : "error", file: ".", message: `${input.backend} ${scan.reason}: ${scan.message}`, remediation: "Install the backend or fall back to supi-native in the design spec.", source: "anti-slop-scan", }); return { result: { name: "anti-slop-scan", passed: scan.reason === "not-installed" || scan.reason === "config-missing", // soft-fail summary: scan.message, findings, durationMs: Date.now() - startedAt, }, scanFindings: [], slopCounts, }; } for (const finding of scan.findings) { if (finding.kind === "duplicate") slopCounts.duplicates += 1; else if (finding.kind === "dead-code") slopCounts.deadCode += 1; else if (finding.kind === "layer-violation") slopCounts.layerViolations += 1; else slopCounts.other += 1; } return { result: { name: "anti-slop-scan", passed: scan.findings.length === 0, summary: scan.findings.length === 0 ? "No slop findings." : `${scan.findings.length} slop finding(s) recorded; see queue.`, findings, durationMs: Date.now() - startedAt, }, scanFindings: scan.findings, slopCounts, }; } function checkSyntheticEdit(input: ValidateStageInput, layerRules: readonly HarnessLayerRule[]): CheckResult { const startedAt = Date.now(); const test = runSyntheticEditTest({ layerRules, hooks: input.hooks, }); const findings: HarnessValidateFinding[] = test.failures.map((failure) => ({ severity: "error" as const, file: "synthetic-edit-test", message: failure, remediation: "Inspect the failing hook implementation; test runs against in-process handlers.", source: "synthetic-edit-test", })); return { name: "synthetic-edit-test", passed: test.failures.length === 0, summary: test.failures.length === 0 ? `${test.hooksFired.length} hook(s) fired as expected.` : `${test.failures.length} synthetic-edit failure(s).`, findings, durationMs: Date.now() - startedAt, }; } function loadLayerRules(cwd: string): HarnessLayerRule[] { const archPath = path.join(cwd, "docs", "architecture.md"); if (!fs.existsSync(archPath)) return []; try { const md = fs.readFileSync(archPath, "utf8"); return parseArchitectureMarkdown(md); } catch { return []; } } /** * Validate the per-layer docs tree: doc validator on each file, index ↔ filesystem * consistency, hook integration smoke test, and sourceHash drift. Soft-failure: missing * `docs/layers/` short-circuits to a no-op pass — the docs stage is opt-in. */ async function checkDocsValidation( ctx: HarnessStageRunnerContext, layerRules: readonly HarnessLayerRule[], ): Promise { const startedAt = Date.now(); const findings: HarnessValidateFinding[] = []; const layersDir = path.join(ctx.cwd, "docs", "layers"); if (!fs.existsSync(layersDir)) { return { name: "docs-validation", passed: true, summary: "Per-layer docs disabled (no docs/layers/).", findings, durationMs: Date.now() - startedAt, }; } const config = resolveDocsConfig(ctx.paths, ctx.cwd); // ── Re-validate every layer doc ────────────────────────────────────── let docFiles: string[] = []; try { docFiles = fs.readdirSync(layersDir).filter((f) => f.endsWith(".md")).sort(); } catch (error) { findings.push({ severity: "warning", file: "docs/layers/", message: `unable to enumerate docs/layers/: ${error instanceof Error ? error.message : String(error)}`, remediation: "Inspect the docs/layers/ directory permissions and re-run validate.", source: "docs-validation", }); } for (const fileName of docFiles) { const layerId = fileName.replace(/\.md$/, ""); const layerPath = `docs/layers/${fileName}`; const docPath = path.join(layersDir, fileName); let contents: string; try { contents = fs.readFileSync(docPath, "utf8"); } catch (error) { findings.push({ severity: "warning", file: layerPath, message: `unable to read doc: ${error instanceof Error ? error.message : String(error)}`, remediation: "Re-run `/supi:harness docs` after fixing filesystem permissions.", source: "docs-validation", }); continue; } const recordedHash = readFrontmatterSourceHashForValidate(contents); const validation = validateLayerDocMarkdown(contents, { expectedLayerId: layerId, expectedSourceHash: recordedHash ?? "", maxDocLoc: config.max_per_doc_loc, maxAgentContextLoc: config.agent_context_loc, }); if (!validation.ok) { for (const err of validation.errors) { findings.push({ severity: "warning", file: layerPath, message: err, remediation: "Re-run `/supi:harness docs` to regenerate the per-layer doc.", source: "docs-validation", }); } } } // ── Index ↔ filesystem consistency ──────────────────────────────────── const indexPath = path.join(ctx.cwd, "docs", "README.md"); if (!fs.existsSync(indexPath)) { findings.push({ severity: "warning", file: "docs/README.md", message: "docs/layers/ exists but docs/README.md is missing.", remediation: "Re-run `/supi:harness docs` to regenerate the index.", source: "docs-validation", }); } else { let indexContents: string; try { indexContents = fs.readFileSync(indexPath, "utf8"); } catch (error) { findings.push({ severity: "warning", file: "docs/README.md", message: `unable to read docs/README.md: ${error instanceof Error ? error.message : String(error)}`, remediation: "Inspect the file permissions and re-run validate.", source: "docs-validation", }); indexContents = ""; } const referenced = new Set(); for (const match of indexContents.matchAll(/docs\/layers\/([A-Za-z0-9._-]+)\.md/g)) { referenced.add(match[1]); } const onDisk = new Set(docFiles.map((f) => f.replace(/\.md$/, ""))); for (const layerId of referenced) { if (!onDisk.has(layerId)) { findings.push({ severity: "warning", file: "docs/README.md", message: `index references docs/layers/${layerId}.md but the file is missing.`, remediation: "Run `/supi:harness docs` or delete the stale row from docs/README.md.", source: "docs-validation", }); } } for (const layerId of onDisk) { if (!referenced.has(layerId)) { findings.push({ severity: "warning", file: "docs/README.md", message: `docs/layers/${layerId}.md exists but is not listed in the index.`, remediation: "Run `/supi:harness docs` to refresh the index.", source: "docs-validation", }); } } } // ── Hook integration smoke test ────────────────────────────────────── for (const rule of layerRules) { const probeFile = pickSampleFileForLayer(ctx.cwd, rule); if (!probeFile) continue; const result = computeLayerAddendum({ cwd: ctx.cwd, candidateFile: probeFile, config: { enabled: true, addendum_max_chars: 800 }, }); const docExists = fs.existsSync(path.join(ctx.cwd, "docs", "layers", `${rule.layer}.md`)); if (docExists && result.reason !== "matched (per-layer doc)") { findings.push({ severity: "warning", file: `docs/layers/${rule.layer}.md`, message: `layer-context-inject hook returned "${result.reason}" despite the per-layer doc existing.`, remediation: "Verify the per-layer doc has a non-empty ## Agent context section.", source: "docs-validation", }); } } // ── Source-hash drift ──────────────────────────────────────────────── if (config.drift_warning.enabled) { const promptVersion = readDocsPromptVersion(); const allFiles = collectAllRepoFilesForValidate(ctx.cwd); const goldenPrinciples = readGoldenPrinciplesForValidate(ctx.cwd); for (const rule of layerRules) { const docPath = path.join(layersDir, `${rule.layer}.md`); if (!fs.existsSync(docPath)) continue; let contents: string; try { contents = fs.readFileSync(docPath, "utf8"); } catch (error) { findings.push({ severity: "warning", file: `docs/layers/${rule.layer}.md`, message: `unable to read doc for drift check: ${error instanceof Error ? error.message : String(error)}`, remediation: "Inspect the file permissions and re-run validate.", source: "docs-validation", }); continue; } const recordedHash = readFrontmatterSourceHashForValidate(contents); if (!recordedHash) continue; const globPaths = allFiles .filter((file) => rule.globs.some((g) => matchesLayerGlob(file, g))) .sort(); const repSelection = selectRepresentativeFiles({ cwd: ctx.cwd, files: globPaths }); const peerLayers = layerRules .filter((peer) => peer.layer !== rule.layer) .map((peer) => ({ id: peer.layer, description: peer.description ?? "" })); const currentHash = computeLayerSourceHash({ layerRule: rule, globPaths, representativeFiles: repSelection.entries.map((e) => ({ path: e.path, contentHash: e.contentHash, })), goldenPrinciples, peerLayers, promptVersion, }); if (currentHash !== recordedHash) { findings.push({ severity: "warning", file: `docs/layers/${rule.layer}.md`, message: `sourceHash drift: layer inputs changed since the doc was generated.`, remediation: "Run `/supi:harness docs` to regenerate the affected layer doc.", source: "docs-validation", }); } } } return { name: "docs-validation", // Findings are advisory only — they never block the stage; the report records them. passed: true, summary: findings.length === 0 ? "Per-layer docs validated." : `${findings.length} per-layer docs finding(s).`, findings, durationMs: Date.now() - startedAt, }; } function readFrontmatterSourceHashForValidate(markdown: string): string | null { let body = markdown; if (body.startsWith("