docguard-cli 0.41.1 → 0.41.3
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 +38 -0
- package/README.md +1 -1
- package/cli/commands/hooks.mjs +37 -17
- package/cli/commands/specs.mjs +7 -0
- package/cli/commands/verify.mjs +8 -0
- package/cli/scanners/spec-registry.mjs +49 -0
- package/cli/validators/spec-registry.mjs +4 -1
- 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,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.41.3] - 2026-09-15
|
|
11
|
+
|
|
12
|
+
Automated weekly release — batches everything merged since `v0.41.2`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- fix: preserve hook composition and evidence gates (#404)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Increased the bounded subprocess deadlines in the hook and Spec Kit helper contract suites so parallel CI load does not misreport healthy integrations as `ETIMEDOUT` failures.
|
|
22
|
+
- Make managed Git hook reinstall idempotent, repair nested marker pairs written
|
|
23
|
+
by affected releases, and allow successful DocGuard blocks to continue into
|
|
24
|
+
user-owned postlude commands.
|
|
25
|
+
- Make direct `verify --evidence` suitable for CI by exiting 1 for contradictions
|
|
26
|
+
and invalid manifests, 2 for unresolved evidence, and 0 for verified or
|
|
27
|
+
unconfigured evidence.
|
|
28
|
+
- Explain deterministic spec-registry drift with bounded field paths and identify
|
|
29
|
+
order-only canonicalization instead of returning an unexplained `STALE` state.
|
|
30
|
+
- Replace the README's release-line label with version-independent wording so it
|
|
31
|
+
cannot become stale on the next automated release.
|
|
32
|
+
|
|
33
|
+
## [0.41.2] - 2026-09-15
|
|
34
|
+
|
|
35
|
+
Automated weekly release — batches everything merged since `v0.41.1`.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Require exact packed CLI version (#402)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Require the extracted npm package's `--version` output to equal its exact
|
|
45
|
+
packed `package.json` version, preventing a plausible but stale semantic
|
|
46
|
+
version from passing release smoke tests.
|
|
47
|
+
|
|
10
48
|
## [0.41.1] - 2026-09-15
|
|
11
49
|
|
|
12
50
|
Automated weekly release — batches everything merged since `v0.41.0`.
|
package/README.md
CHANGED
|
@@ -714,7 +714,7 @@ Two ready-to-use templates ship with the Spec Kit extension and as standalone fi
|
|
|
714
714
|
|
|
715
715
|
## ✨ What's New
|
|
716
716
|
|
|
717
|
-
Highlights
|
|
717
|
+
Highlights from recent releases:
|
|
718
718
|
|
|
719
719
|
- **Adoption baseline** — `guard --update-baseline` freezes a legacy repo's existing findings
|
|
720
720
|
into a committed `.docguard.baseline.json`; guard/ci then gate only NEW drift, with suppression
|
package/cli/commands/hooks.mjs
CHANGED
|
@@ -24,6 +24,19 @@ import { existsSync, mkdirSync, chmodSync, readFileSync, unlinkSync } from 'node
|
|
|
24
24
|
// pre-existing hooks), behavior falls back to the existing --force flow.
|
|
25
25
|
const BEGIN_MARKER = '# BEGIN DOCGUARD MANAGED — do not edit between these markers';
|
|
26
26
|
const END_MARKER = '# END DOCGUARD MANAGED';
|
|
27
|
+
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
|
|
29
|
+
function managedBounds(content) {
|
|
30
|
+
const starts = [...content.matchAll(new RegExp(`^${escapeRegExp(BEGIN_MARKER)}\\r?$`, 'gm'))]
|
|
31
|
+
.map(match => match.index);
|
|
32
|
+
const ends = [...content.matchAll(new RegExp(`^${escapeRegExp(END_MARKER)}\\r?$`, 'gm'))]
|
|
33
|
+
.map(match => match.index);
|
|
34
|
+
const start = starts[0];
|
|
35
|
+
const end = ends.at(-1);
|
|
36
|
+
return Number.isInteger(start) && Number.isInteger(end) && end > start
|
|
37
|
+
? { start, end }
|
|
38
|
+
: null;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
/**
|
|
29
42
|
* Wrap a hook body in BEGIN/END markers so future re-installs can splice
|
|
@@ -44,15 +57,18 @@ function wrapManaged(body) {
|
|
|
44
57
|
* the BEGIN/END markers. Returns the new file content (string) or null
|
|
45
58
|
* when the markers aren't found (caller falls back to legacy behavior).
|
|
46
59
|
*/
|
|
47
|
-
function spliceManagedBlock(existing,
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
function spliceManagedBlock(existing, rawBody) {
|
|
61
|
+
const bounds = managedBounds(existing);
|
|
62
|
+
if (!bounds) return null;
|
|
63
|
+
const startIdx = bounds.start;
|
|
64
|
+
// Use the outermost end marker so one reinstall also repairs hooks written
|
|
65
|
+
// by v0.40.5-v0.41.2, which accidentally nested a second managed block.
|
|
66
|
+
const endIdx = bounds.end;
|
|
51
67
|
const before = existing.slice(0, startIdx);
|
|
52
68
|
const after = existing.slice(endIdx + END_MARKER.length);
|
|
53
|
-
//
|
|
69
|
+
// rawBody has its own shebang — strip it since we're splicing into the
|
|
54
70
|
// middle of an existing file (which already has one).
|
|
55
|
-
const bodyNoShebang =
|
|
71
|
+
const bodyNoShebang = rawBody.replace(/^#!.*\n/, '');
|
|
56
72
|
return `${before}${BEGIN_MARKER}\n${bodyNoShebang.replace(/\n+$/, '')}\n${END_MARKER}${after}`;
|
|
57
73
|
}
|
|
58
74
|
|
|
@@ -62,18 +78,21 @@ function hookState(name, hooksDir) {
|
|
|
62
78
|
let content;
|
|
63
79
|
try { content = readFileSync(path, 'utf-8'); }
|
|
64
80
|
catch { return { kind: 'unreadable', path, content: '' }; }
|
|
65
|
-
if (
|
|
81
|
+
if (managedBounds(content)) {
|
|
66
82
|
return { kind: 'managed', path, content };
|
|
67
83
|
}
|
|
68
|
-
const legacySignature = new RegExp(`^# DocGuard ${name
|
|
84
|
+
const legacySignature = new RegExp(`^# DocGuard ${escapeRegExp(name)} hook(?: \\(auto-fix mode\\))?$`, 'm');
|
|
69
85
|
if (legacySignature.test(content)) return { kind: 'legacy', path, content };
|
|
70
86
|
return { kind: 'foreign', path, content };
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
function removeManagedBlock(content) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
const bounds = managedBounds(content);
|
|
91
|
+
if (!bounds) return null;
|
|
92
|
+
const start = bounds.start;
|
|
93
|
+
// Match spliceManagedBlock's outermost recovery boundary so removal also
|
|
94
|
+
// cleans up already-nested blocks from affected releases.
|
|
95
|
+
const end = bounds.end;
|
|
77
96
|
const remaining = `${content.slice(0, start)}${content.slice(end + END_MARKER.length)}`
|
|
78
97
|
.replace(/\n{3,}/g, '\n\n');
|
|
79
98
|
return remaining;
|
|
@@ -128,7 +147,7 @@ elif [ $EXIT_CODE -eq 2 ]; then
|
|
|
128
147
|
echo "⚠️ DocGuard guard found warnings — commit allowed"
|
|
129
148
|
fi
|
|
130
149
|
|
|
131
|
-
|
|
150
|
+
:
|
|
132
151
|
`,
|
|
133
152
|
},
|
|
134
153
|
|
|
@@ -179,7 +198,7 @@ if [ "$SCORE" -lt "$MIN_SCORE" ]; then
|
|
|
179
198
|
fi
|
|
180
199
|
|
|
181
200
|
echo " ✅ Score meets minimum threshold"
|
|
182
|
-
|
|
201
|
+
:
|
|
183
202
|
`,
|
|
184
203
|
},
|
|
185
204
|
|
|
@@ -215,7 +234,7 @@ if ! echo "$COMMIT_MSG" | head -1 | grep -qE "$PATTERN"; then
|
|
|
215
234
|
exit 1
|
|
216
235
|
fi
|
|
217
236
|
|
|
218
|
-
|
|
237
|
+
:
|
|
219
238
|
`,
|
|
220
239
|
},
|
|
221
240
|
};
|
|
@@ -256,7 +275,7 @@ if [ "$EXIT_CODE" -ne 0 ] && [ "$EXIT_CODE" -ne 2 ]; then
|
|
|
256
275
|
elif [ $EXIT_CODE -eq 2 ]; then
|
|
257
276
|
echo "⚠️ DocGuard guard found warnings — commit allowed"
|
|
258
277
|
fi
|
|
259
|
-
|
|
278
|
+
:
|
|
260
279
|
`;
|
|
261
280
|
|
|
262
281
|
export function runHooks(projectDir, config, flags) {
|
|
@@ -356,7 +375,8 @@ export function runHooks(projectDir, config, flags) {
|
|
|
356
375
|
for (const name of hookTypes) {
|
|
357
376
|
const hookPath = resolve(hooksDir, name);
|
|
358
377
|
const useAutofix = name === 'pre-commit' && flags.autoFix;
|
|
359
|
-
const
|
|
378
|
+
const rawContent = useAutofix ? PRE_COMMIT_AUTOFIX : HOOKS[name].content;
|
|
379
|
+
const newContent = wrapManaged(rawContent);
|
|
360
380
|
const desc = useAutofix ? 'Apply mechanical fixes (fix --write) then guard' : HOOKS[name].description;
|
|
361
381
|
|
|
362
382
|
if (existsSync(hookPath)) {
|
|
@@ -366,7 +386,7 @@ export function runHooks(projectDir, config, flags) {
|
|
|
366
386
|
// preserve everything outside it. The user can extend the hook with
|
|
367
387
|
// their own commands above/below the markers without losing them on
|
|
368
388
|
// re-install.
|
|
369
|
-
const spliced = spliceManagedBlock(existing,
|
|
389
|
+
const spliced = spliceManagedBlock(existing, rawContent);
|
|
370
390
|
if (spliced !== null) {
|
|
371
391
|
safeWrite(hookPath, spliced);
|
|
372
392
|
chmodSync(hookPath, 0o755);
|
package/cli/commands/specs.mjs
CHANGED
|
@@ -226,6 +226,12 @@ function printResult(result) {
|
|
|
226
226
|
console.log(`Spec registry: ${result.status}`);
|
|
227
227
|
console.log(`Registry: ${SPEC_REGISTRY_PATH}`);
|
|
228
228
|
console.log(`Specs: ${result.specs}; tombstones: ${result.tombstones}`);
|
|
229
|
+
if (result.differences?.length) {
|
|
230
|
+
console.log('Differences:');
|
|
231
|
+
for (const difference of result.differences) {
|
|
232
|
+
console.log(` ${difference.path}: ${difference.message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
229
235
|
if (result.issues.length) printIssues(result.issues);
|
|
230
236
|
}
|
|
231
237
|
|
|
@@ -268,6 +274,7 @@ export function runSpecs(projectDir, config, flags = {}) {
|
|
|
268
274
|
specs: projection.registry.specs.length,
|
|
269
275
|
tombstones: projection.registry.tombstones.length,
|
|
270
276
|
issues: projection.issues,
|
|
277
|
+
differences: projection.differences,
|
|
271
278
|
};
|
|
272
279
|
if (flags.format === 'json') console.log(JSON.stringify(result, null, 2));
|
|
273
280
|
else printResult(result);
|
package/cli/commands/verify.mjs
CHANGED
|
@@ -169,6 +169,14 @@ export function runVerify(projectDir, config, flags) {
|
|
|
169
169
|
|
|
170
170
|
function runEvidenceVerification(projectDir, config, flags) {
|
|
171
171
|
const evaluation = evaluateEvidence(projectDir, config);
|
|
172
|
+
// Match guard's severity contract so this command is safe as a direct CI
|
|
173
|
+
// gate: contradictions and invalid manifests fail, while unresolved evidence
|
|
174
|
+
// uses the warning-only status shared by the rest of the CLI.
|
|
175
|
+
process.exitCode = evaluation.status === 'contradicted' || evaluation.status === 'invalid'
|
|
176
|
+
? 1
|
|
177
|
+
: evaluation.status === 'attention-required'
|
|
178
|
+
? 2
|
|
179
|
+
: 0;
|
|
172
180
|
if (flags.format === 'json') {
|
|
173
181
|
console.log(JSON.stringify(evaluation, null, 2));
|
|
174
182
|
return;
|
|
@@ -35,6 +35,51 @@ const posix = path => path.split(sep).join('/').replace(/^\.\//, '');
|
|
|
35
35
|
const digest = content => `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
36
36
|
const sortedUnique = values => [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
37
37
|
|
|
38
|
+
function registryDifferences(left, right) {
|
|
39
|
+
const differences = [];
|
|
40
|
+
let omitted = false;
|
|
41
|
+
const add = difference => {
|
|
42
|
+
if (differences.length < 25) differences.push(difference);
|
|
43
|
+
else omitted = true;
|
|
44
|
+
};
|
|
45
|
+
const memberKeys = values => values.map(value => JSON.stringify(value)).sort();
|
|
46
|
+
const visit = (actual, projected, path) => {
|
|
47
|
+
if (omitted) return;
|
|
48
|
+
if (Object.is(actual, projected)) return;
|
|
49
|
+
if (Array.isArray(actual) && Array.isArray(projected)) {
|
|
50
|
+
if (JSON.stringify(actual) === JSON.stringify(projected)) return;
|
|
51
|
+
const actualMembers = memberKeys(actual);
|
|
52
|
+
const projectedMembers = memberKeys(projected);
|
|
53
|
+
if (actualMembers.length === projectedMembers.length
|
|
54
|
+
&& actualMembers.every((value, index) => value === projectedMembers[index])) {
|
|
55
|
+
add({ path, kind: 'order', message: 'Unordered values are not in canonical sort order.' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const length = Math.max(actual.length, projected.length);
|
|
59
|
+
for (let index = 0; index < length; index++) visit(actual[index], projected[index], `${path}[${index}]`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const actualObject = actual !== null && typeof actual === 'object' && !Array.isArray(actual);
|
|
63
|
+
const projectedObject = projected !== null && typeof projected === 'object' && !Array.isArray(projected);
|
|
64
|
+
if (actualObject && projectedObject) {
|
|
65
|
+
for (const key of [...new Set([...Object.keys(actual), ...Object.keys(projected)])].sort()) {
|
|
66
|
+
visit(actual[key], projected[key], `${path}.${key}`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const kind = actual === undefined ? 'missing'
|
|
71
|
+
: projected === undefined ? 'unexpected'
|
|
72
|
+
: 'value';
|
|
73
|
+
const message = kind === 'missing' ? 'Field is missing from the registry.'
|
|
74
|
+
: kind === 'unexpected' ? 'Field is not part of the deterministic projection.'
|
|
75
|
+
: 'Field value differs from the deterministic projection.';
|
|
76
|
+
add({ path, kind, message });
|
|
77
|
+
};
|
|
78
|
+
visit(left, right, '$');
|
|
79
|
+
if (omitted) differences.push({ path: '$', kind: 'truncated', message: 'Additional differences were omitted.' });
|
|
80
|
+
return differences;
|
|
81
|
+
}
|
|
82
|
+
|
|
38
83
|
function isSafeFile(projectDir, path) {
|
|
39
84
|
try {
|
|
40
85
|
const root = realpathSync(projectDir);
|
|
@@ -472,10 +517,14 @@ export function projectSpecRegistry(projectDir, config = {}, options = {}) {
|
|
|
472
517
|
const current = existing.exists && !existing.error
|
|
473
518
|
? `${JSON.stringify(existing.value, null, 2)}\n` === serialized
|
|
474
519
|
: false;
|
|
520
|
+
const differences = existing.exists && !existing.error
|
|
521
|
+
? registryDifferences(existing.value, projected)
|
|
522
|
+
: [];
|
|
475
523
|
return {
|
|
476
524
|
registry: projected,
|
|
477
525
|
serialized,
|
|
478
526
|
current,
|
|
527
|
+
differences,
|
|
479
528
|
exists: existing.exists,
|
|
480
529
|
issues,
|
|
481
530
|
detected: detected.specs.length,
|
|
@@ -25,13 +25,16 @@ export function validateSpecRegistry(projectDir, config = {}) {
|
|
|
25
25
|
},
|
|
26
26
|
}));
|
|
27
27
|
if (!projection.current && projection.issues.length === 0) {
|
|
28
|
+
const detail = projection.differences.slice(0, 3)
|
|
29
|
+
.map(difference => `${difference.path}: ${difference.message}`)
|
|
30
|
+
.join(' ');
|
|
28
31
|
findings.push(mkFinding({
|
|
29
32
|
code: 'SPR001',
|
|
30
33
|
validator: 'specRegistry',
|
|
31
34
|
severity: 'warn',
|
|
32
35
|
confidence: 'high',
|
|
33
36
|
message: projection.exists
|
|
34
|
-
? `${SPEC_REGISTRY_PATH} does not match the current deterministic spec evidence projection
|
|
37
|
+
? `${SPEC_REGISTRY_PATH} does not match the current deterministic spec evidence projection.${detail ? ` ${detail}` : ''}`
|
|
35
38
|
: `${SPEC_REGISTRY_PATH} is missing while active specifications exist.`,
|
|
36
39
|
location: SPEC_REGISTRY_PATH,
|
|
37
40
|
suggestion: {
|
|
@@ -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.3"
|
|
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.3
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.41.
|
|
12
|
+
<!-- docguard:version: 0.41.3 -->
|
|
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.3
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.41.
|
|
13
|
+
<!-- docguard:version: 0.41.3 -->
|
|
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.3
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.41.
|
|
12
|
+
<!-- docguard:version: 0.41.3 -->
|
|
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.3
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.41.
|
|
12
|
+
<!-- docguard:version: 0.41.3 -->
|
|
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.3
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
|
-
<!-- docguard:version: 0.41.
|
|
10
|
+
<!-- docguard:version: 0.41.3 -->
|
|
11
11
|
|
|
12
12
|
# DocGuard Sync Skill
|
|
13
13
|
|
package/package.json
CHANGED