docguard-cli 0.36.1 → 0.37.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.
Files changed (39) hide show
  1. package/README.md +23 -18
  2. package/cli/commands/diagnose.mjs +3 -22
  3. package/cli/commands/explain.mjs +28 -0
  4. package/cli/commands/guard.mjs +4 -0
  5. package/cli/commands/llms.mjs +3 -2
  6. package/cli/commands/retire.mjs +352 -0
  7. package/cli/commands/specs.mjs +77 -0
  8. package/cli/commands/trace.mjs +24 -35
  9. package/cli/config.mjs +2 -0
  10. package/cli/docguard.mjs +74 -14
  11. package/cli/findings.mjs +54 -0
  12. package/cli/scanners/document-lifecycle.mjs +184 -0
  13. package/cli/scanners/requirement-evidence.mjs +126 -0
  14. package/cli/scanners/spec-registry.mjs +517 -0
  15. package/cli/shared-requirements.mjs +91 -0
  16. package/cli/validators/docs-coverage.mjs +111 -61
  17. package/cli/validators/document-lifecycle.mjs +51 -0
  18. package/cli/validators/schema-sync.mjs +16 -11
  19. package/cli/validators/spec-registry.mjs +47 -0
  20. package/cli/validators/traceability.mjs +73 -199
  21. package/docs/ai-integration.md +18 -5
  22. package/docs/commands.md +45 -0
  23. package/docs/configuration.md +15 -0
  24. package/docs/quickstart.md +1 -1
  25. package/extensions/spec-kit-docguard/README.md +15 -5
  26. package/extensions/spec-kit-docguard/commands/brief.md +34 -0
  27. package/extensions/spec-kit-docguard/commands/preflight.md +52 -0
  28. package/extensions/spec-kit-docguard/extension.yml +18 -5
  29. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
  30. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  31. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  32. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  33. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
  34. package/extensions/spec-kit-docguard/templates/extensions.yml +13 -6
  35. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +1 -1
  36. package/package.json +1 -1
  37. package/schemas/docguard-config.schema.json +2 -0
  38. package/schemas/docguard-specs.schema.json +162 -0
  39. package/templates/ci/github-actions.yml +1 -1
@@ -15,12 +15,23 @@
15
15
 
16
16
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
17
17
  import { resolve, join, relative, basename, extname } from 'node:path';
18
- import { TRACE_MAP, TEST_PATTERNS, isTraceableSource } from '../shared-trace-patterns.mjs';
18
+ import { TRACE_MAP, isTraceableSource } from '../shared-trace-patterns.mjs';
19
19
  import { walkFiles as sharedWalkFiles, listCanonicalDocs } from '../shared-ignore.mjs';
20
20
  import { mkFinding, resultFromFindings } from '../findings.mjs';
21
21
  import { tokenize } from '../shared-diff.mjs';
22
22
  import { rankBySimilarity } from '../shared-ir.mjs';
23
- import { parseJsTs, walk } from '../scanners/js-ast.mjs';
23
+ import {
24
+ isTestSource,
25
+ resolveRequirementReferences as resolveRequirementReferencesShared,
26
+ scanTestFilesForReferences as scanTestFilesForReferencesShared,
27
+ } from '../scanners/requirement-evidence.mjs';
28
+ import {
29
+ DEFAULT_REQ_PATTERNS,
30
+ collectRequirementIdsFromContent,
31
+ requirementPatterns,
32
+ } from '../shared-requirements.mjs';
33
+ import { readRetirementManifest } from '../scanners/document-lifecycle.mjs';
34
+ import { parseSpecId } from '../scanners/spec-registry.mjs';
24
35
 
25
36
  /**
26
37
  * Optional graphify interop (github.com/Graphify-Labs/graphify, MIT).
@@ -72,13 +83,6 @@ function loadGraphifyDocLinks(projectDir) {
72
83
  }
73
84
  }
74
85
 
75
- // A test directory also contains fixtures and configuration. Only source files
76
- // are eligible for annotations or candidate-test similarity hints.
77
- function isTestSource(file) {
78
- return /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|rb|php|sh)$/.test(file)
79
- && (TEST_PATTERNS.some(pattern => pattern.test(file)) || /(?:^|\/)(?:__tests__|tests?)\//.test(file));
80
- }
81
-
82
86
  // IR soft-link recovery (feat 5): tokenize test files once so an untraced
83
87
  // requirement can be matched to the test that most likely already covers it
84
88
  // (TF-IDF cosine, VSM). Capped so a huge test suite can't blow up guard.
@@ -101,29 +105,6 @@ const IGNORE_DIRS = new Set([
101
105
  ]);
102
106
 
103
107
 
104
- // ──── Default requirement ID patterns ────
105
- // Users can override via config.traceability.requirementPattern
106
- // Includes spec-kit standard IDs: FR-xxx, SC-xxx, T-xxx
107
- const DEFAULT_REQ_PATTERNS = [
108
- /\b(REQ)-(\d{2,4})\b/g,
109
- /\b(FR)-(\d{2,4})\b/g,
110
- /\b(NFR)-(\d{2,4})\b/g,
111
- /\b(US)-(\d{2,4})\b/g,
112
- /\b(STORY)-(\d{2,4})\b/g,
113
- /\b(AC)-(\d{2,4})\b/g,
114
- /\b(UC)-(\d{2,4})\b/g,
115
- /\b(SYS)-(\d{2,4})\b/g,
116
- /\b(ARCH)-(\d{2,4})\b/g,
117
- /\b(MOD)-(\d{2,4})\b/g,
118
- /\b(SC)-(\d{2,4})\b/g, // Spec Kit: Success Criteria
119
- // Spec Kit task IDs (T001, T002). Unlike the hyphenated IDs above, a bare
120
- // `T350` over-matches prose (timeouts, model names, status codes), forcing
121
- // spurious "untraced requirement" warnings. Anchor to the two contexts where
122
- // a real task ID actually appears: a markdown checklist marker (`- [ ] T001`,
123
- // the spec-kit tasks.md format) or a test annotation (`@req T001`/`@task`).
124
- /(?<=\[[ xX]\]\s|@(?:req|task|covers)\s)(T)(\d{3,4})\b/g,
125
- ];
126
-
127
108
  /**
128
109
  * Validate traceability — ensures canonical docs have corresponding source artifacts,
129
110
  * and requirement IDs trace through to test files.
@@ -287,16 +268,22 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
287
268
  let total = 0;
288
269
 
289
270
  // Get requirement patterns (user-configurable or defaults)
290
- const customPattern = config.traceability?.requirementPattern;
291
- const patterns = customPattern
292
- ? [new RegExp(customPattern, 'g')]
293
- : DEFAULT_REQ_PATTERNS;
271
+ const patterns = requirementPatterns(config);
294
272
 
295
273
  // ── Step 1: Collect requirement IDs from documentation ──
296
274
  const reqIds = collectRequirementIds(projectDir, config, patterns);
275
+ const retiredReqIds = loadRetiredRequirementIds(projectDir);
297
276
 
298
277
  // ── Step 2: Scan test files for requirement ID references ──
299
278
  const testRefs = scanTestFilesForReferences(projectDir, projectFiles, patterns);
279
+ const resolvedRefs = resolveRequirementReferences(reqIds, testRefs, retiredReqIds);
280
+ const definitionCounts = new Map();
281
+ for (const def of reqIds.values()) definitionCounts.set(def.id, (definitionCounts.get(def.id) || 0) + 1);
282
+ const retiredDefinitionCounts = new Map();
283
+ for (const key of retiredReqIds) {
284
+ const id = key.slice(key.lastIndexOf('#') + 1);
285
+ retiredDefinitionCounts.set(id, (retiredDefinitionCounts.get(id) || 0) + 1);
286
+ }
300
287
 
301
288
  // ── Step 3: Report traceability results ──
302
289
 
@@ -308,14 +295,15 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
308
295
  let testCorpus = null;
309
296
 
310
297
  // Check each documented requirement has at least one test reference
311
- for (const [reqId, location] of reqIds) {
298
+ for (const [key, location] of reqIds) {
299
+ const reqId = location.id;
312
300
  total++;
313
- if (testRefs.has(reqId)) {
301
+ if (resolvedRefs.has(key)) {
314
302
  passed++;
315
303
  } else {
316
304
  // Try to recover a likely-but-unannotated test via TF-IDF cosine.
317
305
  let softHint = '';
318
- let softText = `Review existing tests for this requirement. If a test verifies it, add an @req ${reqId} annotation or requirement ID test label; write a test only if behavioral coverage is actually missing.`;
306
+ let softText = `Review existing tests for this requirement. If a test verifies it, add an @req ${key} annotation or requirement ID test label; write a test only if behavioral coverage is actually missing.`;
319
307
  const queryText = location.text && location.text.length > reqId.length ? location.text : reqId;
320
308
  if (testCorpus === null) testCorpus = buildTestCorpus(projectDir, projectFiles);
321
309
  if (testCorpus.length > 0) {
@@ -324,14 +312,14 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
324
312
  if (top && top.score >= softThreshold) {
325
313
  const pct = (top.score * 100).toFixed(0);
326
314
  softHint = ` — IR soft-match: ${top.id} (${pct}% similar) may already cover it`;
327
- softText = `Review ${top.id} as a candidate (${pct}% text similarity, not coverage evidence). Add @req ${reqId} only if it verifies the requirement; otherwise inspect other tests before deciding a new test is needed.`;
315
+ softText = `Review ${top.id} as a candidate (${pct}% text similarity, not coverage evidence). Add @req ${key} only if it verifies the requirement; otherwise inspect other tests before deciding a new test is needed.`;
328
316
  }
329
317
  }
330
318
  findings.push(mkFinding({
331
319
  code: 'TRC004',
332
320
  validator: 'traceability',
333
321
  severity: 'warn',
334
- message: `Requirement ${reqId} (${location.file}:${location.line}) has no recognized test annotation or label; behavioral coverage is unknown.${softHint}`,
322
+ message: `Requirement ${reqId} (${location.file}:${location.line}) has no recognized test annotation or label; behavioral coverage is unknown.${definitionCounts.get(reqId) > 1 ? ` This ID occurs in multiple documents; use ${key} to disambiguate test references.` : ""}${softHint}`,
335
323
  location: `${location.file}:${location.line}`,
336
324
  suggestion: { kind: 'review', text: softText },
337
325
  }));
@@ -340,15 +328,18 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
340
328
 
341
329
  // Check for orphaned test refs (tests referencing non-existent requirements)
342
330
  for (const [reqId, refs] of testRefs) {
343
- if (!reqIds.has(reqId)) {
331
+ const orphan = refs.find(ref => ref.scope
332
+ ? resolveRequirementReferences(reqIds, new Map([[reqId, [ref]]]), retiredReqIds).size === 0
333
+ : !definitionCounts.has(reqId) && !retiredDefinitionCounts.has(reqId));
334
+ if (orphan) {
344
335
  total++;
345
336
  findings.push(mkFinding({
346
337
  code: 'TRC005',
347
338
  validator: 'traceability',
348
339
  severity: 'warn',
349
- message: `Test references ${reqId} (${refs[0].file}:${refs[0].line}) but no requirement ` +
340
+ message: `Test references ${orphan.scope ? `${orphan.scope}#` : ""}${reqId} (${orphan.file}:${orphan.line}) but no requirement ` +
350
341
  `with this ID exists in documentation. Remove the reference or add the requirement to docs`,
351
- location: `${refs[0].file}:${refs[0].line}`,
342
+ location: `${orphan.file}:${orphan.line}`,
352
343
  suggestion: { kind: 'review', text: 'Remove the stale reference, or add the requirement to the documentation' },
353
344
  }));
354
345
  }
@@ -357,176 +348,59 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
357
348
  return { findings, passed, total };
358
349
  }
359
350
 
360
- function collectRequirementIds(projectDir, config, patterns) {
351
+ /**
352
+ * Retired requirement identities remain known without restoring obsolete prose
353
+ * to active context. Invalid manifests supply no evidence; Document-Lifecycle
354
+ * reports their integrity failure separately.
355
+ */
356
+ function loadRetiredRequirementIds(projectDir) {
357
+ const manifest = readRetirementManifest(projectDir);
358
+ if (!manifest.ok) return new Set();
359
+ const ids = new Set();
360
+ for (const entry of manifest.entries) {
361
+ if (!Array.isArray(entry.requirementIds)) continue;
362
+ const path = entry.path.replaceAll('\\', '/').replace(/^\.\//, '');
363
+ for (const id of entry.requirementIds) {
364
+ if (typeof id === 'string' && id.length <= 128 && /^[^\s#\0]+$/.test(id)) {
365
+ ids.add(`${path}#${id}`);
366
+ }
367
+ }
368
+ }
369
+ return ids;
370
+ }
371
+
372
+ export function collectRequirementIds(projectDir, config, patterns = DEFAULT_REQ_PATTERNS) {
361
373
  const reqIds = new Map(); // reqId → { file, line }
362
374
  const docSearchPaths = getRequirementDocPaths(projectDir, config);
363
375
 
364
376
  for (const docPath of docSearchPaths) {
365
377
  if (!existsSync(docPath)) continue;
366
378
 
379
+ const docName = relative(projectDir, docPath).replaceAll("\\", "/");
367
380
  const content = readFileSync(docPath, 'utf-8');
368
-
369
- // Fast early-return: skip expensive string split if no requirement patterns exist
370
- const hasMatch = patterns.some(p => { p.lastIndex = 0; return p.test(content); });
371
- if (!hasMatch) continue;
372
-
373
- const lines = content.split('\n');
374
- const docName = relative(projectDir, docPath);
375
-
376
- let fence = null;
377
- let exampleLevel = null;
378
- let inComment = false;
379
- for (let i = 0; i < lines.length; i++) {
380
- let line = lines[i];
381
- if (/^(?: {4}|\t)/.test(line) && !fence && !inComment) continue;
382
- const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/);
383
- if (fence) {
384
- if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length
385
- && line.slice(marker[0].length).trim() === '') fence = null;
386
- continue;
387
- }
388
- if (marker) { fence = marker[1]; continue; }
389
- // Comments and fenced examples cannot define requirements. Preserve
390
- // physical line numbers instead of scanning a compacted document.
391
- line = line.replace(/<!--[\s\S]*?-->/g, '');
392
- if (inComment) {
393
- const close = line.indexOf('-->');
394
- if (close < 0) continue;
395
- line = line.slice(close + 3);
396
- inComment = false;
397
- }
398
- const open = line.indexOf('<!--');
399
- if (open >= 0) { line = line.slice(0, open); inComment = true; }
400
- const heading = line.match(/^\s{0,3}(#{1,6})\s+(.*)/);
401
- if (heading) {
402
- if (exampleLevel !== null && heading[1].length <= exampleLevel) exampleLevel = null;
403
- if (exampleLevel === null && /^(?:(?:requirement|task)[ -]+)?(?:examples?|ID[ -]+(?:formats?|syntax|examples?)|(?:formats?|syntax)[ -]+(?:of[ -]+)?IDs?)\b/i.test(heading[2])) {
404
- exampleLevel = heading[1].length;
405
- }
406
- }
407
- if (exampleLevel !== null) continue;
408
- for (const pattern of patterns) {
409
- pattern.lastIndex = 0;
410
- let match;
411
- while ((match = pattern.exec(line)) !== null) {
412
- // Definitions lead a line, heading, list item or first table cell.
413
- // Later prose references must not satisfy a missing requirement ID.
414
- const prefix = line.slice(0, match.index);
415
- if (!/^\s{0,3}(?:#{1,6}\s+|[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+|\|\s*)?[\s*`_]*$/.test(prefix)) {
416
- if (!match[0].length) pattern.lastIndex++;
417
- continue;
418
- }
419
- const reqId = match[0];
420
- if (!reqId.length) { pattern.lastIndex++; continue; }
421
- if (!reqIds.has(reqId)) {
422
- reqIds.set(reqId, { file: docName, line: i + 1, text: line.trim() });
423
- }
424
- }
425
- }
381
+ const specId = parseSpecId(content);
382
+ for (const [key, definition] of collectRequirementIdsFromContent(content, docName, patterns)) {
383
+ if (!reqIds.has(key)) reqIds.set(key, { ...definition, specId });
426
384
  }
427
385
  }
428
386
 
429
387
  return reqIds;
430
388
  }
431
389
 
432
- // A mention in fixture data is not a coverage declaration. Keep the same ID
433
- // patterns, but apply them only to annotations and test labels. In particular,
434
- // prose discussing an annotation ("never annotates @req ...") is not one.
435
- function testDeclarations(content, filename) {
436
- const declarations = [];
437
- const comment = (text, line) => {
438
- for (const [offset, raw] of text.split('\n').entries()) {
439
- const body = raw.replace(/^\s*\*?\s*/, '');
440
- if (/^(?:@(?:req|task|covers)\s|Testing\s)/i.test(body)) {
441
- declarations.push({ text: body, line: line + offset });
442
- }
443
- }
444
- };
445
- const labelName = /^(?:test|it|describe|context|specify|Run|DisplayName)$/;
446
- const ext = extname(filename);
447
- if (/^\.(?:[cm]?[jt]s|[jt]sx)$/.test(ext)) {
448
- const { ast, ok } = parseJsTs(content, filename);
449
- if (ok) {
450
- for (const c of ast.comments || []) comment(c.value, c.loc.start.line);
451
- const isLabelCall = (callee) => {
452
- if (callee?.type === 'Identifier') return labelName.test(callee.name);
453
- if (callee?.type !== 'MemberExpression' || callee.computed) return false;
454
- return labelName.test(callee.property.name)
455
- || (/^(?:only|skip|todo|concurrent|serial)$/.test(callee.property.name)
456
- && isLabelCall(callee.object));
457
- };
458
- walk(ast.program, node => {
459
- if (node.type !== 'CallExpression' || !isLabelCall(node.callee)) return;
460
- const label = node.arguments[0];
461
- if (label?.type === 'StringLiteral'
462
- || (label?.type === 'TemplateLiteral' && label.expressions.length === 0)) {
463
- // Scan source spelling to retain physical lines and custom patterns.
464
- declarations.push({ text: content.slice(label.start + 1, label.end - 1), line: label.loc.start.line });
465
- }
466
- });
467
- return declarations.sort((a, b) => a.line - b.line);
468
- }
469
- }
470
-
471
- // Other languages, and JS/TS without the optional parser: lex comments and
472
- // strings together so comment-like text inside a fixture stays opaque.
473
- // This is deliberately a best-effort tier, like the multilingual scanners.
474
- const tokens = /\/\*[\s\S]*?(?:\*\/|$)|\/\/[^\n]*|\#[^\n]*|"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`/g;
475
- const hashComments = /\.(?:py|rb|php|sh)$/.test(ext);
476
- let end = 0;
477
- let line = 1;
478
- let code = '';
479
- for (const token of content.matchAll(tokens)) {
480
- const gap = content.slice(end, token.index);
481
- line += (gap.match(/\n/g) || []).length;
482
- code += gap;
483
- const text = token[0];
484
- if (text.startsWith('//') || text.startsWith('/*') || (hashComments && text.startsWith('#'))) {
485
- comment(text.replace(/^(?:\/\/|\/\*|#)/, ''), line);
486
- } else if (/^["'`]/.test(text)
487
- && /\b(?:test|it|describe|context|specify|Run|DisplayName)(?:\.(?:only|skip|todo|concurrent|serial))*\s*\(?\s*$/.test(code)) {
488
- declarations.push({ text: text.slice(1, -1), line });
489
- }
490
- line += (text.match(/\n/g) || []).length;
491
- // Strings must break a possible label prefix; comments are whitespace.
492
- code = text.startsWith('/') || text.startsWith('#') ? code + ' ' : ';';
493
- end = token.index + text.length;
494
- }
495
- return declarations;
390
+ /** Resolve positive test links without sharing evidence between document scopes. */
391
+ export function resolveRequirementReferences(definitions, references, retiredDefinitions = new Set()) {
392
+ return resolveRequirementReferencesShared(definitions, references, retiredDefinitions);
496
393
  }
497
394
 
498
- function scanTestFilesForReferences(projectDir, projectFiles, patterns) {
499
- const testFiles = projectFiles.filter(isTestSource);
500
-
501
- const testRefs = new Map(); // reqId [{ file, line }]
502
-
503
- for (const relPath of testFiles) {
504
- const fullPath = resolve(projectDir, relPath);
505
- if (!existsSync(fullPath)) continue;
506
-
507
- let content;
508
- try { content = readFileSync(fullPath, 'utf-8'); } catch { continue; }
509
-
510
- // Fast early-return: skip expensive string split if no requirement patterns exist
511
- const hasMatch = patterns.some(p => { p.lastIndex = 0; return p.test(content); });
512
- if (!hasMatch) continue;
513
-
514
- for (const declaration of testDeclarations(content, relPath)) {
515
- for (const pattern of patterns) {
516
- pattern.lastIndex = 0;
517
- let match;
518
- while ((match = pattern.exec(declaration.text)) !== null) {
519
- if (!match[0]) { pattern.lastIndex++; continue; }
520
- const reqId = match[0];
521
- if (!testRefs.has(reqId)) testRefs.set(reqId, []);
522
- const line = declaration.line + (declaration.text.slice(0, match.index).match(/\n/g) || []).length;
523
- testRefs.get(reqId).push({ file: relPath, line });
524
- }
525
- }
526
- }
527
- }
528
-
529
- return testRefs;
395
+ /**
396
+ * Read explicit requirement annotations and test labels from eligible test sources.
397
+ * Shared by validation and feature scoring; fixture data is not linkage evidence.
398
+ * Callers supply project-relative candidate paths and global requirement regexes.
399
+ * No files are written and no findings or suppression policy are consulted.
400
+ * @returns {Map<string, Array<{file: string, line: number}>>} ID to declaration locations
401
+ */
402
+ export function scanTestFilesForReferences(projectDir, projectFiles, patterns) {
403
+ return scanTestFilesForReferencesShared(projectDir, projectFiles, patterns);
530
404
  }
531
405
 
532
406
  /**
@@ -107,6 +107,7 @@ and degrade gracefully on fork tokens and shallow clones.
107
107
  | `llms.txt` | `docguard llms` | Link index of the canonical docs ([llms.txt standard](https://llmstxt.org)) |
108
108
  | `llms-full.txt` | `docguard llms --full` | Full doc bodies inlined — one fetch, per-doc 400-line cap |
109
109
  | `.docguard/context-pack.md` | `docguard memory --pack` | Compact session-start context: guard status, scanner-derived surface counts, doc index with review dates, your AGENTS.md rules verbatim, known drift. Everything derived from code — regenerable, hallucination-free |
110
+ | `.docguard-specs.json` | `docguard specs --check` / `--write` | Committed spec lifecycle index: reviewed status and lineage plus deterministic artifact, task, and requirement-scoped test evidence. Requirement prose stays in each authoritative spec. |
110
111
 
111
112
  Load the context pack at agent session start; regenerate any time — it is
112
113
  never hand-edited.
@@ -134,15 +135,18 @@ is the canonical source.
134
135
  ## The agent workflow
135
136
 
136
137
  ```
137
- diagnose → fix (research + write) → guard → verify --semantic → done
138
+ specs preflight → diagnose → fix (research + write) → guard → verify --semantic → done
138
139
  ```
139
140
 
140
- 1. **`docguard diagnose`** — one command that identifies everything, with
141
+ 1. **`docguard specs preflight`** — before specification, load the current intent
142
+ briefing. After a draft exists, rerun with `--path <spec.md>` and stop planning
143
+ on deterministic blockers. Review semantic overlap manually.
144
+ 2. **`docguard diagnose`** — one command that identifies everything, with
141
145
  AI-ready fix prompts (add `--format json` for structure).
142
- 2. **`docguard fix --doc <name>`** — emits research steps + expected structure
146
+ 3. **`docguard fix --doc <name>`** — emits research steps + expected structure
143
147
  for one doc. Execute the research, write real content, no placeholders.
144
- 3. **`docguard guard`** — verify. Loop until PASS.
145
- 4. **`docguard verify --semantic`** — extract every checkable documented claim
148
+ 4. **`docguard guard`** — verify. Loop until PASS.
149
+ 5. **`docguard verify --semantic`** — extract every checkable documented claim
146
150
  (counts, limits, enums) with the nearest cited code path. **You** compare
147
151
  each value against the code: a green guard asserts structure, not the truth
148
152
  of documented numbers. This is the highest-value step an agent can run.
@@ -165,6 +169,15 @@ integrity. Each failing metric names its fix.
165
169
 
166
170
  ## Best practices for AI agents
167
171
 
172
+ - Read `.docguard-specs.json` before opening prior planning documents. Follow
173
+ only entries whose reviewed context is `current`; tombstones preserve identity
174
+ and recovery without putting retired prose back into normal context.
175
+ - Require `specId#requirementId` references for lifecycle evidence. A bare local
176
+ ID or a checked task cannot establish completion across a multi-spec project.
177
+ - Run `docguard specs preflight` before drafting and
178
+ `docguard specs preflight --path <spec.md>` before planning. Treat deterministic
179
+ blockers as gates and similarity as review-only context.
180
+
168
181
  1. **MCP first** — native tools beat parsing CLI output.
169
182
  2. **Trust the codes** — every finding has a stable code; `explain` it before
170
183
  acting, suppress at the site with it, report false positives via `feedback`.
package/docs/commands.md CHANGED
@@ -147,6 +147,51 @@ npx docguard-cli audit
147
147
 
148
148
  ---
149
149
 
150
+ ## Specification Lifecycle Commands
151
+
152
+ ### `docguard specs`
153
+
154
+ **Maintain the committed spec lifecycle registry.** The registry keeps reviewed
155
+ approval, delivery, context, lineage, and canonical-document scope separate from
156
+ deterministically observed artifacts, task counts, and requirement-scoped test
157
+ evidence.
158
+
159
+ ```bash
160
+ npx docguard-cli specs --check # CI: registry must match the repository
161
+ npx docguard-cli specs --write # Refresh observations; preserve reviewed fields
162
+ npx docguard-cli specs preflight # Brief prior intent before specification
163
+ npx docguard-cli specs preflight --path specs/007-feature/spec.md
164
+ ```
165
+
166
+ Every active spec needs a stable project-scoped metadata identity such as
167
+ `Spec ID: acme.billing-export` near the top of the authoritative spec. Completion evidence uses
168
+ `specId#requirementId`; bare `FR-001` references remain navigation hints because
169
+ the same local ID commonly appears in several specs.
170
+
171
+ The generated-spec preflight blocks missing or duplicate identity, stale
172
+ registry state, unsafe paths, and broken lifecycle lineage. Text similarity is
173
+ low-confidence review context and never blocks by itself. A future
174
+ `specs complete` transaction will verify exact-revision implementation evidence,
175
+ canonical outcomes, and context regeneration before marking a spec verified.
176
+
177
+ ### `docguard retire`
178
+
179
+ **Remove reviewed stale documents from active AI context while preserving exact
180
+ recovery metadata in Git.** Planning and checking are read-only; writing requires
181
+ explicit paths and a reason.
182
+
183
+ ```bash
184
+ npx docguard-cli retire --plan
185
+ npx docguard-cli retire --check --format json
186
+ npx docguard-cli retire --write --path docs/old-plan.md --reason "Superseded by current architecture"
187
+ ```
188
+
189
+ Retirement fails closed for dirty, untracked, required, symlinked, private, or
190
+ out-of-project content. Generic retirement also refuses active registered specs;
191
+ their lifecycle must move through the dedicated `specs` control plane.
192
+
193
+ ---
194
+
150
195
  ## AI Integration Commands
151
196
 
152
197
  ### `docguard fix`
@@ -198,3 +198,18 @@ These statuses skip currentness assertions; they do not hide structural or other
198
198
  Guard JSON includes checkCoverage and an applicability record per validator. States distinguish checked, partial, disabled, not-applicable, missing-prerequisite, unsupported, no-matches, and error. A passing gate means the selected policy passed; it does not mean unsupported languages or unmatched inputs were examined. CI and reports preserve this disclosure. Python import-graph analysis remains unsupported; mixed Python/JS projects disclose partial architecture coverage.
199
199
 
200
200
  Wrangler configuration supplies evidence for Worker classification. Supported typed Worker bindings participate in environment extraction without executing configuration or application code. Dynamic names, alias/dataflow tracking, and unsupported forms remain outside this bounded analysis. The existing optional Babel parser resolves lexical bindings; the fallback covers ordinary tested scopes and has lower syntax coverage.
201
+
202
+ ### Evidence boundaries in documentation and schema scans
203
+
204
+ Documentation-coverage filename checks search supported Markdown in conventional and explicitly configured homes, mapped role files, and supported extension metadata. Configured homes extend the defaults. Excluded files, private paths and symlinks cannot satisfy a documentation reference. This search does not establish absence of an explanation in external sites, RST, or unsupported formats.
205
+
206
+ A parsed direct file-read/write call supplies stronger path evidence than a path construction or existence check. Ambiguous paths remain low-confidence review signals; a directory name alone does not establish a configuration file. The bounded detector does not resolve arbitrary import aliases or dynamic paths. Schema synchronization applies source exclusions and deduplicates overlapping roots by source file, preserving models in distinct files even when names match.
207
+
208
+ Feature requirement scoring recognizes eligible test annotations and labels using the validator parser. An arbitrary fixture string is not linkage evidence, and a recognized link is not proof of behavioral coverage. Duplicate requirement IDs across separate specifications remain a known scoping limitation.
209
+
210
+
211
+ ### Requirement identity across documents
212
+
213
+ Requirement definitions are identified by repository-relative document path plus ID. A bare test annotation such as `@req FR-001` earns linkage credit only when that ID is defined in one document. When features reuse an ID, qualify the declaration: `@req specs/payments/spec.md#FR-001`. The same spelling works in a test label. Use forward slashes; an optional leading `./` is accepted. Qualifiers are exact repository-relative paths, not paths relative to the test file.
214
+
215
+ Validation and `trace --features` share definition parsing and reference resolution. A qualified reference credits only its target document. Ambiguous bare references credit neither feature and produce a review finding for each unresolved definition. A wrong qualifier is an orphan reference and never falls back to a bare match. Repeated mentions within one document do not create additional identities. Linkage remains evidence of a declaration, not proof of behavioral correctness; lifecycle and arbitrary verification-link semantics are separate concerns.
@@ -68,7 +68,7 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
68
68
  ## Verify
69
69
 
70
70
  ```bash
71
- npx docguard-cli guard # Pass/fail check (27 validators)
71
+ npx docguard-cli guard # Pass/fail check (29 validators)
72
72
  npx docguard-cli score # 0-100 maturity score
73
73
  ```
74
74
 
@@ -9,7 +9,7 @@ Enterprise-grade Canonical-Driven Development (CDD) enforcement and **AI-readabl
9
9
  - **AI-powered Generate** — `generate --plan` builds the code-truth skeleton in `<!-- docguard:section -->` markers and emits a structured agent task manifest; the AI writes the prose.
10
10
  - **Refresh and review** — `sync` surgically refreshes code-truth doc sections in place, **preserves human prose**, flags prose for agent review.
11
11
  - **Mechanical `fix --write`** — deterministic, no-LLM: remove stale documented endpoints, refresh stale "N validators" counts, replace stale version refs, insert missing `## [Unreleased]`.
12
- - **5 AI Skills** — docguard-fix, docguard-guard, docguard-sync, docguard-review, docguard-score (enterprise-grade behavior protocols, not just step-lists)
12
+ - **5 AI Skills** — docguard-fix, docguard-guard, docguard-review, docguard-score, docguard-sync (enterprise-grade behavior protocols, not just step-lists)
13
13
  - **Workflow Chaining** — YAML handoffs enable guard → sync → fix → review → score flows
14
14
  - **Spec Kit Hooks** — Quality gate integrations at implement, tasks, and review phases
15
15
  - **Minimal Dependencies** — one pinned, optional-load parser (`@babel/parser`); Node.js built-ins otherwise
@@ -51,10 +51,12 @@ docguard score
51
51
  | `speckit.docguard.score` | `docguard.score` | CDD maturity score with ROI improvement roadmap |
52
52
  | `speckit.docguard.diagnose` | — | Diagnose issues + generate multi-perspective AI prompts |
53
53
  | `speckit.docguard.generate` | — | Reverse-engineer canonical docs from codebase |
54
+ | `speckit.docguard.brief` | — | Load current spec intent before specification |
55
+ | `speckit.docguard.preflight` | — | Gate the generated spec before task generation |
54
56
 
55
57
  ## AI Skills
56
58
 
57
- DocGuard provides 4 enterprise-grade AI behavior protocols modeled after Spec Kit's skill architecture:
59
+ DocGuard provides 5 enterprise-grade AI behavior protocols modeled after Spec Kit's skill architecture:
58
60
 
59
61
  | Skill | Lines | What It Does |
60
62
  |-------|:-----:|-------------|
@@ -62,6 +64,7 @@ DocGuard provides 4 enterprise-grade AI behavior protocols modeled after Spec Ki
62
64
  | `docguard-fix` | 195 | 7-step research workflow with per-document codebase research, 3-iteration validation loops |
63
65
  | `docguard-review` | 170 | Semantic cross-document analysis with 6 analysis passes and quality scoring matrix |
64
66
  | `docguard-score` | 165 | CDD maturity assessment with ROI-based improvement roadmap and grade progression |
67
+ | `docguard-sync` | — | Refresh code-truth sections while preserving human prose and routing it for review |
65
68
 
66
69
  Skills differ from commands in a critical way: **commands tell agents what to run** (step-lists), while **skills tell agents how to think, validate, and iterate** (behavior protocols).
67
70
 
@@ -73,14 +76,21 @@ DocGuard integrates into the spec-kit workflow through hooks:
73
76
 
74
77
  ```yaml
75
78
  hooks:
76
- after_implement: # Optionalquality gate after /speckit.implement
79
+ before_specify: # Mandatoryread current spec intent first
80
+ command: speckit.docguard.brief
81
+ after_implement: # Mandatory — quality gate after /speckit.implement
77
82
  command: speckit.docguard.guard
78
- before_tasks: # Optionalreview docs before task generation
79
- command: speckit.docguard.review
83
+ before_tasks: # Mandatorygate the generated spec
84
+ command: speckit.docguard.preflight
80
85
  after_tasks: # Optional — show score after tasks
81
86
  command: speckit.docguard.score
82
87
  ```
83
88
 
89
+ The hooks call the deterministic CLI contract. The pre-specification hook emits
90
+ a current-intent briefing; the pre-task hook checks the actual generated spec.
91
+ Spec Kit dispatches the hooks through its agent workflow, while
92
+ `docguard specs --check` remains the CI enforcement surface.
93
+
84
94
  ### Workflow Chaining
85
95
 
86
96
  All commands support YAML handoffs for seamless workflow chaining:
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: "Load current spec intent and lifecycle before creating a new specification"
3
+ allowed-tools: Bash, Read
4
+ ---
5
+
6
+ # DocGuard Spec Briefing
7
+
8
+ Read the committed spec lifecycle registry before creating a new specification.
9
+ This command is a deterministic history gate. It does not decide whether two
10
+ features are semantically equivalent.
11
+
12
+ ## Execution
13
+
14
+ 1. Run the read-only briefing:
15
+
16
+ ```bash
17
+ npx --yes docguard-cli@latest specs preflight --format json
18
+ ```
19
+
20
+ 2. If the result is `BLOCKED`, stop specification work. Report every blocker and
21
+ refresh or repair the registry before continuing. A missing registry is valid
22
+ only when the project has no prior specs.
23
+
24
+ 3. If the result is `BRIEFING`, read each current spec named in `briefing` before
25
+ drafting the new behavior. Treat approval, delivery, task counts, and test
26
+ evidence as separate signals. None of them alone proves that behavior exists.
27
+
28
+ 4. Carry relevant immutable spec IDs and explicit lineage into the draft. Do not
29
+ copy prior requirement prose into the registry or infer completion from a
30
+ checkbox.
31
+
32
+ ## User Input
33
+
34
+ $ARGUMENTS
@@ -0,0 +1,52 @@
1
+ ---
2
+ description: "Gate a generated specification against registry integrity and prior intent"
3
+ allowed-tools: Bash, Read, Edit
4
+ ---
5
+
6
+ # DocGuard Generated-Spec Preflight
7
+
8
+ Run after the specification exists and before planning or task generation. The
9
+ generated draft is the reviewable artifact that DocGuard can gate.
10
+
11
+ ## User Input
12
+
13
+ $ARGUMENTS
14
+
15
+ ## Execution
16
+
17
+ 1. Resolve the current feature's `spec.md`. Use an explicit path from the user
18
+ when supplied. Otherwise use the current Spec Kit feature directory; its
19
+ prerequisite script reports `FEATURE_DIR` in JSON. Use the platform-specific
20
+ script under `.specify/scripts/` and append `/spec.md`.
21
+
22
+ 2. Confirm the draft declares a stable metadata field near the top:
23
+
24
+ ```markdown
25
+ **Spec ID**: `organization.feature-name`
26
+ ```
27
+
28
+ Add a project-scoped lowercase ID when it is missing. Never reuse an ID from
29
+ the briefing, even when an old spec has moved to Git history.
30
+
31
+ 3. Run the read-only generated-spec gate:
32
+
33
+ ```bash
34
+ npx --yes docguard-cli@latest specs preflight --path <feature-spec.md> --format json
35
+ ```
36
+
37
+ 4. If the result is `BLOCKED`, stop before task generation. Fix duplicate or
38
+ missing identity, broken lineage, unsafe paths, or stale registry state, then
39
+ rerun the same command.
40
+
41
+ 5. Review `overlaps` manually. Similarity has low confidence and never blocks by
42
+ itself. Record a reviewed `extends`, `duplicates`, `conflictsWith`,
43
+ `supersedes`, or `supersededBy` relation only when the underlying intent
44
+ supports it.
45
+
46
+ 6. Continue only when the deterministic status is `READY`. Refresh the registry
47
+ after the draft is accepted so CI observes its current digest and requirement
48
+ identities:
49
+
50
+ ```bash
51
+ npx --yes docguard-cli@latest specs --write
52
+ ```