/** * Pure renderer for `docs/README.md`. * * The index is intentionally short and mechanical — a stable pointer surface humans (and * agents) can use to find the canonical Tier-1 docs + per-layer agent docs. Layout is * dictated by the plan's "Index docs/README.md shape" section. */ import type { HarnessLayerRule } from "../../types.js"; import { attachProvenance, computeBodyContentHash, } from "./provenance.js"; export interface RenderDocsIndexInput { layers: readonly HarnessLayerRule[]; /** Session that produced this index. */ sessionId: string; /** ISO timestamp used in the provenance marker + "Generated by" line. */ generatedAt: string; /** Hard cap on the index LOC (default 50). */ maxLoc?: number; } export const DEFAULT_INDEX_MAX_LOC = 50; /** * Render the index. Deterministic given the same input. The output starts with the * provenance marker line and ends with a trailing newline. */ export function renderDocsIndex(input: RenderDocsIndexInput): string { if (input.layers.length === 0) { throw new Error("renderDocsIndex requires at least one layer"); } const maxLoc = input.maxLoc ?? DEFAULT_INDEX_MAX_LOC; const sortedLayers = [...input.layers].sort((a, b) => a.layer.localeCompare(b.layer)); const lines: string[] = []; lines.push("# Repo docs"); lines.push(""); lines.push(`Generated by /supi:harness on ${input.generatedAt}. Do not edit by hand.`); lines.push(""); lines.push("## Agent contract"); lines.push("- AGENTS.md — global agent rules"); lines.push("- docs/architecture.md — layer rules table"); lines.push("- docs/golden-principles.md — mechanical invariants"); lines.push(""); lines.push("## Layer docs"); lines.push(""); lines.push("| Layer | Files | Doc |"); lines.push("|---|---|---|"); for (const layer of sortedLayers) { const globs = layer.globs.map((g) => `\`${g}\``).join(", "); lines.push(`| ${layer.layer} | ${globs || "—"} | docs/layers/${layer.layer}.md |`); } const body = lines.join("\n") + "\n"; if (countLines(body) + 1 /* marker line */ > maxLoc) { // Should be unreachable for any sane layer count; surface as a hard failure so the // caller can cap layer count if this ever fires. throw new Error( `renderDocsIndex output is ${countLines(body) + 1} LOC; max is ${maxLoc} (layers=${input.layers.length})`, ); } return attachProvenance(body, { sessionId: input.sessionId, generatedAt: input.generatedAt, contentHash: computeBodyContentHash(body), }); } function countLines(text: string): number { if (text.length === 0) return 0; let count = 1; for (let i = 0; i < text.length; i += 1) { if (text.charCodeAt(i) === 10) count += 1; } if (text.charCodeAt(text.length - 1) === 10) count -= 1; return count; }