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.
- package/README.md +23 -18
- package/cli/commands/diagnose.mjs +3 -22
- 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 +24 -35
- package/cli/config.mjs +2 -0
- package/cli/docguard.mjs +74 -14
- 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/docs-coverage.mjs +111 -61
- package/cli/validators/document-lifecycle.mjs +51 -0
- package/cli/validators/schema-sync.mjs +16 -11
- package/cli/validators/spec-registry.mjs +47 -0
- package/cli/validators/traceability.mjs +73 -199
- package/docs/ai-integration.md +18 -5
- package/docs/commands.md +45 -0
- package/docs/configuration.md +15 -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
|
@@ -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,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
|
|
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 [
|
|
298
|
+
for (const [key, location] of reqIds) {
|
|
299
|
+
const reqId = location.id;
|
|
312
300
|
total++;
|
|
313
|
-
if (
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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} (${
|
|
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: `${
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
/**
|
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/configuration.md
CHANGED
|
@@ -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.
|
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
|
+
```
|