# 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.8.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-09-30 -->

| Metadata | Value |
|----------|-------|
| **Status** | ![Status](https://img.shields.io/badge/status-active-brightgreen) |
| **Version** | `1.8.0` |
| **Last Updated** | 2026-09-30 |
| **Project Size** | ~47K lines in 147 files across `cli/` — measured 2026-09-30 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 complete` is the only verified-delivery writer |
| **Reconciliation graph** | Inventories changed paths independently from bounded patch text, maps them to direct spec evidence 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`) 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 prefixes and includes are flagged, never guessed. 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 |
| **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 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 |
| **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 until the frozen v2 agent-context benchmark (`benchmarks/agent-context/manifest-v2.json`) decides; 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 |
| **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 |
| **JS/TS extraction** | 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) through one discovery (`scanOrmEntities`) that guard's schema check and `generate` 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. 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. Framework and ORM names are listed in `docs/commands.md` (`generate`) |
| **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 | `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) and `detectCodeTechnologies` in `cli/shared-source.mjs` |
| **Config** | Configuration loading, schema migration, validator policy, and exact finding-code policy | `cli/` | `config.mjs`, `shared.mjs` |
| **Shared** | Cross-cutting utilities — ignore/glob filters, 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` only), `npm`/`gh`/`git push` 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.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-09-30 -->
<!-- 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.

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`.

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`).

## 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.12.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-09-30 -->

| Metadata | Value |
|----------|-------|
| **Status** | ![Status](https://img.shields.io/badge/status-active-brightgreen) |
| **Version** | `0.12.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[]` | No | `["AGENTS.md", "CLAUDE.md"]` | AI agent config file options |
| `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` 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 |

### 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.

## 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. `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` |

`.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.

## Finding channels

<!-- docguard:section id=finding-channels source=human covers="cli/findings.mjs#mkFinding, cli/findings.mjs#PARSER_TIERS" -->
A finding answers three independent questions, one field each. A single `confidence` field had to serve all three, so a certain observation read as an uncertain one, and the feedback loop sampled only the findings its own label already doubted.

| Field | Question | Values |
|-------|----------|--------|
| `severity` / `effectiveSeverity` | Does CI block? | `error`, `warn`, `info` (effective only: from `findingSeverity`, validator `severity`, or a code registered as informational, such as `STR005`; `enforcement.source` says which: `finding`, `validator`, `code` or `intrinsic`) |
| `disposition` | Who decides — the tool or the reader? | `act`, `escalate` |
| `confidence` | How sure is the detector of its **observation**? | `high`, `low` |
| `evidence` | Has the reviewed corpus ever measured this code? | `{ status: 'measured' \| 'not-measured', … }` |
| `parserTier` | Which analyzer produced it? | `js-ast`, `py-ast`, `regex-fallback`, `fallback-language`, `mixed`, `not-applicable` |

`disposition` is `act` when DocGuard asserts a defect and names the correction, `escalate` when it reports a signal whose judgement belongs to the reader. A detector may set it explicitly; otherwise it derives from `suggestion.kind` (`fix`/`suppress` → `act`; `review`/`report` → `escalate`), and falls back to `escalate` when the suggestion is absent or malformed — a finding DocGuard can describe but can only describe is one a human should read. The three channels vary independently: FRS002 counts commits read directly from Git, so it is `confidence: high` and `disposition: escalate`. The count is a fact; the inference to staleness remains the reader's call.

`evidence` is projected per finding code from the reviewed baseline by the same checked projection that serves `precisionEvidence` (`docguard.precision-evidence-loop#FR-019`). A `measured` entry carries `n`, its Wilson 95% interval, and a point estimate once `n` meets the published floor; an unmeasured code reports `not-measured` and always stands alone, inheriting no sibling's number. The object is frozen and shared per code, so a large run allocates one evidence object per code rather than one per finding.

`reportable` is true when `evidence.status` is `not-measured` **or** `confidence` is `low`. Under the previous rule, which read confidence alone, the default feedback sample omitted the population where a wrong label costs most: a confident label on a code the corpus has yet to measure.

`location` is always a string (`path` or `path:line`) or `null`. A detector that supplies `{ file, line }` is normalized at construction. Under the previous contract such findings rendered as `[object Object]`, and the SARIF location parser dropped them, so six codes reached GitHub Code Scanning with no file annotation at all.
<!-- /docguard:section -->

## Fix Command Issue Format

The `fix --format json` output follows this structure:

| Field | Type | Description |
|-------|------|-------------|
| `status` | `string` | `"clean"` or `"issues-found"` |
| `project` | `string` | Project name |
| `projectType` | `string` | Detected project type |
| `issueCount` | `number` | Total issues found |
| `autoFixable` | `number` | Issues fixable by `--auto` |
| `issues[].type` | `string` | `"missing-file"`, `"empty-doc"`, `"partial-doc"`, `"missing-config"` |
| `issues[].severity` | `string` | `"error"`, `"warning"`, `"info"` |
| `issues[].file` | `string` | Affected file path |
| `issues[].autoFixable` | `boolean` | Can be auto-fixed |
| `issues[].fix.action` | `string` | `"create"`, `"rewrite"`, `"improve"` |
| `issues[].fix.ai_instruction` | `string` | AI-actionable fix instruction |

## Score Output Format

The `score --format json` output:

| Field | Type | Description |
|-------|------|-------------|
| `score` | `number` | CDD maturity score (0-100) |
| `grade` | `string` | Letter grade: `A+`, `A`, `B`, `C`, `D`, `F` |
| `categories` | `object` | Per-category score, weight, weighted contribution, and axis |
| `scoreKind` | `string` | `structural-maturity` |
| `assurance` | `object` | Factual accuracy remains unverified; extracted candidate count is heuristic |
| `memory` | `object` | Completeness and structural alignment proxies; accuracy is null |

---

## Revision History

| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.13.0 | 2026-09-30 | DocGuard Team | Guard result `key`/`parent` and the `docSections` key; Claude Code `@path` imports in instruction budgets; tracked-only `glob` fingerprints (spec 049) |
| 0.12.0 | 2026-09-30 | DocGuard Team | `.docguard/` ignores itself, and dirty checks leave it out (spec 042) |
| 0.11.0 | 2026-09-30 | DocGuard Team | Freshness review for specs 028–037: configuration keys for the doc lock, path-scoped rules, ownership, diagrams and the symbol map; `review --prune` as a doc-lock writer; the plan cache (version 3), `budgets.json` and the frozen agent-context protocol v2 |
| 0.10.0 | 2026-09-29 | DocGuard Team | Add registry `lifecycle.origin` / `scope.sourcePaths` for as-built specs, the `specKit` coverage tier, and the `agentInstructions` and `specFirst` configuration |
| 0.9.0 | 2026-09-15 | DocGuard Team | Add bounded field-level spec-registry differences and direct evidence verification exit semantics |
| 0.6.0 | 2026-09-14 | DocGuard Team | Add the document-retirement recovery manifest, retained-ref proof, and retired requirement tombstones |
| 0.4.0 | 2026-03-13 | DocGuard Team | Complete rewrite — documented all config formats, output schemas, metadata headers |
| 0.1.0 | 2026-03-13 | DocGuard Generate | Auto-generated skeleton |

## Score assurance contract

The numeric CDD score estimates structural maturity. Factual accuracy and regulatory assurance require separate evidence. Existing score and grade thresholds remain stable. Score JSON identifies its scope as `structural-maturity`. `memory.accuracy` is nullable: `null` represents unverified factual accuracy; the former proxy is exposed as `memory.structuralAlignment`. Consumers must preserve null as an unknown value.

An `assurance` object accompanies score, diagnose, CI, and report output. It contains `status` (`unverified`), `factualAccuracy` (`null`), and `unverifiedClaims` (a count of extracted candidates, or null if extraction failed). Even zero extracted candidates leaves prose unverified. Claim discovery uses a bounded heuristic. These fields explain evidence limits while existing CI thresholds retain their numeric meaning.

## Evidence verification contract: `.docguard-evidence.json`

The optional version-1 manifest contains at most 128 declarations. Each immutable
ID owns an `always` applicability declaration, one Markdown target, one source,
and one compatible predicate. Unknown fields, duplicate IDs, unsafe paths, and
ambiguous predicate combinations invalidate the manifest.

| Source adapter | Required contract | Compatible predicate |
|---|---|---|
| `json-pointer` | Safe JSON file plus an RFC 6901 pointer | `equals` with an explicit JSON type, or `set-equals` for a duplicate-free string array |
| `collection-count` | One bounded repository-relative glob and explicit `allowEmpty` policy | `count-equals` |
| `python-literal-count` | Safe `.py` path, one ASCII module-level symbol, one uniquely assigned static list/tuple/set/dict literal, and explicit `allowEmpty` policy | `count-equals` |
| `oasdiff` | Saved bounded JSON array, adapter version, producer version, `breaking` or `changelog` command, and current input hashes | `no-findings` |
| `buf` | Saved bounded JSON Lines, adapter version, producer version, `breaking` command, and current input hashes | `no-findings` |

Every result contains the declaration ID, stable claim and evidence identities,
document location, adapter, predicate, captured input hashes, evidence hash,
reason code, and scope limitation. The state is exactly one of
`verified-within-scope`, `contradicted`, `stale`, `inconclusive`, or

<!-- truncated: 67 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.12.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-09-30 -->

> The DocGuard CLI needs no environment variables. Three optional ones exist: `DOCGUARD_API_KEY` for the HTTP MCP server, `DOCGUARD_SPECIFY_TIMEOUT_MS` for `docguard init`'s calls 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.12.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 (`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` with the `specify` CLI installed | Timeout in milliseconds for each `specify` call. Defaults: 15000 for `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

# 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. `fix --write`, `sync --write`, `init`, `review --accept` /
`--prune`, `specs --write` and `memory --pack` write into the project, so they need the normal
mount. 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.

## Revision History

| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 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.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-09-30 -->

## 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 across imported routers. | tests/js-ast.test.mjs, tests/routes-express-mounts.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 until the frozen v2 protocol records its trials; the protocol, not an impression, decides whether it becomes a default.

## 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.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.13.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-09-30 -->

## 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: it passes argv arrays, accepts an integration key only if it matches `^[a-zA-Z0-9_-]{1,32}$`, uses only the options `specify init --help` documents, and bounds each call with a timeout.

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`); the import graph and schema scanners use the same pattern (`cli/scanners/py-ast.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 (`.agent/skills/`, `.agent/commands/`) or prints the Spec Kit setup hint. No other command does, 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 |
| 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] | None | Read-only, including agent skills and the Spec Kit hint; `--suggest` prints a draft ownership block and never writes configuration |
| 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 |
| 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 |

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.

## 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.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.3.0 -->
<!-- docguard:status active -->
<!-- docguard:last-reviewed 2026-09-30 -->

> 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 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` | ✅ 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) | `tests/mcp.test.mjs`, `tests/mcp-http.test.mjs`, `tests/mcp-doc-tools.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` | ✅ |

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.
> Manual checks supplement platform-specific event behavior.

## 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.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, and the symbol map stays
opt-in until those trials are recorded.

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.

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]

**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.

Behaviour changes:

- `docguard mcp` refuses a tool call whose `projectDir` is outside the
  directory it serves. Start it with `--root <dir>` for each other tree a
  client should reach.
- `sync`, `fix` and `generate` no longer run `specify init`. Run `docguard init`
  or `specify init` yourself.
- The Spec Kit extension requires Spec Kit 0.11.2 or later, and `docguard
  init` no longer initializes Spec Kit with an older `specify`.
- In a Claude Code project, `docguard init` installs DocGuard's skills into
  `.claude/skills/` instead of `.agent/`. An existing `.agent/` is left in place.
- The manifest key `provides.workflows` moved to `x-docguard.github_workflows`.
- A `.docguardignore` `dir/` pattern now matches that directory at any depth,
  as in `.gitignore`. This can hide findings that fired before.
- The GitHub Action installs the CLI version it was released with, not
  `@latest`. Set `docguard-version: latest` to keep following npm.
- The MCP tool `docguard_guard` returns the compact form (each fact once).
  Pass `detail: "full"` for the previous shape, with `reportable`,
  `validators[].findings`, every applicability reason and
  `checkCoverage.limitations`.
- `apiSurface` reports `partial` on Go, Java, Ruby and Rust services, and
  Environment reports `partial` when a source language has no env patterns.
  Both used to say `checked`. Partial coverage does not change the exit code;
  it lowers the badge colour and the coverage counts.

### Added

- **Reviewed asset paths for spec reconciliation**
  (`specs/039-asset-path-attribution`). Some files can't name their spec:
  digest-pinned fixtures, recorded outputs. `specs complete` reported every
  such file as unresolved (SPC006), and that blocked completion. A spec's
  `reviewed.scope.assetPaths` now lists them by hand. An entry ending in `/`
  covers a directory, any other entry one file, and wildcards are rejected.
  Reconciliation links a change under a listed path to the spec. Guard
  reports **SPR009** for an entry that covers no tracked file. Spec 036 lists
  its frozen benchmark assets, which had blocked maintenance completions of
  every living spec reviewed before it merged.

- **Agents learn when DocGuard is old, with no network call**
  (`specs/038-update-awareness`). DocGuard reads its own release date from the
  `CHANGELOG.md` it ships with. When that is more than 14 days ago, guard's
  text output, the MCP server's `initialize` instructions and the context pack
  say a newer version may exist and name `docguard upgrade`, the one command
  that checks npm. The DocGuard skills tell the agent to ask the user and
  never upgrade unasked. The note is silent when the date cannot be read for
  certain, in a source checkout, in guard's text output under `CI`, and with
  `DOCGUARD_NO_UPDATE_HINT=1`. JSON, SARIF, JUnit and MCP tool results are
  unchanged. `PRIVACY.md` now also lists `docguard upgrade`'s registry fetch,
  which it omitted.

- **Symbol map in the context pack, opt-in** (`specs/036-symbol-map`).
  `memory --pack --symbols` adds a `## Symbol map`: the source files most
  central in the static import graph (PageRank, deterministic), each with the
  names it exports, within `memory.symbolMap.maxBytes` (default 4096). It costs
  +1.2% of `memory --pack` time on this repository, because symbols are read
  only for the files that fit. Without the flag the pack is unchanged.
  Whether it becomes the default is decided by agent-context protocol v2,
  frozen before any run (`benchmarks/agent-context/manifest-v2.json`): six
  tasks, three of them navigation-bound in a new 29-module fixture. Default-on
  needs no regression and a benefit; a regression withdraws the feature. The
  54-trial matrix has not been run yet.
- **Doc ownership map** (`specs/034-doc-ownership-map`). A project can declare
  which doc section is responsible for which source paths, in an `ownership`
  block in `.docguard.json`:
  - Each tracked file has at most one owner. An exact path beats any glob, a
    longer literal directory prefix beats a shorter one, and anything else is
    a tie.
  - `trace --reverse`, `review --suggest` and the MCP tool
    `docguard_docs_for_path` now report the declared owner.
  - `trace --owners` lists the map; `trace --owners --suggest` drafts one and
    never writes it.
  - The **Doc-Ownership** validator reports unowned source directories (at
    their highest level), ties, patterns and roots that match nothing, and
    entries naming a missing doc or section (OWN001–OWN004). A broken block is
    an error (OWN007), never a pass.
  - A committed `.devin/wiki.json` is linted against Devin's documented rules
    (checked 2026-09-30): 30 pages (80 on enterprise, via
    `devinWiki.maxPages`), 100 notes counted together, 10,000 characters per
    note, required keys, unique titles (OWN005). Paths its notes name that are
    no longer in the repository are OWN006.
  - This repository maps `cli/`, `extensions/`, `tools/`, `benchmarks/` and
    `tests/`: every source file has exactly one owner, checked in 25 ms.
- **Path-scoped agent rules** (`specs/033-path-scoped-rules`). Agents load
  instructions by path, and each harness does it differently. DocGuard now
  reads them the way each one does: Codex, Claude Code, Cursor, Copilot and
  OpenHands. The formats were checked against each vendor's documentation on
  2026-09-30 and recorded in `docs/ai-integration.md`.
  - `docguard rules --for <path>` lists, per harness, the files an agent loads
    for a path, why (always, the directory chain, or the pattern that matched)
    and the bytes. Machine-local rules are included and marked.
  - The **Path-Scoped-Rules** validator reports:
    - PSR001: a scope pattern that matches no tracked file, so the rule never
      loads;
    - PSR002: a pointer to a missing path, including routing-table rows and
      Markdown links;
    - PSR003: instructions for one path over `agentInstructions.maxBytes`;
    - PSR004: a scope the harness cannot read, or reads differently than
      written, e.g. `globs:` in a Claude Code rule. Claude Code reads only
      `paths:`, so it loads that rule for every file.
  - Pointer checks skip frontmatter, patterns, `UPPER_CASE` placeholders,
    sentences saying the file may be absent, gitignored paths, and a skill's
    runtime paths. On this repository they check 12 pointers and find none
    broken; a raw check found 247 false positives in Spec Kit's generated
    skills.
  - Patterns DocGuard does not evaluate (`[...]`, `!`) are reported as not
    checked, never as matching nothing.
- **Diagrams drawn from code** (`specs/035-code-derived-diagrams`). Add an
  empty `<!-- docguard:section id=module-graph source=code -->` block to
  ARCHITECTURE, or `id=entity-diagram` to DATA-MODEL, and `sync --write` fills
  it with a mermaid diagram:
  - `module-graph`: directory modules and the static imports between them;
  - `entity-diagram`: the entities and relationships from the schema scan.

  When imports or schemas change, Generated-Staleness reports the section
  (GST002) and `sync --write` redraws it. The output is byte-identical for the
  same code. Large graphs are grouped and merged to at most
  `diagrams.moduleGraph.maxNodes` nodes (default 30) and 150 edges, and a
  caption states every reduction. A graph missing evidence on this machine
  (no Python interpreter, an unreadable file) is reported `partial`, not
  stale. `sync --write` leaves it alone unless `--allow-partial` is given.
  The import graph is now built once per run for the Architecture validator,
  `impact` and the diagram (a stat-checked cache). Adopting the diagram in
  this repository cost 0.7% of guard time.
- **MCP doc navigation tools** (`specs/032-mcp-doc-tools`). Agents can ask
  which documentation describes a file and read one section, instead of
  loading whole documents:
  - `docguard_docs_for_path` returns the doc lines that name a file (with
    their text, heading and section), agent-instruction lines,
    `@implements`/`@req` IDs, `@doc` annotations, and `covers=` sections with
    their review state;
  - `docguard_doc_structure` returns a document's outline;
  - `docguard_read_section` reads by section id, anchor, heading, or the lines
    around one reference (8 KiB default, 32 KiB cap);
  - `docguard_task_context` returns the `agent --task` packet.

  Finding and reading what describes `cli/scanners/as-built.mjs` costs 11.5%
  of reading those docs whole. The tools are deterministic and read-only,
  refuse traversal, private and symlinked paths, and none of them calls a
  model. `trace --reverse`, `impact` and the tool share one reference matcher.
  The published tool listings (MCPB manifest, Smithery, server.json, docs)
  now match `tools/list`: they had drifted to 5 and 6 of 7.

- **Doc dependency lock: know which docs to re-read when code changes**
  (`specs/030-doc-dependency-lock`, new `docguard review` command).
  - A doc section declares the code it describes, e.g.
    `covers="src/pricing.mjs#discount"` on its `docguard:section` marker.
  - `docguard review --accept <doc>#<id> --reason` records a fingerprint of
    that code in `.docguard-doc-lock.json`. JS/TS symbols use a normalized AST
    and Python symbols `ast.dump`; files use their bytes and globs their
    matching set.
  - Guard's new Doc-Dependency validator reports **DLK001** when covered code
    changes semantically, and the finding carries the `git diff` command.
    Reformatting, comments and moving the function do not trigger it; a
    same-size edit (`0.9` → `0.8`) does.
  - Also reported: DLK002 (dependency gone), DLK003 (not accepted, or
    `covers=` changed), DLK004 (orphan entry, cleared by `--prune`) and DLK005
    (unreadable lock).
  - `--suggest` proposes `covers=` values and never writes.
  - Opt-in: a project without `covers=` sees no change. The validator also runs
    in `--changed-only` pre-commit mode. This replaces the idea of tracking
    line counts or file size, which cannot see same-size edits.

- **Non-regression budgets** (`specs/031-non-regression-budgets`). A new CI
  `budget` job compares every PR with its base on the same runner. It checks:
  - guard wall time (medians of interleaved samples; ×1.15, 150 ms floor);
  - new error findings on this repository;
  - agent-facing bytes: context pack, task packets, MCP responses and llms
    bundles (+10%);
  - packed package weight;
  - runtime dependencies.

  A cost the change needs is declared as `Budget-Exempt: <metric> — <reason>`.
  `npm run budget -- --base <dir>` prints the same table locally. The first
  measurement shows the `docguard_guard` MCP response at about 130 KB per call.

- **Completion revisions survive squash merges** (`specs/028-completion-revision-anchoring`).
  - `specs complete` records the revision it reviewed. A completion committed
    on a PR branch recorded that branch's commit, which a squash merge never
    puts on `main`. Twelve recorded revisions across seven of this repository's
    specs pointed at such commits.
  - **SPR008** reports a recorded revision that does not resolve or is not on
    HEAD's history. A shallow clone reports partial coverage instead.
  - `docguard specs reanchor --id <spec-id> --write` moves the anchor to the
    first first-parent commit whose evidence files are byte-identical, and
    records `reanchoredFrom`. A revision that no longer exists needs
    `--to <revision> --reason` and is recorded as attested; so does a target
    whose evidence changed after the review, and the differing files are
    recorded.
  - Several completions can now run against one revision before a single
    commit: the registry, the active context and outcome blocks they write no
    longer count as changes. `specs complete --write` warns when HEAD is not on
    the remote default branch.

- **As-built specs: reverse-engineer a Spec Kit spec for code that has none,
  and keep it synced (#455, `specs/024-as-built-specs`).** `docguard generate
  --spec <area>` scans one directory for the facts DocGuard can establish
  without an LLM: routes (method and path), exported JS/TS symbols,
  environment variables read, and entities. It proposes one `FR-NNN`
  candidate per fact, each with a `<!-- docguard:fact … -->` marker and a
  file citation. Every requirement statement is left to the agent; DocGuard
  writes no prose.

  With `--write`, the spec lands in the next free feature directory and is
  registered as `origin: as_built` with its `sourcePaths`. Guard then reports
  **`SPR007`** for a fact that appears in the code with no requirement, and
  for a cited fact that disappears. A fact deliberately left unspecified goes
  under `## Out of Scope` with a reason. Both registry fields are serialized
  only when set, so existing registries stay byte-identical.

  The four community brownfield extensions (Brownfield Bootstrap, BrownKit,
  Blueprint Index, Time Machine) were compared first. None validates an
  as-built spec against code at the requirement level, and that is what
  DocGuard adds. It composes with their drafts and never re-runs
  `/speckit.implement` over existing code. `grepEnvUsage` gains a `within`
  scope for this. A TestGuard claim with four faults defends the sync check.

- **`docguard specs require`, a spec-first change gate (#455,
  `specs/015-spec-first-gate`).** Validators evaluate a tree, so none of them
  can notice the spec that was never written. This command evaluates a
  change: the paths touched since the merge base with `--since`, the commit
  messages in that range, and an optional `--message-file` (a PR body, read
  from a file so an untrusted body never reaches a shell).

  A change to governed paths (`.docguard.json` → `specFirst.paths`) passes if
  it does any of these:
  - names an existing `specs/<dir>` path or Spec ID;
  - edits a spec's own files;
  - declares `Spec-Exempt: <release|deps|typo|test-only> — <reason>`.

  An invented reference never passes. The result is one of `covered`,
  `exempt`, `not-governed`, `uncovered` (exit 1) or `inconclusive` (exit 2:
  unknown base, shallow history, oversized inventory). A change that cannot be
  shown to name its spec is not treated as one that does.

  The GitHub Action runs it as `command: spec-first`. This repository enforces
  it in CI for `cli/**`. Replayed over the last 40 merges on `main`, 14 of the
  19 that changed `cli/` named no spec.

- **Spec Kit artifacts are first-class documents (#455,
  `specs/016-spec-kit-artifact-coverage`).** DocGuard is a Spec Kit extension,
  yet the three places that read project documents didn't know Spec Kit
  existed:
  - **Coverage map.** It reported every `specs/**` file as "outside any tier",
    36 of 55 untiered files in this repository. That buried the genuinely
    unenrolled documents and trained adopters to ignore the line.
  - **Constitution.** It sits in `.specify/`, a dot directory every walker
    skips, so it was invisible.
  - **Consequence.** DocGuard's own constitution said "Dependencies: None.
    Zero. Ever." next to a shipped runtime dependency for six months, and
    guard stayed green.

  Now:
  - Coverage has a `specKit` tier: top-level `specs/<feature>/**/*.md` plus the
    constitution. The field is additive, and untiered files here fell from 55
    to 18.
  - Metrics-Consistency and Reference-Existence read the constitution.
  - A new **`MET004`** compares runtime-dependency claims ("zero runtime
    dependencies", "Dependencies: None", "one npm dependency") with
    `package.json`. It flags the pre-v2 constitution and nothing else here.
    Only qualified wording counts, so "no dependencies between validators" is
    never compared. It is an escalation with no mechanical fix: the manifest
    may be the side that is wrong.

- **Instruction files that agents cannot finish reading are reported (#455,
  `specs/017-agent-instruction-budget`).** Codex concatenates `AGENTS.md` from
  the repository root down to the working directory and stops reading at
  32 KiB (`project_doc_max_bytes`). It drops the rest silently, and the nested,
  most specific rules come last, so they are the ones lost.

  The Structure validator now measures every chain. `AGENTS.override.md`
  replaces `AGENTS.md` in its directory, as Codex does.
  - **`STR004`**: a chain is over `agentInstructions.maxBytes`.
  - **Existing debt**: record a per-chain `agentInstructions.allowances` entry.
    The chain may then shrink, but any growth is a reviewed configuration
    change. The finding baseline cannot serve this purpose, because it
    normalizes the digits in messages.
  - **`STR005`**: an allowance has at least 1 KiB of slack and should be
    lowered.

  This repository's chain is 13,222 bytes.

- **Colliding spec numbers are reported (`SPK012`; #462,
  `specs/017-agent-instruction-budget`).** Parallel agents each pick
  "the next number" from their own checkout, which produces `specs/016-a` and
  `specs/016-b`. After that, "spec 016" is ambiguous. open-mercato abandoned
  numbering over exactly this problem. Timestamp-numbered feature directories
  never collide.

- **The Spec Kit extension manifest now says what it needs (#455,
  `specs/018-extension-manifest-hygiene`).**
  - **Spec Kit floor.** It said `>=0.1.0` while using per-event hook lists with
    priority (Spec Kit 0.10.0), `category`/`effect` (0.10.2) and
    `after_converge` (0.11.2). An older Spec Kit would install it, and the hooks
    would then silently fail to register. The floor is now `>=0.11.2`, and a
    contract test derives it from the features the manifest uses.
  - **New fields.** It declares `category: docs` and `effect: read-write`.
  - **Explicit hook priorities.** Every hook has one. The briefing runs at 5,
    ahead of the git extension's branch creation at the default 10, so a
    blocked briefing no longer leaves an empty feature branch behind.
  - **Non-schema keys.** `requires.framework` is removed. The GitHub workflow
    starters move from `provides.workflows`, whose name collides with Spec
    Kit's own workflows, to `x-docguard.github_workflows`.
  - **Tags.** There were 9, against the publishing guide's limit of 2–5, so the
    catalog script kept its own list. The manifest now carries the 5 tags, and
    the submission reads the floor, description, category, effect and tags from
    it.
  - **Manual-install template.** `templates/extensions.yml` told adopters to
    copy it into `.specify/extensions.yml` in a shape Spec Kit doesn't write. It
    is now the exact registry that `specify extension add` produces, and a
    contract test keeps it equal to the manifest's hooks.
  - **Stale count.** `commands/guard.md` no longer promises "160+ automated
    checks".

- **This repository now obeys its validator-isolation principle, and a test
  enforces it.** `cli/validators/docs-sync.mjs` imported
  `cli/validators/api-surface.mjs`, against Constitution IV. OpenAPI discovery
  moved to `cli/shared-openapi.mjs`. `api-surface` re-exports it, so existing
  importers are unaffected. `tests/validator-isolation.test.mjs` fails on any
  validator-to-validator import.

- **`tests/repository-hygiene.test.mjs` fails the build if any of it returns.**
  `.gitignore` stops known paths; these assertions stop the *content*, which is
  what survives a rename or a newly adopted tool: no tracked file may contain a
  private project identifier (held base64-encoded so the guard is not itself the
  leak) or an absolute `/Users/<name>/` or `/home/<name>/` path, and generated or
  machine-local agent state may not be tracked. All three fail against v0.42.1.

- **Metrics-Consistency finishes the "N tests" check it has promised since
  v0.8.2 (MET003).** The validator's header listed "N tests" among the numbers
  it governs, `actuals.tests` was computed on every run, and no pattern ever
  read it — so the README's Testing section said `33 tests across 18 describe
  blocks` from 2026-03-15 through 92 releases while the suite grew past 2,000
  cases, and every self-guard was green. A dead path a passing check cannot
  see.

  The actual is now the number of test cases *declared* in the test files
  (`it`/`test` call sites via the AST tier, `def test_`, `func Test`), which is
  a lower bound of what the runner reports: cases generated in loops run more
  than once. Measured on this repository, 1,721 declared against 2,085
  reported. So MET003 flags a documented count only when it is *below* the
  declared floor — stale for certain — and passes anything at or above it,
  because a static count cannot disprove it. The true number is only known by
  running the suite, so MET003 is review-only and never carries a mechanical
  fix; a `docguard fix --write` that wrote the floor would replace a stale
  number with a wrong one. The claim is bound to the project's own suite by
  the line's vocabulary (`npm test`, `pytest`, `go test`, "suite", "passing"),
  not by the word "docguard": a number about a study's tests or a sample
  output line is out of scope. Thousands separators parse as one number
  ("1,733 tests" is 1733, not 733), and a count under a version-pinned
  heading ("Results — v0.31.0", "released in v0.40.0") is history, not a
  current assertion.

- **The DocGuard technical brief** (`docs/docguard-explained.html`, #449): an
  eight-page, print-ready explainer of what DocGuard checks, what it cannot
  know, and how its findings are calibrated.

- **Releases cut the right version, publish their own notes, and pin what they
  ship** (`specs/026-release-readiness`).
  - The weekly scheduler's bump defaults to `auto`: minor when the curated
    [Unreleased] changelog has Added, Removed or Deprecated entries, patch
    otherwise. It previously defaulted to `patch`, so an unattended Monday cut
    of this release would have published 0.42.2.
  - The changelog cut moves the curated [Unreleased] notes under the new
    version and lists merged commits under their own `### Commits` heading.
    The old splice put a raw commit list above the notes, which gave every
    automated release two `### Changed` headings.
  - The GitHub Action installs the CLI version it was released with. It ran
    `npm install -g docguard-cli@latest`, so pinning `raccioly/docguard@vX.Y.Z`
    did not pin the CLI. The new `docguard-version` input overrides it, and
    `latest` still works when asked for.
  - The Homebrew tap is published by the release workflow. The formula is

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

---

## ROADMAP.md
> Planned features and development roadmap

# DocGuard Roadmap

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

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–037 are merged on `main` and wait for the next release; the
`[Unreleased]` section of `CHANGELOG.md` lists them, and each spec's
`tasks.md` is its checklist. Two items remain open:

- **Symbol map promotion (spec 036, T009–T010).** The frozen v2 protocol's 54
  trials have yet to run. Until they do, `memory --pack --symbols` stays opt-in
  and the default context pack is unchanged.
- **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-09-30 -->

> 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), `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 --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`; 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
- 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.42.1 | [docguard-cli](https://www.npmjs.com/package/docguard-cli)
