docguard-cli 0.36.0 → 0.36.2
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/cli/commands/diagnose.mjs +3 -22
- package/cli/commands/trace.mjs +15 -36
- package/cli/docguard.mjs +5 -2
- package/cli/validators/docs-coverage.mjs +111 -61
- package/cli/validators/schema-sync.mjs +16 -11
- package/cli/validators/traceability.mjs +52 -14
- package/docs/configuration.md +15 -0
- package/extensions/spec-kit-docguard/README.md +6 -6
- package/extensions/spec-kit-docguard/commands/sync.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +2 -2
- package/extensions/spec-kit-docguard/scripts/bash/common.sh +9 -17
- package/extensions/spec-kit-docguard/scripts/bash/docguard-check-docs.sh +18 -11
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +3 -3
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +1 -1
- package/package.json +1 -1
- package/templates/ci/github-actions.yml +1 -1
|
@@ -101,10 +101,8 @@ const FIX_INSTRUCTIONS = {
|
|
|
101
101
|
autoFixable: false,
|
|
102
102
|
},
|
|
103
103
|
'Freshness': {
|
|
104
|
-
action: 'Review
|
|
105
|
-
|
|
106
|
-
llmCommand: '/docguard.fix --doc',
|
|
107
|
-
description: 'Documents haven\'t been reviewed since recent code changes. Re-run fix --doc for each stale doc.',
|
|
104
|
+
action: 'Review document evidence against relevant changes',
|
|
105
|
+
description: 'Review signals do not establish incorrect documentation. Check whether the documentation or implementation needs a change; preserve approved intent.',
|
|
108
106
|
autoFixable: false,
|
|
109
107
|
},
|
|
110
108
|
// ── Routed (Phase F): these used to fall through to a generic "Manual review needed" ──
|
|
@@ -257,23 +255,6 @@ export function runDiagnose(projectDir, config, flags) {
|
|
|
257
255
|
}
|
|
258
256
|
}
|
|
259
257
|
|
|
260
|
-
// Detect stale docs from freshness and map to specific fix --doc targets
|
|
261
|
-
for (const issue of issues) {
|
|
262
|
-
if (issue.validator === 'Freshness' && !issue.docTarget) {
|
|
263
|
-
const match = issue.message.match(/([\w-]+\.md)/i);
|
|
264
|
-
if (match) {
|
|
265
|
-
const docName = match[1].toLowerCase().replace('.md', '');
|
|
266
|
-
const docMap = { 'architecture': 'architecture', 'data-model': 'data-model', 'security': 'security', 'test-spec': 'test-spec', 'environment': 'environment' };
|
|
267
|
-
issue.docTarget = docMap[docName] || null;
|
|
268
|
-
if (issue.docTarget) {
|
|
269
|
-
issue.command = agentMode === 'llm'
|
|
270
|
-
? `/docguard.fix --doc ${issue.docTarget}`
|
|
271
|
-
: `docguard fix --doc ${issue.docTarget}`;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
258
|
// ── Step 4: Output ──
|
|
278
259
|
if (flags.format === 'json') {
|
|
279
260
|
outputJSON(guardData, scoreData, issues);
|
|
@@ -497,7 +478,7 @@ function outputPrompt(projectDir, guardData, scoreData, issues, flags, agentMode
|
|
|
497
478
|
} else {
|
|
498
479
|
lines.push('After making all fixes, run: docguard guard');
|
|
499
480
|
}
|
|
500
|
-
lines.push('Expected result:
|
|
481
|
+
lines.push('Expected result: Resolve verified defects; explain remaining review signals and unsupported checks. Do not rewrite correct documents merely to remove warnings.');
|
|
501
482
|
lines.push(`Structural baseline: ${scoreData.score}/100. Resolve evidenced defects; verify material claims separately.`);
|
|
502
483
|
lines.push('Preserve approved requirements when implementation disagrees. A higher score is not proof of factual correctness.');
|
|
503
484
|
|
package/cli/commands/trace.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
10
10
|
import { resolve, join, extname, basename, relative, dirname } from 'node:path';
|
|
11
11
|
import { c } from '../shared.mjs';
|
|
12
12
|
import { detectSpecKit } from '../scanners/speckit.mjs';
|
|
13
|
+
import { scanTestFilesForReferences, collectRequirementIds, resolveRequirementReferences } from '../validators/traceability.mjs';
|
|
13
14
|
import { listCanonicalDocs } from '../shared-ignore.mjs';
|
|
14
15
|
|
|
15
16
|
const IGNORE_DIRS = new Set([
|
|
@@ -378,7 +379,7 @@ function scanDir(rootDir, dir, files) {
|
|
|
378
379
|
// repo-wide scores that `docguard score` produces. Deterministic signals only —
|
|
379
380
|
// no LLM judgment:
|
|
380
381
|
//
|
|
381
|
-
// reqCoverage 40% FR-/SC- IDs in spec.md
|
|
382
|
+
// reqCoverage 40% FR-/SC- IDs in spec.md explicitly annotated or labeled in test sources
|
|
382
383
|
// taskCompletion 25% checked/total `- [x]` tasks in tasks.md
|
|
383
384
|
// taskEvidence 20% checked tasks whose line names an existing file
|
|
384
385
|
// artifactCompleteness 15% spec.md (40%) + plan.md (30%) + tasks.md (30%)
|
|
@@ -425,34 +426,21 @@ function featureBar(score) {
|
|
|
425
426
|
}
|
|
426
427
|
|
|
427
428
|
/**
|
|
428
|
-
* Collect
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* any occurrence of the ID in file content counts (not just @req lines).
|
|
429
|
+
* Collect declared FR-/SC- test links once for the whole project, using the
|
|
430
|
+
* validator's source eligibility and annotation/label parser. This is positive
|
|
431
|
+
* linkage evidence, independent of validator findings or suppression policy.
|
|
432
432
|
*/
|
|
433
433
|
function collectTestReferencedIds(projectDir) {
|
|
434
434
|
const projectFiles = [];
|
|
435
435
|
scanDir(projectDir, projectDir, projectFiles);
|
|
436
|
-
|
|
437
|
-
TEST_PATTERNS.some(p => p.test(f)) || /__tests__\//.test(f) || /tests?\//.test(f)
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
const ids = new Set();
|
|
441
|
-
for (const rel of testFiles) {
|
|
442
|
-
let content;
|
|
443
|
-
try { content = readFileSync(resolve(projectDir, rel), 'utf-8'); } catch { continue; }
|
|
444
|
-
FEATURE_REQ_RE.lastIndex = 0;
|
|
445
|
-
let m;
|
|
446
|
-
while ((m = FEATURE_REQ_RE.exec(content)) !== null) ids.add(m[0]);
|
|
447
|
-
}
|
|
448
|
-
return ids;
|
|
436
|
+
return scanTestFilesForReferences(projectDir, projectFiles, [FEATURE_REQ_RE]);
|
|
449
437
|
}
|
|
450
438
|
|
|
451
439
|
/**
|
|
452
440
|
* Compute the four adherence signals for one detected spec-kit feature.
|
|
453
441
|
* Each signal: { applicable, value (0..1 | null), ...n/m detail fields }.
|
|
454
442
|
*/
|
|
455
|
-
function computeFeatureSignals(projectDir, feature, testRefIds) {
|
|
443
|
+
function computeFeatureSignals(projectDir, feature, testRefIds, definitions) {
|
|
456
444
|
// ── artifactCompleteness — always measurable ──
|
|
457
445
|
const artifactValue = (feature.hasSpec ? 0.4 : 0)
|
|
458
446
|
+ (feature.hasPlan ? 0.3 : 0)
|
|
@@ -483,21 +471,11 @@ function computeFeatureSignals(projectDir, feature, testRefIds) {
|
|
|
483
471
|
}
|
|
484
472
|
}
|
|
485
473
|
|
|
486
|
-
// ── reqCoverage — spec.md IDs
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const seen = new Set();
|
|
492
|
-
FEATURE_REQ_RE.lastIndex = 0;
|
|
493
|
-
let m;
|
|
494
|
-
while ((m = FEATURE_REQ_RE.exec(spec)) !== null) {
|
|
495
|
-
if (!seen.has(m[0])) { seen.add(m[0]); specIds.push(m[0]); }
|
|
496
|
-
}
|
|
497
|
-
} catch { /* unreadable spec → no IDs */ }
|
|
498
|
-
}
|
|
499
|
-
const covered = specIds.filter(id => testRefIds.has(id));
|
|
500
|
-
const uncovered = specIds.filter(id => !testRefIds.has(id));
|
|
474
|
+
// ── reqCoverage — spec.md IDs declared in eligible test annotations or labels ──
|
|
475
|
+
const specPath = feature.specPath ? relative(projectDir, feature.specPath).replaceAll('\\', '/') : null;
|
|
476
|
+
const specIds = [...definitions.values()].filter(def => def.file === specPath).map(def => def.id);
|
|
477
|
+
const covered = specIds.filter(id => testRefIds.has(`${specPath}#${id}`));
|
|
478
|
+
const uncovered = specIds.filter(id => !testRefIds.has(`${specPath}#${id}`));
|
|
501
479
|
|
|
502
480
|
return {
|
|
503
481
|
reqCoverage: {
|
|
@@ -606,10 +584,11 @@ export function runTraceFeatures(projectDir, config, flags) {
|
|
|
606
584
|
return;
|
|
607
585
|
}
|
|
608
586
|
|
|
609
|
-
const
|
|
587
|
+
const definitions = collectRequirementIds(projectDir, config, [FEATURE_REQ_RE]);
|
|
588
|
+
const testRefIds = resolveRequirementReferences(definitions, collectTestReferencedIds(projectDir));
|
|
610
589
|
|
|
611
590
|
const features = speckit.specs.map(f => {
|
|
612
|
-
const signals = computeFeatureSignals(projectDir, f, testRefIds);
|
|
591
|
+
const signals = computeFeatureSignals(projectDir, f, testRefIds, definitions);
|
|
613
592
|
const score = scoreFromSignals(signals);
|
|
614
593
|
const weakest = weakestSignal(signals);
|
|
615
594
|
const needsFix = weakest !== null && signals[weakest].value < 1;
|
package/cli/docguard.mjs
CHANGED
|
@@ -652,6 +652,8 @@ async function main() {
|
|
|
652
652
|
command !== 'setup' &&
|
|
653
653
|
command !== 'init' &&
|
|
654
654
|
!READ_ONLY_COMMANDS.has(command) &&
|
|
655
|
+
// Agent-family staleness checks must not bootstrap skills or Spec Kit.
|
|
656
|
+
!(command === 'agents' && flags.check) &&
|
|
655
657
|
!headless
|
|
656
658
|
) {
|
|
657
659
|
ensureSkills(projectDir, flags);
|
|
@@ -733,8 +735,9 @@ async function main() {
|
|
|
733
735
|
}
|
|
734
736
|
break;
|
|
735
737
|
case 'agents':
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
+
// A staleness check must bypass init's scaffolding and skill installation.
|
|
739
|
+
if (flags.check) runAgents(projectDir, config, flags);
|
|
740
|
+
else await runInit(projectDir, config, { ...flags, with: ['agents'], skipPrompts: true });
|
|
738
741
|
break;
|
|
739
742
|
case 'generate':
|
|
740
743
|
runGenerate(projectDir, config, flags);
|
|
@@ -19,10 +19,12 @@ import { docRolePath, resolveDocRole } from '../shared-doc-roles.mjs';
|
|
|
19
19
|
* existing tests are unaffected; guard just renders richer output.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
23
|
-
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
22
|
+
import { existsSync, readFileSync, readdirSync, statSync, lstatSync } from 'node:fs';
|
|
23
|
+
import { resolve, join, relative, basename, extname, isAbsolute } from 'node:path';
|
|
24
24
|
import { resolveSourceRoots } from '../shared-source.mjs';
|
|
25
|
-
import { shouldIgnore, walkFiles as sharedWalkFiles,
|
|
25
|
+
import { shouldIgnore, walkFiles as sharedWalkFiles, buildIgnoreFilter, mergeIgnoreFile, DEFAULT_IGNORE_DIRS } from '../shared-ignore.mjs';
|
|
26
|
+
import { resolveDocDirs } from '../shared.mjs';
|
|
27
|
+
import { parseJsTs, walk } from '../scanners/js-ast.mjs';
|
|
26
28
|
import { detectIaC, hasInfrastructureHeading, buildIaCWarning } from '../scanners/iac.mjs';
|
|
27
29
|
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
28
30
|
|
|
@@ -73,7 +75,7 @@ export function validateDocsCoverage(projectDir, config) {
|
|
|
73
75
|
let total = 0;
|
|
74
76
|
|
|
75
77
|
// Collect all doc content for searching
|
|
76
|
-
const allDocContent = collectDocContent(projectDir);
|
|
78
|
+
const allDocContent = collectDocContent(projectDir, config);
|
|
77
79
|
if (!allDocContent) {
|
|
78
80
|
// Literal legacy shape (no findings key) — tests deepEqual this exact object.
|
|
79
81
|
return { errors: [], warnings: [], passed: 0, total: 0 };
|
|
@@ -172,7 +174,7 @@ function checkConfigFiles(projectDir, allDocContent, config = {}) {
|
|
|
172
174
|
code: 'DCV001',
|
|
173
175
|
validator: 'docsCoverage',
|
|
174
176
|
severity: 'warn',
|
|
175
|
-
message: `Config file "${entry}" exists but is not mentioned in
|
|
177
|
+
message: `Config file "${entry}" exists but is not mentioned in scanned supported Markdown or extension YAML documentation. Document its purpose in ARCHITECTURE.md or README.md`,
|
|
176
178
|
location: entry,
|
|
177
179
|
suggestion: { kind: 'fix', text: 'Explain what this config file does in ARCHITECTURE.md or README.md' },
|
|
178
180
|
}));
|
|
@@ -372,42 +374,60 @@ function checkIaCDocumentation(projectDir, iac, config = {}) {
|
|
|
372
374
|
}
|
|
373
375
|
|
|
374
376
|
/**
|
|
375
|
-
* Check 4:
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* patterns — these are configs the project USES. Avoids matching config names
|
|
379
|
-
* sitting in arrays (scan patterns for detecting other projects' configs).
|
|
377
|
+
* Check 4: Distinguish direct file IO from config-like path expressions.
|
|
378
|
+
* Parsed calls exclude comments and example strings. Unparsed text supplies
|
|
379
|
+
* review candidates only; it cannot establish file IO or documentation need.
|
|
380
380
|
*/
|
|
381
381
|
function checkCodeReferencedConfigs(projectDir, allDocContent, config = {}) {
|
|
382
382
|
const findings = [];
|
|
383
383
|
let passed = 0;
|
|
384
384
|
let total = 0;
|
|
385
|
-
|
|
386
385
|
const lowerDocContent = allDocContent.toLowerCase();
|
|
387
|
-
const foundConfigs = new
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// resolve(dir, '.docguardignore'), existsSync('.env.example'), readFileSync('vitest.config.ts')
|
|
391
|
-
const usageRegex = /(?:resolve|join|existsSync|readFileSync|accessSync|writeFileSync)\s*\([^)]*['"`]([^'"`\n]{2,})['"`]/g;
|
|
386
|
+
const foundConfigs = new Map();
|
|
387
|
+
const methods = new Set(['resolve', 'join', 'existsSync', 'accessSync', 'readFileSync', 'writeFileSync']);
|
|
388
|
+
const directIO = new Set(['readFileSync', 'writeFileSync']);
|
|
392
389
|
|
|
393
390
|
const scanFile = (filePath) => {
|
|
394
|
-
|
|
395
|
-
if (!['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(ext)) return;
|
|
391
|
+
if (!['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(extname(filePath))) return;
|
|
396
392
|
let content;
|
|
397
393
|
try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (name.includes('/') || name.startsWith('..'))
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
if (
|
|
410
|
-
|
|
394
|
+
if (!/(?:resolve|join|existsSync|accessSync|readFileSync|writeFileSync)\s*\(/.test(content)) return;
|
|
395
|
+
// Parsing cannot yield a config candidate without a matching literal prefix.
|
|
396
|
+
// Keep every escaped source conservatively: escapes may encode that prefix.
|
|
397
|
+
if (!/['"\x60](?:\.(?![./])|[\w-]+\.config\.)|\\/.test(content)) return;
|
|
398
|
+
const source = relative(projectDir, filePath).split('\\').join('/');
|
|
399
|
+
const record = (name, method, line, parsed) => {
|
|
400
|
+
if (typeof name !== 'string' || name.includes('/') || name.includes('\\') || name.startsWith('..')) return;
|
|
401
|
+
if (!(name.startsWith('.') && name.length > 2) && !/^[\w-]+\.config\.\w+$/.test(name)) return;
|
|
402
|
+
if (/^\.[a-z]{1,4}$/i.test(name) || COMMON_DOTFILES.has(name)) return;
|
|
403
|
+
const direct = parsed && directIO.has(method);
|
|
404
|
+
// Prefer concrete IO when the same name also occurs in a path expression.
|
|
405
|
+
if (!foundConfigs.has(name) || (direct && !foundConfigs.get(name).direct)) {
|
|
406
|
+
foundConfigs.set(name, { direct, parsed, method, source: source + ':' + line });
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const { ast, ok } = parseJsTs(content, filePath);
|
|
410
|
+
if (ok && !ast.errors?.length) {
|
|
411
|
+
walk(ast, node => {
|
|
412
|
+
if (node.type !== 'CallExpression') return;
|
|
413
|
+
const callee = node.callee;
|
|
414
|
+
const method = callee.type === 'Identifier' ? callee.name
|
|
415
|
+
: callee.type === 'MemberExpression' && !callee.computed ? callee.property.name : null;
|
|
416
|
+
if (!methods.has(method)) return;
|
|
417
|
+
// IO and existence checks use argument one as the path; join/resolve
|
|
418
|
+
// may supply a filename in any segment. No dynamic-path evaluation.
|
|
419
|
+
const args = ['join', 'resolve'].includes(method) ? node.arguments : node.arguments.slice(0, 1);
|
|
420
|
+
for (const arg of args) {
|
|
421
|
+
const value = arg.type === 'StringLiteral' ? arg.value
|
|
422
|
+
: arg.type === 'TemplateLiteral' && arg.expressions.length === 0 ? arg.quasis[0].value.cooked : null;
|
|
423
|
+
record(value, method, node.loc.start.line, true);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
const pattern = /\b(resolve|join|existsSync|accessSync|readFileSync|writeFileSync)\s*\([^)]*?['"\x60]([^'"\x60\n]{2,})['"\x60]/g;
|
|
428
|
+
for (const match of content.matchAll(pattern)) {
|
|
429
|
+
record(match[2], match[1], content.slice(0, match.index).split('\n').length, false);
|
|
430
|
+
}
|
|
411
431
|
}
|
|
412
432
|
};
|
|
413
433
|
|
|
@@ -415,19 +435,26 @@ function checkCodeReferencedConfigs(projectDir, allDocContent, config = {}) {
|
|
|
415
435
|
walkFiles(rootDir, scanFile);
|
|
416
436
|
}
|
|
417
437
|
|
|
418
|
-
for (const configName of foundConfigs) {
|
|
419
|
-
if (COMMON_DOTFILES.has(configName)) continue;
|
|
438
|
+
for (const [configName, evidence] of foundConfigs) {
|
|
420
439
|
total++;
|
|
421
440
|
if (lowerDocContent.includes(configName.toLowerCase())) {
|
|
422
441
|
passed++;
|
|
423
442
|
} else {
|
|
443
|
+
const scope = 'scanned supported Markdown or extension YAML documentation';
|
|
444
|
+
const context = evidence.parsed ? evidence.method + ' call' : 'unparsed source text (parser unavailable or failed)';
|
|
424
445
|
findings.push(mkFinding({
|
|
425
446
|
code: 'DCV004',
|
|
426
447
|
validator: 'docsCoverage',
|
|
427
448
|
severity: 'warn',
|
|
428
|
-
|
|
449
|
+
confidence: evidence.direct ? 'high' : 'low',
|
|
450
|
+
message: evidence.direct
|
|
451
|
+
? 'Direct ' + context + ' references config file "' + configName + '" at ' + evidence.source + ', but it is not mentioned in ' + scope + '.'
|
|
452
|
+
: 'Config-like path "' + configName + '" appears in ' + context + ' at ' + evidence.source + '; file use and documentation need are unverified. No mention was found in ' + scope + '.',
|
|
453
|
+
// Preserve the published filename location; source context is in the message.
|
|
429
454
|
location: configName,
|
|
430
|
-
suggestion:
|
|
455
|
+
suggestion: evidence.direct
|
|
456
|
+
? { kind: 'fix', text: 'Describe this config file (purpose and format) in a supported documentation file' }
|
|
457
|
+
: { kind: 'review', text: 'Inspect this source reference: it may be a directory, generated output, or example. Document it only if appropriate.' },
|
|
431
458
|
}));
|
|
432
459
|
}
|
|
433
460
|
}
|
|
@@ -500,36 +527,59 @@ function checkReadmeSections(projectDir) {
|
|
|
500
527
|
/**
|
|
501
528
|
* Collect all documentation content into a single searchable string.
|
|
502
529
|
*/
|
|
503
|
-
function collectDocContent(projectDir) {
|
|
504
|
-
const docPaths =
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
530
|
+
function collectDocContent(projectDir, config = {}) {
|
|
531
|
+
const docPaths = new Set();
|
|
532
|
+
const visited = new Set();
|
|
533
|
+
const isIgnored = buildIgnoreFilter(mergeIgnoreFile(projectDir, { ...config }).ignore);
|
|
534
|
+
|
|
535
|
+
// Check every ancestor before traversal: the shared walker follows symlinks.
|
|
536
|
+
// Explicit homes must remain scoped inside the project, including private aliases.
|
|
537
|
+
const safePath = (path) => {
|
|
538
|
+
const normalized = path.replace(/\\/g, '/');
|
|
539
|
+
if (isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized) || normalized.includes('\0')) return null;
|
|
540
|
+
const parts = normalized.split('/').filter(p => p && p !== '.');
|
|
541
|
+
if (!parts.length || parts.some(p => p === '..' || ['.local', '.git'].includes(p.toLowerCase())
|
|
542
|
+
|| /^\.env(?:\.|$)/i.test(p) || DEFAULT_IGNORE_DIRS.has(p) || IGNORE_DIRS.has(p))) return null;
|
|
543
|
+
let current = resolve(projectDir);
|
|
544
|
+
let rel = '';
|
|
545
|
+
try {
|
|
546
|
+
for (const part of parts) {
|
|
547
|
+
rel = rel ? rel + '/' + part : part;
|
|
548
|
+
if (isIgnored(rel) || isIgnored(rel + '/')) return null;
|
|
549
|
+
current = join(current, part);
|
|
550
|
+
if (lstatSync(current).isSymbolicLink()) return null;
|
|
519
551
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
552
|
+
return current;
|
|
553
|
+
} catch { return null; }
|
|
554
|
+
};
|
|
555
|
+
const addDoc = (rel) => {
|
|
556
|
+
const abs = safePath(rel);
|
|
557
|
+
if (abs && lstatSync(abs).isFile()) docPaths.add(abs);
|
|
558
|
+
};
|
|
559
|
+
const walkDocs = (rel) => {
|
|
560
|
+
const dir = safePath(rel);
|
|
561
|
+
if (!dir || visited.has(dir)) return;
|
|
562
|
+
visited.add(dir);
|
|
563
|
+
let entries;
|
|
564
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
565
|
+
for (const entry of entries) {
|
|
566
|
+
if (entry.isSymbolicLink()) continue;
|
|
567
|
+
const path = relative(projectDir, join(dir, entry.name)).split('\\').join('/');
|
|
568
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) walkDocs(path);
|
|
569
|
+
else if (entry.isFile() && (/\.md$/i.test(entry.name)
|
|
570
|
+
|| (path.startsWith('extensions/') && /\.ya?ml$/i.test(entry.name)))) addDoc(path);
|
|
529
571
|
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
for (const doc of ['README.md', 'AGENTS.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'STANDARD.md']) addDoc(doc);
|
|
575
|
+
for (const dir of resolveDocDirs(projectDir, config)) walkDocs(dir);
|
|
576
|
+
// Resolve roles directly so raw callers get the same boundaries as loadConfig.
|
|
577
|
+
for (const role of Object.keys(config.docs?.roles || {})) {
|
|
578
|
+
const abs = resolveDocRole(projectDir, config, role);
|
|
579
|
+
addDoc(relative(projectDir, abs));
|
|
530
580
|
}
|
|
531
581
|
|
|
532
|
-
if (docPaths.
|
|
582
|
+
if (docPaths.size === 0) return null;
|
|
533
583
|
const parts = [];
|
|
534
584
|
for (const p of docPaths) {
|
|
535
585
|
try { parts.push(readFileSync(p, 'utf-8')); } catch { /* skip */ }
|
|
@@ -10,15 +10,14 @@ import { docRolePath, resolveDocRole } from '../shared-doc-roles.mjs';
|
|
|
10
10
|
* Zero NPM runtime dependencies — pure Node.js built-ins only.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, readFileSync
|
|
14
|
-
import { resolve,
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { resolve, relative, basename } from 'node:path';
|
|
15
15
|
import { resolveSourceRoots } from '../shared-source.mjs';
|
|
16
|
-
import { walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
|
|
16
|
+
import { DEFAULT_IGNORE_DIRS, relPosix, shouldIgnore, walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
|
|
17
17
|
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
18
18
|
|
|
19
19
|
const IGNORE_DIRS = new Set([
|
|
20
|
-
|
|
21
|
-
'.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
|
|
20
|
+
...DEFAULT_IGNORE_DIRS,
|
|
22
21
|
'.amplify-hosting', '.serverless',
|
|
23
22
|
]);
|
|
24
23
|
|
|
@@ -192,7 +191,8 @@ function detectAllModels(projectDir, config = {}) {
|
|
|
192
191
|
* Find schema files for a given detector configuration.
|
|
193
192
|
*/
|
|
194
193
|
function findSchemaFiles(projectDir, detector, config = {}) {
|
|
195
|
-
|
|
194
|
+
// Deduplicate files, not model names: separate schemas may share a name.
|
|
195
|
+
const files = new Set();
|
|
196
196
|
|
|
197
197
|
// Monorepo-aware: resolve each searchDir against the project root AND every
|
|
198
198
|
// configured source root (config.sourceRoot + workspaces), so schemas under
|
|
@@ -203,19 +203,24 @@ function findSchemaFiles(projectDir, detector, config = {}) {
|
|
|
203
203
|
for (const base of bases) {
|
|
204
204
|
for (const searchDir of detector.searchDirs) {
|
|
205
205
|
const dir = resolve(base, searchDir);
|
|
206
|
-
|
|
206
|
+
const rel = relPosix(projectDir, dir);
|
|
207
|
+
if (seenDirs.has(dir) || !existsSync(dir) ||
|
|
208
|
+
rel.split('/').some(part => IGNORE_DIRS.has(part)) ||
|
|
209
|
+
shouldIgnore(rel + '/', config)) continue;
|
|
207
210
|
seenDirs.add(dir);
|
|
208
|
-
scanSchemaDir(dir, detector.filePattern, files);
|
|
211
|
+
scanSchemaDir(dir, detector.filePattern, files, projectDir, config);
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
return files;
|
|
215
|
+
return [...files];
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
// v0.29 consolidation: traversal delegates to the shared canonical walker.
|
|
216
|
-
function scanSchemaDir(dir, filePattern, files) {
|
|
219
|
+
function scanSchemaDir(dir, filePattern, files, projectDir, config) {
|
|
217
220
|
sharedWalkFiles(dir, (full) => {
|
|
218
|
-
if (filePattern.test(basename(full))
|
|
221
|
+
if (filePattern.test(basename(full)) && !shouldIgnore(relPosix(projectDir, full), config)) {
|
|
222
|
+
files.add(full);
|
|
223
|
+
}
|
|
219
224
|
}, { ignoreDirs: IGNORE_DIRS });
|
|
220
225
|
}
|
|
221
226
|
|
|
@@ -297,6 +297,9 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
297
297
|
|
|
298
298
|
// ── Step 2: Scan test files for requirement ID references ──
|
|
299
299
|
const testRefs = scanTestFilesForReferences(projectDir, projectFiles, patterns);
|
|
300
|
+
const resolvedRefs = resolveRequirementReferences(reqIds, testRefs);
|
|
301
|
+
const definitionCounts = new Map();
|
|
302
|
+
for (const def of reqIds.values()) definitionCounts.set(def.id, (definitionCounts.get(def.id) || 0) + 1);
|
|
300
303
|
|
|
301
304
|
// ── Step 3: Report traceability results ──
|
|
302
305
|
|
|
@@ -308,14 +311,15 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
308
311
|
let testCorpus = null;
|
|
309
312
|
|
|
310
313
|
// Check each documented requirement has at least one test reference
|
|
311
|
-
for (const [
|
|
314
|
+
for (const [key, location] of reqIds) {
|
|
315
|
+
const reqId = location.id;
|
|
312
316
|
total++;
|
|
313
|
-
if (
|
|
317
|
+
if (resolvedRefs.has(key)) {
|
|
314
318
|
passed++;
|
|
315
319
|
} else {
|
|
316
320
|
// Try to recover a likely-but-unannotated test via TF-IDF cosine.
|
|
317
321
|
let softHint = '';
|
|
318
|
-
let softText = `Review existing tests for this requirement. If a test verifies it, add an @req ${
|
|
322
|
+
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
323
|
const queryText = location.text && location.text.length > reqId.length ? location.text : reqId;
|
|
320
324
|
if (testCorpus === null) testCorpus = buildTestCorpus(projectDir, projectFiles);
|
|
321
325
|
if (testCorpus.length > 0) {
|
|
@@ -324,14 +328,14 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
324
328
|
if (top && top.score >= softThreshold) {
|
|
325
329
|
const pct = (top.score * 100).toFixed(0);
|
|
326
330
|
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 ${
|
|
331
|
+
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
332
|
}
|
|
329
333
|
}
|
|
330
334
|
findings.push(mkFinding({
|
|
331
335
|
code: 'TRC004',
|
|
332
336
|
validator: 'traceability',
|
|
333
337
|
severity: 'warn',
|
|
334
|
-
message: `Requirement ${reqId} (${location.file}:${location.line}) has no recognized test annotation or label; behavioral coverage is unknown.${softHint}`,
|
|
338
|
+
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
339
|
location: `${location.file}:${location.line}`,
|
|
336
340
|
suggestion: { kind: 'review', text: softText },
|
|
337
341
|
}));
|
|
@@ -340,15 +344,16 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
340
344
|
|
|
341
345
|
// Check for orphaned test refs (tests referencing non-existent requirements)
|
|
342
346
|
for (const [reqId, refs] of testRefs) {
|
|
343
|
-
|
|
347
|
+
const orphan = refs.find(ref => ref.scope ? !reqIds.has(`${ref.scope}#${reqId}`) : !definitionCounts.has(reqId));
|
|
348
|
+
if (orphan) {
|
|
344
349
|
total++;
|
|
345
350
|
findings.push(mkFinding({
|
|
346
351
|
code: 'TRC005',
|
|
347
352
|
validator: 'traceability',
|
|
348
353
|
severity: 'warn',
|
|
349
|
-
message: `Test references ${reqId} (${
|
|
354
|
+
message: `Test references ${orphan.scope ? `${orphan.scope}#` : ""}${reqId} (${orphan.file}:${orphan.line}) but no requirement ` +
|
|
350
355
|
`with this ID exists in documentation. Remove the reference or add the requirement to docs`,
|
|
351
|
-
location: `${
|
|
356
|
+
location: `${orphan.file}:${orphan.line}`,
|
|
352
357
|
suggestion: { kind: 'review', text: 'Remove the stale reference, or add the requirement to the documentation' },
|
|
353
358
|
}));
|
|
354
359
|
}
|
|
@@ -357,7 +362,7 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
357
362
|
return { findings, passed, total };
|
|
358
363
|
}
|
|
359
364
|
|
|
360
|
-
function collectRequirementIds(projectDir, config, patterns) {
|
|
365
|
+
export function collectRequirementIds(projectDir, config, patterns = DEFAULT_REQ_PATTERNS) {
|
|
361
366
|
const reqIds = new Map(); // reqId → { file, line }
|
|
362
367
|
const docSearchPaths = getRequirementDocPaths(projectDir, config);
|
|
363
368
|
|
|
@@ -371,7 +376,7 @@ function collectRequirementIds(projectDir, config, patterns) {
|
|
|
371
376
|
if (!hasMatch) continue;
|
|
372
377
|
|
|
373
378
|
const lines = content.split('\n');
|
|
374
|
-
const docName = relative(projectDir, docPath);
|
|
379
|
+
const docName = relative(projectDir, docPath).replaceAll("\\", "/");
|
|
375
380
|
|
|
376
381
|
let fence = null;
|
|
377
382
|
let exampleLevel = null;
|
|
@@ -418,8 +423,9 @@ function collectRequirementIds(projectDir, config, patterns) {
|
|
|
418
423
|
}
|
|
419
424
|
const reqId = match[0];
|
|
420
425
|
if (!reqId.length) { pattern.lastIndex++; continue; }
|
|
421
|
-
|
|
422
|
-
|
|
426
|
+
const key = `${docName}#${reqId}`;
|
|
427
|
+
if (!reqIds.has(key)) {
|
|
428
|
+
reqIds.set(key, { id: reqId, file: docName, line: i + 1, text: line.trim() });
|
|
423
429
|
}
|
|
424
430
|
}
|
|
425
431
|
}
|
|
@@ -429,6 +435,26 @@ function collectRequirementIds(projectDir, config, patterns) {
|
|
|
429
435
|
return reqIds;
|
|
430
436
|
}
|
|
431
437
|
|
|
438
|
+
/** Resolve positive test links without sharing evidence between document scopes. */
|
|
439
|
+
export function resolveRequirementReferences(definitions, references) {
|
|
440
|
+
const byId = new Map();
|
|
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
|
+
|
|
432
458
|
// A mention in fixture data is not a coverage declaration. Keep the same ID
|
|
433
459
|
// patterns, but apply them only to annotations and test labels. In particular,
|
|
434
460
|
// prose discussing an annotation ("never annotates @req ...") is not one.
|
|
@@ -495,7 +521,14 @@ function testDeclarations(content, filename) {
|
|
|
495
521
|
return declarations;
|
|
496
522
|
}
|
|
497
523
|
|
|
498
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Read explicit requirement annotations and test labels from eligible test sources.
|
|
526
|
+
* Shared by validation and feature scoring; fixture data is not linkage evidence.
|
|
527
|
+
* Callers supply project-relative candidate paths and global requirement regexes.
|
|
528
|
+
* No files are written and no findings or suppression policy are consulted.
|
|
529
|
+
* @returns {Map<string, Array<{file: string, line: number}>>} ID to declaration locations
|
|
530
|
+
*/
|
|
531
|
+
export function scanTestFilesForReferences(projectDir, projectFiles, patterns) {
|
|
499
532
|
const testFiles = projectFiles.filter(isTestSource);
|
|
500
533
|
|
|
501
534
|
const testRefs = new Map(); // reqId → [{ file, line }]
|
|
@@ -520,7 +553,12 @@ function scanTestFilesForReferences(projectDir, projectFiles, patterns) {
|
|
|
520
553
|
const reqId = match[0];
|
|
521
554
|
if (!testRefs.has(reqId)) testRefs.set(reqId, []);
|
|
522
555
|
const line = declaration.line + (declaration.text.slice(0, match.index).match(/\n/g) || []).length;
|
|
523
|
-
|
|
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 });
|
|
524
562
|
}
|
|
525
563
|
}
|
|
526
564
|
}
|
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.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# DocGuard — CDD Enforcement Extension for Spec Kit
|
|
2
2
|
|
|
3
|
-
Enterprise-grade Canonical-Driven Development (CDD) enforcement and **AI-readable project memory** for [Spec Kit](https://github.com/github/spec-kit). DocGuard builds
|
|
3
|
+
Enterprise-grade Canonical-Driven Development (CDD) enforcement and **AI-readable project memory** for [Spec Kit](https://github.com/github/spec-kit). DocGuard builds source-derived documentation context for supported code (`generate --plan`), refreshes generated sections as code changes (`sync`), and checks configured rules (`guard`) — with deterministic mechanical fixes (`fix --write`) where it can and grounded agent prompts where prose is needed.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **Language-
|
|
7
|
+
- **Configurable validators** — Structure, Security, Doc Quality, Test-Spec, Drift-Comments, API-Surface, Freshness, Cross-Reference, and more; use `docguard --help` for the current surface
|
|
8
|
+
- **Language-specific extraction** — Supported checks cover several languages and monorepos. Coverage varies by detector; unsupported inputs remain unverified.
|
|
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
12
|
- **5 AI Skills** — docguard-fix, docguard-guard, docguard-sync, docguard-review, docguard-score (enterprise-grade behavior protocols, not just step-lists)
|
|
13
13
|
- **Workflow Chaining** — YAML handoffs enable guard → sync → fix → review → score flows
|
|
@@ -20,7 +20,7 @@ Enterprise-grade Canonical-Driven Development (CDD) enforcement and **AI-readabl
|
|
|
20
20
|
npm install -g docguard-cli
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Spec Kit orchestration scripts require a local `node_modules/docguard-cli` installation or `docguard` on PATH; they do not implicitly download a CLI. Direct CLI use via npx remains available:
|
|
24
24
|
```bash
|
|
25
25
|
npx docguard-cli guard
|
|
26
26
|
```
|
|
@@ -45,7 +45,7 @@ docguard score
|
|
|
45
45
|
|
|
46
46
|
| Command | Alias | Purpose |
|
|
47
47
|
|---------|-------|---------|
|
|
48
|
-
| `speckit.docguard.guard` | `docguard.guard` | Run
|
|
48
|
+
| `speckit.docguard.guard` | `docguard.guard` | Run configurable quality gate with severity triage |
|
|
49
49
|
| `speckit.docguard.fix` | `docguard.fix` | AI-driven documentation repair with codebase research |
|
|
50
50
|
| `speckit.docguard.review` | `docguard.review` | Cross-document semantic consistency analysis (read-only) |
|
|
51
51
|
| `speckit.docguard.score` | `docguard.score` | CDD maturity score with ROI improvement roadmap |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Refresh canonical code-truth sections and flag prose for review — refresh code-truth sections in place, preserve human prose
|
|
3
3
|
allowed-tools: Bash, Read, Edit
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -3,7 +3,7 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.36.
|
|
6
|
+
version: "0.36.2"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. One pinned runtime dependency (@babel/parser); pure Node.js otherwise."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
@@ -27,7 +27,7 @@ provides:
|
|
|
27
27
|
commands:
|
|
28
28
|
- name: "speckit.docguard.guard"
|
|
29
29
|
file: "commands/guard.md"
|
|
30
|
-
description: "Run
|
|
30
|
+
description: "Run configurable quality gate with severity triage and remediation plan"
|
|
31
31
|
|
|
32
32
|
- name: "speckit.docguard.fix"
|
|
33
33
|
file: "commands/generate.md"
|
|
@@ -23,25 +23,17 @@ find_docguard_root() {
|
|
|
23
23
|
|
|
24
24
|
find_docguard_cli() {
|
|
25
25
|
local root="${1:-$(pwd)}"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
echo "docguard-cli"
|
|
36
|
-
return 0
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
# Fall back to npx
|
|
40
|
-
if command -v npx >/dev/null 2>&1; then
|
|
41
|
-
echo "npx docguard-cli"
|
|
26
|
+
local entry
|
|
27
|
+
for entry in "$root/cli/docguard.mjs" "$root/node_modules/docguard-cli/cli/docguard.mjs"; do
|
|
28
|
+
if [ -f "$entry" ] && command -v node >/dev/null 2>&1; then
|
|
29
|
+
printf '%q %q\n' "$(command -v node)" "$entry"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
if command -v docguard >/dev/null 2>&1; then
|
|
34
|
+
printf '%q\n' "$(command -v docguard)"
|
|
42
35
|
return 0
|
|
43
36
|
fi
|
|
44
|
-
|
|
45
37
|
return 1
|
|
46
38
|
}
|
|
47
39
|
|
|
@@ -104,17 +104,24 @@ if $JSON_MODE; then
|
|
|
104
104
|
# Optionally include score and guard
|
|
105
105
|
EXTRAS=""
|
|
106
106
|
if $VERBOSE; then
|
|
107
|
-
SCORE_OUTPUT=$(eval $CLI_CMD score --format json
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
SCORE_OUTPUT=$(eval "$CLI_CMD score --format json")
|
|
108
|
+
SCORE=$(printf '%s' "$SCORE_OUTPUT" | node -e '
|
|
109
|
+
const d=JSON.parse(require("fs").readFileSync(0,"utf8"));
|
|
110
|
+
if(!Number.isFinite(d.score) || d.score<0 || d.score>100) throw Error("Invalid DocGuard score");
|
|
111
|
+
process.stdout.write(String(d.score));
|
|
112
|
+
')
|
|
113
|
+
GUARD_STATUS=0
|
|
114
|
+
GUARD_OUTPUT=$(eval "$CLI_CMD guard --format json") || GUARD_STATUS=$?
|
|
115
|
+
EXTRAS=$(printf '%s' "$GUARD_OUTPUT" | node -e '
|
|
116
|
+
const d=JSON.parse(require("fs").readFileSync(0,"utf8"));
|
|
117
|
+
const exit=Number(process.argv[2]);
|
|
118
|
+
const expected={PASS:0,WARN:2,FAIL:1};
|
|
119
|
+
if(!Object.hasOwn(expected,d.status) || expected[d.status]!==exit ||
|
|
120
|
+
!Number.isInteger(d.passed) || !Number.isInteger(d.total) ||
|
|
121
|
+
d.passed<0 || d.total<d.passed) throw Error("Invalid DocGuard guard report");
|
|
122
|
+
const fields={score:Number(process.argv[1]),guardPass:d.passed,guardTotal:d.total,guardStatus:d.status};
|
|
123
|
+
process.stdout.write(","+JSON.stringify(fields).slice(1,-1));
|
|
124
|
+
' "$SCORE" "$GUARD_STATUS")
|
|
118
125
|
fi
|
|
119
126
|
|
|
120
127
|
# Check for spec-kit
|
|
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.36.
|
|
9
|
+
version: 0.36.2
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.36.
|
|
12
|
+
<!-- docguard:version: 0.36.2 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Fix Skill
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ If no arguments, fix ALL issues found by `docguard diagnose`.
|
|
|
25
25
|
|
|
26
26
|
## Goal
|
|
27
27
|
|
|
28
|
-
Research the actual codebase to generate or repair canonical documentation that passes DocGuard's
|
|
28
|
+
Research the actual codebase to generate or repair canonical documentation that passes DocGuard's configurable guard suite. This skill replaces generic templates with real, project-specific content and iterates until quality checks pass.
|
|
29
29
|
|
|
30
30
|
## Operating Constraints
|
|
31
31
|
|
|
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.36.
|
|
10
|
+
version: 0.36.2
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.36.
|
|
13
|
+
<!-- docguard:version: 0.36.2 -->
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard Skill
|
|
16
16
|
|
|
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.36.
|
|
9
|
+
version: 0.36.2
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.36.
|
|
12
|
+
<!-- docguard:version: 0.36.2 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Review Skill
|
|
15
15
|
|
|
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.36.
|
|
9
|
+
version: 0.36.2
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.36.
|
|
12
|
+
<!-- docguard:version: 0.36.2 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,10 +4,10 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
|
|
|
4
4
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
5
5
|
metadata:
|
|
6
6
|
author: docguard
|
|
7
|
-
version: 0.36.
|
|
7
|
+
version: 0.36.2
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
|
-
<!-- docguard:version: 0.36.
|
|
10
|
+
<!-- docguard:version: 0.36.2 -->
|
|
11
11
|
|
|
12
12
|
# DocGuard Sync Skill
|
|
13
13
|
|
package/package.json
CHANGED