docguard-cli 0.41.4 → 0.41.6
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/CHANGELOG.md +96 -4
- package/cli/commands/diff.mjs +1 -1
- package/cli/commands/guard.mjs +16 -5
- package/cli/commands/hooks.mjs +19 -2
- package/cli/commands/specs.mjs +8 -4
- package/cli/commands/trace.mjs +1 -1
- package/cli/scanners/api-doc.mjs +1 -1
- package/cli/scanners/spec-registry.mjs +19 -3
- package/cli/shared.mjs +1 -1
- package/cli/validators/cross-reference.mjs +3 -3
- package/cli/validators/diff-suspicion.mjs +1 -1
- package/cli/validators/docs-coverage.mjs +6 -1
- package/cli/validators/environment.mjs +1 -1
- package/cli/validators/freshness.mjs +27 -0
- package/cli/validators/metrics-consistency.mjs +2 -2
- package/cli/validators/test-spec.mjs +1 -1
- package/cli/writers/generate-io.mjs +27 -9
- 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/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +1 -1
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +1 -1
- package/package.json +1 -1
- package/templates/ci/github-actions.yml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,98 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.41.6] - 2026-09-18
|
|
11
|
+
|
|
12
|
+
Automated weekly release — batches everything merged since `v0.41.5`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- test: make this suite probeable by TestGuard, and close the gap it found (#415)
|
|
17
|
+
- fix: do not block projects that never adopted DocGuard; protect hook backups (#414)
|
|
18
|
+
- chore: complete the pass to generic project references (#413)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- TestGuard claim probing for this repository: `testguard.claims.json` plus
|
|
24
|
+
`tools/node-test-json-reporter.mjs`, which translates `node --test` output into
|
|
25
|
+
the Jest-shape JSON TestGuard reads (node:test ships no `json` reporter). The
|
|
26
|
+
first claim covers `backupFile`; probing it found a real gap — one guard was
|
|
27
|
+
masked by another, so its fault survived while every test stayed green — and
|
|
28
|
+
the missing case is now covered.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Completed the pass to generic project references: the earlier sweep matched a
|
|
33
|
+
narrower pattern than the one used to find them, so sixteen mentions survived
|
|
34
|
+
in source comments, tests and changelog prose. Two references are deliberately
|
|
35
|
+
kept: `.docguard-archive.json` records an archived spec path plus the
|
|
36
|
+
`git restore` command that recovers it, and renaming that string would point
|
|
37
|
+
the restore at a path that never existed.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Stop blocking commits in projects that never adopted DocGuard. A Git hook
|
|
42
|
+
lives in the common `.git/hooks` and governs every linked worktree, while
|
|
43
|
+
`.docguard.json` is a branch-local tracked file — during adoption the two
|
|
44
|
+
cannot be consistent, so a hook installed on one branch blocked commits on
|
|
45
|
+
every other branch and worktree, where `guard` exited 1 for missing canonical
|
|
46
|
+
docs. `guard` now exits **3** ("not initialised") instead of 1 when there is
|
|
47
|
+
no `.docguard.json`, and the generated hooks skip enforcement and allow the
|
|
48
|
+
operation. Exit 3 is still non-zero, so a CI gate that fails on any non-zero
|
|
49
|
+
status is unchanged, and an adopted project with real findings still exits 1
|
|
50
|
+
and still blocks. Existing installed hooks do not self-heal — re-run
|
|
51
|
+
`docguard hooks` to pick this up.
|
|
52
|
+
- Stop spending the single backup slot on a no-op. Re-installing a byte-identical
|
|
53
|
+
managed hook wrote a redundant `.bak`; with `--force` a second install
|
|
54
|
+
overwrote the backup holding the user's own original hook, which is
|
|
55
|
+
unrecoverable because a `.git/hooks` file is not in version control. Identical
|
|
56
|
+
writes are now skipped, and a `.bak` DocGuard did not write is preserved
|
|
57
|
+
alongside a timestamped copy rather than replaced.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## [0.41.5] - 2026-09-17
|
|
61
|
+
|
|
62
|
+
Automated weekly release — batches everything merged since `v0.41.4`.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- fix: four tool defects from a downstream field report (#410)
|
|
67
|
+
- chore: use generic project references and stop tracking .wolf/ (#411)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Replaced named third-party project references in source comments, tests and
|
|
73
|
+
changelog history with generic terms. Field-test provenance is still recorded;
|
|
74
|
+
the projects are no longer identified by name. Stopped tracking `.wolf/`
|
|
75
|
+
(local agent working notes, never part of the published package).
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- Stop counting a review stamp as a content change. Adding or updating
|
|
80
|
+
`<!-- docguard:last-reviewed -->` in a spec artifact changed its registry
|
|
81
|
+
digest, so the review that Freshness (FRS002) asks for immediately made the
|
|
82
|
+
spec registry `STALE` and demanded `specs --write` — the tool created the work
|
|
83
|
+
it then reported. That marker is now excluded from the digest source;
|
|
84
|
+
`docguard:version` and `docguard:status` still re-digest, because those are
|
|
85
|
+
substantive lifecycle edits. Registries written by earlier releases keep their
|
|
86
|
+
trust (both digest forms are accepted); a project whose specs already carry a
|
|
87
|
+
review stamp needs one `docguard specs --write` to settle.
|
|
88
|
+
- Stop reporting a document as "review due" while it is staged in the commit
|
|
89
|
+
being checked. A pre-commit hook runs `guard` against the very change being
|
|
90
|
+
made, so an author updating `AGENTS.md` was told to review `AGENTS.md`. Being
|
|
91
|
+
edited in this commit is now treated as the strongest freshness signal and
|
|
92
|
+
outranks the repository-wide commit-count heuristic.
|
|
93
|
+
- Stop leaving `.docguard-specs.json.bak` behind. `specs --write` regenerates a
|
|
94
|
+
Git-tracked, fully derived file, so the backup was noise that DocGuard then
|
|
95
|
+
reported as an undocumented config file (DCV001). DocGuard also no longer
|
|
96
|
+
raises DCV001 for any `.bak` it wrote itself.
|
|
97
|
+
- Report what `specs --write` actually did. It printed the pre-write differences
|
|
98
|
+
under a "Differences:" heading after reporting `WRITTEN`, reading as though the
|
|
99
|
+
drift were still outstanding; resolved differences are now labelled as resolved.
|
|
100
|
+
|
|
101
|
+
|
|
10
102
|
## [0.41.4] - 2026-09-17
|
|
11
103
|
|
|
12
104
|
Automated weekly release — batches everything merged since `v0.41.3`.
|
|
@@ -1233,7 +1325,7 @@ Context7 identified in the platform review).
|
|
|
1233
1325
|
`website/docs/`) are now claim-scanned and counted as "tracked" **without being
|
|
1234
1326
|
enrolled** in `requiredFiles.canonical`. A folder literally named `documentation/`
|
|
1235
1327
|
is unambiguously a doc home DocGuard governs; this stays distinct from the
|
|
1236
|
-
arbitrary-subdir walk the
|
|
1328
|
+
arbitrary-subdir walk the downstream-project scoping fix removed (a number buried
|
|
1237
1329
|
in `security/wolf-archive/` is still never scanned). `config.docs.dirs` EXTENDS
|
|
1238
1330
|
the set with non-standard homes (it never replaces auto-detection); use
|
|
1239
1331
|
`.docguardignore` to exclude a conventional dir. The doc-home set is now a single
|
|
@@ -1401,7 +1493,7 @@ dogfooding.
|
|
|
1401
1493
|
## [0.27.0] - 2026-06-19
|
|
1402
1494
|
|
|
1403
1495
|
Acting on a third end-to-end LLM field report (a coding agent ran DocGuard on a
|
|
1404
|
-
Vite+Vitest
|
|
1496
|
+
Vite+Vitest downstream repo). The headline is architectural: DocGuard is a
|
|
1405
1497
|
tool *for LLMs*, so every run should end with a suggested next action and every
|
|
1406
1498
|
finding it surfaces should be addressable, suppressible, and — when uncertain —
|
|
1407
1499
|
reportable. This release introduces structured **findings** (stable codes +
|
|
@@ -1767,7 +1859,7 @@ Python, and AWS/AppSync projects.
|
|
|
1767
1859
|
longer scans `commands/docguard.*.md` (DocGuard's slash-command docs, which it
|
|
1768
1860
|
installs into the project) for count claims. A stale "N validators" baked into
|
|
1769
1861
|
those shipped docs was being reported as the *user's* drift in every project
|
|
1770
|
-
that had them (field test:
|
|
1862
|
+
that had them (field test: a downstream project, a downstream project). A user's own
|
|
1771
1863
|
`commands/<name>.md` is unaffected.
|
|
1772
1864
|
- **Dogfooding closure** — Canonical-Sync now scans **AGENTS.md** in addition to
|
|
1773
1865
|
README for "ships N commands"/"N validators" surface claims (it only checked
|
|
@@ -2691,7 +2783,7 @@ and adds a cross-cutting "no validator throws" safety net. **22 validators,
|
|
|
2691
2783
|
### Fixed
|
|
2692
2784
|
|
|
2693
2785
|
- **B-5: Freshness validator crashed with `getLastCommitDate is not defined`.** A an enterprise client project install of v0.13.0 produced this ReferenceError despite all the imports being correct in source — we couldn't reproduce locally, but the user's report was clear. Fix: defensive dynamic import in `freshness.mjs` that falls back to the pre-v0.13 inline implementation if `../shared-git.mjs` ever fails to load. Worst-case behavior is now "rename detection silently disabled" instead of "validator crashes with useless message". Also added an inline fallback for the same defensive layering. Reported by an enterprise client project.
|
|
2694
|
-
- **B-6: Cross-Reference didn't URL-decode link target paths.** A markdown link like `[name](../
|
|
2786
|
+
- **B-6: Cross-Reference didn't URL-decode link target paths.** A markdown link like `[name](../Client%20Documentation/foo.md)` (where the directory has a space) was looked up with `existsSync('../Client%20Documentation/foo.md')` literally — the filesystem stores the decoded form. Now: `resolveTarget` tries BOTH the literal path (for paths that legitimately contain `%`) and the URL-decoded form. **Effect on an enterprise client project: Cross-Reference went from 28/28 to 101/101 checks — 73 previously-broken refs now resolve correctly.** Reported by an enterprise client project.
|
|
2695
2787
|
- **Cross-cutting safety net**: new `tests/guard-no-throw.test.mjs` runs guard against a fixture repo and asserts no validator leaks a ReferenceError / TypeError / "is not defined" / "is not a function" / "Cannot read properties of undefined" pattern into user-facing output. Found a *second* lurking bug while writing the test: Structure validator threw `Cannot read properties of undefined (reading 'some')` when `config.requiredFiles.agentFile` was missing — fixed with defensive array-or-string coercion + skip-when-missing for `changelog` too. This safety net runs in CI, catching the entire class of developer-error-leaks before release.
|
|
2696
2788
|
|
|
2697
2789
|
### Added
|
package/cli/commands/diff.mjs
CHANGED
|
@@ -203,7 +203,7 @@ export function diffEntities(dir, config = {}) {
|
|
|
203
203
|
|
|
204
204
|
// v0.16-P4 (revised in v0.17.1): conservative denylist of system env vars
|
|
205
205
|
// that appear in prose ("the venv `PATH`") but are never user-set app env
|
|
206
|
-
// vars. v0.17.1-B7: trimmed to TRULY-system-only after
|
|
206
|
+
// vars. v0.17.1-B7: trimmed to TRULY-system-only after downstream feedback —
|
|
207
207
|
// NODE_ENV / CI / GITHUB_* are legitimately app env vars when read via
|
|
208
208
|
// process.env. Including them caused diff to falsely flag `NODE_ENV` as
|
|
209
209
|
// "in code but not docs" even when ENVIRONMENT.md documented it.
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -224,7 +224,7 @@ function renderableItems(v) {
|
|
|
224
224
|
// .md could drift forever while guard stayed green — the human had to REMEMBER to
|
|
225
225
|
// enroll each doc, which is exactly the step that fails silently. We deliberately
|
|
226
226
|
// do NOT deep-scan every doc for claims (that floods false positives — see the
|
|
227
|
-
//
|
|
227
|
+
// downstream-project scar in metrics-consistency). Instead we cheaply report what's
|
|
228
228
|
// under a validation tier and what isn't, turning silent non-coverage into a
|
|
229
229
|
// visible nudge. Pure visibility — never gates the build.
|
|
230
230
|
//
|
|
@@ -664,12 +664,23 @@ export function runGuard(projectDir, config, flags) {
|
|
|
664
664
|
|
|
665
665
|
const data = runGuardInternal(projectDir, config);
|
|
666
666
|
|
|
667
|
+
// A project with no .docguard.json has not adopted DocGuard. Its "failures"
|
|
668
|
+
// are missing canonical docs, which is not the same fact as "this project
|
|
669
|
+
// failed its checks" — and consumers need to tell them apart. Exit 3 says
|
|
670
|
+
// "not initialised": still non-zero, so a CI gate that fails on any non-zero
|
|
671
|
+
// status is unchanged, but the generated Git hook can let the commit through
|
|
672
|
+
// instead of blocking a project that never adopted the tool.
|
|
673
|
+
const uninitialised = !existsSync(resolvePath(projectDir, '.docguard.json'));
|
|
674
|
+
const exitFor = d => (d.effectiveErrors > 0
|
|
675
|
+
? (uninitialised ? 3 : 1)
|
|
676
|
+
: d.effectiveWarnings > 0 ? 2 : 0);
|
|
677
|
+
|
|
667
678
|
// ── SARIF output (2.1.0) ──
|
|
668
679
|
// Same flush discipline as the JSON branch below (bug-105): set exitCode and
|
|
669
680
|
// write+return so a piped consumer never gets a truncated payload.
|
|
670
681
|
if (flags.format === 'sarif') {
|
|
671
682
|
const sarif = toSarif(data, { projectDir });
|
|
672
|
-
process.exitCode = data
|
|
683
|
+
process.exitCode = exitFor(data);
|
|
673
684
|
process.stdout.write(JSON.stringify(sarif, null, 2) + '\n');
|
|
674
685
|
return;
|
|
675
686
|
}
|
|
@@ -680,7 +691,7 @@ export function runGuard(projectDir, config, flags) {
|
|
|
680
691
|
// Exit-code semantics identical to sarif/json.
|
|
681
692
|
if (flags.format === 'junit') {
|
|
682
693
|
const xml = toJUnit(data);
|
|
683
|
-
process.exitCode = data
|
|
694
|
+
process.exitCode = exitFor(data);
|
|
684
695
|
process.stdout.write(xml + '\n');
|
|
685
696
|
return;
|
|
686
697
|
}
|
|
@@ -689,7 +700,7 @@ export function runGuard(projectDir, config, flags) {
|
|
|
689
700
|
if (flags.format === 'json') {
|
|
690
701
|
// Use severity-aware effective counts for exit code; raw counts stay in the JSON
|
|
691
702
|
// for display tools that want to show the full picture.
|
|
692
|
-
const code = data
|
|
703
|
+
const code = exitFor(data);
|
|
693
704
|
// v0.28: set exitCode + return instead of process.exit(). A large JSON
|
|
694
705
|
// payload (>~8 KB) written to a PIPE flushes asynchronously; an immediate
|
|
695
706
|
// process.exit() truncates it mid-string, so a CI consumer parsing stdout
|
|
@@ -991,5 +1002,5 @@ export function runGuard(projectDir, config, flags) {
|
|
|
991
1002
|
// v0.28: exitCode + return (not process.exit) so the buffered text output
|
|
992
1003
|
// flushes to a pipe before the process exits — same truncation fix as the
|
|
993
1004
|
// JSON path above.
|
|
994
|
-
process.exitCode = data
|
|
1005
|
+
process.exitCode = exitFor(data);
|
|
995
1006
|
}
|
package/cli/commands/hooks.mjs
CHANGED
|
@@ -105,6 +105,17 @@ import { listCanonicalDocs } from '../shared-ignore.mjs';
|
|
|
105
105
|
|
|
106
106
|
// Git enforcement is offline and fail-closed; agent nudges stay best-effort.
|
|
107
107
|
const ENFORCEMENT_RUNTIME = `
|
|
108
|
+
# A Git hook is repo-wide (it lives in the common .git/hooks and is shared by
|
|
109
|
+
# every linked worktree), but .docguard.json is a branch-local tracked file.
|
|
110
|
+
# During adoption the two cannot be consistent: the hook is already active
|
|
111
|
+
# everywhere while the config exists on one branch only. A project that has not
|
|
112
|
+
# adopted DocGuard must not be blocked by a hook installed from another branch.
|
|
113
|
+
if [ ! -f ".docguard.json" ]; then
|
|
114
|
+
echo "DocGuard: not initialised here (no .docguard.json) — skipping."
|
|
115
|
+
echo " Adopt it with: docguard init"
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
108
119
|
if ! command -v node >/dev/null 2>&1; then
|
|
109
120
|
echo "❌ Node.js runtime not found — operation blocked" >&2
|
|
110
121
|
exit 1
|
|
@@ -136,7 +147,10 @@ ${ENFORCEMENT_RUNTIME}
|
|
|
136
147
|
"$DOCGUARD" guard
|
|
137
148
|
EXIT_CODE=$?
|
|
138
149
|
|
|
139
|
-
if [ "$EXIT_CODE" -
|
|
150
|
+
if [ "$EXIT_CODE" -eq 3 ]; then
|
|
151
|
+
echo ""
|
|
152
|
+
echo "DocGuard: project not initialised — commit allowed"
|
|
153
|
+
elif [ "$EXIT_CODE" -ne 0 ] && [ "$EXIT_CODE" -ne 2 ]; then
|
|
140
154
|
echo ""
|
|
141
155
|
echo "❌ DocGuard guard FAILED — commit blocked"
|
|
142
156
|
echo " Fix the errors above, then try again."
|
|
@@ -265,7 +279,10 @@ fi
|
|
|
265
279
|
"$DOCGUARD" guard
|
|
266
280
|
EXIT_CODE=$?
|
|
267
281
|
|
|
268
|
-
if [ "$EXIT_CODE" -
|
|
282
|
+
if [ "$EXIT_CODE" -eq 3 ]; then
|
|
283
|
+
echo ""
|
|
284
|
+
echo "DocGuard: project not initialised — commit allowed"
|
|
285
|
+
elif [ "$EXIT_CODE" -ne 0 ] && [ "$EXIT_CODE" -ne 2 ]; then
|
|
269
286
|
echo ""
|
|
270
287
|
echo "❌ DocGuard guard FAILED — commit blocked."
|
|
271
288
|
echo " Remaining issues need an AI agent (content rewrites, not mechanical):"
|
package/cli/commands/specs.mjs
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
|
-
import { safeWrite } from '../writers/generate-io.mjs';
|
|
13
12
|
import { commitFileTransaction } from '../writers/file-transaction.mjs';
|
|
14
13
|
import { appendImplementationOutcome } from '../writers/spec-outcomes.mjs';
|
|
15
14
|
import { serializeLifecycleContext } from '../scanners/lifecycle-context.mjs';
|
|
@@ -227,7 +226,9 @@ function printResult(result) {
|
|
|
227
226
|
console.log(`Registry: ${SPEC_REGISTRY_PATH}`);
|
|
228
227
|
console.log(`Specs: ${result.specs}; tombstones: ${result.tombstones}`);
|
|
229
228
|
if (result.differences?.length) {
|
|
230
|
-
|
|
229
|
+
// After --write the registry already matches; listing the pre-write diff
|
|
230
|
+
// under "Differences" read as though the drift were still outstanding.
|
|
231
|
+
console.log(result.status === 'WRITTEN' ? 'Resolved by this write:' : 'Differences:');
|
|
231
232
|
for (const difference of result.differences) {
|
|
232
233
|
console.log(` ${difference.path}: ${difference.message}`);
|
|
233
234
|
}
|
|
@@ -264,7 +265,10 @@ export function runSpecs(projectDir, config, flags = {}) {
|
|
|
264
265
|
}
|
|
265
266
|
let status = projection.current ? 'CURRENT' : projection.exists ? 'STALE' : 'MISSING';
|
|
266
267
|
if (flags.write && !projection.current) {
|
|
267
|
-
safeWrite
|
|
268
|
+
// Deliberately not safeWrite: that keeps a .bak, and the registry is
|
|
269
|
+
// generated and Git-tracked, so the backup is noise DocGuard then reports
|
|
270
|
+
// as an undocumented config file.
|
|
271
|
+
writeFileSync(resolve(projectDir, SPEC_REGISTRY_PATH), projection.serialized, 'utf-8');
|
|
268
272
|
status = 'WRITTEN';
|
|
269
273
|
}
|
|
270
274
|
const result = {
|
package/cli/commands/trace.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const CODE_EXTENSIONS = new Set([
|
|
|
26
26
|
|
|
27
27
|
// v0.16-P2: language-aware patterns. The original JS/TS-only sets created
|
|
28
28
|
// false-negative warnings on Python/Rust/Go/Java projects (reported by the
|
|
29
|
-
//
|
|
29
|
+
// a downstream project Python user: TEST-SPEC.md was flagged unlinked even
|
|
30
30
|
// though Python tests existed because `.test.mjs` didn't match `test_*.py`).
|
|
31
31
|
import { TEST_PATTERNS, TRACE_MAP, isTraceableSource } from '../shared-trace-patterns.mjs';
|
|
32
32
|
|
package/cli/scanners/api-doc.mjs
CHANGED
|
@@ -32,7 +32,7 @@ const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', '
|
|
|
32
32
|
* optional c-all `/shop/[[...filters]]` → `/shop/{}`
|
|
33
33
|
* Without the bracket rule, a doc written in Next.js `[id]` syntax never matched
|
|
34
34
|
* the code-scan's `:id`, so every dynamic route double-fired as both
|
|
35
|
-
* "documented-but-absent" and "undocumented" (field test:
|
|
35
|
+
* "documented-but-absent" and "undocumented" (field test: a downstream project).
|
|
36
36
|
* @param {string} raw
|
|
37
37
|
* @returns {string} normalized path (e.g. "/api/users/{}") or '' if not a path
|
|
38
38
|
*/
|
|
@@ -33,6 +33,22 @@ const STOP_TERMS = new Set(['must', 'should', 'with', 'from', 'that', 'this', 'h
|
|
|
33
33
|
|
|
34
34
|
const posix = path => path.split(sep).join('/').replace(/^\.\//, '');
|
|
35
35
|
const digest = content => `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
36
|
+
|
|
37
|
+
// A review stamp is not a content change. `docguard:last-reviewed` is written by
|
|
38
|
+
// reviewing a document — the very thing Freshness (FRS002) asks for — so letting
|
|
39
|
+
// it reach the digest made every review a registry change: review the doc, then
|
|
40
|
+
// `specs --write` to settle the registry the review just invalidated. Strip only
|
|
41
|
+
// that marker; `docguard:version` and `docguard:status` are substantive lifecycle
|
|
42
|
+
// edits and must keep re-digesting.
|
|
43
|
+
const REVIEW_STAMP_RE = /^[ \t]*<!--[ \t]*docguard:last-reviewed[^>]*-->[ \t]*\r?\n?/gim;
|
|
44
|
+
export const digestSource = content => content.replace(REVIEW_STAMP_RE, '');
|
|
45
|
+
const contentDigest = content => digest(digestSource(content));
|
|
46
|
+
|
|
47
|
+
// Registries written before the review stamp was excluded carry a digest over the
|
|
48
|
+
// raw bytes. Accept either so upgrading does not silently drop trust in an
|
|
49
|
+
// otherwise untouched spec; the next `specs --write` migrates the entry.
|
|
50
|
+
const digestMatches = (recorded, content) =>
|
|
51
|
+
recorded === contentDigest(content) || recorded === digest(content);
|
|
36
52
|
const sortedUnique = values => [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
37
53
|
|
|
38
54
|
function registryDifferences(left, right) {
|
|
@@ -281,7 +297,7 @@ export function trustedSpecLifecycleIndex(projectDir) {
|
|
|
281
297
|
try { content = readFileSync(resolve(projectDir, entry.path), 'utf8'); } catch { continue; }
|
|
282
298
|
if (parseSpecId(content) !== entry.specId) continue;
|
|
283
299
|
const artifact = entry.observed?.artifacts?.find(item => item.path === entry.path);
|
|
284
|
-
if (!artifact || artifact.digest
|
|
300
|
+
if (!artifact || !digestMatches(artifact.digest, content)) continue;
|
|
285
301
|
if (entry.reviewed?.lifecycle?.context !== 'current' || entry.reviewed.lifecycle.storage !== 'working_tree') continue;
|
|
286
302
|
trusted.set(`${entry.specId}\0${entry.path}`, entry.reviewed.lifecycle);
|
|
287
303
|
}
|
|
@@ -307,7 +323,7 @@ export function uncommittedPlannedSpecLifecycleIndex(projectDir) {
|
|
|
307
323
|
try { content = readFileSync(resolve(projectDir, entry.path), 'utf8'); } catch { continue; }
|
|
308
324
|
if (parseSpecId(content) !== entry.specId) continue;
|
|
309
325
|
const artifact = entry.observed?.artifacts?.find(item => item.path === entry.path);
|
|
310
|
-
if (!artifact || artifact.digest
|
|
326
|
+
if (!artifact || !digestMatches(artifact.digest, content)) continue;
|
|
311
327
|
if (entry.reviewed.lifecycle.context !== 'current' || entry.reviewed.lifecycle.storage !== 'working_tree') continue;
|
|
312
328
|
candidates.set(`${entry.specId}\0${entry.path}`, entry.reviewed.lifecycle);
|
|
313
329
|
}
|
|
@@ -494,7 +510,7 @@ export function projectSpecRegistry(projectDir, config = {}, options = {}) {
|
|
|
494
510
|
const artifacts = artifactsForFeature
|
|
495
511
|
.map(artifact => ({
|
|
496
512
|
path: posix(relative(projectDir, artifact)),
|
|
497
|
-
digest:
|
|
513
|
+
digest: contentDigest(readFileSync(artifact, 'utf8')),
|
|
498
514
|
}));
|
|
499
515
|
specs.push({
|
|
500
516
|
specId,
|
package/cli/shared.mjs
CHANGED
|
@@ -289,7 +289,7 @@ import { resolve, relative } from 'node:path';
|
|
|
289
289
|
* Conventional documentation-home directory names. A folder named one of these
|
|
290
290
|
* is unambiguously "docs DocGuard governs" — distinct from arbitrary markdown
|
|
291
291
|
* buried in a non-doc subdir (security/wolf-archive/, vendored toolkits), which
|
|
292
|
-
* the
|
|
292
|
+
* the downstream-project scoping fix deliberately excludes. We auto-track the
|
|
293
293
|
* former and never blanket-walk the latter.
|
|
294
294
|
*/
|
|
295
295
|
export const DEFAULT_DOC_DIRS = [
|
|
@@ -295,9 +295,9 @@ function editDistance(a, b) {
|
|
|
295
295
|
function resolveTarget(sourcePath, targetRel, projectDir) {
|
|
296
296
|
if (!targetRel) return null;
|
|
297
297
|
// B-6: try BOTH the literal path and the URL-decoded form. CommonMark
|
|
298
|
-
// accepts `[name](../
|
|
299
|
-
// and the decoded form (`../
|
|
300
|
-
// filesystem. The angle-bracket form `<../
|
|
298
|
+
// accepts `[name](../Client%20Documentation/foo.md)` for paths with spaces,
|
|
299
|
+
// and the decoded form (`../Client Documentation/foo.md`) is what hits the
|
|
300
|
+
// filesystem. The angle-bracket form `<../Client Documentation/foo.md>` is
|
|
301
301
|
// already non-URL-encoded by the time it reaches us. Try literal first
|
|
302
302
|
// (handles paths that legitimately contain `%`), then decoded.
|
|
303
303
|
const candidates = [targetRel];
|
|
@@ -47,7 +47,7 @@ const GENERIC_TOKENS = new Set([
|
|
|
47
47
|
'font', 'color', 'colors', 'tracking', 'surface', 'auto', 'full', 'next',
|
|
48
48
|
'body', 'sans', 'blue', 'accent', 'size', 'spacing', 'margin', 'padding',
|
|
49
49
|
'width', 'height', 'flex', 'grid', 'bold', 'bg', 'rounded',
|
|
50
|
-
// HTTP / REST / handler plumbing — generic across any API route (
|
|
50
|
+
// HTTP / REST / handler plumbing — generic across any API route (a downstream project
|
|
51
51
|
// corpus: a route-inventory doc + heavy rewrites flooded findings with these)
|
|
52
52
|
'code', 'json', 'err', 'error', 'message', 'auth', 'get', 'put', 'post',
|
|
53
53
|
'patch', 'delete', 'req', 'res', 'route', 'routes', 'handler', 'endpoint',
|
|
@@ -56,10 +56,15 @@ const COMMON_DOTFILES = new Set([
|
|
|
56
56
|
// Generated tool artifacts (caches, coverage data, lock-data) that land at the
|
|
57
57
|
// repo root but are NOT configuration a human authors or documents. Treating
|
|
58
58
|
// them as "undocumented config files" is a false positive (field test:
|
|
59
|
-
//
|
|
59
|
+
// a downstream project flagged pytest's `.coverage` SQLite data file). Matched by
|
|
60
60
|
// exact name OR prefix (`.coverage.<host>.<pid>` is coverage.py's parallel form).
|
|
61
61
|
const GENERATED_DOTFILE_PREFIXES = ['.coverage', '.eslintcache', '.stylelintcache', '.tsbuildinfo'];
|
|
62
62
|
function isGeneratedArtifact(name) {
|
|
63
|
+
// `.bak` is written by DocGuard's own safeWrite before it overwrites a file.
|
|
64
|
+
// Reporting it as an undocumented config file made the tool flag its own
|
|
65
|
+
// backup (field report: `specs --write` -> DCV001 on
|
|
66
|
+
// `.docguard-specs.json.bak`).
|
|
67
|
+
if (name.endsWith('.bak')) return true;
|
|
63
68
|
return GENERATED_DOTFILE_PREFIXES.some(p => name === p || name.startsWith(p + '.'));
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -139,7 +139,7 @@ export function validateEnvironment(projectDir, config) {
|
|
|
139
139
|
const varRe = /`([A-Z][A-Z0-9_]*[A-Z0-9])`/g;
|
|
140
140
|
// v0.16-P4 (revised in v0.17.1-B7): skip backticked SYSTEM env vars
|
|
141
141
|
// (PATH, HOME, USER, etc.) that appear in ENVIRONMENT.md prose. Trimmed
|
|
142
|
-
// to TRULY-system-only after
|
|
142
|
+
// to TRULY-system-only after downstream feedback — NODE_ENV / CI / GITHUB_* were
|
|
143
143
|
// causing asymmetric flagging between diff and this validator. Apps
|
|
144
144
|
// legitimately treat NODE_ENV as app config; keep the list to vars that
|
|
145
145
|
// no sane application would read as runtime config.
|
|
@@ -188,6 +188,24 @@ function getCodeHistory(dir, ignored) {
|
|
|
188
188
|
}).filter(commit => commit.paths.length);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Documents staged in the commit currently being checked.
|
|
193
|
+
*
|
|
194
|
+
* A pre-commit hook runs guard against the very change being made, so a doc the
|
|
195
|
+
* author is updating right now was still being reported "review due" — the work
|
|
196
|
+
* the finding asks for is in the index (field report: AGENTS.md flagged while
|
|
197
|
+
* staged). Returns POSIX-relative paths; empty when git is unavailable.
|
|
198
|
+
*/
|
|
199
|
+
function getStagedPaths(dir) {
|
|
200
|
+
try {
|
|
201
|
+
const out = execFileSync('git', ['diff', '--cached', '--name-only', '-z'],
|
|
202
|
+
{ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
203
|
+
return new Set(out.split('\0').filter(Boolean));
|
|
204
|
+
} catch {
|
|
205
|
+
return new Set();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
191
209
|
/**
|
|
192
210
|
* Check if git is available in this project.
|
|
193
211
|
*/
|
|
@@ -256,9 +274,18 @@ export function validateFreshness(dir, config) {
|
|
|
256
274
|
const REVIEW_THRESHOLD_DAYS = 30; // Repository-wide trigger, not proof of drift
|
|
257
275
|
const WARNING_THRESHOLD_COMMITS = 10; // Repository-wide review trigger
|
|
258
276
|
|
|
277
|
+
const stagedPaths = getStagedPaths(dir);
|
|
278
|
+
|
|
259
279
|
for (const docFile of docFiles) {
|
|
260
280
|
const docPath = resolve(dir, docFile);
|
|
261
281
|
if (!existsSync(docPath)) continue;
|
|
282
|
+
// Being edited in this very commit is the strongest possible freshness
|
|
283
|
+
// signal; a repository-wide commit count cannot override it.
|
|
284
|
+
if (stagedPaths.has(relPosix(dir, docPath))) {
|
|
285
|
+
results.push({ status: 'skip', doc: docFile,
|
|
286
|
+
message: `${docFile} is staged in this change — review not due; the document is being updated now` });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
262
289
|
const docStatus = readDocStatus(docPath);
|
|
263
290
|
if (['historical', 'superseded', 'deprecated'].includes(docStatus)) {
|
|
264
291
|
results.push({ status: 'skip', doc: docFile,
|
|
@@ -308,7 +308,7 @@ function findMarkdownFiles(dir, config = {}) {
|
|
|
308
308
|
// The old code recursively walked the WHOLE repo from the root, so it swept in
|
|
309
309
|
// OpenWolf session archives (security/wolf-archive/**/memory.md) and vendored
|
|
310
310
|
// toolkit READMEs whose unrelated "N checks" prose was then reported as the
|
|
311
|
-
// USER's drift (field test:
|
|
311
|
+
// USER's drift (field test: downstream-project, ~39 false warnings the author
|
|
312
312
|
// could not act on). Scoping to the docs DocGuard actually governs fixes it.
|
|
313
313
|
try {
|
|
314
314
|
for (const entry of readdirSync(dir)) {
|
|
@@ -323,7 +323,7 @@ function findMarkdownFiles(dir, config = {}) {
|
|
|
323
323
|
// conventional doc dirs (docs/, documentation/, guides/, …) or honors an
|
|
324
324
|
// explicit config.docs.dirs. NAMED dirs only — code/tooling dirs (security/,
|
|
325
325
|
// backend/, src/, …) and arbitrary subdirs are still NEVER walked (the
|
|
326
|
-
//
|
|
326
|
+
// downstream-project false-positive flood the scoping fix removed).
|
|
327
327
|
const canonical = config && config.requiredFiles && Array.isArray(config.requiredFiles.canonical)
|
|
328
328
|
? config.requiredFiles.canonical : [];
|
|
329
329
|
for (const rel of canonical) {
|
|
@@ -78,7 +78,7 @@ export function validateTestSpec(projectDir, config) {
|
|
|
78
78
|
// different shapes (Controllers, Services, an "Integration Tests" inventory
|
|
79
79
|
// like `| test-file | what it covers |`). Without this guard a prose
|
|
80
80
|
// "what it covers" cell — "Health endpoint with real dependencies" — gets
|
|
81
|
-
// checked as a missing test file (false positive; field test:
|
|
81
|
+
// checked as a missing test file (false positive; field test: downstream-project).
|
|
82
82
|
const isPathLike = (v) => !!v && !/\s/.test(v) && (/[\\/]/.test(v) || /\.[A-Za-z0-9]{1,6}$/.test(v));
|
|
83
83
|
|
|
84
84
|
for (const row of pipeRows.slice(1)) { // skip the header row
|
|
@@ -14,15 +14,33 @@ import { resolve, dirname } from 'node:path';
|
|
|
14
14
|
* Create a .bak backup of an existing file before --force overwrites it.
|
|
15
15
|
* Only backs up if the file exists and has content.
|
|
16
16
|
*/
|
|
17
|
-
export function backupFile(filePath) {
|
|
18
|
-
if (existsSync(filePath))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
export function backupFile(filePath, nextContent = null) {
|
|
18
|
+
if (!existsSync(filePath)) return;
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
21
|
+
if (content.trim().length === 0) return;
|
|
22
|
+
|
|
23
|
+
// Nothing to preserve when the file is already what we are about to write.
|
|
24
|
+
// Backing it up anyway produced a redundant .bak and, worse, consumed the
|
|
25
|
+
// single backup slot that may still hold the user's own original.
|
|
26
|
+
if (nextContent !== null && content === nextContent) return;
|
|
27
|
+
|
|
28
|
+
// The backup slot is single and unversioned. Overwriting it destroys the
|
|
29
|
+
// previous backup, which on a second forced install is the user's ORIGINAL
|
|
30
|
+
// file — unrecoverable, because a .git/hooks file is not in version control.
|
|
31
|
+
// Only ever replace a .bak we wrote ourselves; otherwise keep it and write
|
|
32
|
+
// a timestamped sibling.
|
|
33
|
+
const bak = filePath + '.bak';
|
|
34
|
+
if (existsSync(bak)) {
|
|
35
|
+
const prior = readFileSync(bak, 'utf-8');
|
|
36
|
+
if (prior !== content) {
|
|
37
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
38
|
+
copyFileSync(filePath, `${filePath}.${stamp}.bak`);
|
|
39
|
+
return;
|
|
23
40
|
}
|
|
24
|
-
}
|
|
25
|
-
|
|
41
|
+
}
|
|
42
|
+
copyFileSync(filePath, bak);
|
|
43
|
+
} catch { /* backup failure is non-fatal */ }
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/**
|
|
@@ -31,7 +49,7 @@ export function backupFile(filePath) {
|
|
|
31
49
|
*/
|
|
32
50
|
export function safeWrite(filePath, content) {
|
|
33
51
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
34
|
-
backupFile(filePath);
|
|
52
|
+
backupFile(filePath, content);
|
|
35
53
|
writeFileSync(filePath, content, 'utf-8');
|
|
36
54
|
}
|
|
37
55
|
|
|
@@ -3,7 +3,7 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.41.
|
|
6
|
+
version: "0.41.6"
|
|
7
7
|
description: "Documentation integrity for AI-assisted repositories: lifecycle registry, drift validation, traceability, safe archival, SARIF/JUnit, MCP, GitHub Actions, and Spec Kit hooks."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.41.
|
|
9
|
+
version: 0.41.6
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.41.
|
|
12
|
+
<!-- docguard:version: 0.41.6 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Fix Skill
|
|
15
15
|
|
|
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.41.
|
|
10
|
+
version: 0.41.6
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.41.
|
|
13
|
+
<!-- docguard:version: 0.41.6 -->
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard Skill
|
|
16
16
|
|
|
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.41.
|
|
9
|
+
version: 0.41.6
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.41.
|
|
12
|
+
<!-- docguard:version: 0.41.6 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Review Skill
|
|
15
15
|
|
|
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.41.
|
|
9
|
+
version: 0.41.6
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.41.
|
|
12
|
+
<!-- docguard:version: 0.41.6 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,10 +4,10 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
|
|
|
4
4
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
5
5
|
metadata:
|
|
6
6
|
author: docguard
|
|
7
|
-
version: 0.41.
|
|
7
|
+
version: 0.41.6
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
|
-
<!-- docguard:version: 0.41.
|
|
10
|
+
<!-- docguard:version: 0.41.6 -->
|
|
11
11
|
|
|
12
12
|
# DocGuard Sync Skill
|
|
13
13
|
|
package/package.json
CHANGED