docguard-cli 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -22
- package/cli/commands/agent.mjs +47 -1
- package/cli/commands/explain.mjs +16 -0
- package/cli/commands/feedback.mjs +147 -6
- package/cli/commands/fix.mjs +13 -11
- package/cli/commands/generate.mjs +52 -18
- package/cli/commands/guard.mjs +13 -2
- package/cli/commands/mcp.mjs +22 -2
- package/cli/commands/score.mjs +13 -1
- package/cli/commands/specs.mjs +21 -2
- package/cli/commands/sync.mjs +20 -7
- package/cli/commands/verify.mjs +65 -2
- package/cli/config.mjs +3 -0
- package/cli/docguard.mjs +48 -16
- package/cli/evidence/adapters.mjs +200 -0
- package/cli/evidence/evaluate.mjs +185 -0
- package/cli/evidence/manifest.mjs +194 -0
- package/cli/evidence/markdown.mjs +107 -0
- package/cli/feedback-fixture.mjs +188 -0
- package/cli/findings.mjs +31 -0
- package/cli/repository-root.mjs +159 -0
- package/cli/scanners/py-ast.mjs +39 -2
- package/cli/scanners/task-context.mjs +312 -0
- package/cli/shared-doc-roles.mjs +44 -1
- package/cli/shared-source.mjs +101 -28
- package/cli/validators/architecture.mjs +186 -13
- package/cli/validators/environment.mjs +14 -1
- package/cli/validators/evidence.mjs +52 -0
- package/cli/validators/security.mjs +5 -4
- package/cli/validators/todo-tracking.mjs +45 -2
- package/cli/writers/doc-generators.mjs +31 -17
- package/cli/writers/mechanical.mjs +44 -14
- package/cli/writers/sections.mjs +31 -3
- package/docs/ai-integration.md +31 -6
- package/docs/commands.md +43 -5
- package/docs/configuration.md +11 -3
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/commands/fix.md +4 -2
- package/extensions/spec-kit-docguard/commands/generate.md +6 -1
- package/extensions/spec-kit-docguard/commands/guard.md +3 -2
- package/extensions/spec-kit-docguard/commands/sync.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +14 -3
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +16 -5
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +8 -3
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +3 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +6 -3
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +4 -4
- package/package.json +3 -1
- package/schemas/docguard-agent-context-benchmark.schema.json +92 -0
- package/schemas/docguard-agent-context-result.schema.json +95 -0
- package/schemas/docguard-benchmark.schema.json +84 -0
- package/schemas/docguard-config.schema.json +1 -0
- package/schemas/docguard-evidence.schema.json +169 -0
- package/schemas/docguard-feedback-fixture.schema.json +54 -0
- package/schemas/docguard-task-context.schema.json +144 -0
- package/templates/AGENTS.md.template +9 -4
- package/templates/ci/github-actions.yml +4 -4
- package/templates/commands/docguard.guard.md +5 -1
- package/templates/commands/docguard.review.md +6 -1
- package/templates/evidence-manifest.json +21 -0
- package/templates/feedback-fixture.json +18 -0
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ graph TD
|
|
|
77
77
|
Commands --> setup["setup wizard"]
|
|
78
78
|
Commands --> other["diff · init · fix · trace · impact · sync · reconcile · retire · specs<br/>explain · memory · upgrade · agents · hooks · badge · ci · watch"]
|
|
79
79
|
|
|
80
|
-
guard --> Validators["Validators (
|
|
80
|
+
guard --> Validators["Validators (30)"]
|
|
81
81
|
generate --> Scanners["Scanners (4)<br/>routes · schemas · doc-tools · speckit"]
|
|
82
82
|
score --> Scoring["Weighted Scoring<br/>8 categories"]
|
|
83
83
|
diagnose --> Validators
|
|
@@ -104,7 +104,7 @@ graph TD
|
|
|
104
104
|
|
|
105
105
|
DocGuard checks declared documentation facts against repository evidence and gives agents structured repair tasks. Deterministic checks cover supported facts, references, and generated sections. Human-authored requirements and architectural decisions retain their authority when implementation diverges.
|
|
106
106
|
|
|
107
|
-
A guard result describes the checks performed. The CDD grade measures structural maturity.
|
|
107
|
+
A guard result describes the checks performed. The CDD grade measures structural maturity. Exact declarations in `.docguard-evidence.json` can verify selected statements against current local evidence; every other statement remains unverified. Coverage and unresolved claims remain visible, so teams can choose an appropriate enforcement policy.
|
|
108
108
|
|
|
109
109
|
Research motivates evaluation of this approach. A 2026 study found that repository context files did not generally improve task success and increased inference cost in its evaluated settings. It also found agents generally followed the instructions. These results support testing concise, relevant context and measuring actual task outcomes; they do not establish DocGuard's effectiveness. [Evaluating AGENTS.md, revised June 2026](https://arxiv.org/abs/2602.11988v2).
|
|
110
110
|
|
|
@@ -275,7 +275,7 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
|
|
|
275
275
|
| Command | What It Does |
|
|
276
276
|
|:--------|:-------------|
|
|
277
277
|
| `init` | Bootstrap a project (`--wizard` for interactive · `--with <name>` for scaffolders) |
|
|
278
|
-
| `guard` | Validate against canonical docs —
|
|
278
|
+
| `guard` | Validate against canonical docs — 30 validators |
|
|
279
279
|
| `diff` | Show gaps between docs and code (`--since <ref>` for impact mode) |
|
|
280
280
|
| `sync` | Refresh code-truth doc sections — keeps memory always up to date |
|
|
281
281
|
| `score` | CDD maturity score (0-100; `--diff` for delta between refs) |
|
|
@@ -290,14 +290,15 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
|
|
|
290
290
|
| `fix --write` | Apply deterministic fixes (no AI — version bumps, counts, anchors, sections) |
|
|
291
291
|
| `fix --history` | Audit log of every mechanical fix applied (from `.docguard/fixed.json`) |
|
|
292
292
|
| `generate` | Reverse-engineer docs from existing codebase (`--plan` for AI scan) — includes auto-generated Mermaid ER diagrams from your detected schemas (Prisma/Drizzle/TypeORM/Sequelize/Django/Rails) in DATA-MODEL.md |
|
|
293
|
-
| `agent` | One-shot agent task graph
|
|
293
|
+
| `agent` | One-shot agent task graph, or a bounded current-evidence packet for one task (`--task <text>`, `--format json`) |
|
|
294
294
|
| `explain <warning\|CODE>` | Paste any warning — or a finding code like `SEC001` — to get the validator's docstring, fix path, and how to suppress |
|
|
295
|
+
| `verify --evidence` | Evaluate strict statement-to-source declarations for typed JSON values, bounded collection counts, saved oasdiff JSON, and saved Buf JSON Lines. Results distinguish scoped verification, contradiction, stale inputs, inconclusive evidence, and unsupported formats. |
|
|
295
296
|
| `verify --semantic` | Extract documented numbers/limits/enums (retention days, rate limits, GSI/role counts, status enums) as a task list for an agent to check against code — the semantic-drift class regex/AST can't see |
|
|
296
297
|
| `verify --instructions` | Audit AGENTS.md/CLAUDE.md themselves for drift: duplicate rules, never-vs-always contradictions, stale file pointers, unknown commands — plus clustered rule pairs as agent judgment tasks |
|
|
297
|
-
| `feedback` |
|
|
298
|
+
| `feedback` | Review any finding or a synthetic false-positive/false-negative/unsupported fixture; verify its opposite control, reduce it deterministically, search open and closed duplicates, and optionally emit a test-only contribution. Nothing is submitted automatically. |
|
|
298
299
|
| `retire` | Find completed or superseded planning material (`--plan`/`--check`; `--fail-on-warning` gates advisory candidates) and explicitly remove clean tracked documentation from active AI context. `.docguard-archive.json` records recovery metadata and retired requirement identities, and `--retention-ref` proves the source revision remains reachable. This is separate from the Spec Kit Archive extension, which consolidates feature documents. |
|
|
299
300
|
| `reconcile` | Build a read-only code↔spec review graph since a Git ref. Classifies mechanical facts, approved intent, decisions, unrelated changes, and unsupported evidence; `--write` applies only mechanical generated-section refreshes. |
|
|
300
|
-
| `specs` | Maintain the versioned spec registry, preflight new specs, and apply evidence-gated completion transactions with bounded outcomes and active-context regeneration. |
|
|
301
|
+
| `specs` | Maintain the versioned spec registry, preflight new specs, and apply evidence-gated completion transactions with bounded outcomes and active-context regeneration. Verified living specs can record later reviewed maintenance without reopening or duplicating the specification. |
|
|
301
302
|
| `specs --check` / `specs --write` | Validate or refresh `.docguard-specs.json`, the byte-stable index of immutable spec IDs, reviewed lifecycle/lineage/scope, artifact digests, task state, explicitly scoped test evidence, and archive tombstones. Refreshes preserve the reviewed block. |
|
|
302
303
|
| `specs preflight [--path <spec>]` | Before specification, print current spec lifecycle and evidence. Before planning, check the generated draft for structural blockers and report semantic overlap as review-only evidence. |
|
|
303
304
|
| `mcp` | MCP server — exposes guard/score/explain/verify/report/diagnose as native tools for Claude, Cursor, and any MCP client. Stdio: `claude mcp add docguard -- npx docguard-cli mcp`. Team-shared HTTP: `docguard mcp --transport http --port 8585` (loopback by default; non-loopback binds require `--api-key`) |
|
|
@@ -326,16 +327,23 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
|
|
|
326
327
|
|
|
327
328
|
Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with agents,hooks,badge,ci`).
|
|
328
329
|
|
|
330
|
+
To declare an exact fact, copy `templates/evidence-manifest.json` to
|
|
331
|
+
`.docguard-evidence.json`, point its literal Markdown template at one unique
|
|
332
|
+
statement, and bind that value to a supported local source. Run
|
|
333
|
+
`docguard verify --evidence --format json` before enabling the guard in CI.
|
|
334
|
+
External compatibility declarations consume saved oasdiff or Buf output and
|
|
335
|
+
require current SHA-256 identities for every declared repository input.
|
|
336
|
+
|
|
329
337
|
**Deprecation aliases** — `setup` · `agents` · `hooks` · `badge` · `llms` · `publish` · `impact` remain compatible until v1.0 with a yellow stderr warning. `audit → guard` is permanent and silent; `ci` is a current first-class pipeline command.
|
|
330
338
|
|
|
331
339
|
### CLI Flags
|
|
332
340
|
|
|
333
341
|
| Flag | Description | Commands |
|
|
334
342
|
|:-----|:------------|:---------|
|
|
335
|
-
| `--dir <path>` | Project directory (default: `.`) | All |
|
|
343
|
+
| `--dir <path>` | Project directory (default: `.`); explicit selection suppresses ancestor-root guidance | All |
|
|
336
344
|
| `--verbose` | Show detailed output | All |
|
|
337
345
|
| `--quiet` / `-q` | Suppress banner — for hooks, CI loops, scripts | All |
|
|
338
|
-
| `--format json` | Machine-readable output (clean JSON, no ANSI bleed) | guard, score, diff, trace, diagnose, memory, impact, explain, reconcile, retire, specs |
|
|
346
|
+
| `--format json` | Machine-readable output (clean JSON, no ANSI bleed) | guard, score, diff, trace, diagnose, memory, impact, explain, verify, reconcile, retire, specs |
|
|
339
347
|
| `--format sarif` | SARIF 2.1.0 output — findings as rules/results for GitHub Code Scanning and SARIF dashboards | guard |
|
|
340
348
|
| `--format junit` | JUnit XML output — one testcase per validator, for GitLab CI (`artifacts:reports:junit`), Jenkins, Azure DevOps, CircleCI | guard |
|
|
341
349
|
| `--update-baseline` | Adopt DocGuard on a legacy repo without a red day one: freeze today's findings into a committed `.docguard.baseline.json`; guard/ci then gate only NEW drift. Suppression is always visible ("N pre-existing finding(s) suppressed"), and `--no-baseline` shows the full picture | guard |
|
|
@@ -347,7 +355,7 @@ Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with a
|
|
|
347
355
|
| `--force-redo` | Bypass ping-pong suppression in `.docguard/fixed.json` | fix --write |
|
|
348
356
|
| `--profile <name>` | Starter / standard / enterprise | init |
|
|
349
357
|
| `--no-spec-kit` | Skip auto-init of `.specify/` / `.agent/` scaffolding | init |
|
|
350
|
-
| `--changed-only [--since <ref>]` | Pre-commit lite mode (
|
|
358
|
+
| `--changed-only [--since <ref>]` | Pre-commit lite mode (6 fast validators on changed files only) | guard |
|
|
351
359
|
| `--timings` | Per-validator wall-time profile (slowest first) | guard |
|
|
352
360
|
| `--show-failing` | Show warnings/errors even when status is PASS | guard |
|
|
353
361
|
| `--pin` | Record running CLI version into `.docguard.json` (reproducibility) | guard |
|
|
@@ -361,6 +369,15 @@ Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with a
|
|
|
361
369
|
| `--transport http` `--port` `--host` `--api-key` `--path` | Serve MCP over Streamable HTTP instead of stdio (team-shared server; loopback-only unless an api-key is set) | mcp |
|
|
362
370
|
| `--history` | Show fix audit log | fix |
|
|
363
371
|
|
|
372
|
+
When run from a nested package without `--dir`, DocGuard checks only that
|
|
373
|
+
selected directory. If a bounded ancestor scan finds a `.docguard.json` or an
|
|
374
|
+
npm/pnpm workspace declaration that owns the package, stderr shows an exact
|
|
375
|
+
repository-scope rerun command. DocGuard never changes scope automatically. JSON,
|
|
376
|
+
SARIF, and JUnit stdout remain valid; machine runs receive one typed
|
|
377
|
+
`docguard.repository-root-guidance` JSON diagnostic on stderr. A local config,
|
|
378
|
+
an explicit `--dir`, an unmatched workspace, or a nested Git boundary suppresses
|
|
379
|
+
the suggestion.
|
|
380
|
+
|
|
364
381
|
### Example Output
|
|
365
382
|
|
|
366
383
|
```
|
|
@@ -394,7 +411,7 @@ $ npx docguard-cli generate
|
|
|
394
411
|
|
|
395
412
|
## 🔍 Validators
|
|
396
413
|
|
|
397
|
-
DocGuard runs **
|
|
414
|
+
DocGuard runs **30 automated validators** on every `guard` check. Source-facing validators are language-aware where their evidence model applies; repository and document validators operate independently of source language.
|
|
398
415
|
|
|
399
416
|
| # | Validator | What It Checks | Default |
|
|
400
417
|
|:--|:----------|:--------------|:--------|
|
|
@@ -419,14 +436,15 @@ DocGuard runs **29 automated validators** on every `guard` check. Source-facing
|
|
|
419
436
|
| 19 | **Spec-Kit** | Spec quality validation (FR-IDs, mandatory sections, phased tasks) | ✅ On |
|
|
420
437
|
| 20 | **Document-Lifecycle** | Exact terminal states, advisory completion signals, incomplete coverage, and manifest/working-tree inconsistencies | ✅ On |
|
|
421
438
|
| 21 | **Spec-Registry** | Immutable spec identities, byte-stable evidence projection, reviewed lifecycle preservation, and archive/storage consistency | ✅ On |
|
|
422
|
-
| 22 | **
|
|
423
|
-
| 23 | **
|
|
424
|
-
| 24 | **
|
|
425
|
-
| 25 | **
|
|
426
|
-
| 26 | **
|
|
427
|
-
| 27 | **
|
|
428
|
-
| 28 | **
|
|
429
|
-
| 29 | **
|
|
439
|
+
| 22 | **Evidence** | Exact declared Markdown statements match current typed JSON, bounded collections, or saved compatibility reports; unsupported and missing evidence stays visible | ✅ On |
|
|
440
|
+
| 23 | **Cross-Reference** | Internal markdown links + anchors resolve (with "did you mean?" hints); Obsidian wikilinks validated when the repo uses them as file links (`.obsidian` present or a target resolves) | ✅ On |
|
|
441
|
+
| 24 | **Generated-Staleness** | `source=code` sections match scanner output; `status: draft` doc age | ✅ On |
|
|
442
|
+
| 25 | **Canonical-Sync** | DocGuard's own README count claims match code-truth (DocGuard repo only — N/A elsewhere) | ✅ On |
|
|
443
|
+
| 26 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
|
|
444
|
+
| 27 | **Surface-Sync** | Item-level enumerable drift — names in doc tables/lists (commands, checks, etc.) match code-truth (opt-in via `surfaceSync.surfaces`; N/A unless configured) | ✅ On |
|
|
445
|
+
| 28 | **Diff-Suspicion** | Change-driven: a doc/agent-instruction file that references code changed since the ref AND shares removed domain symbols is flagged for review (arXiv 2010.01625, F1 74.7) | ✅ On |
|
|
446
|
+
| 29 | **Reference-Existence** | Two-revision check: a backticked code symbol present when the doc was last updated but gone at HEAD is flagged as outdated (arXiv 2212.01479) | ✅ On |
|
|
447
|
+
| 30 | **API-Doc-Smells** | Bloated (≥300 words) / Lazy (≤6 prose words) API documentation units, keyed on signature-headed sections (F1 0.90/0.95) | ✅ On |
|
|
430
448
|
|
|
431
449
|
**Per-validator controls** (in `.docguard.json`):
|
|
432
450
|
```json
|
|
@@ -517,7 +535,7 @@ DocGuard provides AI agent slash commands for integrated workflows. Installed au
|
|
|
517
535
|
| Command | What It Does |
|
|
518
536
|
|:--------|:-------------|
|
|
519
537
|
| `/docguard.init` | Initialize Canonical-Driven Development in a new or existing project |
|
|
520
|
-
| `/docguard.guard` | Run quality validation — check all
|
|
538
|
+
| `/docguard.guard` | Run quality validation — check all 30 validators |
|
|
521
539
|
| `/docguard.review` | Analyze doc quality and suggest improvements |
|
|
522
540
|
| `/docguard.fix` | Generate targeted fix prompts for specific issues |
|
|
523
541
|
| `/docguard.update` | Update canonical docs after code changes — detect drift and sync documentation |
|
|
@@ -703,11 +721,16 @@ Highlights of the current line (v0.29 → v0.33):
|
|
|
703
721
|
agent tools: `claude mcp add docguard -- npx docguard-cli mcp`.
|
|
704
722
|
- **Agent-file family sync** — `agents --sync` treats AGENTS.md as canonical and regenerates
|
|
705
723
|
CLAUDE.md / `.cursor/rules` / Copilot / Gemini variants with drift-proof source-hash markers.
|
|
706
|
-
- **`verify --
|
|
707
|
-
|
|
708
|
-
and stale pointers.
|
|
724
|
+
- **`verify --evidence`, `verify --semantic`, and `verify --instructions`** — check exact local
|
|
725
|
+
evidence declarations first, extract remaining numbers/limits/enums as agent tasks, and audit
|
|
726
|
+
agent-instruction files for contradictions and stale pointers.
|
|
709
727
|
- **`docguard agent`** — one-shot ordered task graph with pre-filled code-truth, collapsing ~10
|
|
710
728
|
agent round-trips into one call.
|
|
729
|
+
- **`docguard agent --task <text>`** — opt-in task context from approved current
|
|
730
|
+
specs and canonical docs, with hashed excerpts, source/test pointers, strict
|
|
731
|
+
budgets, and honest abstention. The frozen 27-run evaluation preserved every
|
|
732
|
+
tested behavior and cut median steps by 50% and latency by 17% versus the
|
|
733
|
+
context pack, while using 80% more uncached input tokens.
|
|
711
734
|
|
|
712
735
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
713
736
|
|
package/cli/commands/agent.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @implements docguard.evidence-scoped-verification#FR-012
|
|
3
|
+
* @implements docguard.task-specific-agent-context#FR-010
|
|
2
4
|
* `docguard agent` — the one-shot agent task graph.
|
|
3
5
|
*
|
|
4
6
|
* Field report §2: an LLM told "run docguard and fix the docs" had to drive ~10
|
|
@@ -25,6 +27,8 @@ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
|
|
|
25
27
|
import { c } from '../shared.mjs';
|
|
26
28
|
import { createEvidenceReader, citedSources, taskEvidence, gitEvidence, SEMANTIC_COVERAGE_LIMITATION } from '../scanners/semantic-claims.mjs';
|
|
27
29
|
import { buildScoreAssurance } from './score.mjs';
|
|
30
|
+
import { evaluateEvidence } from '../evidence/evaluate.mjs';
|
|
31
|
+
import { buildTaskContextPacket } from '../scanners/task-context.mjs';
|
|
28
32
|
|
|
29
33
|
const PHASES = ['config', 'canonical-docs', 'verify'];
|
|
30
34
|
|
|
@@ -89,7 +93,7 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
|
|
|
89
93
|
phase: 'verify',
|
|
90
94
|
file: null,
|
|
91
95
|
kind: 'verify',
|
|
92
|
-
instruction: 'Run `docguard guard --format json`. Resolve every error, then re-run until there are 0 errors. Triage warnings and record unresolved warnings; warnings do not fail this acceptance gate. Run `docguard
|
|
96
|
+
instruction: 'Run `docguard guard --format json`. Resolve every error, then re-run until there are 0 errors. Triage warnings and record unresolved warnings; warnings do not fail this acceptance gate. Run `docguard verify --evidence` for exact declared checks and `docguard verify --semantic` for remaining heuristic claim tasks. Run `docguard score` for structural maturity. Neither guard nor score verifies prose; scoped evidence verifies only its selected statements and does not establish whole-document factual accuracy.',
|
|
93
97
|
prefilled: null,
|
|
94
98
|
grounding: null,
|
|
95
99
|
acceptance: { verify: 'docguard guard --format json', expect: '0 errors', warnings: 'triage-and-report', scope: 'structural-only', factualAccuracy: 'unknown' },
|
|
@@ -105,11 +109,13 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
|
|
|
105
109
|
|
|
106
110
|
const assurance = buildScoreAssurance(projectDir, config);
|
|
107
111
|
assurance.limitation += ` ${SEMANTIC_COVERAGE_LIMITATION}`;
|
|
112
|
+
const evidence = evaluateEvidence(projectDir, config);
|
|
108
113
|
|
|
109
114
|
return {
|
|
110
115
|
project: config.projectName,
|
|
111
116
|
provenance: { kind: 'snapshot', git: gitEvidence(projectDir) },
|
|
112
117
|
assurance,
|
|
118
|
+
evidence,
|
|
113
119
|
profile: { name: profileName, kind: plan.profile.kind, languages: plan.profile.languages, frameworks: plan.profile.frameworks },
|
|
114
120
|
order: PHASES,
|
|
115
121
|
counts: {
|
|
@@ -123,6 +129,46 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
|
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
export function runAgent(projectDir, config, flags) {
|
|
132
|
+
if (flags.task !== undefined) {
|
|
133
|
+
let packet;
|
|
134
|
+
try {
|
|
135
|
+
packet = buildTaskContextPacket(projectDir, config, flags.task);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : 'Task context input is invalid.';
|
|
138
|
+
if (flags.format === 'json') console.log(JSON.stringify({ status: 'error', code: 'TASK_CONTEXT_INPUT', message }, null, 2));
|
|
139
|
+
else console.error(`${c.red}Error:${c.reset} ${message}`);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
if (flags.format === 'json') {
|
|
144
|
+
console.log(JSON.stringify(packet, null, 2));
|
|
145
|
+
return packet;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`${c.bold}🎯 DocGuard Task Context — ${config.projectName}${c.reset}`);
|
|
149
|
+
console.log(`${c.dim} ${packet.selection.status} · ${packet.selection.selectedExcerpts} excerpt(s) · task ${packet.task.digest}${c.reset}\n`);
|
|
150
|
+
console.log(` ${c.dim}Retrieval only · factual accuracy remains unknown · verify before editing.${c.reset}\n`);
|
|
151
|
+
if (packet.selection.status === 'abstained') {
|
|
152
|
+
console.log(` ${c.yellow}No evidence met the relevance threshold. Use the navigation map for normal repository discovery.${c.reset}\n`);
|
|
153
|
+
}
|
|
154
|
+
for (const excerpt of packet.excerpts) {
|
|
155
|
+
console.log(` ${c.bold}${excerpt.path}:${excerpt.startLine}-${excerpt.endLine}${c.reset} ${c.dim}[${excerpt.kind} · score ${excerpt.score}]${c.reset}`);
|
|
156
|
+
for (const line of excerpt.content.split('\n')) console.log(` ${line}`);
|
|
157
|
+
console.log('');
|
|
158
|
+
}
|
|
159
|
+
if (packet.pointers.length) {
|
|
160
|
+
console.log(` ${c.bold}Evidence pointers${c.reset}`);
|
|
161
|
+
for (const pointer of packet.pointers) console.log(` - ${pointer.path} (${pointer.kind}: ${pointer.reasons.join(', ')})`);
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
console.log(` ${c.bold}Verify${c.reset}`);
|
|
165
|
+
for (const item of packet.verification) console.log(` - ${item.command} — ${item.purpose}`);
|
|
166
|
+
console.log(`\n ${c.bold}Limits${c.reset}`);
|
|
167
|
+
for (const limitation of packet.limitations) console.log(` - ${limitation}`);
|
|
168
|
+
console.log(`\n ${c.dim}Run with --format json for hashes, machine-readable reasons, assurance, and navigation.${c.reset}`);
|
|
169
|
+
return packet;
|
|
170
|
+
}
|
|
171
|
+
|
|
126
172
|
// Allow `--profile <name>` to preview a profile's plan without having to run
|
|
127
173
|
// `init` first (the field-report agent had no config yet on its first call).
|
|
128
174
|
const cfg = flags.profile ? { ...config, profile: flags.profile } : config;
|
package/cli/commands/explain.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @implements docguard.evidence-scoped-verification#FR-013
|
|
2
3
|
* Explain Command — v0.16-P6.
|
|
3
4
|
*
|
|
4
5
|
* Asked for by a user who'd spent 5-10 minutes per warning spelunking
|
|
@@ -278,6 +279,20 @@ const EXPLAINERS = {
|
|
|
278
279
|
example: '`docguard specs preflight --path specs/007-feature/spec.md` blocks reused IDs and reports prior lifecycle/evidence before planning.',
|
|
279
280
|
standard: 'Canonical-Driven Development lifecycle registry contract',
|
|
280
281
|
},
|
|
282
|
+
evidence: {
|
|
283
|
+
title: 'Evidence — declared statements match bounded local sources',
|
|
284
|
+
what: 'Evaluates opt-in declarations in `.docguard-evidence.json`. Each declaration selects one exact statement under one Markdown heading, reads a bounded local JSON value, file collection, saved oasdiff report, or saved Buf report, and reports verified-within-scope, contradicted, stale, inconclusive, or unsupported.',
|
|
285
|
+
why: 'Structural checks can prove that documentation exists and is connected, but they cannot prove factual claims. Evidence declarations add reproducible checks for selected high-value statements without claiming that the whole document is accurate.',
|
|
286
|
+
triggers: [
|
|
287
|
+
['Evidence manifest is invalid', 'Validate `.docguard-evidence.json` against `schemas/docguard-evidence.schema.json`; unknown fields and ambiguous declarations fail closed.'],
|
|
288
|
+
['Declared evidence contradicts', 'Inspect the selected statement and source. Update the implementation when the canonical statement is still intended, or revise the statement through the normal reviewed documentation workflow.'],
|
|
289
|
+
['Declared evidence is stale', 'Regenerate the saved external-tool report from the declared inputs and update their SHA-256 digests.'],
|
|
290
|
+
['Declared evidence is inconclusive', 'Repair the missing or ambiguous heading/statement, unreadable source, unsafe collection, or malformed saved report before relying on the declaration.'],
|
|
291
|
+
['Declared evidence uses an unsupported', 'Use a documented adapter version and command, or upgrade DocGuard when support for that producer shape becomes available.'],
|
|
292
|
+
],
|
|
293
|
+
example: 'A retention statement under `## Policy` declares JSON Pointer `/retentionDays`; `docguard verify --evidence` reports verified-within-scope only when the exact typed values match.',
|
|
294
|
+
standard: 'RFC 6901 JSON Pointer; JSON Schema 2020-12; DocGuard evidence-scoping contract',
|
|
295
|
+
},
|
|
281
296
|
|
|
282
297
|
// ── Backfilled in v0.24 (field report, Issue A) ─────────────────────────
|
|
283
298
|
// These validators were registered in guard but had no explain entry, so
|
|
@@ -410,6 +425,7 @@ const DISPLAY_NAMES = {
|
|
|
410
425
|
surfaceSync: 'Surface-Sync',
|
|
411
426
|
canonicalSync: 'Canonical-Sync',
|
|
412
427
|
metricsConsistency: 'Metrics-Consistency',
|
|
428
|
+
evidence: 'Evidence',
|
|
413
429
|
diffSuspicion: 'Diff-Suspicion',
|
|
414
430
|
referenceExistence: 'Reference-Existence',
|
|
415
431
|
apiDocSmells: 'API-Doc-Smells',
|
|
@@ -12,15 +12,23 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Nothing is transmitted automatically. This command never scaffolds skills
|
|
14
14
|
* or edits source files. Zero npm dependencies — pure Node.js built-ins.
|
|
15
|
+
* @implements docguard.precision-evidence-loop#FR-012
|
|
16
|
+
* @implements docguard.precision-evidence-loop#FR-015
|
|
17
|
+
* @implements docguard.precision-evidence-loop#FR-016
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
18
|
-
import { resolve, dirname } from 'node:path';
|
|
20
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { resolve, dirname, join, relative, sep } from 'node:path';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
19
23
|
import { fileURLToPath } from 'node:url';
|
|
20
24
|
import { c } from '../shared.mjs';
|
|
21
25
|
import { CODES } from '../findings.mjs';
|
|
22
26
|
import { safeWrite } from '../writers/generate-io.mjs';
|
|
23
27
|
import { runGuardInternal } from './guard.mjs';
|
|
28
|
+
import {
|
|
29
|
+
buildTestOnlyContribution, feedbackFindingIdentity, feedbackSearchUrls, parseFeedbackFixture,
|
|
30
|
+
reduceFixtureDeterministically,
|
|
31
|
+
} from '../feedback-fixture.mjs';
|
|
24
32
|
|
|
25
33
|
const _PKG = JSON.parse(
|
|
26
34
|
readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf-8')
|
|
@@ -39,16 +47,20 @@ function shortId(str) {
|
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/** Shared output deliberately excludes every source-derived string. */
|
|
42
|
-
export function buildIssueUrl(finding) {
|
|
50
|
+
export function buildIssueUrl(finding, context = null) {
|
|
43
51
|
const code = Object.hasOwn(CODES, finding.code || '') ? finding.code : 'FINDING';
|
|
44
52
|
const validator = CODES[code]?.validator || 'unknown';
|
|
45
|
-
const
|
|
53
|
+
const classification = context?.classification || 'false_positive';
|
|
54
|
+
const title = `[feedback] ${code} (${validator}): ${classification.replaceAll('_', ' ')}`;
|
|
46
55
|
const confidence = ['high', 'medium', 'low'].includes(finding.confidence) ? finding.confidence : 'unknown';
|
|
47
56
|
const body = [
|
|
48
57
|
`DocGuard v${CLI_VERSION}`,
|
|
49
58
|
`- Code: ${code}`,
|
|
50
59
|
`- Validator: ${validator}`,
|
|
51
60
|
`- Confidence: ${confidence}`,
|
|
61
|
+
`- Classification: ${classification}`,
|
|
62
|
+
...(context?.parserTier ? [`- Parser tier: ${context.parserTier}`] : []),
|
|
63
|
+
...(context?.duplicateIdentity ? [`- Duplicate identity: ${context.duplicateIdentity}`] : []),
|
|
52
64
|
'',
|
|
53
65
|
'Expected behavior:',
|
|
54
66
|
'Actual behavior:',
|
|
@@ -60,12 +72,139 @@ export function buildIssueUrl(finding) {
|
|
|
60
72
|
'Generated by docguard feedback. No project paths, messages, source code, or secret values are included.',
|
|
61
73
|
].join('\n');
|
|
62
74
|
const url = `${ISSUES_BASE}/new?labels=docguard-feedback&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
|
63
|
-
const query = `repo:raccioly/docguard ${code}`;
|
|
75
|
+
const query = context?.duplicateIdentity ? `repo:raccioly/docguard "${context.duplicateIdentity}"` : `repo:raccioly/docguard ${code}`;
|
|
64
76
|
const searchUrl = `https://github.com/search?q=${encodeURIComponent(query)}&type=issues`;
|
|
65
77
|
return { url: url.length <= URL_CAP ? url : `${ISSUES_BASE}/new`, title, searchUrl };
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
const FEEDBACK_CLASSES = new Set(['false_positive', 'false_negative', 'unsupported_syntax', 'ambiguous', 'policy_disagreement']);
|
|
81
|
+
|
|
82
|
+
function projectFile(projectDir, value, label) {
|
|
83
|
+
if (typeof value !== 'string' || !value || value.includes('\\')) throw new Error(`${label} must be a project-relative path.`);
|
|
84
|
+
const root = resolve(projectDir);
|
|
85
|
+
const path = resolve(root, value);
|
|
86
|
+
const rel = relative(root, path);
|
|
87
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === '..' || rel.split(sep).some(part => ['.git', '.local'].includes(part.toLowerCase()))) {
|
|
88
|
+
throw new Error(`${label} must remain in the project and outside protected directories.`);
|
|
89
|
+
}
|
|
90
|
+
return path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function evaluateFileSet(manifest, fileSet) {
|
|
94
|
+
const root = mkdtempSync(join(tmpdir(), 'docguard-feedback-fixture-'));
|
|
95
|
+
try {
|
|
96
|
+
for (const file of fileSet.files) {
|
|
97
|
+
const target = resolve(root, file.path);
|
|
98
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
99
|
+
writeFileSync(target, file.content, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
const report = runGuardInternal(root, manifest.config);
|
|
102
|
+
const validator = (report.validators || []).find(item => item.key === manifest.detector.validator);
|
|
103
|
+
return {
|
|
104
|
+
identities: (report.findings || []).filter(item => item.code === manifest.detector.code).map(feedbackFindingIdentity).sort(),
|
|
105
|
+
applicability: validator?.applicability?.status || 'unknown',
|
|
106
|
+
};
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function predicateMatches(manifest, result) {
|
|
113
|
+
if (manifest.interestingness.predicate === 'finding_present') return result.identities.includes(manifest.expectedIdentity);
|
|
114
|
+
if (manifest.interestingness.predicate === 'finding_absent') return !result.identities.includes(manifest.expectedIdentity);
|
|
115
|
+
return result.applicability === 'unsupported';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function oppositeMatches(manifest, result) {
|
|
119
|
+
if (manifest.interestingness.predicate === 'finding_present') return result.identities.includes(manifest.expectedIdentity);
|
|
120
|
+
if (manifest.interestingness.predicate === 'finding_absent') return !result.identities.includes(manifest.expectedIdentity);
|
|
121
|
+
return result.applicability === 'checked';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function runFixtureFeedback(projectDir, flags) {
|
|
125
|
+
let manifest;
|
|
126
|
+
try {
|
|
127
|
+
const manifestPath = projectFile(projectDir, flags.fixtureManifest, '--fixture-manifest');
|
|
128
|
+
if (!existsSync(manifestPath) || lstatSync(manifestPath).isSymbolicLink()) throw new Error('Fixture manifest must be a regular non-symlink file.');
|
|
129
|
+
const text = readFileSync(manifestPath, 'utf8');
|
|
130
|
+
if (Buffer.byteLength(text) > 262_144) throw new Error('Fixture manifest exceeds 256 KiB.');
|
|
131
|
+
manifest = parseFeedbackFixture(JSON.parse(text));
|
|
132
|
+
if (flags.classification && flags.classification !== manifest.classification) throw new Error('--classification disagrees with the fixture manifest.');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.log(JSON.stringify({ status: 'ERROR', error: error.message }, null, 2));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const initial = evaluateFileSet(manifest, manifest.fixture);
|
|
140
|
+
const control = evaluateFileSet(manifest, manifest.oppositeControl);
|
|
141
|
+
const reproduced = predicateMatches(manifest, initial);
|
|
142
|
+
const controlConfirmed = oppositeMatches(manifest, control);
|
|
143
|
+
if (!reproduced || !controlConfirmed) {
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
status: 'NOT_REPRODUCED', classification: manifest.classification,
|
|
146
|
+
reproductionConfirmed: reproduced, controlConfirmed,
|
|
147
|
+
message: 'The explicit predicate and opposite control must both reproduce before reduction or contribution.',
|
|
148
|
+
}, null, 2));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let reduction = null;
|
|
154
|
+
if (flags.reduce) {
|
|
155
|
+
reduction = reduceFixtureDeterministically(manifest, candidate => predicateMatches(candidate, evaluateFileSet(candidate, candidate.fixture)));
|
|
156
|
+
manifest = reduction.manifest;
|
|
157
|
+
}
|
|
158
|
+
const searches = feedbackSearchUrls(manifest, ISSUES_BASE);
|
|
159
|
+
const issue = buildIssueUrl({ code: manifest.detector.code, confidence: 'unknown' }, {
|
|
160
|
+
classification: manifest.classification, parserTier: manifest.parserTier, duplicateIdentity: searches.identity,
|
|
161
|
+
});
|
|
162
|
+
const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
|
|
163
|
+
let record = null;
|
|
164
|
+
let contribution = null;
|
|
165
|
+
let contributionPreview = null;
|
|
166
|
+
try {
|
|
167
|
+
if (!flags.preview) {
|
|
168
|
+
record = resolve(feedbackDir, `${searches.identity}.fixture.json`);
|
|
169
|
+
safeWrite(record, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
170
|
+
}
|
|
171
|
+
if (flags.contribution) {
|
|
172
|
+
if (!/^tests\/[^/]+\.test\.mjs$/.test(flags.contribution)) {
|
|
173
|
+
throw new Error('--contribution must be a direct tests/<name>.test.mjs path.');
|
|
174
|
+
}
|
|
175
|
+
contributionPreview = buildTestOnlyContribution(manifest);
|
|
176
|
+
if (!flags.preview) {
|
|
177
|
+
contribution = projectFile(projectDir, flags.contribution, '--contribution');
|
|
178
|
+
safeWrite(contribution, contributionPreview);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.log(JSON.stringify({ status: 'ERROR', error: error.message }, null, 2));
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const output = {
|
|
187
|
+
status: 'READY', preview: Boolean(flags.preview), classification: manifest.classification,
|
|
188
|
+
reproductionConfirmed: true, controlConfirmed: true, reduction: reduction ? { status: reduction.status, attempts: reduction.attempts } : null,
|
|
189
|
+
duplicateIdentity: searches.identity, searchUrls: searches, issueUrl: issue.url,
|
|
190
|
+
record: record ? relative(projectDir, record).split(sep).join('/') : null,
|
|
191
|
+
contribution: contribution ? relative(projectDir, contribution).split(sep).join('/') : null,
|
|
192
|
+
contributionPreview: flags.preview && flags.contribution ? contributionPreview : undefined,
|
|
193
|
+
};
|
|
194
|
+
console.log(JSON.stringify(output, null, 2));
|
|
195
|
+
}
|
|
196
|
+
|
|
68
197
|
export function runFeedback(projectDir, config, flags) {
|
|
198
|
+
if (flags.fixtureManifest) return runFixtureFeedback(projectDir, flags);
|
|
199
|
+
const classification = flags.classification || 'false_positive';
|
|
200
|
+
if (!FEEDBACK_CLASSES.has(classification) || classification === 'false_negative' || classification === 'unsupported_syntax') {
|
|
201
|
+
const error = classification === 'false_negative' || classification === 'unsupported_syntax'
|
|
202
|
+
? 'This classification requires --fixture-manifest with an explicit expected identity and opposite control.'
|
|
203
|
+
: 'Unknown feedback classification.';
|
|
204
|
+
if (flags.format === 'json') console.log(JSON.stringify({ error, reportable: [] })); else console.error(error);
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
69
208
|
const data = runGuardInternal(projectDir, config);
|
|
70
209
|
const selectedCode = typeof flags.code === 'string' ? flags.code.toUpperCase() : null;
|
|
71
210
|
if (flags.code !== undefined && (!selectedCode || !Object.hasOwn(CODES, selectedCode))) {
|
|
@@ -93,7 +232,7 @@ export function runFeedback(projectDir, config, flags) {
|
|
|
93
232
|
const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
|
|
94
233
|
const items = reportable.map((f) => {
|
|
95
234
|
const id = shortId(`${f.code}|${f.location || f.message}`);
|
|
96
|
-
const { url, title, searchUrl } = buildIssueUrl(f);
|
|
235
|
+
const { url, title, searchUrl } = buildIssueUrl(f, { classification });
|
|
97
236
|
const fileName = `${(f.code || 'finding').toLowerCase()}-${id}.json`;
|
|
98
237
|
const filePath = resolve(feedbackDir, fileName);
|
|
99
238
|
return { finding: f, id, url, title, searchUrl, fileName, filePath, saved: false, error: null };
|
|
@@ -105,6 +244,7 @@ export function runFeedback(projectDir, config, flags) {
|
|
|
105
244
|
if (!existsSync(feedbackDir)) mkdirSync(feedbackDir, { recursive: true });
|
|
106
245
|
safeWrite(it.filePath, JSON.stringify({
|
|
107
246
|
capturedBy: `docguard feedback (v${CLI_VERSION})`,
|
|
247
|
+
classification,
|
|
108
248
|
finding: it.finding,
|
|
109
249
|
issueUrl: it.url,
|
|
110
250
|
searchUrl: it.searchUrl,
|
|
@@ -123,6 +263,7 @@ export function runFeedback(projectDir, config, flags) {
|
|
|
123
263
|
console.log(JSON.stringify({
|
|
124
264
|
version: CLI_VERSION,
|
|
125
265
|
preview: Boolean(flags.preview),
|
|
266
|
+
classification,
|
|
126
267
|
reportable: items.map(it => ({
|
|
127
268
|
code: it.finding.code,
|
|
128
269
|
location: it.finding.location,
|
package/cli/commands/fix.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @implements docguard.language-repository-coverage#FR-010
|
|
3
|
+
* @implements docguard.language-repository-coverage#FR-011
|
|
4
|
+
*/
|
|
5
|
+
import { assertMappedFullDocumentWrites, docRolePath, isMappedDocPath, resolveDocRole } from '../shared-doc-roles.mjs';
|
|
2
6
|
/**
|
|
3
7
|
* Fix Command — The AI Orchestrator
|
|
4
8
|
*
|
|
@@ -14,7 +18,7 @@ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
|
|
|
14
18
|
* --auto Create skeleton files (NOT content) via init
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
|
-
import { existsSync, readFileSync,
|
|
21
|
+
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
18
22
|
import { resolve, basename, dirname } from 'node:path';
|
|
19
23
|
import { execSync, execFileSync } from 'node:child_process';
|
|
20
24
|
import { fileURLToPath } from 'node:url';
|
|
@@ -23,10 +27,9 @@ import { computeApiSurfaceDrift } from '../validators/api-surface.mjs';
|
|
|
23
27
|
import { removeEndpoints, hasGeneratedMarker } from '../writers/api-reference.mjs';
|
|
24
28
|
import { applyMechanicalFixes } from '../writers/mechanical.mjs';
|
|
25
29
|
import { loadFixMemory } from '../writers/fix-memory.mjs';
|
|
30
|
+
import { safeWrite } from '../writers/generate-io.mjs';
|
|
26
31
|
import { runGuardInternal } from './guard.mjs';
|
|
27
32
|
|
|
28
|
-
const API_DOC = 'docs-canonical/API-REFERENCE.md';
|
|
29
|
-
|
|
30
33
|
/**
|
|
31
34
|
* Apply DETERMINISTIC, no-LLM API-surface fixes: remove endpoints documented in
|
|
32
35
|
* API-REFERENCE.md that the OpenAPI spec confirms no longer exist. Removes the
|
|
@@ -38,13 +41,13 @@ const API_DOC = 'docs-canonical/API-REFERENCE.md';
|
|
|
38
41
|
* @returns {{ applied: boolean, removed: Array<{method,path}>, skipped?: string }}
|
|
39
42
|
*/
|
|
40
43
|
export function applyApiSurfaceWrites(projectDir, config, { force = false } = {}) {
|
|
41
|
-
assertDefaultDocWrites(config);
|
|
42
44
|
const drift = computeApiSurfaceDrift(projectDir, config);
|
|
43
45
|
// Only spec-confirmed absences are safe to delete deterministically.
|
|
44
46
|
const removable = drift.confidence === 'spec' ? drift.documentedButAbsent : [];
|
|
45
47
|
if (removable.length === 0) return { applied: false, removed: [] };
|
|
46
48
|
|
|
47
|
-
const
|
|
49
|
+
const apiDoc = docRolePath(config, 'apiReference');
|
|
50
|
+
const apiDocPath = resolveDocRole(projectDir, config, 'apiReference');
|
|
48
51
|
if (!existsSync(apiDocPath)) return { applied: false, removed: [] };
|
|
49
52
|
|
|
50
53
|
const content = readFileSync(apiDocPath, 'utf-8');
|
|
@@ -52,17 +55,18 @@ export function applyApiSurfaceWrites(projectDir, config, { force = false } = {}
|
|
|
52
55
|
return {
|
|
53
56
|
applied: false,
|
|
54
57
|
removed: [],
|
|
55
|
-
skipped: `${
|
|
58
|
+
skipped: `${apiDoc} is not marked '<!-- docguard:generated true -->'. ` +
|
|
56
59
|
`Re-run with --force to edit it, or fix it via an AI agent (/docguard.fix --doc api-reference).`,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
62
|
+
if (isMappedDocPath(config, apiDoc)) assertMappedFullDocumentWrites(projectDir, config, ['apiReference']);
|
|
59
63
|
|
|
60
64
|
const { content: newContent, removed } = removeEndpoints(content, removable);
|
|
61
65
|
if (removed.length === 0 || newContent === content) {
|
|
62
66
|
return { applied: false, removed: [] }; // idempotent no-op
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
safeWrite(apiDocPath, newContent);
|
|
66
70
|
// Map removed keys back to {method,path} for reporting.
|
|
67
71
|
const removedEndpoints = removable.filter(e => removed.includes(`${e.method.toUpperCase()} ${normalizeForKey(e.path)}`));
|
|
68
72
|
return { applied: true, removed: removedEndpoints.length ? removedEndpoints : removable };
|
|
@@ -275,7 +279,6 @@ IMPORTANT: A new contributor should be able to follow this doc and have the proj
|
|
|
275
279
|
* @returns {{ applied: object[], skipped: object[], total: number }}
|
|
276
280
|
*/
|
|
277
281
|
export function applyAllMechanicalFixes(projectDir, config, opts = {}) {
|
|
278
|
-
assertDefaultDocWrites(config);
|
|
279
282
|
const { force = false, forceRedo = false } = opts;
|
|
280
283
|
const guardData = runGuardInternal(projectDir, config);
|
|
281
284
|
const fixes = [];
|
|
@@ -284,7 +287,7 @@ export function applyAllMechanicalFixes(projectDir, config, opts = {}) {
|
|
|
284
287
|
}
|
|
285
288
|
// v0.14-P1: forwarding forceRedo so users with `--force-redo` can override
|
|
286
289
|
// ping-pong suppression for a specific fix they actually want re-applied.
|
|
287
|
-
const { applied, skipped } = applyMechanicalFixes(projectDir, fixes, { force, forceRedo });
|
|
290
|
+
const { applied, skipped } = applyMechanicalFixes(projectDir, fixes, { force, forceRedo, config });
|
|
288
291
|
return { applied, skipped, total: fixes.length };
|
|
289
292
|
}
|
|
290
293
|
|
|
@@ -375,7 +378,6 @@ function runWriteMode(projectDir, config, flags) {
|
|
|
375
378
|
// ── Main Entry ─────────────────────────────────────────────────────────────
|
|
376
379
|
|
|
377
380
|
export function runFix(projectDir, config, flags) {
|
|
378
|
-
if (flags.write) assertDefaultDocWrites(config);
|
|
379
381
|
const isJson = flags.format === 'json';
|
|
380
382
|
const isPrompt = flags.format === 'prompt';
|
|
381
383
|
const autoFix = flags.auto || false;
|