docguard-cli 0.22.1 → 0.24.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 (61) hide show
  1. package/README.md +4 -4
  2. package/cli/commands/demo.mjs +1 -1
  3. package/cli/commands/diff.mjs +19 -8
  4. package/cli/commands/explain.mjs +178 -17
  5. package/cli/commands/fix.mjs +17 -2
  6. package/cli/commands/generate.mjs +2 -2
  7. package/cli/commands/guard.mjs +86 -11
  8. package/cli/commands/hooks.mjs +12 -7
  9. package/cli/commands/init.mjs +18 -6
  10. package/cli/commands/score.mjs +147 -61
  11. package/cli/commands/setup.mjs +2 -2
  12. package/cli/commands/trace.mjs +3 -101
  13. package/cli/commands/upgrade.mjs +61 -13
  14. package/cli/config.mjs +245 -0
  15. package/cli/docguard.mjs +21 -217
  16. package/cli/ensure-skills.mjs +24 -26
  17. package/cli/scanners/api-doc.mjs +17 -3
  18. package/cli/scanners/doc-tools.mjs +32 -15
  19. package/cli/scanners/frontend.mjs +24 -8
  20. package/cli/scanners/js-ast.mjs +432 -0
  21. package/cli/scanners/memory-plan.mjs +1 -1
  22. package/cli/scanners/py-ast.mjs +213 -0
  23. package/cli/scanners/routes.mjs +194 -69
  24. package/cli/scanners/schemas.mjs +97 -51
  25. package/cli/scanners/speckit.mjs +14 -0
  26. package/cli/shared-git.mjs +0 -0
  27. package/cli/shared-ignore.mjs +16 -1
  28. package/cli/shared-source.mjs +59 -2
  29. package/cli/shared-trace-patterns.mjs +118 -0
  30. package/cli/shared.mjs +60 -1
  31. package/cli/validator-markers.mjs +91 -0
  32. package/cli/validators/api-surface.mjs +37 -3
  33. package/cli/validators/canonical-sync.mjs +22 -19
  34. package/cli/validators/doc-quality.mjs +27 -44
  35. package/cli/validators/docs-coverage.mjs +13 -0
  36. package/cli/validators/docs-diff.mjs +16 -6
  37. package/cli/validators/docs-sync.mjs +4 -3
  38. package/cli/validators/drift.mjs +3 -2
  39. package/cli/validators/freshness.mjs +47 -15
  40. package/cli/validators/metadata-sync.mjs +21 -11
  41. package/cli/validators/metrics-consistency.mjs +45 -17
  42. package/cli/validators/security.mjs +13 -5
  43. package/cli/validators/structure.mjs +6 -5
  44. package/cli/validators/surface-sync.mjs +7 -5
  45. package/cli/validators/test-spec.mjs +76 -51
  46. package/cli/validators/todo-tracking.mjs +4 -2
  47. package/cli/validators/traceability.mjs +12 -54
  48. package/cli/writers/sections.mjs +32 -19
  49. package/docs/commands.md +1 -1
  50. package/docs/configuration.md +11 -0
  51. package/docs/faq.md +1 -1
  52. package/extensions/spec-kit-docguard/README.md +1 -1
  53. package/extensions/spec-kit-docguard/extension.yml +2 -2
  54. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
  55. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  56. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  57. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  58. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -1
  59. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +3 -2
  60. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +2 -2
  61. package/package.json +5 -3
@@ -14,7 +14,7 @@ import { resolve, dirname } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { createInterface } from 'node:readline';
16
16
  import { execSync } from 'node:child_process';
17
- import { c, PROFILES } from '../shared.mjs';
17
+ import { c, PROFILES, CURRENT_SCHEMA_VERSION } from '../shared.mjs';
18
18
  import { ensureSkills, detectAgentMode, detectAIAgent, isSpecKitAvailable, isSpecKitInitialized, getDetectedAgent, safeSpawnSpecify } from '../ensure-skills.mjs';
19
19
 
20
20
  // v0.20: scaffolder names that can be passed via `init --with <name>` and
@@ -287,7 +287,7 @@ export async function runInit(projectDir, config, flags) {
287
287
  // JSON-Schema-aware editor; ignored by DocGuard itself.
288
288
  $schema: 'https://raccioly.github.io/docguard/schemas/docguard-config.schema.json',
289
289
  projectName: config.projectName,
290
- version: '0.5',
290
+ version: CURRENT_SCHEMA_VERSION, // single source of truth (shared.mjs) — never hardcode
291
291
  profile: profileName,
292
292
  projectType: detectedType,
293
293
  projectTypeConfig: ptc,
@@ -373,8 +373,18 @@ poetry.lock
373
373
  const specKitAvailable = isSpecKitAvailable();
374
374
  const specKitInitialized = isSpecKitInitialized(projectDir);
375
375
 
376
- if (flags.noSpecKit) {
377
- console.log(`\n ${c.dim}⏭️ Spec Kit init skipped (--no-spec-kit).${c.reset}`);
376
+ // v0.24 (field report #1): the `starter` profile is "minimal, for side
377
+ // projects" — it skips the heavy Spec Kit framework scaffold (.specify/
378
+ // templates/scripts/memory, ~30 files) by default. DocGuard's own canonical
379
+ // docs and its lightweight agent skills/commands still install (ensureSkills
380
+ // below). Opt back in with --spec-kit. Other profiles are unaffected.
381
+ const starterSkipsSpecKit = profileName === 'starter' && !flags.specKit;
382
+
383
+ if (flags.noSpecKit || starterSkipsSpecKit) {
384
+ const why = flags.noSpecKit
385
+ ? '--no-spec-kit'
386
+ : 'starter profile is minimal — pass --spec-kit to include the framework scaffold';
387
+ console.log(`\n ${c.dim}⏭️ Spec Kit framework scaffold skipped (${why}).${c.reset}`);
378
388
  } else if (specKitAvailable && !specKitInitialized) {
379
389
  console.log(`\n ${c.bold}🌱 Spec Kit Integration${c.reset}`);
380
390
 
@@ -487,8 +497,10 @@ poetry.lock
487
497
  }
488
498
  }
489
499
 
490
- // Auto-install DocGuard skills and commands (spec-kit skills handled by specify init)
491
- ensureSkills(projectDir, flags);
500
+ // Auto-install DocGuard's own skills and commands. Thread the spec-kit skip
501
+ // decision through so ensureSkills doesn't re-trigger the framework scaffold
502
+ // we just declined for the starter profile (or --no-spec-kit).
503
+ ensureSkills(projectDir, { ...flags, noSpecKit: flags.noSpecKit || starterSkipsSpecKit });
492
504
 
493
505
  // v0.20: `docguard init --with agents,hooks,ci,badge,llms,publish` runs
494
506
  // the named scaffolders after init has finished. Each one runs in sequence
@@ -6,7 +6,7 @@
6
6
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
7
  import { resolve, join, extname } from 'node:path';
8
8
  import { execSync } from 'node:child_process';
9
- import { c } from '../shared.mjs';
9
+ import { c, docHasSection } from '../shared.mjs';
10
10
  import { validateSecurity } from '../validators/security.mjs';
11
11
  import { runGuardInternal } from './guard.mjs';
12
12
 
@@ -407,16 +407,23 @@ function calcAllScores(projectDir, config) {
407
407
  const scores = {};
408
408
  const details = {}; // Per-category failure details for actionable suggestions
409
409
 
410
- scores.structure = calcStructureScore(projectDir, config);
411
- const dqResult = calcDocQualityScore(projectDir, config);
412
- scores.docQuality = dqResult.score;
413
- details.docQuality = dqResult.failures;
414
- scores.testing = calcTestingScore(projectDir, config);
415
- scores.security = calcSecurityScore(projectDir, config);
416
- scores.environment = calcEnvironmentScore(projectDir, config);
417
- scores.drift = calcDriftScore(projectDir, config);
418
- scores.changelog = calcChangelogScore(projectDir, config);
419
- scores.architecture = calcArchitectureScore(projectDir, config);
410
+ // Every calc*Score returns { score, failures } so the "Top improvements"
411
+ // line can name the sub-checks that actually failed instead of printing a
412
+ // static per-category template (field report, Issue B).
413
+ for (const [cat, fn] of [
414
+ ['structure', calcStructureScore],
415
+ ['docQuality', calcDocQualityScore],
416
+ ['testing', calcTestingScore],
417
+ ['security', calcSecurityScore],
418
+ ['environment', calcEnvironmentScore],
419
+ ['drift', calcDriftScore],
420
+ ['changelog', calcChangelogScore],
421
+ ['architecture', calcArchitectureScore],
422
+ ]) {
423
+ const { score, failures } = fn(projectDir, config);
424
+ scores[cat] = score;
425
+ details[cat] = failures || [];
426
+ }
420
427
 
421
428
  let totalScore = 0;
422
429
  for (const [category, score] of Object.entries(scores)) {
@@ -432,23 +439,29 @@ function calcAllScores(projectDir, config) {
432
439
  function calcStructureScore(dir, config) {
433
440
  let found = 0;
434
441
  let total = 0;
442
+ const failures = [];
435
443
 
436
444
  for (const file of config.requiredFiles.canonical) {
437
445
  total++;
438
446
  if (existsSync(resolve(dir, file))) found++;
447
+ else failures.push({ issue: `missing ${file}` });
439
448
  }
440
449
 
441
450
  total++;
442
451
  const hasAgent = config.requiredFiles.agentFile.some(f => existsSync(resolve(dir, f)));
443
452
  if (hasAgent) found++;
453
+ else failures.push({ issue: `missing agent file (${config.requiredFiles.agentFile.join(' or ')})` });
444
454
 
445
455
  total++;
446
456
  if (existsSync(resolve(dir, config.requiredFiles.changelog))) found++;
457
+ else failures.push({ issue: `missing ${config.requiredFiles.changelog}` });
447
458
 
448
459
  total++;
449
460
  if (existsSync(resolve(dir, config.requiredFiles.driftLog))) found++;
461
+ else failures.push({ issue: `missing ${config.requiredFiles.driftLog}` });
450
462
 
451
- return total === 0 ? 0 : Math.round((found / total) * 100);
463
+ const score = total === 0 ? 0 : Math.round((found / total) * 100);
464
+ return { score, failures };
452
465
  }
453
466
 
454
467
  function calcDocQualityScore(dir, config) {
@@ -476,7 +489,10 @@ function calcDocQualityScore(dir, config) {
476
489
 
477
490
  for (const section of sections) {
478
491
  total++;
479
- if (content.includes(section)) {
492
+ // v0.24: synonym/number-tolerant (docHasSection) so arc42/C4 headings
493
+ // count — was a literal substring check that flagged equivalent sections
494
+ // as missing, making structured docs score worse than the skeleton.
495
+ if (docHasSection(content, section)) {
480
496
  found++;
481
497
  } else {
482
498
  failures.push({ file, issue: `missing section: ${section}`, fixCmd: `docguard fix --doc ${docName}` });
@@ -499,6 +515,7 @@ function calcDocQualityScore(dir, config) {
499
515
 
500
516
  function calcTestingScore(dir, config) {
501
517
  let score = 0;
518
+ const failures = [];
502
519
 
503
520
  // ── Check 1: Test files exist (40 pts) ──
504
521
  // Check top-level test directories
@@ -561,32 +578,47 @@ function calcTestingScore(dir, config) {
561
578
  }
562
579
 
563
580
  if (hasTopLevelTestDir || hasColocatedTests || hasPatternTests || hasConfigTests) score += 40;
581
+ else failures.push({ issue: 'no test files found (looked in tests/, src/**/__tests__, and configured testPatterns)' });
564
582
 
565
583
  // ── Check 2: TEST-SPEC.md exists (30 pts) ──
566
584
  if (existsSync(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
585
+ else failures.push({ issue: 'TEST-SPEC.md missing', fixCmd: 'docguard fix --doc test-spec' });
567
586
 
568
587
  // ── Check 3: Test config or built-in runner (15 pts) ──
569
- const testConfigs2 = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
570
- const hasTestConfig = testConfigs2.some(f => existsSync(resolve(dir, f)));
588
+ const testConfigFiles = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
589
+ let hasTestRunner = testConfigFiles.some(f => existsSync(resolve(dir, f)));
590
+
591
+ // Python: pytest config usually lives inside pyproject.toml ([tool.pytest.ini_options])
592
+ // or tox.ini ([pytest]) — not a standalone file. Detect those too, so a uv/pytest
593
+ // project isn't told to "add a test runner" it already configured (field report, Issue B).
594
+ if (!hasTestRunner) {
595
+ for (const [file, marker] of [['pyproject.toml', /\[tool\.pytest/], ['tox.ini', /\[pytest\]/]]) {
596
+ const p = resolve(dir, file);
597
+ if (!existsSync(p)) continue;
598
+ try { if (marker.test(readFileSync(p, 'utf-8'))) { hasTestRunner = true; break; } } catch { /* skip */ }
599
+ }
600
+ }
571
601
 
572
- if (hasTestConfig) {
573
- score += 15;
574
- } else {
602
+ // node:test has no config file — recognize it via projectTypeConfig or package.json.
603
+ if (!hasTestRunner) {
575
604
  const ptc = config.projectTypeConfig || {};
576
- const pkgPath = resolve(dir, 'package.json');
577
605
  if (ptc.testFramework === 'node:test') {
578
- score += 15;
579
- } else if (existsSync(pkgPath)) {
580
- try {
581
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
582
- const testScript = pkg.scripts?.test || '';
583
- if (testScript.includes('node --test') || testScript.includes('node:test')) {
584
- score += 15;
585
- }
586
- } catch { /* skip */ }
606
+ hasTestRunner = true;
607
+ } else {
608
+ const pkgPath = resolve(dir, 'package.json');
609
+ if (existsSync(pkgPath)) {
610
+ try {
611
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
612
+ const testScript = pkg.scripts?.test || '';
613
+ if (testScript.includes('node --test') || testScript.includes('node:test')) hasTestRunner = true;
614
+ } catch { /* skip */ }
615
+ }
587
616
  }
588
617
  }
589
618
 
619
+ if (hasTestRunner) score += 15;
620
+ else failures.push({ issue: 'no test runner config detected (jest/vitest/pytest/node:test)' });
621
+
590
622
  // ── Check 4: CI test step (15 pts) ──
591
623
  // Support multiple CI systems — not just GitHub Actions
592
624
  const ciFiles = [
@@ -613,8 +645,9 @@ function calcTestingScore(dir, config) {
613
645
  }
614
646
 
615
647
  if (hasCITest) score += 15;
648
+ else failures.push({ issue: 'no CI test step (.github/workflows, .gitlab-ci.yml, Jenkinsfile, etc.)' });
616
649
 
617
- return Math.min(100, score);
650
+ return { score: Math.min(100, score), failures };
618
651
  }
619
652
 
620
653
  /**
@@ -668,9 +701,11 @@ function walkForTests(d, ignoreSet) {
668
701
  function calcSecurityScore(dir, config) {
669
702
  let score = 0;
670
703
  const ptc = config.projectTypeConfig || {};
704
+ const failures = [];
671
705
 
672
706
  // SECURITY.md exists (25 pts)
673
707
  if (existsSync(resolve(dir, 'docs-canonical/SECURITY.md'))) score += 25;
708
+ else failures.push({ issue: 'SECURITY.md missing', fixCmd: 'docguard fix --doc security' });
674
709
 
675
710
  // .gitignore exists and includes .env (15 + 15 pts)
676
711
  const gitignorePath = resolve(dir, '.gitignore');
@@ -678,16 +713,22 @@ function calcSecurityScore(dir, config) {
678
713
  score += 15;
679
714
  const content = readFileSync(gitignorePath, 'utf-8');
680
715
  if (content.includes('.env')) score += 15;
716
+ else failures.push({ issue: '.gitignore does not list .env' });
717
+ } else {
718
+ failures.push({ issue: '.gitignore missing' });
681
719
  }
682
720
 
683
721
  // No .env file committed (10 pts)
684
722
  if (!existsSync(resolve(dir, '.env')) || existsSync(gitignorePath)) score += 10;
723
+ else failures.push({ issue: '.env is committed without a .gitignore' });
685
724
 
686
725
  // .env.example exists (safe template) — only check if project needs env vars (10 pts)
687
726
  if (ptc.needsEnvExample === false) {
688
727
  score += 10; // Full marks — project doesn't need env vars
689
728
  } else if (existsSync(resolve(dir, '.env.example'))) {
690
729
  score += 10;
730
+ } else {
731
+ failures.push({ issue: '.env.example missing' });
691
732
  }
692
733
 
693
734
  // No hardcoded secrets found by security validator (25 pts)
@@ -703,26 +744,31 @@ function calcSecurityScore(dir, config) {
703
744
  if (findingCount <= 2) score += 15;
704
745
  else if (findingCount <= 5) score += 5;
705
746
  // 6+ findings = 0 pts for this check
747
+ failures.push({ issue: `${findingCount} possible secret(s) / unsafe pattern(s) in code — run \`docguard guard --verbose\`` });
706
748
  }
707
749
  } catch {
708
750
  // If validator fails to run, give benefit of the doubt
709
751
  score += 25;
710
752
  }
711
753
 
712
- return Math.min(100, score);
754
+ return { score: Math.min(100, score), failures };
713
755
  }
714
756
 
715
757
  function calcEnvironmentScore(dir, config) {
716
758
  let score = 0;
717
759
  const ptc = config.projectTypeConfig || {};
760
+ const failures = [];
718
761
 
719
762
  if (existsSync(resolve(dir, 'docs-canonical/ENVIRONMENT.md'))) score += 40;
763
+ else failures.push({ issue: 'ENVIRONMENT.md missing', fixCmd: 'docguard fix --doc environment' });
720
764
 
721
765
  // .env.example — only check if project needs env vars
722
766
  if (ptc.needsEnvExample === false) {
723
767
  score += 30; // Full marks — project doesn't need env vars
724
768
  } else if (existsSync(resolve(dir, '.env.example'))) {
725
769
  score += 30;
770
+ } else {
771
+ failures.push({ issue: '.env.example missing' });
726
772
  }
727
773
 
728
774
  // Check for setup documentation
@@ -733,56 +779,79 @@ function calcEnvironmentScore(dir, config) {
733
779
  score += 30;
734
780
  } else {
735
781
  score += 15; // README exists but no setup section
782
+ failures.push({ issue: 'README has no Setup / Getting Started section' });
736
783
  }
784
+ } else {
785
+ failures.push({ issue: 'README.md missing' });
737
786
  }
738
787
 
739
- return Math.min(100, score);
788
+ return { score: Math.min(100, score), failures };
740
789
  }
741
790
 
742
791
  function calcDriftScore(dir, config) {
743
792
  // Perfect score if drift log exists and no unlogged drift comments
744
- if (!existsSync(resolve(dir, config.requiredFiles.driftLog))) return 0;
793
+ if (!existsSync(resolve(dir, config.requiredFiles.driftLog))) {
794
+ return { score: 0, failures: [{ issue: `${config.requiredFiles.driftLog} missing` }] };
795
+ }
745
796
 
746
797
  let score = 50; // Drift log exists
798
+ const failures = [];
747
799
 
748
800
  const content = readFileSync(resolve(dir, config.requiredFiles.driftLog), 'utf-8');
749
801
 
750
802
  // Has structure (headers)
751
803
  if (content.includes('## ') || content.includes('| ')) score += 25;
804
+ else failures.push({ issue: `${config.requiredFiles.driftLog} has no headers or table structure` });
752
805
 
753
806
  // Has entries (not just template)
754
807
  const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('<!--'));
755
808
  if (lines.length > 3) score += 25;
809
+ else failures.push({ issue: `${config.requiredFiles.driftLog} has no entries yet (template only)` });
756
810
 
757
- return Math.min(100, score);
811
+ return { score: Math.min(100, score), failures };
758
812
  }
759
813
 
760
814
  function calcChangelogScore(dir, config) {
761
815
  const path = resolve(dir, config.requiredFiles.changelog);
762
- if (!existsSync(path)) return 0;
816
+ if (!existsSync(path)) {
817
+ return { score: 0, failures: [{ issue: `${config.requiredFiles.changelog} missing` }] };
818
+ }
763
819
 
764
820
  let score = 40; // Exists
821
+ const failures = [];
765
822
  const content = readFileSync(path, 'utf-8');
766
823
 
767
824
  if (content.includes('[Unreleased]') || content.includes('[unreleased]')) score += 30;
825
+ else failures.push({ issue: 'no [Unreleased] section' });
768
826
  if (/## \[[\d.]+\]/.test(content)) score += 30;
827
+ else failures.push({ issue: 'no versioned release headings (## [x.y.z])' });
769
828
 
770
- return Math.min(100, score);
829
+ return { score: Math.min(100, score), failures };
771
830
  }
772
831
 
773
832
  function calcArchitectureScore(dir) {
774
833
  const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
775
- if (!existsSync(archPath)) return 0;
834
+ if (!existsSync(archPath)) {
835
+ return { score: 0, failures: [{ issue: 'ARCHITECTURE.md missing', fixCmd: 'docguard fix --doc architecture' }] };
836
+ }
776
837
 
777
838
  let score = 30;
839
+ const failures = [];
778
840
  const content = readFileSync(archPath, 'utf-8');
779
841
 
780
- if (content.includes('## Layer Boundaries') || content.includes('## Component Map')) score += 25;
842
+ // v0.24: heading checks are synonym/number-tolerant (docHasSection) so arc42
843
+ // ("## 5.4 Layer boundaries") and C4 ("## Building Block View") docs score
844
+ // their real content instead of being told to add sections they have.
845
+ if (docHasSection(content, '## Layer Boundaries') || docHasSection(content, '## Component Map')) score += 25;
846
+ else failures.push({ issue: 'no Layer Boundaries / Component Map section' });
781
847
  if (content.includes('```mermaid') || content.includes('graph ')) score += 20;
782
- if (content.includes('## External Dependencies')) score += 15;
783
- if (content.includes('## Revision History')) score += 10;
848
+ else failures.push({ issue: 'no architecture diagram (mermaid / graph)' });
849
+ if (docHasSection(content, '## External Dependencies')) score += 15;
850
+ else failures.push({ issue: 'no External Dependencies section' });
851
+ if (docHasSection(content, '## Revision History')) score += 10;
852
+ else failures.push({ issue: 'no Revision History section' });
784
853
 
785
- return Math.min(100, score);
854
+ return { score: Math.min(100, score), failures };
786
855
  }
787
856
 
788
857
  // ── Helpers ────────────────────────────────────────────────────────────────
@@ -803,37 +872,54 @@ function getGrade(score) {
803
872
  return 'F';
804
873
  }
805
874
 
875
+ // Default fix command per category, used when a failure doesn't carry its own.
876
+ const CATEGORY_FIX = {
877
+ structure: 'docguard init',
878
+ docQuality: 'docguard fix',
879
+ testing: 'docguard fix --doc test-spec',
880
+ security: 'docguard fix --doc security',
881
+ environment: 'docguard fix --doc environment',
882
+ architecture: 'docguard fix --doc architecture',
883
+ };
884
+
885
+ // Static fallback — only reached if a category scores < 100 but recorded no
886
+ // specific failure (shouldn't happen now that every deduction tracks one).
887
+ const STATIC_SUGGESTIONS = {
888
+ structure: 'Run `docguard init` to create missing documentation',
889
+ docQuality: 'Run `docguard fix` to get AI prompts for each doc that needs content',
890
+ testing: 'Add test files and create TEST-SPEC.md → Run `docguard fix --doc test-spec`',
891
+ security: 'Create SECURITY.md and add .env to .gitignore → Run `docguard fix --doc security`',
892
+ environment: 'Document env variables and create .env.example → Run `docguard fix --doc environment`',
893
+ drift: 'Create DRIFT-LOG.md and log any code deviations',
894
+ changelog: 'Maintain CHANGELOG.md with [Unreleased] section',
895
+ architecture: 'Add layer boundaries and Mermaid diagrams → Run `docguard fix --doc architecture`',
896
+ };
897
+
806
898
  function getSuggestion(category, score, details) {
807
- // Dynamic, specific suggestions based on actual failures
808
- if (category === 'docQuality' && details?.docQuality?.length > 0) {
809
- const failures = details.docQuality;
810
- // Group by doc
899
+ const failures = details?.[category];
900
+
901
+ // docQuality groups its failures by doc (they carry { file, issue, fixCmd }).
902
+ if (category === 'docQuality' && failures?.length > 0) {
811
903
  const byDoc = {};
812
904
  for (const f of failures) {
813
- const doc = f.file.replace('docs-canonical/', '');
905
+ const doc = (f.file || '').replace('docs-canonical/', '') || 'docs';
814
906
  if (!byDoc[doc]) byDoc[doc] = [];
815
907
  byDoc[doc].push(f.issue);
816
908
  }
817
909
  const parts = Object.entries(byDoc).map(([doc, issues]) => `${doc}: ${issues.join(', ')}`);
818
- const fixCmd = failures.find(f => f.fixCmd)?.fixCmd || 'docguard fix';
910
+ const fixCmd = failures.find(f => f.fixCmd)?.fixCmd || CATEGORY_FIX.docQuality;
819
911
  return `${parts.join(' | ')} → Run \`${fixCmd}\``;
820
912
  }
821
913
 
822
- const suggestions = {
823
- structure: 'Run `docguard init` to create missing documentation',
824
- docQuality: 'Run `docguard fix` to get AI prompts for each doc that needs content',
825
- testing: score < 40
826
- ? 'Add test files (tests/, src/**/__tests__/, or configure testPatterns in .docguard.json) and create TEST-SPEC.md'
827
- : 'Configure TEST-SPEC.md and add CI test step → Run `docguard fix --doc test-spec`',
828
- security: score < 50
829
- ? 'Create SECURITY.md and add .env to .gitignore → Run `docguard fix --doc security`'
830
- : 'Review security findings with `docguard guard --verbose` — configure securityIgnore for false positives',
831
- environment: 'Document env variables and create .env.example → Run `docguard fix --doc environment`',
832
- drift: 'Create DRIFT-LOG.md and log any code deviations',
833
- changelog: 'Maintain CHANGELOG.md with [Unreleased] section',
834
- architecture: 'Add layer boundaries and Mermaid diagrams → Run `docguard fix --doc architecture`',
835
- };
836
- return suggestions[category] || 'Review and improve this area';
914
+ // Every other category: name the sub-checks that actually failed, so the line
915
+ // never describes work that's already done (field report, Issue B).
916
+ if (failures?.length > 0) {
917
+ const base = failures.map(f => f.issue).join('; ');
918
+ const fixCmd = failures.find(f => f.fixCmd)?.fixCmd || CATEGORY_FIX[category];
919
+ return fixCmd ? `${base} → Run \`${fixCmd}\`` : base;
920
+ }
921
+
922
+ return STATIC_SUGGESTIONS[category] || 'Review and improve this area';
837
923
  }
838
924
 
839
925
  /**
@@ -22,7 +22,7 @@ import { resolve, dirname, basename } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { createInterface } from 'node:readline';
24
24
  import { execSync } from 'node:child_process';
25
- import { c } from '../shared.mjs';
25
+ import { c, CURRENT_SCHEMA_VERSION } from '../shared.mjs';
26
26
  import { ensureSkills, detectAgentMode, isSpecKitInitialized, getDetectedAgent } from '../ensure-skills.mjs';
27
27
 
28
28
  const __filename = fileURLToPath(import.meta.url);
@@ -133,7 +133,7 @@ export async function runSetup(projectDir, config, flags) {
133
133
 
134
134
  const defaultConfig = {
135
135
  projectName: config.projectName,
136
- version: '0.4',
136
+ version: CURRENT_SCHEMA_VERSION, // single source of truth (shared.mjs)
137
137
  profile: 'standard',
138
138
  projectType: detectedType,
139
139
  projectTypeConfig: typeDefaults[detectedType] || typeDefaults.unknown,
@@ -25,106 +25,8 @@ const CODE_EXTENSIONS = new Set([
25
25
  // false-negative warnings on Python/Rust/Go/Java projects (reported by the
26
26
  // quick-recon-tool Python user: TEST-SPEC.md was flagged unlinked even
27
27
  // though Python tests existed because `.test.mjs` didn't match `test_*.py`).
28
- // Each `glob` is now a single regex that ALSO matches the equivalent
29
- // patterns in other ecosystems we care about.
30
- const TEST_PATTERNS = [
31
- // JS/TS
32
- /\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\.test\.(mjs|cjs)$/,
33
- // Python — pytest conventions
34
- /(^|\/)test_[^/]+\.py$/, /[^/]+_test\.py$/, /(^|\/)tests?\/[^/]+\.py$/,
35
- // Go
36
- /_test\.go$/,
37
- // Java/Kotlin — JUnit/TestNG conventions
38
- /(?:Test|Tests|Spec|IT)\.(?:java|kt)$/,
39
- // Rust — tests live in tests/ or as #[cfg(test)] modules; pattern below covers integration tests
40
- /(^|\/)tests\/[^/]+\.rs$/,
41
- // Ruby/RSpec
42
- /_spec\.rb$/, /_test\.rb$/,
43
- // PHP/PHPUnit
44
- /Test\.php$/, /(^|\/)tests?\/[^/]+\.php$/,
45
- ];
28
+ import { TEST_PATTERNS, TRACE_MAP, isTraceableSource } from '../shared-trace-patterns.mjs';
46
29
 
47
- /**
48
- * Mapping of canonical documents to the code/config artifacts they trace to.
49
- * Each entry defines what source patterns prove coverage of that canonical doc.
50
- *
51
- * v0.16-P2: every glob is now multi-language. JS/TS patterns are preserved
52
- * (the most common case); Python/Rust/Go/Java/Ruby/PHP equivalents are
53
- * appended so non-JS projects don't false-negative.
54
- */
55
- const TRACE_MAP = {
56
- 'ARCHITECTURE.md': {
57
- standard: 'arc42 / C4 Model',
58
- sourcePatterns: [
59
- // Entry points: JS (index/main/app/server.[jt]sx?), Python (__main__.py, main.py, app.py, cli.py),
60
- // Go (main.go, cmd/), Rust (main.rs, lib.rs), Java (Application.java, Main.java)
61
- { label: 'Entry points', glob: /(?:^|\/)(?:index|main|app|server|cli|__main__|Application|Main)\.(?:[jt]sx?|mjs|cjs|py|go|rs|java|kt|rb)$|(?:^|\/)cmd\// },
62
- // Config files: JS (package.json/tsconfig/next.config/vite.config), Python (pyproject.toml/setup.py/setup.cfg),
63
- // Rust (Cargo.toml), Go (go.mod), Java/Kotlin (pom.xml/build.gradle), Ruby (Gemfile), PHP (composer.json)
64
- { label: 'Config files', glob: /(?:^|\/)(?:package\.json|tsconfig|next\.config|vite\.config|pyproject\.toml|setup\.(?:py|cfg)|Cargo\.toml|go\.mod|pom\.xml|build\.gradle|Gemfile|composer\.json)/ },
65
- // Route handlers + module dirs
66
- { label: 'Route handlers / modules', glob: /(?:^|\/)(?:routes?|api|pages|app|controllers?|handlers?|views?|services?)\// },
67
- ],
68
- },
69
- 'DATA-MODEL.md': {
70
- standard: 'C4 Component / ER (Chen)',
71
- sourcePatterns: [
72
- // Schema/model files: JS (schema/model/entity/migration/prisma), Python (models.py/schema.py/Pydantic/SQLAlchemy),
73
- // Go (models/), Rust (struct definitions in models/), Java (entities/)
74
- { label: 'Schema definitions', glob: /(?:schema|model|entity|migration|prisma)/i },
75
- // Type definitions: JS types.ts, Python types.py, Rust types.rs
76
- { label: 'Type definitions', glob: /(?:^|\/)types?\.(?:[jt]sx?|mjs|py|rs|go|java|kt)$/ },
77
- // ORM/database libs (any language)
78
- { label: 'Database configs', glob: /(?:drizzle|knex|sequelize|typeorm|sqlalchemy|alembic|django|diesel|sqlx|gorm|hibernate|active.?record)/i },
79
- ],
80
- },
81
- 'TEST-SPEC.md': {
82
- standard: 'ISO/IEC/IEEE 29119-3',
83
- sourcePatterns: [
84
- // Test files in any ecosystem (mirrors TEST_PATTERNS above)
85
- { label: 'Test files', glob: /\.(?:test|spec)\.(?:mjs|cjs|[jt]sx?)$|(?:^|\/)test_[^/]+\.py$|[^/]+_test\.py$|_test\.go$|(?:Test|Spec|IT)\.(?:java|kt)$|(?:^|\/)tests?\/[^/]+\.(?:rs|py|rb|php)$|_(?:spec|test)\.rb$|Test\.php$/ },
86
- // Test runner configs: JS (jest/vitest/playwright/cypress), Python (pytest.ini/tox.ini), Rust (Cargo.toml has [[test]]),
87
- // Java (pom.xml/build.gradle), Go (no config file typically)
88
- { label: 'Test config', glob: /(?:jest|vitest|playwright|cypress|pytest|tox|phpunit)\.config|(?:^|\/)pytest\.ini$|(?:^|\/)tox\.ini$|(?:^|\/)phpunit\.xml$/ },
89
- { label: 'E2E / integration tests', glob: /(?:^|\/)(?:e2e|integration|tests?\/integration)\// },
90
- ],
91
- },
92
- 'SECURITY.md': {
93
- standard: 'OWASP ASVS v4.0',
94
- sourcePatterns: [
95
- // Auth modules — semantic, language-agnostic
96
- { label: 'Auth modules', glob: /(?:auth|login|session|jwt|oauth|middleware|guard|csrf|cors|permissions?|policy)/i },
97
- // Secret configs — .env family + secrets.* / keyring patterns
98
- { label: 'Secret configs', glob: /\.env(?:\.|$)|(?:^|\/)secrets?\.(?:py|js|ts|yaml|yml|json)$|keyring/i },
99
- // Gitignore + ignore files
100
- { label: 'Ignore files', glob: /^\.(?:git|docker|npm)ignore$/ },
101
- ],
102
- },
103
- 'ENVIRONMENT.md': {
104
- standard: '12-Factor App',
105
- sourcePatterns: [
106
- // .env family across all ecosystems
107
- { label: 'Env files', glob: /\.env(?:\.|$)|(?:^|\/)\.envrc$/ },
108
- // Containerization
109
- { label: 'Container configs', glob: /(?:^|\/)(?:Dockerfile|docker-compose|\.dockerignore|Containerfile)/ },
110
- // Python venv / requirements / lock files
111
- { label: 'Python env', glob: /(?:^|\/)(?:requirements[^/]*\.txt|Pipfile|poetry\.lock|uv\.lock|pyproject\.toml)$/ },
112
- // CI/CD configs
113
- { label: 'CI/CD configs', glob: /(?:^|\/)\.(?:github|gitlab-ci|circleci|drone|gitea)/ },
114
- ],
115
- },
116
- 'API-REFERENCE.md': {
117
- standard: 'OpenAPI 3.1',
118
- sourcePatterns: [
119
- // Route handlers + Python views/urls + Java/Spring controllers
120
- { label: 'Route handlers', glob: /(?:^|\/)(?:routes?|controllers?|handlers?|views?|urls?\.py)/ },
121
- // OpenAPI / API specs
122
- { label: 'API spec', glob: /(?:openapi|swagger|asyncapi)\.(?:json|ya?ml)/ },
123
- // Middleware / decorators
124
- { label: 'API middleware', glob: /(?:^|\/)middleware\/|decorators?\.py$/ },
125
- ],
126
- },
127
- };
128
30
 
129
31
  /**
130
32
  * L-2 / S-3 — Reverse trace: given a code file, find which canonical doc
@@ -287,7 +189,7 @@ export function runTrace(projectDir, config, flags) {
287
189
  // Find matching source files for each pattern
288
190
  const traces = [];
289
191
  for (const pattern of traceInfo.sourcePatterns) {
290
- const matches = projectFiles.filter(f => pattern.glob.test(f));
192
+ const matches = projectFiles.filter(f => isTraceableSource(f) && pattern.glob.test(f));
291
193
  traces.push({
292
194
  label: pattern.label,
293
195
  matchCount: matches.length,
@@ -472,7 +374,7 @@ function findRelatedTests(projectFiles, sourcePatterns) {
472
374
  const relatedTests = new Set();
473
375
 
474
376
  for (const pattern of sourcePatterns) {
475
- const sourceFiles = projectFiles.filter(f => pattern.glob.test(f));
377
+ const sourceFiles = projectFiles.filter(f => isTraceableSource(f) && pattern.glob.test(f));
476
378
  for (const src of sourceFiles) {
477
379
  const srcBase = basename(src).replace(/\.[^.]+$/, '');
478
380
  const srcDir = src.split('/')[0];