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.
- package/README.md +4 -4
- package/cli/commands/demo.mjs +1 -1
- package/cli/commands/diff.mjs +19 -8
- package/cli/commands/explain.mjs +178 -17
- package/cli/commands/fix.mjs +17 -2
- package/cli/commands/generate.mjs +2 -2
- package/cli/commands/guard.mjs +86 -11
- package/cli/commands/hooks.mjs +12 -7
- package/cli/commands/init.mjs +18 -6
- package/cli/commands/score.mjs +147 -61
- package/cli/commands/setup.mjs +2 -2
- package/cli/commands/trace.mjs +3 -101
- package/cli/commands/upgrade.mjs +61 -13
- package/cli/config.mjs +245 -0
- package/cli/docguard.mjs +21 -217
- package/cli/ensure-skills.mjs +24 -26
- package/cli/scanners/api-doc.mjs +17 -3
- package/cli/scanners/doc-tools.mjs +32 -15
- package/cli/scanners/frontend.mjs +24 -8
- package/cli/scanners/js-ast.mjs +432 -0
- package/cli/scanners/memory-plan.mjs +1 -1
- package/cli/scanners/py-ast.mjs +213 -0
- package/cli/scanners/routes.mjs +194 -69
- package/cli/scanners/schemas.mjs +97 -51
- package/cli/scanners/speckit.mjs +14 -0
- package/cli/shared-git.mjs +0 -0
- package/cli/shared-ignore.mjs +16 -1
- package/cli/shared-source.mjs +59 -2
- package/cli/shared-trace-patterns.mjs +118 -0
- package/cli/shared.mjs +60 -1
- package/cli/validator-markers.mjs +91 -0
- package/cli/validators/api-surface.mjs +37 -3
- package/cli/validators/canonical-sync.mjs +22 -19
- package/cli/validators/doc-quality.mjs +27 -44
- package/cli/validators/docs-coverage.mjs +13 -0
- package/cli/validators/docs-diff.mjs +16 -6
- package/cli/validators/docs-sync.mjs +4 -3
- package/cli/validators/drift.mjs +3 -2
- package/cli/validators/freshness.mjs +47 -15
- package/cli/validators/metadata-sync.mjs +21 -11
- package/cli/validators/metrics-consistency.mjs +45 -17
- package/cli/validators/security.mjs +13 -5
- package/cli/validators/structure.mjs +6 -5
- package/cli/validators/surface-sync.mjs +7 -5
- package/cli/validators/test-spec.mjs +76 -51
- package/cli/validators/todo-tracking.mjs +4 -2
- package/cli/validators/traceability.mjs +12 -54
- package/cli/writers/sections.mjs +32 -19
- package/docs/commands.md +1 -1
- package/docs/configuration.md +11 -0
- package/docs/faq.md +1 -1
- package/extensions/spec-kit-docguard/README.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -1
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +3 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +2 -2
- package/package.json +5 -3
package/cli/commands/init.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
377
|
-
|
|
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
|
|
491
|
-
ensureSkills
|
|
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
|
package/cli/commands/score.mjs
CHANGED
|
@@ -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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
570
|
-
|
|
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
|
-
|
|
573
|
-
|
|
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
|
-
|
|
579
|
-
} else
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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)))
|
|
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))
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
783
|
-
if (content
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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 ||
|
|
910
|
+
const fixCmd = failures.find(f => f.fixCmd)?.fixCmd || CATEGORY_FIX.docQuality;
|
|
819
911
|
return `${parts.join(' | ')} → Run \`${fixCmd}\``;
|
|
820
912
|
}
|
|
821
913
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
/**
|
package/cli/commands/setup.mjs
CHANGED
|
@@ -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:
|
|
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,
|
package/cli/commands/trace.mjs
CHANGED
|
@@ -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
|
-
|
|
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];
|