docguard-cli 0.41.2 → 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 CHANGED
@@ -7,6 +7,29 @@ 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
+
10
33
  ## [0.41.2] - 2026-09-15
11
34
 
12
35
  Automated weekly release — batches everything merged since `v0.41.1`.
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 through the current v0.40 release line:
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
@@ -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, newBody) {
48
- const startIdx = existing.indexOf(BEGIN_MARKER);
49
- const endIdx = existing.indexOf(END_MARKER);
50
- if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null;
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
- // newBody has its own shebang — strip it since we're splicing into the
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 = newBody.replace(/^#!.*\n/, '');
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 (content.includes(BEGIN_MARKER) && content.includes(END_MARKER)) {
81
+ if (managedBounds(content)) {
66
82
  return { kind: 'managed', path, content };
67
83
  }
68
- const legacySignature = new RegExp(`^# DocGuard ${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} hook(?: \\(auto-fix mode\\))?$`, 'm');
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 start = content.indexOf(BEGIN_MARKER);
75
- const end = content.indexOf(END_MARKER);
76
- if (start === -1 || end === -1 || end < start) return null;
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
- exit 0
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
- exit 0
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
- exit 0
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
- exit 0
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 newContent = wrapManaged(useAutofix ? PRE_COMMIT_AUTOFIX : HOOKS[name].content);
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, newContent);
389
+ const spliced = spliceManagedBlock(existing, rawContent);
370
390
  if (spliced !== null) {
371
391
  safeWrite(hookPath, spliced);
372
392
  chmodSync(hookPath, 0o755);
@@ -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);
@@ -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.2"
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.2
9
+ version: 0.41.3
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.41.2 -->
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.2
10
+ version: 0.41.3
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.41.2 -->
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.2
9
+ version: 0.41.3
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.41.2 -->
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.2
9
+ version: 0.41.3
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.41.2 -->
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.2
7
+ version: 0.41.3
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
- <!-- docguard:version: 0.41.2 -->
10
+ <!-- docguard:version: 0.41.3 -->
11
11
 
12
12
  # DocGuard Sync Skill
13
13
 
@@ -44,7 +44,7 @@ jobs:
44
44
  fetch-depth: 0
45
45
 
46
46
  - name: Run DocGuard fix --write + auto-commit + PR comment
47
- uses: raccioly/docguard@v0.41.2
47
+ uses: raccioly/docguard@v0.41.3
48
48
  with:
49
49
  command: fix
50
50
  auto-commit: 'true'
@@ -35,7 +35,7 @@ jobs:
35
35
  node-version: '20'
36
36
 
37
37
  - name: Install DocGuard
38
- run: npm install --global --ignore-scripts docguard-cli@0.41.2
38
+ run: npm install --global --ignore-scripts docguard-cli@0.41.3
39
39
 
40
40
  - name: Run DocGuard
41
41
  shell: bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.41.2",
3
+ "version": "0.41.3",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@ jobs:
31
31
  node-version: '20'
32
32
 
33
33
  - name: Install DocGuard
34
- run: npm install --global --ignore-scripts docguard-cli@0.41.2
34
+ run: npm install --global --ignore-scripts docguard-cli@0.41.3
35
35
 
36
36
  - name: Run DocGuard
37
37
  shell: bash