docguard-cli 0.11.0 → 0.11.1
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/cli/scanners/cdk.mjs +10 -0
- package/cli/scanners/iac.mjs +235 -0
- package/cli/shared-ignore.mjs +29 -2
- package/cli/shared-source.mjs +2 -1
- package/cli/validators/docs-coverage.mjs +125 -6
- package/cli/validators/docs-sync.mjs +49 -8
- package/cli/validators/test-spec.mjs +129 -11
- package/cli/validators/todo-tracking.mjs +55 -2
- 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/templates/ARCHITECTURE.md.template +52 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDK Detector — Re-export shim.
|
|
3
|
+
*
|
|
4
|
+
* The CDK-specific detector has been generalized into a multi-tool IaC
|
|
5
|
+
* detector at cli/scanners/iac.mjs covering CDK, Terraform, Pulumi, SAM,
|
|
6
|
+
* and Serverless Framework. This module re-exports the CDK-only API for
|
|
7
|
+
* backward compatibility. New code should import from iac.mjs directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { detectCDK, hasInfrastructureHeading } from './iac.mjs';
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IaC Detector — Identifies Infrastructure-as-Code projects.
|
|
3
|
+
*
|
|
4
|
+
* IaC code is real production source that defines cloud infrastructure.
|
|
5
|
+
* It MUST be documented in ARCHITECTURE.md, not silently ignored. This
|
|
6
|
+
* detector identifies which IaC tool the project uses so docs-coverage
|
|
7
|
+
* can emit ONE consolidated actionable warning naming the actual layout
|
|
8
|
+
* (instead of multiple generic per-directory warnings).
|
|
9
|
+
*
|
|
10
|
+
* Supported tools:
|
|
11
|
+
* - AWS CDK → cdk.json marker file
|
|
12
|
+
* - Terraform → *.tf files in any non-ignored directory
|
|
13
|
+
* - Pulumi → Pulumi.yaml marker file
|
|
14
|
+
* - AWS SAM → template.yaml/yml with "AWS::Serverless::"
|
|
15
|
+
* - Serverless Fmw → serverless.yml/serverless.yaml/serverless.ts
|
|
16
|
+
*
|
|
17
|
+
* Zero NPM dependencies — pure Node.js built-ins only.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
21
|
+
import { join, relative } from 'node:path';
|
|
22
|
+
import { DEFAULT_IGNORE_DIRS } from '../shared-ignore.mjs';
|
|
23
|
+
|
|
24
|
+
const MAX_DEPTH = 6;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-tool conventions: marker file/pattern + the directories that hold the
|
|
28
|
+
* actual IaC source. Used to construct the consolidated warning text.
|
|
29
|
+
*/
|
|
30
|
+
const TOOL_PROFILES = {
|
|
31
|
+
cdk: {
|
|
32
|
+
label: 'AWS CDK',
|
|
33
|
+
markerFile: 'cdk.json',
|
|
34
|
+
sourceDirs: ['bin/ (app entrypoint)', 'lib/stacks/', 'lib/constructs/'],
|
|
35
|
+
headingPattern: /^#+\s+(infrastructure|cdk|iac)\b/im,
|
|
36
|
+
},
|
|
37
|
+
terraform: {
|
|
38
|
+
label: 'Terraform',
|
|
39
|
+
markerFile: null, // any *.tf file
|
|
40
|
+
sourceDirs: ['*.tf (root module)', 'modules/ (reusable modules)', 'environments/ (per-env tfvars)'],
|
|
41
|
+
headingPattern: /^#+\s+(infrastructure|terraform|iac)\b/im,
|
|
42
|
+
},
|
|
43
|
+
pulumi: {
|
|
44
|
+
label: 'Pulumi',
|
|
45
|
+
markerFile: 'Pulumi.yaml',
|
|
46
|
+
sourceDirs: ['index.ts (main program)', 'stacks/', 'config/'],
|
|
47
|
+
headingPattern: /^#+\s+(infrastructure|pulumi|iac)\b/im,
|
|
48
|
+
},
|
|
49
|
+
sam: {
|
|
50
|
+
label: 'AWS SAM',
|
|
51
|
+
markerFile: 'template.yaml', // also template.yml — checked below
|
|
52
|
+
sourceDirs: ['template.yaml (SAM manifest)', 'src/ (Lambda handlers)', 'events/'],
|
|
53
|
+
headingPattern: /^#+\s+(infrastructure|sam|serverless|iac)\b/im,
|
|
54
|
+
},
|
|
55
|
+
serverless: {
|
|
56
|
+
label: 'Serverless Framework',
|
|
57
|
+
markerFile: 'serverless.yml', // also .yaml, .ts — checked below
|
|
58
|
+
sourceDirs: ['serverless.yml (manifest)', 'handlers/', 'src/'],
|
|
59
|
+
headingPattern: /^#+\s+(infrastructure|serverless|iac)\b/im,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detect every IaC tool used in the project. Walks the tree from projectDir
|
|
65
|
+
* looking for marker files, respecting DEFAULT_IGNORE_DIRS.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} projectDir - Absolute path to project root
|
|
68
|
+
* @returns {{
|
|
69
|
+
* isIaC: boolean,
|
|
70
|
+
* tools: Array<{
|
|
71
|
+
* tool: string, // 'cdk' | 'terraform' | 'pulumi' | 'sam' | 'serverless'
|
|
72
|
+
* label: string, // 'AWS CDK' etc.
|
|
73
|
+
* markerPaths: string[], // relative paths to detected marker files
|
|
74
|
+
* packageDirs: string[], // relative dirs containing the markers
|
|
75
|
+
* sourceDirs: string[], // expected source layout per tool convention
|
|
76
|
+
* }>
|
|
77
|
+
* }}
|
|
78
|
+
*/
|
|
79
|
+
export function detectIaC(projectDir) {
|
|
80
|
+
const findings = {
|
|
81
|
+
cdk: { markerPaths: [], packageDirs: [] },
|
|
82
|
+
terraform: { markerPaths: [], packageDirs: [] },
|
|
83
|
+
pulumi: { markerPaths: [], packageDirs: [] },
|
|
84
|
+
sam: { markerPaths: [], packageDirs: [] },
|
|
85
|
+
serverless: { markerPaths: [], packageDirs: [] },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const recordFinding = (tool, fullPath) => {
|
|
89
|
+
const relPath = relative(projectDir, fullPath);
|
|
90
|
+
findings[tool].markerPaths.push(relPath);
|
|
91
|
+
const pkgDir = relative(projectDir, dirnameOf(fullPath)) || '.';
|
|
92
|
+
if (!findings[tool].packageDirs.includes(pkgDir)) {
|
|
93
|
+
findings[tool].packageDirs.push(pkgDir);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const walk = (dir, depth) => {
|
|
98
|
+
if (depth > MAX_DEPTH) return;
|
|
99
|
+
let entries;
|
|
100
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
101
|
+
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (!e.isFile()) continue;
|
|
104
|
+
const full = join(dir, e.name);
|
|
105
|
+
|
|
106
|
+
// CDK
|
|
107
|
+
if (e.name === 'cdk.json') recordFinding('cdk', full);
|
|
108
|
+
|
|
109
|
+
// Terraform — any .tf file (we record one per directory, not per file)
|
|
110
|
+
if (e.name.endsWith('.tf')) {
|
|
111
|
+
const pkgDir = relative(projectDir, dir) || '.';
|
|
112
|
+
if (!findings.terraform.packageDirs.includes(pkgDir)) {
|
|
113
|
+
findings.terraform.markerPaths.push(relative(projectDir, full));
|
|
114
|
+
findings.terraform.packageDirs.push(pkgDir);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Pulumi
|
|
119
|
+
if (e.name === 'Pulumi.yaml' || e.name === 'Pulumi.yml') {
|
|
120
|
+
recordFinding('pulumi', full);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// SAM — template.yaml/yml WITH AWS::Serverless::
|
|
124
|
+
if (e.name === 'template.yaml' || e.name === 'template.yml') {
|
|
125
|
+
if (fileContains(full, 'AWS::Serverless::')) recordFinding('sam', full);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Serverless Framework
|
|
129
|
+
if (
|
|
130
|
+
e.name === 'serverless.yml' ||
|
|
131
|
+
e.name === 'serverless.yaml' ||
|
|
132
|
+
e.name === 'serverless.ts' ||
|
|
133
|
+
e.name === 'serverless.js'
|
|
134
|
+
) {
|
|
135
|
+
recordFinding('serverless', full);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
if (!e.isDirectory()) continue;
|
|
141
|
+
if (DEFAULT_IGNORE_DIRS.has(e.name)) continue;
|
|
142
|
+
if (e.name.startsWith('.')) continue;
|
|
143
|
+
walk(join(dir, e.name), depth + 1);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (existsSync(projectDir)) {
|
|
148
|
+
try {
|
|
149
|
+
if (statSync(projectDir).isDirectory()) walk(projectDir, 0);
|
|
150
|
+
} catch { /* skip */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tools = [];
|
|
154
|
+
for (const [tool, data] of Object.entries(findings)) {
|
|
155
|
+
if (data.markerPaths.length > 0) {
|
|
156
|
+
tools.push({
|
|
157
|
+
tool,
|
|
158
|
+
label: TOOL_PROFILES[tool].label,
|
|
159
|
+
markerPaths: data.markerPaths,
|
|
160
|
+
packageDirs: data.packageDirs,
|
|
161
|
+
sourceDirs: TOOL_PROFILES[tool].sourceDirs,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { isIaC: tools.length > 0, tools };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check whether ARCHITECTURE.md content includes an Infrastructure/CDK/IaC/
|
|
171
|
+
* Terraform/Pulumi/SAM heading at any level. Case-insensitive.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} archContent - Full ARCHITECTURE.md content
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
export function hasInfrastructureHeading(archContent) {
|
|
177
|
+
if (!archContent) return false;
|
|
178
|
+
return /^#+\s+(infrastructure|cdk|iac|terraform|pulumi|sam|serverless)\b/im.test(archContent);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build the consolidated warning text for a detected IaC tool.
|
|
183
|
+
* One warning per tool — names the marker location and required content.
|
|
184
|
+
*/
|
|
185
|
+
export function buildIaCWarning(toolFinding) {
|
|
186
|
+
const primary = toolFinding.markerPaths[0];
|
|
187
|
+
const pkgDir = toolFinding.packageDirs[0];
|
|
188
|
+
const where = pkgDir === '.' ? '' : pkgDir + '/';
|
|
189
|
+
const sourceList = toolFinding.sourceDirs
|
|
190
|
+
.map(s => s.startsWith('*.') ? `${where}${s}` : `${where}${s}`)
|
|
191
|
+
.join(', ');
|
|
192
|
+
return `${toolFinding.label} detected at ${primary} — add an "Infrastructure" section to ` +
|
|
193
|
+
`ARCHITECTURE.md covering ${sourceList}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function dirnameOf(p) {
|
|
199
|
+
const i = p.lastIndexOf('/');
|
|
200
|
+
if (i < 0) {
|
|
201
|
+
const j = p.lastIndexOf('\\');
|
|
202
|
+
return j < 0 ? p : p.slice(0, j);
|
|
203
|
+
}
|
|
204
|
+
return p.slice(0, i);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fileContains(filePath, needle) {
|
|
208
|
+
try {
|
|
209
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
210
|
+
return content.includes(needle);
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Backwards-compatibility shim ────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Legacy CDK-only API kept for callers that don't need multi-tool detection.
|
|
220
|
+
* Delegates to detectIaC and projects the CDK slice into the old shape.
|
|
221
|
+
*
|
|
222
|
+
* @deprecated Use detectIaC for new code.
|
|
223
|
+
*/
|
|
224
|
+
export function detectCDK(projectDir) {
|
|
225
|
+
const result = detectIaC(projectDir);
|
|
226
|
+
const cdk = result.tools.find(t => t.tool === 'cdk');
|
|
227
|
+
if (!cdk) {
|
|
228
|
+
return { isCDK: false, cdkJsonPaths: [], cdkPackageDirs: [] };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
isCDK: true,
|
|
232
|
+
cdkJsonPaths: cdk.markerPaths,
|
|
233
|
+
cdkPackageDirs: cdk.packageDirs,
|
|
234
|
+
};
|
|
235
|
+
}
|
package/cli/shared-ignore.mjs
CHANGED
|
@@ -15,6 +15,31 @@
|
|
|
15
15
|
* Zero NPM dependencies — pure Node.js built-ins only.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Canonical set of directory names that should never be scanned, regardless
|
|
20
|
+
* of validator. Build outputs, VCS internals, package caches, framework synth
|
|
21
|
+
* outputs. Validators MAY extend this with their own additions but SHOULD
|
|
22
|
+
* start from this base so behavior is consistent across the tool.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_IGNORE_DIRS = new Set([
|
|
25
|
+
// Package managers
|
|
26
|
+
'node_modules', 'vendor', '.venv', '__pycache__',
|
|
27
|
+
// VCS
|
|
28
|
+
'.git', '.jj', '.hg', '.svn',
|
|
29
|
+
// Build outputs — JS/TS, Rust/Java, generic
|
|
30
|
+
'dist', 'build', 'out', 'coverage', 'target', '.gradle',
|
|
31
|
+
// Framework synth/cache
|
|
32
|
+
'.next', '.nuxt', '.turbo', '.vercel', '.cache', '.svelte-kit', 'cdk.out',
|
|
33
|
+
// OS
|
|
34
|
+
'.DS_Store',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Regex for paths that must always be rejected at any depth, regardless of
|
|
38
|
+
// the glob pattern matching them. These are duplicate file trees (worktrees)
|
|
39
|
+
// or runtime caches that should NEVER be treated as primary source.
|
|
40
|
+
const ALWAYS_REJECT_PATH_RE =
|
|
41
|
+
/(?:^|[/\\])(?:node_modules|\.claude[/\\]worktrees|\.git[/\\]worktrees|\.jj)(?:[/\\]|$)/;
|
|
42
|
+
|
|
18
43
|
/**
|
|
19
44
|
* Convert a glob pattern to a RegExp.
|
|
20
45
|
* Supports: * (any chars except /), ** (any path segments), . (literal dot).
|
|
@@ -111,8 +136,10 @@ function globToMatchRegex(pattern) {
|
|
|
111
136
|
export function globMatch(relPath, patterns) {
|
|
112
137
|
if (!relPath || !patterns || patterns.length === 0) return false;
|
|
113
138
|
|
|
114
|
-
// Always reject paths
|
|
115
|
-
|
|
139
|
+
// Always reject paths inside node_modules / worktree copies / .jj at any
|
|
140
|
+
// depth. A user's testPatterns like "**/*.test.ts" would otherwise match
|
|
141
|
+
// duplicate trees under .claude/worktrees and inflate test counts.
|
|
142
|
+
if (ALWAYS_REJECT_PATH_RE.test(relPath)) return false;
|
|
116
143
|
|
|
117
144
|
const regexes = patterns.map(p => globToMatchRegex(p));
|
|
118
145
|
return regexes.some(r => r.test(relPath));
|
package/cli/shared-source.mjs
CHANGED
|
@@ -18,8 +18,9 @@ import { resolve, join, dirname, relative, extname } from 'node:path';
|
|
|
18
18
|
import { shouldIgnore } from './shared-ignore.mjs';
|
|
19
19
|
|
|
20
20
|
const IGNORE_DIRS = new Set([
|
|
21
|
-
'node_modules', '.git', '.next', 'dist', 'build',
|
|
21
|
+
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
|
|
22
22
|
'coverage', '.cache', '__pycache__', '.venv', 'vendor', '.turbo',
|
|
23
|
+
'cdk.out',
|
|
23
24
|
]);
|
|
24
25
|
|
|
25
26
|
const CODE_EXTENSIONS = new Set([
|
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
17
|
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
18
18
|
import { resolveSourceRoots } from '../shared-source.mjs';
|
|
19
|
+
import { shouldIgnore } from '../shared-ignore.mjs';
|
|
20
|
+
import { detectIaC, hasInfrastructureHeading, buildIaCWarning } from '../scanners/iac.mjs';
|
|
19
21
|
|
|
20
22
|
const IGNORE_DIRS = new Set([
|
|
21
|
-
'node_modules', '.git', '.next', 'dist', 'build', '
|
|
22
|
-
'.cache', '__pycache__', '.venv', 'vendor',
|
|
23
|
+
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
|
|
24
|
+
'coverage', '.cache', '__pycache__', '.venv', 'vendor',
|
|
25
|
+
'.turbo', '.vercel', '.svelte-kit', 'cdk.out', '.claude',
|
|
26
|
+
'target', '.gradle',
|
|
23
27
|
]);
|
|
24
28
|
|
|
25
29
|
// Dotfiles that are universally common and don't need documentation
|
|
@@ -50,8 +54,12 @@ export function validateDocsCoverage(projectDir, config) {
|
|
|
50
54
|
return { errors: [], warnings, passed: 0, total: 0 };
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
// IaC detection runs once and informs both Check 3 (suppression) and
|
|
58
|
+
// Check 6 (consolidated warning). One scan, two consumers.
|
|
59
|
+
const iac = detectIaC(projectDir);
|
|
60
|
+
|
|
53
61
|
// ── Check 1: Project-specific config/dotfiles referenced in docs ──
|
|
54
|
-
const configChecks = checkConfigFiles(projectDir, allDocContent);
|
|
62
|
+
const configChecks = checkConfigFiles(projectDir, allDocContent, config);
|
|
55
63
|
total += configChecks.total;
|
|
56
64
|
passed += configChecks.passed;
|
|
57
65
|
warnings.push(...configChecks.warnings);
|
|
@@ -63,7 +71,7 @@ export function validateDocsCoverage(projectDir, config) {
|
|
|
63
71
|
warnings.push(...binChecks.warnings);
|
|
64
72
|
|
|
65
73
|
// ── Check 3: Source directory structure matches ARCHITECTURE.md ──
|
|
66
|
-
const dirChecks = checkSourceDirs(projectDir, allDocContent, config);
|
|
74
|
+
const dirChecks = checkSourceDirs(projectDir, allDocContent, config, iac);
|
|
67
75
|
total += dirChecks.total;
|
|
68
76
|
passed += dirChecks.passed;
|
|
69
77
|
warnings.push(...dirChecks.warnings);
|
|
@@ -80,6 +88,12 @@ export function validateDocsCoverage(projectDir, config) {
|
|
|
80
88
|
passed += readmeChecks.passed;
|
|
81
89
|
warnings.push(...readmeChecks.warnings);
|
|
82
90
|
|
|
91
|
+
// ── Check 6: IaC-aware Infrastructure documentation ──
|
|
92
|
+
const iacChecks = checkIaCDocumentation(projectDir, iac);
|
|
93
|
+
total += iacChecks.total;
|
|
94
|
+
passed += iacChecks.passed;
|
|
95
|
+
warnings.push(...iacChecks.warnings);
|
|
96
|
+
|
|
83
97
|
return { errors: [], warnings, passed, total };
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -88,8 +102,10 @@ export function validateDocsCoverage(projectDir, config) {
|
|
|
88
102
|
/**
|
|
89
103
|
* Check 1: Project-specific config/dotfiles are mentioned in docs.
|
|
90
104
|
* Skips universally common files (.gitignore, .eslintrc, etc.).
|
|
105
|
+
* Honors config.ignore (FR-015 — applies user-configured ignore patterns
|
|
106
|
+
* consistently across all docs-coverage checks).
|
|
91
107
|
*/
|
|
92
|
-
function checkConfigFiles(projectDir, allDocContent) {
|
|
108
|
+
function checkConfigFiles(projectDir, allDocContent, config = {}) {
|
|
93
109
|
const warnings = [];
|
|
94
110
|
let passed = 0;
|
|
95
111
|
let total = 0;
|
|
@@ -111,6 +127,17 @@ function checkConfigFiles(projectDir, allDocContent) {
|
|
|
111
127
|
if (COMMON_DOTFILES.has(entry)) continue;
|
|
112
128
|
if (entry === 'tsconfig.json' || entry === 'package-lock.json') continue;
|
|
113
129
|
|
|
130
|
+
// Skip directories — this check is for configuration FILES, not dirs.
|
|
131
|
+
// Build-cache dotdirs (.nuxt, .next, .turbo, etc.) are handled by IGNORE_DIRS.
|
|
132
|
+
try {
|
|
133
|
+
if (statSync(join(projectDir, entry)).isDirectory()) continue;
|
|
134
|
+
} catch { continue; }
|
|
135
|
+
|
|
136
|
+
// Honor user-configured ignore patterns (FR-015 / IR-5).
|
|
137
|
+
// Same dual-form check as checkSourceDirs: relative path and trailing-slash
|
|
138
|
+
// form so dotfile-style patterns and dir-style patterns both apply.
|
|
139
|
+
if (shouldIgnore(entry, config) || shouldIgnore(entry + '/', config)) continue;
|
|
140
|
+
|
|
114
141
|
total++;
|
|
115
142
|
if (lowerDocContent.includes(entry.toLowerCase())) {
|
|
116
143
|
passed++;
|
|
@@ -160,8 +187,13 @@ function checkPackageBins(projectDir, allDocContent) {
|
|
|
160
187
|
|
|
161
188
|
/**
|
|
162
189
|
* Check 3: Source directories are referenced in ARCHITECTURE.md.
|
|
190
|
+
*
|
|
191
|
+
* Honors config.ignore (FR-006). When IaC is detected and the Infrastructure
|
|
192
|
+
* heading is missing, per-directory warnings inside the IaC package roots
|
|
193
|
+
* are suppressed — Check 6 emits one consolidated warning per IaC tool
|
|
194
|
+
* instead (FR-011).
|
|
163
195
|
*/
|
|
164
|
-
function checkSourceDirs(projectDir, allDocContent, config = {}) {
|
|
196
|
+
function checkSourceDirs(projectDir, allDocContent, config = {}, iac = { isIaC: false, tools: [] }) {
|
|
165
197
|
const warnings = [];
|
|
166
198
|
let passed = 0;
|
|
167
199
|
let total = 0;
|
|
@@ -173,6 +205,15 @@ function checkSourceDirs(projectDir, allDocContent, config = {}) {
|
|
|
173
205
|
try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { warnings, passed, total }; }
|
|
174
206
|
|
|
175
207
|
const lowerArchContent = archContent.toLowerCase();
|
|
208
|
+
const infraDocumented = hasInfrastructureHeading(archContent);
|
|
209
|
+
|
|
210
|
+
// Only suppress per-dir warnings when IaC exists AND no Infrastructure
|
|
211
|
+
// heading is present — Check 6 will fire the consolidated message instead.
|
|
212
|
+
const suppressIaCDirs = iac.isIaC && !infraDocumented;
|
|
213
|
+
|
|
214
|
+
// Flatten every IaC tool's package dirs into a single Set for fast lookup.
|
|
215
|
+
const iacPackageDirs = [];
|
|
216
|
+
for (const tool of iac.tools) iacPackageDirs.push(...tool.packageDirs);
|
|
176
217
|
|
|
177
218
|
// Monorepo-aware: honor config.sourceRoot + workspaces instead of a hardcoded list.
|
|
178
219
|
for (const rootDir of resolveSourceRoots(projectDir, config)) {
|
|
@@ -189,6 +230,22 @@ function checkSourceDirs(projectDir, allDocContent, config = {}) {
|
|
|
189
230
|
|
|
190
231
|
if (IGNORE_DIRS.has(entry) || entry.startsWith('.') || entry === '__tests__' || entry === '__test__') continue;
|
|
191
232
|
|
|
233
|
+
const relPath = relative(projectDir, fullPath);
|
|
234
|
+
|
|
235
|
+
// Honor user-configured ignore patterns (FR-006 / IR-5).
|
|
236
|
+
// Patterns like `**/cdk.out/**` are written to match files INSIDE the
|
|
237
|
+
// directory; appending '/' lets us match the directory itself too.
|
|
238
|
+
if (shouldIgnore(relPath, config) || shouldIgnore(relPath + '/', config)) continue;
|
|
239
|
+
|
|
240
|
+
// Suppress per-dir warnings for IaC-relevant subdirs inside an IaC
|
|
241
|
+
// package — the consolidated Check 6 warning covers them. Includes CDK
|
|
242
|
+
// (bin/, lib/, stacks/, constructs/), Terraform (modules/, environments/),
|
|
243
|
+
// Pulumi (stacks/), SAM (events/, src/), Serverless (handlers/, src/).
|
|
244
|
+
if (suppressIaCDirs && isInsideIaCPackage(relPath, iacPackageDirs)
|
|
245
|
+
&& IAC_SUBDIR_NAMES.has(entry)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
192
249
|
total++;
|
|
193
250
|
const searchName = entry.toLowerCase();
|
|
194
251
|
if (lowerArchContent.includes(searchName) || lowerArchContent.includes(root + '/' + entry)) {
|
|
@@ -204,6 +261,68 @@ function checkSourceDirs(projectDir, allDocContent, config = {}) {
|
|
|
204
261
|
return { warnings, passed, total };
|
|
205
262
|
}
|
|
206
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Subdirectory names recognized as IaC-relevant across all supported tools.
|
|
266
|
+
* When IaC is detected and the Infrastructure heading is missing, these dirs
|
|
267
|
+
* inside the IaC package are suppressed from Check 3 to avoid double-warning.
|
|
268
|
+
*/
|
|
269
|
+
const IAC_SUBDIR_NAMES = new Set([
|
|
270
|
+
// CDK
|
|
271
|
+
'bin', 'lib', 'stacks', 'constructs',
|
|
272
|
+
// Terraform
|
|
273
|
+
'modules', 'environments',
|
|
274
|
+
// SAM / Serverless / Pulumi
|
|
275
|
+
'handlers', 'events', 'src',
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* True if `relPath` is inside any of the IaC package directories.
|
|
280
|
+
* Both inputs are project-relative POSIX paths.
|
|
281
|
+
*/
|
|
282
|
+
function isInsideIaCPackage(relPath, packageDirs) {
|
|
283
|
+
if (!packageDirs || packageDirs.length === 0) return false;
|
|
284
|
+
const normalized = relPath.split('\\').join('/');
|
|
285
|
+
return packageDirs.some(pkgDir => {
|
|
286
|
+
const p = pkgDir === '.' ? '' : pkgDir.split('\\').join('/');
|
|
287
|
+
if (p === '') return true;
|
|
288
|
+
return normalized === p || normalized.startsWith(p + '/');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check 6: IaC projects should document their Infrastructure layer.
|
|
294
|
+
*
|
|
295
|
+
* Emits ONE consolidated warning per detected IaC tool when ARCHITECTURE.md
|
|
296
|
+
* has no Infrastructure heading. Suppresses the generic per-directory
|
|
297
|
+
* warnings that would otherwise fire for bin/, lib/, modules/, handlers/, etc.
|
|
298
|
+
*/
|
|
299
|
+
function checkIaCDocumentation(projectDir, iac) {
|
|
300
|
+
const warnings = [];
|
|
301
|
+
if (!iac || !iac.isIaC) return { warnings, passed: 0, total: 0 };
|
|
302
|
+
|
|
303
|
+
const archPath = resolve(projectDir, 'docs-canonical/ARCHITECTURE.md');
|
|
304
|
+
if (!existsSync(archPath)) {
|
|
305
|
+
// No ARCHITECTURE.md at all — structure validator will catch that.
|
|
306
|
+
// Don't double-warn here.
|
|
307
|
+
return { warnings, passed: 0, total: 0 };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let archContent;
|
|
311
|
+
try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { warnings, passed: 0, total: 0 }; }
|
|
312
|
+
|
|
313
|
+
if (hasInfrastructureHeading(archContent)) {
|
|
314
|
+
// One pass per tool — counted as total per IaC tool present.
|
|
315
|
+
return { warnings, passed: iac.tools.length, total: iac.tools.length };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// One actionable warning per detected IaC tool. Most projects use one tool,
|
|
319
|
+
// but a multi-tool monorepo gets one targeted message each.
|
|
320
|
+
for (const tool of iac.tools) {
|
|
321
|
+
warnings.push(buildIaCWarning(tool));
|
|
322
|
+
}
|
|
323
|
+
return { warnings, passed: 0, total: iac.tools.length };
|
|
324
|
+
}
|
|
325
|
+
|
|
207
326
|
/**
|
|
208
327
|
* Check 4: Config files that code actually READS are documented.
|
|
209
328
|
*
|
|
@@ -7,10 +7,38 @@ import { resolve, join, extname, basename } from 'node:path';
|
|
|
7
7
|
import { resolveSourceRoots } from '../shared-source.mjs';
|
|
8
8
|
|
|
9
9
|
const IGNORE_DIRS = new Set([
|
|
10
|
-
'node_modules', '.git', '.next', 'dist', 'build',
|
|
10
|
+
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
|
|
11
11
|
'coverage', '.cache', '__pycache__', '.venv', 'vendor',
|
|
12
|
+
// Co-located test dirs — these are not the source under documentation.
|
|
13
|
+
'__tests__', '__test__',
|
|
12
14
|
]);
|
|
13
15
|
|
|
16
|
+
// Files that are tests, not source. Matched against the relative path AND
|
|
17
|
+
// the basename. Covers Jest/Vitest/Mocha/Jasmine/pytest/Go/Java conventions.
|
|
18
|
+
const TEST_PATH_RE = /(^|\/)__tests?__\//;
|
|
19
|
+
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|java|go)$/;
|
|
20
|
+
|
|
21
|
+
// Next.js App Router uses a strict filename convention for route handlers.
|
|
22
|
+
// Other files in the app/api/ tree (helpers, types) are NOT routes.
|
|
23
|
+
const NEXTJS_ROUTE_FILE_RE = /(^|\/)route\.(ts|tsx|js|jsx|mjs)$/;
|
|
24
|
+
const NEXTJS_API_DIR_RE = /(^|\/)app\/api(\/|$)/;
|
|
25
|
+
|
|
26
|
+
function isTestFile(relPath) {
|
|
27
|
+
return TEST_PATH_RE.test(relPath) || TEST_FILE_RE.test(relPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* For Next.js App Router directories (app/api/...), only `route.{ts,js}` files
|
|
32
|
+
* are actual route handlers. Helpers and types in the same tree should not be
|
|
33
|
+
* treated as routes.
|
|
34
|
+
*/
|
|
35
|
+
function isValidRouteFile(relPath) {
|
|
36
|
+
if (NEXTJS_API_DIR_RE.test(relPath)) {
|
|
37
|
+
return NEXTJS_ROUTE_FILE_RE.test(relPath);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
/**
|
|
15
43
|
* Expand sub-path patterns (e.g. 'routes', 'src/routes') against the project
|
|
16
44
|
* root AND every configured source root, returning de-duplicated existing dirs.
|
|
@@ -53,15 +81,22 @@ export function validateDocsSync(projectDir, config) {
|
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
// Find route/API files (monorepo-aware) and check they're mentioned in docs.
|
|
56
|
-
|
|
84
|
+
// Note: bare 'api' is intentionally excluded — it collides with frontend
|
|
85
|
+
// API client conventions (src/api/client.ts). Backend routes use
|
|
86
|
+
// src/routes/ or routes/ (Express). Next.js App Router uses src/app/api/
|
|
87
|
+
// or app/api/ with strict route.{ts,js} filename matching applied below.
|
|
88
|
+
const routeDirs = expandDirs(projectDir, config, ['src/routes', 'src/app/api', 'routes', 'app/api']);
|
|
57
89
|
for (const routeDir of routeDirs) {
|
|
58
90
|
const files = getFilesRecursive(routeDir);
|
|
59
91
|
for (const file of files) {
|
|
60
92
|
const ext = extname(file);
|
|
61
|
-
if (!['.ts', '.js', '.mjs', '.py', '.java', '.go'].includes(ext)) continue;
|
|
93
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py', '.java', '.go'].includes(ext)) continue;
|
|
62
94
|
|
|
63
|
-
results.total++;
|
|
64
95
|
const relPath = file.replace(projectDir + '/', '');
|
|
96
|
+
if (isTestFile(relPath)) continue;
|
|
97
|
+
if (!isValidRouteFile(relPath)) continue;
|
|
98
|
+
|
|
99
|
+
results.total++;
|
|
65
100
|
const name = basename(file, ext);
|
|
66
101
|
|
|
67
102
|
// Check if the file path or name is mentioned in any canonical doc
|
|
@@ -79,10 +114,12 @@ export function validateDocsSync(projectDir, config) {
|
|
|
79
114
|
const files = getFilesRecursive(serviceDir);
|
|
80
115
|
for (const file of files) {
|
|
81
116
|
const ext = extname(file);
|
|
82
|
-
if (!['.ts', '.js', '.mjs', '.py', '.java', '.go'].includes(ext)) continue;
|
|
117
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py', '.java', '.go'].includes(ext)) continue;
|
|
83
118
|
|
|
84
|
-
results.total++;
|
|
85
119
|
const relPath = file.replace(projectDir + '/', '');
|
|
120
|
+
if (isTestFile(relPath)) continue;
|
|
121
|
+
|
|
122
|
+
results.total++;
|
|
86
123
|
const name = basename(file, ext);
|
|
87
124
|
|
|
88
125
|
if (canonicalContent.includes(relPath) || canonicalContent.includes(name)) {
|
|
@@ -117,11 +154,15 @@ export function validateDocsSync(projectDir, config) {
|
|
|
117
154
|
|
|
118
155
|
if (openapiContent && openapiFile) {
|
|
119
156
|
// Check that route files have corresponding paths in OpenAPI spec (monorepo-aware)
|
|
120
|
-
for (const routeDir of expandDirs(projectDir, config, ['src/routes', 'src/app/api', 'routes', 'api'])) {
|
|
157
|
+
for (const routeDir of expandDirs(projectDir, config, ['src/routes', 'src/app/api', 'routes', 'app/api'])) {
|
|
121
158
|
const files = getFilesRecursive(routeDir);
|
|
122
159
|
for (const file of files) {
|
|
123
160
|
const ext = extname(file);
|
|
124
|
-
if (!['.ts', '.js', '.mjs'].includes(ext)) continue;
|
|
161
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs'].includes(ext)) continue;
|
|
162
|
+
|
|
163
|
+
const relPathForFilter = file.replace(projectDir + '/', '');
|
|
164
|
+
if (isTestFile(relPathForFilter)) continue;
|
|
165
|
+
if (!isValidRouteFile(relPathForFilter)) continue;
|
|
125
166
|
|
|
126
167
|
// Skip index/middleware files
|
|
127
168
|
const rawName = basename(file, ext).toLowerCase();
|
|
@@ -126,17 +126,22 @@ export function validateTestSpec(projectDir, config) {
|
|
|
126
126
|
continue;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// For a ✅ journey, verify the referenced test file actually
|
|
130
|
-
// rather than trusting the glyph.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
// For a ✅ journey, verify the referenced test file(s) actually exist
|
|
130
|
+
// rather than trusting the glyph. Cells may list multiple paths in
|
|
131
|
+
// backticks separated by commas (e.g. `a.test.ts`, `b.test.ts`) and
|
|
132
|
+
// may include "(N suites)" annotations or globs.
|
|
133
|
+
if (testFile && testFile.trim() !== '—' && !testFile.includes('N/A')) {
|
|
134
|
+
const paths = parseTestPathCell(testFile);
|
|
135
|
+
if (paths.length > 0) {
|
|
136
|
+
results.total++;
|
|
137
|
+
const anyExists = paths.some(p => testEvidenceExists(projectDir, p));
|
|
138
|
+
if (anyExists) {
|
|
139
|
+
results.passed++;
|
|
140
|
+
} else {
|
|
141
|
+
results.warnings.push(
|
|
142
|
+
`E2E Journey #${num} (${journey}) marked ✅ but test file not found: ${paths.join(', ')}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
}
|
|
@@ -182,6 +187,119 @@ export function validateTestSpec(projectDir, config) {
|
|
|
182
187
|
return results;
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Parse a TEST-SPEC.md table cell into a list of test path strings.
|
|
192
|
+
*
|
|
193
|
+
* Real-world Journey rows commonly list multiple test files in one cell:
|
|
194
|
+
* `path/a.test.ts`, `path/b.test.ts`
|
|
195
|
+
* `idor_*.test.ts (3 suites)`
|
|
196
|
+
*
|
|
197
|
+
* Strategy:
|
|
198
|
+
* 1. Split on commas that are OUTSIDE backticks.
|
|
199
|
+
* 2. For each segment: strip backticks, strip trailing "(N suites)" or
|
|
200
|
+
* "(N tests)" annotations, trim whitespace.
|
|
201
|
+
* 3. Drop empties.
|
|
202
|
+
*
|
|
203
|
+
* The "(N suites)" annotation is preserved as evidence — if a glob like
|
|
204
|
+
* `idor_*.test.ts` doesn't expand to a literal file, testEvidenceExists()
|
|
205
|
+
* accepts the annotation as the author's claim of coverage.
|
|
206
|
+
*/
|
|
207
|
+
export function parseTestPathCell(cell) {
|
|
208
|
+
if (!cell) return [];
|
|
209
|
+
// Split on commas that are NOT inside backticks. Track backtick parity.
|
|
210
|
+
const segments = [];
|
|
211
|
+
let buf = '';
|
|
212
|
+
let inBackticks = false;
|
|
213
|
+
for (const ch of cell) {
|
|
214
|
+
if (ch === '`') { inBackticks = !inBackticks; buf += ch; continue; }
|
|
215
|
+
if (ch === ',' && !inBackticks) {
|
|
216
|
+
segments.push(buf);
|
|
217
|
+
buf = '';
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
buf += ch;
|
|
221
|
+
}
|
|
222
|
+
if (buf) segments.push(buf);
|
|
223
|
+
|
|
224
|
+
const result = [];
|
|
225
|
+
for (let seg of segments) {
|
|
226
|
+
seg = seg.replace(/`/g, '').trim();
|
|
227
|
+
if (!seg || seg === '—') continue;
|
|
228
|
+
result.push(seg);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* True if a TEST-SPEC.md path segment has supporting evidence on disk.
|
|
235
|
+
*
|
|
236
|
+
* Accepts: exact file match, glob expansion (e.g. `foo_*.test.ts`), or an
|
|
237
|
+
* "(N suites)" / "(N tests)" annotation when the literal path doesn't exist.
|
|
238
|
+
* The annotation is the author's explicit claim of coverage — believe it
|
|
239
|
+
* rather than reject the row outright; the audit trail is in the markdown.
|
|
240
|
+
*/
|
|
241
|
+
export function testEvidenceExists(projectDir, pathSegment) {
|
|
242
|
+
if (!pathSegment) return false;
|
|
243
|
+
|
|
244
|
+
// Strip a trailing "(N suites)" / "(N tests)" annotation for the file check.
|
|
245
|
+
const annotationMatch = pathSegment.match(/\s*\((\d+)\s+(?:suites?|tests?)\)\s*$/i);
|
|
246
|
+
const pathOnly = annotationMatch ? pathSegment.slice(0, annotationMatch.index).trim() : pathSegment;
|
|
247
|
+
const hasAnnotation = !!annotationMatch;
|
|
248
|
+
|
|
249
|
+
if (!pathOnly) return hasAnnotation;
|
|
250
|
+
|
|
251
|
+
// Glob support — if the segment contains *, ?, or [, walk the parent dir.
|
|
252
|
+
if (/[*?[]/.test(pathOnly)) {
|
|
253
|
+
const matches = expandGlob(projectDir, pathOnly);
|
|
254
|
+
if (matches.length > 0) return true;
|
|
255
|
+
// Glob with annotation but no expansion → trust the annotation.
|
|
256
|
+
return hasAnnotation;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Plain path — must exist on disk.
|
|
260
|
+
if (existsSync(resolve(projectDir, pathOnly))) return true;
|
|
261
|
+
// Plain path with explicit annotation → still trust the author's claim.
|
|
262
|
+
return hasAnnotation;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Minimal glob expansion: only handles the `*` and `?` wildcards in a single
|
|
267
|
+
* path segment. e.g. `backend/src/test-helpers/security/idor_*.test.ts`.
|
|
268
|
+
* Pure Node.js built-ins; zero dependencies.
|
|
269
|
+
*/
|
|
270
|
+
function expandGlob(projectDir, pattern) {
|
|
271
|
+
const parts = pattern.split('/');
|
|
272
|
+
const start = resolve(projectDir);
|
|
273
|
+
let candidates = [start];
|
|
274
|
+
for (const part of parts) {
|
|
275
|
+
if (!/[*?[]/.test(part)) {
|
|
276
|
+
candidates = candidates.map(c => resolve(c, part)).filter(c => existsSync(c));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const re = globPartToRegex(part);
|
|
280
|
+
const next = [];
|
|
281
|
+
for (const dir of candidates) {
|
|
282
|
+
let entries;
|
|
283
|
+
try { entries = readdirSync(dir); } catch { continue; }
|
|
284
|
+
for (const e of entries) {
|
|
285
|
+
if (re.test(e)) next.push(resolve(dir, e));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
candidates = next;
|
|
289
|
+
if (candidates.length === 0) return [];
|
|
290
|
+
}
|
|
291
|
+
return candidates;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function globPartToRegex(part) {
|
|
295
|
+
const escaped = part
|
|
296
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
297
|
+
.replace(/\\\[/g, '[').replace(/\\\]/g, ']') // restore character classes
|
|
298
|
+
.replace(/\*/g, '.*')
|
|
299
|
+
.replace(/\?/g, '.');
|
|
300
|
+
return new RegExp(`^${escaped}$`);
|
|
301
|
+
}
|
|
302
|
+
|
|
185
303
|
/** Recursively check if a directory contains test files */
|
|
186
304
|
function hasTestFilesRecursive(dir) {
|
|
187
305
|
const ignore = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
|
|
@@ -38,6 +38,25 @@ const TEST_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx']);
|
|
|
38
38
|
const TODO_PATTERN = /\b(TODO|FIXME|HACK|XXX|TEMP(?!late|orar)|WORKAROUND)\s*[(:]/;
|
|
39
39
|
const TODO_EXTRACT = /\b(TODO|FIXME|HACK|XXX|TEMP(?!late|orar)|WORKAROUND)\s*[:(]?\s*(.+)/;
|
|
40
40
|
|
|
41
|
+
// Matches a comment-opening marker. Real TODOs live in comments — restricting
|
|
42
|
+
// matches to text AFTER a comment marker prevents false positives from regex
|
|
43
|
+
// literals or strings that happen to contain a TODO keyword.
|
|
44
|
+
// // — JS/TS/C/C++/Rust/Go/Java line comment
|
|
45
|
+
// # — Python/Ruby/shell/YAML
|
|
46
|
+
// /* — JS/C/C++ block comment open
|
|
47
|
+
// * — block comment continuation (when at start of line)
|
|
48
|
+
// <!-- — HTML/Markdown
|
|
49
|
+
const COMMENT_MARKER = /(?:\/\/|#|\/\*|<!--|^\s*\*\s)/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return the portion of a line after the first comment marker, or null if
|
|
53
|
+
* the line has no comment. Used to constrain TODO matching to comments.
|
|
54
|
+
*/
|
|
55
|
+
function commentPortion(line) {
|
|
56
|
+
const m = line.match(COMMENT_MARKER);
|
|
57
|
+
return m ? line.slice(m.index + m[0].length) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
// Test skip patterns for common test frameworks
|
|
42
61
|
const SKIP_PATTERNS = [
|
|
43
62
|
/\btest\.skip\s*\(/,
|
|
@@ -290,10 +309,31 @@ function findTestFiles(rootDir, dir, files, config) {
|
|
|
290
309
|
}
|
|
291
310
|
}
|
|
292
311
|
|
|
312
|
+
// Test-file path patterns — TODO scanning skips these by default to avoid
|
|
313
|
+
// false positives from test fixture strings (writeFileSync(..., '// xxxxx:')
|
|
314
|
+
// inside template literals is a comment marker for the regex but not a real
|
|
315
|
+
// annotation to track). Set config.todoTracking.includeTestFiles = true to override.
|
|
316
|
+
const TEST_FILE_RE = /(^|\/)__tests?__\//;
|
|
317
|
+
const TEST_NAME_RE = /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|java|go)$/;
|
|
318
|
+
|
|
319
|
+
// The validator's own source file describes the keyword list in its docstring
|
|
320
|
+
// and code. Skipping itself avoids self-referential false positives.
|
|
321
|
+
const SELF_PATH = new URL(import.meta.url).pathname;
|
|
322
|
+
|
|
323
|
+
function isTestFilePath(relPath) {
|
|
324
|
+
return TEST_FILE_RE.test(relPath) || TEST_NAME_RE.test(relPath);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isSelfPath(fullPath) {
|
|
328
|
+
return fullPath === SELF_PATH;
|
|
329
|
+
}
|
|
330
|
+
|
|
293
331
|
function findTodos(rootDir, dir, todos, config) {
|
|
294
332
|
let entries;
|
|
295
333
|
try { entries = readdirSync(dir); } catch { return; }
|
|
296
334
|
|
|
335
|
+
const includeTests = config?.todoTracking?.includeTestFiles === true;
|
|
336
|
+
|
|
297
337
|
for (const entry of entries) {
|
|
298
338
|
if (IGNORE_DIRS.has(entry)) continue;
|
|
299
339
|
if (entry.startsWith('.')) continue;
|
|
@@ -310,6 +350,15 @@ function findTodos(rootDir, dir, todos, config) {
|
|
|
310
350
|
|
|
311
351
|
const relPath = relative(rootDir, full);
|
|
312
352
|
|
|
353
|
+
// Skip test files unless explicitly opted in — test fixture strings
|
|
354
|
+
// commonly contain comment markers inside template literals that the
|
|
355
|
+
// single-line heuristic can't distinguish from real comments.
|
|
356
|
+
if (!includeTests && isTestFilePath(relPath)) continue;
|
|
357
|
+
|
|
358
|
+
// Skip the validator's own source file — its docstring legitimately
|
|
359
|
+
// names the annotation keywords it scans for.
|
|
360
|
+
if (isSelfPath(full)) continue;
|
|
361
|
+
|
|
313
362
|
// Apply config ignore patterns (todoIgnore + global ignore)
|
|
314
363
|
if (config && shouldIgnore(relPath, config, 'todoIgnore')) continue;
|
|
315
364
|
|
|
@@ -322,8 +371,12 @@ function findTodos(rootDir, dir, todos, config) {
|
|
|
322
371
|
const lines = content.split('\n');
|
|
323
372
|
|
|
324
373
|
for (let i = 0; i < lines.length; i++) {
|
|
325
|
-
|
|
326
|
-
|
|
374
|
+
// Restrict scanning to text inside a comment — keeps the regex from
|
|
375
|
+
// matching its own keyword list when DocGuard reads its own source.
|
|
376
|
+
const commentText = commentPortion(lines[i]);
|
|
377
|
+
if (commentText === null) continue;
|
|
378
|
+
if (TODO_PATTERN.test(commentText)) {
|
|
379
|
+
const match = commentText.match(TODO_EXTRACT);
|
|
327
380
|
if (match) {
|
|
328
381
|
todos.push({
|
|
329
382
|
keyword: match[1].toUpperCase(),
|
|
@@ -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.11.1"
|
|
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.11.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.11.1 -->
|
|
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.11.1
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.11.1 -->
|
|
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.11.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.11.1 -->
|
|
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.11.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.11.1 -->
|
|
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.11.1
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -58,6 +58,58 @@
|
|
|
58
58
|
|---------|---------|-----|----------|
|
|
59
59
|
| <!-- e.g. Stripe --> | <!-- e.g. Payments --> | <!-- e.g. 99.99% --> | <!-- e.g. Queue + retry --> |
|
|
60
60
|
|
|
61
|
+
## Infrastructure (IaC)
|
|
62
|
+
|
|
63
|
+
<!--
|
|
64
|
+
Skip this section if the project does not use Infrastructure-as-Code.
|
|
65
|
+
DocGuard auto-detects AWS CDK (cdk.json), Terraform (*.tf), Pulumi
|
|
66
|
+
(Pulumi.yaml), AWS SAM (template.yaml with AWS::Serverless::), and
|
|
67
|
+
Serverless Framework (serverless.yml). When any are detected and this
|
|
68
|
+
section is missing, DocGuard emits ONE consolidated reminder per tool.
|
|
69
|
+
|
|
70
|
+
Document the layout for YOUR IaC tool below. Remove the blocks that
|
|
71
|
+
don't apply.
|
|
72
|
+
-->
|
|
73
|
+
|
|
74
|
+
### AWS CDK
|
|
75
|
+
|
|
76
|
+
<!-- Remove this block if you don't use CDK -->
|
|
77
|
+
|
|
78
|
+
| Artifact | Location | Purpose |
|
|
79
|
+
|----------|----------|---------|
|
|
80
|
+
| App entrypoint | <!-- e.g. packages/cdk/bin/app.ts --> | Instantiates stacks per environment |
|
|
81
|
+
| Stacks | <!-- e.g. packages/cdk/lib/stacks/ --> | One file per CloudFormation stack |
|
|
82
|
+
| Constructs | <!-- e.g. packages/cdk/lib/constructs/ --> | Reusable infrastructure components |
|
|
83
|
+
| Synth config | <!-- e.g. packages/cdk/cdk.json --> | CDK CLI configuration |
|
|
84
|
+
| Context cache | <!-- e.g. packages/cdk/cdk.context.json --> | Environment lookup results (committed) |
|
|
85
|
+
| Synth output | <!-- e.g. packages/cdk/cdk.out/ --> | Generated CloudFormation (gitignored) |
|
|
86
|
+
|
|
87
|
+
### Terraform
|
|
88
|
+
|
|
89
|
+
<!-- Remove this block if you don't use Terraform -->
|
|
90
|
+
|
|
91
|
+
| Artifact | Location | Purpose |
|
|
92
|
+
|----------|----------|---------|
|
|
93
|
+
| Root module | <!-- e.g. infra/*.tf --> | Top-level resources |
|
|
94
|
+
| Reusable modules | <!-- e.g. infra/modules/ --> | Composable infrastructure blocks |
|
|
95
|
+
| Environment configs | <!-- e.g. infra/environments/ --> | Per-env tfvars (dev, staging, prod) |
|
|
96
|
+
| State backend | <!-- e.g. S3 bucket + DynamoDB lock table --> | Remote state storage |
|
|
97
|
+
|
|
98
|
+
### Pulumi / SAM / Serverless Framework
|
|
99
|
+
|
|
100
|
+
<!--
|
|
101
|
+
Document app program (index.ts), stack config, or manifest location.
|
|
102
|
+
Remove this block if you don't use these tools.
|
|
103
|
+
-->
|
|
104
|
+
|
|
105
|
+
### Deployment Pipeline
|
|
106
|
+
|
|
107
|
+
<!-- How does IaC code reach the cloud? -->
|
|
108
|
+
|
|
109
|
+
- <!-- e.g. PR → CI workflow → terraform plan + manual approval → terraform apply -->
|
|
110
|
+
- <!-- e.g. PR → CodePipeline → cdk deploy → dev/staging/prod -->
|
|
111
|
+
|
|
112
|
+
|
|
61
113
|
## Diagrams
|
|
62
114
|
|
|
63
115
|
<!-- Architecture diagrams using Mermaid, ASCII art, or linked images -->
|