docguard-cli 0.22.1 → 0.23.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 +3 -3
- package/cli/commands/demo.mjs +1 -1
- package/cli/commands/diff.mjs +18 -7
- package/cli/commands/trace.mjs +1 -99
- package/cli/config.mjs +228 -0
- package/cli/docguard.mjs +2 -217
- package/cli/scanners/speckit.mjs +14 -0
- package/cli/shared-trace-patterns.mjs +105 -0
- package/cli/validators/doc-quality.mjs +25 -2
- package/cli/validators/docs-diff.mjs +16 -6
- package/cli/validators/traceability.mjs +2 -52
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- 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 +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,9 +106,9 @@ Recent highlights across the v0.16 → v0.19 line:
|
|
|
106
106
|
two commits. Pinpoints regressions without re-running the full suite by hand.
|
|
107
107
|
- **`docguard upgrade --apply --pr`** — when the config schema bumps, DocGuard migrates
|
|
108
108
|
`.docguard.json` for you and (optionally) opens a PR with the change.
|
|
109
|
-
- **Language-aware
|
|
110
|
-
Java, Ruby, and PHP layouts in addition to JS/TS
|
|
111
|
-
|
|
109
|
+
- **Language-aware traceability** — both `docguard trace` *and* the guard-time Traceability validator
|
|
110
|
+
understand Python, Rust, Go, Java, Ruby, and PHP layouts in addition to JS/TS, via a shared pattern
|
|
111
|
+
set (`cli/shared-trace-patterns.mjs`) so the two never drift apart.
|
|
112
112
|
- **Per-validator severity overrides** — escalate `freshness` to `high` for production repos,
|
|
113
113
|
demote `doc-quality` to `low` for prototypes. Configurable per-project.
|
|
114
114
|
- **JSON Schema for `.docguard.json`** — IDE autocomplete, in-line docs, and validation via
|
package/cli/commands/demo.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
25
25
|
import { c } from '../shared.mjs';
|
|
26
26
|
import { runGuardInternal, classifyResult } from './guard.mjs';
|
|
27
27
|
import { runScoreInternal } from './score.mjs';
|
|
28
|
-
import { loadConfig } from '../
|
|
28
|
+
import { loadConfig } from '../config.mjs';
|
|
29
29
|
|
|
30
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
31
|
const __dirname = dirname(__filename);
|
package/cli/commands/diff.mjs
CHANGED
|
@@ -347,22 +347,33 @@ function diffTests(dir, config = {}) {
|
|
|
347
347
|
|
|
348
348
|
// Glob-aware matching (documented entries are often patterns or basenames).
|
|
349
349
|
const codeArr = [...codeTests];
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
|
|
351
|
+
// PERFORMANCE OPTIMIZATION: Pre-compile regular expressions to avoid O(N*M)
|
|
352
|
+
// instantiation bottlenecks inside the nested .filter and .some loops below.
|
|
353
|
+
const docMatchers = [...docTests].map(docEntry => {
|
|
352
354
|
const entry = String(docEntry).trim();
|
|
353
355
|
const hasSlash = entry.includes('/');
|
|
354
356
|
const target = hasSlash ? entry : basename(entry);
|
|
355
|
-
const subject = hasSlash ? codeRel : basename(codeRel);
|
|
356
357
|
const rx = new RegExp('^' + target.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*+/g, '.*') + '$');
|
|
357
|
-
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
original: docEntry,
|
|
361
|
+
hasSlash,
|
|
362
|
+
rx
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const matches = (matcher, codeRel) => {
|
|
367
|
+
const subject = matcher.hasSlash ? codeRel : basename(codeRel);
|
|
368
|
+
return matcher.rx.test(subject);
|
|
358
369
|
};
|
|
359
370
|
|
|
360
371
|
return {
|
|
361
372
|
title: 'Test Files',
|
|
362
373
|
icon: '🧪',
|
|
363
|
-
onlyInDocs:
|
|
364
|
-
onlyInCode: codeArr.filter(c => !
|
|
365
|
-
matched:
|
|
374
|
+
onlyInDocs: docMatchers.filter(m => !codeArr.some(c => matches(m, c))).map(m => m.original),
|
|
375
|
+
onlyInCode: codeArr.filter(c => !docMatchers.some(m => matches(m, c))),
|
|
376
|
+
matched: docMatchers.filter(m => codeArr.some(c => matches(m, c))).map(m => m.original),
|
|
366
377
|
};
|
|
367
378
|
}
|
|
368
379
|
|
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 } 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
|
package/cli/config.mjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocGuard — configuration loading.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from docguard.mjs (v0.23.0) to break the demo.mjs → docguard.mjs
|
|
5
|
+
* import cycle. demo.mjs runs guard/score against a temp fixture and needs
|
|
6
|
+
* loadConfig, but docguard.mjs statically imports every command (including
|
|
7
|
+
* demo). Importing loadConfig from here — which only pulls shared.mjs and
|
|
8
|
+
* shared-ignore.mjs, never a command module — keeps the import graph acyclic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { resolve, basename } from 'node:path';
|
|
13
|
+
import { c, PROFILES } from './shared.mjs';
|
|
14
|
+
import { mergeIgnoreFile } from './shared-ignore.mjs';
|
|
15
|
+
|
|
16
|
+
export function loadConfig(projectDir) {
|
|
17
|
+
const configPath = resolve(projectDir, '.docguard.json');
|
|
18
|
+
const defaults = {
|
|
19
|
+
projectName: basename(projectDir),
|
|
20
|
+
version: '0.2',
|
|
21
|
+
profile: 'standard',
|
|
22
|
+
requiredFiles: {
|
|
23
|
+
canonical: [
|
|
24
|
+
'docs-canonical/ARCHITECTURE.md',
|
|
25
|
+
'docs-canonical/DATA-MODEL.md',
|
|
26
|
+
'docs-canonical/SECURITY.md',
|
|
27
|
+
'docs-canonical/TEST-SPEC.md',
|
|
28
|
+
'docs-canonical/ENVIRONMENT.md',
|
|
29
|
+
],
|
|
30
|
+
agentFile: ['AGENTS.md', 'CLAUDE.md'],
|
|
31
|
+
changelog: 'CHANGELOG.md',
|
|
32
|
+
driftLog: 'DRIFT-LOG.md',
|
|
33
|
+
},
|
|
34
|
+
// All CDD document types — required vs optional
|
|
35
|
+
documentTypes: {
|
|
36
|
+
// Canonical (design intent) — required by default
|
|
37
|
+
'docs-canonical/ARCHITECTURE.md': { required: true, category: 'canonical', description: 'System design, components, layer boundaries' },
|
|
38
|
+
'docs-canonical/DATA-MODEL.md': { required: true, category: 'canonical', description: 'Database schemas, entities, relationships' },
|
|
39
|
+
'docs-canonical/SECURITY.md': { required: true, category: 'canonical', description: 'Authentication, authorization, secrets management' },
|
|
40
|
+
'docs-canonical/TEST-SPEC.md': { required: true, category: 'canonical', description: 'Test categories, coverage rules, service-to-test map' },
|
|
41
|
+
'docs-canonical/ENVIRONMENT.md': { required: true, category: 'canonical', description: 'Environment variables, setup steps, prerequisites' },
|
|
42
|
+
'docs-canonical/DEPLOYMENT.md': { required: false, category: 'canonical', description: 'Infrastructure, CI/CD pipeline, DNS, monitoring' },
|
|
43
|
+
'docs-canonical/ADR.md': { required: false, category: 'canonical', description: 'Architecture Decision Records with rationale' },
|
|
44
|
+
// Implementation (current state) — optional by default
|
|
45
|
+
'docs-implementation/KNOWN-GOTCHAS.md': { required: false, category: 'implementation', description: 'Lessons learned — symptom/gotcha/fix format' },
|
|
46
|
+
'docs-implementation/TROUBLESHOOTING.md': { required: false, category: 'implementation', description: 'Error diagnosis guides by category' },
|
|
47
|
+
'docs-implementation/RUNBOOKS.md': { required: false, category: 'implementation', description: 'Operational procedures (deploy, rollback, backup)' },
|
|
48
|
+
'docs-implementation/CURRENT-STATE.md': { required: false, category: 'implementation', description: 'Deployment status, feature completion, tech debt' },
|
|
49
|
+
'docs-implementation/VENDOR-BUGS.md': { required: false, category: 'implementation', description: 'Third-party bug tracker with workarounds' },
|
|
50
|
+
// Root files
|
|
51
|
+
'AGENTS.md': { required: true, category: 'agent', description: 'AI agent behavior rules and project context' },
|
|
52
|
+
'CHANGELOG.md': { required: true, category: 'tracking', description: 'All notable changes per Keep a Changelog format' },
|
|
53
|
+
'DRIFT-LOG.md': { required: true, category: 'tracking', description: 'Documented deviations from canonical docs' },
|
|
54
|
+
'ROADMAP.md': { required: false, category: 'tracking', description: 'Project phases, feature tracking, vision' },
|
|
55
|
+
},
|
|
56
|
+
sourcePatterns: {
|
|
57
|
+
services: 'src/services/**/*.{ts,js,py,java}',
|
|
58
|
+
routes: 'src/routes/**/*.{ts,js,py,java}',
|
|
59
|
+
tests: 'tests/**/*.test.{ts,js,py,java}',
|
|
60
|
+
},
|
|
61
|
+
validators: {
|
|
62
|
+
structure: true,
|
|
63
|
+
docsSync: true,
|
|
64
|
+
drift: true,
|
|
65
|
+
changelog: true,
|
|
66
|
+
architecture: false,
|
|
67
|
+
testSpec: true,
|
|
68
|
+
security: false,
|
|
69
|
+
environment: true,
|
|
70
|
+
freshness: true,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (existsSync(configPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
77
|
+
|
|
78
|
+
// Apply profile presets BEFORE merging user config
|
|
79
|
+
// Profile sets the baseline, user config can override anything
|
|
80
|
+
const profileName = userConfig.profile || defaults.profile;
|
|
81
|
+
const profilePreset = PROFILES[profileName];
|
|
82
|
+
const withProfile = profilePreset
|
|
83
|
+
? deepMerge(defaults, profilePreset)
|
|
84
|
+
: defaults;
|
|
85
|
+
|
|
86
|
+
// v0.17-P4: normalize validator/severity keys before merging so the
|
|
87
|
+
// user can write either kebab-case (`test-spec`) or camelCase (`testSpec`)
|
|
88
|
+
// and the internal lookups (always camelCase) still hit.
|
|
89
|
+
const merged = deepMerge(withProfile, normalizeConfig(userConfig));
|
|
90
|
+
merged.profile = profileName;
|
|
91
|
+
|
|
92
|
+
// Auto-detect project type if not set
|
|
93
|
+
if (!merged.projectType) {
|
|
94
|
+
merged.projectType = autoDetectProjectType(projectDir);
|
|
95
|
+
}
|
|
96
|
+
// Ensure projectTypeConfig has sensible defaults based on type
|
|
97
|
+
merged.projectTypeConfig = {
|
|
98
|
+
...getProjectTypeDefaults(merged.projectType),
|
|
99
|
+
...(merged.projectTypeConfig || {}),
|
|
100
|
+
};
|
|
101
|
+
// Normalize testPattern (string) → testPatterns (array) for backward compat
|
|
102
|
+
if (merged.testPattern && !merged.testPatterns) {
|
|
103
|
+
merged.testPatterns = [merged.testPattern];
|
|
104
|
+
} else if (merged.testPattern && merged.testPatterns) {
|
|
105
|
+
// Both set — merge, deduplicate
|
|
106
|
+
if (!merged.testPatterns.includes(merged.testPattern)) {
|
|
107
|
+
merged.testPatterns.push(merged.testPattern);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Merge .docguardignore patterns into config.ignore so every validator
|
|
111
|
+
// honors them without having to know about the file.
|
|
112
|
+
mergeIgnoreFile(projectDir, merged);
|
|
113
|
+
return merged;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// No config file — auto-detect everything
|
|
121
|
+
defaults.projectType = autoDetectProjectType(projectDir);
|
|
122
|
+
defaults.projectTypeConfig = getProjectTypeDefaults(defaults.projectType);
|
|
123
|
+
// .docguardignore is read even when no .docguard.json exists — keeps
|
|
124
|
+
// ignore-only projects (no config but want to skip paths) working.
|
|
125
|
+
mergeIgnoreFile(projectDir, defaults);
|
|
126
|
+
return defaults;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// PROFILES is exported from shared.mjs (re-exported at line 43)
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Auto-detect project type from package.json and file structure.
|
|
133
|
+
* Returns: 'cli' | 'library' | 'webapp' | 'api' | 'unknown'
|
|
134
|
+
*/
|
|
135
|
+
function autoDetectProjectType(dir) {
|
|
136
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
137
|
+
if (existsSync(pkgPath)) {
|
|
138
|
+
try {
|
|
139
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
140
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
141
|
+
|
|
142
|
+
// CLI tool: has "bin" field
|
|
143
|
+
if (pkg.bin) return 'cli';
|
|
144
|
+
|
|
145
|
+
// Web app: has a frontend framework
|
|
146
|
+
if (allDeps.next || allDeps.react || allDeps.vue || allDeps['@angular/core'] ||
|
|
147
|
+
allDeps.svelte || allDeps.nuxt || allDeps['@sveltejs/kit']) return 'webapp';
|
|
148
|
+
|
|
149
|
+
// API: has a server framework but no frontend
|
|
150
|
+
if (allDeps.express || allDeps.fastify || allDeps.hono || allDeps.koa) return 'api';
|
|
151
|
+
|
|
152
|
+
// Library: has "main" or "exports" and no framework
|
|
153
|
+
if (pkg.main || pkg.exports || pkg.module) return 'library';
|
|
154
|
+
} catch { /* fall through */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Python project
|
|
158
|
+
if (existsSync(resolve(dir, 'manage.py'))) return 'webapp';
|
|
159
|
+
if (existsSync(resolve(dir, 'setup.py')) || existsSync(resolve(dir, 'pyproject.toml'))) return 'library';
|
|
160
|
+
|
|
161
|
+
return 'unknown';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get default projectTypeConfig for a given project type.
|
|
166
|
+
*/
|
|
167
|
+
function getProjectTypeDefaults(type) {
|
|
168
|
+
const defaults = {
|
|
169
|
+
cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'node:test', runCommand: null },
|
|
170
|
+
library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'vitest', runCommand: null },
|
|
171
|
+
webapp: { needsEnvVars: true, needsEnvExample: true, needsE2E: true, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
|
|
172
|
+
api: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
|
|
173
|
+
unknown: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: null, runCommand: null },
|
|
174
|
+
};
|
|
175
|
+
return defaults[type] || defaults.unknown;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* v0.17-P4: normalize validator-key naming so users can write either
|
|
180
|
+
* `validators: { "test-spec": true }` (kebab-case, matches CLI display)
|
|
181
|
+
* or `validators: { testSpec: true }` (camelCase, matches JSON internals)
|
|
182
|
+
* in `.docguard.json`. We normalize the WHOLE config tree's known validator
|
|
183
|
+
* keys to camelCase before merging. Same treatment applied to `severity`.
|
|
184
|
+
*
|
|
185
|
+
* Non-validator keys are left alone. Unknown keys (forward-compat) are
|
|
186
|
+
* normalized blindly: kebab-case→camelCase always.
|
|
187
|
+
*/
|
|
188
|
+
const _KNOWN_VALIDATORS = [
|
|
189
|
+
'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
|
|
190
|
+
'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
|
|
191
|
+
'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
|
|
192
|
+
'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
|
|
193
|
+
'canonicalSync', 'surfaceSync', 'metricsConsistency',
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
function _kebabToCamel(k) {
|
|
197
|
+
return k.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _normalizeValidatorKeys(map) {
|
|
201
|
+
if (!map || typeof map !== 'object' || Array.isArray(map)) return map;
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const [k, v] of Object.entries(map)) {
|
|
204
|
+
const normalized = k.includes('-') ? _kebabToCamel(k) : k;
|
|
205
|
+
out[normalized] = v;
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeConfig(cfg) {
|
|
211
|
+
if (!cfg || typeof cfg !== 'object') return cfg;
|
|
212
|
+
const out = { ...cfg };
|
|
213
|
+
if (out.validators) out.validators = _normalizeValidatorKeys(out.validators);
|
|
214
|
+
if (out.severity) out.severity = _normalizeValidatorKeys(out.severity);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function deepMerge(target, source) {
|
|
219
|
+
const result = { ...target };
|
|
220
|
+
for (const key of Object.keys(source)) {
|
|
221
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
222
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
223
|
+
} else {
|
|
224
|
+
result[key] = source[key];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
package/cli/docguard.mjs
CHANGED
|
@@ -49,224 +49,9 @@ import { ensureSkills } from './ensure-skills.mjs';
|
|
|
49
49
|
|
|
50
50
|
// ── Shared constants (imported to break circular dependencies) ──────────
|
|
51
51
|
import { c, PROFILES } from './shared.mjs';
|
|
52
|
-
import {
|
|
52
|
+
import { loadConfig } from './config.mjs';
|
|
53
53
|
export { c, PROFILES };
|
|
54
54
|
|
|
55
|
-
// ── Config Loading ─────────────────────────────────────────────────────────
|
|
56
|
-
export function loadConfig(projectDir) {
|
|
57
|
-
const configPath = resolve(projectDir, '.docguard.json');
|
|
58
|
-
const defaults = {
|
|
59
|
-
projectName: basename(projectDir),
|
|
60
|
-
version: '0.2',
|
|
61
|
-
profile: 'standard',
|
|
62
|
-
requiredFiles: {
|
|
63
|
-
canonical: [
|
|
64
|
-
'docs-canonical/ARCHITECTURE.md',
|
|
65
|
-
'docs-canonical/DATA-MODEL.md',
|
|
66
|
-
'docs-canonical/SECURITY.md',
|
|
67
|
-
'docs-canonical/TEST-SPEC.md',
|
|
68
|
-
'docs-canonical/ENVIRONMENT.md',
|
|
69
|
-
],
|
|
70
|
-
agentFile: ['AGENTS.md', 'CLAUDE.md'],
|
|
71
|
-
changelog: 'CHANGELOG.md',
|
|
72
|
-
driftLog: 'DRIFT-LOG.md',
|
|
73
|
-
},
|
|
74
|
-
// All CDD document types — required vs optional
|
|
75
|
-
documentTypes: {
|
|
76
|
-
// Canonical (design intent) — required by default
|
|
77
|
-
'docs-canonical/ARCHITECTURE.md': { required: true, category: 'canonical', description: 'System design, components, layer boundaries' },
|
|
78
|
-
'docs-canonical/DATA-MODEL.md': { required: true, category: 'canonical', description: 'Database schemas, entities, relationships' },
|
|
79
|
-
'docs-canonical/SECURITY.md': { required: true, category: 'canonical', description: 'Authentication, authorization, secrets management' },
|
|
80
|
-
'docs-canonical/TEST-SPEC.md': { required: true, category: 'canonical', description: 'Test categories, coverage rules, service-to-test map' },
|
|
81
|
-
'docs-canonical/ENVIRONMENT.md': { required: true, category: 'canonical', description: 'Environment variables, setup steps, prerequisites' },
|
|
82
|
-
'docs-canonical/DEPLOYMENT.md': { required: false, category: 'canonical', description: 'Infrastructure, CI/CD pipeline, DNS, monitoring' },
|
|
83
|
-
'docs-canonical/ADR.md': { required: false, category: 'canonical', description: 'Architecture Decision Records with rationale' },
|
|
84
|
-
// Implementation (current state) — optional by default
|
|
85
|
-
'docs-implementation/KNOWN-GOTCHAS.md': { required: false, category: 'implementation', description: 'Lessons learned — symptom/gotcha/fix format' },
|
|
86
|
-
'docs-implementation/TROUBLESHOOTING.md': { required: false, category: 'implementation', description: 'Error diagnosis guides by category' },
|
|
87
|
-
'docs-implementation/RUNBOOKS.md': { required: false, category: 'implementation', description: 'Operational procedures (deploy, rollback, backup)' },
|
|
88
|
-
'docs-implementation/CURRENT-STATE.md': { required: false, category: 'implementation', description: 'Deployment status, feature completion, tech debt' },
|
|
89
|
-
'docs-implementation/VENDOR-BUGS.md': { required: false, category: 'implementation', description: 'Third-party bug tracker with workarounds' },
|
|
90
|
-
// Root files
|
|
91
|
-
'AGENTS.md': { required: true, category: 'agent', description: 'AI agent behavior rules and project context' },
|
|
92
|
-
'CHANGELOG.md': { required: true, category: 'tracking', description: 'All notable changes per Keep a Changelog format' },
|
|
93
|
-
'DRIFT-LOG.md': { required: true, category: 'tracking', description: 'Documented deviations from canonical docs' },
|
|
94
|
-
'ROADMAP.md': { required: false, category: 'tracking', description: 'Project phases, feature tracking, vision' },
|
|
95
|
-
},
|
|
96
|
-
sourcePatterns: {
|
|
97
|
-
services: 'src/services/**/*.{ts,js,py,java}',
|
|
98
|
-
routes: 'src/routes/**/*.{ts,js,py,java}',
|
|
99
|
-
tests: 'tests/**/*.test.{ts,js,py,java}',
|
|
100
|
-
},
|
|
101
|
-
validators: {
|
|
102
|
-
structure: true,
|
|
103
|
-
docsSync: true,
|
|
104
|
-
drift: true,
|
|
105
|
-
changelog: true,
|
|
106
|
-
architecture: false,
|
|
107
|
-
testSpec: true,
|
|
108
|
-
security: false,
|
|
109
|
-
environment: true,
|
|
110
|
-
freshness: true,
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (existsSync(configPath)) {
|
|
115
|
-
try {
|
|
116
|
-
const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
117
|
-
|
|
118
|
-
// Apply profile presets BEFORE merging user config
|
|
119
|
-
// Profile sets the baseline, user config can override anything
|
|
120
|
-
const profileName = userConfig.profile || defaults.profile;
|
|
121
|
-
const profilePreset = PROFILES[profileName];
|
|
122
|
-
const withProfile = profilePreset
|
|
123
|
-
? deepMerge(defaults, profilePreset)
|
|
124
|
-
: defaults;
|
|
125
|
-
|
|
126
|
-
// v0.17-P4: normalize validator/severity keys before merging so the
|
|
127
|
-
// user can write either kebab-case (`test-spec`) or camelCase (`testSpec`)
|
|
128
|
-
// and the internal lookups (always camelCase) still hit.
|
|
129
|
-
const merged = deepMerge(withProfile, normalizeConfig(userConfig));
|
|
130
|
-
merged.profile = profileName;
|
|
131
|
-
|
|
132
|
-
// Auto-detect project type if not set
|
|
133
|
-
if (!merged.projectType) {
|
|
134
|
-
merged.projectType = autoDetectProjectType(projectDir);
|
|
135
|
-
}
|
|
136
|
-
// Ensure projectTypeConfig has sensible defaults based on type
|
|
137
|
-
merged.projectTypeConfig = {
|
|
138
|
-
...getProjectTypeDefaults(merged.projectType),
|
|
139
|
-
...(merged.projectTypeConfig || {}),
|
|
140
|
-
};
|
|
141
|
-
// Normalize testPattern (string) → testPatterns (array) for backward compat
|
|
142
|
-
if (merged.testPattern && !merged.testPatterns) {
|
|
143
|
-
merged.testPatterns = [merged.testPattern];
|
|
144
|
-
} else if (merged.testPattern && merged.testPatterns) {
|
|
145
|
-
// Both set — merge, deduplicate
|
|
146
|
-
if (!merged.testPatterns.includes(merged.testPattern)) {
|
|
147
|
-
merged.testPatterns.push(merged.testPattern);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// Merge .docguardignore patterns into config.ignore so every validator
|
|
151
|
-
// honors them without having to know about the file.
|
|
152
|
-
mergeIgnoreFile(projectDir, merged);
|
|
153
|
-
return merged;
|
|
154
|
-
} catch (e) {
|
|
155
|
-
console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// No config file — auto-detect everything
|
|
161
|
-
defaults.projectType = autoDetectProjectType(projectDir);
|
|
162
|
-
defaults.projectTypeConfig = getProjectTypeDefaults(defaults.projectType);
|
|
163
|
-
// .docguardignore is read even when no .docguard.json exists — keeps
|
|
164
|
-
// ignore-only projects (no config but want to skip paths) working.
|
|
165
|
-
mergeIgnoreFile(projectDir, defaults);
|
|
166
|
-
return defaults;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// PROFILES is exported from shared.mjs (re-exported at line 43)
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Auto-detect project type from package.json and file structure.
|
|
173
|
-
* Returns: 'cli' | 'library' | 'webapp' | 'api' | 'unknown'
|
|
174
|
-
*/
|
|
175
|
-
function autoDetectProjectType(dir) {
|
|
176
|
-
const pkgPath = resolve(dir, 'package.json');
|
|
177
|
-
if (existsSync(pkgPath)) {
|
|
178
|
-
try {
|
|
179
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
180
|
-
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
181
|
-
|
|
182
|
-
// CLI tool: has "bin" field
|
|
183
|
-
if (pkg.bin) return 'cli';
|
|
184
|
-
|
|
185
|
-
// Web app: has a frontend framework
|
|
186
|
-
if (allDeps.next || allDeps.react || allDeps.vue || allDeps['@angular/core'] ||
|
|
187
|
-
allDeps.svelte || allDeps.nuxt || allDeps['@sveltejs/kit']) return 'webapp';
|
|
188
|
-
|
|
189
|
-
// API: has a server framework but no frontend
|
|
190
|
-
if (allDeps.express || allDeps.fastify || allDeps.hono || allDeps.koa) return 'api';
|
|
191
|
-
|
|
192
|
-
// Library: has "main" or "exports" and no framework
|
|
193
|
-
if (pkg.main || pkg.exports || pkg.module) return 'library';
|
|
194
|
-
} catch { /* fall through */ }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Python project
|
|
198
|
-
if (existsSync(resolve(dir, 'manage.py'))) return 'webapp';
|
|
199
|
-
if (existsSync(resolve(dir, 'setup.py')) || existsSync(resolve(dir, 'pyproject.toml'))) return 'library';
|
|
200
|
-
|
|
201
|
-
return 'unknown';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get default projectTypeConfig for a given project type.
|
|
206
|
-
*/
|
|
207
|
-
function getProjectTypeDefaults(type) {
|
|
208
|
-
const defaults = {
|
|
209
|
-
cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'node:test', runCommand: null },
|
|
210
|
-
library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'vitest', runCommand: null },
|
|
211
|
-
webapp: { needsEnvVars: true, needsEnvExample: true, needsE2E: true, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
|
|
212
|
-
api: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
|
|
213
|
-
unknown: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: null, runCommand: null },
|
|
214
|
-
};
|
|
215
|
-
return defaults[type] || defaults.unknown;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* v0.17-P4: normalize validator-key naming so users can write either
|
|
220
|
-
* `validators: { "test-spec": true }` (kebab-case, matches CLI display)
|
|
221
|
-
* or `validators: { testSpec: true }` (camelCase, matches JSON internals)
|
|
222
|
-
* in `.docguard.json`. We normalize the WHOLE config tree's known validator
|
|
223
|
-
* keys to camelCase before merging. Same treatment applied to `severity`.
|
|
224
|
-
*
|
|
225
|
-
* Non-validator keys are left alone. Unknown keys (forward-compat) are
|
|
226
|
-
* normalized blindly: kebab-case→camelCase always.
|
|
227
|
-
*/
|
|
228
|
-
const _KNOWN_VALIDATORS = [
|
|
229
|
-
'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
|
|
230
|
-
'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
|
|
231
|
-
'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
|
|
232
|
-
'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
|
|
233
|
-
'canonicalSync', 'surfaceSync', 'metricsConsistency',
|
|
234
|
-
];
|
|
235
|
-
|
|
236
|
-
function _kebabToCamel(k) {
|
|
237
|
-
return k.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function _normalizeValidatorKeys(map) {
|
|
241
|
-
if (!map || typeof map !== 'object' || Array.isArray(map)) return map;
|
|
242
|
-
const out = {};
|
|
243
|
-
for (const [k, v] of Object.entries(map)) {
|
|
244
|
-
const normalized = k.includes('-') ? _kebabToCamel(k) : k;
|
|
245
|
-
out[normalized] = v;
|
|
246
|
-
}
|
|
247
|
-
return out;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function normalizeConfig(cfg) {
|
|
251
|
-
if (!cfg || typeof cfg !== 'object') return cfg;
|
|
252
|
-
const out = { ...cfg };
|
|
253
|
-
if (out.validators) out.validators = _normalizeValidatorKeys(out.validators);
|
|
254
|
-
if (out.severity) out.severity = _normalizeValidatorKeys(out.severity);
|
|
255
|
-
return out;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function deepMerge(target, source) {
|
|
259
|
-
const result = { ...target };
|
|
260
|
-
for (const key of Object.keys(source)) {
|
|
261
|
-
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
262
|
-
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
263
|
-
} else {
|
|
264
|
-
result[key] = source[key];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
55
|
// ── Banner ─────────────────────────────────────────────────────────────────
|
|
271
56
|
function printBanner() {
|
|
272
57
|
console.log(`
|
|
@@ -288,7 +73,7 @@ ${c.bold}First-time? Try the demo (no install, no setup):${c.reset}
|
|
|
288
73
|
|
|
289
74
|
${c.bold}The Daily 5${c.reset} ${c.dim}— what you'll reach for 95% of the time${c.reset}
|
|
290
75
|
${c.green}init${c.reset} Bootstrap a project — auto-detects existing code and scans (${c.cyan}--skeleton${c.reset} for blank templates, ${c.cyan}--wizard${c.reset} for guided, ${c.cyan}--with <name>${c.reset} for scaffolders)
|
|
291
|
-
${c.green}guard${c.reset} Validate against canonical docs (
|
|
76
|
+
${c.green}guard${c.reset} Validate against canonical docs (all validators)
|
|
292
77
|
${c.green}diff${c.reset} Show gaps between docs and code (add ${c.cyan}--since <ref>${c.reset} for changed-file impact)
|
|
293
78
|
${c.green}sync${c.reset} Refresh code-truth doc sections — keeps memory always up to date
|
|
294
79
|
${c.green}score${c.reset} CDD maturity score (0-100; ${c.cyan}--diff${c.reset} for delta between refs)
|
package/cli/scanners/speckit.mjs
CHANGED
|
@@ -221,6 +221,20 @@ function validateSpecQuality(specPath) {
|
|
|
221
221
|
const issues = [];
|
|
222
222
|
const content = readFileSync(specPath, 'utf-8');
|
|
223
223
|
|
|
224
|
+
// Bugfix / lightweight specs document a defect (symptom → root cause → fix),
|
|
225
|
+
// not a feature (User Scenarios / Requirements / Success Criteria with FR/SC
|
|
226
|
+
// IDs). The full feature template doesn't fit them — forcing it produces
|
|
227
|
+
// ceremony, not clarity. Opt in with `<!-- docguard:spec-type bugfix -->`
|
|
228
|
+
// and we validate the bugfix-appropriate shape instead: the spec must still
|
|
229
|
+
// state a Root Cause and a Fix, so it's a narrower check, not a free pass.
|
|
230
|
+
if (/<!--\s*docguard:spec-type\s+(?:bugfix|lightweight|patch)\b/i.test(content)) {
|
|
231
|
+
const { missing } = checkSections(content, ['Root Cause', 'Fix']);
|
|
232
|
+
for (const section of missing) {
|
|
233
|
+
issues.push(`Bugfix spec missing "${section}" — a defect spec must state its root cause and fix`);
|
|
234
|
+
}
|
|
235
|
+
return issues;
|
|
236
|
+
}
|
|
237
|
+
|
|
224
238
|
// Check mandatory sections
|
|
225
239
|
const { missing } = checkSections(content, SPEC_MANDATORY_SECTIONS);
|
|
226
240
|
for (const section of missing) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared trace patterns — the single source of truth for doc→code traceability,
|
|
3
|
+
* used by BOTH the `docguard trace` command (cli/commands/trace.mjs) and the
|
|
4
|
+
* guard-time Traceability validator (cli/validators/traceability.mjs).
|
|
5
|
+
*
|
|
6
|
+
* Previously each file had its own copy: trace.mjs was multilingual (v0.16-P2)
|
|
7
|
+
* while the validator stayed JS/TS-only, so the README's "language-aware trace
|
|
8
|
+
* mapping" claim was false for the `guard` path. Sharing here makes the claim
|
|
9
|
+
* true and prevents the two from drifting again (field-report Issue 3).
|
|
10
|
+
*
|
|
11
|
+
* The API-REFERENCE entry additionally carries the explicit Next.js App Router
|
|
12
|
+
* pattern (app/api, pages/api) preserved from the v0.22.0 #195 fix.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const TEST_PATTERNS = [
|
|
16
|
+
// JS/TS
|
|
17
|
+
/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\.test\.(mjs|cjs)$/,
|
|
18
|
+
// Python — pytest conventions
|
|
19
|
+
/(^|\/)test_[^/]+\.py$/, /[^/]+_test\.py$/, /(^|\/)tests?\/[^/]+\.py$/,
|
|
20
|
+
// Go
|
|
21
|
+
/_test\.go$/,
|
|
22
|
+
// Java/Kotlin — JUnit/TestNG conventions
|
|
23
|
+
/(?:Test|Tests|Spec|IT)\.(?:java|kt)$/,
|
|
24
|
+
// Rust — tests live in tests/ or as #[cfg(test)] modules; pattern below covers integration tests
|
|
25
|
+
/(^|\/)tests\/[^/]+\.rs$/,
|
|
26
|
+
// Ruby/RSpec
|
|
27
|
+
/_spec\.rb$/, /_test\.rb$/,
|
|
28
|
+
// PHP/PHPUnit
|
|
29
|
+
/Test\.php$/, /(^|\/)tests?\/[^/]+\.php$/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const TRACE_MAP = {
|
|
33
|
+
'ARCHITECTURE.md': {
|
|
34
|
+
standard: 'arc42 / C4 Model',
|
|
35
|
+
sourcePatterns: [
|
|
36
|
+
// Entry points: JS (index/main/app/server.[jt]sx?), Python (__main__.py, main.py, app.py, cli.py),
|
|
37
|
+
// Go (main.go, cmd/), Rust (main.rs, lib.rs), Java (Application.java, Main.java)
|
|
38
|
+
{ label: 'Entry points', glob: /(?:^|\/)(?:index|main|app|server|cli|__main__|Application|Main)\.(?:[jt]sx?|mjs|cjs|py|go|rs|java|kt|rb)$|(?:^|\/)cmd\// },
|
|
39
|
+
// Config files: JS (package.json/tsconfig/next.config/vite.config), Python (pyproject.toml/setup.py/setup.cfg),
|
|
40
|
+
// Rust (Cargo.toml), Go (go.mod), Java/Kotlin (pom.xml/build.gradle), Ruby (Gemfile), PHP (composer.json)
|
|
41
|
+
{ 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)/ },
|
|
42
|
+
// Route handlers + module dirs
|
|
43
|
+
{ label: 'Route handlers / modules', glob: /(?:^|\/)(?:routes?|api|pages|app|controllers?|handlers?|views?|services?)\// },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
'DATA-MODEL.md': {
|
|
47
|
+
standard: 'C4 Component / ER (Chen)',
|
|
48
|
+
sourcePatterns: [
|
|
49
|
+
// Schema/model files: JS (schema/model/entity/migration/prisma), Python (models.py/schema.py/Pydantic/SQLAlchemy),
|
|
50
|
+
// Go (models/), Rust (struct definitions in models/), Java (entities/)
|
|
51
|
+
{ label: 'Schema definitions', glob: /(?:schema|model|entity|migration|prisma)/i },
|
|
52
|
+
// Type definitions: JS types.ts, Python types.py, Rust types.rs
|
|
53
|
+
{ label: 'Type definitions', glob: /(?:^|\/)types?\.(?:[jt]sx?|mjs|py|rs|go|java|kt)$/ },
|
|
54
|
+
// ORM/database libs (any language)
|
|
55
|
+
{ label: 'Database configs', glob: /(?:drizzle|knex|sequelize|typeorm|sqlalchemy|alembic|django|diesel|sqlx|gorm|hibernate|active.?record)/i },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
'TEST-SPEC.md': {
|
|
59
|
+
standard: 'ISO/IEC/IEEE 29119-3',
|
|
60
|
+
sourcePatterns: [
|
|
61
|
+
// Test files in any ecosystem (mirrors TEST_PATTERNS above)
|
|
62
|
+
{ 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$/ },
|
|
63
|
+
// Test runner configs: JS (jest/vitest/playwright/cypress), Python (pytest.ini/tox.ini), Rust (Cargo.toml has [[test]]),
|
|
64
|
+
// Java (pom.xml/build.gradle), Go (no config file typically)
|
|
65
|
+
{ label: 'Test config', glob: /(?:jest|vitest|playwright|cypress|pytest|tox|phpunit)\.config|(?:^|\/)pytest\.ini$|(?:^|\/)tox\.ini$|(?:^|\/)phpunit\.xml$/ },
|
|
66
|
+
{ label: 'E2E / integration tests', glob: /(?:^|\/)(?:e2e|integration|tests?\/integration)\// },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
'SECURITY.md': {
|
|
70
|
+
standard: 'OWASP ASVS v4.0',
|
|
71
|
+
sourcePatterns: [
|
|
72
|
+
// Auth modules — semantic, language-agnostic
|
|
73
|
+
{ label: 'Auth modules', glob: /(?:auth|login|session|jwt|oauth|middleware|guard|csrf|cors|permissions?|policy)/i },
|
|
74
|
+
// Secret configs — .env family + secrets.* / keyring patterns
|
|
75
|
+
{ label: 'Secret configs', glob: /\.env(?:\.|$)|(?:^|\/)secrets?\.(?:py|js|ts|yaml|yml|json)$|keyring/i },
|
|
76
|
+
// Gitignore + ignore files
|
|
77
|
+
{ label: 'Ignore files', glob: /^\.(?:git|docker|npm)ignore$/ },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
'ENVIRONMENT.md': {
|
|
81
|
+
standard: '12-Factor App',
|
|
82
|
+
sourcePatterns: [
|
|
83
|
+
// .env family across all ecosystems
|
|
84
|
+
{ label: 'Env files', glob: /\.env(?:\.|$)|(?:^|\/)\.envrc$/ },
|
|
85
|
+
// Containerization
|
|
86
|
+
{ label: 'Container configs', glob: /(?:^|\/)(?:Dockerfile|docker-compose|\.dockerignore|Containerfile)/ },
|
|
87
|
+
// Python venv / requirements / lock files
|
|
88
|
+
{ label: 'Python env', glob: /(?:^|\/)(?:requirements[^/]*\.txt|Pipfile|poetry\.lock|uv\.lock|pyproject\.toml)$/ },
|
|
89
|
+
// CI/CD configs
|
|
90
|
+
{ label: 'CI/CD configs', glob: /(?:^|\/)\.(?:github|gitlab-ci|circleci|drone|gitea)/ },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
'API-REFERENCE.md': {
|
|
94
|
+
standard: 'OpenAPI 3.1',
|
|
95
|
+
sourcePatterns: [
|
|
96
|
+
// Route handlers + Python views/urls + Java/Spring controllers
|
|
97
|
+
{ label: 'Route handlers', glob: /(?:^|\/)(?:routes?|controllers?|handlers?|views?|urls?\.py)/ },
|
|
98
|
+
{ label: 'Next.js API routes', glob: /(^|\/)(app|pages)\/api\// },
|
|
99
|
+
// OpenAPI / API specs
|
|
100
|
+
{ label: 'API spec', glob: /(?:openapi|swagger|asyncapi)\.(?:json|ya?ml)/ },
|
|
101
|
+
// Middleware / decorators
|
|
102
|
+
{ label: 'API middleware', glob: /(?:^|\/)middleware\/|decorators?\.py$/ },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -516,6 +516,21 @@ function getCanonicalDocs(projectDir) {
|
|
|
516
516
|
* paragraphs are scored. Documents that are mostly tables/code/reference
|
|
517
517
|
* material are skipped for readability (they'd score 0/100 unfairly).
|
|
518
518
|
*/
|
|
519
|
+
/**
|
|
520
|
+
* Parse a per-doc quality-rule override marker, e.g.
|
|
521
|
+
* <!-- docguard:quality negation-load off — security doc, prohibitive language is precise -->
|
|
522
|
+
* <!-- docguard:quality negation-load 0.35 — operational doc -->
|
|
523
|
+
* Returns { off: true } | { threshold: <number> } | null. A required reason
|
|
524
|
+
* after the value is encouraged (and self-documenting) but not enforced here.
|
|
525
|
+
*/
|
|
526
|
+
function parseQualityOverride(content, rule) {
|
|
527
|
+
const re = new RegExp('<!--\\s*docguard:quality\\s+' + rule + '\\s+(off|\\d*\\.?\\d+)\\b', 'i');
|
|
528
|
+
const m = content.match(re);
|
|
529
|
+
if (!m) return null;
|
|
530
|
+
const v = m[1].toLowerCase();
|
|
531
|
+
return v === 'off' ? { off: true } : { threshold: parseFloat(v) };
|
|
532
|
+
}
|
|
533
|
+
|
|
519
534
|
function analyzeDocument(doc) {
|
|
520
535
|
const content = readFileSync(doc.path, 'utf-8');
|
|
521
536
|
const proseText = extractProse(content);
|
|
@@ -554,6 +569,7 @@ function analyzeDocument(doc) {
|
|
|
554
569
|
conditionalLoad: conditional.ratio,
|
|
555
570
|
},
|
|
556
571
|
details: { passive, ambiguous, atomicity, negation, conditional },
|
|
572
|
+
overrides: { negationLoad: parseQualityOverride(content, 'negation-load') },
|
|
557
573
|
};
|
|
558
574
|
}
|
|
559
575
|
|
|
@@ -650,13 +666,20 @@ export function validateDocQuality(projectDir, config) {
|
|
|
650
666
|
}
|
|
651
667
|
|
|
652
668
|
// ── Check 7: Negation Load ──
|
|
669
|
+
// Per-doc override (security/operational docs legitimately use "never",
|
|
670
|
+
// "must not", "cannot") and a project-wide config threshold both honored.
|
|
653
671
|
results.total++;
|
|
654
|
-
|
|
672
|
+
const negOv = analysis.overrides?.negationLoad;
|
|
673
|
+
const negThreshold = negOv?.threshold
|
|
674
|
+
?? config.docQuality?.negationLoadThreshold
|
|
675
|
+
?? THRESHOLDS.negationLoad.warn;
|
|
676
|
+
if (negOv?.off || m.negationLoad <= negThreshold) {
|
|
655
677
|
results.passed++;
|
|
656
678
|
} else {
|
|
657
679
|
results.warnings.push(
|
|
658
680
|
`${doc.name}: High negation load (${(m.negationLoad * 100).toFixed(0)}% of sentences use negation). ` +
|
|
659
|
-
`Rephrase in positive terms: "must not fail" → "must succeed" (IEEE 830 §4.3)`
|
|
681
|
+
`Rephrase in positive terms: "must not fail" → "must succeed" (IEEE 830 §4.3). ` +
|
|
682
|
+
`If the negation is intentional, add: <!-- docguard:quality negation-load off — your reason -->`
|
|
660
683
|
);
|
|
661
684
|
}
|
|
662
685
|
|
|
@@ -169,24 +169,34 @@ function diffTests(dir, config) {
|
|
|
169
169
|
// match it against code test paths (or basenames when the entry has no slash).
|
|
170
170
|
// Exact-string comparison produced the false "N documented but not found".
|
|
171
171
|
const codeArr = [...codeTests];
|
|
172
|
-
const docArr = [...docTests];
|
|
173
172
|
|
|
174
|
-
|
|
173
|
+
// PERFORMANCE OPTIMIZATION: Pre-compile regular expressions to avoid O(N*M)
|
|
174
|
+
// instantiation bottlenecks inside the nested .filter and .some loops below.
|
|
175
|
+
const docMatchers = [...docTests].map(docEntry => {
|
|
175
176
|
const entry = String(docEntry).trim();
|
|
176
177
|
const hasSlash = entry.includes('/');
|
|
177
178
|
const target = hasSlash ? entry : basename(entry);
|
|
178
|
-
const subject = hasSlash ? codeRel : basename(codeRel);
|
|
179
179
|
// Glob -> regex: escape regex specials, then any run of '*' becomes '.*'.
|
|
180
180
|
const rx = new RegExp('^' + target
|
|
181
181
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
182
182
|
.replace(/\*+/g, '.*') + '$');
|
|
183
|
-
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
original: docEntry,
|
|
186
|
+
hasSlash,
|
|
187
|
+
rx
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const matches = (matcher, codeRel) => {
|
|
192
|
+
const subject = matcher.hasSlash ? codeRel : basename(codeRel);
|
|
193
|
+
return matcher.rx.test(subject);
|
|
184
194
|
};
|
|
185
195
|
|
|
186
196
|
return {
|
|
187
197
|
title: 'Test Files',
|
|
188
|
-
onlyInDocs:
|
|
189
|
-
onlyInCode: codeArr.filter(c => !
|
|
198
|
+
onlyInDocs: docMatchers.filter(m => !codeArr.some(c => matches(m, c))).map(m => m.original),
|
|
199
|
+
onlyInCode: codeArr.filter(c => !docMatchers.some(m => matches(m, c))),
|
|
190
200
|
};
|
|
191
201
|
}
|
|
192
202
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
17
|
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
18
|
+
import { TRACE_MAP, TEST_PATTERNS } from '../shared-trace-patterns.mjs';
|
|
18
19
|
|
|
19
20
|
const IGNORE_DIRS = new Set([
|
|
20
21
|
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
@@ -22,57 +23,6 @@ const IGNORE_DIRS = new Set([
|
|
|
22
23
|
'.amplify-hosting', '.serverless',
|
|
23
24
|
]);
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
* Mapping of canonical docs to source code patterns they should trace to.
|
|
27
|
-
*/
|
|
28
|
-
const TRACE_MAP = {
|
|
29
|
-
'ARCHITECTURE.md': {
|
|
30
|
-
sourcePatterns: [
|
|
31
|
-
{ label: 'Entry points', glob: /^(index|main|app|server)\.[jt]sx?$/ },
|
|
32
|
-
{ label: 'Config files', glob: /^(package\.json|tsconfig.*|next\.config|vite\.config)/ },
|
|
33
|
-
{ label: 'Route handlers', glob: /(routes?|api|pages|app)\// },
|
|
34
|
-
],
|
|
35
|
-
},
|
|
36
|
-
'DATA-MODEL.md': {
|
|
37
|
-
sourcePatterns: [
|
|
38
|
-
{ label: 'Schema definitions', glob: /(schema|model|entity|migration|prisma)/i },
|
|
39
|
-
{ label: 'Type definitions', glob: /types?\.[jt]sx?$/ },
|
|
40
|
-
{ label: 'Database configs', glob: /(drizzle|knex|sequelize|typeorm)/i },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
'TEST-SPEC.md': {
|
|
44
|
-
sourcePatterns: [
|
|
45
|
-
{ label: 'Test files', glob: /\.(test|spec)\.(mjs|cjs|[jt]sx?)$/ },
|
|
46
|
-
{ label: 'Test config', glob: /(jest|vitest|playwright|cypress)\.config/ },
|
|
47
|
-
{ label: 'E2E tests', glob: /(e2e|integration)\// },
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
'SECURITY.md': {
|
|
51
|
-
sourcePatterns: [
|
|
52
|
-
{ label: 'Auth modules', glob: /(auth|login|session|jwt|oauth|middleware)/i },
|
|
53
|
-
{ label: 'Secret configs', glob: /\.(env|env\.example|env\.local)$/ },
|
|
54
|
-
{ label: 'Gitignore', glob: /^\.gitignore$/ },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
'ENVIRONMENT.md': {
|
|
58
|
-
sourcePatterns: [
|
|
59
|
-
{ label: 'Env files', glob: /\.env/ },
|
|
60
|
-
{ label: 'Docker configs', glob: /(Dockerfile|docker-compose|\.dockerignore)/ },
|
|
61
|
-
{ label: 'CI/CD configs', glob: /\.(github|gitlab-ci|circleci)/ },
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
'API-REFERENCE.md': {
|
|
65
|
-
sourcePatterns: [
|
|
66
|
-
{ label: 'Route handlers', glob: /(routes?|controllers?|handlers?)\// },
|
|
67
|
-
// Next.js App Router (and Pages Router) — `app/api/`, `src/app/api/`,
|
|
68
|
-
// `pages/api/`, `src/pages/api/`. Without this, a perfectly-populated
|
|
69
|
-
// Next.js API tree gets reported as "API-REFERENCE.md — unlinked doc".
|
|
70
|
-
{ label: 'Next.js API routes', glob: /(^|\/)(app|pages)\/api\// },
|
|
71
|
-
{ label: 'OpenAPI spec', glob: /(openapi|swagger)\.(json|ya?ml)/ },
|
|
72
|
-
{ label: 'API middleware', glob: /middleware\// },
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
26
|
|
|
77
27
|
// ──── Default requirement ID patterns ────
|
|
78
28
|
// Users can override via config.traceability.requirementPattern
|
|
@@ -285,7 +235,7 @@ function collectRequirementIds(projectDir, config, patterns) {
|
|
|
285
235
|
|
|
286
236
|
function scanTestFilesForReferences(projectDir, projectFiles, patterns) {
|
|
287
237
|
const testFiles = projectFiles.filter(f =>
|
|
288
|
-
|
|
238
|
+
TEST_PATTERNS.some(p => p.test(f)) || // multilingual: JS/TS, Python, Go, Rust, Java/Kotlin, Ruby, PHP
|
|
289
239
|
/__tests__\//.test(f) ||
|
|
290
240
|
/tests?\//.test(f)
|
|
291
241
|
);
|
|
@@ -3,7 +3,7 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.23.0"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.23.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.23.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Fix Skill
|
|
15
15
|
|
|
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.
|
|
10
|
+
version: 0.23.0
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.23.0 -->
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard Skill
|
|
16
16
|
|
|
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.23.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.23.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Review Skill
|
|
15
15
|
|
|
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.23.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.23.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
|
|
|
4
4
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
5
5
|
metadata:
|
|
6
6
|
author: docguard
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.23.0
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
10
|
|
package/package.json
CHANGED