import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import type { InitParams, DocgraphToolDetails } from "../types.js"; import { readFileSafe, writeFileSafe, ensureDir, renderMetadata, syncBacklog, relativeLink, docLabel, contentText, } from "../utils.js"; // ── Document Scaffolding Templates ───────────────────────────────────── const TODAY = new Date().toISOString().slice(0, 10); /** * Render "- [Label](href) — note" doc links relative to `sourcePath`, with * hrefs computed via path utilities instead of hardcoded strings. */ function docLinkList( sourcePath: string, refs: Array<{ path: string; note: string }>, ): string { return refs .map( (r) => `- [${docLabel(r.path)}](${relativeLink(sourcePath, r.path)}) — ${r.note}`, ) .join("\n"); } /** * Render a "| Document | Purpose | Read When |" index table relative to * `sourcePath`, with hrefs computed via path utilities. */ function docIndexTable( sourcePath: string, rows: Array<{ path: string; purpose: string; when: string }>, ): string { return [ "| Document | Purpose | Read When |", "|----------|----------|----------|", ...rows.map( (r) => `| [${docLabel(r.path)}](${relativeLink(sourcePath, r.path)}) | ${r.purpose} | ${r.when} |`, ), ].join("\n"); } /** Render a single "[Label](href)" doc link relative to `sourcePath`. */ function docLink(sourcePath: string, path: string): string { return `[${docLabel(path)}](${relativeLink(sourcePath, path)})`; } export interface ScaffoldEntry { path: string; title: string; metadata: { purpose: string; audience: "Human" | "AI" | "Both"; dependsOn: string[]; referencedBy: string[]; }; body: string; } export const SCAFFOLDS: ScaffoldEntry[] = [ { path: "README.md", title: "# Project", metadata: { purpose: "Primary documentation entry point for human developers.", audience: "Human", dependsOn: [], referencedBy: ["AGENTS.md"], }, body: `## Overview TODO: Brief project overview — what problem does this solve? ## Features TODO: Key features summary ## Technology Stack TODO: Languages, frameworks, databases ## Prerequisites TODO: What's needed before starting ## Installation TODO: Setup instructions ## Development Development is **test-driven**: every change starts with an automated test written before any production code. For every new feature, function, enhancement, refactor, or bug fix: 1. Write the test first — encode the expected behavior and acceptance criteria as an automated test. 2. Run the test and confirm that it fails for the expected reason (Red). 3. Write the minimum code necessary to make the test pass (Green). 4. Refactor the implementation while keeping all tests passing (Refactor). 5. Run the relevant test suite to verify no regressions. The test-first step is mandatory — do not skip it, even for small changes or seemingly trivial bug fixes. ## Testing TODO: How to run tests (framework, commands, CI) Every change ends by running the relevant test suite — the Red–Green–Refactor cycle is not complete until all tests pass. ## Deployment TODO: Deployment overview ## Project Structure TODO: Key directories and their purpose ## Contributing TODO: Contribution guidelines ## Documentation ${docLinkList("README.md", [ { path: "AGENTS.md", note: "AI coding agent entry point" }, { path: "docs/SPEC.md", note: "Functional specification" }, { path: "docs/ARCHITECTURE.md", note: "Technical architecture" }, { path: "docs/API.md", note: "API contracts" }, { path: "docs/DESIGN.md", note: "Design system" }, { path: "docs/ROADMAP.md", note: "Product roadmap" }, { path: "docs/BACKLOG.md", note: "Work queue" }, ])} `, }, { path: "AGENTS.md", title: "# AGENTS.md", metadata: { purpose: "Documentation router and entry point for AI coding agents. Helps AI determine which documentation is relevant.", audience: "AI", dependsOn: [], referencedBy: [ "README.md", "docs/SPEC.md", "docs/ARCHITECTURE.md", "docs/API.md", "docs/DESIGN.md", "docs/ROADMAP.md", "docs/BACKLOG.md", ], }, body: `## Project Overview TODO: Brief project overview for AI agents. ## Source of Truth The implementation is always authoritative. Documentation describes the implementation; if they disagree, trust the code and update the documentation. ## Development Workflow — TDD Is Mandatory Every change — new feature, function, enhancement, refactor, or bug fix — starts with an automated test. Write the test **before** writing or modifying any production code. 1. **Write the test first** — encode the expected behavior and acceptance criteria as an automated test. 2. **Run the test and confirm that it fails** for the expected reason (Red). 3. **Write the minimum code necessary** to make the test pass (Green). 4. **Refactor the implementation** while keeping all tests passing (Refactor). 5. **Run the relevant test suite** to verify that the change has not introduced regressions. Do not skip the test-first step, even for small changes or seemingly trivial bug fixes. ## Ticket Completion & Verification Rules - Mark a checklist item \`- [x]\` under \`## Acceptance Criteria\` or \`## Definition of Done\` **only after the criterion is verified** — never from code inspection or assumption alone. - For criteria that can be verified automatically, run the relevant tests, type checks, lint, or build before marking them complete. - Criteria that require human/manual testing remain \`- [ ]\` unless that verification has actually been performed. - For each criterion that still requires human verification, add an explicit HTML comment **inline on the same line** (after the criterion text) describing the exact steps and expected result. The ticket tooling preserves the checkbox state and inline comments across updates; a comment written on its own line would otherwise be rewritten as a separate unchecked item. For verified criteria, record the evidence the same way, e.g.: - [x] pnpm dev serves a page at localhost:3000 - [ ] pnpm dev serves a page at localhost:3000 - Never claim that human verification was performed when it was not. - Do not remove, rewrite, weaken, or reinterpret acceptance criteria or Definition of Done items to make a ticket appear complete. - In the final implementation summary, explicitly report any outstanding human verification — identify which criteria remain unverified and what the human must test. If none is outstanding, state that explicitly. ## Documentation Philosophy - Every document has one clear responsibility - Avoid duplicated information — prefer linking - Optimize for selective retrieval (read 1–2 docs per task) - Every document begins with a metadata block ## Documentation Index ${docIndexTable("AGENTS.md", [ { path: "README.md", purpose: "Human onboarding", when: "Learning the project" }, { path: "docs/SPEC.md", purpose: "Functional requirements", when: "Building or modifying features" }, { path: "docs/ARCHITECTURE.md", purpose: "System architecture", when: "Refactoring or structural changes" }, { path: "docs/API.md", purpose: "API contracts", when: "Backend/frontend integration" }, { path: "docs/DESIGN.md", purpose: "Design system", when: "UI work" }, { path: "docs/ROADMAP.md", purpose: "Product direction", when: "Understanding long-term goals" }, { path: "docs/BACKLOG.md", purpose: "Active work queue", when: "Determining the next ticket" }, ])} ## Navigation Guide 1. Read \`AGENTS.md\` first to determine which documentation is relevant 2. Read only the required document(s) 3. Follow additional references only if necessary 4. Return to the codebase — the source of truth ## Updating Documentation When implementation changes, update the affected documentation immediately. Never leave stale documentation. `, }, { path: "docs/SPEC.md", title: "# Specification", metadata: { purpose: "Describes what the system does and why.", audience: "Both", dependsOn: ["AGENTS.md"], referencedBy: [ "README.md", "docs/ARCHITECTURE.md", "docs/API.md", "docs/DESIGN.md", "docs/BACKLOG.md", ], }, body: `## Product Overview TODO: High-level product description. ## Goals TODO: What does success look like? ## Functional Requirements TODO: Feature-by-feature requirements. ## User Workflows TODO: Key user journeys. ## Business Rules TODO: Domain logic and constraints. ## Constraints TODO: Technical, compliance, budget, timeline constraints. ## Assumptions TODO: What are we assuming to be true? ## Non-Functional Requirements - Performance - Security - Accessibility - Scalability `, }, { path: "docs/ARCHITECTURE.md", title: "# Architecture", metadata: { purpose: "Describes how the system is built.", audience: "Both", dependsOn: ["docs/SPEC.md"], referencedBy: [ "docs/SPEC.md", "docs/API.md", "docs/DESIGN.md", ], }, body: `## Technology Stack TODO: Runtime, frameworks, databases, platforms. ## Folder Structure TODO: Key directories and module layout. ## System Architecture TODO: High-level architecture diagram and description. ## Module Boundaries TODO: Module responsibilities and interfaces. ## Layers TODO: Layering strategy (presentation, domain, data, etc.) ## Services TODO: Service inventory and responsibilities. ## Database TODO: Schema, migrations, indexing strategy. ## Authentication & Authorization TODO: Auth flow and permission model. ## State Management TODO: Client and server state strategies. ## Integrations TODO: External services and APIs. ## Data Flow TODO: Request lifecycle and data paths. ## Event Flow TODO: Event sourcing, messaging, pub/sub. ## Design Patterns TODO: Patterns adopted across the codebase. ## Deployment Architecture TODO: Infrastructure, CI/CD, environments. `, }, { path: "docs/API.md", title: "# API", metadata: { purpose: "Describes communication contracts between system components.", audience: "Both", dependsOn: ["docs/SPEC.md", "docs/ARCHITECTURE.md"], referencedBy: ["docs/ARCHITECTURE.md"], }, body: `## Overview TODO: API philosophy and conventions. ## Endpoints TODO: REST/GraphQL/RPC endpoint documentation. ## Authentication TODO: Auth headers, tokens, sessions. ## Authorization TODO: Permission scopes, roles. ## Request/Response Formats TODO: Schemas and examples. ## Validation TODO: Input validation rules. ## Error Handling TODO: Error codes, response format. ## Versioning TODO: API versioning strategy. `, }, { path: "docs/DESIGN.md", title: "# Design System", metadata: { purpose: "Describes the project's design system.", audience: "Both", dependsOn: ["docs/SPEC.md"], referencedBy: ["docs/ARCHITECTURE.md"], }, body: `## Design Philosophy TODO: Design principles and UX approach. ## Accessibility TODO: A11y standards and practices. ## Color Palette TODO: Colors, tokens, semantics. ## Typography TODO: Fonts, scale, weights. ## Spacing TODO: Spacing scale and usage. ## Components TODO: Component inventory. ## Icons TODO: Icon set and usage. ## Responsive Behavior TODO: Breakpoints and adaptive strategies. ## Interaction Patterns TODO: Common interaction patterns. ## Motion TODO: Animation principles and tokens. ## Design Tokens TODO: Token definitions. `, }, { path: "docs/ROADMAP.md", title: "# Roadmap", metadata: { purpose: "Describes the long-term product direction.", audience: "Both", dependsOn: ["docs/SPEC.md"], referencedBy: [ "README.md", "AGENTS.md", "docs/BACKLOG.md", ], }, body: `## Vision TODO: Long-term product vision. ## Milestones ### v0.1 TODO: First milestone goals. ### v1.0 TODO: v1 goals. ## Strategic Goals TODO: Key strategic objectives. > This document contains high-level planning only. See ${docLink("docs/ROADMAP.md", "docs/BACKLOG.md")} for the active work queue. `, }, { path: "docs/BACKLOG.md", title: "# Backlog", metadata: { purpose: "Canonical work queue for the project.", audience: "Both", dependsOn: ["docs/ROADMAP.md"], referencedBy: [ "AGENTS.md", "docs/ROADMAP.md", "docs/tickets/", ], }, body: `> Auto-generated index of tickets in \`docs/tickets/\`. Regenerated by \`docgraph_init\`, \`docgraph_ticket_create\`, and \`docgraph_ticket_update\`. ## Backlog ## Ready ## In Progress ## Review ## Done `, }, ]; export interface ScaffoldDocumentOptions { /** ISO date (yyyy-mm-dd) written into the `Last Updated` metadata field. */ lastUpdated: string; /** Optional project name override for README.md. */ projectName?: string; /** Override the scaffold body (used by --force to preserve existing content). */ body?: string; } /** * Build the full markdown document for a scaffold entry. * * The metadata block and any internal links are rendered relative to the * entry's own path via `relativeLink`, so a doc under `docs/` links to * `SPEC.md` while `README.md` at the repo root links to `docs/SPEC.md`. */ export function renderScaffoldDocument( sc: ScaffoldEntry, opts: ScaffoldDocumentOptions, ): string { const title = sc.path === "README.md" && opts.projectName ? `# ${opts.projectName}` : sc.title; const metaBlock = renderMetadata( { ...sc.metadata, lastUpdated: opts.lastUpdated, sourceOfTruth: "Codebase (implementation is authoritative)", }, sc.path, ); return `${title}\n\n${metaBlock}\n\n${opts.body ?? sc.body}`; } export function registerDocgraphInit(pi: ExtensionAPI): void { pi.registerTool({ name: "docgraph_init", label: "Docgraph Init", description: "Initialize or verify the AI-native documentation scaffolding (AGENTS.md, README.md, docs/*). Creates missing files with TODO placeholders. Safe to run multiple times.", parameters: Type.Object({ force: Type.Optional( Type.Boolean({ description: "Re-scaffold even if already initialized (preserves existing content)", }), ), projectName: Type.Optional( Type.String({ description: "Override project name in README.md" }), ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const p = params as InitParams; const cwd = ctx.cwd; // Check existing scaffolds — when force is set, still check which files exist const created: string[] = []; const skipped: string[] = []; const errors: string[] = []; ensureDir("docs", cwd); ensureDir("docs/tickets", cwd); for (const sc of SCAFFOLDS) { const existing = readFileSafe(sc.path, cwd); if (existing !== null && !p.force) { skipped.push(sc.path); continue; } // If we already have content, try to preserve the body while updating // the metadata block let preservedBody: string | undefined; if (existing !== null && p.force) { // Extract existing body (everything after the metadata block) // Keep existing body if it exists, otherwise use scaffold const bodyStart = existing.indexOf("\n## "); if (bodyStart > 0) { preservedBody = existing.slice(bodyStart).trimStart(); } } const content = renderScaffoldDocument(sc, { lastUpdated: TODAY, projectName: p.projectName, body: preservedBody, }); if (writeFileSafe(sc.path, content, cwd)) { created.push(sc.path); } else { errors.push(`Failed to write ${sc.path}`); } } // (Re)generate docs/BACKLOG.md as the canonical index derived from any // existing tickets under docs/tickets/. Keeps it consistent with the // ticket files even when init is re-run without --force. if (!syncBacklog(cwd)) { errors.push("Failed to generate docs/BACKLOG.md"); } const state = { initialized: true, schemaVersion: 1, }; // Ensure the session status reflects that docgraph is now initialized, // even though the status was previously set to "not initialized" at // session start (before any tools had run)." ctx.ui.setStatus("docgraph", "Docgraph: initialized"); const summary = created.length > 0 ? `Created: ${created.join(", ")}. ` : "" + (skipped.length > 0 ? `Skipped (already exist): ${skipped.join(", ")}. ` : "") + (errors.length > 0 ? `Errors: ${errors.join("; ")}` : ""); return { content: [ { type: "text", text: summary || "All documentation files are already initialized. Use --force to re-scaffold.", }, ], details: { action: "init", state, created, skipped, errors, } as DocgraphToolDetails & { created: string[]; skipped: string[]; errors: string[] }, }; }, renderCall(_args, theme) { let text = theme.fg("toolTitle", theme.bold("docgraph-init ")); text += theme.fg("muted", "scaffold documentation"); return new Text(text, 0, 0); }, renderResult(result, _opts, theme) { const details = result.details as DocgraphToolDetails & { created?: string[]; skipped?: string[]; errors?: string[]; }; if (!details) { return new Text( theme.fg("muted", contentText(result.content?.[0])), 0, 0, ); } let out = ""; if (details.created?.length) { out += theme.fg("success", `Created ${details.created.length} file(s):\n`); for (const f of details.created) out += ` ${theme.fg("muted", f)}\n`; } if (details.skipped?.length) { out += theme.fg("dim", `Skipped ${details.skipped.length} existing file(s)\n`); } if (details.errors?.length) { out += theme.fg("error", `Errors: ${details.errors.join(", ")}`); } return new Text( out || theme.fg("muted", "Already initialized"), 0, 0, ); }, }); }