# docguard
> The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.

<!-- llms-full.txt — full-content form. The link-index form is llms.txt. -->
<!-- Generated by DocGuard (docguard llms --full). Regenerate after doc changes. -->

---

## docs-canonical/ARCHITECTURE.md
> System architecture, component boundaries, and tech stack

# Architecture

<!-- docguard:version 1.11.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-10-10 -->

| Metadata | Value |
|----------|-------|
| **Status** | ![Status](https://img.shields.io/badge/status-active-brightgreen) |
| **Version** | `1.11.0` |
| **Last Updated** | 2026-10-10 |
| **Project Size** | ~54K lines in 161 files across `cli/` — measured 2026-10-10 with `wc -l` over `cli/**/*.mjs`; re-measure rather than trust this figure |

---

## System Overview

DocGuard is a near-zero-dependency Node.js CLI tool. It carries one exact-pinned npm runtime dependency, `@babel/parser`, for AST-accurate JS/TS parsing, and uses the developer's own `python3` (no pip/npm dependency) for an AST-accurate Python tier. Both parsers load **optionally**. Route and schema scanners retain bounded fallbacks; architecture analysis reports Python import coverage as unsupported when the interpreter is absent rather than fabricating a graph. Other language scanners retain their declared regex (beta) scope. It enforces **Canonical-Driven Development (CDD)** — a methodology where documentation is the source of truth. DocGuard audits, scores, and guards project documentation. It generates AI-actionable fix prompts and integrates with CI/CD pipelines.

It targets development teams and AI coding agents that need to maintain documentation quality across projects of any stack (JavaScript, Python, Java, etc.).

## Component Map

| Component | Responsibility | Location | Key Files |
|-----------|---------------|----------|-----------|
| **CLI Entry Point** | Argument parsing, config loading, command routing | `cli/` | `docguard.mjs` |
| **Commands** | User-facing commands (the Daily 5 — init/guard/diff/sync/score — plus situational tools including reconcile, retire, specs, and `init --with` scaffolders) | `cli/commands/` | `*.mjs` |
| **Document lifecycle** | Finds exact terminal-status docs and completed-task review candidates; explicit retirement removes documentation from active context only after its source revision is reachable from a retained Git ref | `cli/scanners/document-lifecycle.mjs`, `cli/validators/document-lifecycle.mjs`, `cli/commands/retire.mjs` | Scanner is read-only; retirement uses the shared multi-file transaction and remains explicit |
| **Spec lifecycle registry** | Projects immutable spec identities, reviewed lifecycle/lineage/scope, artifact digests, task state, qualified implementation/test evidence, bounded outcomes, and recovery tombstones into one byte-stable control file | `cli/scanners/spec-registry.mjs`, `cli/scanners/requirement-evidence.mjs`, `cli/scanners/revision-anchor.mjs`, `cli/validators/spec-registry.mjs`, `cli/commands/specs.mjs` | `specs --write` preserves reviewed fields; stale checks identify bounded field paths and distinguish canonical ordering from changed content; only committed, clean, digest-current lifecycle entries can defer traceability, while a current planned entry that is new, removed from the index, or modified pending commit remains advisory; `specs approve` records approval and a `planned`, `in_progress` or `implemented` delivery and refuses `verified`/`released`; `specs complete` is the only verified-delivery writer |
| **Reconciliation graph** | Inventories changed paths independently from bounded patch text, maps them to direct spec evidence, to a spec whose reviewed `assetPaths` cover them (files that cannot carry an annotation, such as digest-pinned fixtures; spec 039), or to a requirement declared in a canonical document (`docs-canonical/<doc>.md#<ID>`), and keeps mechanical facts, approved intent, decisions, unrelated changes, and unsupported evidence separate | `cli/shared-git.mjs`, `cli/scanners/reconciliation.mjs`, `cli/commands/reconcile.mjs` | Timeout, overflow, parse failure, or incomplete inventory blocks a ready result; planning is read-only and `--write` never rewrites requirements |
| **Analyzer tier** | Decides which analyzer produced each piece of evidence at the two entry points (`parseJsTs`, `extractPythonFiles`), carries `tier`/`tierReason` on scanned items, reports `parserTier` on every finding, and downgrades a validator to `partial` when an AST-capable language was read by the pattern fallback (`regex-fallback`) or when a language with no AST tier was read by pattern (`fallback-language`: Go, Java, Kotlin, Ruby, Rust, PHP, C#). The Environment validator is `partial` when a source language has no env patterns. The symbol map and module graph name the languages the import graph does not read | `cli/shared-source.mjs` (`tierFor`, `summarizeTiers`, `tierApplicability`, `grepEnvUsage`, `LANGUAGE_NAMES`), `cli/scanners/routes.mjs`, `cli/validators/api-surface.mjs`, `cli/validators/environment.mjs` | Findings are retained; only coverage is downgraded. The two tiers give different reasons: a parser was unavailable, or none exists. A package segment (`src/main/java/com/example`, Go `internal/example`) is product code, not an examples directory (specs/043-fallback-language-coverage) |
| **Python web extraction** | One module outline per Python file, from the `python3` AST tier or the pattern tier when no interpreter is available; the resolvers compose FastAPI/Flask router prefixes across modules, follow Django URL configurations from `ROOT_URLCONF` (including DRF router registrations), classify ORM models (SQLAlchemy, Django, SQLModel) with relationship cardinality, and read `BaseSettings` environment variables | `cli/scanners/py-outline.mjs`, `cli/scanners/py-sources.mjs`, `cli/scanners/python-routes.mjs`, `cli/scanners/python-models.mjs`, `cli/scanners/py-env.mjs`, `cli/scanners/py-ast.mjs` | Both tiers share the resolvers, so they differ only in what they can read; pattern-tier facts keep `parserTier: regex-fallback`, lower `generate` confidence and mark their sections partial. Unresolved paths, prefixes and includes are flagged, never guessed, and join the route scan's `unresolved` list so API-surface coverage is `partial` with their count and first location (specs/063-coverage-followups). Schema-Sync reads the same model scanner as `generate` |
| **Spec Kit artifact coverage** | Treats Spec Kit's own documents as governed. Coverage counts top-level `specs/<feature>/**/*.md` and the constitution under a `specKit` tier. Metrics-Consistency (including `MET004` dependency claims) and Reference-Existence read the constitution | `cli/scanners/speckit.mjs` (`findConstitution`), `cli/commands/guard.mjs`, `cli/validators/metrics-consistency.mjs`, `cli/validators/reference-existence.mjs` | The constitution usually lives in a dot directory the walkers skip, so it is located explicitly. Nested fixture `specs/` trees are not the project's artifacts |
| **Headline coverage** | Reports how many validators actually checked beside the passed/total check count, and caps the badge colour at `green` when any active validator was partial, unsupported, missing a prerequisite, or errored | `cli/commands/guard.mjs` (`badgeColor`, `incompleteCoverage`) | `no-matches` keeps the top grade: a validator that ran and found nothing applicable completed its job |
| **Finding channels** | Separates the three questions a finding answers — does CI block (`severity`), who decides (`disposition`), how well is the code measured (`evidence`), and which analyzer produced it (`parserTier`) — so a certain observation is never labelled uncertain and the feedback loop samples the labels that have never been validated | `cli/findings.mjs`, `cli/precision-evidence.mjs`, `cli/writers/sarif.mjs` | Channels are additive to the published finding contract; `confidence` keeps its meaning as the detector's certainty in its own observation |
| **Precision evidence** | Runs labelled synthetic and exact-commit public cases, separates deterministic results from observations, calculates null-safe quality metrics and confidence bounds, compares case-first baselines, persists them in a provenance envelope whose measure (`benchmark-precision`) and caveat are derived from the cases and re-verified on load, and projects per-code evidence into a generated module the CLI quotes at finding time | `benchmarks/`, `cli/precision-evidence.mjs`, `schemas/docguard-benchmark.schema.json`, `schemas/docguard-benchmark-baseline.schema.json`, `schemas/docguard-precision-evidence.schema.json` | External runs are explicit; third-party project code is never executed and disposable checkouts are removed by default |
| **Documentation site and generated references** | `docs/` is the GitHub Pages site (Just the Docs remote theme in `docs/_config.yml`, which Pages builds itself; no build step or dependency in the repository). `npm run docs:reference` renders `docs/reference/findings.md` from the `CODES` and `BLOCKER_CODES` registries and `explain`'s `DISPLAY_NAMES`; `npm run llms` renders `llms.txt`/`llms-full.txt` | `docs/`, `tools/generate-docs-reference.mjs`, `tools/generate-llms.mjs` | `tests/user-docs.test.mjs` and `tests/llms-bundle-drift.test.mjs` fail on a stale generated file and name the command. `docs/` ships in the npm package (PDFs excluded); `tools/` does not (specs/053-user-documentation) |
| **Non-regression budgets** | Compares a PR with its base on one runner: interleaved guard wall time, self-guard error findings, agent-facing output bytes, packed package weight and runtime dependencies, against the ratios and floors in `budgets.json`; `Budget-Exempt` waives one metric with a reason | `tools/budget.mjs`, `budgets.json` | CI `budget` job; development tooling, not shipped in the npm package |
| **Doc dependency lock** | A doc section declares the code it describes (`covers=` on its `docguard:section` marker); `review --accept` records AST or content fingerprints in `.docguard-doc-lock.json`; the Doc-Dependency validator reports a covered symbol whose code changed (DLK001), a dependency that no longer resolves, an unaccepted or orphaned entry, or an unreadable lock | `cli/scanners/doc-deps.mjs`, `cli/validators/doc-dependency.mjs`, `cli/commands/review.mjs` | Opt-in by declaration; only `review` writes the lock; runs in `--changed-only` pre-commit mode |
| **Version-age note** | Reads the running release's date from the shipped `CHANGELOG.md` heading for exactly its version; when it is more than 14 days old, the guard text footer, the MCP `initialize` instructions and the context pack say a newer version may exist and name `docguard upgrade` | `cli/release-age.mjs`, `cli/commands/guard.mjs`, `cli/commands/mcp.mjs`, `cli/commands/memory.mjs` | No network call; silent when unsure (no CHANGELOG, no heading, bad date, clock before release), in a source checkout, with `DOCGUARD_NO_UPDATE_HINT=1`, and in the guard footer under `CI`; never in JSON, SARIF, JUnit or MCP tool results |
| **Compact guard response** | Projects the full guard result so each fact appears once: findings once, each code's agent-facing evidence once, validator status and counts, each standard applicability reason once; the MCP guard tool's default | `cli/shared-guard-json.mjs`, `cli/validator-coverage.mjs` (`STANDARD_APPLICABILITY_REASONS`), `cli/commands/mcp.mjs`, `cli/commands/guard.mjs` (`--compact`) | A projection, never a recomputation; `detail: "full"` and the CLI's default JSON keep the complete contract |
| **MCP project confinement** | The MCP server serves its `--dir` (else its working directory) plus each `--root`, fixed at startup. A tool call's `projectDir` resolves against the served directory with symlinks followed and must land inside a served root before anything under it is read | `cli/commands/mcp.mjs` (`isWithinRoot`) | Same check over stdio and HTTP; a refused path gets the same error whether or not it exists; a client cannot widen the roots (specs/041-mcp-project-confinement) |
| **MCP doc navigation** | Exact, bounded answers for agents: which doc lines, instruction lines, requirement IDs and covered sections describe a file; a document's outline; one section or the lines around a reference; the task context packet. One reference matcher shared with `trace --reverse` and `impact` | `cli/scanners/doc-references.mjs`, `cli/shared-headings.mjs`, `cli/commands/mcp.mjs` | Read-only; paths pass the evidence reader's checks; 8 KiB default and 32 KiB cap per section read; no tool calls a model |
| **Feedback fixtures** | Validates synthetic reproductions and opposite controls, reduces them under an explicit predicate, derives duplicate identities, and emits test-only contributions | `cli/feedback-fixture.mjs`, `cli/commands/feedback.mjs`, `schemas/docguard-feedback-fixture.schema.json` | Publication remains user-controlled; contribution generation requires reviewed redaction, scope, and benchmark-delta evidence |
| **Evidence-scoped verification** | Binds one exact Markdown statement to a typed JSON Pointer value, bounded file collection, static Python container literal, or saved upstream compatibility report and returns one of five explicit states | `cli/evidence/`, `cli/validators/evidence.mjs`, `cli/commands/verify.mjs`, `schemas/docguard-evidence.schema.json` | Reads stay local, bounded, non-executable, and symlink/private-path safe; direct verification exits 1 for contradiction/invalid input, 2 for unresolved evidence, and 0 only for verified or unconfigured evidence |
| **Managed Git hooks** | Installs bounded DocGuard blocks while preserving user-owned hook commands before and after them | `cli/commands/hooks.mjs` | Reinstall and removal use one outer marker pair, repair nested markers from affected releases, fail closed on enforcement errors, and fall through after success so user postludes execute. A hook owned by husky, lefthook or simple-git-hooks is never written, even under `--force`; the command prints the line to add to that manager |
| **Readiness assessment** | Combines guard enforcement and optional CI score policy without changing structural score semantics | `cli/assessment.mjs`, `cli/commands/ci.mjs`, `cli/commands/diagnose.mjs`, `cli/commands/report.mjs` | READY requires a passing guard and configured gates; ATTENTION carries advisory warnings; BLOCKED identifies failed enforcement |
| **Task-specific agent context** | Ranks exact task paths, qualified requirements, finding codes, identifiers, and bounded lexical overlap across current governed evidence | `cli/scanners/task-context.mjs`, `cli/commands/agent.mjs`, `schemas/docguard-task-context.schema.json` | Read-only and deterministic; excludes retired, unapproved, digest-stale, private, and unsafe material; abstains on weak relevance and never upgrades prose accuracy |
| **Suppressions** | Parses `.docguardignore` entries with an optional `expires:` day and `# reason`, and applies only the active ones everywhere the file is read; models every native suppression (ignore file, config arrays, inline pragmas, N/A markers, baseline) with its reason and state; the Suppressions validator reports expired, unreadable and (opt-in) reason-less entries (IGN001–IGN003); `docguard ignore --format claimspec` exports the entries whose reason, expiry and scope survive exactly | `cli/shared-ignore.mjs` (`parseDocguardIgnore`), `cli/scanners/suppressions.mjs`, `cli/validators/suppressions.mjs`, `cli/commands/ignore.mjs`, `cli/writers/claimspec-ignore.mjs`, `cli/claimspec-schema.mjs`, `schemas/claimspec/v1/` | Read-only. Expired and unreadable entries exclude nothing. The claimspec schemas are vendored byte-identical and checked by a keyword-limited validator that refuses keywords it does not implement; nothing is exported without a written reason (specs/057-ignore-reasons) |
| **Doc ownership map** | Resolves each tracked file to the one doc section responsible for it (exact path beats glob; longer literal prefix beats shorter), reports unowned code and ties, and lints a committed `.devin/wiki.json` | `cli/scanners/doc-ownership.mjs`, `cli/validators/doc-ownership.mjs`, `cli/commands/trace.mjs` | The map is declared by people in `.docguard.json`; `trace --owners --suggest` drafts one and never writes |
| **Path-scoped agent rules** | Derives each agent instruction file's scope per harness (Codex, Claude Code, Cursor, Copilot, OpenHands) and checks it against the tracked files; `rules --for` resolves what an agent loads for a path | `cli/scanners/instruction-scopes.mjs`, `cli/scanners/frontmatter.mjs`, `cli/validators/path-scoped-rules.mjs`, `cli/commands/rules.mjs` | Tracked files only in guard; formats verified 2026-09-30 and recorded in `docs/ai-integration.md`; patterns DocGuard does not evaluate are reported as not checked |
| **Cross-language import graph** | Resolves repository-local JS/TS and Python static imports for cycle and layer checks, once per run for every consumer. JS/TS aliases resolve through the nearest `tsconfig.json`/`jsconfig.json` (`compilerOptions.paths` and `baseUrl`, following `extends`) | `cli/scanners/import-graph.mjs`, `cli/scanners/ts-paths.mjs`, `cli/validators/architecture.mjs`, `cli/scanners/py-ast.mjs` | Python supports regular flat/`src/` packages and explicit relatives; dynamic imports, runtime path changes, parse failures, missing interpreters, and ambiguous modules remain explicit limitations. An alias that names no project file adds no edge; configs are read as JSONC, never executed; `extends` stops at depth 8 or a cycle |
| **Symbol map** | `memory --pack --symbols`: ranks source files by PageRank over the cached import graph and lists their exported names within a byte budget; symbols are read only for the files that fit | `cli/scanners/symbol-map.mjs`, `cli/scanners/js-ast.mjs` (`moduleSymbols`), `cli/scanners/py-ast.mjs` | Opt-in by benchmark decision: frozen agent-context protocol v3 (`benchmarks/agent-context/manifest-v3.json`, v2 with only the executable pin changed) computed `opt-in`, with no regression and no predeclared benefit; names only, no claim about behavior |
| **Code-derived diagrams** | Draws the `module-graph` (directory modules and their imports) and `entity-diagram` (schema entities) mermaid sections from code, so GST002 reports drift and `sync --write` redraws them; a graph missing evidence is `partial`, never stale | `cli/scanners/module-diagram.mjs`, `cli/scanners/memory-plan.mjs`, `cli/validators/generated-staleness.mjs` | Only for a doc that has the marker or is being created; node, edge and depth limits are captioned; hand-drawn diagrams are untouched |
| **Repository-root guidance** | Detects a likely governing ancestor without changing the selected scan directory | `cli/repository-root.mjs`, `cli/docguard.mjs` | Requires ancestor DocGuard configuration or npm/pnpm membership, respects nested Git boundaries, and uses typed stderr diagnostics for machine modes |
| **Lifecycle transactions and context** | Stages registry, recovery, spec outcome, and current-context changes before any visible mutation and rolls the set back on write or validation failure | `cli/writers/file-transaction.mjs`, `cli/writers/spec-outcomes.mjs`, `cli/scanners/lifecycle-context.mjs` | Active context includes approved current spec pointers and content hashes; retired prose is excluded |
| **Adoption baseline** | `guard --update-baseline` freezes the current findings in `.docguard.baseline.json` as fingerprint → occurrence count; guard and `ci` then suppress up to that many of each and gate everything new, reporting how many were suppressed and how many entries are stale | `cli/writers/baseline.mjs`, `cli/commands/guard.mjs` | A fingerprint hashes the code, the location without its line number and the message with digits masked, minus an advisory tail declared per code in `ADVISORY_TAILS` (today only TRC004's IR soft-match hint, which names whichever test file matches best). Guard also accepts the legacy full-message fingerprint so a baseline from an earlier release keeps working; one finding consumes one entry (specs/052-baseline-stable-fingerprint) |
| **Agent instruction budget** | Measures every `AGENTS.md` chain from the root to each instruction directory, as Codex loads it (`AGENTS.override.md` replaces `AGENTS.md`), against `agentInstructions.maxBytes` (default 32768) or a per-chain allowance | `cli/scanners/agent-instructions.mjs`, `cli/validators/structure.mjs` | `STR004` is an escalation; `STR005` suggests tightening a slack allowance. Increasing an allowance is a reviewed config change, which the digit-normalized finding baseline could not guarantee |
| **As-built specs** | `generate --spec <area>` collects deterministic facts under one code area (routes, exported JS/TS symbols, env vars read, entities), renders a Spec Kit spec with one agent-written candidate per fact, and registers it as `origin: as_built` with `sourcePaths`. The Spec-Registry validator re-scans those paths (`SPR007`) | `cli/scanners/as-built.mjs`, `cli/scanners/js-ast.mjs` (`exportedNames`, shared with the symbol map), `cli/commands/generate-as-built.mjs`, `cli/validators/spec-registry.mjs` | DocGuard writes no requirement prose. Facts key on method+path, file+export, name, so line moves never churn them. An App Router handler export is not a second fact beside its route. Route and env facts cite the file and line of the registration or first read. The registry fields are serialized only when set. `--write` commits the spec and its registry entry in one file transaction |
| **Go, Spring and Rails routes** | Reports each route at the path it is served under. Go (gin, echo, chi, fiber, gorilla/mux, net/http): every router value is a node whose prefix comes from `Group`, chi `Route`/`Mount`, gorilla `PathPrefix().Subrouter()` or `http.StripPrefix`, across blocks, functions and files, resolved after every file is read. Spring (Java and Kotlin): class-level and method mappings, including the `path =`/`value =` forms, arrays, constants and `@RequestMapping(method = …)`. Rails: `config/routes.rb` and the files it `draw`s, with `namespace`, `scope`, nested `resources`, `only:`/`except:`, `member` and `collection` | `cli/scanners/go-routes.mjs`, `cli/scanners/spring-routes.mjs`, `cli/scanners/rails-routes.mjs`, `cli/scanners/route-lexing.mjs`, `cli/scanners/routes.mjs` | No parser: a shared lexer masks comments and strings and pairs brackets, so comments, `_test.go` files, HTTP-client calls and Feign clients do not count. Routes carry `parserTier: fallback-language`. A path or prefix that is not literal, or a Go call with several possible callees, places no route rather than a guessed one (specs/047-go-spring-rails-routes) |
| **Sinatra, Starlette, Warp and Tide routes** | Reads the frameworks spec 060 added (#519). Sinatra: classic and modular apps (classes inheriting from `Sinatra::Base` across files), `namespace` prefixes, `register`ed extensions, and the prefixes `config.ru` maps or a host app `use`s. Starlette: `Starlette`/`Router` objects join the FastAPI/Flask graph, with their `routes=` lists, `Mount` children and `add_route`. Warp: filter expressions evaluated to path/method alternatives through `and`/`or`, `let` bindings and functions across files, from what `warp::serve` runs. Tide: `at()` trees and `nest()` mounts resolved after every file is read. A Go module with only `net/http` is classified so `go-routes.mjs` runs | `cli/scanners/sinatra-routes.mjs`, `cli/scanners/rust-routes.mjs`, `cli/scanners/python-routes.mjs`, `cli/scanners/project-type.mjs`, `cli/scanners/routes.mjs` | No parser for Ruby or Rust: a tokenizer (Ruby) or the shared lexer (Rust) masks comments and strings first. Ruby and Rust routes carry `parserTier: fallback-language`, Starlette routes the Python tier. A path that is not literal is omitted, and the API-surface coverage reason counts the omissions (specs/060-more-route-frameworks) |
| **Laravel, Symfony and ASP.NET Core routes** | Reports each route at the path it is served under. Laravel: the route files the app loads (`bootstrap/app.php` `withRouting`, else `RouteServiceProvider` mappings, else `routes/web.php` and `routes/api.php` under `/api`), with `prefix`/`middleware`/`group` chains and resource expansion. Symfony: `#[Route]` attributes and `@Route` annotations under the class-level route, and `config/routes.yaml` routes and prefixed imports. ASP.NET Core: attribute-routed controllers (inherited class routes, verb attributes, `[controller]`/`[action]`/`[area]` tokens) and minimal APIs (`Map*` under `MapGroup` prefixes, followed into the methods a group is passed to). The classifier labels a `Microsoft.NET.Sdk.Web` project ASP.NET Core | `cli/scanners/php-routes.mjs`, `cli/scanners/dotnet-routes.mjs`, `cli/scanners/routes.mjs`, `cli/scanners/project-type.mjs`, `cli/validators/api-surface.mjs` | No parser: per-language lexers mask comments and strings (PHP `#` comments, heredocs, interpolation; C# verbatim, interpolated and raw strings) and reuse the shared bracket pairing. Routes carry `parserTier: fallback-language`. A path or prefix that is not literal is omitted, counted, and reported in the API-surface coverage reason with its first locations (specs/059-php-dotnet-routes). ASP.NET Core routes carry `caseInsensitive: true`, and `compareEndpoints` (`cli/scanners/api-doc.mjs`) ignores case for them only, as ASP.NET Core does; every other framework compares exactly (specs/063-coverage-followups) |
| **JS/TS extraction** | Route case sensitivity per router (`cli/scanners/route-case.mjs`: each JavaScript framework's router options and app setting, carried down the mount chain, and the NestJS adapter; an unreadable setting compares exactly and is noted, specs/063-coverage-followups); router `.route('/x').get().post()` chains with mount prefixes; per-route auth from the route's own middleware and handler, an earlier `use(auth)`, an auth mount above it, or an App Router `middleware` matcher; env reads including destructuring, each read site recorded with file, line and default; the App Router `pages/` and `components/` roots; ORM entities (schema files, table builders, document schemas, decorated entity classes with inherited columns, `define`/`init` models) through one discovery (`scanOrmEntities`) that guard's schema check, `generate` and `diff` share | `cli/scanners/js-ast.mjs`, `cli/scanners/routes.mjs`, `cli/shared-source.mjs` (`grepEnvUsage`), `cli/scanners/schemas.mjs`, `cli/validators/schema-sync.mjs`, `cli/scanners/project-type.mjs` | Auth is recognised by name (`requireAuth`, `verifyToken`, `passport.authenticate(...)`, `getServerSession`); text elsewhere in a file never marks a route. A route path that is not literal or a resolvable constant (a runtime template value, a regular expression) is not reported at a placeholder; it goes to `scan.unresolved` and makes the API-surface check `partial`; a path or mount prefix built from same-file or imported `const` strings (through `import` or `require()`) is resolved, and a prefix that cannot be read leaves the routers below it out instead of at the root (specs/064-js-route-shapes). Table schemas come from their ORM config first, else a bounded search; every schema file is read once. The regex fallback reads route chains, per-statement auth, and table and document-schema fields from the same object text; decorated entity classes and model-definition calls are read as decorator and argument text by both tiers and interpreted once; every table, document schema and entity class read records the tier that read it. Framework and ORM names are listed in `docs/commands.md` (`generate`) |
| **NestJS, Workers and router-library routes** | Reports the routes of NestJS controllers (`@Controller` + method decorators under `setGlobalPrefix`, URI versioning and `RouterModule` paths, auth from guards), of a Cloudflare Worker's `fetch` entry (pathname comparisons, `switch`, keyed tables and `URLPattern` in the entry and the modules it imports, methods from the `request.method` checks around them), of the small edge and Node routers (`basePath`, `base`, `prefix`, `route()` and `use(sub.routes())` mounts and application mounts across files, through `import` and `require`, and OpenAPI route definitions whose `{param}` segments become `:param`), and of file-routed edge functions (each `onRequest*` export of a file under `functions/` at the path its location names, with `_middleware` files as auth evidence for the routes below them, kept only where the output directory's include/exclude routing file lets requests reach them; an output-directory worker file replaces that tree and is read as a `fetch` entry). Each reader is a pure function of file contents; `routes.mjs` gives them their files and records each file's tier and every path a reader could not resolve (`scan.unresolved`) | `cli/scanners/nest-routes.mjs`, `cli/scanners/worker-routes.mjs`, `cli/scanners/router-routes.mjs`, `cli/scanners/pages-routes.mjs`, `cli/scanners/js-route-support.mjs`, `cli/scanners/routes.mjs`, `cli/validators/api-surface.mjs` (`routeScanGaps`) | Syntax tree first, pattern fallback per file; routes carry `tier`. A path that is not literal or a constant the reader resolves (same-file; for the router readers also `const` strings imported from project files) is omitted and makes the API-surface check `partial`, naming the framework, count and first location. Durable Object classes, browser `location.pathname` and modules the Worker entry never imports are not read. A router below a prefix that is not literal reports no route. Framework names are listed in `docs/reference/languages.md` (specs/058-js-framework-routes, specs/064-js-route-shapes) |
| **Spec-first gate** | Classifies a change between the merge base and HEAD as covered, exempt, not governed, uncovered or inconclusive. It reads governed paths from `specFirst` and resolves spec references against `specs/*/spec.md` and the registry | `cli/scanners/spec-first.mjs`, `cli/commands/specs.mjs` | Read-only. An unresolved reference never passes. Git or inventory failure is inconclusive (exit 2), never a pass. The PR body arrives as a file |
| **Spec Kit delegation** | The only code that invokes the `specify` CLI: reads its documented `init` options and `--version`, resolves the integration key from `.specify/integration.json` / `init-options.json` / unambiguous agent signals, initializes Spec Kit, registers the packaged DocGuard extension and re-registers it when the registered version differs; writes the extension commands for the `generic` integration, which Spec Kit registers none for | `cli/spec-kit-delegation.mjs`, `cli/ensure-skills.mjs` | Called only by `docguard init` and `upgrade --apply`, and `ensureSkills` runs only from `init` too (the dispatcher installs nothing, so every other command writes only its own outputs, spec 042); argument arrays only; every failure returns a structured result with the CLI's error and a manual command; requires Spec Kit ≥ 0.11.2, the extension's `requires.speckit_version`, read from the manifest as `MIN_SPEC_KIT_VERSION` and checked before initializing or registering (spec 048) |
| **Agent surface** | What the project's agent can run: the active integration's command layout, derived from Spec Kit's install manifest (`.specify/integrations/<key>.manifest.json`); where DocGuard's own skills go; whether each mandatory hook in `.specify/extensions.yml` resolves; and command hints that name a slash command only when its file exists | `cli/agent-surface.mjs` | Read-only. No per-agent table: the file Spec Kit wrote for `constitution` gives the directory, shape and separator. Paths from `.specify/*.json` are checked to stay inside the project before any write (spec 048) |
| **Validators** | Independent validation modules that check specific aspects of CDD compliance — all emitting structured findings with stable codes (the `CODES` registry in `findings.mjs`) | `cli/validators/` | `*.mjs` |
| **Scanners** | Project file scanners for test discovery, route detection, schema mapping, CDK/IaC, doc-tools, integrations, frontend surface, spec-kit, memory-plan, semantic claims, agent readability, and the tech stack (`tech-stack.mjs`: what every ecosystem's manifests and unambiguous source text show, and which absences a manifest proves) | `cli/scanners/` | `*.mjs` |
| **Writers** | Deterministic doc-mutation and output modules — section-addressable edits, mapped-role ownership authorization, mechanical fix registry, API-Reference writer, generate I/O + doc builders (split from generate.mjs), SARIF emitter (no LLM) | `cli/writers/`, `cli/shared-doc-roles.mjs` | Mapped human docs expose only unique `source=code` sections; new or explicitly generated single-role targets permit whole-document writes; whole-document replacements use backups and `--force` cannot grant ownership. `generate --plan --write` goes through `writeOwnedSections`, which skips the backup only for a DocGuard-authored document (generated marker, or an unedited init template) whose every line outside unpinned code-section bodies survives, and never replaces a `source=human` section. Generators and checks share their facts: `collectEnvVars` (env templates plus code reads) in `cli/shared-source.mjs`, and `detectCodeTechnologies` in `cli/scanners/tech-stack.mjs`, which records the file behind each technology: a manifest of any ecosystem, or an unambiguous source-text signal in a product file for the two technologies commonly used without a dependency (specs/061-dogfood-round-2 lists the signals) |
| **Config** | Configuration loading, schema migration, validator policy, and exact finding-code policy | `cli/` | `config.mjs`, `shared.mjs` |
| **Shared** | Cross-cutting utilities — ignore/glob filters (with `DEFAULT_IGNORE_DIRS`, and `isIgnoredDirEntry` skipping a `bin/` only beside a .NET project file), Git-ignore-aware bounded indexing, package capability counts, source-root resolution, Git helpers, declaration-shaped requirement identity parsing, and the shared doc→code trace patterns used by both `trace` and the Traceability validator | `cli/` | `shared-ignore.mjs`, `shared-validator-surface.mjs`, `shared-source.mjs`, `shared-git.mjs`, `shared-requirements.mjs`, `shared-trace-patterns.mjs`, `shared-openapi.mjs` (OpenAPI discovery shared by API-Surface and Docs-Sync), `shared-test-cases.mjs` (declared test-case counts for Metrics-Consistency `MET003`), `shared.mjs` |
| **Templates** | Document skeletons (ARCHITECTURE, SECURITY, etc.) and slash command files for AI agents | `templates/` | `*.template`, `commands/*.md` |
| **Extension** | Spec Kit extension: AI skills, slash commands, bash scripts and workflow hooks, as declared in `extension.yml` | `extensions/spec-kit-docguard/` | `skills/*/SKILL.md`, `scripts/bash/*.sh` |
| **Tests** | Per-validator unit tests + command-level integration tests using `node:test` | `tests/` | `*.test.mjs` |

## Tech Stack

| Category | Technology | Rationale |
|----------|-----------|-----------|
| Language | JavaScript (ES Modules) | Universal runtime, zero-friction `npx` usage |
| Runtime | Node.js ≥ 18 | Native `node:test`, `node:fs`, `node:child_process` |
| Dependencies | **One npm dep** — `@babel/parser` (exact-pinned, optional-load) | AST-accurate JS/TS parsing; minimal, vetted supply-chain surface |
| Optional external | `python3` (the developer's own) | AST-accurate Python route/schema/import parsing; not an npm/pip dependency; import-graph coverage abstains when absent |
| Package Manager | npm | Standard for Node.js CLIs |
| Testing | `node:test` + `node:assert` | Built-in, no test framework dependency |
| Docker | `Dockerfile` (MCP server image) | Published to GHCR for stdio MCP use; HTTP transport is also available with explicit configuration |

### Recognized Config Files

DocGuard recognizes and validates these project config files:

| File | Purpose |
|------|---------|
| `.docguard.json` | Project-level DocGuard configuration |
| `.docguardignore` | Per-project file exclusions (like `.gitignore`) |
| `vitest.config.ts` / `jest.config.ts` | Test runner config (scanned for custom test patterns) |
| `.storybook/` | Component documentation tool (detected for docs-coverage) |
| `.jules-setup.sh` | This repo's own Google Jules environment bootstrap script (internal tooling, not shipped) |
| `.pre-commit-hooks.yaml` | This repo as a pre-commit hook source — consumers reference `repo: raccioly/docguard` to run `docguard-guard` (changed-only) per commit |
| `glama.json` | Glama MCP directory metadata — declares repo maintainers so the Glama listing can be claimed/managed |
| `server.json` | Official MCP Registry manifest (`io.github.raccioly/docguard`) — server name, npm package, stdio transport |

## Layer Boundaries

The architecture separates command orchestration, validation, extraction, output, configuration, and shared utilities. The boundaries below describe responsibilities and permitted dependencies.

| Layer | Contains | Can Import From | Cannot Import From |
|-------|----------|----------------|--------------------|
| **Extension** (`extensions/spec-kit-docguard/`) | AI skills (SKILL.md), bash scripts, hooks, commands | CLI (via npx), Node.js built-ins | Isolated — spec-kit integration layer |
| **Commands** (`cli/commands/`) | User-facing command logic | Validators, Scanners, Writers, Evidence, Shared utilities, Config | Other commands, except the shared internals a command exports for reuse (e.g. `runGuardInternal`) |
| **Validators** (`cli/validators/`) | Independent validation modules | Scanners, Shared utilities, the pure section parsers in `writers/sections.mjs`, Node.js built-ins | Commands, other validators, and any writer that touches the file system |
| **Evidence** (`cli/evidence/`) | Strict manifest loading, exact Markdown selection, file-only adapters, scoped identities | Safe scanner primitives, Shared utilities, Node.js built-ins | Cannot execute project code, external tools, package managers, or network requests |
| **Scanners** (`cli/scanners/`) | Project intelligence — detect routes, schemas, IaC, frontend surface | Shared utilities, the pure parsers in `writers/sections.mjs` and `writers/spec-outcomes.mjs`, Node.js built-ins | Validators, Commands, and any writer that touches the file system |
| **Writers** (`cli/writers/`) | Mutate canonical docs surgically (section-addressable, no LLM) | Shared helpers, Scanners for generated content, Node.js built-ins | Cannot import from Commands or Validators |
| **Shared** (`cli/shared-*.mjs`) | Cross-cutting utilities: ignore/glob filters, source-root resolution, static Worker/Pages binding scopes, git helpers, shared trace patterns | Node.js built-ins plus optional direct parser loading where documented | Cannot import from Validators, Commands, or Writers |
| **Config** (`cli/config.mjs`) | `loadConfig` + defaults/profile merge + project-type detection, which maps the ecosystem profile from `scanners/project-type.mjs` (the detector `init` and `generate` use) | Shared utilities, the project-type scanner, Node.js built-ins | Cannot import from Commands (extracted so `demo`→`docguard` is no longer a cycle) |
| **Entry Point** (`cli/docguard.mjs`) | ANSI colors, argument parsing, command dispatch, banner/help | Commands, Config (`loadConfig`) | Calls validators only through commands |

### Key rule

**Key Rule**: Validators are pure functions. They receive `projectDir` and `config`, then return results. They stay isolated from commands and the CLI entry point. The Extension layer operates independently, using the CLI as an external tool.

### Layer graph

```mermaid
graph TD
    A["CLI Entry Point<br/>docguard.mjs"] --> B["Shared Constants<br/>shared.mjs"]
    A --> C["Commands<br/>cli/commands/*.mjs"]
    C --> B
    C --> D["Validators<br/>cli/validators/*.mjs"]
    D --> E["Node.js Built-ins<br/>fs, path, child_process"]
    C --> E
    A --> F[".docguard.json<br/>Project Config"]
    D --> G["docs-canonical/<br/>Canonical Docs"]

    style A fill:#4a9eff,color:#fff
    style B fill:#6c757d,color:#fff
    style C fill:#28a745,color:#fff
    style D fill:#ffc107,color:#000
    style F fill:#17a2b8,color:#fff
    style G fill:#e83e8c,color:#fff
```

### Module graph

Drawn from the static imports in `cli/` by `docguard sync --write`
(`diagrams.moduleGraph` in `.docguard.json`). The layer graph above is the
intended shape and is maintained by hand; this one is what the code does, and
Generated-Staleness (GST002) reports it when an import between modules changes.

<!-- docguard:section id=module-graph source=code -->
```mermaid
graph LR
  m_cli["cli"]
  m_cli_commands["cli/commands"]
  m_cli_evidence["cli/evidence"]
  m_cli_scanners["cli/scanners"]
  m_cli_validators["cli/validators"]
  m_cli_writers["cli/writers"]
  m_cli --> m_cli_commands
  m_cli --> m_cli_scanners
  m_cli_commands --> m_cli
  m_cli_commands --> m_cli_evidence
  m_cli_commands --> m_cli_scanners
  m_cli_commands --> m_cli_validators
  m_cli_commands --> m_cli_writers
  m_cli_evidence --> m_cli
  m_cli_evidence --> m_cli_scanners
  m_cli_scanners --> m_cli
  m_cli_scanners --> m_cli_writers
  m_cli_validators --> m_cli
  m_cli_validators --> m_cli_evidence
  m_cli_validators --> m_cli_scanners
  m_cli_validators --> m_cli_writers
  m_cli_writers --> m_cli
  m_cli_writers --> m_cli_scanners
```
<!-- /docguard:section -->

## Data Flow

### Request Lifecycle: `docguard guard`

```
User runs: npx docguard guard
     │
     ▼
docguard.mjs
  ├── parseArgs(process.argv)      → flags: { format, dir, ... }
  ├── loadConfig(projectDir)       → .docguard.json → merged with defaults
  │     ├── Reads .docguard.json
  │     ├── Reads package.json (name, type detection)
  │     └── Merges: defaults ← config ← CLI flags
  │
  ▼
guard.mjs
  ├── For each enabled validator in cli/validators/ (the list lives in guard.mjs;
  │   e.g. structure, docs-sync, architecture, freshness, doc-dependency,
  │   path-scoped-rules, doc-ownership)
  │
  ├── Collects: { pass: [...], warn: [...], fail: [...] }
  │
  ▼
Output (text | json)
  └── Exit code: 0 (pass) | 1 (fail) | 2 (warn) | 3 (errors, but project not initialised)
```

### AI Fix Flow: `docguard fix --doc architecture`

```
fix.mjs
  ├── Looks up DOC_EXPECTATIONS['docs-canonical/ARCHITECTURE.md']
  ├── assessDocQuality(content, expectations)
  │     └── Checks: line count, placeholder count, content quality signals
  ├── Outputs: TASK, PURPOSE, RESEARCH STEPS, WRITE THE DOCUMENT
  │
  ▼
AI Agent (Claude Code, Cursor, Copilot, etc.)
  ├── Reads stdout (the research instructions)
  ├── Executes research: reads package.json, scans directories, maps imports
  ├── Writes docs-canonical/ARCHITECTURE.md with real content
  │
  ▼
docguard guard → validates the newly written document
```

## Key Design Decisions

| Decision | Rationale |
|----------|-----------|
| **Minimal dependencies** | One exact-pinned, vetted runtime dep (`@babel/parser`) earns its place by fixing silent regex truncation; it loads optionally so installs stay robust. Everything else is Node.js built-ins. |
| **Config-driven validation** | `.docguard.json` lets projects customize which validators run. A CLI project can skip database docs. |
| **Validators are independent** | Each validator is a self-contained module. Adding a validator keeps existing ones stable. |
| **AI as author, CLI as orchestrator** | The CLI detects problems and generates structured prompts. Documentation writing is the AI's responsibility. |
| **Exit codes for CI** | `0` (pass), `1` (fail), `2` (warn), `3` (errors in a project with no `.docguard.json`) enables `docguard ci` to gate deployments. `3` stays non-zero so an any-non-zero gate is unchanged, but it lets the generated Git hook distinguish "never adopted DocGuard" from "failed its checks". |
| **Scoped factual evidence** | `.docguard-evidence.json` declares narrow, typed source-to-statement predicates. Contradictions fail guard; stale, inconclusive, and unsupported evidence stays visible. A verified statement never exempts its document from freshness or semantic review. |
| **Evidence before context volume** | `agent --task` returns a bounded retrieval packet only after the frozen evaluation showed equal hidden-test safety and lower steps/latency. It remains opt-in because uncached token use increased and the synthetic protocol does not establish universal benefit. |

---

## External Dependencies

DocGuard declares one exact-pinned runtime dependency, `@babel/parser`. It loads optionally: installations without Babel use a less precise regex fallback. The modules below supply the remaining runtime functionality.

| Module | Usage |
|--------|-------|
| `node:fs` | File system operations (read docs, check existence) |
| `node:path` | Path resolution and manipulation |
| `node:child_process` | Git operations, the optional `python3` AST tier, the optional `specify` CLI (`init` and `upgrade --apply`), `npm install -g` for `upgrade --apply`, `git`/`gh` for `upgrade --pr`, `gh` for `impact`, and re-running the CLI itself (`fix`, `diagnose`). All take argv arrays; see SECURITY.md |
| `node:url` | ES Module URL resolution |
| `node:crypto` | Content hashes: doc-lock fingerprints, registry and evidence digests, plan-cache identity |
| `node:http` | The opt-in MCP HTTP transport (`mcp --transport http`) |
| `node:readline` | Interactive prompts (init command) |
| `node:test` | Built-in test framework |
| `node:assert` | Test assertions |
| `node:os` | Temp directory for tests |

**Dev dependencies**: None. Tests use `node:test` (built-in since Node.js 18).

---

## Revision History

| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.11.0 | 2026-10-10 | DocGuard Team | Coverage follow-ups: Python route limitations reach API-surface coverage, ASP.NET Core routes, and JavaScript framework routes as their router settings decide, compare case-insensitively, fallback advice names the missing prerequisite, .NET `obj/`/`bin/` and `_build/` are not source (spec 063) |
| 1.10.0 | 2026-10-10 | DocGuard Team | Suppressions component: `.docguardignore` reasons and expiries, the suppression model, the Suppressions validator and the claimspec `ignore` export (spec 057) |
| 1.9.0 | 2026-10-10 | DocGuard Team | Freshness review for specs 039–053: Go/Spring/Rails route readers, the adoption baseline and its stable fingerprint, MCP project confinement, the documentation site and its generators, asset-path reconciliation links, `specs approve`, `upgrade --apply` as a `specify` and `npm` caller; project size re-measured |
| 1.8.0 | 2026-09-30 | DocGuard Team | `ensureSkills` runs only from `init`; the dispatcher no longer installs skills for other commands (spec 042) |
| 1.7.0 | 2026-09-30 | DocGuard Team | Freshness review for specs 028–037: layer rules corrected to the code (scanners and validators use the pure section parsers in `writers/`), guard data flow no longer lists a fixed validator set, subprocess and built-in module inventory completed, project size re-measured |
| 1.6.0 | 2026-09-29 | DocGuard Team | Freshness review for specs 014–026: hook-manager ownership, Spec Kit version floors, the shared test-case counter, subprocess uses, and counts replaced by their authoritative source |
| 1.5.0 | 2026-09-18 | DocGuard Team | Freshness review: corrected a project-size figure stale since 2026-05-29 and recorded guard exit code 3 for uninitialised projects |
| 1.4.0 | 2026-09-15 | DocGuard Team | Bound package capability claims to shipped modules, pruned ignored and nested checkout copies from instruction pointers, and made non-clean planned lifecycle state advisory only |
| 1.3.0 | 2026-09-15 | DocGuard Team | Made managed hooks composable and self-repairing, aligned direct evidence exit codes with guard severity, and exposed field-level registry drift |
| 1.2.0 | 2026-09-15 | DocGuard Team | Made router mounts symbol-aware and statically composable, retained negative scan evidence as review-only, and aligned monorepo/config/design-sync discovery boundaries |
| 1.1.0 | 2026-09-15 | DocGuard Team | Added packed adoption qualification, independent diff inventory, exact finding-code policy, combined readiness assessment, lifecycle-aware traceability, static Python literal evidence, and transitive static router-mount resolution with test-client exclusion |
| 1.0.0 | 2026-09-14 | DocGuard Team | Added deterministic task-specific context selection, lifecycle and safe-reader boundaries, strict packet schema, and the frozen promotion benchmark |
| 0.9.0 | 2026-09-14 | DocGuard Team | Added strict evidence manifests, typed local adapters, five-state evaluation, exact semantic-claim coverage, and guard/agent assurance integration |
| 0.8.0 | 2026-09-14 | DocGuard Team | Added transactional retirement/completion writes, reconciliation review graphs, qualified implementation evidence, bounded outcomes, active-context regeneration, and Spec Kit completion hooks |
| 0.7.0 | 2026-09-14 | DocGuard Team | Added the deterministic spec lifecycle registry, immutable spec-ID resolution, shared requirement evidence scanner, recovery tombstones, and two-stage preflight boundary |
| 0.6.0 | 2026-05-31 | DocGuard Team | Refresh for v0.24.0: Python promoted to full support via a `python3` AST tier (`cli/scanners/py-ast.mjs`); JS/TS route extraction extended with cross-file mount-prefix resolution, object-form route declarations, and AST router-screen detection (`cli/scanners/js-ast.mjs`); removed the retired editor extension from the tech stack |
| 0.5.0 | 2026-05-29 | DocGuard Team | Refresh for v0.22–v0.23: validator + scanner set updated, new `config.mjs` (config extracted to break the demo↔docguard cycle) and `shared-trace-patterns.mjs` (shared multilingual trace patterns) |
| 0.4.0 | 2026-03-13 | DocGuard Team | Complete rewrite with real project data, AI orchestration architecture |
| 0.1.0 | 2026-03-13 | DocGuard Generate | Auto-generated skeleton |


### Requirement identity across documents

<!-- docguard:section id=requirement-identity source=human covers="cli/shared-requirements.mjs#collectRequirementIdsFromContent, cli/commands/specs.mjs#planSpecCompletion" -->
Requirement definitions are identified by immutable spec ID plus requirement ID when a spec declares `Spec ID` metadata. Repository-relative path qualifiers remain supported during migration. A bare test annotation such as `@req FR-001` earns linkage credit only when that ID is defined in one active or retired document. Prefer `@req acme.payments#FR-001`; `@req specs/payments/spec.md#FR-001` remains valid while the spec is active. Path qualifiers use forward slashes and are repository-relative.

Validation, `trace --features`, and the spec registry share definition parsing and reference resolution. A qualified reference credits only its target document. Ambiguous bare references credit neither feature and produce a review finding for each unresolved definition. A wrong qualifier is an orphan reference and never falls back to a bare match. Registry completion evidence always requires an explicit spec ID or exact path qualifier, even when a bare ID is currently unique. Repeated mentions within one document do not create additional identities. Linkage remains evidence of a declaration, not proof of behavioral correctness.

Completion also supports reviewed maintenance of a verified or released living spec. It reconciles from the prior reviewed revision and appends a status-preserving outcome when a linked source, test, canonical document, or decision changed. Eligibility comes exclusively from those reviewable changes; generated registry, active-context, and implementation-outcome updates are excluded.
<!-- /docguard:section -->


---

## docs-canonical/CI-RECIPES.md

# CI Recipes

<!-- docguard:last-reviewed 2026-10-10 -->
<!-- docguard:status active -->

## Recipe 1 — Guard (mandatory CI gate)

Run `docguard init --with ci` to create `.github/workflows/docguard.yml`. Existing workflows are preserved; explicit `--force` backs up and replaces the file. The standalone `docguard ci` command continues to execute checks. Start from `templates/ci/github-actions.yml` or the Spec Kit guard workflow in `extensions/spec-kit-docguard/templates/github-workflows/`. These checked-in templates are the maintained source for action pins, runtime selection, and report handling. Copying a template does not configure repository branch protection; require its check independently.

Use a fixed tool version, full Git history for freshness, and explicit warning policy. Run the check against the actual revision proposed for merging. A missing executable, malformed report, or unexpected nonzero exit is a tool failure, not a successful scan. Configure merge-queue triggers if the repository uses a merge queue.

```sh
node_modules/.bin/docguard ci --format json --no-history > docguard-report.json
```

The CLI exits 0 for pass, 1 for failure, and 2 for warning-only results. A plain shell step treats both 1 and 2 as failures. To permit warnings, capture the exit status explicitly and allow only 0 or 2. To block warnings, use `ci --fail-on-warning`. Severity overrides retain their configured meaning.

To adopt the gate on a repository that already has findings, run `guard --update-baseline` once and commit `.docguard.baseline.json`. Guard and `ci` then suppress up to the frozen number of each finding, report how many they suppressed, and gate everything new; `--no-baseline` shows everything for one run. A finding is matched by its code, file path without line number, and message with numbers masked. TRC004's trailing IR soft-match hint is left out, so adding an unrelated test file does not bring a frozen finding back. A baseline written by an earlier release still matches, and `--update-baseline` rewrites it in the current form (`specs/052-baseline-stable-fingerprint`).

When `.docguard-evidence.json` exists, guard also evaluates its declarations.
Contradictions are high-confidence errors. Stale input digests, missing or
ambiguous targets, malformed evidence, and unsupported report shapes remain
visible warnings. Inspect the complete contract with:

```bash
npx docguard-cli verify --evidence --format json
```

This direct command exits 0 when every configured declaration is verified, 2
when evidence is stale, inconclusive, or unsupported, and 1 when a declaration
is contradicted or the manifest is invalid. CI that permits unresolved evidence
must explicitly allow only status 2; a contradiction is always a failed gate.

Generate oasdiff or Buf reports in an earlier pinned CI step, save their machine
output, and declare SHA-256 identities for every repository input. DocGuard
consumes those artifacts; it does not install or invoke either producer. Keep
the broad freshness and semantic review paths enabled because exact evidence
does not cover undeclared prose.

## Recipe 2 — Auto-Fix (PR-time mechanical fixes)

Run `fix --write` on a controlled checkout when documentation mutation is intended. Review the resulting diff and rerun guard. Preserve human-authored intent; a disagreement may require fixing implementation rather than rewriting the specification.

Mechanical replacements require their existing provenance and generated-section safeguards. A scheduled or PR repair workflow should create a reviewable branch/PR and deduplicate existing repair work. Grant write privileges only to that explicitly enabled workflow. Fork contributions should receive read-only verification unless a separate trusted process handles repair.

The shipped auto-fix template and composite action expose optional commit/comment behavior. Review those flags and their permissions before enabling them. A generated workflow is executable code and deserves the same review as another repository change.

## Recipe 3 — Sync (memory refresh on a schedule or pre-merge)

`sync --write` regenerates sections declared as code-derived. Human sections retain judgment and rationale. Cache identity reflects relevant inputs, so ordinary source edits invalidate a prior plan. `--since <ref>` refreshes only the sections whose sources changed (`specs/029-sync-section-scope`). A section drawn from incomplete evidence, such as a `module-graph` on a runner without `python3` for a Python project, is skipped unless `--allow-partial`; run the refresh where the evidence is complete.

On a schedule, produce a diff, check for an existing repair PR, and create a new proposal only when meaningful work remains. Keep clean runs quiet. Set an owner and response expectation for unresolved findings. Scheduled source scans cannot detect every external deployment or vendor change; operational checks need their own evidence.

## Recipe 3a — Protected scheduled releases

The repository's scheduled release workflow opens a reviewable `release/vX.Y.Z`
pull request because `main` requires pull requests and six checks: four runtime checks (`test (18/20/22/24)`) and the supply-chain checks `osv-scan-pr / osv-scan` and `websec`. GitHub
places pull-request workflows created with the repository `GITHUB_TOKEN` into an
approval-required state. Explicit `workflow_dispatch` events run, but their jobs
do not satisfy branch protection's required pull-request checks, and their
completion does not produce a downstream `workflow_run` when the repository token
authored the dispatch. GitHub documents a personal token or GitHub App as the
fully automated alternative. DocGuard instead keeps the repository token and one
explicit maintainer action: select **Approve workflows to run** on the generated
PR. No credential is stored for the release PR or its merge; the only release
secret is the Homebrew tap deploy key described below.

After approval, ordinary pull-request CI supplies the six required contexts.
Before the branch is pushed, the trusted scheduler validates the base repository,
bot author, branch/title/version agreement, next-version increment, synchronized
package surfaces, and changed-file allowlist. It then arms GitHub's native squash
auto-merge. Native auto-merge remains blocked by the six required checks, binds
eligibility to the current PR head, and resets when that head changes. The
scheduler waits up to ten minutes for the merge and then dispatches the
idempotent release workflow. Approvals rarely arrive inside that wait, so a
tag-driven release sweep every 10 minutes is the usual publication route (#447);
tagged versions exit after the small detect job. If publication is interrupted,
either the sweep or the next release
schedule sees the current package version without a tag and retries publication
before considering another bump. An orphaned release branch fails closed; an
existing open release PR is reused and has auto-merge re-armed.

The bump is `auto` unless a maintainer dispatches `patch` or `minor`
(`specs/026-release-readiness`). `.github/scripts/release-changelog.mjs infer`
reads the curated `## [Unreleased]` section. Any populated `### Added`,
`### Removed` or `### Deprecated` heading makes a minor release; otherwise the
release is a patch. The same script's `cut` moves the curated notes under the
new `## [x.y.z]` heading and lists the merged commit subjects under
`### Commits`, so the published notes are the reviewed ones. The release PR
also moves `action.yml`'s `DOCGUARD_RELEASED_VERSION` and the copyable
`raccioly/docguard@vX.Y.Z` examples in `README.md` and `docs/ai-integration.md`.
`.github/scripts/sync-release-version.mjs` updates every versioned surface in one
fail-closed pass, including the checked-in `.agent/skills/` and `.agent/commands/`
copies, which mirror their sources byte for byte; the release commit stages them
and the changed-file allowlist (`RELEASE_PATH_ALLOWLIST`) admits them
(`specs/051-release-cut-green`).

After npm serves the new tarball, the `publish-homebrew` job renders
`packaging/homebrew/docguard.rb` from it and pushes the result to
`raccioly/homebrew-tap`. The tarball is checked against npm's `dist.integrity`
first. The push uses a write deploy key that can reach only the tap, stored as
the Actions secret `HOMEBREW_TAP_DEPLOY_KEY`. Without the secret the job warns
and skips. With it, the 10-minute sweep retries until the tap serves the
released version.

Do not use a post-approval `workflow_run` listener as the release continuation.
The approval-required completion is the event that listener observes; approving
the held run executes its jobs without producing a second completion event for
the listener. Release PR #380 demonstrated this boundary while publishing
v0.40.1. Release PR #383 then proved repository-token native auto-merge, while
also proving that its resulting push is recursion-suppressed and cannot be the
sole publication trigger. The bounded wait catches the rare fast approval; the
10-minute tag sweep publishes everything else without continuous polling or
another credential.

The v0.40.3 release is the retained end-to-end proof. Scheduled run
`34922506777` opened repository-token PR #386 and armed native auto-merge. After
one maintainer workflow approval, CI run `34922605581` and supply-chain run
`34922605917` passed, GitHub merged
`e27d6bf0203708ee8206a1434eb292520f4c4494`, and the bounded wait dispatched
publication run `34922784629`. That run published npm, PyPI, GHCR, the GitHub
Release, extension ZIP, and MCPB and refreshed the catalog reminder.

Catalog submission remains an explicit human action. The release and manual
catalog workflows maintain one open reminder in this repository: each run
refreshes the newest matching issue to the current version and closes older
matching reminders as superseded.

## Recipe 3b — Spec completion and post-hoc reconciliation

Run `docguard reconcile --since <merge-base> --format json` when implementation
may have changed approved behavior outside the original Spec Kit flow. Review
unsupported files and intent-change classifications; write mode can refresh only
DocGuard-owned mechanical sections. After declared tasks, source and test
evidence, and affected canonical docs are reviewed, run `docguard specs complete --id <spec-id>
--since <merge-base> --check` as the merge gate. Apply the same command with
`--write --reason "<reviewed outcome>"` on a clean controlled checkout to record
verification. A taskless living verification contract is eligible only when every
requirement has qualified evidence. Keep living specs current; archive only when the registry reports
that the selected persistence model is ready.

Under squash merges, run completions on a branch whose HEAD is the default
branch's tip, run as many `specs complete --write` as needed (the registry,
active context and outcome blocks they write do not count as changes), and
commit once. Every outcome then records a revision that stays on the default
branch. A completion recorded on a PR branch's own commit would be discarded by
the squash, and `specs complete` warns when that is about to happen. Guard
reports such a revision as `SPR008`; `docguard specs reanchor --id <spec-id>
--write` moves it to the first commit on HEAD's first-parent history whose
evidence files are byte-identical (`specs/028-completion-revision-anchoring`).
When no commit carries those bytes, or the old revision no longer resolves,
`--to <merge commit> --reason "<why>"` records an attested anchor and the
evidence files that differ. When one spec has dangling revisions from different
merges, add `--from <revision>` so each is attested to its own merge.

## Recipe 3c — Spec-first gate

Require every pull request that changes governed code to name its spec. Declare the
governed paths in `.docguard.json` (`"specFirst": { "paths": ["src/**"] }`; unset, every
changed path except Markdown, `specs/**` and tests is governed), then run:

```yaml
- uses: actions/checkout@<sha>
  with: { fetch-depth: 0 }
- uses: raccioly/docguard@<sha>
  with: { command: spec-first }
```

The command is `docguard specs require --since origin/<base> --message-file <body>`. It passes
when the PR description or a commit message names an existing `specs/<dir>` path or Spec ID,
when the change edits a spec's own files, or on one line `Spec-Exempt: <kind> — <reason>`
(kinds `release`, `deps`, `typo`, `test-only` unless `specFirst.exemptKinds` says otherwise;
reason of 10+ characters). An invented reference never passes. Exit 1 means uncovered and exit
2 means inconclusive (unknown base, shallow history, oversized inventory); both should fail
the job. When calling the CLI from your own workflow, pass the PR body through `env` into a
file. Never splice `${{ github.event.pull_request.body }}` into a `run:` script.

## Recipe 3d — Agent instruction budget

`guard` measures every `AGENTS.md` chain the way Codex loads it: root to working directory,
with `AGENTS.override.md` replacing `AGENTS.md`. It reports `STR004` above
`agentInstructions.maxBytes` (default 32768, Codex's `project_doc_max_bytes`). The recipe
needs no extra step: it runs inside Recipe 1.

For a monorepo that is already over budget, freeze the debt instead of raising the budget
for everyone. The key is the chain's deepest file:

```json
{ "agentInstructions": { "allowances": { "packages/api/AGENTS.md": 61440 } } }
```

A chain may shrink under its allowance, and one byte of growth past it fails again.
`STR005` asks you to lower an allowance once the chain has shrunk by 1 KiB or more, so every
increase stays a reviewed diff.

## Recipe 3e — Non-regression budgets

The `budget` job in `ci.yml` checks the PR's base commit out beside the head
and runs `node tools/budget.mjs ab`. Guard runs interleaved over the same
targets for both CLIs, so the time comparison measures the code, not the
runner. Each tree also reports its own agent-facing output sizes (context pack,
task packets, MCP responses, llms bundles), its packed package weight, its
runtime dependencies and its self-guard findings. `budgets.json` holds the
ratios and floors. A breach fails the job unless the PR body declares
`Budget-Exempt: <metric> — <reason>`; the full table goes to the job summary.
An unmeasurable base is inconclusive, never a pass. To reproduce the table
locally, run `git worktree add ../base origin/main` and then
`npm run budget -- --base ../base` (`specs/031-non-regression-budgets`).

Guard time is each tree's median of `samples` rounds (5 in `budgets.json`),
and the tree that runs first alternates between rounds. A median of N samples
ignores up to (N − 1) / 2 slowed samples of either tree: with five rounds, two
guard runs delayed by a load burst, however long, cannot pull a median above
the slowest of the other three. A load that rises steadily through the run
moves the two medians only one sample apart. A difference under the floor
(150 ms) never fails. Below three samples there is no outlier margin: with
`--samples 1`, one delayed process decides the verdict
(`specs/062-budget-deterministic`).

## Recipe 4 — Score (track CDD maturity over time)

`score --format json` reports structural maturity. Its numeric threshold is stable, while `assurance` explicitly states that factual accuracy remains unverified. Comparing scores is meaningful only with the same tool/configuration and a comparable coverage scope.

Use guard findings and declared verification evidence for enforcement. A high score alone does not establish current documentation, correct prose, or regulatory compliance.

## Recipe 1b — GitLab CI / Jenkins (JUnit output)

`guard --format sarif` emits SARIF 2.1.0 for GitHub Code Scanning and enterprise dashboards. Every result carries the finding's channels in `properties`: `disposition` (`act` when DocGuard names the correction, `escalate` when the judgement is the reader's), `confidence`, `evidence` (`measured` or `not-measured`), `parserTier`, `reportable`, `suggestionKind`, and the intrinsic and effective severities. Gate automation on `disposition` rather than severity alone — a blocking result can still be an escalation, and applying an automated fix to one silences a signal without resolving it. `parserTier` of `regex-fallback` or `fallback-language` means no syntax tree was available for that file, so absence of a result there is weak evidence.

`guard --format junit` emits a test report suitable for GitLab/Jenkins ingestion. Install a fixed DocGuard version in the job, capture the exit status, and upload the report even on failures. Permitting exit 2 is an explicit warning policy; other nonzero statuses remain failures.

## Recipe 4b — Score history across ephemeral CI runs

`ci` records history by default. `--no-history` opts out. Ephemeral runners need an explicitly configured artifact or cache policy if trends are to span runs. Treat restored history as informational data, not proof that the current checkout was verified. Avoid sharing writable caches between untrusted pull requests and privileged release workflows.

## Recipe 4c — Multi-repo scorecard (no extra tooling)

Run `ci --format json` per repository and retain project, revision, tool version, configuration, status, and assurance scope. Aggregate findings by code while preserving their repository ownership. Report unsupported and unclassified coverage alongside successful checks.

## Recipe 4d — Detector precision regression

Run the network-free synthetic corpus on ordinary pull requests:

```sh
node benchmarks/run.mjs --baseline benchmarks/baseline.json
```

Run the full pinned public corpus in a separate trusted, network-enabled job when detector or scanner behavior changes:

```sh
node benchmarks/run.mjs --external --baseline benchmarks/baseline.json
```

Treat a core comparison failure as a quality regression. In the network-free run the pinned public baseline cases were not selected; they appear under `comparison.core.outOfSelection` and are not regressions. The loader validates the baseline envelope before the run starts and refuses one whose metrics or caveat no longer match its cases. Persisted runtime snapshots stay advisory even when environment labels match. Apply the 20-percent gate only to at least five controlled samples from the same paired comparison session. Updating the baseline is a reviewed change: inspect every added or removed case, label, unsupported result, and confidence limit before using `--replace-baseline`.

## Pre-commit hook (no GitHub Actions required)

`docguard hooks --type pre-commit` installs a local gate that prefers the repository's installed DocGuard binary. The hook blocks an unavailable runtime. When husky, lefthook or simple-git-hooks owns the hook, DocGuard writes nothing and prints the line to add to that manager's configuration. `--auto-fix` additionally applies mechanical fixes and stages their output; enable it only when that mutation is intended.

A Git hook lives in the shared `.git/hooks` and is active on every branch and linked worktree, while `.docguard.json` is a branch-local tracked file. The installed hook therefore skips any working tree with no `.docguard.json` and lets the commit through, and treats guard exit `3` (errors in an uninitialised project) as allowed rather than blocking. A project that never adopted DocGuard is not blocked by a hook installed from another branch; adopted projects are gated exactly as before.

Regenerate installed hooks after upgrading to pick up changes in hook behavior. The pre-push score hook parses real JSON and enforces its configured minimum; it complements the full CI gate. Local hooks can be bypassed, so protected merges remain necessary for shared enforcement.

## Recipe 5 — Pre-commit lite (changed files only)

`guard --changed-only --since <ref>` runs its curated validator subset with changed-file scoping, plus explicitly escalated validators. Use a full guard at the merge boundary. The entry point and `guard.mjs` define the current subset; a copied list in this recipe would drift.

## Permissions cheatsheet

| Operation | Default authority | Additional authority |
|---|---|---|
| Guard, score, report | Repository read | Artifact storage if configured |
| Mechanical repair | Read/write controlled checkout | Branch/PR publication only when enabled |
| Feedback preview | Local analysis | User submits reviewed public metadata voluntarily |
| External precision corpus | Public read-only Git fetch | Network access to exact pinned commits; no project script execution |
| Scheduled review | Repository read | Notification or publication only when explicitly configured |

## Action inputs reference

`action.yml` is the authoritative composite-action input contract. Review command selection, CLI version (`docguard-version`; empty installs the version the action release was published with), warning policy, score threshold, working directory, and optional commit/comment flags. Pin the action to a reviewed commit and retain the corresponding release label for maintenance.

## Action outputs reference

Read the outputs declared in `action.yml` and the command's JSON schema before wiring downstream steps. Preserve unknown/unverified values. An integrity digest detects changes to covered report data; it is neither a trusted signature nor proof of a correct scanner.


---

## docs-canonical/DATA-MODEL.md
> Database schemas, entity relationships, and data flow

# Data Model

<!-- docguard:version 0.16.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-10-10 -->

| Metadata | Value |
|----------|-------|
| **Status** | ![Status](https://img.shields.io/badge/status-active-brightgreen) |
| **Version** | `0.16.0` |
| **Database** | None — DocGuard is a stateless CLI tool |
| **Storage** | File-system only (reads project files, writes generated docs) |

---

## Entities

DocGuard uses filesystem artifacts for configuration, optional caches, and history. Commands read project files and produce structured output. The "data model" consists of the configuration schemas, validator output formats, and document metadata structures documented below. All data is file-system based — DocGuard reads `.docguard.json`, scans the project directory, and validates canonical documents against the codebase.

## Configuration: `.docguard.json`

The primary data structure. Controls all CLI behavior.

### Identity and required files

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `projectName` | `string` | No | Inferred from `package.json` name or directory | Display name for reports |
| `version` | `string` | No | `"0.1"` | Config schema version |
| `projectType` | `string` | No | Auto-detected | One of: `cli`, `webapp`, `api`, `library`, `monorepo` |
| `requiredFiles.canonical` | `string[]` | No | 5 docs-canonical files | Paths to required CDD documents |
| `requiredFiles.agentFile` | `string[]` \| `string` | No | `["AGENTS.md", "CLAUDE.md"]` | AI agent config file options; any one suffices. A single string is read as a one-element list |
| `requiredFiles.changelog` | `string` | No | `"CHANGELOG.md"` | Changelog file path |
| `requiredFiles.driftLog` | `string` | No | `"DRIFT-LOG.md"` | Drift log file path |

### Project-type behavior

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `projectTypeConfig.needsEnvVars` | `boolean` | No | `true` | Whether ENVIRONMENT.md should check for env var docs |
| `projectTypeConfig.needsEnvExample` | `boolean` | No | `true` | Whether `.env.example` is expected |
| `projectTypeConfig.needsE2E` | `boolean` | No | `true` | Whether E2E test docs are expected |
| `projectTypeConfig.needsDatabase` | `boolean` | No | `true` | Whether DATA-MODEL should expect entity docs |
| `projectTypeConfig.testFramework` | `string` | No | Auto-detected | Test framework name (e.g., `"node:test"`, `"jest"`) |
| `projectTypeConfig.runCommand` | `string` | No | Auto-detected | Command to run the project |

### Validator tuning

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `validators.*` | `boolean` | No | `true` | Enable/disable individual validators |
| `validators.docSections` / `severity.docSections` | `boolean` / `string` | No | follows `structure` | Doc Sections (STR003). Unset, the `structure` switch, severity and N/A marker apply to it |
| `collections.*` | `string` (glob) | No | — | Binds a documentation noun to a code collection: `"extractors": "src/extractors/*.py"` lets Metrics-Consistency flag a documented count that disagrees with the file count |
| `docs.dirs` | `string[]` | No | Auto-detected | EXTENDS the auto-detected documentation homes (docs/, documentation/, guides/, …) with non-standard dirs; exclude via `.docguardignore` |
| `severity.*` | `"high" \| "medium" \| "low"` | No | `"medium"` | Per-validator exit-code weight — `high` promotes warnings to blocking, `low` demotes them (display unchanged) |
| `findingSeverity.<CODE>` | `"high" \| "medium" \| "low"` | No | — | Exact stable-code enforcement; takes precedence over validator policy. Intrinsic errors require an exact code entry to be demoted. |
| `agentInstructions.maxBytes` | `integer` | No | `32768` | Byte budget per `AGENTS.md` chain (STR004) and per harness and path (PSR003). For Claude Code the total includes files pulled in by `@path` imports (relative to the importing file, up to 5 hops); a missing import is PSR002 |
| `agentInstructions.allowances.<file>` | `integer` | No | — | Per-chain allowance keyed by the chain's deepest file; slack of 1 KiB or more reports STR005 |
| `specFirst.paths` / `specFirst.exemptKinds` | `string[]` | No | Everything except Markdown, `specs/**` and tests / `release, deps, typo, test-only` | Governed paths and allowed `Spec-Exempt` kinds for `specs require` |
| `validators.docDependency` / `pathScopedRules` / `docOwnership` | `boolean` | No | `true` | Each applies only when its input exists: a `covers=` declaration, agent instruction files, an `ownership` block or `.devin/wiki.json` |
| `detection.includeNonProduct` | `boolean` | No | `false` | Read test, fixture, example and mock directories as product code during surface detection (routes, env vars, frameworks). Package segments such as `src/main/java/com/example` or a Go `internal/example` package are always product code |
| `diagrams.moduleGraph.depth` / `maxNodes` / `include` | `integer` / `integer` / `string[]` | No | `2` / `30` (cap 60) / every product source file | Shape of the `module-graph` section |
| `memory.symbolMap.maxBytes` | `integer` | No | `4096` (256–16384) | Budget of the symbol map `memory --pack --symbols` adds |
| `devinWiki.maxPages` | `integer` | No | `30` (up to 80) | Page cap for linting `.devin/wiki.json`; 80 on Devin enterprise plans |
| `ownership` | `object` | No | — | Doc ownership map, described below |
| `baseline` | `boolean` | No | `true` | `false` ignores a committed `.docguard.baseline.json`, as `--no-baseline` does on every run |
| `ignore` / `securityIgnore` / `todoIgnore` | `string[]` | No | `[]` | Path patterns skipped by every validator / Security only / TODO-Tracking only. `ignore` is merged with the active `.docguardignore` entries at load. Plain patterns: no reason, no expiry |
| `validators.suppressions` | `boolean` | No | `true` | `.docguardignore` entry hygiene (IGN001–IGN003); not applicable when the file has no entries |
| `suppressions.requireReason` | `boolean` | No | `false` | Report IGN002 for each `.docguardignore` entry with no `# reason`; the entry still applies |

### Example Configuration

```json
{
  "projectName": "docguard",
  "version": "0.3",
  "projectType": "cli",
  "requiredFiles": {
    "canonical": [
      "docs-canonical/ARCHITECTURE.md",
      "docs-canonical/DATA-MODEL.md",
      "docs-canonical/SECURITY.md",
      "docs-canonical/TEST-SPEC.md",
      "docs-canonical/ENVIRONMENT.md"
    ],
    "agentFile": ["AGENTS.md", "CLAUDE.md"],
    "changelog": "CHANGELOG.md",
    "driftLog": "DRIFT-LOG.md"
  },
  "projectTypeConfig": {
    "needsEnvVars": false,
    "needsE2E": false,
    "needsDatabase": false,
    "testFramework": "node:test"
  },
  "validators": {
    "structure": true,
    "docsSync": true,
    "drift": true,
    "changelog": true,
    "architecture": false,
    "testSpec": true,
    "security": false,
    "environment": true,
    "freshness": true
  }
}
```

## Retirement Manifest: `.docguard-archive.json`

The manifest is an append-only recovery ledger for documentation removed from
active context by `docguard retire`. Git content remains authoritative; the
manifest stores no retired prose.

| Field | Type | Description |
|-------|------|-------------|
| `schemaVersion` | `number` | Manifest contract version; currently `1` |
| `strategy` | `"git-history"` | Recovery storage strategy |
| `entries[].path` | `string` | Former repository-relative document path |
| `entries[].archivedAt` | ISO timestamp | Historical field name for retirement time |
| `entries[].archivedFrom` | Git object ID | Source revision containing the exact document |
| `entries[].blob` | Git object ID | Exact retired content identity; length follows repository object format |
| `entries[].reason` | `string` | Reviewed retirement rationale |
| `entries[].supersededBy` | `string` | Optional current replacement document |
| `entries[].evidence` | `string[]` | Optional clean documents containing consolidated outcomes |
| `entries[].requirementIds` | `string[]` | Requirement identities declared by the retired file; traceability keeps them as tombstones and never treats them as active requirements |
| `entries[].retentionRef` | `string` | Branch ref proven to contain the source revision |
| `entries[].objectFormat` | `"sha1" \| "sha256"` | Git repository object format |
| `entries[].recoverability` | `"verified"` | Result of the retained-ref ancestor check at retirement time |
| `entries[].restore` | `string` | Convenience command derived from structured source/path fields |

Existing manifests may carry one shared top-level `retention` record for a
batch created before per-entry retention metadata was introduced. The spec
registry projects both forms into one normalized tombstone model. Lifecycle
and traceability consumers reject incomplete recovery entries; an unverified
manifest cannot suppress active-context or orphan-reference findings.

## Spec Lifecycle Registry: `.docguard-specs.json`

<!-- docguard:section id=spec-registry source=human covers="schemas/docguard-specs.schema.json, cli/commands/specs.mjs#completeSpec, cli/commands/specs.mjs#reanchorSpec" -->
The committed registry indexes which specifications govern the project and what
the repository can prove about them. It never copies requirement prose. Its
normative JSON Schema is `schemas/docguard-specs.schema.json`.

| Field | Authority | Description |
|-------|-----------|-------------|
| `$schema`, `schemaVersion` | Contract | Exact schema URL and version `2`; version 1 is read for migration and projects stale until refreshed |
| `specs[].specId` | Spec metadata | Immutable lowercase namespaced identity; never generated or reused |
| `specs[].path` | Projection | Current spec path or former path for a retired record |
| `specs[].reviewed.lifecycle` | Human review | Orthogonal approval, delivery, context, retirement reason, storage, and persistence policy. `specs approve` records approval and a `planned`, `in_progress` or `implemented` delivery; `specs complete` records `verified` |
| `specs[].reviewed.relations` | Human review | `extends`, `duplicates`, `conflictsWith`, `supersedes`, and `supersededBy` spec-ID edges |
| `specs[].reviewed.scope.canonicalDocs` | Human review | Canonical documents affected by the specification |
| `specs[].reviewed.scope.sourcePaths` | `generate --spec --write` | Optional. Code areas an as-built spec describes; SPR007 re-scans them. Serialized only when set |
| `specs[].reviewed.scope.assetPaths` | Human review | Optional. Files the spec owns that cannot carry a spec annotation (digest-pinned fixtures, recorded outputs): `dir/` covers a directory, any other entry one file, no wildcards. Reconciliation links changes under them to the spec; an entry covering no tracked file is SPR009. Serialized only when set |
| `specs[].reviewed.lifecycle.origin` | `generate --spec --write` | Optional. `as_built` marks a spec that records existing behaviour. Absent for forward specs, so older registries stay byte-identical |
| `specs[].reviewed.reconciliation.lastReviewedRevision` | Human review | Exact Git revision whose doc impact was reviewed, or `null` |
| `specs[].reviewed.reconciliation.outcomes` | Human review | Up to 20 reviewed implementation outcomes with revision, bounded rationale, evidence paths, deviations, and optional successor |
| `specs[].reviewed.reconciliation.outcomes[].reanchoredFrom` | `specs reanchor` | Optional: the discarded revision an outcome was recorded at, and `method` — `blob-equal` (evidence compared) or `attested` (with a `reason`, and `differing` evidence paths when the target's bytes differ). Serialized only when set |
| `specs[].intent.requirements` | Projection | `specId#requirementId` identities parsed from the active spec |
| `specs[].observed.artifacts` | Projection | Paths and SHA-256 content identities for spec, plan, and tasks |
| `specs[].observed.taskCompletion` | Projection | Checked and total Markdown task boxes; not proof of delivery |
| `specs[].observed.testEvidence` | Projection | Explicitly spec-qualified test annotations or labels only |
| `specs[].observed.implementationEvidence` | Projection | Explicit `@implements specId#requirementId` source annotations only; names and proximity do not earn completion credit |
| `tombstones[]` | Recovery projection | Retired identities linked to source revision, blob, retention ref, object format, and recoverability |

`docguard specs --write` regenerates only projected fields and preserves the
entire `reviewed` block. Unknown reviewed fields, invalid lifecycle values,
duplicate identities, and archive/storage contradictions fail closed. The
output omits timestamps and sorts unordered collections, so `specs --check`
can compare a byte-stable result in CI. A non-current projection exposes up to
25 bounded `differences` with a JSON-style field path, kind, and explanation.
Order-only differences use kind `order`; changed, missing, and unexpected
content remain distinct. Additional differences are reported as truncated.

`docguard specs complete` requires a clean Git revision (changes confined to DocGuard's
own `.docguard/` state directory still count as clean), coverage for every
requirement through qualified implementation or test evidence, existing affected
canonical documents, a supported reconciliation plan, and a guard result without
errors. Declared task ledgers must be non-empty and fully checked. An approved
`living` verification contract may omit the task ledger because its qualified
requirement evidence is the durable completion proof; other persistence models
still require one. Its staged transaction updates the bounded outcome,
registry, feature-spec outcome index, and `.docguard/current-context.json` as one
validated set. The context file contains pointers and content hashes rather than
copying governing prose, and excludes every retired spec. A verified or released
living spec can append a status-preserving maintenance outcome only when a new
linked source, test, canonical document, or decision changed after the last
reviewed revision. Generated registry and outcome updates do not satisfy that
gate.
<!-- /docguard:section -->

## Doc Dependency Lock: `.docguard-doc-lock.json`

A documentation section can declare the code it describes, on its marker:

```markdown
<!-- docguard:section id=pricing source=human covers="src/pricing.mjs#discount, src/rules/**" -->
```

`covers` entries are `path` (the file's bytes), `path#symbol` (a top-level
declaration or `Class.method`) or a glob (the matching set). Paths are
relative to the project; absolute paths, `..`, symlinks and `.docguardignore`d
paths are refused. Only `docguard review` writes it: `--accept` records a
review and `--prune` removes entries whose section dropped its `covers` attribute. Normative schema: `schemas/docguard-doc-lock.schema.json`.

| Field | Meaning |
|---|---|
| `schemaVersion` | `1` |
| `sections["<doc>#<id>"].dependencies["<covers entry>"]` | `{ tier, fingerprint }`: `ast` (normalized JS/TS AST of the symbol; formatting, comments and line moves do not change it), `python-ast` (`ast.dump` of the definition), `content` (sha256 of the file), or `glob` (hash of the sorted per-file fingerprints of the tracked files it matches; the working tree outside git) |
| `sections[…].reviewedRevision` | HEAD at acceptance, or `null`; used only to print the diff command, so squash merges and shallow clones do not break comparison |
| `sections[…].reviewedAt` | UTC date of acceptance |
| `sections[…].reason` | What the reviewer checked (8–500 characters) |

Keys are sorted at every level. A project with no `covers=` has no lock and no
new findings.

## Doc Ownership Map: `ownership`

The `ownership` block in `.docguard.json` maps doc sections to the source
paths they are responsible for (specs/034-doc-ownership-map). People write it;
DocGuard only reads it.

| Field | Type | Required | Meaning |
|---|---|---|---|
| `roots` | `string[]` | No | Directories whose source must be owned (default: top-level source modules) |
| `entries[].doc` | `string` | Yes | Project-relative `.md` path |
| `entries[].section` | `string` | No | A `docguard:section` id or heading anchor in `doc` |
| `entries[].purpose` | `string` | No | What the section explains about these paths |
| `entries[].paths` | `string[]` | Yes | Project-relative files, directories or globs |

Each tracked file resolves to at most one owner: an exact path beats any glob,
and a longer literal directory prefix beats a shorter one; two equal matches
are a tie (OWN002). A malformed block is an error (OWN007), and every owner
lookup reports that error until the block is fixed.

## Adoption Baseline: `.docguard.baseline.json`

`docguard guard --update-baseline` writes this file at the project root, to be
committed so the team and CI share it. Guard and `ci` suppress up to the frozen
number of each finding and gate everything else; `--no-baseline` or
`"baseline": false` ignores the file.

| Field | Meaning |
|---|---|
| `_comment` | Fixed explanation of the file and how to regenerate it |
| `version` | `2`: each fingerprint carries an occurrence count |
| `generatedAt` | ISO timestamp of the write; guard reports the baseline's age from it and never enforces one |
| `count` | Number of distinct fingerprints |
| `fingerprints` | Fingerprint → occurrence count, keys sorted |

A fingerprint is the first 16 hex characters of SHA-256 over
`code|path|message`. `code` is the finding code (`UNCODED` when absent), `path`
is the location without its trailing `:line`, and `message` has every digit run
replaced by `#` and whitespace collapsed. Before masking, an advisory tail
declared for the code in `ADVISORY_TAILS` (`cli/writers/baseline.mjs`) is
removed: text a validator appends that changes when unrelated files change.
TRC004's ` — IR soft-match: …` hint is the only one, so every other code's
fingerprint is the same as before (specs/052-baseline-stable-fingerprint).

Guard matches a finding against its stable fingerprint first and then against
the legacy fingerprint (the full message), so a baseline written by an earlier
release keeps suppressing what it froze; `--update-baseline` rewrites it in the
stable form. One finding consumes one unit from one entry. A malformed file, or
one with no positive counts, is treated as absent, so every finding surfaces.

## Ignore file: `.docguardignore`

One entry per pattern line (specs/057-ignore-reasons):

```text
pattern  [expires:YYYY-MM-DD]  [# reason]
```

A line starting with `#` is a comment. `#` starts the reason only after
whitespace; `expires:` is read only as the last word before the reason. A line
with neither means what it meant before the grammar existed.
`parseDocguardIgnore` (`cli/shared-ignore.mjs`) returns, per entry:

| Field | Meaning |
|---|---|
| `pattern` | The pattern, or `null` when the line has only an expiry or reason |
| `reason` | Text after the `#`, or `null` |
| `expires` | The `YYYY-MM-DD` day, or `null` (absent or unreadable) |
| `state` | `active`; `expired` (the UTC day is past); `invalid` (unreadable expiry or no pattern) |
| `problem` | `no-pattern`, `unreadable-expiry` or `null` |
| `line` | 1-based line number |

Only `active` entries exclude anything: `loadDocguardIgnore` returns their
patterns, so every validator and command applies the same rule. Patterns are matched by
`globToRegex` (same file): gitignore wildcards, `?`, bracket expressions, `\`
escapes and a leading-`/` root anchor; a middle slash does not anchor and a
leading `!` is literal ([pattern syntax](../docs/configuration.md), "Excluding paths").

### Suppression model

`docguard ignore --format json` (`collectSuppressions`,
`cli/scanners/suppressions.mjs`) lists every native suppression:

| Field | Meaning |
|---|---|
| `asOf` | The UTC day expiries were judged against |
| `entries[].kind` | `path`, `code` (inline pragma), `validator` / `section` (N/A marker), `fingerprint` (baseline) |
| `entries[].pattern` / `codes` / `key` | The glob or fingerprint / the pragma's codes (`null` = any code; `docguard:ignore-secret` is `["SEC*"]`) / the validator key or section id |
| `entries[].scope` | `all-validators`, `validator:<key>`, `section:<id>`, `line` (the pragma's line and the next), `finding` |
| `entries[].reason` | What the author wrote, or `null`. Never synthesised |
| `entries[].expires` | `YYYY-MM-DD` or `null`; only `.docguardignore` carries one |
| `entries[].state` | `active`, `expired`, `invalid` (does not apply), `disabled` (baseline off) |
| `entries[].source` | `{ file, line }` or `{ file, key }` |
| `coverage.inlineScan` | `complete`, or `partial` when files could not be read |
| `export` | The claimspec export report: `exported`, `candidates`, `omitted[]` (`at`, `pattern`, `why`), `rewritten[]` (`at`, `from`, `to`), `notRepresentable[]` (`category`, `count`, `why`), `complete` |

### claimspec `ignore` export

`docguard ignore --format claimspec` writes a claimspec v1 `ignore` document
(`schemaVersion: 1`, `entries[]` of `kind: "path"`, `pattern`, `reason`,
optional `expires`). Only `.docguardignore` entries are exported, and only when
the reason is 8–2000 characters, the expiry is readable or absent, and gitignore
syntax states the pattern's DocGuard scope exactly (`**/` is prefixed to an
unanchored pattern containing a slash, a trailing `/` is dropped, and a leading
`!`, a `**` inside a segment, an unclosed `[` or a POSIX class is refused). `expires` is the
first instant after the stated day. The schemas are vendored byte-identical in
`schemas/claimspec/v1/` from raccioly/testguard at the revision recorded in
`cli/claimspec-schema.mjs`, and every document is validated before it is printed.

## Generated caches and budgets

| File | Written by | Shape |
|---|---|---|
| `.docguard/plan.cache.json` | the memory plan (guard, sync, generate) | `{ v, configKey, treeHash, plan, writtenAt }`. `v` is `"3"`: code sections may carry `completeness: "partial"` and a `partialReason`. A cache with another version, config key or tree hash is a miss and is rebuilt |
| `budgets.json` | maintainers | `schemaVersion: 1`; sample count, guard targets, the time and byte budgets, and the agent tasks and MCP calls `tools/budget.mjs` measures |
| `benchmarks/agent-context/manifest-v2.json` | frozen once, before any run | Agent-context protocol v2: conditions `task-only`, `context-pack`, `context-pack-symbols`, six tasks with fixture and hidden-evaluator digests, and the promotion rule. Never run: its CLI version and model are unavailable on ChatGPT-account Codex (superseded by v3). `run.mjs` holds its digest and loads it only while every byte matches. Schemas: `docguard-agent-context-benchmark-v2.schema.json`, `docguard-agent-context-result-v2.schema.json` |
| `benchmarks/agent-context/manifest-v3.json` | frozen once (2026-10-10), before any run | Agent-context protocol v3: v2 with only the executable pin (`model.cliVersion`, `model.id`) changed. `run.mjs` holds its digest, loads it only while every byte matches, and rejects it if anything outside the pin differs from v2. Results: `results/observed-v3.json` in the v2 result format |

`.docguard/` holds only local state: the plan cache above, score history, fix memory, feedback records, the nudge throttle, the context pack and the active context. It ignores itself: the first write creates `.docguard/.gitignore` containing `*` and keeps an existing one, so the project's own `.gitignore` stays as the project wrote it. `git rm -r --cached .docguard` untracks files that an earlier version let a project commit. DocGuard's dirty checks leave `.docguard/` out either way.

## Task Context Packet

`docguard agent --task <text> --format json` emits a transient
`docguard.task-context` object governed by
`schemas/docguard-task-context.schema.json`. The command does not persist the
task or packet.

| Field | Description |
|-------|-------------|
| `task.digest`, `task.characters` | Normalized task identity and bounded length; raw task text is omitted |
| `provenance.git`, `provenance.registry` | Captured Git and lifecycle-registry state |
| `assurance` | Retrieval-only scope, unknown factual accuracy, and unverified status |
| `selection` | Targeted or abstained state, threshold, candidate/omission counts, excluded lifecycle documents, and fixed budgets |
| `excerpts[]` | Repository-relative path, line range, content/file hashes, kind, optional spec ID, score, reasons, and bounded content |
| `pointers[]` | Safe task, cited-source, implementation, or test paths with hashes and qualified requirements |
| `verification[]` | Commands and purposes that still need execution |
| `navigation` | Safe canonical-document inventory and approved current spec paths |
| `limitations`, `coreDigest` | Explicit epistemic limits and deterministic packet-core identity |

Selection reads at most 32 documents and 256 chunks, emits at most six
16-line excerpts totaling 6,000 characters and eight pointers, and limits task
input to 2,000 characters. An abstention emits no excerpts or pointers.

## Document Metadata Headers

Every CDD document includes DocGuard metadata as HTML comments at the top:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `docguard:version` | `string` | Yes | Semantic version of the document |
| `docguard:status` | `string` | Yes | One of: `draft`, `active`, `deprecated` |
| `docguard:last-reviewed` | `string` | Yes | ISO date (`YYYY-MM-DD`) |
| `docguard:generated` | `boolean` | No | `true` if auto-generated by DocGuard |

### Example Metadata Header

```markdown
<!-- docguard:version 0.4.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-03-13 -->
```

## Validator Output Format

Validators emit findings and aggregate counts. The guard adapter adds names and statuses:

| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Validator name (e.g., `"structure"`, `"changelog"`) |
| `key` | `string` | Stable result key, unique per result (`structure`, `docSections`, `changelog`, …) |
| `parent` | `string` | Present on a result a validator module emits beside its own (Doc Sections: `"structure"`); such a result is a check, not a separate validator module |
| `status` | `string` | `"pass"`, `"warn"`, or `"fail"` |
| `findings` | `object[]` | Stable code, validator, intrinsic `severity`, `effectiveSeverity`, enforcement source/key, `confidence`, `disposition`, `evidence`, `parserTier`, location, message, and normalized suggestion |
| `passed`, `total` | `number` | Applicable check counts |
| `errors`, `warnings` | `string[]` | Compatibility message arrays |
| `applicable` | `boolean` | Optional applicability indicator; false becomes N/A |
| `effectiveErrors`, `effectiveWarnings`, `effectiveInfos` | `number` | Exit-code counts after exact-code and validator policy |
| `effectiveStatus` | `string` | Per-validator `pass`, `warn`, or `fail` after policy; intrinsic `status` remains available |

## Precision evidence contract

`guard` results carry `precisionEvidence`, scoped to the finding codes that run emitted (`schemas/docguard-precision-evidence.schema.json`). The unit of evidence is the finding code. DocGuard defines many more codes than the reviewed corpus measures, so a code the corpus never exercised reports `status: "not-measured"`, carries no ratio, and never inherits the measured precision of another code in the same validator. A measured code whose own precision denominator is below `minN` is marked `quotable: false` with a reason, and may carry a `backoff` to a coarser measured tier that names that tier (`validator` or `aggregate`). `measures` is always `benchmark-precision`; `caveat` is the sentence a consumer must show beside any quoted ratio; `source.matchesRunningVersion` is false when the numbers were measured on a different build than the one reporting them. `coverage` counts codes in the run by measurement status.

The block is served from `cli/precision-evidence-data.mjs`, a generated module derived from `benchmarks/baseline.json` by `npm run generate:precision-evidence`, because `benchmarks/` is not part of the published package. A test compares the committed module against that projection, so a stale number fails the suite rather than shipping. Findings are written verbatim into feedback records, so their shape is a contract: it grows only by specification, and a test pins the exact key set.

<!-- truncated: 197 more lines — read docs-canonical/DATA-MODEL.md directly -->

---

## docs-canonical/ENVIRONMENT.md
> Setup instructions, environment variables, and prerequisites

# Environment

<!-- docguard:quality negation-load off — an environment doc precisely describes the ABSENCE of requirements (no install step, no database, no credential for the CLI); the prohibitive phrasing is accurate and intentional, not sloppy writing -->

<!-- docguard:version 0.13.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-10-10 -->

> The DocGuard CLI needs no environment variables. Three optional ones exist: `DOCGUARD_API_KEY` for the HTTP MCP server, `DOCGUARD_SPECIFY_TIMEOUT_MS` for the calls `docguard init` and `docguard upgrade --apply` make to the `specify` CLI, and `DOCGUARD_NO_UPDATE_HINT` to silence the version-age note. DocGuard has a single optional-load npm dependency (`@babel/parser`) and optionally uses the developer's own `python3`; everything else is Node.js built-ins.

| Metadata | Value |
|----------|-------|
| **Status** | ![Status](https://img.shields.io/badge/status-active-brightgreen) |
| **Version** | `0.13.0` |

---

## Prerequisites

| Tool | Version | Installation |
|------|---------|-------------|
| Node.js | ≥18.0.0 | [nodejs.org](https://nodejs.org) |
| npm | ≥8 | Included with Node.js |
| Git | Any | [git-scm.com](https://git-scm.com) |
| Python 3 | **Optional** — ≥3.8, enables the AST-accurate Python scanning tier; the scanners use regex otherwise | [python.org](https://python.org) |
| Spec Kit (`specify`) | **Optional** — ≥ 0.11.2 for `docguard init` to initialize Spec Kit and register the DocGuard extension, and for `docguard upgrade --apply` to refresh a registered one (`requires.speckit_version`; an older `specify` is neither initialized nor registered) | [github/spec-kit](https://github.com/github/spec-kit) |

## Environment Variables

> **None required.** Every CLI command (`guard`, `score`, `diff`, `trace`, …)
> reads project files directly — no `.env` file, no database connections, no
> credential of any kind. (Its one npm dependency, `@babel/parser`, needs no
> configuration.)

Three **optional** variables exist:

| Variable | When it applies | Purpose |
|----------|-----------------|---------|
| `DOCGUARD_API_KEY` | `docguard mcp --transport http`: optional on loopback; **required to bind a non-loopback host** | Shared secret for the HTTP MCP server. Equivalent to `--api-key <key>`, which takes precedence. When set, every request must carry `Authorization: Bearer <key>` or `X-API-Key: <key>`, else `401`. |
| `DOCGUARD_NO_UPDATE_HINT` | guard text output, the MCP server's instructions, `memory --pack` | `1` turns off the note that the installed release is more than 14 days old (spec 038). The note makes no network call; `CI` set to a non-empty value other than `false` or `0` also silences it in guard's text output. |
| `DOCGUARD_SPECIFY_TIMEOUT_MS` | `docguard init` and `docguard upgrade --apply` with the `specify` CLI installed | Timeout in milliseconds for each `specify` call. Defaults: 15000 for `specify --version` and `specify init --help`, 60000 for `specify init` and `specify extension add`. A missing, zero or non-numeric value uses the default. |

The server binds `127.0.0.1` by default and **refuses to start** on a
non-loopback host without a key, rather than exposing project read access to
the network. The stdio transport (`docguard mcp`, the default) never reads `DOCGUARD_API_KEY`.
See [SECURITY.md](SECURITY.md) for the full posture.

## Setup Steps

1. Clone the repository: `git clone https://github.com/raccioly/docguard.git`
2. Run `npm ci` to install the locked Babel parser dependency for the full JS/TS extraction tier
3. Run directly: `node cli/docguard.mjs --help`
4. Or use via npx: `npx docguard-cli --help`

## Development

```bash
# Run CLI locally
node cli/docguard.mjs audit

# Run the full test suite (node:test)
npm test

# Test a command on a target project
node cli/docguard.mjs diagnose --dir /path/to/project

# Quick health check
node cli/docguard.mjs guard --format json

# Regenerate llms.txt / llms-full.txt after editing a canonical or optional doc
# (tests/llms-bundle-drift.test.mjs fails and names this command)
npm run llms

# Regenerate docs/reference/findings.md after changing a finding code or a
# validator's display name (tests/user-docs.test.mjs fails and names this command)
npm run docs:reference

# Measure this tree against the budgets in budgets.json (the CI `budget` job
# runs the same comparison against the base branch)
npm run budget
```

## CI/CD

```bash
# GitHub Actions — use the shipped template
cp templates/ci/github-actions.yml .github/workflows/docguard.yml

# GitLab CI — the shipped component (see CI-RECIPES.md, Recipe 1b)
cp templates/ci/gitlab-component.yml .gitlab-ci.yml

# Or run CI command directly
node cli/docguard.mjs ci --threshold 70 --format json
```

---

## Running inside an agent sandbox

DocGuard needs no network access and no credentials, so a sandbox such as ai-jail can run it
with the default read-write project mount. `docguard guard` runs fine under `--lockdown`, which
makes everything read-only. Commands that write into the project need the normal mount:
`init`, `generate`, `hooks`, `fix --write`, `sync --write`, `review --accept` / `--prune`,
`specs --write`, `specs approve --write`, `specs complete --write`, `specs reanchor --write`,
`retire --write`, `memory --pack`, `guard --update-baseline` and `upgrade --apply`. The
[Command Safety Levels](SECURITY.md#command-safety-levels) table in SECURITY.md is the full
list. Read and preview modes (`sync` and `generate --spec` without `--write`, `rules --for`)
write at most the plan cache and other local state in `.docguard/`, which is best-effort and
ignores itself.

What must be visible:

- **Git metadata.** Freshness, Diff-Suspicion, Reference-Existence and the Spec-Kit
  untouched-task check (SPK010) read history. In a linked worktree, `.git` is a file that
  points at a gitdir elsewhere, and sandboxes hide that gitdir by default (ai-jail needs
  `--worktree`). DocGuard detects the case. The affected checks report `missing-prerequisite`,
  or `partial` for Spec-Kit, naming git's own error and the remedy, and never report a clean
  "no matches". Path-scoped rules and the doc ownership map list files with `git ls-files`
  and fall back to a directory walk; path-scoped rules also ask `git check-ignore`, and a path
  git cannot classify is reported as unknown rather than as missing.
- **`python3` (optional).** It provides AST-accurate Python analysis. Without it, the regex
  tier runs, and each affected validator reports the degraded parser tier. A doc section that
  covers a Python symbol cannot be compared, so the doc-dependency check reports `partial`, and
  the generated module graph says the interpreter was unavailable instead of drawing Python
  edges it cannot see. `generate --plan` names the parser tier of its endpoints and entities,
  reports `low` confidence with a note, and marks those sections partial; `generate --spec`
  reports the tier and `low` confidence; SPR007 findings on routes and entities carry
  `parserTier: regex-fallback` and `low` confidence. The pattern tier still composes router
  prefixes and Django URL configurations the same way, so on standard layouts it reports the
  same facts.
- **The `specify` CLI (optional).** It is only needed for `docguard init` to set up Spec Kit and
  for `docguard upgrade --apply` to refresh the registered DocGuard extension.

## Revision History

| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.13.0 | 2026-10-10 | DocGuard Team | Freshness review for specs 038–053: `upgrade --apply` also calls `specify` (spec 048) and `specify --version` uses the 15-second default; `npm run docs:reference`; the commands that write, with SECURITY.md as the full list |
| 0.12.0 | 2026-09-30 | DocGuard Team | `sync --write`, not a bare `sync`, writes into the project; read and preview modes write only the self-ignoring `.docguard/` (spec 042) |
| 0.11.0 | 2026-09-30 | DocGuard Team | `DOCGUARD_NO_UPDATE_HINT` and the `CI` rule for the version-age note (spec 038) |
| 0.10.0 | 2026-09-30 | DocGuard Team | Freshness review: `npm run budget`; the full list of commands that write; how path-scoped rules and ownership read git; what a missing `python3` makes partial (specs 030, 033, 034, 035) |
| 0.9.0 | 2026-09-29 | DocGuard Team | Freshness review: documented `DOCGUARD_SPECIFY_TIMEOUT_MS` and the optional Spec Kit prerequisite; the agent-sandbox section (spec 022) is recorded here |
| 0.8.0 | 2026-09-18 | DocGuard Team | Freshness review: verified the MCP key contract against `cli/commands/mcp.mjs`; added `npm run llms` and the shipped GitLab CI component, both of which were missing |
| 0.7.0 | 2026-09-17 | @raccioly | Documented `DOCGUARD_API_KEY` (HTTP MCP server); corrected the blanket "no API keys" claim that contradicted SECURITY.md |
| 0.6.0 | 2026-05-31 | DocGuard Team | v0.24.0: documented Python 3 as an optional prerequisite (enables the AST Python tier; regex fallback when absent); de-bristled the test-count example |
| 0.5.0 | 2026-03-13 | @raccioly | Added diagnose, CI template, development examples |
| 0.3.0 | 2026-03-12 | @raccioly | Proper CLI environment docs, no env vars |
| 0.1.0 | 2026-03-12 | DocGuard Generate | Auto-generated (corrected) |


---

## docs-canonical/REQUIREMENTS.md

# Requirements

<!-- docguard:quality negation-load off — requirements define explicit failure and non-disclosure boundaries -->
<!-- docguard:version 0.9.1 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-10-10 -->

## Functional Requirements

| ID | Priority | Requirement | Verification |
|---|---|---|---|
| FR-001 | P1 | Score distinguishes structural maturity from unverified factual accuracy, even when candidate extraction finds nothing. | tests/score-assurance.test.mjs |
| FR-002 | P1 | Users can dispute any active finding, preview feedback, and prepare public metadata without sharing source-derived strings automatically. | tests/feedback-contributions.test.mjs |
| FR-003 | P1 | CI, diagnose, and report preserve score assurance limits in machine output. Existing score thresholds keep their numeric meaning. | tests/score-assurance.test.mjs |
| FR-004 | P1 | Detector quality is measured with independently labelled defect/control pairs, split-safe repository groups, explicit unsupported coverage, null-safe metrics, confidence limits, and case-first baseline comparison. | tests/benchmark-manifest.test.mjs, tests/benchmark-metrics.test.mjs, tests/benchmark-runner.test.mjs |
| FR-005 | P1 | Users can turn a false positive, false negative, unsupported syntax case, ambiguity, or policy dispute into a redaction-attested synthetic fixture with an opposite control, deterministic reduction, duplicate search, and optional test-only contribution. | tests/feedback-fixture.test.mjs, tests/feedback-contributions.test.mjs |
| FR-006 | P1 | Teams can bind an exact Markdown statement to safe, local, typed evidence and receive scoped verified, contradicted, stale, inconclusive, or unsupported results through verify, guard, and agent assurance without granting whole-document accuracy. Direct verification must fail CI on contradiction or invalid input and distinguish unresolved evidence with the warning exit status. | tests/evidence-manifest.test.mjs, tests/evidence-adapters.test.mjs, tests/evidence-integration.test.mjs |
| FR-016 | P1 | A finding answers three independent questions in three fields: whether CI blocks (`severity`), who decides (`disposition`: act or escalate), and how well the code is measured (`evidence`), alongside the analyzer that produced it (`parserTier`). A certain observation is never labelled uncertain, and a suggestion with an unsupported kind is omitted rather than coerced. | tests/findings-channels.test.mjs, tests/calibrated-channels-acceptance.test.mjs |
| FR-017 | P1 | Feedback selection covers every finding whose code is unmeasured or whose confidence is low, and discloses what it excludes, so the channel that validates a confidence label is never restricted to findings that label already doubts. | tests/feedback-sampling.test.mjs |
| FR-018 | P1 | A reviewed disagreement is recorded as a corpus row with rationale, date and opposite control, counted per code and corpus-wide, and excluded from every precision and recall denominator. Removing one fails baseline comparison. | tests/adjudication-record.test.mjs |
| FR-019 | P1 | The analyzer tier is computed at run time, carried on every finding, and a validator whose inputs fell back to pattern matching for an AST-capable language reports partial coverage naming the cause. Findings are retained; only coverage is downgraded. | tests/parser-tier.test.mjs |
| FR-020 | P2 | A threshold derived from observed data is fitted against a strictly proper scoring rule; every hand-set threshold names that constraint. The badge reports coverage honestly and withholds its top grade from a run that could not check everything it was asked to. | tests/threshold-constraint.test.mjs, tests/coverage-headline.test.mjs |
| FR-021 | P2 | A completed task naming a deliverable path that exists but that the feature never changed is reported as a review signal, distinct from a task whose deliverable is absent. | tests/untouched-claims.test.mjs |
| FR-007 | P1 | An agent can request a deterministic bounded task-context packet that prioritizes exact current evidence, excludes retired and unsafe material, preserves retrieval-only assurance, and abstains rather than returning weak matches. Existing task-graph behavior remains compatible. | tests/task-context.test.mjs, tests/agent-context-benchmark.test.mjs |
| FR-008 | P1 | The packed-package adoption journey distinguishes foreign hooks, composes one self-repairing managed block with user hook commands, emits complete remediation text, explains deterministic registry drift by field, follows proposed actions, and verifies the resulting state. | tests/adoption-workflow.test.mjs, tests/hooks.test.mjs, tests/hooks-contract.test.mjs, tests/spec-registry.test.mjs, tests/npm-pack-smoke.test.mjs |
| FR-009 | P1 | Reconciliation keeps changed-path inventory independent from bounded patch text and reports partial coverage instead of a ready result after timeout, overflow, or Git failure. | tests/shared-git.test.mjs, tests/reconcile.test.mjs |
| FR-010 | P1 | Instruction pointers resolve only safe exact paths or one unique basename; Git-ignored paths and nested Git checkouts are excluded from basename evidence, while ambiguity, symlinks, unsafe paths, and incomplete indexes remain explicit. | tests/instruction-audit.test.mjs |
| FR-011 | P1 | Planned requirements defer test traceability only when committed, clean, digest-current schema-v2 lifecycle evidence proves they remain planned. A structurally current registry that is new, removed from the Git index, or modified pending commit remains non-authoritative and explains restore-or-commit remediation without recommending artificial test markers. | tests/traceability-lifecycle.test.mjs |
| FR-012 | P1 | Exact finding-code policy can promote or demote one finding without weakening its validator, while intrinsic and effective severity remain visible in machine formats. | tests/severity.test.mjs, tests/sarif.test.mjs, tests/junit.test.mjs |
| FR-013 | P1 | CI, diagnose, and report expose combined READY, ATTENTION, or BLOCKED assessment while standalone score remains structural maturity rather than a guard verdict. | tests/assessment.test.mjs |
| FR-014 | P1 | Python collection-size evidence uses bounded non-executable static literal parsing and abstains on dynamic or ambiguous syntax. | tests/evidence-python-literal.test.mjs, tests/evidence-integration.test.mjs |
| FR-015 | P1 | JavaScript route discovery excludes HTTP-client calls and non-product helpers before deduplication, and composes static Express mounts (literal or built from `const` strings) across imported routers; a route path or mount prefix that cannot be read is recorded as partial coverage, never reported at a placeholder or at the root. | tests/js-ast.test.mjs, tests/routes-express-mounts.test.mjs, tests/js-route-shapes.test.mjs |
| FR-022 | P1 | API contract omissions remain review-only because negative route extraction cannot prove runtime absence or authorize deletion. | tests/api-authority-precision.test.mjs, tests/api-write.test.mjs, tests/doc-role-boundaries.test.mjs |
| FR-023 | P1 | Field warning precision preserves historical prose, multiline skip reasons, test-fixture context, package-local capability counts, package-local env templates, authoritative OpenAPI selection, route-parameter equivalence, service boundaries, and runtime/schema parity. | tests/metrics-consistency.test.mjs, tests/todo-tracking.test.mjs, tests/field-context-precision.test.mjs, tests/environment.test.mjs, tests/docs-sync.test.mjs, tests/docguard-config-schema.test.mjs |
| FR-024 | P1 | A repo-wide Git hook must not block a working tree that never adopted DocGuard: guard reports errors in a project without `.docguard.json` as exit 3 rather than 1, the installed hook skips such a tree and permits exit 3, and adopted projects stay gated. Overwriting a foreign hook requires explicit repeated `--force` and never silently discards its backup. | tests/hook-fail-open.test.mjs, tests/hooks-contract.test.mjs |
| FR-025 | P2 | Metrics-Consistency compares a documented count with what ships (validators counted from `cli/validators/`), and reports one drifted value once per file and label, not once per occurrence. | tests/metrics-dedup.test.mjs |

## Non-Functional Requirements

| ID | Category | Requirement | Verification |
|---|---|---|---|
| NFR-001 | Security | Untrusted input passed to subprocesses uses argv-based invocation and validation appropriate to the command. | tests/security-init-injection.test.mjs |
| NFR-002 | Portability | The distributed CLI runs on supported Node versions. Babel supplies the full JS/TS tier; the CLI retains a regex fallback when the parser is absent. | tests/npm-pack-smoke.test.mjs |
| NFR-003 | Correctness | Cached memory plans invalidate when relevant working-tree inputs, configuration, or scanner implementation change. Unreadable or unsupported cache inputs cause a miss. | tests/plan-disk-cache.test.mjs |
| NFR-004 | Distribution integrity | Every relative README link in the npm artifact resolves inside that artifact; repository-only material uses an absolute URL. | tests/npm-pack-smoke.test.mjs |

## Success Criteria

The full supported-runtime test matrix and guard determine local release readiness. The reviewed benchmark records observed detector precision and recall with explicit coverage limits; its finite confidence interval is not universal accuracy. The frozen R7 evaluation supports opt-in task context through equal measured correctness, 50% fewer median steps, and 17% lower median latency against context packs. It also recorded 80% more median uncached input and does not establish universal agent productivity. The symbol map (`memory --pack --symbols`, spec 036) stays opt-in: the frozen protocol, not an impression, decided it. v2 could not run, so v3 (v2 with only the executable pin changed) ran all 54 trials on 2026-10-10 and computed `opt-in`, with no regression and no predeclared benefit.

## User Scenarios

A developer edits a source file without committing. The next memory plan reflects that change. An adopter upgrades DocGuard in an existing repository and can inspect hooks, follow every proposed remediation, and verify the result without hidden initialization. An agent requests context for one qualified requirement and receives current hashed excerpts and linked tests, or an explicit abstention. The agent inspects a high structural grade and sees the separate readiness verdict and that factual accuracy remains unverified. A contributor challenges a confident finding, previews a metadata-only report, checks existing work, and supplies a synthetic regression example voluntarily.

## Traceability Matrix

The verification column above links each requirement to executable tests. The tests carry explicit requirement annotations. Fixture content and example IDs cannot satisfy a real requirement.

## Revision History

| Version | Date | Changes |
|---|---|---|
| 0.9.1 | 2026-10-10 | Freshness review for specs 038–053: every requirement and verification path still holds; those specs state their own requirements under `specs/`, so none is copied here |
| 0.9.0 | 2026-09-30 | Add FR-025 (Metrics-Consistency counts what ships and reports each drifted value once); record that the symbol map stays opt-in until the v2 protocol runs |
| 0.8.0 | 2026-09-29 | Renumber the second FR-016/017/018 rows to FR-022/023/024: each ID was used for two different requirements, and path-qualified test annotations bound to the second meaning |
| 0.7.0 | 2026-09-18 | Record the uninitialised-project hook boundary (exit 3) and foreign-hook backup protection shipped without a stated requirement |
| 0.6.0 | 2026-09-15 | Exclude disposable checkout copies from pointer evidence, distinguish shipped capability counts from enabled configuration, and explain non-clean planned registries without weakening traceability |
| 0.5.0 | 2026-09-15 | Require composable managed hooks, CI-safe evidence exits, and field-level registry drift explanations |
| 0.4.0 | 2026-09-15 | Make API omission remediation review-only and add field-replay precision contracts for routes, fixtures, histories, monorepos, design sync, and config schemas |
| 0.3.0 | 2026-09-15 | Add packed adoption, fail-closed reconciliation and pointers, lifecycle-aware traceability, exact-code policy, combined assessment, Python literal evidence, and route-discovery precision contracts |
| 0.2.0 | 2026-09-11 | Replace template requirements with implemented trust, feedback, and cache contracts |


---

## docs-canonical/SECURITY.md
> Authentication, authorization, secrets management, and security policies

# Security

<!-- docguard:quality negation-load off — prohibitions define security boundaries -->
<!-- docguard:version 0.15.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-10-10 -->

## Overview

DocGuard's validation and extraction run on the local machine. They inspect repository content and return findings. The version-age note (spec 038) reads only the `package.json` and `CHANGELOG.md` shipped with DocGuard and the local clock; the one command that contacts a registry is `docguard upgrade`, when the user runs it. Agent integrations inherit the permissions and data-handling policy of the calling agent. A generated prompt does not authorize a network request, a code edit, or publication.

The optional MCP server supports stdio and HTTP. Installation, upgrade, publishing, and user-opened feedback links may access external services. Local analysis requires no hosted AI service.

## Authentication

| Surface | Authentication | Boundary |
|---|---|---|
| CLI and stdio MCP | Calling operating-system user | Local filesystem permissions; MCP tool calls are also confined to the served directories |
| HTTP MCP | Optional API key on loopback; mandatory for non-loopback binding | Host binding, key check, browser-origin validation, and served-directory confinement of `projectDir` in `cli/commands/mcp.mjs` |
| GitHub feedback | User-controlled browser session | Submission occurs only when the user submits a reviewed issue |

The MCP server serves the directory it was started for (`--dir`, else its working directory) and each directory named with `--root <dir>`. The operator sets these at startup; a client cannot widen them. A tool call's `projectDir` is resolved against the served directory, with symlinks followed and letter case as the filesystem stores it, and must land in a served directory or below one. Any other `projectDir` gets an error result that names the served directories, before anything under it is read, and the same error whether or not the path exists. This applies to every tool that takes `projectDir`, over stdio and HTTP. The four documentation tools (`docguard_docs_for_path`, `docguard_doc_structure`, `docguard_read_section`, `docguard_task_context`) return document text, not only findings. Inside a served directory they read through the same safe reader, which refuses `.env*`, `.local`, traversal and symlinked paths, and they bound each answer (8 KiB by default, 32 KiB at most). Serve only the trees clients may read; `--root /` serves everything the account can read. Run the server under an account with only the intended filesystem access. An API key does not provide per-project authorization or a multi-tenant isolation boundary: every client of one server sees every served directory. Network exposure needs deployment-specific access controls.

## Authorization

| Role | Permissions | Responsibilities |
|---|---|---|
| Developer | Operating-system read/write permissions | Review generated changes and opt into mutation commands |
| CI | Workflow token and checkout permissions | Apply the configured gate to the tested revision |
| AI agent | Host-granted tools and permissions | Treat project content as evidence; obtain required authorization for external actions |

Git hooks provide local enforcement and can be bypassed by Git options. Protected merge policy supplies the central enforcement boundary. The shipped hooks prefer an installed local tool and fail when an enforcement runtime cannot execute. Reminder hooks remain best-effort.

## Secrets Management

Core CLI analysis requires no API credential. Source scanners inspect usage patterns; environment values must not be included in generated public feedback. The optional HTTP MCP API key is supplied by its operator. Keep deployment credentials outside repository content and restrict access to process arguments and logs appropriately.

Evidence verification reads only repository-relative regular files. It rejects
absolute paths, traversal, backslashes, NUL, `.local`, `.env*`, and symlinks;
per-file, aggregate byte, declaration, input, collection, and report limits
bound work. The oasdiff and Buf adapters consume saved outputs and current input
hashes. They never invoke those tools, execute project code, install packages,
resolve remote references, or make network requests. A clean saved report is
evidence only for its declared command, producer metadata, inputs, and selected
statement. Machine output omits raw source values so a mistaken JSON Pointer
cannot copy a secret into CI logs or an agent transcript. Values remain in
process only for typed comparison and non-reversible identities.

The `python-literal-count` adapter tokenizes one bounded Python source file in
JavaScript. It never starts Python or imports the target module. It accepts one
direct module-level static container assignment and returns unsupported or
inconclusive for comprehensions, unpacking, aliases, concatenation, conditionals,
duplicate assignments, malformed syntax, and parser-budget exhaustion.

Reconciliation invokes Git with argument arrays and disables text conversion.
Changed-path inventory and patch text have separate budgets. Any timeout,
overflow, parse failure, or path-limit breach returns partial coverage and cannot
produce a ready claim. Instruction-pointer indexing rejects traversal, private
paths, symlinks, and ambiguous basenames and never follows a match outside the
selected repository.

Task-context selection reuses the same bounded safe reader. It accepts at most
2,000 normalized task characters, stores only the task digest in output, and
does not execute project code, hooks, package managers, an LLM, or network
requests. Eligible prose is limited to configured canonical documents, approved
current specs whose recorded digest matches, and bounded project rules. `.local`,
environment files, traversal, backslashes, symlinks, oversized files, retired or
unapproved specs, and stale registry artifacts never enter selected content.

Feedback issue URLs contain allowlisted detector metadata, classification, parser tier, and a synthetic-shape duplicate identity. Full local finding records can include private paths and diagnostic text. Fixture manifests are accepted only with explicit synthetic-content and redaction-review attestations; they reject escaping paths, `.git`, `.local`, symlinked inputs, unsafe config values, and oversized content. Preview mode avoids all writes. Generated tests contain the attested synthetic fixture, so users must review it before contribution.

The optional external benchmark accepts only credential-free public HTTPS Git URLs pinned to full commit hashes. It disables interactive Git authentication and global/system Git configuration, forbids the file protocol, never runs project scripts, copies no `.git` metadata into case projects, and removes its temporary root by default. External execution is absent from ordinary tests and package installation.

## Subprocess Safety

Pass untrusted arguments through argv arrays and validate values for their intended operation. Avoid interpolating configuration or repository content into shell commands. Existing static command strings do not authorize expanding their input surface. Regression tests in `tests/security-init-injection.test.mjs` exercise the input boundary. `cli/spec-kit-delegation.mjs` is the only caller of the `specify` CLI, and only `init` and `upgrade --apply` reach it: it passes argv arrays (on Windows, `cmd.exe /c specify.cmd` with the same array), accepts an integration key only if it matches `^[a-zA-Z0-9_-]{1,32}$`, uses only the `init` options `specify init --help` documents, and bounds each call with a timeout. Its other calls are `specify --version` and `specify extension add <packaged extension> --dev`.

Every other subprocess also takes an argv array and no shell:

- Git: `ls-files -z --cached` / `--deleted` (tracked files), `check-ignore --no-index -z --stdin` (paths go through stdin, never the command line), `cat-file -e` with a regex-validated revision, and `rev-parse` (`cli/shared-git.mjs`, `cli/scanners/doc-deps.mjs`, `cli/scanners/revision-anchor.mjs`, `cli/commands/review.mjs`).
- `python3 -c` with a fixed script that only calls `ast.parse`, on a path already checked by the doc lock's path guard, with a 15-second timeout (`cli/scanners/doc-deps.mjs`). `cli/scanners/py-ast.mjs` runs one fixed extractor script that reads file paths on stdin, parses each with `ast.parse` and never imports or executes them, with a 30-second timeout; the import graph, the symbol map and the Python route, model and environment scanners share it.
- `upgrade --apply` runs `npm install -g docguard-cli@latest` when npm has a newer release (on Windows through the shell, with the same fixed arguments, because `npm` is a `.cmd` shim), and `upgrade --apply --pr` runs `git` and `gh` to open the schema-migration pull request (`cli/commands/upgrade.mjs`).
- Development only: `tools/budget.mjs` runs `node` and `npm pack --dry-run --ignore-scripts`.

Only two shell strings remain, and both are static: `git rev-parse --is-inside-work-tree` (`cli/shared-git.mjs`) and `which specify` / `where specify` (`cli/ensure-skills.mjs`).

Paths that come from documents or configuration are checked before use. A `covers=` target must be relative, without `..` or a symlink, resolve inside the project, and respect `.docguardignore`. An `ownership` pattern with `..` or an absolute prefix is an error (OWN007). `rules --for` refuses an absolute path, `..`, and a symlink that leaves the project. Instruction-scope discovery reads at most 200 files and walks at most 100,000 entries without following links.

## Command Safety Levels

Only `init`, and the aliases that run it (`setup`, `agents`, `hooks`, `badge`, `llms`, `publish`), installs DocGuard's agent skills and slash commands or prints the Spec Kit setup hint; `upgrade --apply` refreshes copies that are out of date. They go where the project's agent reads them: the skills directory in Spec Kit's install manifest, else a known integration folder (`.claude/skills/`, `.cursor/skills/`, `.agents/skills/`), else `.agent/skills/` and `.agent/commands/`. A directory read from `.specify/*.json` must stay inside the project. No other command installs them, in any mode, and an unknown command name writes nothing. Read, report, check and preview modes leave the working tree unchanged. The only place they may write is DocGuard's own state directory, `.docguard/`, which ignores itself: each writer creates `.docguard/.gitignore` containing `*` when it is missing and keeps an existing one. DocGuard's dirty-tree checks (`memory --pack`, `report`, `reconcile`, `specs complete`) leave `.docguard/` out.

| Operation | Source writes | Auxiliary writes / effects |
|---|---|---|
| guard, score, diff, diagnose | None by default | Plan caching may write into the self-ignoring `.docguard/`; explicit mutation flags change behavior. `guard --update-baseline` writes `.docguard.baseline.json` at the project root |
| ci | None | Records history unless `--no-history` is set |
| feedback | None | Saves local records or an explicitly requested direct `tests/*.test.mjs` contribution unless `--preview`; prints opt-in URLs but never submits |
| memory --pack | None | Writes a generated context pack unless `--stdout` is used; `--symbols` adds a bounded symbol map |
| agent, agent --task | None | Emits a task graph or transient bounded context; never stores raw task text or selected output |
| fix --write, sync --write | Targeted documentation edits | Mapped human documents permit only unique `source=code` sections; backups and fix history remain enabled where supported. `sync --write` skips a section drawn from incomplete evidence unless `--allow-partial` |
| review --accept, review --prune | `.docguard-doc-lock.json` only | One file transaction; `--accept` requires a reason. Plain `review` and `review --suggest` write nothing |
| rules --for, trace --owners [--suggest], ignore | None | Read-only, including agent skills and the Spec Kit hint; `--suggest` prints a draft ownership block and never writes configuration; `ignore --format claimspec` prints its document to stdout |
| reconcile | None by default | `--write` delegates only mechanical generated-section refreshes to `sync` |
| specs, specs preflight, specs require | None for check/plan modes | `specs --write` refreshes the registry; `specs approve --write` records a reviewed approval and delivery state in the registry only; `specs complete --write` transactionally records a reviewed outcome and active context; `specs reanchor --write` moves recorded outcome revisions in the registry |
| verify --evidence | None | Reads the strict local manifest, selected Markdown, source files, and saved reports; guard consumes the same evaluator |
| retire --write | Explicit clean tracked documentation only | Requires retained-ref recovery proof, clean replacement/evidence docs, and no live Markdown backreferences |
| init, generate | Documentation and configuration scaffolding | Explicit force options may overwrite content. When `specify` is installed and Spec Kit is not initialized, `init` runs `specify init --here --force` (skip with `--no-spec-kit`) and registers the packaged extension. `generate --spec --write` adds one spec and its registry entry in one transaction |
| hooks | Hook configuration and executable scripts | Inventory distinguishes managed, legacy, foreign, missing, and unreadable hooks; removal preserves foreign commands around a managed block; auto-fix hooks may edit and stage documentation. A foreign hook is skipped under `--force` and no `.bak` is written, so overwriting one requires `--force` twice rather than a plain re-install. A hook owned by husky, lefthook or simple-git-hooks is never written, even under `--force`. An installed hook skips a working tree with no `.docguard.json` and permits guard exit 3, so a branch or worktree that never adopted DocGuard is not blocked by a repo-wide hook |
| report | None by default | `--out` writes an artifact |
| upgrade | None by default | Contacts the npm registry unless `--schema-only`; `--check-only` only reports. `--apply` installs the newer CLI globally, migrates `.docguard.json`, refreshes out-of-date agent files and re-registers the Spec Kit extension when its registered version differs; `--pr` puts the migration on a new branch and opens a pull request instead of editing in place |

Review the exact command and flags before assigning privileges. CLI help is the authoritative command inventory.

Mapped paths do not weaken the write boundary. A new target or an existing
`docguard:generated true` file can receive a single-role full-document write;
otherwise only an exact code-owned section can change. The command validates
all mapped targets before its first visible write, rejects malformed or shared
ownership, and treats `--force` as overwrite intent rather than authorization.

## Supply Chain

The package declares one exact-pinned dependency, `@babel/parser`, with its transitive Babel dependencies recorded in `package-lock.json`. AST extraction degrades to a regex fallback when Babel is unavailable. Python AST extraction optionally uses the installed `python3` runtime. No additional runtime package is introduced by the trust improvements.

Cloudflare binding extraction reads source and Wrangler file presence only. It
does not load configuration, import application modules, contact Cloudflare, or
read binding values. AST trust requires an official handler/export/import/class
signal and lexical identity; similarly named local objects do not grant binding
status. The parser fallback discloses unsupported class, Pages, and imported-env
forms instead of treating their absence from the inventory as proof.

Repository-root guidance reads only bounded ancestor metadata: regular
`.docguard.json`, `package.json`, and `pnpm-workspace.yaml` files, plus Git's
reported working-tree root. It does not execute package managers or project
code, follow manifest symlinks, scan outside the selected Git boundary, or
change the directory passed to a command.

Dependency audit results are time-specific observations. Run the current audit and supported Node-version matrix before release; a historical clean audit is not a continuing guarantee. Pin third-party CI actions to verified commit SHAs and install from the lockfile. The release workflow's `publish-homebrew` job holds the only stored release secret, `HOMEBREW_TAP_DEPLOY_KEY`, a write deploy key scoped to `raccioly/homebrew-tap`. It checks the downloaded npm tarball against `dist.integrity` and pins GitHub's SSH host keys before pushing.

## .gitignore Audit

Exclude `node_modules`, environment values, generated build output, and private local files from version control. `.docguardignore` controls analysis coverage separately; it is not a secrecy boundary for every tool that runs in the repository.

A `.docguardignore` entry can end: `expires:YYYY-MM-DD` keeps it through that UTC day. An expired entry, or one whose expiry is not a calendar date, excludes nothing, so the Security validator scans the paths it hid again; guard reports IGN001 or IGN003. `securityIgnore` patterns carry no reason or expiry; `docguard ignore` lists them with every other suppression.

## Security Rules Checklist

- Validate subprocess inputs at their call boundaries.
- Preserve provenance checks before mechanical edits.
- Keep private diagnostics separate from public feedback payloads.
- Treat submitted reproductions as untrusted data.
- Require credentials for non-loopback HTTP MCP binding.
- Disclose unknown or unsupported verification instead of asserting success.
- Verify protected merge policy independently of local hook installation.

## Revision History

| Version | Date | Changes |
|---|---|---|
| 0.15.0 | 2026-10-10 | `.docguardignore` expiries fail closed (an expired or unreadable entry excludes nothing); `docguard ignore` is read-only (spec 057) |
| 0.14.0 | 2026-10-10 | Freshness review for specs 038–053: `upgrade --apply` also installs agent files and calls `specify` (spec 048), the `upgrade` and `guard --update-baseline` write levels, the `npm`/`git`/`gh` subprocesses of `upgrade`, the shared Python extractor's stdin and timeout, and where agent files are written |
| 0.13.0 | 2026-09-30 | Only `init` installs agent skills and slash commands; read and preview modes write nothing; `.docguard/` ignores itself and dirty checks leave it out (spec 042) |
| 0.12.0 | 2026-09-30 | Freshness review for specs 028–037: full subprocess inventory (git, `python3 -c`, dev tooling), the remaining static shell strings, path checks for `covers=`, ownership patterns and `rules --for`, the MCP documentation tools' boundary, and the `review`, `rules` and `trace --owners` write levels |
| 0.11.0 | 2026-09-29 | Freshness review: the `specify` subprocess boundary, hook-manager ownership, the spec-first gate, and the Homebrew tap deploy key |
| 0.10.0 | 2026-09-14 | Prevent scoped evidence output from exposing raw source values |
| 0.9.0 | 2026-09-14 | Document public benchmark isolation and synthetic feedback-fixture privacy boundaries |
| 0.8.0 | 2026-09-14 | Document reconciliation and transactional spec lifecycle authority |
| 0.7.0 | 2026-09-11 | Document HTTP MCP, auxiliary writes, enforcement scope, and feedback privacy |


---

## docs-canonical/TEST-SPEC.md
> Test coverage requirements, testing strategy, and quality rules

# Test Specification

<!-- docguard:version 1.4.2 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-10-10 -->

> DocGuard has a single optional-load npm dependency (`@babel/parser`) and an optional `python3` AST tier. CLI integration tests cover the full stack with `node:test` (zero dev dependencies) and exercise both AST extractors (`js-ast`, `py-ast`) plus their regex fallbacks. The Python AST tests skip themselves automatically on a machine that lacks `python3`.

| Metadata | Value |
|----------|-------|
| **Status** | ![Status](https://img.shields.io/badge/status-active-brightgreen) |
| **Project Type** | CLI |
| **Test Framework** | `node:test` (built-in) |
| **Test Files** | `tests/` |

---

DocGuard's tests verify command behavior through subprocess execution. Command tests run the full CLI binary as a subprocess, capturing stdout and checking output patterns. This approach tests the complete stack in a single pass: argument parsing, config loading, validator execution, and output formatting.

Tests are designed to be config-aware. They verify that project-type settings like needsEnvExample and testFramework correctly influence scoring and validation behavior. Regression guards preserve known failures with dedicated assertions and neighboring valid cases.

All tests use the built-in node:test framework with zero test dependencies. CI runs the suite on Node 18, 20, 22, and 24. Its runtime budget (`TEST_BUDGET_MS` in `.github/workflows/ci.yml`, 360 seconds since spec 051; `tests/release-cut-green.test.mjs` keeps it above normal runs and below a known regression) catches large regressions; local timing depends on runtime and filesystem. Record measured timing with its environment rather than asserting a universal duration.

Test names follow the pattern: "verb + expected behavior" (e.g., "runs and shows a score", "respects projectTypeConfig"). Each test should isolate its mutable fixtures and clean up its resources.

## Test Categories

| Category | Framework | Location | Run Command |
|----------|-----------|----------|-------------|
| Unit | node:test | tests/ | `npm test` |
| CLI Integration | node:test | tests/ | `npm test` |

> **CLI integration tests cover command routing; packed adoption tests cover the
> release boundary.** Source-tree subprocess success does not prove that npm
> contains every linked or imported file, that remediation is actionable, or
> that an existing repository remains unchanged during inspection.

All test files live in `tests/` and match the glob `tests/*.test.mjs` — the test runner supplies the current inventory as the suite grows; see the Source-to-Test Map below for the source→test traceability that matters.

## Coverage Rules

| Metric | Target | Current |
|--------|:------:|:-------:|
| Command Coverage | Every public command | Scenario coverage; inspect tests before claiming exhaustive behavior |
| Validator Coverage | Every validator | Positive, negative, and regression cases |
| Flag Coverage | Risk-based | Tested scenarios; no exhaustive coverage claim |
| Test Count | — | Current count is emitted by `npm test` |

## Source-to-Test Map

| Source File | Test File | Status |
|------------|-----------|:------:|
| `cli/docguard.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/shared.mjs` | `tests/commands.test.mjs`, `tests/ignore-parser-parity.test.mjs` | ✅ |
| `cli/commands/init.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/guard.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/score.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/diff.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/generate.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/agents.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/agent.mjs`, `cli/scanners/task-context.mjs` | `tests/agent.test.mjs`, `tests/task-context.test.mjs` | ✅ |
| `cli/commands/hooks.mjs` | `tests/hooks.test.mjs`, `tests/hooks-contract.test.mjs` | ✅ |
| `extensions/spec-kit-docguard/extension.yml`, `extensions/spec-kit-docguard/commands/*.md` | `tests/hooks-contract.test.mjs` | ✅ |
| `cli/commands/diagnose.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/badge.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/ci.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/fix.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/watch.mjs` | `tests/commands.test.mjs`, `tests/watch-reliability.test.mjs` | ✅ pass |
| `cli/commands/publish.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/commands/trace.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/shared-requirements.mjs` | `tests/traceability.test.mjs`, `tests/archive.test.mjs` | ✅ |
| `cli/scanners/requirement-evidence.mjs` | `tests/traceability.test.mjs`, `tests/spec-registry.test.mjs` | ✅ |
| `cli/commands/retire.mjs` | `tests/archive.test.mjs` | ✅ |
| `cli/writers/file-transaction.mjs` | `tests/file-transaction.test.mjs` | ✅ |
| `cli/scanners/reconciliation.mjs`, `cli/commands/reconcile.mjs` | `tests/reconcile.test.mjs` | ✅ |
| `cli/commands/specs.mjs` completion path | `tests/spec-completion.test.mjs` | ✅ |
| Packed install adoption journey | `tests/adoption-workflow.test.mjs`, `tests/npm-pack-smoke.test.mjs` | ✅ |
| `cli/shared-git.mjs`, reconciliation coverage | `tests/shared-git.test.mjs`, `tests/reconcile.test.mjs` | ✅ |
| `cli/shared-ignore.mjs`, `cli/scanners/instruction-audit.mjs` | `tests/docguardignore.test.mjs`, `tests/instruction-audit.test.mjs` | ✅ |
| `cli/shared-validator-surface.mjs`, `cli/validators/metrics-consistency.mjs`, `cli/validators/canonical-sync.mjs` | `tests/metrics-consistency.test.mjs`, `tests/canonical-sync.test.mjs`, `tests/mechanical.test.mjs` | ✅ |
| `cli/validators/traceability.mjs`, lifecycle trust in `cli/scanners/spec-registry.mjs` | `tests/traceability-lifecycle.test.mjs` | ✅ |
| Finding-code enforcement and machine writers | `tests/severity.test.mjs`, `tests/sarif.test.mjs`, `tests/junit.test.mjs` | ✅ |
| Combined readiness assessment | `tests/assessment.test.mjs` | ✅ |
| `benchmarks/lib/manifest.mjs`, `benchmarks/lib/metrics.mjs`, `benchmarks/lib/compare.mjs` | `tests/benchmark-manifest.test.mjs`, `tests/benchmark-metrics.test.mjs` | ✅ |
| `benchmarks/lib/runner.mjs`, `benchmarks/run.mjs` | `tests/benchmark-runner.test.mjs` | ✅ |
| `benchmarks/lib/baseline.mjs`, `schemas/docguard-benchmark-baseline.schema.json`, `benchmarks/baseline.json` | `tests/benchmark-baseline.test.mjs` | ✅ |
| `benchmarks/lib/precision-evidence.mjs`, `benchmarks/generate-precision-evidence.mjs`, `cli/precision-evidence.mjs`, `cli/precision-evidence-data.mjs`, `schemas/docguard-precision-evidence.schema.json` | `tests/precision-evidence.test.mjs` | ✅ |
| `benchmarks/agent-context/run.mjs`, task-context schemas and fixtures | `tests/agent-context-benchmark.test.mjs` | ✅ |
| `cli/evidence/*.mjs`, `cli/validators/evidence.mjs` | `tests/evidence-manifest.test.mjs`, `tests/evidence-adapters.test.mjs`, `tests/evidence-integration.test.mjs` | ✅ |
| `cli/feedback-fixture.mjs`, `cli/commands/feedback.mjs` | `tests/feedback-fixture.test.mjs`, `tests/feedback-contributions.test.mjs` | ✅ |
| `cli/validators/document-lifecycle.mjs` | `tests/document-lifecycle.test.mjs` | ✅ |
| `cli/commands/specs.mjs`, `cli/scanners/spec-registry.mjs`, `cli/validators/spec-registry.mjs` | `tests/spec-registry.test.mjs` | ✅ |
| `cli/spec-kit-delegation.mjs`, `cli/ensure-skills.mjs` | `tests/spec-kit-delegation.test.mjs`, `tests/security-init-injection.test.mjs` | ✅ |
| `cli/scanners/spec-first.mjs` (`specs require`) | `tests/spec-first.test.mjs` | ✅ |
| `cli/scanners/agent-instructions.mjs` (STR004/STR005) | `tests/agent-instruction-budget.test.mjs` | ✅ |
| `cli/scanners/as-built.mjs`, `cli/commands/generate-as-built.mjs` | `tests/as-built-specs.test.mjs` | ✅ |
| `cli/shared-test-cases.mjs` | `tests/test-case-count.test.mjs` | ✅ |
| `cli/scanners/speckit.mjs` (SPK010 directory claims) | `tests/spk010-directory-claims.test.mjs`, `tests/spec-kit-artifact-coverage.test.mjs` | ✅ |
| `.github/scripts/release-changelog.mjs`, `.github/scripts/homebrew-formula.mjs` | `tests/release-readiness.test.mjs` | ✅ |
| `cli/scanners/revision-anchor.mjs` (`specs reanchor`, SPR008) | `tests/completion-revision-anchoring.test.mjs` | ✅ |
| `cli/scanners/reconciliation.mjs` (canonical requirement links) | `tests/canonical-requirement-links.test.mjs`, `tests/reconcile.test.mjs` | ✅ |
| `cli/validators/structure.mjs` | `tests/commands.test.mjs` | ✅ |
| `cli/validators/docs-diff.mjs` | `tests/commands.test.mjs`, `tests/docs-diff.test.mjs` | ✅ |
| `cli/scanners/doc-deps.mjs`, `cli/validators/doc-dependency.mjs`, `cli/commands/review.mjs` (doc dependency lock, DLK001–DLK005) | `tests/doc-dependency-lock.test.mjs` | ✅ |
| `tools/budget.mjs` (non-regression budgets) | `tests/budget.test.mjs` | ✅ |
| `cli/commands/mcp.mjs` (MCP server over stdio and loopback HTTP; served-directory confinement) | `tests/mcp.test.mjs`, `tests/mcp-http.test.mjs`, `tests/mcp-doc-tools.test.mjs`, `tests/mcp-project-confinement.test.mjs` | ✅ |
| `cli/release-age.mjs` (version-age note on guard text, MCP instructions and the context pack) | `tests/update-awareness.test.mjs` | ✅ |
| `cli/shared-guard-json.mjs` (compact guard response) | `tests/compact-guard-response.test.mjs` | ✅ |
| `cli/scanners/doc-references.mjs`, `cli/shared-headings.mjs` (MCP doc navigation) | `tests/mcp-doc-tools.test.mjs`, `tests/trace-reverse.test.mjs` | ✅ |
| `cli/shared-sync-scope.mjs` (`sync --since` section scope) | `tests/sync-since.test.mjs` | ✅ |
| `cli/scanners/instruction-scopes.mjs`, `cli/scanners/frontmatter.mjs`, `cli/validators/path-scoped-rules.mjs`, `cli/commands/rules.mjs` (PSR001–PSR004, `rules --for`) | `tests/path-scoped-rules.test.mjs` | ✅ |
| `cli/scanners/doc-ownership.mjs`, `cli/validators/doc-ownership.mjs` (OWN001–OWN007, `trace --owners`) | `tests/doc-ownership.test.mjs` | ✅ |
| `cli/scanners/import-graph.mjs`, `cli/scanners/module-diagram.mjs` (module and entity diagrams) | `tests/code-derived-diagrams.test.mjs`, `tests/architecture.test.mjs`, `tests/python-import-graph.test.mjs` | ✅ |
| `cli/scanners/symbol-map.mjs`, `benchmarks/agent-context/run.mjs` (symbol map, agent-context protocol v2) | `tests/symbol-map.test.mjs`, `tests/agent-context-benchmark.test.mjs` | ✅ |
| `cli/scanners/go-routes.mjs`, `cli/scanners/spring-routes.mjs`, `cli/scanners/rails-routes.mjs`, `cli/scanners/route-lexing.mjs` (Go, Spring and Rails routes) | `tests/go-spring-rails-routes.test.mjs`, `tests/routes-multilang.test.mjs` | ✅ |
| `cli/scanners/php-routes.mjs`, `cli/scanners/dotnet-routes.mjs` (Laravel, Symfony and ASP.NET Core routes) | `tests/php-dotnet-routes.test.mjs` | ✅ |
| `cli/shared-ignore.mjs` (`.docguardignore` reasons and expiries), `cli/scanners/suppressions.mjs`, `cli/validators/suppressions.mjs`, `cli/commands/ignore.mjs`, `cli/writers/claimspec-ignore.mjs`, `cli/claimspec-schema.mjs` (IGN001–IGN003, `docguard ignore`, claimspec export) | `tests/ignore-reasons.test.mjs`, `tests/docguardignore.test.mjs` | ✅ |
| `cli/writers/baseline.mjs` (adoption baseline, stable fingerprints and legacy match) | `tests/baseline.test.mjs`, `tests/baseline-stable-fingerprint.test.mjs` | ✅ |
| `cli/scanners/reconciliation.mjs`, `cli/validators/spec-registry.mjs` (reviewed `assetPaths`, SPR009) | `tests/asset-path-attribution.test.mjs` | ✅ |
| `cli/shared-source.mjs`, `cli/shared-ignore.mjs` (`fallback-language` tier, product-code package segments) | `tests/fallback-language-coverage.test.mjs`, `tests/parser-tier.test.mjs` | ✅ |
| `cli/commands/specs.mjs` (first-spec preflight, `specs approve`) | `tests/first-spec-preflight.test.mjs` | ✅ |
| `cli/writers/state-dir.mjs`, `cli/docguard.mjs` dispatch (read-only commands, self-ignoring `.docguard/`) | `tests/read-only-commands.test.mjs` | ✅ |
| `cli/scanners/py-outline.mjs`, `cli/scanners/python-routes.mjs`, `cli/scanners/python-models.mjs`, `cli/scanners/py-env.mjs` (both Python tiers) | `tests/python-extraction.test.mjs`, `tests/py-ast.test.mjs` | ✅ |
| `cli/scanners/js-ast.mjs`, `cli/scanners/ts-paths.mjs`, `cli/scanners/schemas.mjs` (Express and Next.js extraction) | `tests/js-ts-extraction.test.mjs` | ✅ |
| `cli/scanners/schemas.mjs`, `cli/scanners/js-ast.mjs`, `cli/validators/schema-sync.mjs` (decorated entity classes and model-definition calls, both parser tiers) | `tests/typeorm-models.test.mjs` | ✅ |
| `cli/writers/doc-generators.mjs`, `cli/writers/generate-io.mjs`, `cli/commands/generate.mjs` (generated docs pass DocGuard's own checks) | `tests/generated-docs-consistency.test.mjs`, `tests/generated-docs-e2e.test.mjs` | ✅ |
| Command output across guard, fix, sync, trace, upgrade and init (spec 049) | `tests/output-ux.test.mjs` | ✅ |
| `cli/agent-surface.mjs`, `cli/spec-kit-delegation.mjs` (hooks that resolve, extension refresh) | `tests/spec-kit-integration-honesty.test.mjs` | ✅ |
| `cli/config.mjs`, `cli/shared-doc-roles.mjs`, `cli/validators/metrics-consistency.mjs` (dogfood fixes) | `tests/dogfood-findings.test.mjs` | ✅ |
| `.github/scripts/sync-release-version.mjs`, `cli/release-pr-policy.mjs`, the CI runtime budget | `tests/release-cut-green.test.mjs`, `tests/release-version-sync.test.mjs` | ✅ |
| `tools/generate-docs-reference.mjs`, `docs/` site navigation and links | `tests/user-docs.test.mjs` | ✅ |

Completion tests cover initial `implemented → verified` delivery, evidence and
dirty-tree blockers, living-spec `verified → verified` maintenance, and refusal
of empty repeats driven only by generated lifecycle artifacts.

> **Note**: `watch.mjs` is an interactive file-watcher (uses `fs.watch` + process signals). It is
> covered by automated lifecycle tests, including filesystem watcher error handling.
> `tests/watch-reliability.test.mjs` drives the 500 ms debounce with an injected fake clock
> (`runtime.setTimeout` / `runtime.clearTimeout`) and captures console output, so its coalescing and
> queued-check assertions are driven by a fake clock and hold under any machine load (spec 055). The spawn
> tests in `tests/commands.test.mjs` cover real `fs.watch` events and real timers end to end.
> Manual checks supplement platform-specific event behavior.

> **Note**: `tools/budget.mjs` compares guard wall time between two trees. `tests/budget.test.mjs`
> decides every guard-time verdict on a scripted timing source (the `sample` option of `guardPair`):
> seeded load bursts, a 30% slowdown, the interleaving order and a steady drift, so no assertion
> depends on machine load (spec 062). One test spawns real stub trees and asserts only that each
> median is at least the stub's busy-wait. The CI `budget` job exercises the real path on every PR.

## Critical CLI Flows

| # | Flow | Test File | Status |
|---|------|-----------|:------:|
| 1 | `docguard audit` | `tests/commands.test.mjs` | ✅ |
| 2 | `docguard init` | `tests/commands.test.mjs` | ✅ |
| 3 | `docguard guard` | `tests/commands.test.mjs` | ✅ |
| 4 | `docguard guard --format json` | `tests/commands.test.mjs` | ✅ |
| 5 | `docguard score` | `tests/commands.test.mjs` | ✅ |
| 6 | `docguard score --format json` | `tests/commands.test.mjs` | ✅ |
| 7 | `docguard score --tax` | `tests/commands.test.mjs` | ✅ |
| 8 | `docguard diagnose` | `tests/commands.test.mjs` | ✅ |
| 9 | `docguard diagnose --format json` | `tests/commands.test.mjs` | ✅ |
| 10 | `docguard generate` | `tests/commands.test.mjs` | ✅ |
| 11 | `docguard init --profile starter` | `tests/commands.test.mjs` | ✅ |

---

## Revision History

| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.4.2 | 2026-10-10 | DocGuard Team | Record the scripted timing source behind the budget tests (spec 062) |
| 1.4.1 | 2026-10-10 | DocGuard Team | Map `watch.mjs` to its deterministic reliability tests (spec 055) |
| 1.4.0 | 2026-10-10 | DocGuard Team | Freshness review for specs 038–053: map the modules those specs added or changed to their tests; record the 360-second CI runtime budget (spec 051) |
| 1.3.0 | 2026-09-30 | DocGuard Team | Freshness review: map the MCP server and the compact guard response to their tests; describe the frozen v2 agent-context protocol beside v1 |
| 1.2.0 | 2026-09-29 | DocGuard Team | Freshness review: map the modules specs 014–026 added to their tests, and correct the benchmark case count |
| 1.1.0 | 2026-09-15 | DocGuard Team | Add field-replay controls for shipped validator counts, ignored checkout pointer copies, and non-clean planned lifecycle advice |
| 1.0.0 | 2026-09-15 | DocGuard Team | Add executable hook-composition, direct evidence exit-code, and field-level registry-difference regressions |
| 0.7.0 | 2026-03-13 | @raccioly | Added trace, publish; watch.mjs coverage justified (ISO 29119); 15 commands |
| 0.5.0 | 2026-03-13 | @raccioly | Added diagnose, guard JSON, profile, tax tests (24→30) |
| 0.3.0 | 2026-03-12 | @raccioly | Real tests, project-type-aware spec |
| 0.1.0 | 2026-03-12 | DocGuard Generate | Auto-generated (corrected) |

## Trust regression scenarios

`tests/score-assurance.test.mjs` checks that structural grades never claim factual verification and that CI, diagnose, and reports retain this boundary. `tests/feedback-contributions.test.mjs` checks confident-finding selection, preview behavior, and outbound metadata privacy. Cache tests must change source contents without changing a manifest or Git HEAD, including repeated edits and fresh-process reads. Hook tests execute generated scripts against controlled runtimes rather than merely matching shell text. Traceability tests pair synthetic fixture IDs with genuine requirement annotations.

A detector fix should include a clean near-miss and a real defect. Held-out neighboring cases are required to evaluate generalization. `benchmarks/baseline.json` is the reviewed reference: 26 measured cases across 13 repository groups plus one explicit unsupported case. Its observed perfect point estimates retain Wilson confidence limits and do not establish exhaustive correctness. The envelope's `review.measures` is `benchmark-precision` and its `review.caveat` must accompany any quoted number; tests assert the committed metrics and caveat are recomputable from the committed cases, and that a hand-edited ratio, stale caveat, or pre-provenance envelope is rejected.

The finding shape is itself a contract, because findings are copied verbatim into shareable feedback records. `tests/precision-evidence.test.mjs` pins the exact key set a guard run emits, so a field added without a specification fails the suite; it also asserts that `location` is a string or null and that `disposition` and `evidence.status` hold values from their declared vocabularies. `tests/findings-channels.test.mjs` pins the derivation rules for the three channels: `disposition` from `suggestion.kind` with `escalate` as the fail-closed default, `evidence` projected per code and always standing alone, and `reportable` true for an unmeasured code even at high confidence. A malformed `suggestion.kind` must omit the suggestion, so a typo stays visible as a missing suggestion instead of reclassifying a fix as a review.

Retirement tests use disposable Git repositories and verify both sides of the boundary: completed planning material is reported for review, while active neighboring material stays clean. Write-path tests must prove retained-ref recovery metadata and refusal of source code, dirty, untracked, required, symlinked, private, protected, submodule, and out-of-root paths. Read-only plan and check modes must not modify repository state.

## Enterprise precision regressions

Regression cases are synthetic and name no consumer repositories. Keep a valid near-neighbor beside every detected defect: formatting versus declaration deletion; negated versus current technology use; explained versus unexplained skips; mock expectations versus credentials; implemented versus omitted contract endpoints; Worker bindings versus local variables; historical versus active documents. Check coverage tests distinguish unsupported and missing inputs from executed checks. Document-role tests exercise mapped findings, raw/loaded configuration parity, unsafe paths, read-only planning, successful section-bounded generation/sync/repair, backup creation, multi-target preflight atomicity, and force-resistant malformed/shared ownership.

Worker fixtures reproduce Cloudflare's module-handler argument, exported Pages
context, imported global `env`, and entrypoint-class property forms. Every form
has a lookalike source, class, function, shadow, or computed-key control. The
packed-package test runs without installed Babel and requires AST-only forms to
remain disclosed as limitations.

Python import-graph fixtures cover regular flat and `src/` packages, absolute
and explicit-relative imports, cycles, and configured layer violations. Paired
controls keep standard-library and third-party imports out of the local graph;
dynamic imports, `sys.path` mutation, parse failures, interpreter absence, and
ambiguous workspace modules must remain partial or unsupported applicability.
Tests never import or execute fixture modules.

Repository-root fixtures pair ancestor DocGuard, npm workspace, and pnpm
include evidence with explicit-dir, local-config, exclusion, Git-only, and
nested-repository controls. Command tests require the selected package result to
remain unchanged, human guidance to contain an exact rerun, and machine stdout
to remain parseable while stderr carries the typed diagnostic.

Independent review must challenge suppression paths, not only the original false-positive example. Cross-project runs use disposable snapshots and verify consumer content remains unchanged. Finding counts alone cannot establish precision or recall.

Task-context tests require exact path and qualified-requirement priority,
determinism, fixed excerpt/read budgets, stale and retired lifecycle exclusion,
private/symlink rejection, honest abstention, aligned human/JSON output, and
unchanged task-graph behavior. The frozen agent benchmark keeps hidden
evaluators outside copied repositories, proves original fail-to-pass and
pass-to-pass states plus reviewed references, records all 27 observations, and
applies its committed non-inferiority and efficiency gate without an LLM judge.
Protocol v2 (spec 036) is frozen beside v1 rather than replacing it: six tasks
(three on the 29-module `ledger-service` fixture), three context conditions
(task only, context pack, context pack with symbol map) and three repetitions,
so 54 trials.
The runner refuses to start when a task's fixture or evaluator digest, or the
manifest digest, differs from the committed value. v2 was frozen with a CLI
version and model that ChatGPT-account Codex no longer serves, so protocol v3
changes only that executable pin, and the runner rejects a v3 manifest that
differs from v2 anywhere else. The v3 trials are recorded in
`results/observed-v3.json`, and a test recomputes their decision (`opt-in`)
from the recorded trials.

Run `node benchmarks/run.mjs --baseline benchmarks/baseline.json` for the network-free synthetic corpus (CI does this on every push; pinned public baseline cases are reported as `outOfSelection`, not as removed). Run `node benchmarks/run.mjs --external --baseline benchmarks/baseline.json` explicitly for pinned public sources. The comparator gates new case-level false positives, false negatives, removals within the selection, and supported-case abstentions. Persisted cold/warm timings are observational because matching Node and platform metadata cannot exclude host contention. Runtime regression claims require at least five controlled samples from the same paired comparison session and a greater-than-20-percent real-workload change.

Evidence adapter tests use paired valid and invalid fixtures. They cover RFC
6901 escaping and array indices, strict typed values, duplicate-free sets,
bounded collections, saved clean and non-clean oasdiff/Buf reports, malformed
reports, unknown adapter versions, input-digest staleness, missing and ambiguous
Markdown targets, unsafe paths, and stable identity behavior. Integration tests
confirm that guard, JSON, SARIF, JUnit, score assurance, agent context, and direct
CLI exit codes retain the five-state boundary. Contradicted and invalid direct
verification exits 1; stale, inconclusive, and unsupported evidence exits 2;
verified evidence exits 0. No test invokes third-party project code or an
external compatibility binary.

Python literal evidence adds paired list, tuple, set, dictionary, annotated,
multiline, string/comment, dynamic, duplicate-assignment, malicious, symlink,
and explicit-budget fixtures. Tests assert that project Python is never
executed. The packed adopter workflow starts with a foreign hook and a
specification missing immutable metadata, runs the installed CLI, validates
every rendered suggestion, follows the proposed remediation, and requires the
post-action registry check to pass.
Managed-hook tests reinstall into a hook with commands on both sides, require
exactly one marker pair, repair nested markers emitted by affected releases,
execute every successful postlude, and retain fail-closed exits. Spec-registry
tests require stale JSON and text output to identify bounded field paths,
including order-only canonicalization.

Express route fixtures pair real registrations with chained HTTP-client calls,
same-path test requests, conventional test-helper trees, single-level mounts,
and nested imported mounts. They also cover static path constants, pathless
mounts, aliased router exports, trailing middleware, and multiple router symbols
in one module. Non-product evidence is rejected before route-key deduplication
so a discarded test call cannot hide a later product route.

Go, Spring and Rails route fixtures are eight reference projects (gin, echo,
chi, net/http, gorilla/mux with fiber, Spring in Java and in Kotlin, Rails).
Each ground truth was written from the framework's routing rules before the
scanner was changed, and the scanner must report exactly that set. Neighbouring
cases cover router names reused in two functions, same-named methods told apart
by package and receiver type, cyclic calls, HTTP-client calls, comments,
`_test.go` files and Feign clients. Prefixes that cannot be read must yield no
route at all.

Sinatra, Starlette, Warp, Tide and `net/http`-only Go route fixtures are five
reference projects run end to end from framework detection, each with a ground
truth written from the framework's routing rules before its reader existed
(all five reported no route before). Neighbouring cases cover Sinatra apps
resolved through project superclasses, `use` and `Rack::URLMap`, Rack::Test
calls, heredocs and `=begin` blocks; Starlette at both Python tiers and a
`Router` from another library; Warp alternatives that cannot match; Tide
bindings reused in two functions; and non-literal paths, which must be omitted
and counted in the API-surface reason.

API omission tests preserve documented endpoints across generated and human
documents, mapped roles, and forced writes whenever the only runtime-absence
evidence is a bounded scan. Field-replay controls pair historical and current
metrics, package capability and enabled-project validator counts, multiline and
disconnected skip reasons, repeated and unrepeated test credentials,
package-local and ignored environment templates, ignored and nested-checkout
pointer copies, committed and non-clean planned lifecycle registries,
authoritative and stale OpenAPI copies, parameter-equivalent and different
routes, frontend utilities and real services, and known versus unknown validator
schema keys.


---

## DRIFT-LOG.md
> Known deviations from canonical documentation

# Drift Log

<!-- docguard:last-reviewed 2026-09-14 -->

> Documents conscious deviations from canonical specifications.
> Every `// DRIFT: reason` in code must have a corresponding entry here.

| Date | File | Canonical Doc | Drift Description | Severity | Resolution |
|------|------|---------------|-------------------|----------|------------|
| 2026-03-13 | `cli/commands/generate.mjs` | ARCHITECTURE.md | AGENTS.md template includes `// DRIFT: reason` as an instruction pattern for end users. These are template strings, not actual code deviations. | Info | By design — template content |
| 2026-03-13 | `cli/commands/generate.mjs` | ARCHITECTURE.md | DRIFT-LOG.md template includes `// DRIFT: reason` as placeholder text. | Info | By design — template content |
| 2026-03-13 | `cli/commands/agents.mjs` | ARCHITECTURE.md | Agent config generators include `// DRIFT: reason` as instruction text for AI agents. 3 occurrences across Windsurf, Cursor, and generic agent configs. | Info | By design — instruction content |
| 2026-03-13 | `cli/validators/drift.mjs` | ARCHITECTURE.md | Drift validator references `// DRIFT:` pattern in JSDoc and regex. | Info | By design — validator implementation |
| 2026-05-12 | `tests/drift.test.mjs` | ARCHITECTURE.md | Drift validator tests use `// DRIFT:` comments to simulate project files having drift comments. | Info | By design — test implementation |
| 2026-05-26 | `tests/scoping-extended.test.mjs` | ARCHITECTURE.md | v0.15 P3 test fixture builds `// D' + 'RIFT:` strings via concat to test changed-files scoping without false-positiving the outer scan. | Info | By design — test implementation; mitigated by v0.15.1 hotfix that skips test files by default in Drift-Comments |
| 2026-05-26 | `cli/validators/drift.mjs` | ARCHITECTURE.md | Drift-Comments validator v0.15.1+ skips test files by default (matches TODO-Tracking's pattern). Opt in via `config.drift.includeTestFiles` if your project genuinely uses DRIFT markers in test code. | Info | By design — defensive default to prevent fixture false-positives |
| 2026-05-26 | `CHANGELOG.md` / `extensions/spec-kit-docguard/skills/*` | None | v0.12-v0.15 changelogs and release notes reference `// DRIFT:` in feature descriptions (e.g. K-3 .docguardignore, v0.13 sync, v0.14 P3 scoping). Documentation prose only, not actionable drift. | Info | By design — release notes |
| 2026-07-03 | `templates/commands/*`, `CHANGELOG.md`, `docs/ai-integration.md` | None | v0.29 batch audit: the DRIFT mentions in recently-committed files are the known by-design classes above (template instruction text, validator docstrings, changelog prose, and the new AI-integration guide's workflow step 6 teaching the drift protocol). No new code deviations from canonical docs were introduced by the findings migration, generate split, or integration-surface work. | Info | Audited — no actionable drift |
| 2026-07-03 | post-v0.29 batch (`cli/scanners/speckit.mjs`, `tests/speckit-phantom.test.mjs`, `packaging/*`) | None | Post-release batch audit (phantom detection, instruction audit, trace --features, distribution files): DRIFT mentions are validator/test/doc prose of the by-design classes above. No new code deviations. | Info | Audited — no actionable drift |


---

## CHANGELOG.md
> Version history and release notes

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Koa mounts, Cloudflare Pages Functions and `@hono/zod-openapi` routes
  are read** (`specs/064-js-route-shapes`, part of #519). Three JavaScript
  route shapes spec 058 left out:
  - Koa: routers move to the router reader, so `new Router({ prefix })`,
    `router.prefix()`, nested `router.use('/users', users.routes())` and
    koa-mount `mount('/admin', admin)` compose across files (also through
    `require`/`module.exports`); `router.del()` and named routes are read,
    a `use()` path guards everything under it, and routes now carry their
    tier. Every literal-path route the old scanner found is still reported.
  - Cloudflare Pages: a Wrangler file with `pages_build_output_dir`, or a
    `functions/` tree exporting `onRequest` handlers, is labelled Cloudflare
    Pages (a Wrangler file without that key stays a Worker). Each
    `onRequest*` export is a route at the path its file serves (`[id]` as
    `:id`, `[[path]]` as `:path*`, `index` for its directory), and a
    `_middleware` file that checks credentials marks the routes below it as
    auth (`cli/scanners/pages-routes.mjs`).
  - `@hono/zod-openapi`: `app.openapi(createRoute({ method, path }), h)`,
    with the definition inline, in a variable or imported from another file,
    is read under `basePath()` and `route()` mounts, `{id}` as `:id`; `doc()`
    serves `GET` at its path.
  - Pages advanced mode: a `_worker.js` in the output directory (or a
    `_worker.ts` source) is read as a Worker `fetch` entry and `functions/`
    is ignored, as Pages ignores it; `_routes.json` include/exclude rules
    drop the Functions requests never reach.
  - koa-router `redirect('/from', to)` is a route, `ALL /from`.
  - Express and Fastify: a route path that is not literal (a template with a
    runtime value, an imported constant, a regular expression) is no longer
    reported as `/x/:param`; it is recorded and makes the API-surface check
    `partial`. Literal routes are unchanged.
  - Express mounts: a `use()` prefix built from same-file or imported
    `const` strings is resolved; one that cannot be read (`config.apiPrefix`,
    `` `${process.env.BASE}/v1` ``) no longer leaves the mounted router's
    routes at the root. They are omitted and the prefix is recorded.
  - Imported constants: Express, Fastify, Koa, Hono and itty-router route
    paths and prefixes built from `const` strings another project file
    exports resolve, through `import` (named or namespace) and CommonJS
    (`const { PREFIX } = require('./config')`, `cfg.PREFIX` from
    `module.exports = { PREFIX: '/api' }` or `exports.PREFIX = '/api'`).

  A prefix, mount path, route definition or Pages export that is not literal
  is recorded and makes the API-surface check `partial`; the routes below an
  unresolved Koa, Hono or itty-router prefix are no longer reported at the
  root.
- **Routes for Sinatra, Starlette, Warp, Tide and Go `net/http`-only
  services** (`specs/060-more-route-frameworks`, part of #519). These
  frameworks were classified but no scanner read them, so `generate`, `diff`
  and guard's API-surface check saw no endpoint. Now:
  - Sinatra: classic and modular apps across files, nested `namespace`
    prefixes, `register`ed extensions, and `config.ru` `map`/`Rack::URLMap`
    mounts (`cli/scanners/sinatra-routes.mjs`).
  - Starlette: `Route`, `Mount`, `Router` and `Starlette(routes=...)` lists,
    `add_route` and `@app.route`, in the same graph as FastAPI and Flask, at the
    Python parser tier (`cli/scanners/python-routes.mjs`).
  - Warp: filter chains evaluated through `and`/`or`, `let` bindings and
    functions in other files; Tide: `at()` trees and `nest()` mounts
    (`cli/scanners/rust-routes.mjs`).
  - Go: a module with no router dependency that serves HTTP with `net/http` is
    classified `net/http`, so its routes are read.

  A route whose path is not literal text is omitted, never guessed, and the
  API-surface coverage reason says how many were omitted.
- **Laravel, Symfony and ASP.NET Core routes are read**
  (`specs/059-php-dotnet-routes`, part of #519). These frameworks were
  classified but no scanner read their routes, so guard's API-surface check,
  `diff` and `generate` saw no endpoints. Two new pattern readers
  (`php-routes.mjs`, `dotnet-routes.mjs`) now report:
  - Laravel routes from the files the app loads (`routes/web.php`, and
    `routes/api.php` under `/api`, or what `bootstrap/app.php` or
    `RouteServiceProvider` maps), with `prefix`/`middleware`/`group` chains
    and `resource`, `apiResource` and `singleton` expanded; `auth` and `can:`
    middleware count as auth evidence;
  - Symfony `#[Route]` attributes and `@Route` annotations under their class
    route, and `config/routes.yaml` routes and prefixed imports;
  - ASP.NET Core attribute-routed controllers (with `[controller]`,
    `[action]` and `[area]` tokens and `[Authorize]`/`[AllowAnonymous]`) and
    minimal APIs (`MapGet` and the other `Map*` calls under `MapGroup`
    prefixes, including groups passed to extension methods).

  A route whose path is not literal text is omitted, never guessed, and the
  API-surface check now reports how many were omitted and where. A `.csproj`
  using the `Microsoft.NET.Sdk.Web` SDK (every `dotnet new webapi` project)
  is now labelled ASP.NET Core; before, the check tested the wrong string and
  never matched.
- **Routes for NestJS and Cloudflare Workers** (`specs/058-js-framework-routes`,
  part of #519). Both were detected but their routes were not read, so the
  API-surface check, `diff` and `generate` saw no endpoints.
  - NestJS: `@Controller` paths joined with `@Get`, `@Post` and the other
    method decorators, under `setGlobalPrefix` (with `exclude`), URI versioning
    (`@Version`, `defaultVersion`, `VERSION_NEUTRAL`) and `RouterModule`
    paths. `@UseGuards` with an auth guard, a global auth guard and
    `@Public()`-style opt-outs set each route's auth; `ThrottlerGuard` is not
    auth. A NestJS project that also lists `express` is now labelled NestJS.
  - Cloudflare Workers: the paths a Worker's `fetch` entry tests
    (`url.pathname === '/x'`, `switch`, tables keyed by pathname, `URLPattern`),
    with methods from the `request.method` checks around them. A Durable
    Object's `fetch` is not public and is not read.
  - itty-router (with `base`) and Hono on Workers. Hono now applies
    `basePath()` and `app.route('/prefix', subApp)` across files, with or
    without Wrangler, and reads routers with any name.

  These routes carry the tier they were read at (`js-ast`, or
  `regex-fallback` for a file that does not parse). A path DocGuard cannot
  read statically, such as an imported constant or a `startsWith` dispatch, is
  left out, and the API-surface check reports `partial` with the count and the
  first location.

- **Suppressions that say why, and until when** (`specs/057-ignore-reasons`,
  #420). A `.docguardignore` line can now end with an expiry and a reason:
  `src/legacy/**  expires:2026-12-31  # removed with the v2 API`. A `#` starts
  the reason only after whitespace, so `C#/` is still a pattern, and a line
  without the new parts reads exactly as before; a line such as
  `fixtures/ # test data`, which used to match nothing, now excludes
  `fixtures/`. An entry applies through its expiry day (UTC) and then excludes
  nothing. A new Suppressions validator reports an expired entry (IGN001), an
  entry whose expiry is not a calendar date, which also excludes nothing
  (IGN003), and, with `"suppressions": { "requireReason": true }`, an entry
  without a reason (IGN002). `docguard ignore` lists every suppression the
  project has (ignore file, `.docguard.json` arrays, inline `docguard:ignore`
  pragmas with the reason written after the codes, validator and section N/A
  markers, baseline fingerprints) with its reason, expiry and state, and
  writes nothing. `docguard ignore --format claimspec` prints a claimspec v1
  `ignore` document holding only the `.docguardignore` entries whose reason
  (8 to 2000 characters), expiry and scope survive exactly; every other
  suppression is listed or counted on stderr with the reason it was left out,
  and no reason is invented. The claimspec `ignore` and `common` schemas are
  vendored from raccioly/testguard and every document is validated before it
  is printed.
- **The symbol map's benchmark has run, and it stays opt-in**
  (`specs/036-symbol-map`). Agent-context protocol v2 could not run: its pinned
  Codex CLI is no longer installed, and its model is refused for
  ChatGPT-account Codex. Protocol v3 (`benchmarks/agent-context/manifest-v3.json`)
  changes only that executable pin, to `codex-cli 0.162.0-alpha.17.2` and
  `gpt-6.1-sol` at low reasoning. It was frozen and committed before the first
  trial, and the runner rejects a v3 manifest that differs from v2 anywhere
  else (`--protocol v3`). All 54 trials succeeded. With the map, median
  uncached input rose 1.0% and median steps did not change. On the
  navigation-bound tasks, median latency fell 6.0%, short of the 15% threshold.
  The frozen rule therefore computed `opt-in`: `memory --pack --symbols` stays
  available, and the default pack is unchanged
  (`benchmarks/agent-context/results/README-v3.md`).

- **A documentation site with the guides a larger project ships**
  (`specs/053-user-documentation`). The GitHub Pages site
  (raccioly.github.io/docguard) now has a landing page, sidebar navigation and
  search, using the Just the Docs theme that Pages builds itself, with no
  build step or dependency. New pages:
  - guides for existing projects, Spec Kit, CI, monorepos, upgrading and
    troubleshooting;
  - references for languages and frameworks, coding agents, the MCP server and
    every finding code;
  - a fuller installation page.

  The finding-code reference is generated from the code registry
  (`npm run docs:reference`), and a test fails when it is stale. Existing page
  paths are unchanged. The Spanish and Portuguese READMEs link the English
  guides.

### Removed

- **`templates/commands/`**, an old copy of the slash commands that nothing
  installed (the installer reads `commands/`). Its `/docguard.init` and
  `/docguard.update` were listed in the README although no agent ever received
  them. The README, `ai-integration.md`, `quickstart.md` and CLI help now match
  what DocGuard installs and does (`specs/053-user-documentation` FR-009).

### Fixed

- **Coverage follow-ups from specs 056–061** (`specs/063-coverage-followups`).
  - `generate --plan` called pattern-read output complete. Endpoints and
    entities read in a language with no parser (Go, Java, Kotlin, Ruby, Rust,
    PHP, C#) reported `degraded: false` with no partial marking, and a project
    mixing JavaScript with Go was reported `degraded` and told to install
    `python3`. `surface.parserTiers` now reports `patternRead` and
    `patternLanguages` per surface and for the plan, beside `degraded`, which
    keeps its meaning (a file whose language has a parser read by the pattern
    fallback). Pattern-read sections are marked partial with a note naming the
    languages, and confidence is unchanged. Go, Rust, JPA and Rails entities
    now carry their file's tier. `generate --spec` likewise lowers confidence
    only for the pattern fallback, not for a `mixed` tier.
  - .NET build output was scanned as source. `obj/` and `_build/` (Mix, dune,
    Sphinx) are skipped everywhere, and `bin/` is skipped when the directory
    holding it has a `*.csproj`, `*.fsproj` or `*.vbproj`; a Node CLI's
    `bin/cli.js` is still read.
  - ASP.NET Core routes compared case-sensitively, so `[controller]` routes
    (`/api/Users`) documented as `/api/users` showed a false gap in guard and
    `diff`. ASP.NET Core routes now carry `caseInsensitive: true` and compare
    ignoring case; Spring, Laravel and every other framework compare exactly.
  - Express and Koa routers match ignoring case by default, but their routes
    compared exactly. Each route is now marked from its own router's options
    (`express.Router({ caseSensitive })`, Koa `new Router({ sensitive })`),
    the Express app's `case sensitive routing` setting for app routes and the
    mount prefixes the app matches, Fastify's `caseSensitive: false`, and
    NestJS's adapter (`cli/scanners/route-case.mjs`). A setting whose value is
    not a literal leaves its routes compared exactly, and `diff` names it.
  - Python route paths, prefixes and includes the reader could not resolve
    never lowered coverage. They now make the API-surface check `partial`
    with the count, the framework and the first location, as NestJS and
    Worker routes already did.
  - Angular and Svelte were in the tech-stack vocabulary with no dependency
    mapping, so a Node project documenting them always got DDF001
    "documented but not found". `@angular/core`, `svelte` and `@sveltejs/kit`
    now map to them, and the official `mongodb`, `mysql` and `postgres`
    clients to MongoDB, MySQL and PostgreSQL.
  - `diff` read `docs-canonical/API-REFERENCE.md`, `ARCHITECTURE.md`,
    `DATA-MODEL.md` and `ENVIRONMENT.md` by fixed path. It now resolves each
    with `docs.roles` and nested canonical documents, as guard does.
  - The pattern-fallback note of `generate --plan` and `generate --spec` said
    "Install python3 (or fix the parse error)" even for a TypeScript file that
    did not parse. It now names only what the affected files lack: `python3` on
    `PATH`, a Python or JavaScript/TypeScript syntax fix, or a reinstall so
    `@babel/parser` loads.

- **The non-regression budget tests no longer decide verdicts on wall-clock
  timing** (`specs/062-budget-deterministic`). "ten base-vs-base comparisons
  never fail" in `tests/budget.test.mjs` measured one sample per tree on a
  60 ms stub, so a single process delayed past the 150 ms floor failed it; it
  failed once in a full local run under CPU load. The tool was not at fault:
  the CI job takes the median of five interleaved samples, which the test had
  bypassed. `tools/budget.mjs` now exposes its interleaved sampling loop as
  `guardPair`, with the timing source injectable (`measurePair` and the CLI
  are unchanged). The tests script the samples with a seeded generator: ten
  base-vs-base comparisons with load bursts on two of five head samples pass,
  the same with a 30% slower head fail, and a one-sample comparison shows why
  a single sample is not a measurement. One test still spawns real stub trees
  and asserts only what load cannot break. The file runs in about 3 s
  instead of 15 s. `docs-canonical/CI-RECIPES.md` Recipe 3e states which
  noise the guard-time verdict absorbs.

- **False positives found by dogfooding, round 2** (`specs/061-dogfood-round-2`).
  - DDF002 read documented tests only when they were named `*.test.*` or
    `*.spec.*`, while the code side also used `testPattern`. A TEST-SPEC
    listing `test_bot.mjs` under `"testPattern": "test_*.mjs"` never matched.
    Both sides, and `diff`, now use the configured patterns; a bare name is
    compared with the pattern's last segment (#525).
  - DDF001 found technologies only in manifests, so DynamoDB called through
    a hand-signed HTTP client and Docker run from `start.sh` were always
    "documented but not found in code". It now also reads unambiguous source
    text in product files: a DynamoDB API target, endpoint or `DDB_ENDPOINT`
    setting, the DynamoDB Local image, `AWS.DynamoDB`, boto3's
    `client('dynamodb')`, the `@aws-sdk/lib-dynamodb` dependency; and
    `docker run`, `docker build` or `docker compose` in a shell script,
    Makefile or CI workflow, or a compose file anywhere. A technology found
    this way names its file in the DDF001 message, and `diff` names the file
    behind every technology (`evidence` in JSON). Tests, fixtures and
    commented lines are not evidence. Suppressing a document-level finding
    with a reason, also asked for in #526, is spec 057's.
  - DDF001 now names TypeORM and Sequelize, as it names Prisma and Drizzle.
    Mongoose still counts as MongoDB.
  - DDF001 ran only when a `package.json` existed, so Python, Go, Ruby, Rust,
    PHP, C# and Java projects had no tech-stack check. It now reads every
    manifest `generate` reads. A documented technology is reported missing
    only where a manifest could have shown it, so a Python project naming
    React or DynamoDB (through boto3) is not told they are absent;
    `docs/reference/languages.md` lists what each ecosystem proves. Node
    projects get the same results. Technology detection moved to
    `cli/scanners/tech-stack.mjs`.
  - `diff` read `docs-canonical/TEST-SPEC.md` even when `docs.roles.testSpec`
    pointed elsewhere. It now reads the mapped document, like DDF002.
  - SEC001 reported translation-catalog entries such as
    `f_password: 'Password'` as hardcoded passwords. A capitalised word for
    "password" in a common language, a short phrase containing one
    (`Confirm Password`), or the key's own words is now skipped, unless the
    key is an all-caps constant such as `DB_PASSWORD`. `password: 'password'`
    and other credentials are still reported (#527).
- **Ignore patterns read gitignore syntax correctly** (`specs/057-ignore-reasons`
  FR-010–FR-012, #420). The matcher behind `.docguardignore`, `ignore`,
  `securityIgnore`, `todoIgnore` and `documentLifecycle` passed `?`, `[`, `(`,
  `+`, `|` and the other regular-expression characters straight into a regular
  expression: `foo?` matched `fo`, `a|b` excluded unrelated files (176 in this
  repository), and an unclosed `[` made five validators fail with a
  regular-expression error. It now reads `?` as one character, `[…]` as a
  gitignore bracket expression (`[!ab]` negates), `\x` as a literal `x` and
  every other character literally. `**/fixtures/**` now also matches a
  top-level `fixtures/`, `a/**/b` matches `a/b`, and a leading `/` anchors to
  the project root instead of matching nothing. A pattern without those
  characters and without `**/` matches exactly what it did before (checked
  against every tracked file for this repository's patterns). `docguard ignore
  --format claimspec` now exports the reasoned entries that read the same in
  both syntaxes. The `documentLifecycle` array is now in the config schema and
  the configuration guide.
- **TypeORM and Sequelize models reach `generate`, `generate --plan` and
  `diff`** (`specs/056-typeorm-models`, #520). Only guard's schema check read
  them, by a pattern that missed common layouts, so a TypeORM or Sequelize
  project got an empty entity table and ER diagram. Both ORMs now go through
  the discovery Prisma, Drizzle and Mongoose use, so guard, `generate`, the
  plan, as-built specs and `diff` read the same models:
  - TypeORM: `@Entity` classes in any file that imports `typeorm`
    (`src/entity/User.ts` from `typeorm init` and NestJS's
    `src/users/user.entity.ts` included), named by class with the table name
    kept. Columns carry their type, nullability, primary key, `unique`,
    default and comment; columns declared on a base class are inherited; each
    relation is drawn once, from its owning side.
  - Sequelize: `sequelize.define('Name', …)` and `Model.init(…, { sequelize })`
    models with their declared attributes. Guard used to miss every `init`
    model. `belongsTo`, `hasMany`, `hasOne` and `belongsToMany` associations
    are drawn once.
  - An entity whose decorators come through a project barrel that
    re-exports `typeorm` (relative, tsconfig path alias or workspace package)
    is read; one whose barrel re-exports MikroORM is not.
  - A file that does not parse is read by pattern into the same entities,
    which then report the `regex-fallback` tier. Drizzle and Mongoose entities
    now report their tier too, so a pattern-read schema marks the plan's
    entity surface partial.
  - Zod schemas are the data model only when no ORM entity is found; a
    Mongoose project with Zod schemas used to report both. A Zod-only project
    listed each schema twice; it now lists each once.
  - SCH001's help names every model source guard reads.

  Guard now names a TypeORM entity by its class and accepts the class or the
  table name in DATA-MODEL.md. Knex is still read by guard only, by table name.
- **The watch reliability tests no longer depend on wall-clock timing**
  (`specs/055-watch-deterministic`, #523). `tests/watch-reliability.test.mjs`
  slept 300–600 ms around the 500 ms debounce and let the watcher print to
  stdout, which the test runner also reads its own messages from; it failed
  intermittently in full local runs on a loaded machine. The tests now drive
  the debounce with a fake clock through two new optional `runWatch` runtime
  hooks (`setTimeout`, `clearTimeout`, defaulting to the global timers),
  observe check completion by draining queued promise callbacks, and capture
  console output. They assert the coalescing at the exact window edge, and a
  new case covers a save that lands while a queued check's window is still
  open. The file runs in about 0.1 s instead of 4 s. `docguard watch` itself
  is unchanged.

- **Defects found while documenting DocGuard** (`specs/054-docs-verification-findings`).
  Running every documented flow on scratch projects turned up:
  - A Koa project was labelled Hono. It is now labelled Koa. Its routes are
    read as before, by the same method-shorthand scanner, and carry
    `source: koa`.
  - Agent files written by `init --with agents` carried no sync marker, so
    `agents --sync` skipped every one of them. They are now marked. Files
    written before this release still need one `agents --sync --force`.
  - `agents --sync` re-ran the whole `init`, Spec Kit step included. It now
    runs only the agent-file sync, like `agents --check`.
  - The config schema rejected an array `sourceRoot`, which the CLI accepts.
  - PSR002 reported a file excluded by `.docguardignore` as missing. Ignoring a
    file keeps it out of validation; pointers to it still resolve.
  - Pinned examples (the README's pre-commit `rev:`, `.pre-commit-hooks.yaml`,
    the GitLab component) said 0.29.0. They now say the current release, and
    the release sync keeps them current.
  - The MCP server's Origin check comment said it applied on loopback binds
    only. It applies on every bind.

- **Adding a test file no longer brings back baselined TRC004 findings**
  (`specs/052-baseline-stable-fingerprint`, #478). A baseline fingerprint
  hashed the whole message, including TRC004's ` — IR soft-match: …` hint,
  which names whichever test file currently matches best. A new, unrelated
  test could change the hint, so frozen findings came back as new and
  `--fail-on-warning` blocked the commit. Fingerprints now leave out advisory
  tails, declared per finding code; TRC004's hint is the only one. Every other
  fingerprint is unchanged. An existing `.docguard.baseline.json` keeps working:
  guard also accepts the old fingerprint, and `guard --update-baseline` rewrites
  it in the new form. Guard tries the exact fingerprint first: findings that
  differ only in digits and in the hint share one stable fingerprint, and trying
  that first let a hinted finding spend another finding's entry (found
  dogfooding a real repository before release, where 3 of 163 frozen findings
  came back).

## [0.43.1] - 2026-10-05

Automated weekly release — batches everything merged since `v0.43.0`.

### Commits

- test: add missing tests for cli command exports (#516)
- docs: link the README's brief to its rendered GitHub Pages copy (#515)
- chore(deps): bump @babel/parser (#513)

## [0.43.0] - 2026-10-01

**Upgrading from 0.42.x.** No new check fails a build: every new finding is a
warning or an escalation, so the worst case is `guard` moving from PASS to WARN
(exit 2). That still fails CI that runs with `--fail-on-warning` or treats any
non-zero exit as failure. Most likely triggers:

- **STR004:** an `AGENTS.md` instruction chain over 32 KiB. Codex stops reading
  there.
- **TDO002:** a comment that starts with a bare `TODO` or `FIXME`.
- **SPR006/SPR007:** Spec Kit projects whose checked tasks carry no
  `@implements` annotation, and as-built specs that drifted.
- **PSR001–PSR004:** agent instruction files (including a root `AGENTS.md`)
  that point at missing paths, or whose path scopes match nothing or cannot
  be read.
- **ENV003 on Go, Java, Kotlin, Ruby, Rust, PHP and C# projects:** env reads in
  those languages (and Spring `${X}` placeholders) are now found, so an
  undocumented variable is reported where it was invisible before.
- **SCH002 and ENV findings on Python projects:** SQLAlchemy models and
  pydantic `BaseSettings` fields are now read, so ones the docs do not
  mention yet are reported.


<!-- truncated: 5085 more lines — read CHANGELOG.md directly -->

---

## ROADMAP.md
> Planned features and development roadmap

# DocGuard Roadmap

<!-- docguard:last-reviewed 2026-10-10 -->

This file contains current product intent only. Released work belongs in
`CHANGELOG.md`; implementation history remains recoverable from Git. Completed or
superseded specifications leave the working tree through `docguard retire` so
people and AI agents do not mistake old plans for current requirements.

DocGuard's product goal is dependable, low-maintenance evidence that connects
approved intent, implementation facts, tests, and operational reality. A clean
structural score is useful, but it is not proof that arbitrary prose is true.

## Active roadmap

Specs 014–051 shipped in v0.43.0 (2026-10-01). Spec 052 (baseline fingerprints)
and spec 053 (the documentation site) wait for the next release; the
`[Unreleased]` section of `CHANGELOG.md` lists them, and each spec's `tasks.md`
is its checklist. One item remains open:

- **Native suppressions for claimspec (issue #420).** Whether DocGuard adopts
  the claimspec ignore format depends on reason-bearing suppressions; the
  decision is open.

New work starts with a reviewed specification and measurable acceptance
criteria rather than an evergreen task list that can become stale.

## Delivered roadmap

R1–R9 are implemented, evidence-reviewed, and released. The maintained living
specifications remain current verification contracts; historical implementation
plans are recoverable from Git and do not create a second source of truth.

### R9 — Adoption workflow integrity (released in v0.41.0)

Governing spec: `specs/012-adoption-workflow-integrity/spec.md`.

The installed journey now covers existing repository state, rendered
remediation, post-action verification, and the packed documentation surface.
Incomplete diff coverage, lifecycle evidence, and path resolution fail closed.
Exact finding-code policy preserves intrinsic detector severity, and structural
maturity is reported separately from the combined readiness verdict.

- [x] Merge the reviewed implementation in PR #395 at `f925e40`.
- [x] Pass the complete 1,864-test release suite, frozen precision benchmark,
  packed-package workflow, self-guard, and read-only web/Python adopter replays.
- [x] Pass the approved CI run `34989354122` and supply-chain run `34989355584`
  for release PR #396.
- [x] Merge and tag v0.41.0 at `7ec6356`.
- [x] Complete publication run `34989969217` for npm, PyPI, GHCR, GitHub
  Release, the Spec Kit extension ZIP, MCPB, and catalog sync.
- [x] Re-run the expanded 1,866-test suite while finalizing the living lifecycle
  evidence, with no failures or accepted deviations.

This milestone intentionally has no `plan.md` or `tasks.md`. Its living spec and
qualified implementation/test links are the maintained verification contract;
the release history is retained in Git and `CHANGELOG.md` without leaving a
one-time plan in active AI context.

### R1 — Document lifecycle foundation (released in v0.37.0)

Give specifications and planning documents an explicit end of life.

- [x] Ship `docguard retire --plan|--check` and explicit, fail-closed writes.
- [x] Keep archived content in Git and record only recovery metadata in
  `.docguard-archive.json`; do not copy obsolete prose into a second document tree.
- [x] Retire DocGuard's own completed specs, migration plans, and historical
  audits after their current outcomes are represented in canonical docs and the
  changelog.
- [x] Add lifecycle status validation for `active`, `completed`, `superseded`,
  and `archived`; task completion and `Completed` artifact maturity remain
  review signals rather than proof of retirement.
- [x] Add `.docguard-specs.json`, a committed lifecycle control plane. Reviewed
  approval, delivery, context, storage, persistence policy, lineage, and scope
  are authoritative; requirement references and
  implementation/test evidence are deterministic projections. Approved prose
  remains the source of behavioral intent.
- [x] Add a dedicated `docguard specs` command family with deterministic
  `--write|--check`, advisory request briefing, and generated-spec preflight.
  Generic retirement refuses active registered specs so it cannot bypass the
  lifecycle control plane.
- [x] Add a pre-specification briefing and a generated-spec gate so the actual
  draft is checked against active and prior requirements plus current code before
  planning starts. The briefing informs; only the reviewable draft can be gated.
- [x] Connect the Spec Kit extension's mandatory `before_specify` and
  `before_tasks` hooks to the same deterministic briefing and generated-spec
  gate. Keep `docguard specs --check` as the CI enforcement surface because
  hooks are agent-dispatched workflow automation.
- [x] Give every active spec an immutable metadata ID; use
  `specId#requirementId` for completion evidence and preserve retired identities
  as registry tombstones so bare IDs cannot rebind.
- [x] Cross-check spec storage state against `.docguard-archive.json`. The
  archive manifest owns document recovery; the spec registry owns governance,
  and disagreement between them blocks a transition.
- [x] Merge the reviewed implementation in PR #349 and publish the verified
  npm, PyPI, GHCR, MCPB, and Spec Kit extension artifacts as v0.37.0.

R1 deliberately shipped the registry and safe-retirement boundary before adding
completion writes. Transaction rollback, status adapters, monorepo identity,
restore/re-retire handling, and reviewed completion transitions followed in R2
and shipped in v0.38.0; they are outside the v0.37.0 contract.

### R2 — Completion and post-hoc reconciliation (released in v0.38.0)

Close the lifecycle loop without allowing current code to silently redefine
approved intent.

- [x] Add staged registry/recovery transactions with rollback before any command
  can update both lifecycle ledgers.
- [x] Add an `implemented → verified` completion transaction that appends a
  bounded outcome record, refreshes mechanical facts, records the exact
  reconciliation revision, and regenerates active AI context.
- [x] Add optional Spec Kit hooks that check archive readiness after convergence
CLI and CI remain the enforcement boundary for lifecycle verification.

Post-hoc implementation changes are classified without silently redefining
approved intent. `docguard reconcile --since <ref>` reports affected material:

1. mechanical code facts that `sync` can safely refresh;
2. approved requirements that may indicate a code regression;
3. superseded decisions that need a replacement or archive action;
4. unsupported or ambiguous evidence that needs human review.

The command produces a review plan before any write. It never rewrites a
requirement merely because the current code differs. Acceptance requires seeded
examples for intentional behavior changes, accidental regressions, and unrelated
edits; each class must remain distinguishable in JSON output.

Spec Kit already publishes persistence models and supports lifecycle hooks, while
community Archive and Reconcile extensions perform agent-authored artifact
updates. DocGuard will validate and index those outcomes rather than duplicate
their prompt workflows. A future upstream contribution should standardize only
the generic lifecycle metadata or hook contract after interoperability is proven.

Delivered slices include explicit changed-file-to-spec evidence edges,
replacement-spec fields in bounded outcomes, decision-record classification,
transaction rollback fixtures, and `after_implement`/`after_converge` evidence
gates. Broader symbol inference remains intentionally unsupported until R3 can
measure its false-positive cost.

### R3 — Independent precision benchmark (released in v0.39.0; living baseline)

Governing spec: `specs/007-precision-evidence-loop/spec.md`.

The reproducible corpus now covers JavaScript, TypeScript, Python,
fallback-language, monorepo, generated-code, and sparse-doc shapes. Five pinned
public projects supplement repository-owned fixtures. Labels and exact mutations
are fixed before output review, and the evaluation split is isolated by
repository and causal family.

The reviewed baseline reports case-level and grouped TP/FP/FN, precision, recall,
false positives per repository, abstention, unsupported coverage, cold/warm
runtime, repair outcomes, and Wilson confidence limits. Comparisons fail on a
new miss, false positive, removed case, or supported-case abstention even when
the total warning count falls.

Maintained artifacts: `benchmarks/corpus.json`, `benchmarks/baseline.json`, the
runner, comparison, and baseline-envelope libraries,
`schemas/docguard-benchmark.schema.json`, and
`schemas/docguard-benchmark-baseline.schema.json`. The baseline states what its
ratios measure (`benchmark-precision`) and carries a derived caveat; it is not,
and will not become, a calibration document — a balanced corpus has no base
rate to calibrate against. Per-code evidence is projected from that baseline
into a shipped module and quoted beside findings, where the dominant and honest
answer is that a code has never been benchmarked.

### R4 — Contribution-to-regression loop (released in v0.39.0; open to contributions)

Governing spec: `specs/007-precision-evidence-loop/spec.md`.

`feedback` accepts redaction-attested synthetic fixture manifests with detector,
configuration, expected identity, parser tier, explicit predicate, and opposite
control. False positives, false negatives, unsupported syntax, ambiguity, and
policy disagreements retain distinct classifications.

Public payloads remain opt-in and use reviewed synthetic content. Preview exposes
a deterministic duplicate identity and open/closed searches without submission.
The reducer preserves explicit interestingness, and test-only generation enforces
reproduction, neighboring control, scope, redaction, and benchmark-delta evidence.

Maintained artifacts: `templates/feedback-fixture.json`,
`schemas/docguard-feedback-fixture.schema.json`, and the generated direct
`tests/*.test.mjs` contribution path.

### R5 — Evidence-scoped verification (released in v0.40.0)

Governing spec: `specs/008-evidence-scoped-verification/spec.md`.

Replace broad age-based review prompts with declared source-to-document
dependencies where available. Start with bounded claim types such as named JSON
values, enum sets, and counts tied to documented collections. Results remain one
of verified-within-scope, contradicted, unsupported, inconclusive, or stale.

Contribution slices: dependency declarations, exact claim predicates, saved
oasdiff/Buf evidence adapters, and review invalidation fixtures. Upstream tools
retain ownership of their domain semantics; DocGuard links results to affected
prose, examples, requirements, and migration guidance.

The implementation exposes the same scoped result through CLI, guard, score,
agent context, SARIF/JUnit findings, and MCP. Verification passed 1,733 tests on
Node 18, 20, 22, and 24, package extraction without the optional parser, Draft
2020-12 schema validation, and the frozen 24-case public/synthetic corpus with
no baseline regression.

### R6 — Language and repository coverage (released in v0.40.0)

Governing spec: `specs/009-language-repository-coverage/spec.md`.

Add capabilities only with explicit applicability and controls. Priorities are
Python import relationships, additional Worker binding forms, custom document
role writers with section ownership, and repository-root guidance for monorepos.
Unsupported extraction must remain visible and must not become a success claim.

Contribution slices: one parser or framework per pull request, paired supported
and unsupported fixtures, and benchmark deltas for any performance-sensitive
scanner change.

The implementation now covers Python static import graphs, current Cloudflare
binding forms, ownership-safe mapped document writers, and advisory npm/pnpm
workspace-root discovery. Verification passed 1,765 tests on Node 18, 20, 22,
and 24, package extraction without the optional parser, schema and source syntax
checks, and the frozen corpus with 24 evaluable cases passing plus one expected
unsupported dynamic-Python case. Self-guard has no errors; its three DSP001
warnings are low-confidence review prompts for canonical documents already
updated in the same change set.

### R7 — Task-specific agent context (released in v0.40.0)

Governing spec: `specs/010-task-specific-agent-context/spec.md`.

Evaluate targeted evidence packets against ordinary repository context and the
existing DocGuard context pack. Freeze repository snapshots, model/harness
versions, prompts, and budgets; measure hidden-test success, requirement
violations, unnecessary edits, tokens, latency, and human intervention.

Ship only if repeated trials improve task outcomes or reduce cost within a
predeclared non-inferiority margin. An LLM judge or DocGuard score alone is not
sufficient evidence.

Protocol v1 freezes three synthetic tasks, three conditions, three repetitions,
the model and harness identity, hidden regression tests, safety checks, metrics,
and promotion threshold before any trial results. The experimental selector
must abstain on weak evidence and remains outside the public CLI until all gates
pass.

All 27 frozen trials passed their hidden requirements, visible regressions, and
changed-file policy. Against the existing context pack, targeted packets reduced
median tool steps from 10 to 5 and median latency by 17%, while increasing
median uncached input by 80%. The result clears the predeclared gate through
steps and latency, supports an explicit opt-in interface, and does not support a
general token-cost claim. The retained result and limitations live under
`benchmarks/agent-context/results/`.

The promoted CLI, selector, schemas, docs, and evaluator pass 1,784 tests on
Node 18, 20, 22, and 24. Packed-package tests run task context without the
optional parser, and the independent detector corpus remains regression-free
across 24 evaluable cases plus one explicit unsupported case.

### R8 — Tokenless scheduled releases (released in v0.40.3)

Governing spec: `specs/011-tokenless-scheduled-releases/spec.md`.

Replace the long-lived release PR credential with an ephemeral repository token
and one explicit maintainer approval for the generated PR's workflows. The
implementation validates the generated candidate before push, arms protected
native auto-merge, starts publication from the merged version push, and recovers
a missing tag before another version increment.

- [x] Freeze the security and recovery contract before implementation.
- [x] Add pure release-candidate and exact-run policy tests.
- [x] Publish without a stored personal or app credential while preserving the
  maintainer workflow approval required by protected pull-request checks.
- [x] Preserve Dependabot/Jules policy and pinned-action controls.
- [x] Record the reviewed lifecycle outcome at durable revision `46531e4` with
  no accepted deviations.
- [x] Publish v0.40.1 through release PR #380 and verify npm, PyPI, GHCR, GitHub
  release assets, and the Spec Kit catalog reminder.
- [x] Verify the corrected repository-token native auto-merge continuation on
  v0.40.3 and retain exact evidence.

The retained live probe used CI run `34912654565` and privileged gate run
`34912788971`. All Node 18, 20, 22, and 24 jobs passed. The trusted gate
identified pull request #372 as a non-release candidate and refused to merge it;
the temporary pull request and branch were then removed. The complete reviewed
evidence landed through pull requests #371, #373, and #374.

Release PR #380 passed CI run `34920481989` and supply-chain run `34920482409`,
merged at `5b2dfe4`, and published v0.40.1 in run `34920702346`. That run also
proved the final GitHub boundary: approving the held workflow does not emit a
second `workflow_run` completion. Release PR #383 proved the repository token
can arm native auto-merge and GitHub can merge after approved checks, while its
bot-originated push proved publication needs an explicit continuation.

Scheduled run `34922506777` created repository-token release PR #386 and armed
native auto-merge. After one maintainer approval, CI run `34922605581` and
supply-chain run `34922605917` passed; GitHub merged the exact head at
`e27d6bf0203708ee8206a1434eb292520f4c4494`. The bounded scheduler wait then
dispatched publication run `34922784629`, which passed the four-runtime matrix,
self-guard, npm, PyPI, GHCR, GitHub Release, extension ZIP, MCPB, and catalog
reminder jobs. The published npm and PyPI versions and GitHub tag all resolve to
v0.40.3, completing R8 without a stored release credential.

## Contribution standard

Before opening work, search existing open and closed issues and pull requests.
Each proposal should name the failure mode, include a minimal reproduction and a
valid control, state supported and unsupported scope, and define the acceptance
test. See `CONTRIBUTING.md` for repository mechanics.

## Deferred ideas

A hosted dashboard, leaderboards, and notification integrations remain deferred
until user research shows that the CLI, CI outputs, and existing observability
systems cannot meet a concrete team need. They are not active commitments.


---

## AGENTS.md
> AI agent behavior rules and workflow instructions

# AI Agent Instructions — DocGuard

<!-- docguard:last-reviewed 2026-10-10 -->

> This project follows **Canonical-Driven Development (CDD)**.
> Documentation is the source of truth. Read before coding.
> DocGuard is an official [GitHub Spec Kit](https://github.com/github/spec-kit) community extension.

## Workflow

1. **Read** `docs-canonical/` before suggesting changes
2. **Check** existing patterns in the codebase
3. **Run** `docguard diagnose` to see what needs fixing
4. **Confirm** your approach before writing code
5. **Implement** matching existing code style
6. **Log** any deviations in `DRIFT-LOG.md` with `// DRIFT: reason`
7. **Verify** with `docguard guard` — all checks must pass

## Project Stack

- **Language**: JavaScript (ES modules)
- **Runtime**: Node.js 18+
- **Dependencies**: One — `@babel/parser` (exact-pinned, optional-load); Node.js built-ins otherwise
- **Testing**: `node:test` (built-in)
- **Distribution**: npm + PyPI; the release also publishes the MCP server image to GHCR, the Claude Desktop MCPB bundle and the Homebrew tap
- **Version**: see `package.json` (single source of truth — do not hardcode here)

## Key Files

| File | Purpose |
|------|---------|
| `docs-canonical/ARCHITECTURE.md` | System design |
| `docs-canonical/DATA-MODEL.md` | Config, file formats and output contracts |
| `docs-canonical/SECURITY.md` | Auth & secrets |
| `docs-canonical/TEST-SPEC.md` | Test requirements |
| `docs-canonical/ENVIRONMENT.md` | Environment setup |
| `docs-canonical/REQUIREMENTS.md` | Spec-kit aligned requirements |
| `CHANGELOG.md` | Change tracking |
| `DRIFT-LOG.md` | Documented deviations |

## Commands

`docguard --help` is the authoritative list (counts intentionally not hardcoded
here — they drift). The surface, grouped as `--help` shows it:

**The Daily 5** — `init` (bootstrap + scan), `guard` (CI gate, all validators),
`diff` (doc↔code gaps; `--since <ref>` for changed-file impact), `sync` (refresh
code-truth sections), `score` (CDD maturity 0-100).

**Tools** — `demo` (zero-install tour), `diagnose` (guard → AI fix prompts),
`fix` (AI fix instructions; `--doc <name>`), `generate` (reverse-engineer docs;
`--plan`; `--spec <area>` for an as-built Spec Kit spec), `agent` (task graph; `--task <text>` for bounded current evidence),
`retire` (remove reviewed docs from active context),
`specs` (check/refresh/preflight/approve/complete the spec lifecycle registry; `require` for the spec-first gate; `reanchor` for a squash-discarded revision),
`reconcile` (classify code/spec changes since a Git ref without rewriting intent),
`review` (doc sections whose covered code changed; `--accept`, `--prune`, `--suggest`),
`rules` (which instruction files each agent harness loads for a path; `--for <path>`),
`explain` (explain a validator/warning), `verify` (declared evidence; `--semantic` claims), `feedback` (report false positives), `mcp` (MCP server; stdio or loopback HTTP; tool calls stay inside the served directory and each `--root`), `report` (compliance bundle), `ci` (guard + score gate), `memory` (what DocGuard
remembers; `--pack [--symbols]` for the context pack), `trace` (requirements traceability; `--reverse`; `--owners` for the doc ownership map), `upgrade` (check npm for a newer
release and migrate the config schema; `--apply` also refreshes out-of-date agent files and the registered Spec Kit extension; `--apply --schema-only` stays offline), `watch` (live re-guard).

**`init --with <name>`** scaffolders — `agents`, `hooks`, `ci`, `badge`, `llms`,
`publish` (also reachable as standalone deprecation aliases).

**Deprecation aliases** — `setup` → `init --wizard`; `audit` → `guard`
(permanent); `impact` → `diff --since`.

## Consuming Guard Output (agents)

Prefer the machine contract over parsing prose: `docguard guard --format json`
returns `status` (PASS/WARN/FAIL, matches exit code 0/2/1 — but FAIL exits `3`,
not `1`, when the project has no `.docguard.json`, so branch on `status` rather
than assuming `1`), `findings[]`
(`{code, severity, confidence, disposition, evidence, parserTier, message, location, suggestion}`),
`nextStep`, `reportable[]` (findings whose code is unmeasured or whose
confidence is low — verify before acting), `coverage`
(Markdown tier map incl. the `specKit` tier and `unclassified[]`), `evidence`, and `semanticClaims.count`
(documented numbers not yet verified against code).

When a command is launched from an implicitly selected nested package, inspect
stderr for repository-root guidance. Human output supplies an exact rerun;
machine modes emit a `docguard.repository-root-guidance` JSON diagnostic there
so stdout remains a valid JSON/SARIF/JUnit artifact. The current command still
checks only the selected package unless it is explicitly rerun with `--dir`.

- Every structured finding has a stable code (`STR001`, `ENV003`, `XRF002`, …).
  `docguard explain <CODE>` gives the contract and fix.
- Triage on `disposition`, not on severity alone. `act` means DocGuard asserts a
  defect and names the correction — apply it. `escalate` means DocGuard observed
  a signal and the judgement is yours; read the source before changing anything.
  A high-confidence finding can still be an escalation: FRS002 counts commits
  exactly and still does not establish that the document is stale.
- Read `evidence.status` before trusting a confidence label. `measured` quotes
  the reviewed corpus with `n` and a Wilson interval; `not-measured` means the
  label is a maintainer's prior that has never been checked against labelled
  cases. Most codes are unmeasured — that does not make their findings wrong,
  only unverified.
- Read `parserTier` when a finding concerns source code. `regex-fallback` or
  `fallback-language` means an AST was unavailable for that file, so absence of
  a finding is weak evidence; the owning validator also reports `partial`.
- When `evidence.configured` is true, inspect `docguard verify --evidence
  --format json` first. A `verified-within-scope` result covers only its selected
  statement. Contradictions can indicate code regressions from approved intent;
  stale reports must be regenerated from their declared inputs.
- Mechanical fixes go through `docguard fix --write` (provenance-checked,
  fail-closed) — never hand-apply what the tool fixes deterministically.
- Genuine false positives: suppress at the site with `// docguard:ignore <CODE>`
  (reason required) or `<!-- docguard:validator <key> n/a — reason -->`, and
  report them via `docguard feedback`.
- Doc≠code does not mean the doc is wrong — canonical docs are the spec. If the
  code regressed from a documented decision, fix the code or log a
  `// DRIFT: reason` + DRIFT-LOG.md entry instead of rewriting the doc.
- Treat `specs/` and planning docs as active intent only. Review candidates with
  `docguard retire --plan`; retire only explicit, clean tracked documents after
  their shipped outcomes are represented in current docs and `CHANGELOG.md`.
- Run `docguard specs preflight` before drafting and `docguard specs preflight
  --path <spec>` before planning. Refresh `.docguard-specs.json` with `specs
  --write` and record approval with `specs approve --id <id> --write`; never edit
  generated intent/evidence fields or treat bare requirement IDs as completion proof.
- Run `docguard reconcile --since <ref> --format json` before completing a
  changed feature. Only its mechanical write plan may run automatically;
  unsupported changes and possible regressions require review. `docguard specs
  complete` records that review and regenerates the active context projection.
- For a concrete implementation task, prefer `docguard agent --task <text>
  --format json`. Follow only selected current evidence, retain its
  retrieval-only assurance, inspect additional repository facts as needed, and
  fall back to normal discovery when `selection.status` is `abstained`.

## AI Skills

DocGuard provides enterprise-grade AI behavior protocols via the Spec Kit extension:

| Skill | Purpose |
|-------|---------|
| `docguard-guard` | 6-step quality gate with severity triage and structured reporting |
| `docguard-fix` | 7-step research workflow with validation loops (max 3 iterations) |
| `docguard-review` | Read-only semantic cross-document consistency analysis |
| `docguard-score` | CDD maturity assessment with ROI-based improvement roadmap |
| `docguard-sync` | Mechanical code-truth refresh with explicit prose review boundaries |

Skills are located at `extensions/spec-kit-docguard/skills/*/SKILL.md`. They tell agents **how to think**, not just what to run.

## Spec Kit Hooks

DocGuard integrates into the spec-kit workflow. `extensions/spec-kit-docguard/extension.yml`
is authoritative; a hook is mandatory when its `optional` field is `false`.

| Hook | Command | Required? |
|------|---------|-----------|
| `before_specify` | `speckit.docguard.brief` | Mandatory |
| `before_tasks` | `speckit.docguard.preflight` | Mandatory |
| `after_tasks` | `speckit.docguard.score` | Optional |
| `after_implement` | `speckit.docguard.guard`, then `speckit.docguard.complete` | Mandatory, then optional |
| `after_converge` | `speckit.docguard.complete` | Optional |

## Extension Structure

```
extensions/spec-kit-docguard/
├── skills/                    # AI behavior protocols (docguard-guard, -fix, -review, -score, -sync)
├── scripts/bash/              # Orchestration scripts (--json output)
├── commands/                  # Spec Kit slash commands
├── templates/                 # Hook registration templates
└── extension.yml              # Commands, scripts, hooks declaration
```

## Rules

- **Spec-first (Constitution X).** A change to `cli/**` runs the Spec Kit pipeline:
  `specs/###-slug/` spec → plan → tasks → implement. The PR description names the
  spec (`specs/015-spec-first-gate` or its Spec ID), or declares one line
  `Spec-Exempt: <release|deps|typo|test-only> — <reason>`. CI enforces this with
  `docguard specs require --since origin/main --message-file <body>`. When the area
  being changed has no spec, write an as-built spec scoped to that area first.
- **Changes must not make DocGuard slower, noisier or heavier by accident.** The
  CI `budget` job compares the PR with its base on one runner: guard wall time
  (×1.15, 150 ms floor), new error findings on this repository, agent-facing
  bytes (context pack, task packets, MCP responses, llms bundles; +10%), package
  weight and runtime dependencies. A cost the change needs is declared in the PR
  body as `Budget-Exempt: <metric> — <reason>`. Reproduce locally with
  `npm run budget -- --base <base-worktree>` (specs/031-non-regression-budgets).
- **PR-first workflow — no direct-to-main commits.** Create a branch (`git checkout -b <type>/<slug>`), push, `gh pr create`, let CI run, self-review, squash-merge. Tag releases only after merge on `main`. The only acceptable direct-to-main: typo fixes in comments or README badge URLs.
- Never commit without updating CHANGELOG.md
- Regenerate checked-in generated files in the same change; each has a drift test
  that fails and names its command: `npm run llms` after editing a canonical or
  optional doc (`llms.txt`, `llms-full.txt`), `npm run docs:reference` after
  changing a finding code or a validator's display name (the docs site's
  finding-code reference), and `npm run generate:precision-evidence` after
  a reviewed benchmark baseline change (`cli/precision-evidence-data.mjs`).
- If code deviates from docs, add `// DRIFT: reason`
- Security rules in SECURITY.md are mandatory
- Test requirements in TEST-SPEC.md must be met
- Run `docguard guard` before pushing — all checks must pass
- Standalone file writes use `safeWrite()` with backups before overwrite.
  Lifecycle operations spanning multiple files use `commitFileTransaction()` so
  preparation, rollback, and post-write validation cover the complete set.


## Agent Rules

### Automated agents / bots (Jules "Sentinel", "Bolt", "Palette", and any auto-PR agent)
- **Never open a duplicate PR.** Before opening ANY PR, search existing **open
  AND closed** PRs and issues for the same topic/title. If it exists, STOP — do
  not open another. (Dozens of duplicate command-injection and diff-optimization
  PRs were closed as noise.)
- **Do not re-open resolved work.** See `.jules/sentinel.md` (execSync/command
  injection — RESOLVED in v0.21.1 + #296) and `.jules/bolt.md` (diff/scan
  micro-optimizations — already applied; code refactored since). These are
  historical learnings, **not** standing mandates to re-scan every run.
- **Bar for a new PR:** a genuinely new, unaddressed finding, with evidence — a
  concrete exploit path / failing test (security) or a benchmark showing >20%
  real-workload improvement (performance). A Big-O note alone is insufficient.
- This repo has **no web UI and no VS Code extension** — skip all UX tasks.

### Dependencies
- Never add a package without first verifying it exists on the official registry (npm/PyPI).
- Always pin to exact versions in `package.json` and `requirements.txt`. No ^, ~, or >= ranges.
- Prefer packages with >10k weekly downloads and >1 maintainer.
- If you suggest a package, confirm its first-publish date is older than 30 days.
- Never modify .npmrc, pnpm-workspace.yaml, or dependabot.yml without explicit user confirmation.

### CI/CD
- Never write a workflow using `pull_request_target` with checkout of PR-controlled refs.
- Always pin third-party GitHub Actions to commit SHA, not @v1 or @main.

## Claim probing (TestGuard)

`testguard.claims.json` states what this codebase guarantees, as claims with
mechanical faults. `testguard probe` injects each fault and reports the ones the
suite fails to detect — a test that pins a *defect* stays green under coverage,
so this is the check that catches it.

This suite runs on `node --test`, which ships no `json` reporter, while TestGuard
reads Jest-shape JSON. `tools/node-test-json-reporter.mjs` translates between
them:

```bash
npx testguard-cli@0.15.0 probe --confirm 3 --serial \
  --node-modules "$PWD/node_modules" \
  --runner-cmd "node --test --test-reporter=$PWD/tools/node-test-json-reporter.mjs --test-reporter-destination={out} {files}"
```

Pass an absolute reporter path: the probe runs in a scratch worktree built from
a commit, so a relative path resolves there and untracked files do not exist.

`npx testguard-cli@0.15.0 gate --changed origin/main` reports changed source files
that carry no claim. It does not find bugs; it refuses to let a change land without stating
what must be true, which is the step that surfaces them. A file the probe cannot
observe (for example, one that only runs as a subprocess) is excused in
`testguard.ignore.json`, with a reason that names what protects it instead.

## Evidence and contributions

A structural score is a maturity proxy. Preserve `assurance` and nullable factual accuracy in automation; a clean guard does not establish arbitrary prose correctness. Review human intent separately from generated code facts. To challenge any finding, run `docguard feedback --code <CODE> --preview`, inspect the metadata-only public draft, and check the supplied search link for existing open and closed work. Contribute a synthetic failing example paired with a neighboring valid case. Submission remains opt-in.


---
Generated by DocGuard v0.43.1 | [docguard-cli](https://www.npmjs.com/package/docguard-cli)
