docguard-cli 0.35.0 → 0.36.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 +8 -15
- package/cli/commands/agent.mjs +27 -6
- package/cli/commands/ci.mjs +3 -0
- package/cli/commands/diagnose.mjs +8 -2
- package/cli/commands/feedback.mjs +83 -89
- package/cli/commands/fix.mjs +4 -0
- package/cli/commands/generate.mjs +3 -0
- package/cli/commands/guard.mjs +37 -20
- package/cli/commands/hooks.mjs +61 -40
- package/cli/commands/init.mjs +51 -5
- package/cli/commands/memory.mjs +29 -15
- package/cli/commands/report.mjs +12 -7
- package/cli/commands/score.mjs +39 -19
- package/cli/commands/sync.mjs +2 -0
- package/cli/commands/watch.mjs +113 -70
- package/cli/config.mjs +6 -3
- package/cli/docguard.mjs +12 -4
- package/cli/findings.mjs +13 -13
- package/cli/scanners/memory-plan.mjs +279 -134
- package/cli/scanners/project-type.mjs +6 -1
- package/cli/scanners/semantic-claims.mjs +176 -26
- package/cli/shared-diff.mjs +22 -1
- package/cli/shared-doc-roles.mjs +59 -0
- package/cli/shared-ignore.mjs +15 -2
- package/cli/shared-source.mjs +223 -1
- package/cli/validator-coverage.mjs +20 -0
- package/cli/validators/api-surface.mjs +94 -70
- package/cli/validators/architecture.mjs +19 -5
- package/cli/validators/diff-suspicion.mjs +45 -9
- package/cli/validators/docs-coverage.mjs +6 -5
- package/cli/validators/docs-diff.mjs +51 -7
- package/cli/validators/environment.mjs +3 -2
- package/cli/validators/freshness.mjs +140 -83
- package/cli/validators/schema-sync.mjs +3 -2
- package/cli/validators/security.mjs +58 -23
- package/cli/validators/structure.mjs +3 -1
- package/cli/validators/test-spec.mjs +3 -2
- package/cli/validators/todo-tracking.mjs +61 -28
- package/cli/validators/traceability.mjs +152 -38
- package/docs/configuration.md +41 -0
- package/extensions/spec-kit-docguard/extension.yml +2 -3
- 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 +1 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +74 -29
- package/package.json +1 -1
- package/schemas/docguard-config.schema.json +43 -1
- package/templates/ci/github-actions.yml +51 -11
package/cli/commands/score.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyDocRoles, remapDocPath, resolveDocRole } from '../shared-doc-roles.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Score Command — Calculate CDD maturity score (0-100)
|
|
3
4
|
* Shows category breakdown with weighted scoring.
|
|
@@ -146,6 +147,7 @@ const WEIGHTS = {
|
|
|
146
147
|
};
|
|
147
148
|
|
|
148
149
|
export function runScore(projectDir, config, flags) {
|
|
150
|
+
config = applyDocRoles(projectDir, config);
|
|
149
151
|
// v0.33: `--trend` renders the local score history recorded by `docguard
|
|
150
152
|
// ci` (.docguard/history.jsonl) instead of recomputing a score.
|
|
151
153
|
if (flags.trend) return runTrend(projectDir, config, flags);
|
|
@@ -162,9 +164,9 @@ export function runScore(projectDir, config, flags) {
|
|
|
162
164
|
|
|
163
165
|
const { scores, totalScore, grade, details } = calcAllScores(projectDir, config);
|
|
164
166
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
167
|
+
// Structural signals are useful proxies, not a measurement of factual truth.
|
|
168
|
+
// Preserve threshold arithmetic while making the evidence boundary explicit.
|
|
169
|
+
const assurance = buildScoreAssurance(projectDir, config);
|
|
168
170
|
const COMPLETENESS = new Set(['structure', 'docQuality']);
|
|
169
171
|
const memory = (() => {
|
|
170
172
|
let cW = 0, cP = 0, aW = 0, aP = 0;
|
|
@@ -175,7 +177,8 @@ export function runScore(projectDir, config, flags) {
|
|
|
175
177
|
}
|
|
176
178
|
return {
|
|
177
179
|
completeness: cW ? Math.round(cP / cW) : 0,
|
|
178
|
-
|
|
180
|
+
structuralAlignment: aW ? Math.round(aP / aW) : 0,
|
|
181
|
+
accuracy: null,
|
|
179
182
|
};
|
|
180
183
|
})();
|
|
181
184
|
|
|
@@ -185,6 +188,8 @@ export function runScore(projectDir, config, flags) {
|
|
|
185
188
|
project: config.projectName,
|
|
186
189
|
score: totalScore,
|
|
187
190
|
grade,
|
|
191
|
+
scoreKind: 'structural-maturity',
|
|
192
|
+
assurance,
|
|
188
193
|
memory,
|
|
189
194
|
categories: {},
|
|
190
195
|
};
|
|
@@ -193,7 +198,7 @@ export function runScore(projectDir, config, flags) {
|
|
|
193
198
|
score,
|
|
194
199
|
weight: WEIGHTS[cat],
|
|
195
200
|
weighted: Math.round((score / 100) * WEIGHTS[cat]),
|
|
196
|
-
axis: COMPLETENESS.has(cat) ? 'completeness' : '
|
|
201
|
+
axis: COMPLETENESS.has(cat) ? 'completeness' : 'structuralAlignment',
|
|
197
202
|
};
|
|
198
203
|
}
|
|
199
204
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -217,11 +222,11 @@ export function runScore(projectDir, config, flags) {
|
|
|
217
222
|
console.log(` ${gradeColor}${c.bold}CDD Maturity Score: ${totalScore}/100 (${grade})${c.reset}`);
|
|
218
223
|
// Memory framing: is the documentation memory COMPLETE and ACCURATE?
|
|
219
224
|
const memColor = (s) => s >= 80 ? c.green : s >= 60 ? c.yellow : c.red;
|
|
220
|
-
console.log(` ${c.dim}Memory:${c.reset} ${memColor(memory.completeness)}Completeness ${memory.completeness}%${c.reset} ${c.dim}·${c.reset} ${
|
|
225
|
+
console.log(` ${c.dim}Memory:${c.reset} ${memColor(memory.completeness)}Completeness ${memory.completeness}%${c.reset} ${c.dim}·${c.reset} ${c.cyan}Factual accuracy: unverified${c.reset}`);
|
|
221
226
|
|
|
222
227
|
// Grade description
|
|
223
228
|
const descriptions = {
|
|
224
|
-
'A+': 'Excellent —
|
|
229
|
+
'A+': 'Excellent structural maturity — factual claims still require verification',
|
|
225
230
|
'A': 'Great — Strong CDD compliance',
|
|
226
231
|
'B': 'Good — Most CDD practices in place',
|
|
227
232
|
'C': 'Fair — Partial CDD adoption',
|
|
@@ -229,6 +234,7 @@ export function runScore(projectDir, config, flags) {
|
|
|
229
234
|
'F': 'Not Started — Run `docguard init` first',
|
|
230
235
|
};
|
|
231
236
|
console.log(` ${c.dim}${descriptions[grade]}${c.reset}\n`);
|
|
237
|
+
console.log(` ${c.dim}${assurance.unverifiedClaims ?? 'Unknown number of'} extracted claim(s) await verification. No extracted claims does not prove correctness.${c.reset}\n`);
|
|
232
238
|
|
|
233
239
|
// Suggestions
|
|
234
240
|
const weakest = Object.entries(scores)
|
|
@@ -401,8 +407,21 @@ function runTrend(projectDir, config, flags) {
|
|
|
401
407
|
* Used by badge, ci, and other commands that need the score.
|
|
402
408
|
*/
|
|
403
409
|
export function runScoreInternal(projectDir, config) {
|
|
410
|
+
config = applyDocRoles(projectDir, config);
|
|
404
411
|
const { scores, totalScore, grade } = calcAllScores(projectDir, config);
|
|
405
|
-
return { score: totalScore, grade, categories: scores };
|
|
412
|
+
return { score: totalScore, grade, categories: scores, scoreKind: 'structural-maturity', assurance: buildScoreAssurance(projectDir, config) };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Evidence boundary shared by human, CI, report, and MCP score consumers. */
|
|
416
|
+
export function buildScoreAssurance(projectDir, config) {
|
|
417
|
+
let unverifiedClaims = null;
|
|
418
|
+
try { unverifiedClaims = extractSemanticClaims(projectDir, config).length; } catch { /* unknown, never zero on failure */ }
|
|
419
|
+
return {
|
|
420
|
+
status: 'unverified',
|
|
421
|
+
factualAccuracy: null,
|
|
422
|
+
unverifiedClaims,
|
|
423
|
+
limitation: 'Structural maturity is not factual accuracy. Claim discovery is heuristic; uncaptured prose remains unverified.',
|
|
424
|
+
};
|
|
406
425
|
}
|
|
407
426
|
|
|
408
427
|
/**
|
|
@@ -528,11 +547,11 @@ export function computeAlcoaCompliance(projectDir, config, scores) {
|
|
|
528
547
|
} else {
|
|
529
548
|
attributes.push({
|
|
530
549
|
name: 'Accurate',
|
|
531
|
-
met:
|
|
532
|
-
status: '
|
|
533
|
-
evidence:
|
|
534
|
-
gap:
|
|
535
|
-
fix:
|
|
550
|
+
met: false,
|
|
551
|
+
status: 'unverified',
|
|
552
|
+
evidence: null,
|
|
553
|
+
gap: 'No candidate claims extracted; factual accuracy of the prose has not been established',
|
|
554
|
+
fix: 'Review material claims against source evidence and approved requirements',
|
|
536
555
|
});
|
|
537
556
|
}
|
|
538
557
|
|
|
@@ -657,7 +676,8 @@ function calcDocQualityScore(dir, config) {
|
|
|
657
676
|
let total = 0;
|
|
658
677
|
const failures = []; // Track specific failures for actionable suggestions
|
|
659
678
|
|
|
660
|
-
for (const [
|
|
679
|
+
for (const [defaultFile, sections] of Object.entries(checks)) {
|
|
680
|
+
const file = remapDocPath(config, defaultFile);
|
|
661
681
|
const fullPath = resolve(dir, file);
|
|
662
682
|
if (!existsSync(fullPath)) {
|
|
663
683
|
failures.push({ file, issue: 'file missing' });
|
|
@@ -761,7 +781,7 @@ function calcTestingScore(dir, config) {
|
|
|
761
781
|
else failures.push({ issue: 'no test files found (looked in tests/, src/**/__tests__, and configured testPatterns)' });
|
|
762
782
|
|
|
763
783
|
// ── Check 2: TEST-SPEC.md exists (30 pts) ──
|
|
764
|
-
if (existsSync(
|
|
784
|
+
if (existsSync(resolveDocRole(dir, config, 'testSpec'))) score += 30;
|
|
765
785
|
else failures.push({ issue: 'TEST-SPEC.md missing', fixCmd: 'docguard fix --doc test-spec' });
|
|
766
786
|
|
|
767
787
|
// ── Check 3: Test config or built-in runner (15 pts) ──
|
|
@@ -853,7 +873,7 @@ function calcSecurityScore(dir, config) {
|
|
|
853
873
|
const failures = [];
|
|
854
874
|
|
|
855
875
|
// SECURITY.md exists (25 pts)
|
|
856
|
-
if (existsSync(
|
|
876
|
+
if (existsSync(resolveDocRole(dir, config, 'security'))) score += 25;
|
|
857
877
|
else failures.push({ issue: 'SECURITY.md missing', fixCmd: 'docguard fix --doc security' });
|
|
858
878
|
|
|
859
879
|
// .gitignore exists and includes .env (15 + 15 pts)
|
|
@@ -908,7 +928,7 @@ function calcEnvironmentScore(dir, config) {
|
|
|
908
928
|
const ptc = config.projectTypeConfig || {};
|
|
909
929
|
const failures = [];
|
|
910
930
|
|
|
911
|
-
if (existsSync(
|
|
931
|
+
if (existsSync(resolveDocRole(dir, config, 'environment'))) score += 40;
|
|
912
932
|
else failures.push({ issue: 'ENVIRONMENT.md missing', fixCmd: 'docguard fix --doc environment' });
|
|
913
933
|
|
|
914
934
|
// .env.example — only check if project needs env vars
|
|
@@ -978,8 +998,8 @@ function calcChangelogScore(dir, config) {
|
|
|
978
998
|
return { score: Math.min(100, score), failures };
|
|
979
999
|
}
|
|
980
1000
|
|
|
981
|
-
function calcArchitectureScore(dir) {
|
|
982
|
-
const archPath =
|
|
1001
|
+
function calcArchitectureScore(dir, config) {
|
|
1002
|
+
const archPath = resolveDocRole(dir, config, 'architecture');
|
|
983
1003
|
if (!existsSync(archPath)) {
|
|
984
1004
|
return { score: 0, failures: [{ issue: 'ARCHITECTURE.md missing', fixCmd: 'docguard fix --doc architecture' }] };
|
|
985
1005
|
}
|
package/cli/commands/sync.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Sync Command — keep the documentation memory ALWAYS UP TO DATE.
|
|
3
4
|
*
|
|
@@ -78,6 +79,7 @@ function sectionTouchedByChanges(sectionId, changedFiles) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export function runSync(projectDir, config, flags) {
|
|
82
|
+
if (flags.write) assertDefaultDocWrites(config);
|
|
81
83
|
// v0.28 (field report #10): `--tests` reconciles the hand-maintained TEST-SPEC
|
|
82
84
|
// Source-to-Test Map from disk (ghost-source removal + new co-located pairs) —
|
|
83
85
|
// a distinct path from the generated code-truth section refresh below.
|
package/cli/commands/watch.mjs
CHANGED
|
@@ -7,21 +7,23 @@
|
|
|
7
7
|
* --auto-fix: When guard finds issues, output AI fix prompts automatically.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { watch as fsWatch,
|
|
11
|
-
import { resolve,
|
|
10
|
+
import { watch as fsWatch, readdirSync, lstatSync } from 'node:fs';
|
|
11
|
+
import { resolve, extname } from 'node:path';
|
|
12
12
|
import { c } from '../shared.mjs';
|
|
13
13
|
import { runGuardInternal } from './guard.mjs';
|
|
14
|
+
import { clearMemoryPlanCache } from '../scanners/memory-plan.mjs';
|
|
15
|
+
import { buildIgnoreFilter, loadDocguardIgnore, DEFAULT_IGNORE_DIRS, relPosix } from '../shared-ignore.mjs';
|
|
14
16
|
|
|
15
17
|
const DEBOUNCE_MS = 500;
|
|
16
|
-
const IGNORE_DIRS = new Set([
|
|
17
|
-
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
18
|
-
'.cache', '__pycache__', '.venv', 'vendor', '.turbo',
|
|
19
|
-
]);
|
|
20
18
|
const WATCH_EXTS = new Set([
|
|
21
|
-
'.md', '.json', '.mjs', '.js', '.ts', '.tsx', '.jsx', '.py',
|
|
19
|
+
'.md', '.json', '.mjs', '.cjs', '.js', '.ts', '.tsx', '.jsx', '.mts', '.cts', '.py',
|
|
20
|
+
'.java', '.go', '.rs', '.rb', '.php', '.yaml', '.yml', '.toml',
|
|
22
21
|
]);
|
|
23
22
|
|
|
24
|
-
export function runWatch(projectDir, config, flags) {
|
|
23
|
+
export function runWatch(projectDir, config, flags = {}, runtime = {}) {
|
|
24
|
+
const watch = runtime.watch || fsWatch;
|
|
25
|
+
const guard = runtime.guard || runGuardInternal;
|
|
26
|
+
const clearCache = runtime.clearCache || clearMemoryPlanCache;
|
|
25
27
|
console.log(`${c.bold}👁️ DocGuard Watch — ${config.projectName}${c.reset}`);
|
|
26
28
|
console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
|
|
27
29
|
if (flags.autoFix) {
|
|
@@ -29,53 +31,104 @@ export function runWatch(projectDir, config, flags) {
|
|
|
29
31
|
}
|
|
30
32
|
console.log(`${c.dim} Watching for changes... (Ctrl+C to stop)${c.reset}\n`);
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Collect directories to watch
|
|
36
|
-
const watchDirs = collectWatchDirs(projectDir);
|
|
37
|
-
console.log(`${c.dim} Watching ${watchDirs.length} directories${c.reset}\n`);
|
|
38
|
-
|
|
34
|
+
let ignored = buildIgnoreFilter([...(config.ignore || []), ...loadDocguardIgnore(projectDir)]);
|
|
35
|
+
const skip = path => path.split('/').some(part => part === '.local' || DEFAULT_IGNORE_DIRS.has(part)) || ignored(path);
|
|
36
|
+
const watchers = new Map();
|
|
39
37
|
let debounceTimer = null;
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
let running = false;
|
|
39
|
+
let pending = false;
|
|
40
|
+
let stopped = false;
|
|
41
|
+
const changes = new Set();
|
|
42
|
+
|
|
43
|
+
function stop() {
|
|
44
|
+
if (stopped) return;
|
|
45
|
+
stopped = true;
|
|
46
|
+
clearTimeout(debounceTimer);
|
|
47
|
+
for (const watcher of watchers.values()) watcher.close();
|
|
48
|
+
watchers.clear();
|
|
49
|
+
process.removeListener('SIGINT', onSignal);
|
|
50
|
+
process.removeListener('SIGTERM', onSignal);
|
|
51
|
+
}
|
|
52
|
+
function onSignal() {
|
|
53
|
+
stop();
|
|
54
|
+
console.log(`\n${c.dim} Watch stopped.${c.reset}\n`);
|
|
55
|
+
}
|
|
56
|
+
function failed(err, dir) {
|
|
57
|
+
if (stopped) return;
|
|
58
|
+
console.error(`${c.red} Watch failed for ${relPosix(projectDir, dir) || '.'}: ${err.code || err.message}. Watch stopped; resolve the error and restart.${c.reset}`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
stop();
|
|
61
|
+
}
|
|
62
|
+
async function check() {
|
|
63
|
+
if (stopped) return;
|
|
64
|
+
if (running) { pending = true; return; }
|
|
65
|
+
running = true;
|
|
66
|
+
pending = false;
|
|
67
|
+
const changed = [...changes];
|
|
68
|
+
changes.clear();
|
|
69
|
+
if (changed.length) console.log(`\n${c.dim} Changed: ${c.cyan}${changed.join(', ')}${c.reset}`);
|
|
43
70
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Debounce — wait for rapid saves to settle
|
|
54
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
55
|
-
debounceTimer = setTimeout(() => {
|
|
56
|
-
console.log(`\n${c.dim} Changed: ${c.cyan}${changePath}${c.reset}`);
|
|
57
|
-
runGuardQuiet(projectDir, config, flags);
|
|
58
|
-
lastChange = '';
|
|
59
|
-
}, DEBOUNCE_MS);
|
|
60
|
-
});
|
|
61
|
-
} catch {
|
|
62
|
-
// Some directories may not be watchable
|
|
71
|
+
clearCache();
|
|
72
|
+
await runGuardQuiet(projectDir, config, flags, guard);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(`${c.red} Guard failed: ${err.message}${c.reset}`);
|
|
75
|
+
} finally {
|
|
76
|
+
running = false;
|
|
77
|
+
// If saves are still arriving, let their debounce expire first.
|
|
78
|
+
if (pending && !debounceTimer && !stopped) void check();
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
function changed(dir, filename) {
|
|
82
|
+
if (stopped) return;
|
|
83
|
+
const name = filename == null ? null : String(filename);
|
|
84
|
+
const path = name == null ? relPosix(projectDir, dir) : relPosix(projectDir, resolve(dir, name));
|
|
85
|
+
if (skip(path) || path.startsWith('../')) return;
|
|
86
|
+
if (name && name.startsWith('.') && !['.docguard.json', '.docguardignore', '.env.example'].includes(name)) return;
|
|
87
|
+
// Rename events include directory creation/removal. Reconcile subscriptions
|
|
88
|
+
// after the burst settles so new subdirectories are watched too.
|
|
89
|
+
if (name && extname(name) && !WATCH_EXTS.has(extname(name).toLowerCase()) && name !== '.docguardignore' && name !== '.env.example') return;
|
|
90
|
+
changes.add(path || '(unknown path)');
|
|
91
|
+
clearTimeout(debounceTimer);
|
|
92
|
+
debounceTimer = setTimeout(() => {
|
|
93
|
+
debounceTimer = null;
|
|
94
|
+
reconcile();
|
|
95
|
+
void check();
|
|
96
|
+
}, DEBOUNCE_MS);
|
|
97
|
+
}
|
|
98
|
+
function reconcile() {
|
|
99
|
+
if (stopped) return;
|
|
100
|
+
ignored = buildIgnoreFilter([...(config.ignore || []), ...loadDocguardIgnore(projectDir)]);
|
|
101
|
+
const dirs = collectWatchDirs(projectDir, skip, failed);
|
|
102
|
+
if (stopped) return;
|
|
103
|
+
const wanted = new Set(dirs);
|
|
104
|
+
for (const [dir, watcher] of watchers) {
|
|
105
|
+
if (!wanted.has(dir)) { watcher.close(); watchers.delete(dir); }
|
|
106
|
+
}
|
|
107
|
+
for (const dir of dirs) {
|
|
108
|
+
if (watchers.has(dir)) continue;
|
|
109
|
+
try {
|
|
110
|
+
const watcher = watch(dir, { persistent: true }, (_event, filename) => changed(dir, filename));
|
|
111
|
+
watchers.set(dir, watcher);
|
|
112
|
+
watcher.on('error', err => failed(err, dir));
|
|
113
|
+
} catch (err) { failed(err, dir); break; }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
process.on('SIGINT', onSignal);
|
|
117
|
+
process.on('SIGTERM', onSignal);
|
|
118
|
+
reconcile();
|
|
119
|
+
if (!stopped) {
|
|
120
|
+
void check();
|
|
121
|
+
console.log(`${c.dim} Watching ${watchers.size} directories${c.reset}\n`);
|
|
122
|
+
}
|
|
123
|
+
return { close: stop };
|
|
71
124
|
}
|
|
72
125
|
|
|
73
|
-
function runGuardQuiet(projectDir, config, flags) {
|
|
126
|
+
async function runGuardQuiet(projectDir, config, flags, guard) {
|
|
74
127
|
const timestamp = new Date().toLocaleTimeString();
|
|
75
128
|
console.log(`${c.dim} [${timestamp}] Running guard...${c.reset}`);
|
|
76
129
|
|
|
77
130
|
try {
|
|
78
|
-
const data =
|
|
131
|
+
const data = await guard(projectDir, config);
|
|
79
132
|
|
|
80
133
|
if (data.status === 'PASS') {
|
|
81
134
|
console.log(` ${c.green}✅ PASS${c.reset} — ${data.passed}/${data.total} checks passed`);
|
|
@@ -110,34 +163,24 @@ function runGuardQuiet(projectDir, config, flags) {
|
|
|
110
163
|
}
|
|
111
164
|
}
|
|
112
165
|
|
|
113
|
-
function collectWatchDirs(rootDir) {
|
|
114
|
-
const dirs = [
|
|
115
|
-
|
|
166
|
+
function collectWatchDirs(rootDir, skip, onError) {
|
|
167
|
+
const dirs = [];
|
|
116
168
|
function walk(dir) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
} catch { /* skip unreadable */ }
|
|
169
|
+
dirs.push(dir);
|
|
170
|
+
let entries;
|
|
171
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); }
|
|
172
|
+
catch (err) { onError(err, dir); return; }
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (entry.name.startsWith('.')) continue;
|
|
175
|
+
const full = resolve(dir, entry.name);
|
|
176
|
+
if (skip(relPosix(rootDir, full)) || entry.isSymbolicLink()) continue;
|
|
177
|
+
try {
|
|
178
|
+
if (lstatSync(full).isDirectory()) walk(full);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err.code !== 'ENOENT') onError(err, full);
|
|
131
181
|
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Always watch docs-canonical explicitly
|
|
136
|
-
const docsDir = resolve(rootDir, 'docs-canonical');
|
|
137
|
-
if (existsSync(docsDir) && !dirs.includes(docsDir)) {
|
|
138
|
-
dirs.push(docsDir);
|
|
182
|
+
}
|
|
139
183
|
}
|
|
140
|
-
|
|
141
184
|
walk(rootDir);
|
|
142
185
|
return dirs;
|
|
143
186
|
}
|
package/cli/config.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hasWorkerConfig } from './shared-source.mjs';
|
|
2
|
+
import { applyDocRoles } from './shared-doc-roles.mjs';
|
|
1
3
|
/**
|
|
2
4
|
* DocGuard — configuration loading.
|
|
3
5
|
*
|
|
@@ -78,9 +80,9 @@ export function loadConfig(projectDir) {
|
|
|
78
80
|
environment: true,
|
|
79
81
|
freshness: true,
|
|
80
82
|
// v0.31.0 — all three default ON. Soft (confidence:low, never break CI),
|
|
81
|
-
//
|
|
83
|
+
// heuristic (field cases require ongoing precision checks), and quiet when
|
|
82
84
|
// not applicable (no diff / no API-reference doc). api-doc-smells is
|
|
83
|
-
//
|
|
85
|
+
// Detection yield and false positives must be measured per supported syntax.
|
|
84
86
|
diffSuspicion: true,
|
|
85
87
|
referenceExistence: true,
|
|
86
88
|
apiDocSmells: true,
|
|
@@ -139,7 +141,7 @@ export function loadConfig(projectDir) {
|
|
|
139
141
|
// Merge .docguardignore patterns into config.ignore so every validator
|
|
140
142
|
// honors them without having to know about the file.
|
|
141
143
|
mergeIgnoreFile(projectDir, merged);
|
|
142
|
-
return merged;
|
|
144
|
+
return applyDocRoles(projectDir, merged);
|
|
143
145
|
} catch (e) {
|
|
144
146
|
console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
|
|
145
147
|
process.exit(1);
|
|
@@ -162,6 +164,7 @@ export function loadConfig(projectDir) {
|
|
|
162
164
|
* Returns: 'cli' | 'library' | 'webapp' | 'api' | 'unknown'
|
|
163
165
|
*/
|
|
164
166
|
function autoDetectProjectType(dir) {
|
|
167
|
+
if (hasWorkerConfig(dir)) return 'api';
|
|
165
168
|
const pkgPath = resolve(dir, 'package.json');
|
|
166
169
|
if (existsSync(pkgPath)) {
|
|
167
170
|
try {
|
package/cli/docguard.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { assertDefaultDocWrites } from './shared-doc-roles.mjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* DocGuard CLI — The enforcement tool for Canonical-Driven Development (CDD)
|
|
@@ -292,10 +293,10 @@ const COMMAND_HELP = {
|
|
|
292
293
|
examples: ['docguard memory', 'docguard memory --diff'],
|
|
293
294
|
},
|
|
294
295
|
feedback: {
|
|
295
|
-
summary: '
|
|
296
|
-
usage: 'docguard feedback [--format json]',
|
|
297
|
-
flags: [['--format json', 'Machine-readable
|
|
298
|
-
examples: ['docguard feedback'],
|
|
296
|
+
summary: 'Review detection feedback locally. Select any finding with --code or --all, check duplicates, and prepare a metadata-only issue URL. Nothing is submitted automatically.',
|
|
297
|
+
usage: 'docguard feedback [--code <CODE> | --all] [--preview] [--format json]',
|
|
298
|
+
flags: [['--code <CODE>', 'Select a finding regardless of confidence'], ['--all', 'Select every active finding'], ['--preview', 'Skip local feedback-record writes'], ['--format json', 'Machine-readable selected findings and issue/search URLs']],
|
|
299
|
+
examples: ['docguard feedback', 'docguard feedback --code TRC005 --preview', 'docguard feedback --all --format json'],
|
|
299
300
|
},
|
|
300
301
|
verify: {
|
|
301
302
|
summary: 'Extract the semantic claims in your canonical docs — documented numbers, limits, and enums (retention days, rate limits, GSI/role counts, status enums) — as a verification task list the agent checks against the code. This is the highest-value bug class (a doc value that drifted from code) and the one regex/AST cannot judge. DocGuard finds the claims; the LLM confirms them.',
|
|
@@ -537,6 +538,12 @@ async function main() {
|
|
|
537
538
|
// mcp --transport http: HTTP mount path (default /mcp).
|
|
538
539
|
flags.path = args[i + 1];
|
|
539
540
|
i++;
|
|
541
|
+
} else if (args[i] === '--code') {
|
|
542
|
+
flags.code = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : '';
|
|
543
|
+
} else if (args[i] === '--all') {
|
|
544
|
+
flags.all = true;
|
|
545
|
+
} else if (args[i] === '--preview') {
|
|
546
|
+
flags.preview = true;
|
|
540
547
|
} else if (args[i] === '--signals') {
|
|
541
548
|
flags.signals = true;
|
|
542
549
|
} else if (args[i] === '--debate') {
|
|
@@ -597,6 +604,7 @@ async function main() {
|
|
|
597
604
|
if (!headless) printBanner();
|
|
598
605
|
|
|
599
606
|
const config = loadConfig(projectDir);
|
|
607
|
+
if (['init', 'setup', 'generate'].includes(command) && !(command === 'generate' && flags.plan && !flags.write) || ['sync', 'fix'].includes(command) && flags.write || command === 'diagnose' && flags.auto) assertDefaultDocWrites(config);
|
|
600
608
|
|
|
601
609
|
// `--no-baseline` disables the committed adoption baseline for this run —
|
|
602
610
|
// threaded through config so guard, ci, report, and mcp all honor it the
|
package/cli/findings.mjs
CHANGED
|
@@ -140,32 +140,32 @@ export const CODES = {
|
|
|
140
140
|
},
|
|
141
141
|
FRS001: {
|
|
142
142
|
validator: 'freshness',
|
|
143
|
-
title: '
|
|
144
|
-
help: '
|
|
145
|
-
suppress:
|
|
143
|
+
title: 'Document review signal unavailable',
|
|
144
|
+
help: 'No committed update or dated review signal was found. Review the document’s intended behavior and implementation before recording a review date. An approved status is an author signal, not semantic verification.',
|
|
145
|
+
suppress: null,
|
|
146
146
|
},
|
|
147
147
|
FRS002: {
|
|
148
148
|
validator: 'freshness',
|
|
149
|
-
title: '
|
|
150
|
-
help: '10
|
|
149
|
+
title: 'Document review due after repository activity',
|
|
150
|
+
help: 'At least 10 code commits followed the document update or review. This repository-wide heuristic does not prove the document is stale. Review its purpose, intended behavior and relevant code before deciding which side needs a change.',
|
|
151
151
|
suppress: null,
|
|
152
152
|
},
|
|
153
153
|
FRS003: {
|
|
154
154
|
validator: 'freshness',
|
|
155
|
-
title: '
|
|
156
|
-
help: 'The
|
|
155
|
+
title: 'Document review due after elapsed repository history',
|
|
156
|
+
help: 'The document update or review predates the latest code change by more than 30 days. This repository-wide heuristic does not establish drift. Review relevance and intent before changing documentation or code.',
|
|
157
157
|
suppress: null,
|
|
158
158
|
},
|
|
159
159
|
FRS004: {
|
|
160
160
|
validator: 'freshness',
|
|
161
|
-
title: '
|
|
162
|
-
help: '
|
|
161
|
+
title: 'Release-note review due',
|
|
162
|
+
help: 'The changelog update predates the latest code change by more than a week. Review whether these changes need release notes; timing alone does not prove entries are missing.',
|
|
163
163
|
suppress: null,
|
|
164
164
|
},
|
|
165
165
|
FRS005: {
|
|
166
166
|
validator: 'freshness',
|
|
167
|
-
title: '
|
|
168
|
-
help: 'Recent commits added
|
|
167
|
+
title: 'Deviation-log review due',
|
|
168
|
+
help: 'Recent code commits added DRIFT comment lines. Review whether the deviations are already recorded or resolved; history alone does not prove the log is incomplete.',
|
|
169
169
|
suppress: null,
|
|
170
170
|
},
|
|
171
171
|
DSY001: {
|
|
@@ -350,8 +350,8 @@ export const CODES = {
|
|
|
350
350
|
},
|
|
351
351
|
TRC004: {
|
|
352
352
|
validator: 'traceability',
|
|
353
|
-
title: 'Requirement ID without test
|
|
354
|
-
help: 'A requirement
|
|
353
|
+
title: 'Requirement ID without a recognized test annotation or label',
|
|
354
|
+
help: 'A documented requirement has no recognized annotation or test label in the scanned tests. This is a traceability gap, not proof of missing behavioral coverage. Review existing tests and add an `@req <ID>` annotation or ID test label only where the requirement is verified. Write a test only if coverage is actually missing.',
|
|
355
355
|
suppress: null,
|
|
356
356
|
},
|
|
357
357
|
TRC005: {
|