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.
Files changed (51) hide show
  1. package/README.md +8 -15
  2. package/cli/commands/agent.mjs +27 -6
  3. package/cli/commands/ci.mjs +3 -0
  4. package/cli/commands/diagnose.mjs +8 -2
  5. package/cli/commands/feedback.mjs +83 -89
  6. package/cli/commands/fix.mjs +4 -0
  7. package/cli/commands/generate.mjs +3 -0
  8. package/cli/commands/guard.mjs +37 -20
  9. package/cli/commands/hooks.mjs +61 -40
  10. package/cli/commands/init.mjs +51 -5
  11. package/cli/commands/memory.mjs +29 -15
  12. package/cli/commands/report.mjs +12 -7
  13. package/cli/commands/score.mjs +39 -19
  14. package/cli/commands/sync.mjs +2 -0
  15. package/cli/commands/watch.mjs +113 -70
  16. package/cli/config.mjs +6 -3
  17. package/cli/docguard.mjs +12 -4
  18. package/cli/findings.mjs +13 -13
  19. package/cli/scanners/memory-plan.mjs +279 -134
  20. package/cli/scanners/project-type.mjs +6 -1
  21. package/cli/scanners/semantic-claims.mjs +176 -26
  22. package/cli/shared-diff.mjs +22 -1
  23. package/cli/shared-doc-roles.mjs +59 -0
  24. package/cli/shared-ignore.mjs +15 -2
  25. package/cli/shared-source.mjs +223 -1
  26. package/cli/validator-coverage.mjs +20 -0
  27. package/cli/validators/api-surface.mjs +94 -70
  28. package/cli/validators/architecture.mjs +19 -5
  29. package/cli/validators/diff-suspicion.mjs +45 -9
  30. package/cli/validators/docs-coverage.mjs +6 -5
  31. package/cli/validators/docs-diff.mjs +51 -7
  32. package/cli/validators/environment.mjs +3 -2
  33. package/cli/validators/freshness.mjs +140 -83
  34. package/cli/validators/schema-sync.mjs +3 -2
  35. package/cli/validators/security.mjs +58 -23
  36. package/cli/validators/structure.mjs +3 -1
  37. package/cli/validators/test-spec.mjs +3 -2
  38. package/cli/validators/todo-tracking.mjs +61 -28
  39. package/cli/validators/traceability.mjs +152 -38
  40. package/docs/configuration.md +41 -0
  41. package/extensions/spec-kit-docguard/extension.yml +2 -3
  42. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
  43. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  44. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  45. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  46. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
  47. package/extensions/spec-kit-docguard/templates/extensions.yml +1 -2
  48. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +74 -29
  49. package/package.json +1 -1
  50. package/schemas/docguard-config.schema.json +43 -1
  51. package/templates/ci/github-actions.yml +51 -11
@@ -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
- // ── "Memory" framing: split signals into Completeness vs Accuracy ──
166
- // Completeness = "is the memory whole?" Accuracy = "does it match code?"
167
- // No weight changes — just a derived view of the existing per-category scores.
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
- accuracy: aW ? Math.round(aP / aW) : 0,
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' : 'accuracy',
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} ${memColor(memory.accuracy)}Accuracy ${memory.accuracy}%${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 — CDD fully adopted',
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: true,
532
- status: 'met',
533
- evidence: `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}%, no unverified factual claims`,
534
- gap: null,
535
- fix: null,
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 [file, sections] of Object.entries(checks)) {
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(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
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(resolve(dir, 'docs-canonical/SECURITY.md'))) score += 25;
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(resolve(dir, 'docs-canonical/ENVIRONMENT.md'))) score += 40;
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 = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
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
  }
@@ -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.
@@ -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, existsSync, readdirSync, statSync } from 'node:fs';
11
- import { resolve, relative, extname } from 'node:path';
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
- // Run guard immediately on start
33
- runGuardQuiet(projectDir, config, flags);
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 lastChange = '';
41
-
42
- for (const dir of watchDirs) {
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
- fsWatch(dir, { persistent: true }, (eventType, filename) => {
45
- if (!filename) return;
46
- const ext = extname(filename);
47
- if (!WATCH_EXTS.has(ext)) return;
48
-
49
- const changePath = relative(projectDir, resolve(dir, filename));
50
- if (changePath === lastChange) return; // skip duplicates
51
- lastChange = changePath;
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
- // Keep process alive
67
- process.on('SIGINT', () => {
68
- console.log(`\n${c.dim} Watch stopped.${c.reset}\n`);
69
- process.exit(0);
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 = runGuardInternal(projectDir, config);
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 = [rootDir];
115
-
166
+ function collectWatchDirs(rootDir, skip, onError) {
167
+ const dirs = [];
116
168
  function walk(dir) {
117
- try {
118
- const entries = readdirSync(dir);
119
- for (const entry of entries) {
120
- if (IGNORE_DIRS.has(entry)) continue;
121
- if (entry.startsWith('.')) continue;
122
-
123
- const fullPath = resolve(dir, entry);
124
- try {
125
- const stat = statSync(fullPath);
126
- if (stat.isDirectory()) {
127
- dirs.push(fullPath);
128
- walk(fullPath);
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
- } catch { /* skip unreadable */ }
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
- // precise (zero false positives across the 6-repo corpus), and quiet when
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
- // low-yield but zero-FP, so on-by-default beats a self-counting split.
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: 'Report likely false positives back to DocGuard. Collects the low-confidence findings of a guard run, saves a full local record under .docguard/feedback/, and prints a one-click, prefilled, redacted GitHub issue URL (zero typing, no source code or secret values).',
296
- usage: 'docguard feedback [--format json]',
297
- flags: [['--format json', 'Machine-readable list of reportable findings + URLs']],
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: 'Doc has no freshness signal',
144
- help: 'The doc exists but is not committed to git and carries no review marker, so its currency cannot be assessed. Commit it, or stamp it with `<!-- docguard:last-reviewed YYYY-MM-DD -->` (or `<!-- docguard:status approved -->` for a doc generated this session).',
145
- suppress: '<!-- docguard:status approved -->',
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: 'Code moved on since the doc was last updated',
150
- help: '10+ code commits landed after the doc\'s last update/review its code-truth sections are likely stale. Run `docguard sync --write` to refresh them in one pass, or review and stamp `<!-- docguard:last-reviewed YYYY-MM-DD -->` if it is still accurate.',
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: 'Doc predates the latest code change by 30+ days',
156
- help: 'The doc\'s last update is more than 30 days older than the newest code commit. Review it against the current code, then update it or stamp it reviewed.',
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: 'CHANGELOG lagging behind code changes',
162
- help: 'Code changed but CHANGELOG.md has not been updated in over a week. Add the missing entries under [Unreleased].',
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: 'DRIFT-LOG possibly stale',
168
- help: 'Recent commits added `DRIFT:` comments but DRIFT-LOG.md has not kept pace. Log the new deviations (each DRIFT comment needs a DRIFT-LOG entry) or remove resolved markers.',
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 coverage',
354
- help: 'A requirement ID found in the docs (REQ-/FR-/SC-/T-style) has no matching reference in any test file. Add an `@req <ID>` comment to the test that verifies it. Requirement traceability is opt-in: it only activates once IDs appear in your docs.',
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: {