docguard-cli 0.36.2 → 0.37.1
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/README.md +23 -18
- package/cli/commands/explain.mjs +28 -0
- package/cli/commands/guard.mjs +4 -0
- package/cli/commands/llms.mjs +3 -2
- package/cli/commands/retire.mjs +352 -0
- package/cli/commands/specs.mjs +77 -0
- package/cli/commands/trace.mjs +11 -1
- package/cli/config.mjs +2 -0
- package/cli/docguard.mjs +78 -18
- package/cli/findings.mjs +54 -0
- package/cli/scanners/document-lifecycle.mjs +184 -0
- package/cli/scanners/requirement-evidence.mjs +126 -0
- package/cli/scanners/spec-registry.mjs +517 -0
- package/cli/shared-requirements.mjs +91 -0
- package/cli/validators/document-lifecycle.mjs +51 -0
- package/cli/validators/spec-registry.mjs +47 -0
- package/cli/validators/traceability.mjs +52 -216
- package/docs/ai-integration.md +18 -5
- package/docs/commands.md +45 -0
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/README.md +15 -5
- package/extensions/spec-kit-docguard/commands/brief.md +34 -0
- package/extensions/spec-kit-docguard/commands/preflight.md +52 -0
- package/extensions/spec-kit-docguard/extension.yml +18 -5
- 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/extensions.yml +13 -6
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +1 -1
- package/package.json +1 -1
- package/schemas/docguard-config.schema.json +2 -0
- package/schemas/docguard-specs.schema.json +162 -0
- package/templates/ci/github-actions.yml +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
2
|
+
import { projectSpecRegistry, SPEC_REGISTRY_PATH } from '../scanners/spec-registry.mjs';
|
|
3
|
+
|
|
4
|
+
export function validateSpecRegistry(projectDir, config = {}) {
|
|
5
|
+
const projection = projectSpecRegistry(projectDir, config);
|
|
6
|
+
if (projection.detected === 0 && !projection.exists) {
|
|
7
|
+
return resultFromFindings([], { passed: 0, total: 0, applicable: false });
|
|
8
|
+
}
|
|
9
|
+
const findings = projection.issues.map(issue => mkFinding({
|
|
10
|
+
code: issue.code,
|
|
11
|
+
validator: 'specRegistry',
|
|
12
|
+
severity: 'warn',
|
|
13
|
+
confidence: 'high',
|
|
14
|
+
message: issue.message,
|
|
15
|
+
location: issue.path,
|
|
16
|
+
suggestion: {
|
|
17
|
+
kind: 'review',
|
|
18
|
+
text: issue.code === 'SPR002'
|
|
19
|
+
? 'Assign a unique immutable Spec ID in the authoritative spec metadata.'
|
|
20
|
+
: 'Resolve the lifecycle or registry integrity conflict, then refresh the registry.',
|
|
21
|
+
command: 'docguard specs --write',
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
if (!projection.current && projection.issues.length === 0) {
|
|
25
|
+
findings.push(mkFinding({
|
|
26
|
+
code: 'SPR001',
|
|
27
|
+
validator: 'specRegistry',
|
|
28
|
+
severity: 'warn',
|
|
29
|
+
confidence: 'high',
|
|
30
|
+
message: projection.exists
|
|
31
|
+
? `${SPEC_REGISTRY_PATH} does not match the current deterministic spec evidence projection.`
|
|
32
|
+
: `${SPEC_REGISTRY_PATH} is missing while active specifications exist.`,
|
|
33
|
+
location: SPEC_REGISTRY_PATH,
|
|
34
|
+
suggestion: {
|
|
35
|
+
kind: 'fix',
|
|
36
|
+
text: 'Refresh derived evidence without changing reviewed lifecycle fields.',
|
|
37
|
+
command: 'docguard specs --write',
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const checks = Math.max(1, projection.registry.specs.length + projection.registry.tombstones.length);
|
|
42
|
+
return resultFromFindings(findings, {
|
|
43
|
+
passed: Math.max(0, checks - findings.length),
|
|
44
|
+
total: checks,
|
|
45
|
+
applicable: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -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,
|
|
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 {
|
|
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,19 +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
|
|
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);
|
|
300
|
-
const resolvedRefs = resolveRequirementReferences(reqIds, testRefs);
|
|
279
|
+
const resolvedRefs = resolveRequirementReferences(reqIds, testRefs, retiredReqIds);
|
|
301
280
|
const definitionCounts = new Map();
|
|
302
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
|
+
}
|
|
303
287
|
|
|
304
288
|
// ── Step 3: Report traceability results ──
|
|
305
289
|
|
|
@@ -344,7 +328,9 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
344
328
|
|
|
345
329
|
// Check for orphaned test refs (tests referencing non-existent requirements)
|
|
346
330
|
for (const [reqId, refs] of testRefs) {
|
|
347
|
-
const orphan = refs.find(ref => ref.scope
|
|
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));
|
|
348
334
|
if (orphan) {
|
|
349
335
|
total++;
|
|
350
336
|
findings.push(mkFinding({
|
|
@@ -362,6 +348,27 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
362
348
|
return { findings, passed, total };
|
|
363
349
|
}
|
|
364
350
|
|
|
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
|
+
|
|
365
372
|
export function collectRequirementIds(projectDir, config, patterns = DEFAULT_REQ_PATTERNS) {
|
|
366
373
|
const reqIds = new Map(); // reqId → { file, line }
|
|
367
374
|
const docSearchPaths = getRequirementDocPaths(projectDir, config);
|
|
@@ -369,66 +376,11 @@ export function collectRequirementIds(projectDir, config, patterns = DEFAULT_REQ
|
|
|
369
376
|
for (const docPath of docSearchPaths) {
|
|
370
377
|
if (!existsSync(docPath)) continue;
|
|
371
378
|
|
|
372
|
-
const content = readFileSync(docPath, 'utf-8');
|
|
373
|
-
|
|
374
|
-
// Fast early-return: skip expensive string split if no requirement patterns exist
|
|
375
|
-
const hasMatch = patterns.some(p => { p.lastIndex = 0; return p.test(content); });
|
|
376
|
-
if (!hasMatch) continue;
|
|
377
|
-
|
|
378
|
-
const lines = content.split('\n');
|
|
379
379
|
const docName = relative(projectDir, docPath).replaceAll("\\", "/");
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
for (let i = 0; i < lines.length; i++) {
|
|
385
|
-
let line = lines[i];
|
|
386
|
-
if (/^(?: {4}|\t)/.test(line) && !fence && !inComment) continue;
|
|
387
|
-
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/);
|
|
388
|
-
if (fence) {
|
|
389
|
-
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length
|
|
390
|
-
&& line.slice(marker[0].length).trim() === '') fence = null;
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
if (marker) { fence = marker[1]; continue; }
|
|
394
|
-
// Comments and fenced examples cannot define requirements. Preserve
|
|
395
|
-
// physical line numbers instead of scanning a compacted document.
|
|
396
|
-
line = line.replace(/<!--[\s\S]*?-->/g, '');
|
|
397
|
-
if (inComment) {
|
|
398
|
-
const close = line.indexOf('-->');
|
|
399
|
-
if (close < 0) continue;
|
|
400
|
-
line = line.slice(close + 3);
|
|
401
|
-
inComment = false;
|
|
402
|
-
}
|
|
403
|
-
const open = line.indexOf('<!--');
|
|
404
|
-
if (open >= 0) { line = line.slice(0, open); inComment = true; }
|
|
405
|
-
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.*)/);
|
|
406
|
-
if (heading) {
|
|
407
|
-
if (exampleLevel !== null && heading[1].length <= exampleLevel) exampleLevel = null;
|
|
408
|
-
if (exampleLevel === null && /^(?:(?:requirement|task)[ -]+)?(?:examples?|ID[ -]+(?:formats?|syntax|examples?)|(?:formats?|syntax)[ -]+(?:of[ -]+)?IDs?)\b/i.test(heading[2])) {
|
|
409
|
-
exampleLevel = heading[1].length;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
if (exampleLevel !== null) continue;
|
|
413
|
-
for (const pattern of patterns) {
|
|
414
|
-
pattern.lastIndex = 0;
|
|
415
|
-
let match;
|
|
416
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
417
|
-
// Definitions lead a line, heading, list item or first table cell.
|
|
418
|
-
// Later prose references must not satisfy a missing requirement ID.
|
|
419
|
-
const prefix = line.slice(0, match.index);
|
|
420
|
-
if (!/^\s{0,3}(?:#{1,6}\s+|[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+|\|\s*)?[\s*`_]*$/.test(prefix)) {
|
|
421
|
-
if (!match[0].length) pattern.lastIndex++;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
const reqId = match[0];
|
|
425
|
-
if (!reqId.length) { pattern.lastIndex++; continue; }
|
|
426
|
-
const key = `${docName}#${reqId}`;
|
|
427
|
-
if (!reqIds.has(key)) {
|
|
428
|
-
reqIds.set(key, { id: reqId, file: docName, line: i + 1, text: line.trim() });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
380
|
+
const content = readFileSync(docPath, 'utf-8');
|
|
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 });
|
|
432
384
|
}
|
|
433
385
|
}
|
|
434
386
|
|
|
@@ -436,89 +388,8 @@ export function collectRequirementIds(projectDir, config, patterns = DEFAULT_REQ
|
|
|
436
388
|
}
|
|
437
389
|
|
|
438
390
|
/** Resolve positive test links without sharing evidence between document scopes. */
|
|
439
|
-
export function resolveRequirementReferences(definitions, references) {
|
|
440
|
-
|
|
441
|
-
for (const [key, definition] of definitions) {
|
|
442
|
-
if (!byId.has(definition.id)) byId.set(definition.id, []);
|
|
443
|
-
byId.get(definition.id).push(key);
|
|
444
|
-
}
|
|
445
|
-
const resolved = new Map();
|
|
446
|
-
for (const [id, refs] of references) {
|
|
447
|
-
const candidates = byId.get(id) || [];
|
|
448
|
-
for (const ref of refs) {
|
|
449
|
-
const key = ref.scope ? `${ref.scope}#${id}` : candidates.length === 1 ? candidates[0] : null;
|
|
450
|
-
if (!key || !definitions.has(key)) continue;
|
|
451
|
-
if (!resolved.has(key)) resolved.set(key, []);
|
|
452
|
-
resolved.get(key).push(ref);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return resolved;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// A mention in fixture data is not a coverage declaration. Keep the same ID
|
|
459
|
-
// patterns, but apply them only to annotations and test labels. In particular,
|
|
460
|
-
// prose discussing an annotation ("never annotates @req ...") is not one.
|
|
461
|
-
function testDeclarations(content, filename) {
|
|
462
|
-
const declarations = [];
|
|
463
|
-
const comment = (text, line) => {
|
|
464
|
-
for (const [offset, raw] of text.split('\n').entries()) {
|
|
465
|
-
const body = raw.replace(/^\s*\*?\s*/, '');
|
|
466
|
-
if (/^(?:@(?:req|task|covers)\s|Testing\s)/i.test(body)) {
|
|
467
|
-
declarations.push({ text: body, line: line + offset });
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
const labelName = /^(?:test|it|describe|context|specify|Run|DisplayName)$/;
|
|
472
|
-
const ext = extname(filename);
|
|
473
|
-
if (/^\.(?:[cm]?[jt]s|[jt]sx)$/.test(ext)) {
|
|
474
|
-
const { ast, ok } = parseJsTs(content, filename);
|
|
475
|
-
if (ok) {
|
|
476
|
-
for (const c of ast.comments || []) comment(c.value, c.loc.start.line);
|
|
477
|
-
const isLabelCall = (callee) => {
|
|
478
|
-
if (callee?.type === 'Identifier') return labelName.test(callee.name);
|
|
479
|
-
if (callee?.type !== 'MemberExpression' || callee.computed) return false;
|
|
480
|
-
return labelName.test(callee.property.name)
|
|
481
|
-
|| (/^(?:only|skip|todo|concurrent|serial)$/.test(callee.property.name)
|
|
482
|
-
&& isLabelCall(callee.object));
|
|
483
|
-
};
|
|
484
|
-
walk(ast.program, node => {
|
|
485
|
-
if (node.type !== 'CallExpression' || !isLabelCall(node.callee)) return;
|
|
486
|
-
const label = node.arguments[0];
|
|
487
|
-
if (label?.type === 'StringLiteral'
|
|
488
|
-
|| (label?.type === 'TemplateLiteral' && label.expressions.length === 0)) {
|
|
489
|
-
// Scan source spelling to retain physical lines and custom patterns.
|
|
490
|
-
declarations.push({ text: content.slice(label.start + 1, label.end - 1), line: label.loc.start.line });
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
return declarations.sort((a, b) => a.line - b.line);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Other languages, and JS/TS without the optional parser: lex comments and
|
|
498
|
-
// strings together so comment-like text inside a fixture stays opaque.
|
|
499
|
-
// This is deliberately a best-effort tier, like the multilingual scanners.
|
|
500
|
-
const tokens = /\/\*[\s\S]*?(?:\*\/|$)|\/\/[^\n]*|\#[^\n]*|"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`/g;
|
|
501
|
-
const hashComments = /\.(?:py|rb|php|sh)$/.test(ext);
|
|
502
|
-
let end = 0;
|
|
503
|
-
let line = 1;
|
|
504
|
-
let code = '';
|
|
505
|
-
for (const token of content.matchAll(tokens)) {
|
|
506
|
-
const gap = content.slice(end, token.index);
|
|
507
|
-
line += (gap.match(/\n/g) || []).length;
|
|
508
|
-
code += gap;
|
|
509
|
-
const text = token[0];
|
|
510
|
-
if (text.startsWith('//') || text.startsWith('/*') || (hashComments && text.startsWith('#'))) {
|
|
511
|
-
comment(text.replace(/^(?:\/\/|\/\*|#)/, ''), line);
|
|
512
|
-
} else if (/^["'`]/.test(text)
|
|
513
|
-
&& /\b(?:test|it|describe|context|specify|Run|DisplayName)(?:\.(?:only|skip|todo|concurrent|serial))*\s*\(?\s*$/.test(code)) {
|
|
514
|
-
declarations.push({ text: text.slice(1, -1), line });
|
|
515
|
-
}
|
|
516
|
-
line += (text.match(/\n/g) || []).length;
|
|
517
|
-
// Strings must break a possible label prefix; comments are whitespace.
|
|
518
|
-
code = text.startsWith('/') || text.startsWith('#') ? code + ' ' : ';';
|
|
519
|
-
end = token.index + text.length;
|
|
520
|
-
}
|
|
521
|
-
return declarations;
|
|
391
|
+
export function resolveRequirementReferences(definitions, references, retiredDefinitions = new Set()) {
|
|
392
|
+
return resolveRequirementReferencesShared(definitions, references, retiredDefinitions);
|
|
522
393
|
}
|
|
523
394
|
|
|
524
395
|
/**
|
|
@@ -529,42 +400,7 @@ function testDeclarations(content, filename) {
|
|
|
529
400
|
* @returns {Map<string, Array<{file: string, line: number}>>} ID to declaration locations
|
|
530
401
|
*/
|
|
531
402
|
export function scanTestFilesForReferences(projectDir, projectFiles, patterns) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const testRefs = new Map(); // reqId → [{ file, line }]
|
|
535
|
-
|
|
536
|
-
for (const relPath of testFiles) {
|
|
537
|
-
const fullPath = resolve(projectDir, relPath);
|
|
538
|
-
if (!existsSync(fullPath)) continue;
|
|
539
|
-
|
|
540
|
-
let content;
|
|
541
|
-
try { content = readFileSync(fullPath, 'utf-8'); } catch { continue; }
|
|
542
|
-
|
|
543
|
-
// Fast early-return: skip expensive string split if no requirement patterns exist
|
|
544
|
-
const hasMatch = patterns.some(p => { p.lastIndex = 0; return p.test(content); });
|
|
545
|
-
if (!hasMatch) continue;
|
|
546
|
-
|
|
547
|
-
for (const declaration of testDeclarations(content, relPath)) {
|
|
548
|
-
for (const pattern of patterns) {
|
|
549
|
-
pattern.lastIndex = 0;
|
|
550
|
-
let match;
|
|
551
|
-
while ((match = pattern.exec(declaration.text)) !== null) {
|
|
552
|
-
if (!match[0]) { pattern.lastIndex++; continue; }
|
|
553
|
-
const reqId = match[0];
|
|
554
|
-
if (!testRefs.has(reqId)) testRefs.set(reqId, []);
|
|
555
|
-
const line = declaration.line + (declaration.text.slice(0, match.index).match(/\n/g) || []).length;
|
|
556
|
-
// A document qualifier is repository-relative and exact; never fall
|
|
557
|
-
// back to a bare ID when a supplied qualifier fails to resolve.
|
|
558
|
-
const prefix = declaration.text.slice(0, match.index);
|
|
559
|
-
const qualifier = prefix.match(/([^\s`"'<>()[\]{}]+)#$/);
|
|
560
|
-
const scope = qualifier ? qualifier[1].replaceAll('\\', '/').replace(/^\.\//, '') : null;
|
|
561
|
-
testRefs.get(reqId).push({ file: relPath, line, scope });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return testRefs;
|
|
403
|
+
return scanTestFilesForReferencesShared(projectDir, projectFiles, patterns);
|
|
568
404
|
}
|
|
569
405
|
|
|
570
406
|
/**
|
package/docs/ai-integration.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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`
|
package/docs/quickstart.md
CHANGED
|
@@ -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 (
|
|
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-
|
|
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
|
|
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
|
-
|
|
79
|
+
before_specify: # Mandatory — read 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: #
|
|
79
|
-
command: speckit.docguard.
|
|
83
|
+
before_tasks: # Mandatory — gate 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
|
+
```
|