docguard-cli 0.9.11 → 0.11.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 (55) hide show
  1. package/PHILOSOPHY.md +59 -106
  2. package/README.md +26 -3
  3. package/cli/commands/diagnose.mjs +171 -58
  4. package/cli/commands/diff.mjs +110 -137
  5. package/cli/commands/fix.mjs +152 -4
  6. package/cli/commands/generate.mjs +148 -27
  7. package/cli/commands/guard.mjs +45 -24
  8. package/cli/commands/hooks.mjs +40 -2
  9. package/cli/commands/score.mjs +22 -0
  10. package/cli/commands/sync.mjs +123 -0
  11. package/cli/docguard.mjs +22 -0
  12. package/cli/scanners/api-doc.mjs +122 -0
  13. package/cli/scanners/doc-tools.mjs +1 -1
  14. package/cli/scanners/frontend.mjs +438 -0
  15. package/cli/scanners/integrations.mjs +116 -0
  16. package/cli/scanners/memory-plan.mjs +242 -0
  17. package/cli/scanners/project-type.mjs +310 -0
  18. package/cli/scanners/routes.mjs +194 -32
  19. package/cli/scanners/schemas.mjs +174 -1
  20. package/cli/shared-source.mjs +247 -0
  21. package/cli/validators/api-surface.mjs +254 -0
  22. package/cli/validators/architecture.mjs +4 -3
  23. package/cli/validators/changelog.mjs +45 -4
  24. package/cli/validators/doc-quality.mjs +3 -2
  25. package/cli/validators/docs-coverage.mjs +9 -14
  26. package/cli/validators/docs-diff.mjs +117 -66
  27. package/cli/validators/docs-sync.mjs +30 -24
  28. package/cli/validators/drift.mjs +6 -2
  29. package/cli/validators/environment.mjs +43 -3
  30. package/cli/validators/freshness.mjs +4 -3
  31. package/cli/validators/metadata-sync.mjs +17 -7
  32. package/cli/validators/metrics-consistency.mjs +9 -4
  33. package/cli/validators/schema-sync.mjs +19 -10
  34. package/cli/validators/security.mjs +20 -7
  35. package/cli/validators/structure.mjs +8 -1
  36. package/cli/validators/test-spec.mjs +26 -17
  37. package/cli/validators/todo-tracking.mjs +21 -8
  38. package/cli/validators/traceability.mjs +61 -36
  39. package/cli/writers/api-reference.mjs +101 -0
  40. package/cli/writers/mechanical.mjs +116 -0
  41. package/cli/writers/sections.mjs +148 -0
  42. package/commands/docguard.fix.md +19 -3
  43. package/commands/docguard.guard.md +5 -4
  44. package/docs/doc-sections.md +37 -0
  45. package/docs/quickstart.md +1 -1
  46. package/extensions/spec-kit-docguard/README.md +8 -5
  47. package/extensions/spec-kit-docguard/commands/fix.md +74 -0
  48. package/extensions/spec-kit-docguard/commands/generate.md +25 -2
  49. package/extensions/spec-kit-docguard/commands/guard.md +6 -5
  50. package/extensions/spec-kit-docguard/commands/sync.md +62 -0
  51. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +11 -1
  52. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +3 -2
  53. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +111 -0
  54. package/package.json +1 -1
  55. package/templates/commands/docguard.guard.md +3 -3
@@ -11,6 +11,8 @@ import { c } from '../shared.mjs';
11
11
  import { detectDocTools } from '../scanners/doc-tools.mjs';
12
12
  import { scanRoutesDeep } from '../scanners/routes.mjs';
13
13
  import { scanSchemasDeep, generateERDiagram } from '../scanners/schemas.mjs';
14
+ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
15
+ import { upsertSection } from '../writers/sections.mjs';
14
16
 
15
17
  const IGNORE_DIRS = new Set([
16
18
  'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
@@ -111,7 +113,96 @@ function appendStandardsCitation(content, docName) {
111
113
  return content.trimEnd() + '\n' + footer;
112
114
  }
113
115
 
116
+ /**
117
+ * `docguard generate --plan` — AI-powered Generate.
118
+ * Builds the code-truth skeleton (marked sections) and emits the agent task
119
+ * manifest. `--format json` → machine manifest for an agent; text → summary.
120
+ * `--write` → scaffold the skeleton docs (code sections filled; prose sections
121
+ * inserted as agent-task placeholders), respecting human prose via markers.
122
+ */
123
+ export function runGeneratePlan(projectDir, config, flags) {
124
+ const plan = buildMemoryPlan(projectDir, config);
125
+
126
+ if (flags.format === 'json') {
127
+ console.log(JSON.stringify({
128
+ project: config.projectName,
129
+ profile: {
130
+ languages: plan.profile.languages,
131
+ frameworks: plan.profile.frameworks,
132
+ polyglot: plan.profile.polyglot,
133
+ kind: plan.profile.kind,
134
+ ecosystems: plan.profile.ecosystems.map(e => ({ dir: e.dir, language: e.language, framework: e.framework, kind: e.kind })),
135
+ },
136
+ surface: {
137
+ endpoints: plan.surface.endpoints.length,
138
+ entities: plan.surface.entities.length,
139
+ screens: plan.surface.screens.length,
140
+ components: plan.surface.components.length,
141
+ envVars: plan.surface.envVars.length,
142
+ },
143
+ docs: plan.docs.map(d => ({
144
+ path: d.path,
145
+ sections: d.sections.map(s => s.source === 'code'
146
+ ? { id: s.id, source: 'code' }
147
+ : { id: s.id, source: 'human', task: s.task, grounding: s.grounding }),
148
+ })),
149
+ agentTasks: plan.agentTasks,
150
+ timestamp: new Date().toISOString(),
151
+ }, null, 2));
152
+ return;
153
+ }
154
+
155
+ // --write: scaffold the skeleton docs with code sections + agent-task placeholders.
156
+ if (flags.write) {
157
+ const docsDir = resolve(projectDir, 'docs-canonical');
158
+ if (!existsSync(docsDir)) mkdirSync(docsDir, { recursive: true });
159
+ let wrote = 0;
160
+ for (const doc of plan.docs) {
161
+ const full = resolve(projectDir, doc.path);
162
+ const title = basename(doc.path, '.md').replace(/-/g, ' ');
163
+ let content = existsSync(full)
164
+ ? readFileSync(full, 'utf-8')
165
+ : `# ${title}\n\n<!-- docguard:generated true -->\n`;
166
+ for (const sec of doc.sections) {
167
+ const body = sec.source === 'code'
168
+ ? sec.body
169
+ : `> **AI task:** ${sec.task}\n<!-- docguard:pending agent writes this section -->`;
170
+ content = upsertSection(content, sec.id, body, { source: sec.source }).content;
171
+ }
172
+ writeFileSync(full, content, 'utf-8');
173
+ wrote++;
174
+ }
175
+ console.log(`${c.bold}🔮 DocGuard Generate --plan --write — ${config.projectName}${c.reset}`);
176
+ console.log(` ${c.green}✅ Scaffolded ${wrote} doc(s)${c.reset} with code-truth sections + ${plan.agentTasks.length} agent task(s).`);
177
+ console.log(` ${c.dim}Now run your AI agent (/docguard.fix) to write the prose sections, then ${c.cyan}docguard guard${c.dim}.${c.reset}\n`);
178
+ return;
179
+ }
180
+
181
+ // Text summary.
182
+ console.log(`${c.bold}🔮 DocGuard Generate Plan — ${config.projectName}${c.reset}`);
183
+ console.log(`${c.dim} ${plan.profile.polyglot ? 'Polyglot' : 'Single-language'}: ${plan.profile.languages.join(', ')} | frameworks: ${plan.profile.frameworks.join(', ') || '—'} | kind: ${plan.profile.kind}${c.reset}\n`);
184
+ console.log(` ${c.bold}Code-truth surface:${c.reset} ${plan.surface.endpoints.length} endpoints · ${plan.surface.entities.length} entities · ${plan.surface.screens.length} screens · ${plan.surface.components.length} components · ${plan.surface.envVars.length} env vars\n`);
185
+ console.log(` ${c.bold}Documents to build (${plan.docs.length}):${c.reset}`);
186
+ for (const d of plan.docs) {
187
+ const code = d.sections.filter(s => s.source === 'code').length;
188
+ const prose = d.sections.filter(s => s.source === 'human').length;
189
+ console.log(` ${c.cyan}${d.path}${c.reset} ${c.dim}(${code} code section(s), ${prose} agent task(s))${c.reset}`);
190
+ }
191
+ console.log(`\n ${c.bold}🤖 Agent tasks (${plan.agentTasks.length}):${c.reset} ${c.dim}prose the AI must write, grounded in scanned facts.${c.reset}`);
192
+ for (const t of plan.agentTasks) {
193
+ console.log(` ${c.dim}• [${t.doc} → ${t.sectionId}] ${t.instruction}${c.reset}`);
194
+ }
195
+ console.log(`\n ${c.dim}Scaffold the skeleton: ${c.cyan}docguard generate --plan --write${c.dim} · Machine manifest: ${c.cyan}--plan --format json${c.reset}\n`);
196
+ }
197
+
114
198
  export function runGenerate(projectDir, config, flags) {
199
+ // --plan: emit the AI-powered "memory plan" — the agent task manifest. The CLI
200
+ // builds the code-truth skeleton (marked sections) + tells the agent exactly
201
+ // what prose to write per section. This is the language-aware Generate path.
202
+ if (flags.plan) {
203
+ return runGeneratePlan(projectDir, config, flags);
204
+ }
205
+
115
206
  console.log(`${c.bold}🔮 DocGuard Generate — ${config.projectName}${c.reset}`);
116
207
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
117
208
  console.log(`${c.dim} Scanning codebase to generate canonical documentation...${c.reset}\n`);
@@ -311,6 +402,8 @@ function detectStack(dir) {
311
402
 
312
403
  // ── Project Scanner ────────────────────────────────────────────────────────
313
404
 
405
+
406
+
314
407
  function scanProject(dir) {
315
408
  const scan = {
316
409
  routes: [],
@@ -324,6 +417,30 @@ function scanProject(dir) {
324
417
  totalLines: 0,
325
418
  };
326
419
 
420
+ scanRoutes(dir, scan);
421
+ scanModels(dir, scan);
422
+ scanServices(dir, scan);
423
+ scanTests(dir, scan);
424
+ scanComponents(dir, scan);
425
+ scanMiddlewares(dir, scan);
426
+ scanEnvVars(dir, scan);
427
+
428
+ // Count files and lines
429
+ countFilesAndLines(dir, scan);
430
+
431
+ // ── Filter test files out of source lists ──
432
+ // Test files (*.test.*, *.spec.*, __tests__/) should NOT appear as source files
433
+ const isTestFile = (f) => f.includes('__tests__') || f.includes('__test__') || /\.(test|spec)\.[^.]+$/.test(f);
434
+ scan.routes = scan.routes.filter(f => !isTestFile(f));
435
+ scan.models = scan.models.filter(f => !isTestFile(f));
436
+ scan.services = scan.services.filter(f => !isTestFile(f));
437
+ scan.components = scan.components.filter(f => !isTestFile(f));
438
+ scan.middlewares = scan.middlewares.filter(f => !isTestFile(f));
439
+
440
+ return scan;
441
+ }
442
+
443
+ function scanRoutes(dir, scan) {
327
444
  // Find routes
328
445
  ['src/app/api', 'src/routes', 'routes', 'api', 'src/api'].forEach(routeDir => {
329
446
  const fullDir = resolve(dir, routeDir);
@@ -334,7 +451,10 @@ function scanProject(dir) {
334
451
  }
335
452
  }
336
453
  });
454
+ }
337
455
 
456
+
457
+ function scanModels(dir, scan) {
338
458
  // Find models/entities
339
459
  ['src/models', 'models', 'src/entities', 'entities', 'src/schema', 'schema', 'prisma'].forEach(modelDir => {
340
460
  const fullDir = resolve(dir, modelDir);
@@ -345,7 +465,10 @@ function scanProject(dir) {
345
465
  }
346
466
  }
347
467
  });
468
+ }
348
469
 
470
+
471
+ function scanServices(dir, scan) {
349
472
  // Find services
350
473
  ['src/services', 'services', 'src/lib', 'lib'].forEach(svcDir => {
351
474
  const fullDir = resolve(dir, svcDir);
@@ -356,7 +479,10 @@ function scanProject(dir) {
356
479
  }
357
480
  }
358
481
  });
482
+ }
359
483
 
484
+
485
+ function scanTests(dir, scan) {
360
486
  // Find tests — top-level test dirs
361
487
  ['tests', 'test', '__tests__', 'spec', 'e2e'].forEach(testDir => {
362
488
  const fullDir = resolve(dir, testDir);
@@ -416,7 +542,10 @@ function scanProject(dir) {
416
542
  break; // Use first found config
417
543
  }
418
544
  }
545
+ }
419
546
 
547
+
548
+ function scanComponents(dir, scan) {
420
549
  // Find components
421
550
  ['src/components', 'components', 'src/ui'].forEach(compDir => {
422
551
  const fullDir = resolve(dir, compDir);
@@ -427,7 +556,10 @@ function scanProject(dir) {
427
556
  }
428
557
  }
429
558
  });
559
+ }
430
560
 
561
+
562
+ function scanMiddlewares(dir, scan) {
431
563
  // Find middleware
432
564
  ['src/middleware', 'middleware', 'src/middlewares'].forEach(mwDir => {
433
565
  const fullDir = resolve(dir, mwDir);
@@ -438,7 +570,10 @@ function scanProject(dir) {
438
570
  }
439
571
  }
440
572
  });
573
+ }
574
+
441
575
 
576
+ function scanEnvVars(dir, scan) {
442
577
  // Parse .env.example for env vars
443
578
  const envExample = resolve(dir, '.env.example');
444
579
  if (existsSync(envExample)) {
@@ -451,20 +586,6 @@ function scanProject(dir) {
451
586
  }
452
587
  }
453
588
  }
454
-
455
- // Count files and lines
456
- countFilesAndLines(dir, scan);
457
-
458
- // ── Filter test files out of source lists ──
459
- // Test files (*.test.*, *.spec.*, __tests__/) should NOT appear as source files
460
- const isTestFile = (f) => f.includes('__tests__') || f.includes('__test__') || /\.(test|spec)\.[^.]+$/.test(f);
461
- scan.routes = scan.routes.filter(f => !isTestFile(f));
462
- scan.models = scan.models.filter(f => !isTestFile(f));
463
- scan.services = scan.services.filter(f => !isTestFile(f));
464
- scan.components = scan.components.filter(f => !isTestFile(f));
465
- scan.middlewares = scan.middlewares.filter(f => !isTestFile(f));
466
-
467
- return scan;
468
589
  }
469
590
 
470
591
  function countFilesAndLines(dir, scan) {
@@ -535,7 +656,7 @@ function generateArchitecture(dir, config, stack, scan, flags, docTools) {
535
656
  ## 1. Introduction & Goals
536
657
  <!-- arc42: §1 — Introduction and Goals -->
537
658
 
538
- <!-- TODO: Describe what this system does, who it's for, and key quality goals -->
659
+ <!-- TBD: Describe what this system does, who it's for, and key quality goals -->
539
660
  ${config.projectName} is a ${stack.framework || stack.language || 'software'} application.
540
661
 
541
662
  ### Quality Goals
@@ -611,8 +732,8 @@ See \\\`docs-canonical/DEPLOYMENT.md\\\` for details.
611
732
  | Environment | Infrastructure | URL |
612
733
  |-------------|---------------|-----|
613
734
  | Development | localhost | http://localhost:3000 |
614
- | Staging | ${stack.hosting || 'TBD'} | <!-- TODO --> |
615
- | Production | ${stack.hosting || 'TBD'} | <!-- TODO --> |
735
+ | Staging | ${stack.hosting || 'TBD'} | <!-- TBD --> |
736
+ | Production | ${stack.hosting || 'TBD'} | <!-- TBD --> |
616
737
 
617
738
  ## 8. Crosscutting Concepts
618
739
  <!-- arc42: §8 — Crosscutting Concepts -->
@@ -715,7 +836,7 @@ ${r.description ? `- **Description:** ${r.description}` : ''}
715
836
 
716
837
  | Parameter | In | Type | Required | Description |
717
838
  |-----------|-----|------|:--------:|-------------|
718
- | <!-- TODO --> | | | | |
839
+ | <!-- TBD --> | | | | |
719
840
 
720
841
  | Status | Response |
721
842
  |--------|----------|
@@ -742,7 +863,7 @@ ${routeDetails}`;
742
863
  |----------|-------|
743
864
  | **Status** | ![Status](https://img.shields.io/badge/status-draft-yellow) |
744
865
  | **Base URL** | \`http://localhost:3000\` |
745
- | **Auth** | <!-- TODO: Describe auth mechanism --> |
866
+ | **Auth** | <!-- TBD: Describe auth mechanism --> |
746
867
  | **Total Endpoints** | ${deepRoutes.length} |
747
868
  | **Source** | ${deepRoutes[0]?.source || 'code scan'} |
748
869
 
@@ -826,7 +947,7 @@ function generateDataModel(dir, config, stack, scan, flags, deepSchemas) {
826
947
 
827
948
  | Field | Type | Required | Default | Constraints | Description |
828
949
  |-------|------|----------|---------|-------------|-------------|
829
- | <!-- TODO: Fill in fields --> | | | | | |
950
+ | <!-- TBD: Fill in fields --> | | | | | |
830
951
  `;
831
952
  }
832
953
  const fieldRows = e.fields.map(f =>
@@ -913,7 +1034,7 @@ ${erDiagram}
913
1034
 
914
1035
  | Table | Index Name | Fields | Type | Purpose |
915
1036
  |-------|-----------|--------|------|---------|
916
- | <!-- TODO: Document indexes --> | | | | |
1037
+ | <!-- TBD: Document indexes --> | | | | |
917
1038
 
918
1039
  ---
919
1040
 
@@ -1046,9 +1167,9 @@ function generateTestSpec(dir, config, stack, scan, flags) {
1046
1167
 
1047
1168
  | Metric | Target | Current |
1048
1169
  |--------|:------:|:-------:|
1049
- | Line Coverage | 80% | <!-- TODO --> |
1050
- | Branch Coverage | 70% | <!-- TODO --> |
1051
- | Function Coverage | 80% | <!-- TODO --> |
1170
+ | Line Coverage | 80% | <!-- TBD --> |
1171
+ | Branch Coverage | 70% | <!-- TBD --> |
1172
+ | Function Coverage | 80% | <!-- TBD --> |
1052
1173
 
1053
1174
  ## Service-to-Test Map
1054
1175
 
@@ -1103,7 +1224,7 @@ function generateSecurity(dir, config, stack, scan, flags) {
1103
1224
 
1104
1225
  | Method | Provider | Token Type | Expiry |
1105
1226
  |--------|---------|-----------|--------|
1106
- | ${stack.auth || '<!-- TODO -->'} | | | |
1227
+ | ${stack.auth || '<!-- TBD -->'} | | | |
1107
1228
 
1108
1229
  ## Authorization
1109
1230
 
@@ -1117,8 +1238,8 @@ function generateSecurity(dir, config, stack, scan, flags) {
1117
1238
  | Secret | Storage | Rotation | Access |
1118
1239
  |--------|---------|----------|--------|
1119
1240
  ${scan.envVars.filter(v => isSecretVar(v.name)).map(v =>
1120
- `| \`${v.name}\` | Environment Variable | <!-- TODO --> | Application |`
1121
- ).join('\n') || '| <!-- TODO --> | | | |'}
1241
+ `| \`${v.name}\` | Environment Variable | <!-- TBD --> | Application |`
1242
+ ).join('\n') || '| <!-- TBD --> | | | |'}
1122
1243
 
1123
1244
  ## Security Rules
1124
1245
 
@@ -20,6 +20,7 @@ import { validateArchitecture } from '../validators/architecture.mjs';
20
20
  import { validateFreshness } from '../validators/freshness.mjs';
21
21
  import { validateTraceability } from '../validators/traceability.mjs';
22
22
  import { validateDocsDiff } from '../validators/docs-diff.mjs';
23
+ import { validateApiSurface } from '../validators/api-surface.mjs';
23
24
  import { validateMetadataSync } from '../validators/metadata-sync.mjs';
24
25
  import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
25
26
  import { validateDocsCoverage } from '../validators/docs-coverage.mjs';
@@ -32,6 +33,38 @@ import { validateSpecKitIntegration } from '../scanners/speckit.mjs';
32
33
  * Internal guard — returns structured data, no console output, no process.exit.
33
34
  * Used by diagnose, ci, and guard --format json.
34
35
  */
36
+ /**
37
+ * Classify a validator result into a status + quality badge.
38
+ *
39
+ * Critically, a check that found NOTHING to validate (no errors, no warnings,
40
+ * total === 0) — or that explicitly reports `applicable === false` — is status
41
+ * 'na' (not applicable), NOT 'pass'. This prevents a validator from rendering a
42
+ * confident green ✅ when it actually checked nothing (the root cause of the
43
+ * "clean bill of health on out-of-sync docs" incident).
44
+ */
45
+ export function classifyResult(result) {
46
+ const hasErrors = result.errors.length > 0;
47
+ const hasWarnings = result.warnings.length > 0;
48
+
49
+ if (!hasErrors && !hasWarnings && (result.applicable === false || result.total === 0)) {
50
+ return { status: 'na', quality: null };
51
+ }
52
+
53
+ const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
54
+
55
+ // Quality label: HIGH/MEDIUM/LOW (inspired by CJE quality stratification, Lopez et al. TRACE 2026)
56
+ let quality;
57
+ if (hasErrors) {
58
+ quality = 'LOW';
59
+ } else if (hasWarnings) {
60
+ quality = 'MEDIUM';
61
+ } else {
62
+ const ratio = result.total > 0 ? result.passed / result.total : 1;
63
+ quality = ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
64
+ }
65
+ return { status, quality };
66
+ }
67
+
35
68
  export function runGuardInternal(projectDir, config) {
36
69
  const validators = config.validators || {};
37
70
  const results = [];
@@ -40,7 +73,7 @@ export function runGuardInternal(projectDir, config) {
40
73
  { key: 'structure', name: 'Structure', fn: () => validateStructure(projectDir, config) },
41
74
  { key: 'structure', name: 'Doc Sections', fn: () => validateDocSections(projectDir, config) },
42
75
  { key: 'docsSync', name: 'Docs-Sync', fn: () => validateDocsSync(projectDir, config) },
43
- { key: 'drift', name: 'Drift', fn: () => validateDrift(projectDir, config) },
76
+ { key: 'drift', name: 'Drift-Comments', fn: () => validateDrift(projectDir, config) },
44
77
  { key: 'changelog', name: 'Changelog', fn: () => validateChangelog(projectDir, config) },
45
78
  { key: 'testSpec', name: 'Test-Spec', fn: () => validateTestSpec(projectDir, config) },
46
79
  { key: 'environment', name: 'Environment', fn: () => validateEnvironment(projectDir, config) },
@@ -60,6 +93,7 @@ export function runGuardInternal(projectDir, config) {
60
93
  }},
61
94
  { key: 'traceability', name: 'Traceability', fn: () => validateTraceability(projectDir, config) },
62
95
  { key: 'docsDiff', name: 'Docs-Diff', fn: () => validateDocsDiff(projectDir, config) },
96
+ { key: 'apiSurface', name: 'API-Surface', fn: () => validateApiSurface(projectDir, config) },
63
97
  { key: 'metadataSync', name: 'Metadata-Sync', fn: () => validateMetadataSync(projectDir, config) },
64
98
  { key: 'docsCoverage', name: 'Docs-Coverage', fn: () => validateDocsCoverage(projectDir, config) },
65
99
  { key: 'docQuality', name: 'Doc-Quality', fn: () => validateDocQuality(projectDir, config) },
@@ -77,23 +111,7 @@ export function runGuardInternal(projectDir, config) {
77
111
 
78
112
  try {
79
113
  const result = fn();
80
- const hasErrors = result.errors.length > 0;
81
- const hasWarnings = result.warnings.length > 0;
82
- const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
83
-
84
- // Quality label: HIGH/MEDIUM/LOW (inspired by CJE quality stratification, Lopez et al. TRACE 2026)
85
- let quality;
86
- if (hasErrors) {
87
- quality = 'LOW';
88
- } else if (hasWarnings) {
89
- quality = 'MEDIUM';
90
- } else {
91
- // Pass — check coverage ratio for HIGH vs MEDIUM
92
- const ratio = result.total > 0 ? result.passed / result.total : 1;
93
- quality = ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
94
- }
95
-
96
- results.push({ ...result, name, key, status, quality });
114
+ results.push({ ...result, name, key, ...classifyResult(result) });
97
115
  } catch (err) {
98
116
  results.push({ name, key, status: 'fail', quality: 'LOW', errors: [err.message], warnings: [], passed: 0, total: 1 });
99
117
  }
@@ -103,12 +121,7 @@ export function runGuardInternal(projectDir, config) {
103
121
  if (validators.metricsConsistency !== false) {
104
122
  try {
105
123
  const result = validateMetricsConsistency(projectDir, config, results);
106
- const hasErrors = result.errors.length > 0;
107
- const hasWarnings = result.warnings.length > 0;
108
- const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
109
- const ratio = result.total > 0 ? result.passed / result.total : 1;
110
- const quality = hasErrors ? 'LOW' : hasWarnings ? 'MEDIUM' : ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
111
- results.push({ ...result, name: 'Metrics-Consistency', key: 'metricsConsistency', status, quality });
124
+ results.push({ ...result, name: 'Metrics-Consistency', key: 'metricsConsistency', ...classifyResult(result) });
112
125
  } catch (err) {
113
126
  results.push({ name: 'Metrics-Consistency', key: 'metricsConsistency', status: 'fail', quality: 'LOW', errors: [err.message], warnings: [], passed: 0, total: 1 });
114
127
  }
@@ -161,6 +174,14 @@ export function runGuard(projectDir, config, flags) {
161
174
  continue;
162
175
  }
163
176
 
177
+ // Not applicable — nothing to validate. Render neutrally (NOT a green pass)
178
+ // so the reader can tell "checked and clean" apart from "nothing checked".
179
+ if (v.status === 'na') {
180
+ const reason = v.note ? ` ${c.dim}(${v.note})${c.reset}` : ` ${c.dim}(nothing to validate)${c.reset}`;
181
+ console.log(` ${c.dim}➖ ${v.name}${c.reset} ${c.dim}[N/A]${c.reset}${reason}`);
182
+ continue;
183
+ }
184
+
164
185
  // Quality label badge
165
186
  const qColor = v.quality === 'HIGH' ? c.green : v.quality === 'MEDIUM' ? c.yellow : c.red;
166
187
  const qBadge = `${qColor}[${v.quality}]${c.reset}`;
@@ -128,6 +128,40 @@ exit 0
128
128
  },
129
129
  };
130
130
 
131
+ // Auto-fix variant of the pre-commit hook: apply deterministic fixes, re-stage,
132
+ // then validate. Installed with: docguard hooks --type pre-commit --auto-fix
133
+ const PRE_COMMIT_AUTOFIX = `#!/bin/sh
134
+ # DocGuard pre-commit hook (auto-fix mode)
135
+ # Applies deterministic (no-LLM) fixes, then validates.
136
+ # Install: docguard hooks --type pre-commit --auto-fix
137
+ # Remove: rm .git/hooks/pre-commit
138
+
139
+ RUN="npx docguard-cli"
140
+ if command -v docguard >/dev/null 2>&1; then RUN="docguard"; fi
141
+
142
+ echo "🛡️ DocGuard: applying mechanical fixes…"
143
+ # 1. Deterministically remove stale documented endpoints (safe, no AI).
144
+ $RUN fix --write
145
+ # 2. Re-stage anything DocGuard rewrote so the fix is part of THIS commit.
146
+ git add docs-canonical/ 2>/dev/null
147
+
148
+ # 3. Validate.
149
+ $RUN guard
150
+ EXIT_CODE=$?
151
+
152
+ if [ $EXIT_CODE -eq 1 ]; then
153
+ echo ""
154
+ echo "❌ DocGuard guard FAILED — commit blocked."
155
+ echo " Remaining issues need an AI agent (content rewrites, not mechanical):"
156
+ echo " Run: $RUN diagnose (emits ready-to-paste agent fix prompts)"
157
+ echo " To skip: git commit --no-verify"
158
+ exit 1
159
+ elif [ $EXIT_CODE -eq 2 ]; then
160
+ echo "⚠️ DocGuard guard found warnings — commit allowed"
161
+ fi
162
+ exit 0
163
+ `;
164
+
131
165
  export function runHooks(projectDir, config, flags) {
132
166
  console.log(`${c.bold}🪝 DocGuard Hooks — ${config.projectName}${c.reset}`);
133
167
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
@@ -208,9 +242,13 @@ export function runHooks(projectDir, config, flags) {
208
242
  continue;
209
243
  }
210
244
 
211
- writeFileSync(hookPath, HOOKS[name].content, 'utf-8');
245
+ // pre-commit supports an auto-fix variant (applies mechanical fixes first).
246
+ const useAutofix = name === 'pre-commit' && flags.autoFix;
247
+ const content = useAutofix ? PRE_COMMIT_AUTOFIX : HOOKS[name].content;
248
+ writeFileSync(hookPath, content, 'utf-8');
212
249
  chmodSync(hookPath, 0o755); // Make executable
213
- console.log(` ${c.green}✅ ${name}${c.reset}: ${HOOKS[name].description}`);
250
+ const desc = useAutofix ? 'Apply mechanical fixes (fix --write) then guard' : HOOKS[name].description;
251
+ console.log(` ${c.green}✅ ${name}${c.reset}: ${desc}`);
214
252
  installed++;
215
253
  }
216
254
 
@@ -26,12 +26,30 @@ export function runScore(projectDir, config, flags) {
26
26
 
27
27
  const { scores, totalScore, grade, details } = calcAllScores(projectDir, config);
28
28
 
29
+ // ── "Memory" framing: split signals into Completeness vs Accuracy ──
30
+ // Completeness = "is the memory whole?" Accuracy = "does it match code?"
31
+ // No weight changes — just a derived view of the existing per-category scores.
32
+ const COMPLETENESS = new Set(['structure', 'docQuality']);
33
+ const memory = (() => {
34
+ let cW = 0, cP = 0, aW = 0, aP = 0;
35
+ for (const [cat, s] of Object.entries(scores)) {
36
+ const w = WEIGHTS[cat] || 0;
37
+ if (COMPLETENESS.has(cat)) { cW += w; cP += s * w; }
38
+ else { aW += w; aP += s * w; }
39
+ }
40
+ return {
41
+ completeness: cW ? Math.round(cP / cW) : 0,
42
+ accuracy: aW ? Math.round(aP / aW) : 0,
43
+ };
44
+ })();
45
+
29
46
  // ── Display Results ──
30
47
  if (flags.format === 'json') {
31
48
  const result = {
32
49
  project: config.projectName,
33
50
  score: totalScore,
34
51
  grade,
52
+ memory,
35
53
  categories: {},
36
54
  };
37
55
  for (const [cat, score] of Object.entries(scores)) {
@@ -39,6 +57,7 @@ export function runScore(projectDir, config, flags) {
39
57
  score,
40
58
  weight: WEIGHTS[cat],
41
59
  weighted: Math.round((score / 100) * WEIGHTS[cat]),
60
+ axis: COMPLETENESS.has(cat) ? 'completeness' : 'accuracy',
42
61
  };
43
62
  }
44
63
  console.log(JSON.stringify(result, null, 2));
@@ -60,6 +79,9 @@ export function runScore(projectDir, config, flags) {
60
79
 
61
80
  const gradeColor = totalScore >= 80 ? c.green : totalScore >= 60 ? c.yellow : c.red;
62
81
  console.log(` ${gradeColor}${c.bold}CDD Maturity Score: ${totalScore}/100 (${grade})${c.reset}`);
82
+ // Memory framing: is the documentation memory COMPLETE and ACCURATE?
83
+ const memColor = (s) => s >= 80 ? c.green : s >= 60 ? c.yellow : c.red;
84
+ 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}`);
63
85
 
64
86
  // Grade description
65
87
  const descriptions = {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Sync Command — keep the documentation memory ALWAYS UP TO DATE.
3
+ *
4
+ * Re-derives the code-truth surface (endpoints, entities, screens, tech-stack,
5
+ * env vars) and refreshes the matching `source=code` sections of existing
6
+ * canonical docs IN PLACE — mechanically, no LLM, idempotent. Human prose is
7
+ * never touched (it lives outside markers / in `source=human` sections).
8
+ *
9
+ * When a code section changes, the prose sections in that doc are flagged for
10
+ * agent review (e.g. "endpoints changed → re-read the API overview").
11
+ *
12
+ * Default is a DRY RUN (preview); `--write` applies. `--since <ref>` adds the
13
+ * git diff as context. Only edits docguard:generated docs unless `--force`.
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { execFileSync } from 'node:child_process';
19
+ import { c } from '../shared.mjs';
20
+ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
21
+ import { getSection, replaceSection } from '../writers/sections.mjs';
22
+ import { hasGeneratedMarker } from '../writers/api-reference.mjs';
23
+
24
+ function gitChangedFiles(projectDir, since) {
25
+ const run = (args) => {
26
+ try {
27
+ return execFileSync('git', args, { cwd: projectDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] })
28
+ .split('\n').map(s => s.trim()).filter(Boolean);
29
+ } catch { return null; }
30
+ };
31
+ const committed = run(['diff', '--name-only', `${since}...HEAD`]);
32
+ if (committed === null) return null;
33
+ const working = run(['diff', '--name-only', since]) || [];
34
+ return [...new Set([...committed, ...working])];
35
+ }
36
+
37
+ export function runSync(projectDir, config, flags) {
38
+ const plan = buildMemoryPlan(projectDir, config);
39
+ const apply = !!flags.write;
40
+ const isJson = flags.format === 'json';
41
+ const changed = flags.since ? gitChangedFiles(projectDir, flags.since) : null;
42
+
43
+ const updates = []; // { doc, section, status }
44
+ const reviews = []; // { doc, section, reason }
45
+ const skipped = []; // { doc, reason }
46
+
47
+ for (const doc of plan.docs) {
48
+ const full = resolve(projectDir, doc.path);
49
+ if (!existsSync(full)) {
50
+ skipped.push({ doc: doc.path, reason: 'not present — run `generate --plan --write` to create it' });
51
+ continue;
52
+ }
53
+ let content = readFileSync(full, 'utf-8');
54
+ if (!hasGeneratedMarker(content) && !flags.force) {
55
+ skipped.push({ doc: doc.path, reason: 'not marked docguard:generated (use --force to sync anyway)' });
56
+ continue;
57
+ }
58
+
59
+ let docChanged = false;
60
+ let codeSectionChanged = false;
61
+ for (const sec of doc.sections) {
62
+ if (sec.source !== 'code') continue;
63
+ const existing = getSection(content, sec.id);
64
+ if (!existing) continue; // sync refreshes sections that already exist
65
+ if (existing.body.trim() === String(sec.body).trim()) continue; // already current
66
+ codeSectionChanged = true;
67
+ updates.push({ doc: doc.path, section: sec.id, status: apply ? 'updated' : 'stale' });
68
+ if (apply) { content = replaceSection(content, sec.id, sec.body).content; docChanged = true; }
69
+ }
70
+
71
+ // If code changed, the prose around it may need an agent's eyes.
72
+ if (codeSectionChanged) {
73
+ for (const sec of doc.sections) {
74
+ if (sec.source === 'human') {
75
+ reviews.push({ doc: doc.path, section: sec.id, reason: 'a code section in this doc changed — review the prose' });
76
+ }
77
+ }
78
+ }
79
+
80
+ if (apply && docChanged) writeFileSync(full, content, 'utf-8');
81
+ }
82
+
83
+ if (isJson) {
84
+ console.log(JSON.stringify({
85
+ project: config.projectName,
86
+ since: flags.since || null,
87
+ changedFiles: changed,
88
+ applied: apply,
89
+ updates,
90
+ reviews,
91
+ skipped,
92
+ timestamp: new Date().toISOString(),
93
+ }, null, 2));
94
+ return;
95
+ }
96
+
97
+ console.log(`${c.bold}🔄 DocGuard Sync — ${config.projectName}${c.reset}`);
98
+ if (flags.since) {
99
+ const n = changed === null ? 'git unavailable' : `${changed.length} file(s) changed since ${flags.since}`;
100
+ console.log(`${c.dim} ${n}${c.reset}`);
101
+ }
102
+ console.log(`${c.dim} ${apply ? 'Applying' : 'Dry run (use --write to apply)'}${c.reset}\n`);
103
+
104
+ if (updates.length === 0) {
105
+ console.log(` ${c.green}✅ Documentation memory is up to date — no code-truth sections drifted.${c.reset}\n`);
106
+ } else {
107
+ console.log(` ${apply ? c.green : c.yellow}${apply ? '✅ Refreshed' : '⚠️ Stale'} ${updates.length} code-truth section(s):${c.reset}`);
108
+ for (const u of updates) console.log(` ${apply ? c.green : c.yellow}${apply ? '↻' : '•'} ${u.doc} → ${u.section}${c.reset}`);
109
+ if (reviews.length > 0) {
110
+ console.log(`\n ${c.bold}🤖 Prose to review (${reviews.length}) — code changed near these sections:${c.reset}`);
111
+ for (const r of reviews) console.log(` ${c.dim}• ${r.doc} → ${r.section}${c.reset}`);
112
+ console.log(` ${c.dim}Run your AI agent (/docguard.fix) to refresh the prose, then ${c.cyan}docguard guard${c.dim}.${c.reset}`);
113
+ }
114
+ if (!apply) console.log(`\n ${c.dim}Apply mechanical refreshes: ${c.cyan}docguard sync --write${c.reset}`);
115
+ console.log('');
116
+ }
117
+
118
+ if (skipped.length > 0 && flags.verbose) {
119
+ console.log(` ${c.dim}Skipped:${c.reset}`);
120
+ for (const s of skipped) console.log(` ${c.dim}- ${s.doc}: ${s.reason}${c.reset}`);
121
+ console.log('');
122
+ }
123
+ }