docguard-cli 0.11.0 → 0.11.2
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/commands/diff.mjs +15 -17
- package/cli/commands/guard.mjs +12 -2
- package/cli/docguard.mjs +2 -0
- package/cli/ensure-skills.mjs +8 -1
- 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 +19 -7
- package/cli/validators/docs-coverage.mjs +129 -7
- package/cli/validators/docs-sync.mjs +49 -8
- package/cli/validators/environment.mjs +9 -3
- 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
package/cli/commands/diff.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { collectPackageJsons, detectDocker, grepEnvUsage, resolveSourceRoots } f
|
|
|
10
10
|
import { parseApiReferenceDoc, compareEndpoints } from '../scanners/api-doc.mjs';
|
|
11
11
|
import { resolveApiSurface } from '../validators/api-surface.mjs';
|
|
12
12
|
import { collectCodeTests } from '../validators/docs-diff.mjs';
|
|
13
|
+
import { scanSchemasDeep } from '../scanners/schemas.mjs';
|
|
14
|
+
import { detectDocTools } from '../scanners/doc-tools.mjs';
|
|
13
15
|
|
|
14
16
|
const IGNORE_DIRS = new Set([
|
|
15
17
|
'node_modules', '.git', '.next', 'dist', 'build',
|
|
@@ -163,22 +165,17 @@ function diffEntities(dir, config = {}) {
|
|
|
163
165
|
docEntities.add(name.toLowerCase());
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
//
|
|
168
|
+
// Use the REAL exported entity names from scanSchemasDeep, not file basenames
|
|
169
|
+
// (a file `dynamoModels.ts` exports `User`/`Order`/etc. — its basename is not
|
|
170
|
+
// an entity). scanSchemasDeep covers JS ORMs, SQLAlchemy/Pydantic, Diesel,
|
|
171
|
+
// Go structs, JPA, Rails, and OpenAPI schemas.
|
|
172
|
+
const docTools = detectDocTools(dir);
|
|
173
|
+
const schemas = scanSchemasDeep(dir, {}, docTools);
|
|
167
174
|
const codeEntities = new Set();
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const modelDir = join(root, sub);
|
|
173
|
-
if (!existsSync(modelDir)) continue;
|
|
174
|
-
const files = getFilesRecursive(modelDir);
|
|
175
|
-
for (const f of files) {
|
|
176
|
-
const name = basename(f, extname(f)).toLowerCase();
|
|
177
|
-
// Skip non-entity infrastructure/aggregation filenames.
|
|
178
|
-
if (CODE_ENTITY_NOISE.has(name)) continue;
|
|
179
|
-
codeEntities.add(name);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
175
|
+
for (const e of (schemas.entities || [])) {
|
|
176
|
+
const n = String(e.name || '').toLowerCase();
|
|
177
|
+
if (!n || CODE_ENTITY_NOISE.has(n)) continue;
|
|
178
|
+
codeEntities.add(n);
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
// No code-side entity source (e.g. DynamoDB single-table design with no model
|
|
@@ -207,7 +204,8 @@ function diffEnvVars(dir, config = {}) {
|
|
|
207
204
|
|
|
208
205
|
// Extract env var names from ENVIRONMENT.md
|
|
209
206
|
const docVars = new Set();
|
|
210
|
-
|
|
207
|
+
// Reject names ending in `_` (e.g. the literal prefix `VITE_` in prose).
|
|
208
|
+
const varRegex = /`([A-Z][A-Z0-9_]*[A-Z0-9])`/g;
|
|
211
209
|
let match;
|
|
212
210
|
while ((match = varRegex.exec(content)) !== null) {
|
|
213
211
|
docVars.add(match[1]);
|
|
@@ -220,7 +218,7 @@ function diffEnvVars(dir, config = {}) {
|
|
|
220
218
|
const envExamplePath = resolve(dir, envFile);
|
|
221
219
|
if (existsSync(envExamplePath)) {
|
|
222
220
|
const envContent = readFileSync(envExamplePath, 'utf-8');
|
|
223
|
-
const envRegex = /^([A-Z][A-Z0-9_]
|
|
221
|
+
const envRegex = /^([A-Z][A-Z0-9_]*[A-Z0-9])\s*=/gm;
|
|
224
222
|
while ((match = envRegex.exec(envContent)) !== null) {
|
|
225
223
|
codeVars.add(match[1]);
|
|
226
224
|
}
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -194,16 +194,26 @@ export function runGuard(projectDir, config, flags) {
|
|
|
194
194
|
console.log(` ${c.yellow}⚠️ ${v.name}${c.reset} ${qBadge}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
// --show-failing forces enumeration of every error/warning regardless of
|
|
198
|
+
// overall validator status — useful when a validator passes overall
|
|
199
|
+
// (passed < total) without surfacing the specific failing checks.
|
|
200
|
+
const show = flags.verbose || flags.showFailing;
|
|
201
|
+
if (show || v.status === 'fail') {
|
|
198
202
|
for (const err of v.errors) {
|
|
199
203
|
console.log(` ${c.red}✗ ${err}${c.reset}`);
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
|
-
if (
|
|
206
|
+
if (show || v.status === 'warn') {
|
|
203
207
|
for (const warn of v.warnings) {
|
|
204
208
|
console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
|
|
205
209
|
}
|
|
206
210
|
}
|
|
211
|
+
// If a validator reports passed < total but has no errors/warnings, surface
|
|
212
|
+
// the gap honestly so users aren't left wondering where the deficit went.
|
|
213
|
+
if (v.status === 'pass' && v.total > v.passed && v.errors.length === 0 && v.warnings.length === 0) {
|
|
214
|
+
const gap = v.total - v.passed;
|
|
215
|
+
console.log(` ${c.yellow}⚠ ${gap} check(s) did not pass but emitted no message — likely a validator bug. Please file an issue.${c.reset}`);
|
|
216
|
+
}
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
// Summary
|
package/cli/docguard.mjs
CHANGED
|
@@ -365,6 +365,8 @@ async function main() {
|
|
|
365
365
|
} else if (args[i] === '--since' && args[i + 1]) {
|
|
366
366
|
flags.since = args[i + 1];
|
|
367
367
|
i++;
|
|
368
|
+
} else if (args[i] === '--show-failing') {
|
|
369
|
+
flags.showFailing = true;
|
|
368
370
|
} else if (args[i] === '--doc' && args[i + 1]) {
|
|
369
371
|
flags.doc = args[i + 1];
|
|
370
372
|
i++;
|
package/cli/ensure-skills.mjs
CHANGED
|
@@ -54,8 +54,13 @@ export function detectAgentMode(projectDir) {
|
|
|
54
54
|
'.specify',
|
|
55
55
|
'.github/copilot-instructions.md',
|
|
56
56
|
'CLAUDE.md',
|
|
57
|
+
'GEMINI.md',
|
|
57
58
|
'.gemini',
|
|
58
59
|
'.agents',
|
|
60
|
+
'.antigravity',
|
|
61
|
+
'ANTIGRAVITY.md',
|
|
62
|
+
'.kiro',
|
|
63
|
+
'.windsurf',
|
|
59
64
|
];
|
|
60
65
|
|
|
61
66
|
for (const signal of llmSignals) {
|
|
@@ -105,7 +110,9 @@ export function detectAIAgent(projectDir) {
|
|
|
105
110
|
{ signal: '.claude', agent: 'claude' },
|
|
106
111
|
{ signal: 'CLAUDE.md', agent: 'claude' },
|
|
107
112
|
{ signal: '.gemini', agent: 'gemini' },
|
|
108
|
-
{ signal: '.agents', agent: 'agy' }, // Antigravity
|
|
113
|
+
{ signal: '.agents', agent: 'agy' }, // Antigravity (Spec Kit convention)
|
|
114
|
+
{ signal: '.antigravity', agent: 'agy' }, // Antigravity (alt convention)
|
|
115
|
+
{ signal: 'ANTIGRAVITY.md', agent: 'agy' }, // Antigravity rules file
|
|
109
116
|
{ signal: '.github/copilot-instructions.md', agent: 'copilot' },
|
|
110
117
|
{ signal: '.windsurf', agent: 'windsurf' },
|
|
111
118
|
{ signal: '.codex', agent: 'codex' },
|
|
@@ -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([
|
|
@@ -209,11 +210,16 @@ export function grepEnvUsage(projectDir, config = {}) {
|
|
|
209
210
|
const roots = resolveSourceRoots(projectDir, config);
|
|
210
211
|
const seen = new Set();
|
|
211
212
|
|
|
213
|
+
// Require names to start with a letter and END with a letter/digit (NOT an
|
|
214
|
+
// underscore) — fixes "VITE_" being captured as a literal env var name.
|
|
215
|
+
const NAME = '([A-Z][A-Z0-9_]*[A-Z0-9])';
|
|
212
216
|
const patterns = [
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
new RegExp(`process\\.env\\.${NAME}`, 'g'),
|
|
218
|
+
new RegExp(`process\\.env\\[\\s*['"]${NAME}['"]\\s*\\]`, 'g'),
|
|
219
|
+
new RegExp(`import\\.meta\\.env\\.${NAME}`, 'g'),
|
|
216
220
|
];
|
|
221
|
+
// Vite injects these at build time; they are not user-set env vars.
|
|
222
|
+
const VITE_INTRINSICS = new Set(['DEV', 'PROD', 'MODE', 'BASE_URL', 'SSR']);
|
|
217
223
|
|
|
218
224
|
const visit = (filePath) => {
|
|
219
225
|
if (seen.has(filePath)) return;
|
|
@@ -224,10 +230,16 @@ export function grepEnvUsage(projectDir, config = {}) {
|
|
|
224
230
|
let content;
|
|
225
231
|
try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
|
|
226
232
|
if (!content.includes('env')) return;
|
|
227
|
-
|
|
233
|
+
// patterns[2] is the import.meta.env one — its matches are Vite-injected
|
|
234
|
+
// when the name is an intrinsic, and must not be reported as user env vars.
|
|
235
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
228
236
|
let m;
|
|
229
|
-
const rx = new RegExp(
|
|
230
|
-
|
|
237
|
+
const rx = new RegExp(patterns[i].source, 'g');
|
|
238
|
+
const isViteSource = i === 2;
|
|
239
|
+
while ((m = rx.exec(content)) !== null) {
|
|
240
|
+
if (isViteSource && VITE_INTRINSICS.has(m[1])) continue;
|
|
241
|
+
names.add(m[1]);
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
};
|
|
233
245
|
|
|
@@ -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
|
*
|
|
@@ -303,9 +422,12 @@ function checkReadmeSections(projectDir) {
|
|
|
303
422
|
}
|
|
304
423
|
}
|
|
305
424
|
|
|
425
|
+
// Recommended sections are a BONUS — present = +1 to both passed and total,
|
|
426
|
+
// missing = no-op. Counting missing recommended toward `total` without a
|
|
427
|
+
// corresponding warning would be a silent fail (caught by B-4 nudge).
|
|
306
428
|
for (const section of recommendedSections) {
|
|
307
|
-
total++;
|
|
308
429
|
if (section.patterns.some(p => lowerContent.includes(p))) {
|
|
430
|
+
total++;
|
|
309
431
|
passed++;
|
|
310
432
|
}
|
|
311
433
|
}
|
|
@@ -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();
|
|
@@ -41,13 +41,19 @@ export function validateEnvironment(projectDir, config) {
|
|
|
41
41
|
// CLI/library projects that declare no env vars skip this.)
|
|
42
42
|
if (ptc.needsEnvVars !== false) {
|
|
43
43
|
const documented = new Set();
|
|
44
|
-
|
|
44
|
+
// Require the matched name to end with a letter/digit — prevents prose-only
|
|
45
|
+
// tokens like `VITE_` (the convention prefix) from being treated as a real
|
|
46
|
+
// variable name.
|
|
47
|
+
const varRe = /`([A-Z][A-Z0-9_]*[A-Z0-9])`/g;
|
|
45
48
|
let m;
|
|
46
|
-
while ((m = varRe.exec(content)) !== null)
|
|
49
|
+
while ((m = varRe.exec(content)) !== null) {
|
|
50
|
+
if (m[1].length < 3) continue; // 'OK' / 'ID' etc. are too short to be env var refs
|
|
51
|
+
documented.add(m[1]);
|
|
52
|
+
}
|
|
47
53
|
for (const envFile of ['.env.example', '.env.template']) {
|
|
48
54
|
const p = resolve(projectDir, envFile);
|
|
49
55
|
if (!existsSync(p)) continue;
|
|
50
|
-
const re = /^([A-Z][A-Z0-9_]
|
|
56
|
+
const re = /^([A-Z][A-Z0-9_]*[A-Z0-9])\s*=/gm;
|
|
51
57
|
const ex = readFileSync(p, 'utf-8');
|
|
52
58
|
let em;
|
|
53
59
|
while ((em = re.exec(ex)) !== null) documented.add(em[1]);
|
|
@@ -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.2"
|
|
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.2
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.11.2 -->
|
|
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.2
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.11.2 -->
|
|
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.2
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.11.2 -->
|
|
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.2
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.11.2 -->
|
|
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.2
|
|
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 -->
|