docguard-cli 0.31.0 → 0.33.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/PHILOSOPHY.md +1 -0
- package/README.md +70 -30
- package/cli/commands/ci.mjs +52 -13
- package/cli/commands/guard.mjs +80 -0
- package/cli/commands/hooks.mjs +167 -2
- package/cli/commands/impact.mjs +213 -5
- package/cli/commands/mcp.mjs +195 -53
- package/cli/commands/report.mjs +200 -0
- package/cli/commands/score.mjs +55 -1
- package/cli/docguard.mjs +101 -13
- package/cli/findings.mjs +6 -0
- package/cli/scanners/agent-readability.mjs +6 -1
- package/cli/scanners/semantic-claims.mjs +10 -2
- package/cli/shared-git.mjs +23 -0
- package/cli/validators/architecture.mjs +8 -1
- package/cli/validators/cross-reference.mjs +124 -3
- package/cli/validators/docs-coverage.mjs +5 -0
- package/cli/validators/reference-existence.mjs +172 -18
- package/cli/validators/traceability.mjs +63 -0
- package/cli/writers/baseline.mjs +84 -0
- package/cli/writers/history.mjs +82 -0
- package/cli/writers/junit.mjs +103 -0
- package/docs/commands.md +30 -2
- package/docs/configuration.md +14 -0
- package/docs/faq.md +12 -0
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
- package/package.json +1 -1
- package/schemas/docguard-config.schema.json +5 -0
package/PHILOSOPHY.md
CHANGED
|
@@ -115,6 +115,7 @@ CDD is a practitioner methodology whose patterns align with peer-reviewed resear
|
|
|
115
115
|
- **Generate → validate → evaluate pipeline** — inspired by the AITPG framework (Lopez et al., IEEE TSE 2026): multi-agent generation grounded in standards produces more comprehensive documentation while staying semantically aligned with expert references.
|
|
116
116
|
- **Calibrated quality evaluation** — DocGuard's HIGH/MEDIUM/LOW labels and multi-signal scoring adapt the CJE framework from TRACE (Lopez et al., IEEE TMLCN 2026).
|
|
117
117
|
- **Standards-grounded generation** — each canonical document maps to a relevant standard (arc42, C4, OWASP ASVS, ISO 29119, OpenAPI, 12-Factor App).
|
|
118
|
+
- **Enforcement over instructions** — a 2026 ETH Zurich study (138 repos, 5,694 agent PRs) found the most common style of agent-instruction file *degrades* agent performance; practitioner reports converge on the same conclusion: written rules are routinely ignored, programmatic checks are respected. CDD's answer is to make the docs machine-verified rather than merely machine-readable.
|
|
118
119
|
|
|
119
120
|
> **Lead researcher**: [Martin Manuel Lopez](https://github.com/martinmanuel9) · [ORCID 0009-0002-7652-2385](https://orcid.org/0009-0002-7652-2385), University of Arizona
|
|
120
121
|
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
- [Slash Commands](#-slash-commands)
|
|
42
42
|
- [Examples](#-examples)
|
|
43
43
|
- [Testing](#-testing)
|
|
44
|
+
- [Enterprise Adoption](#-enterprise-adoption)
|
|
44
45
|
- [CI/CD Integration](#%EF%B8%8F-cicd-integration)
|
|
45
46
|
- [What's New](#-whats-new)
|
|
46
47
|
- [File Structure](#-file-structure)
|
|
@@ -62,13 +63,13 @@ DocGuard enforces **Canonical-Driven Development (CDD)** — a methodology where
|
|
|
62
63
|
|
|
63
64
|
DocGuard is an official [GitHub Spec Kit](https://github.com/github/spec-kit) community extension. It validates the artifacts that Spec Kit creates, ensuring your specs stay high-quality throughout the development lifecycle.
|
|
64
65
|
|
|
65
|
-
📖 **[Philosophy](PHILOSOPHY.md)** · 📋 **[CDD Standard](STANDARD.md)** · ⚖️ **[Comparisons](COMPARISONS.md)** · 🗺️ **[Roadmap](ROADMAP.md)**
|
|
66
|
+
📖 **[Philosophy](PHILOSOPHY.md)** · 📋 **[CDD Standard](STANDARD.md)** · ⚖️ **[Comparisons](COMPARISONS.md)** · 🔬 **[Validation](VALIDATION.md)** · 🗺️ **[Roadmap](ROADMAP.md)**
|
|
66
67
|
|
|
67
68
|
### Architecture
|
|
68
69
|
|
|
69
70
|
```mermaid
|
|
70
71
|
graph TD
|
|
71
|
-
CLI["CLI Entry<br/>docguard.mjs"] --> Commands["Commands (
|
|
72
|
+
CLI["CLI Entry<br/>docguard.mjs"] --> Commands["Commands (20)"]
|
|
72
73
|
Commands --> guard["guard"]
|
|
73
74
|
Commands --> generate["generate"]
|
|
74
75
|
Commands --> score["score"]
|
|
@@ -108,6 +109,14 @@ against what the code does, on every commit, with no LLM required. The full
|
|
|
108
109
|
thesis (and the research behind it) lives in [PHILOSOPHY.md](PHILOSOPHY.md);
|
|
109
110
|
recent feature highlights moved [below](#-whats-new).
|
|
110
111
|
|
|
112
|
+
The field data backs the enforcement-over-instructions bet: an ETH Zurich
|
|
113
|
+
study across 138 repos / 5,694 agent PRs found the most popular style of
|
|
114
|
+
agent-instruction file *hurts* agent performance, and practitioners keep
|
|
115
|
+
converging on the same lesson — written rules are routinely ignored;
|
|
116
|
+
programmatic checks are what agents (and humans) actually respect. That is
|
|
117
|
+
exactly the layer DocGuard provides: not another instructions file, but the
|
|
118
|
+
validator suite that makes the instructions and docs verifiably true.
|
|
119
|
+
|
|
111
120
|
---
|
|
112
121
|
|
|
113
122
|
## ⚡ Quick Start
|
|
@@ -247,7 +256,7 @@ This installs DocGuard's slash commands (`/docguard.init`, `/docguard.guard`, `/
|
|
|
247
256
|
|
|
248
257
|
## Usage
|
|
249
258
|
|
|
250
|
-
DocGuard ships **
|
|
259
|
+
DocGuard ships **20 commands** (the "Daily 5" + 15 situational tools, including the zero-install `demo`, the `mcp` server, and the `ci` pipeline gate). Six additional one-shot scaffolders are accessed via `docguard init --with <name>`. Seven v0.19 commands continue to work as deprecation aliases through v0.20.x — see [MIGRATION-v0.20.md](docs-implementation/MIGRATION-v0.20.md).
|
|
251
260
|
|
|
252
261
|
**The Daily 5** — what you'll reach for 95% of the time:
|
|
253
262
|
|
|
@@ -274,7 +283,10 @@ DocGuard ships **18 commands** (the "Daily 5" + 13 situational tools, including
|
|
|
274
283
|
| `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 |
|
|
275
284
|
| `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 |
|
|
276
285
|
| `feedback` | Report likely false positives back to DocGuard — local-first record + a 1-click prefilled, redacted GitHub issue (zero typing) |
|
|
277
|
-
| `mcp` | MCP server
|
|
286
|
+
| `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`) |
|
|
287
|
+
| `report` | Compliance-evidence bundle for audits — guard verdict + CDD score + ALCOA+ attributes + fix history, stamped with git commit and a tamper-evident sha256 integrity hash (`--format json`, `--out <file>`). Evidence, not a gate: always exits 0 |
|
|
288
|
+
| `ci` | Pipeline gate: guard + score in one command — never scaffolds or touches source; its only write is its own `.docguard/history.jsonl` (opt out: `--no-history`). `--threshold <n>` fails below a score, `--fail-on-warning` for strict mode, `--format json` for parsers |
|
|
289
|
+
| `score --trend` | Score trajectory from recorded `ci` runs — sparkline, delta, and the last 10 runs with commit stamps |
|
|
278
290
|
| `memory` | Per-domain accuracy headline (endpoints / entities / env / tech) |
|
|
279
291
|
| `memory --diff` | Drill into which specific claims don't match code |
|
|
280
292
|
| `memory --pack` | Write `.docguard/context-pack.md` — compact, code-truth-stamped session-start context for AI agents |
|
|
@@ -308,6 +320,8 @@ Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with a
|
|
|
308
320
|
| `--quiet` / `-q` | Suppress banner — for hooks, CI loops, scripts | All |
|
|
309
321
|
| `--format json` | Machine-readable output (clean JSON, no ANSI bleed) | guard, score, diff, trace, diagnose, memory, impact, explain |
|
|
310
322
|
| `--format sarif` | SARIF 2.1.0 output — findings as rules/results for GitHub Code Scanning and SARIF dashboards | guard |
|
|
323
|
+
| `--format junit` | JUnit XML output — one testcase per validator, for GitLab CI (`artifacts:reports:junit`), Jenkins, Azure DevOps, CircleCI | guard |
|
|
324
|
+
| `--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 |
|
|
311
325
|
| `--full` | Generate `llms-full.txt` (full doc bodies inlined) instead of the `llms.txt` link index | llms |
|
|
312
326
|
| `--pack` | Write `.docguard/context-pack.md` — agent session-start context | memory |
|
|
313
327
|
| `--sync` | Regenerate the agent-file family (CLAUDE.md, Copilot, Cursor, …) from AGENTS.md; hash-marked, never touches hand-written files without `--force` | agents |
|
|
@@ -325,6 +339,9 @@ Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with a
|
|
|
325
339
|
| `--apply` | Actually run the migration | upgrade |
|
|
326
340
|
| `--pr` | Open a PR with the migration | upgrade |
|
|
327
341
|
| `--reverse <file>` | Reverse traceability (code → docs) | trace |
|
|
342
|
+
| `--no-indirect` | Skip the reverse-import-graph analysis (docs about modules that import a changed file) | impact, diff --since |
|
|
343
|
+
| `--prs` | Open-PR doc-conflict analysis — two PRs impacting the same canonical doc = merge-order risk (needs the `gh` CLI) | impact |
|
|
344
|
+
| `--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 |
|
|
328
345
|
| `--history` | Show fix audit log | fix |
|
|
329
346
|
|
|
330
347
|
### Example Output
|
|
@@ -383,7 +400,7 @@ DocGuard runs **27 automated validators** on every `guard` check. Every one is *
|
|
|
383
400
|
| 17 | **TODO-Tracking** | Untracked TODOs/FIXMEs and skipped tests (skips test files by default) | ✅ On |
|
|
384
401
|
| 18 | **Schema-Sync** | Database models documented in DATA-MODEL.md | ✅ On |
|
|
385
402
|
| 19 | **Spec-Kit** | Spec quality validation (FR-IDs, mandatory sections, phased tasks) | ✅ On |
|
|
386
|
-
| 20 | **Cross-Reference** | Internal markdown links + anchors resolve (with "did you mean?" hints) | ✅ On |
|
|
403
|
+
| 20 | **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 |
|
|
387
404
|
| 21 | **Generated-Staleness** | `source=code` sections match scanner output; `status: draft` doc age | ✅ On |
|
|
388
405
|
| 22 | **Canonical-Sync** | DocGuard's own README count claims match code-truth (DocGuard repo only — N/A elsewhere) | ✅ On |
|
|
389
406
|
| 23 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
|
|
@@ -450,6 +467,19 @@ DocGuard works with **every major AI coding agent**. All canonical docs are plai
|
|
|
450
467
|
| Google Gemini CLI | ✅ | `docguard agents --agent gemini` |
|
|
451
468
|
| Kiro (AWS) | ✅ | — |
|
|
452
469
|
|
|
470
|
+
### Always-on nudge hook (Claude Code)
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
docguard hooks --claude # install (remove: docguard hooks --claude --remove)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Registers a `PostToolUse` hook in the project's `.claude/settings.json`. After the
|
|
477
|
+
agent edits a canonical doc it is nudged to run `docguard guard --changed-only`;
|
|
478
|
+
after it edits a code file the docs reference, it is nudged toward `docguard impact`.
|
|
479
|
+
Merge-safe (only DocGuard's own entry is ever added/removed), throttled to one nudge
|
|
480
|
+
per file per 30 minutes, and the hook runtime can never break a session (errors are
|
|
481
|
+
silent by contract). Explicit opt-in — `init` never installs it for you.
|
|
482
|
+
|
|
453
483
|
---
|
|
454
484
|
|
|
455
485
|
## ⚡ Slash Commands
|
|
@@ -547,6 +577,20 @@ DocGuard runs its own `guard`, `score`, `diff`, `diagnose`, and `badge` commands
|
|
|
547
577
|
|
|
548
578
|
---
|
|
549
579
|
|
|
580
|
+
## 🏢 Enterprise Adoption
|
|
581
|
+
|
|
582
|
+
Everything runs local or in your CI — no SaaS, no data leaving your infra.
|
|
583
|
+
The pieces that matter at company scale:
|
|
584
|
+
|
|
585
|
+
| Need | DocGuard answer |
|
|
586
|
+
|------|-----------------|
|
|
587
|
+
| **Adopt on a legacy repo** without a red pipeline on day one | `guard --update-baseline` freezes existing findings into a committed `.docguard.baseline.json`; only NEW drift gates from then on (suppression always visible) |
|
|
588
|
+
| **Audit trail** for compliance reviews | `docguard report` — commit-stamped evidence bundle (guard verdict, findings by code, CDD score, ALCOA+ data-integrity attributes, fix history) with a tamper-evident sha256 integrity hash |
|
|
589
|
+
| **Every CI system**, not just GitHub | `guard --format sarif` (GitHub Code Scanning) · `--format junit` (GitLab, Jenkins, Azure DevOps, CircleCI) · `--format json` (anything else) |
|
|
590
|
+
| **Trajectory, not snapshots** | `docguard ci` records every run to `.docguard/history.jsonl`; `score --trend` shows the sparkline + delta |
|
|
591
|
+
| **AI agents on the team** | MCP server (stdio or team-shared HTTP) exposes guard/score/explain/verify/report/diagnose as read-only tools; `agents --sync` keeps the whole agent-file family drift-proof |
|
|
592
|
+
| **Data-integrity framing auditors know** | ALCOA+ scoring (FDA 21 CFR Part 11 / EMA Annex 11 vocabulary) built into `score` and `report` |
|
|
593
|
+
|
|
550
594
|
## ⚙️ CI/CD Integration
|
|
551
595
|
|
|
552
596
|
> **Full recipes:** see [`docs-canonical/CI-RECIPES.md`](./docs-canonical/CI-RECIPES.md) for guard, auto-fix (commits mechanical fixes back to PRs), nightly sync, score-on-PR, and pre-commit configs.
|
|
@@ -615,31 +659,27 @@ Two ready-to-use templates ship with the Spec Kit extension and as standalone fi
|
|
|
615
659
|
|
|
616
660
|
## ✨ What's New
|
|
617
661
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
-
|
|
627
|
-
`.docguard.
|
|
628
|
-
- **
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
-
|
|
638
|
-
|
|
639
|
-
- **Headless-aware banner** — `--quiet`, `--format json`, `--write`, and `--changed-only`
|
|
640
|
-
automatically suppress the banner so JSON output stays parse-clean.
|
|
641
|
-
- **npm-pack smoke gate** — every release now extracts the actual tarball and runs the CLI
|
|
642
|
-
end-to-end before publish, catching missing-file regressions.
|
|
662
|
+
Highlights of the current line (v0.29 → v0.33):
|
|
663
|
+
|
|
664
|
+
- **Adoption baseline** — `guard --update-baseline` freezes a legacy repo's existing findings
|
|
665
|
+
into a committed `.docguard.baseline.json`; guard/ci then gate only NEW drift, with suppression
|
|
666
|
+
always visible. Adopt today, burn down at your own pace.
|
|
667
|
+
- **`docguard report`** — commit-stamped compliance-evidence bundle (guard verdict, findings by
|
|
668
|
+
code, CDD score, ALCOA+ attributes, fix history) with a tamper-evident sha256 integrity hash.
|
|
669
|
+
Also exposed as the `docguard_report` MCP tool.
|
|
670
|
+
- **Score history + `score --trend`** — `docguard ci` records every run to
|
|
671
|
+
`.docguard/history.jsonl`; the trend view shows the sparkline and delta over time.
|
|
672
|
+
- **Three machine formats for guard** — `--format json`, `--format sarif` (GitHub Code
|
|
673
|
+
Scanning), and `--format junit` (GitLab, Jenkins, Azure DevOps, CircleCI).
|
|
674
|
+
- **MCP server, stdio + team HTTP** — guard/score/explain/verify/report/diagnose as read-only
|
|
675
|
+
agent tools: `claude mcp add docguard -- npx docguard-cli mcp`.
|
|
676
|
+
- **Agent-file family sync** — `agents --sync` treats AGENTS.md as canonical and regenerates
|
|
677
|
+
CLAUDE.md / `.cursor/rules` / Copilot / Gemini variants with drift-proof source-hash markers.
|
|
678
|
+
- **`verify --semantic` and `verify --instructions`** — extract documented numbers/limits/enums
|
|
679
|
+
as agent verification tasks; audit the agent-instruction files themselves for contradictions
|
|
680
|
+
and stale pointers.
|
|
681
|
+
- **`docguard agent`** — one-shot ordered task graph with pre-filled code-truth, collapsing ~10
|
|
682
|
+
agent round-trips into one call.
|
|
643
683
|
|
|
644
684
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
645
685
|
|
package/cli/commands/ci.mjs
CHANGED
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
* 0 = All pass, score meets threshold
|
|
7
7
|
* 1 = Guard errors or score below threshold
|
|
8
8
|
* 2 = Guard warnings only
|
|
9
|
+
*
|
|
10
|
+
* v0.33: each run appends one line to `.docguard/history.jsonl` (score,
|
|
11
|
+
* grade, commit, guard counts) so `docguard score --trend` can show the
|
|
12
|
+
* trajectory. Opt out with `--no-history`. The append is silent-on-failure —
|
|
13
|
+
* recording history must never fail the pipeline it records.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { c } from '../shared.mjs';
|
|
12
17
|
import { runGuardInternal } from './guard.mjs';
|
|
13
18
|
import { runScoreInternal } from './score.mjs';
|
|
19
|
+
import { appendHistory } from '../writers/history.mjs';
|
|
20
|
+
import { getHeadInfo, isGitRepo } from '../shared-git.mjs';
|
|
14
21
|
|
|
15
22
|
export function runCI(projectDir, config, flags) {
|
|
16
23
|
const threshold = parseInt(flags.threshold || '0', 10);
|
|
@@ -26,12 +33,41 @@ export function runCI(projectDir, config, flags) {
|
|
|
26
33
|
|
|
27
34
|
// ── Run guard (internal — no subprocess) ──
|
|
28
35
|
const guardData = runGuardInternal(projectDir, config);
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
// Severity-aware effective counts (M2): `guard` gates on these, so `ci`
|
|
37
|
+
// must too — a severity=low demotion or severity=high escalation has to
|
|
38
|
+
// produce the same verdict from both commands.
|
|
39
|
+
const hasErrors = guardData.effectiveErrors > 0;
|
|
40
|
+
const hasWarnings = guardData.effectiveWarnings > 0;
|
|
31
41
|
|
|
32
42
|
// ── Get score ──
|
|
33
43
|
const scoreData = runScoreInternal(projectDir, config);
|
|
34
44
|
|
|
45
|
+
// Status reflects EVERY gate, not just guard (L3): a threshold or
|
|
46
|
+
// --fail-on-warning failure exits 1 and must not be recorded as PASS in
|
|
47
|
+
// history or the JSON consumers parse.
|
|
48
|
+
const thresholdMet = threshold <= 0 || scoreData.score >= threshold;
|
|
49
|
+
const status =
|
|
50
|
+
hasErrors || !thresholdMet || (failOnWarning && hasWarnings) ? 'FAIL'
|
|
51
|
+
: hasWarnings ? 'WARN'
|
|
52
|
+
: 'PASS';
|
|
53
|
+
|
|
54
|
+
// ── Record history (unless opted out) ──
|
|
55
|
+
if (!flags.noHistory) {
|
|
56
|
+
const git = isGitRepo(projectDir) ? getHeadInfo(projectDir) : null;
|
|
57
|
+
appendHistory(projectDir, {
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
commit: git ? git.commit.slice(0, 12) : null,
|
|
60
|
+
score: scoreData.score,
|
|
61
|
+
grade: scoreData.grade,
|
|
62
|
+
errors: guardData.errors,
|
|
63
|
+
warnings: guardData.warnings,
|
|
64
|
+
baselineSuppressed: guardData.baselineSuppressed || 0,
|
|
65
|
+
passed: guardData.passed,
|
|
66
|
+
total: guardData.total,
|
|
67
|
+
status,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
// ── Output ──
|
|
36
72
|
if (isJson) {
|
|
37
73
|
const result = {
|
|
@@ -44,14 +80,18 @@ export function runCI(projectDir, config, flags) {
|
|
|
44
80
|
passed: guardData.passed,
|
|
45
81
|
total: guardData.total,
|
|
46
82
|
status: guardData.status,
|
|
83
|
+
baselineSuppressed: guardData.baselineSuppressed || 0,
|
|
47
84
|
validators: guardData.validators.filter(v => v.status !== 'skipped'),
|
|
48
85
|
},
|
|
49
86
|
threshold,
|
|
50
|
-
thresholdMet
|
|
51
|
-
status
|
|
87
|
+
thresholdMet,
|
|
88
|
+
status,
|
|
52
89
|
timestamp: new Date().toISOString(),
|
|
53
90
|
};
|
|
54
|
-
|
|
91
|
+
// Machine output must survive a pipe: stdout.write + natural exit, never
|
|
92
|
+
// console.log + process.exit (>8 KB payloads truncate mid-flush — same
|
|
93
|
+
// class as the guard --format json bug fixed in v0.28).
|
|
94
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
55
95
|
} else {
|
|
56
96
|
// Text output
|
|
57
97
|
const guardStatus = hasErrors
|
|
@@ -61,20 +101,19 @@ export function runCI(projectDir, config, flags) {
|
|
|
61
101
|
: `${c.green}✅ PASS${c.reset}`;
|
|
62
102
|
|
|
63
103
|
console.log(` ${c.bold}Guard:${c.reset} ${guardStatus} (${guardData.passed}/${guardData.total})`);
|
|
104
|
+
if (guardData.baselineSuppressed > 0) {
|
|
105
|
+
console.log(` ${c.dim}📋 ${guardData.baselineSuppressed} pre-existing finding(s) suppressed by the committed baseline${c.reset}`);
|
|
106
|
+
}
|
|
64
107
|
console.log(` ${c.bold}Score:${c.reset} ${scoreData.score}/100 (${scoreData.grade})`);
|
|
65
108
|
|
|
66
109
|
if (threshold > 0) {
|
|
67
|
-
|
|
68
|
-
console.log(` ${c.bold}Threshold:${c.reset} ${met ? `${c.green}✅ ≥${threshold}` : `${c.red}❌ <${threshold}`}${c.reset}`);
|
|
110
|
+
console.log(` ${c.bold}Threshold:${c.reset} ${thresholdMet ? `${c.green}✅ ≥${threshold}` : `${c.red}❌ <${threshold}`}${c.reset}`);
|
|
69
111
|
}
|
|
70
112
|
|
|
71
113
|
console.log('');
|
|
72
114
|
}
|
|
73
115
|
|
|
74
|
-
// Exit code
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (failOnWarning && hasWarnings) process.exit(1);
|
|
78
|
-
if (hasWarnings) process.exit(2);
|
|
79
|
-
process.exit(0);
|
|
116
|
+
// Exit code follows `status` exactly — one derivation, no drift between
|
|
117
|
+
// what history/JSON record and what the pipeline does.
|
|
118
|
+
process.exitCode = status === 'FAIL' ? 1 : status === 'WARN' ? 2 : 0;
|
|
80
119
|
}
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { checkUpgradeStatus } from './upgrade.mjs';
|
|
|
16
16
|
import { changedFilesSince, isGitRepo } from '../shared-git.mjs';
|
|
17
17
|
import { extractSemanticClaims } from '../scanners/semantic-claims.mjs';
|
|
18
18
|
import { toSarif } from '../writers/sarif.mjs';
|
|
19
|
+
import { toJUnit } from '../writers/junit.mjs';
|
|
20
|
+
import { loadBaseline, saveBaseline, fingerprintFinding, BASELINE_FILE } from '../writers/baseline.mjs';
|
|
19
21
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
20
22
|
import { resolve as resolvePath, relative as relativePath } from 'node:path';
|
|
21
23
|
import { fileURLToPath as fp } from 'node:url';
|
|
@@ -393,6 +395,42 @@ export function runGuardInternal(projectDir, config) {
|
|
|
393
395
|
}
|
|
394
396
|
}
|
|
395
397
|
|
|
398
|
+
// ── Adoption baseline (v0.33) ──
|
|
399
|
+
// If the repo committed `.docguard.baseline.json`, findings frozen at
|
|
400
|
+
// adoption time are suppressed BEFORE any tally — so exit codes, severity
|
|
401
|
+
// rollups, json/sarif/junit, ci, and report all gate only NEW drift.
|
|
402
|
+
// Suppression is visible (baselineSuppressed in the payload + a display
|
|
403
|
+
// note), applies only to findings-backed results (legacy string-only
|
|
404
|
+
// errors/warnings can't be fingerprinted), and `--no-baseline`
|
|
405
|
+
// (config.baseline === false) turns it off.
|
|
406
|
+
let baselineSuppressed = 0;
|
|
407
|
+
const baselineMap = config.baseline === false ? null : loadBaseline(projectDir);
|
|
408
|
+
if (baselineMap) {
|
|
409
|
+
// Occurrence budget: each fingerprint suppresses at most its frozen
|
|
410
|
+
// count (H2). Validators run in a fixed order, so consumption is
|
|
411
|
+
// deterministic — the same tree always suppresses the same instances.
|
|
412
|
+
const remaining = new Map(baselineMap);
|
|
413
|
+
for (const r of results) {
|
|
414
|
+
if (!Array.isArray(r.findings) || r.findings.length === 0) continue;
|
|
415
|
+
if (r.errors.length + r.warnings.length !== r.findings.length) continue;
|
|
416
|
+
const kept = r.findings.filter(f => {
|
|
417
|
+
const fp = fingerprintFinding(f);
|
|
418
|
+
const budget = remaining.get(fp) || 0;
|
|
419
|
+
if (budget <= 0) return true;
|
|
420
|
+
remaining.set(fp, budget - 1);
|
|
421
|
+
return false;
|
|
422
|
+
});
|
|
423
|
+
const removed = r.findings.length - kept.length;
|
|
424
|
+
if (removed === 0) continue;
|
|
425
|
+
baselineSuppressed += removed;
|
|
426
|
+
r.findings = kept;
|
|
427
|
+
r.errors = kept.filter(f => f.severity === 'error').map(f => f.message);
|
|
428
|
+
r.warnings = kept.filter(f => f.severity !== 'error').map(f => f.message);
|
|
429
|
+
r.total = r.passed + kept.length;
|
|
430
|
+
Object.assign(r, classifyResult(r));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
396
434
|
const activeResults = results.filter(r => r.status !== 'skipped');
|
|
397
435
|
const totalErrors = activeResults.reduce((sum, r) => sum + r.errors.length, 0);
|
|
398
436
|
const totalWarnings = activeResults.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
@@ -464,6 +502,7 @@ export function runGuardInternal(projectDir, config) {
|
|
|
464
502
|
// things they've marked as high-severity.
|
|
465
503
|
effectiveErrors,
|
|
466
504
|
effectiveWarnings,
|
|
505
|
+
baselineSuppressed,
|
|
467
506
|
coverage,
|
|
468
507
|
semanticClaims,
|
|
469
508
|
validators: results,
|
|
@@ -555,6 +594,30 @@ export function runGuard(projectDir, config, flags) {
|
|
|
555
594
|
console.log(`${c.cyan}⚡ docguard guard --changed-only${c.reset} ${c.dim}(${label})${c.reset}${escalatedNote}\n`);
|
|
556
595
|
}
|
|
557
596
|
|
|
597
|
+
// ── `--update-baseline`: freeze the CURRENT full finding set ──
|
|
598
|
+
// Runs with the baseline disabled so the file captures everything visible
|
|
599
|
+
// today (updating through an active baseline would only ever shrink it).
|
|
600
|
+
if (flags.updateBaseline) {
|
|
601
|
+
// --changed-only rewrites config.validators to the 5-validator lite set;
|
|
602
|
+
// freezing THAT would silently shrink the committed team baseline to a
|
|
603
|
+
// subset (L1). Refuse the combination rather than corrupt the file.
|
|
604
|
+
if (flags.changedOnly) {
|
|
605
|
+
console.error(`${c.red}✗ --update-baseline cannot be combined with --changed-only — the baseline must freeze the FULL validator set, not the pre-commit lite subset.${c.reset}`);
|
|
606
|
+
process.exitCode = 1;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const fullData = runGuardInternal(projectDir, { ...config, baseline: false });
|
|
610
|
+
const n = saveBaseline(projectDir, fullData.findings || []);
|
|
611
|
+
if (flags.format === 'json') {
|
|
612
|
+
process.stdout.write(JSON.stringify({ written: true, file: BASELINE_FILE, fingerprints: n, findings: (fullData.findings || []).length }, null, 2) + '\n');
|
|
613
|
+
} else {
|
|
614
|
+
console.log(`${c.green}✅ Baseline written:${c.reset} ${BASELINE_FILE} (${n} fingerprint(s))`);
|
|
615
|
+
console.log(`${c.dim} Commit it. guard/ci now gate only NEW findings; --no-baseline shows everything.${c.reset}`);
|
|
616
|
+
}
|
|
617
|
+
process.exitCode = 0;
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
558
621
|
const data = runGuardInternal(projectDir, config);
|
|
559
622
|
|
|
560
623
|
// ── SARIF output (2.1.0) ──
|
|
@@ -567,6 +630,17 @@ export function runGuard(projectDir, config, flags) {
|
|
|
567
630
|
return;
|
|
568
631
|
}
|
|
569
632
|
|
|
633
|
+
// ── JUnit XML output ──
|
|
634
|
+
// SARIF is GitHub's language; JUnit is everyone else's (GitLab
|
|
635
|
+
// artifacts:reports:junit, Jenkins junit step, Azure DevOps, CircleCI).
|
|
636
|
+
// Exit-code semantics identical to sarif/json.
|
|
637
|
+
if (flags.format === 'junit') {
|
|
638
|
+
const xml = toJUnit(data);
|
|
639
|
+
process.exitCode = data.effectiveErrors > 0 ? 1 : data.effectiveWarnings > 0 ? 2 : 0;
|
|
640
|
+
process.stdout.write(xml + '\n');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
570
644
|
// ── JSON output ──
|
|
571
645
|
if (flags.format === 'json') {
|
|
572
646
|
// Use severity-aware effective counts for exit code; raw counts stay in the JSON
|
|
@@ -671,6 +745,12 @@ export function runGuard(projectDir, config, flags) {
|
|
|
671
745
|
console.log(` ${c.red}${c.bold}❌ FAIL${c.reset} ${c.red}— ${data.passed}/${data.total} passed, ${data.effectiveErrors} blocking issue(s)${warnSuffix}${c.reset}`);
|
|
672
746
|
}
|
|
673
747
|
|
|
748
|
+
// Baseline suppression is always visible — a gate that hides findings
|
|
749
|
+
// silently is the false-green failure mode this tool exists to prevent.
|
|
750
|
+
if (data.baselineSuppressed > 0) {
|
|
751
|
+
console.log(` ${c.dim}📋 ${data.baselineSuppressed} pre-existing finding(s) suppressed by ${BASELINE_FILE} (--no-baseline to show)${c.reset}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
674
754
|
// ── Next steps — every run ends with a suggested action (v0.27) ──
|
|
675
755
|
// The field-report principle: whenever DocGuard calls out an issue it must
|
|
676
756
|
// suggest what to do next; on a clean run it points at the next workflow step
|
package/cli/commands/hooks.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Creates git hooks that run guard/score before commits.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, writeFileSync, mkdirSync, chmodSync, readFileSync, unlinkSync } from 'node:fs';
|
|
6
|
+
import { existsSync, writeFileSync, mkdirSync, chmodSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
7
7
|
|
|
8
8
|
// v0.16-P3: managed-block markers. Letting users extend the hook with their
|
|
9
9
|
// own commands (data-file guards, lint checks, etc.) without us clobbering
|
|
@@ -54,7 +54,7 @@ function spliceManagedBlock(existing, newBody) {
|
|
|
54
54
|
const bodyNoShebang = newBody.replace(/^#!.*\n/, '');
|
|
55
55
|
return `${before}${BEGIN_MARKER}\n${bodyNoShebang.replace(/\n+$/, '')}\n${END_MARKER}${after}`;
|
|
56
56
|
}
|
|
57
|
-
import { resolve } from 'node:path';
|
|
57
|
+
import { resolve, relative, basename } from 'node:path';
|
|
58
58
|
import { c } from '../shared.mjs';
|
|
59
59
|
import { getHooksDir } from '../shared-git.mjs';
|
|
60
60
|
|
|
@@ -217,6 +217,15 @@ export function runHooks(projectDir, config, flags) {
|
|
|
217
217
|
console.log(`${c.bold}🪝 DocGuard Hooks — ${config.projectName}${c.reset}`);
|
|
218
218
|
console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
|
|
219
219
|
|
|
220
|
+
// ── Claude Code agent nudge: `docguard hooks --claude` ──
|
|
221
|
+
// Separate path from git hooks: it edits .claude/settings.json, needs no
|
|
222
|
+
// git repo, and is explicitly opt-in (writing agent config unasked is a
|
|
223
|
+
// trust break — same class as the ensureSkills READ_ONLY_COMMANDS rule).
|
|
224
|
+
if (flags.claude) {
|
|
225
|
+
installClaudeNudge(projectDir, { remove: !!flags.remove });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
// Resolve the real hooks dir via git — NOT `<projectDir>/.git/hooks`, which
|
|
221
230
|
// is wrong inside a linked worktree (where `.git` is a file, not a dir) and
|
|
222
231
|
// ignores a custom core.hooksPath.
|
|
@@ -337,3 +346,159 @@ export function runHooks(projectDir, config, flags) {
|
|
|
337
346
|
|
|
338
347
|
console.log('');
|
|
339
348
|
}
|
|
349
|
+
|
|
350
|
+
// ── Claude Code agent nudge ─────────────────────────────────────────────────
|
|
351
|
+
//
|
|
352
|
+
// `docguard hooks --claude` registers a PostToolUse hook in the PROJECT's
|
|
353
|
+
// .claude/settings.json. After the agent edits a canonical doc (or a code
|
|
354
|
+
// file the docs reference), the hook nudges it toward the right DocGuard
|
|
355
|
+
// command — the graphify "query-first hook" distribution pattern, pointed at
|
|
356
|
+
// doc integrity instead of graph queries.
|
|
357
|
+
//
|
|
358
|
+
// Trust rules:
|
|
359
|
+
// - Explicit opt-in only (never installed by ensureSkills/init).
|
|
360
|
+
// - Merge-safe: parses the existing settings.json and adds/removes ONLY the
|
|
361
|
+
// entry whose command contains the NUDGE_HOOK_COMMAND marker. A file that
|
|
362
|
+
// doesn't parse is never touched.
|
|
363
|
+
// - The runtime (`docguard nudge-hook`) is throttled and can never break an
|
|
364
|
+
// agent session: any internal error exits 0 with no output.
|
|
365
|
+
|
|
366
|
+
const NUDGE_HOOK_COMMAND = 'docguard nudge-hook';
|
|
367
|
+
const NUDGE_THROTTLE_MS = 30 * 60 * 1000; // one nudge per file per 30 min
|
|
368
|
+
const NUDGE_STATE_PATH = '.docguard/nudge-state.json';
|
|
369
|
+
const NUDGE_CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$/;
|
|
370
|
+
const NUDGE_AGENT_FILES = new Set(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
371
|
+
|
|
372
|
+
function isOurNudgeGroup(group) {
|
|
373
|
+
return Array.isArray(group?.hooks) &&
|
|
374
|
+
group.hooks.some(h => typeof h?.command === 'string' && h.command.includes(NUDGE_HOOK_COMMAND));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function installClaudeNudge(projectDir, { remove = false } = {}) {
|
|
378
|
+
const settingsDir = resolve(projectDir, '.claude');
|
|
379
|
+
const settingsPath = resolve(settingsDir, 'settings.json');
|
|
380
|
+
|
|
381
|
+
let settings = {};
|
|
382
|
+
if (existsSync(settingsPath)) {
|
|
383
|
+
try {
|
|
384
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
385
|
+
} catch {
|
|
386
|
+
console.log(` ${c.red}❌ .claude/settings.json exists but is not valid JSON — refusing to touch it.${c.reset}`);
|
|
387
|
+
console.log(` ${c.dim}Fix the file, then re-run docguard hooks --claude.${c.reset}\n`);
|
|
388
|
+
process.exitCode = 1;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const groups = Array.isArray(settings.hooks?.PostToolUse) ? settings.hooks.PostToolUse : [];
|
|
394
|
+
const present = groups.some(isOurNudgeGroup);
|
|
395
|
+
|
|
396
|
+
if (remove) {
|
|
397
|
+
if (!present) {
|
|
398
|
+
console.log(` ${c.dim}⏭️ No DocGuard nudge hook found in .claude/settings.json — nothing to remove.${c.reset}\n`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
settings.hooks.PostToolUse = groups.filter(g => !isOurNudgeGroup(g));
|
|
402
|
+
if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
|
|
403
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
404
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
405
|
+
console.log(` ${c.yellow}🗑️ Removed the DocGuard nudge hook from .claude/settings.json${c.reset} ${c.dim}(everything else preserved)${c.reset}\n`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (present) {
|
|
410
|
+
console.log(` ${c.green}✅ DocGuard nudge hook already installed${c.reset} ${c.dim}(.claude/settings.json — idempotent)${c.reset}\n`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!settings.hooks) settings.hooks = {};
|
|
415
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
416
|
+
settings.hooks.PostToolUse.push({
|
|
417
|
+
matcher: 'Edit|Write|MultiEdit',
|
|
418
|
+
hooks: [{ type: 'command', command: NUDGE_HOOK_COMMAND }],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
|
|
422
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
423
|
+
console.log(` ${c.green}✅ Installed the DocGuard nudge hook${c.reset} → .claude/settings.json (PostToolUse)`);
|
|
424
|
+
console.log(` ${c.dim}After an agent edits a canonical doc (or code the docs reference), it is${c.reset}`);
|
|
425
|
+
console.log(` ${c.dim}nudged toward docguard guard --changed-only / docguard impact.${c.reset}`);
|
|
426
|
+
console.log(` ${c.dim}Throttled: one nudge per file per 30 minutes. Remove: docguard hooks --claude --remove${c.reset}\n`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Runtime for the PostToolUse hook (`docguard nudge-hook`). Reads the Claude
|
|
431
|
+
* Code hook payload from stdin, classifies the edited file, and emits a
|
|
432
|
+
* `{"decision":"block","reason":…}` nudge on stdout when — and only when —
|
|
433
|
+
* DocGuard has something specific to say. stdout is the machine channel:
|
|
434
|
+
* nothing else may be printed. Errors NEVER propagate (exit 0, silent) — a
|
|
435
|
+
* broken nudge must not degrade the agent session.
|
|
436
|
+
*/
|
|
437
|
+
export function runNudgeHook(projectDir) {
|
|
438
|
+
try {
|
|
439
|
+
const payload = JSON.parse(readFileSync(0, 'utf-8'));
|
|
440
|
+
const toolInput = payload.tool_input || payload.toolInput || {};
|
|
441
|
+
const filePath = toolInput.file_path || toolInput.filePath;
|
|
442
|
+
if (!filePath) return;
|
|
443
|
+
|
|
444
|
+
const rel = relative(projectDir, resolve(projectDir, filePath)).replace(/\\/g, '/');
|
|
445
|
+
if (!rel || rel.startsWith('..')) return;
|
|
446
|
+
|
|
447
|
+
let reason = null;
|
|
448
|
+
const base = basename(rel);
|
|
449
|
+
const isCanonicalDoc =
|
|
450
|
+
(rel.startsWith('docs-canonical/') && rel.endsWith('.md')) ||
|
|
451
|
+
(NUDGE_AGENT_FILES.has(base) && !rel.includes('/'));
|
|
452
|
+
|
|
453
|
+
if (isCanonicalDoc) {
|
|
454
|
+
reason = `DocGuard: you edited ${rel} — a canonical/agent doc that gates this repo. ` +
|
|
455
|
+
`Before finishing, run \`docguard guard --changed-only\` to confirm links, anchors, and count claims still hold.`;
|
|
456
|
+
} else if (NUDGE_CODE_EXT.test(rel)) {
|
|
457
|
+
const referencingDocs = docsReferencingFile(projectDir, base);
|
|
458
|
+
if (referencingDocs.length > 0) {
|
|
459
|
+
reason = `DocGuard: ${referencingDocs.slice(0, 3).join(', ')} reference${referencingDocs.length === 1 ? 's' : ''} ` +
|
|
460
|
+
`\`${base}\` — after your edits, run \`docguard impact\` to see which doc sections to re-read or update.`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (!reason) return;
|
|
464
|
+
|
|
465
|
+
// Throttle per file — iterative edits must not spam the agent.
|
|
466
|
+
const statePath = resolve(projectDir, NUDGE_STATE_PATH);
|
|
467
|
+
let state = {};
|
|
468
|
+
try { state = JSON.parse(readFileSync(statePath, 'utf-8')); } catch { /* first run */ }
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
if (state[rel] && now - state[rel] < NUDGE_THROTTLE_MS) return;
|
|
471
|
+
state[rel] = now;
|
|
472
|
+
try {
|
|
473
|
+
mkdirSync(resolve(projectDir, '.docguard'), { recursive: true });
|
|
474
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
475
|
+
} catch { /* state is best-effort; still nudge */ }
|
|
476
|
+
|
|
477
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
|
|
478
|
+
} catch {
|
|
479
|
+
// Silent by contract.
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Which canonical/agent docs mention this basename? Cheap line scan. */
|
|
484
|
+
function docsReferencingFile(projectDir, base) {
|
|
485
|
+
const docs = [];
|
|
486
|
+
const check = (name, full) => {
|
|
487
|
+
try {
|
|
488
|
+
if (readFileSync(full, 'utf-8').includes(base)) docs.push(name);
|
|
489
|
+
} catch { /* unreadable */ }
|
|
490
|
+
};
|
|
491
|
+
const dir = resolve(projectDir, 'docs-canonical');
|
|
492
|
+
if (existsSync(dir)) {
|
|
493
|
+
try {
|
|
494
|
+
for (const f of readdirSync(dir)) {
|
|
495
|
+
if (f.endsWith('.md')) check(f, resolve(dir, f));
|
|
496
|
+
}
|
|
497
|
+
} catch { /* unreadable dir */ }
|
|
498
|
+
}
|
|
499
|
+
for (const a of NUDGE_AGENT_FILES) {
|
|
500
|
+
const p = resolve(projectDir, a);
|
|
501
|
+
if (existsSync(p)) check(a, p);
|
|
502
|
+
}
|
|
503
|
+
return docs;
|
|
504
|
+
}
|