@whitehatd/crag 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/analyze/ci-extractors.js +317 -0
- package/src/analyze/doc-mining.js +142 -0
- package/src/analyze/gates.js +417 -0
- package/src/analyze/normalize.js +146 -0
- package/src/analyze/stacks.js +453 -0
- package/src/analyze/task-runners.js +146 -0
- package/src/commands/analyze.js +158 -205
- package/src/governance/yaml-run.js +58 -2
package/src/commands/analyze.js
CHANGED
|
@@ -5,16 +5,33 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { detectWorkspace } = require('../workspace/detect');
|
|
7
7
|
const { enumerateMembers } = require('../workspace/enumerate');
|
|
8
|
-
const {
|
|
8
|
+
const { isGateCommand } = require('../governance/yaml-run');
|
|
9
9
|
const { cliWarn, cliError, EXIT_INTERNAL } = require('../cli-errors');
|
|
10
|
+
const { detectStack } = require('../analyze/stacks');
|
|
11
|
+
const { inferGates } = require('../analyze/gates');
|
|
12
|
+
const { normalizeCiGates } = require('../analyze/normalize');
|
|
13
|
+
const { extractCiCommands } = require('../analyze/ci-extractors');
|
|
14
|
+
const { mineTaskTargets } = require('../analyze/task-runners');
|
|
15
|
+
const { mineDocGates } = require('../analyze/doc-mining');
|
|
16
|
+
|
|
17
|
+
// Directories commonly used for test fixtures and examples in workspaces.
|
|
18
|
+
// When analyze auto-wires to workspace detection, these are excluded from
|
|
19
|
+
// member enumeration so we don't emit 79 per-fixture sections for vite.
|
|
20
|
+
const FIXTURE_DIR_PATTERNS = [
|
|
21
|
+
/^playground$/,
|
|
22
|
+
/^fixtures$/,
|
|
23
|
+
/^examples?$/,
|
|
24
|
+
/^demos?$/,
|
|
25
|
+
/^test-fixtures$/,
|
|
26
|
+
/^__fixtures__$/,
|
|
27
|
+
];
|
|
10
28
|
|
|
11
29
|
/**
|
|
12
30
|
* crag analyze — generate governance.md from existing project without interview.
|
|
13
|
-
* Reads CI configs, package manifests, linter configs, git history.
|
|
14
31
|
*/
|
|
15
32
|
function analyze(args) {
|
|
16
33
|
const dryRun = args.includes('--dry-run');
|
|
17
|
-
const
|
|
34
|
+
const workspaceFlag = args.includes('--workspace');
|
|
18
35
|
const merge = args.includes('--merge');
|
|
19
36
|
const cwd = process.cwd();
|
|
20
37
|
|
|
@@ -22,17 +39,26 @@ function analyze(args) {
|
|
|
22
39
|
|
|
23
40
|
const analysis = analyzeProject(cwd);
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
// Workspace detection always runs. If a workspace is detected, we either:
|
|
43
|
+
// (a) emit per-member sections when --workspace is passed, OR
|
|
44
|
+
// (b) print a hint so the user knows to opt in.
|
|
45
|
+
const ws = detectWorkspace(cwd);
|
|
46
|
+
if (ws.type !== 'none') {
|
|
47
|
+
analysis.workspaceType = ws.type;
|
|
48
|
+
if (workspaceFlag) {
|
|
49
|
+
const members = filterFixtureMembers(enumerateMembers(ws));
|
|
50
|
+
console.log(` Workspace detected: ${ws.type} (${members.length} real members after fixture filter)\n`);
|
|
30
51
|
analysis.workspace = { type: ws.type, members: [] };
|
|
31
|
-
|
|
32
52
|
for (const member of members) {
|
|
33
53
|
const memberAnalysis = analyzeProject(member.path);
|
|
34
|
-
analysis.workspace.members.push({
|
|
54
|
+
analysis.workspace.members.push({
|
|
55
|
+
name: member.name,
|
|
56
|
+
relativePath: member.relativePath,
|
|
57
|
+
...memberAnalysis,
|
|
58
|
+
});
|
|
35
59
|
}
|
|
60
|
+
} else {
|
|
61
|
+
console.log(` Workspace detected: ${ws.type}. Pass --workspace for per-member gates.\n`);
|
|
36
62
|
}
|
|
37
63
|
}
|
|
38
64
|
|
|
@@ -56,7 +82,6 @@ function analyze(args) {
|
|
|
56
82
|
fs.copyFileSync(govPath, backupPath);
|
|
57
83
|
console.log(` Backed up existing governance to ${path.basename(backupPath)}`);
|
|
58
84
|
} catch (err) {
|
|
59
|
-
// Backup failure shouldn't block the analyze, but warn loudly.
|
|
60
85
|
cliWarn(`could not create backup (continuing anyway): ${err.message}`);
|
|
61
86
|
}
|
|
62
87
|
}
|
|
@@ -78,12 +103,23 @@ function analyze(args) {
|
|
|
78
103
|
console.log(` Run 'crag check' to verify infrastructure.\n`);
|
|
79
104
|
}
|
|
80
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Filter out test fixture directories from workspace member lists.
|
|
108
|
+
* These directories (playground/, fixtures/, etc.) contain dozens of one-off
|
|
109
|
+
* test setups that should not each get their own governance section.
|
|
110
|
+
*/
|
|
111
|
+
function filterFixtureMembers(members) {
|
|
112
|
+
return members.filter(m => {
|
|
113
|
+
const parts = m.relativePath.split(/[\\/]/);
|
|
114
|
+
return !parts.some(p => FIXTURE_DIR_PATTERNS.some(rx => rx.test(p)));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
function analyzeProject(dir) {
|
|
82
119
|
const result = {
|
|
83
120
|
name: path.basename(dir),
|
|
84
121
|
description: '',
|
|
85
122
|
stack: [],
|
|
86
|
-
gates: [],
|
|
87
123
|
linters: [],
|
|
88
124
|
formatters: [],
|
|
89
125
|
testers: [],
|
|
@@ -93,175 +129,116 @@ function analyzeProject(dir) {
|
|
|
93
129
|
deployment: [],
|
|
94
130
|
ci: null,
|
|
95
131
|
ciGates: [],
|
|
132
|
+
advisories: [],
|
|
133
|
+
docGates: [],
|
|
134
|
+
taskTargets: { make: [], task: [], just: [] },
|
|
96
135
|
};
|
|
97
136
|
|
|
98
|
-
//
|
|
137
|
+
// Stack detection (languages, frameworks, package managers)
|
|
99
138
|
detectStack(dir, result);
|
|
100
139
|
|
|
101
|
-
//
|
|
102
|
-
|
|
140
|
+
// CI system detection + raw command extraction (all supported CI systems)
|
|
141
|
+
const ci = extractCiCommands(dir);
|
|
142
|
+
result.ci = ci.system;
|
|
143
|
+
result.ciGates = normalizeCiGates(ci.commands.filter(c => isGateCommand(c)));
|
|
103
144
|
|
|
104
|
-
//
|
|
105
|
-
|
|
145
|
+
// Task runner target mining
|
|
146
|
+
result.taskTargets = mineTaskTargets(dir);
|
|
106
147
|
|
|
107
|
-
//
|
|
108
|
-
|
|
148
|
+
// Gate inference per language/runtime (consumes _manifests)
|
|
149
|
+
inferGates(dir, result);
|
|
109
150
|
|
|
110
|
-
//
|
|
151
|
+
// Emit explicit gates for mined task targets
|
|
152
|
+
emitTaskRunnerGates(result);
|
|
153
|
+
|
|
154
|
+
// Documentation-based gate mining (advisory)
|
|
155
|
+
result.docGates = mineDocGates(dir);
|
|
156
|
+
|
|
157
|
+
// Legacy deployment detection
|
|
111
158
|
detectDeployment(dir, result);
|
|
112
159
|
|
|
113
|
-
//
|
|
160
|
+
// Git-derived branch strategy + commit convention
|
|
114
161
|
inferGitPatterns(dir, result);
|
|
115
162
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
121
|
-
result.stack.push('node');
|
|
122
|
-
try {
|
|
123
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
124
|
-
result.name = pkg.name || result.name;
|
|
125
|
-
result.description = pkg.description || '';
|
|
126
|
-
|
|
127
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
128
|
-
if (deps.next) result.stack.push('next.js');
|
|
129
|
-
if (deps.react && !deps.next) result.stack.push('react');
|
|
130
|
-
if (deps.vue) result.stack.push('vue');
|
|
131
|
-
if (deps.svelte) result.stack.push('svelte');
|
|
132
|
-
if (deps.express) result.stack.push('express');
|
|
133
|
-
if (deps.fastify) result.stack.push('fastify');
|
|
134
|
-
if (deps.typescript) result.stack.push('typescript');
|
|
135
|
-
} catch { /* skip */ }
|
|
163
|
+
// Carry advisories collected during gate inference (hadolint, actionlint)
|
|
164
|
+
if (result._advisories) {
|
|
165
|
+
result.advisories.push(...result._advisories);
|
|
166
|
+
delete result._advisories;
|
|
136
167
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (fs.existsSync(path.join(dir, 'pom.xml'))) result.stack.push('java/maven');
|
|
142
|
-
if (fs.existsSync(path.join(dir, 'Dockerfile'))) result.stack.push('docker');
|
|
168
|
+
// Drop the internal manifests attachment before returning
|
|
169
|
+
delete result._manifests;
|
|
170
|
+
|
|
171
|
+
return result;
|
|
143
172
|
}
|
|
144
173
|
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const out = [];
|
|
154
|
-
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
155
|
-
const full = path.join(d, entry.name);
|
|
156
|
-
if (entry.isDirectory()) out.push(...walk(full));
|
|
157
|
-
else if (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml')) out.push(full);
|
|
158
|
-
}
|
|
159
|
-
return out;
|
|
160
|
-
};
|
|
161
|
-
for (const file of walk(workflowDir)) {
|
|
162
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
163
|
-
for (const cmd of extractRunCommands(content)) {
|
|
164
|
-
if (isGateCommand(cmd)) result.ciGates.push(cmd);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
} catch { /* skip */ }
|
|
174
|
+
function emitTaskRunnerGates(result) {
|
|
175
|
+
const { make, task, just } = result.taskTargets;
|
|
176
|
+
for (const target of make) {
|
|
177
|
+
// Canonical gate name: use `make X` literally
|
|
178
|
+
const cmd = `make ${target}`;
|
|
179
|
+
if (isTestTarget(target)) addUnique(result.testers, cmd);
|
|
180
|
+
else if (isLintTarget(target)) addUnique(result.linters, cmd);
|
|
181
|
+
else if (isBuildTarget(target)) addUnique(result.builders, cmd);
|
|
168
182
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
result.
|
|
183
|
+
for (const target of task) {
|
|
184
|
+
const cmd = `task ${target}`;
|
|
185
|
+
if (isTestTarget(target)) addUnique(result.testers, cmd);
|
|
186
|
+
else if (isLintTarget(target)) addUnique(result.linters, cmd);
|
|
187
|
+
else if (isBuildTarget(target)) addUnique(result.builders, cmd);
|
|
173
188
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
result.
|
|
189
|
+
for (const target of just) {
|
|
190
|
+
const cmd = `just ${target}`;
|
|
191
|
+
if (isTestTarget(target)) addUnique(result.testers, cmd);
|
|
192
|
+
else if (isLintTarget(target)) addUnique(result.linters, cmd);
|
|
193
|
+
else if (isBuildTarget(target)) addUnique(result.builders, cmd);
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
function extractPackageScripts(dir, result) {
|
|
184
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
185
|
-
if (!fs.existsSync(pkgPath)) return;
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
189
|
-
const scripts = pkg.scripts || {};
|
|
190
|
-
|
|
191
|
-
const scriptMap = {
|
|
192
|
-
test: 'testers',
|
|
193
|
-
lint: 'linters',
|
|
194
|
-
build: 'builders',
|
|
195
|
-
format: 'formatters',
|
|
196
|
-
typecheck: 'builders',
|
|
197
|
-
check: 'linters',
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
for (const [key, category] of Object.entries(scriptMap)) {
|
|
201
|
-
if (scripts[key]) {
|
|
202
|
-
result[category].push(`npm run ${key}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Also check for specific patterns
|
|
207
|
-
if (scripts['lint:fix']) result.formatters.push('npm run lint:fix');
|
|
208
|
-
if (scripts['format:check']) result.linters.push('npm run format:check');
|
|
209
|
-
} catch { /* skip */ }
|
|
197
|
+
function addUnique(arr, item) {
|
|
198
|
+
if (!arr.includes(item)) arr.push(item);
|
|
210
199
|
}
|
|
211
200
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
['prettier.config.js', 'prettier'], ['prettier.config.mjs', 'prettier'],
|
|
219
|
-
['ruff.toml', 'ruff'], ['.ruff.toml', 'ruff'],
|
|
220
|
-
['clippy.toml', 'clippy'], ['.clippy.toml', 'clippy'],
|
|
221
|
-
['rustfmt.toml', 'rustfmt'], ['.rustfmt.toml', 'rustfmt'],
|
|
222
|
-
['tsconfig.json', 'typescript'],
|
|
223
|
-
['.mypy.ini', 'mypy'], ['mypy.ini', 'mypy'],
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
for (const [file, tool] of linterConfigs) {
|
|
227
|
-
if (fs.existsSync(path.join(dir, file))) {
|
|
228
|
-
if (!result.linters.includes(tool)) result.linters.push(tool);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Build/task file detection
|
|
233
|
-
const taskFiles = ['Makefile', 'Taskfile.yml', 'justfile'];
|
|
234
|
-
for (const file of taskFiles) {
|
|
235
|
-
if (fs.existsSync(path.join(dir, file))) {
|
|
236
|
-
result.builders.push(`${file} detected`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
201
|
+
const TEST_TARGETS = new Set(['test', 'tests', 'spec', 'check', 'ci', 'verify', 'validate']);
|
|
202
|
+
const LINT_TARGETS = new Set(['lint', 'format', 'fmt', 'style', 'typecheck', 'type-check', 'types']);
|
|
203
|
+
const BUILD_TARGETS = new Set(['build', 'compile']);
|
|
204
|
+
const isTestTarget = (t) => TEST_TARGETS.has(t);
|
|
205
|
+
const isLintTarget = (t) => LINT_TARGETS.has(t);
|
|
206
|
+
const isBuildTarget = (t) => BUILD_TARGETS.has(t);
|
|
240
207
|
|
|
241
208
|
function detectDeployment(dir, result) {
|
|
242
209
|
if (fs.existsSync(path.join(dir, 'Dockerfile'))) result.deployment.push('docker');
|
|
243
|
-
if (fs.existsSync(path.join(dir, 'docker-compose.yml')) ||
|
|
210
|
+
if (fs.existsSync(path.join(dir, 'docker-compose.yml')) ||
|
|
211
|
+
fs.existsSync(path.join(dir, 'docker-compose.yaml')) ||
|
|
212
|
+
fs.existsSync(path.join(dir, 'compose.yml')) ||
|
|
213
|
+
fs.existsSync(path.join(dir, 'compose.yaml'))) result.deployment.push('docker-compose');
|
|
244
214
|
if (fs.existsSync(path.join(dir, 'vercel.json')) || fs.existsSync(path.join(dir, '.vercel'))) result.deployment.push('vercel');
|
|
245
215
|
if (fs.existsSync(path.join(dir, 'fly.toml'))) result.deployment.push('fly.io');
|
|
246
216
|
if (fs.existsSync(path.join(dir, 'netlify.toml'))) result.deployment.push('netlify');
|
|
247
217
|
if (fs.existsSync(path.join(dir, 'render.yaml'))) result.deployment.push('render');
|
|
218
|
+
if (fs.existsSync(path.join(dir, 'railway.json')) || fs.existsSync(path.join(dir, 'railway.toml'))) result.deployment.push('railway');
|
|
219
|
+
if (fs.existsSync(path.join(dir, 'wrangler.toml'))) result.deployment.push('cloudflare-workers');
|
|
220
|
+
if (fs.existsSync(path.join(dir, 'app.yaml'))) result.deployment.push('gcp-app-engine');
|
|
221
|
+
if (fs.existsSync(path.join(dir, 'serverless.yml')) || fs.existsSync(path.join(dir, 'serverless.yaml'))) result.deployment.push('serverless-framework');
|
|
222
|
+
if (fs.existsSync(path.join(dir, 'template.yaml')) || fs.existsSync(path.join(dir, 'template.yml'))) result.deployment.push('aws-sam');
|
|
248
223
|
|
|
249
|
-
// Kubernetes
|
|
250
224
|
try {
|
|
251
225
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
252
226
|
for (const entry of entries) {
|
|
253
|
-
if (entry.isDirectory() && (entry.name === 'k8s' || entry.name === 'kubernetes' || entry.name === 'deploy')) {
|
|
227
|
+
if (entry.isDirectory() && (entry.name === 'k8s' || entry.name === 'kubernetes' || entry.name === 'deploy' || entry.name === 'manifests')) {
|
|
254
228
|
result.deployment.push('kubernetes');
|
|
255
229
|
break;
|
|
256
230
|
}
|
|
257
231
|
}
|
|
258
232
|
} catch { /* skip */ }
|
|
259
233
|
|
|
260
|
-
// Terraform
|
|
261
234
|
try {
|
|
262
235
|
const entries = fs.readdirSync(dir);
|
|
263
236
|
if (entries.some(f => f.endsWith('.tf'))) result.deployment.push('terraform');
|
|
264
237
|
} catch { /* skip */ }
|
|
238
|
+
|
|
239
|
+
if (fs.existsSync(path.join(dir, 'Chart.yaml'))) result.deployment.push('helm');
|
|
240
|
+
if (fs.existsSync(path.join(dir, 'Pulumi.yaml')) || fs.existsSync(path.join(dir, 'Pulumi.yml'))) result.deployment.push('pulumi');
|
|
241
|
+
if (fs.existsSync(path.join(dir, 'ansible.cfg'))) result.deployment.push('ansible');
|
|
265
242
|
}
|
|
266
243
|
|
|
267
244
|
function inferGitPatterns(dir, result) {
|
|
@@ -269,19 +246,14 @@ function inferGitPatterns(dir, result) {
|
|
|
269
246
|
const log = execSync('git log --oneline --all -50', { cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
270
247
|
const lines = log.trim().split('\n');
|
|
271
248
|
|
|
272
|
-
// Detect conventional commits
|
|
273
249
|
const conventional = lines.filter(l => /\b(feat|fix|docs|chore|style|refactor|test|build|ci|perf|revert)[\(:!]/.test(l));
|
|
274
250
|
result.commitConvention = conventional.length > lines.length * 0.3 ? 'conventional' : 'free-form';
|
|
275
251
|
|
|
276
|
-
// Detect branch strategy
|
|
277
252
|
const branches = execSync('git branch -a --format="%(refname:short)"', { cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
278
253
|
const branchList = branches.trim().split('\n');
|
|
279
254
|
const featureBranches = branchList.filter(b => /^(feat|fix|docs|chore|feature|hotfix|release)\//.test(b));
|
|
280
255
|
result.branchStrategy = featureBranches.length > 2 ? 'feature-branches' : 'trunk-based';
|
|
281
256
|
} catch (err) {
|
|
282
|
-
// git may not be installed, or this may not be a git repo — non-fatal,
|
|
283
|
-
// just report and leave defaults. Matches the "best-effort" semantics of
|
|
284
|
-
// analyze (it infers what it can from whatever exists).
|
|
285
257
|
if (err && err.code !== 'ENOENT') {
|
|
286
258
|
cliWarn(`could not detect git patterns in ${path.basename(dir)}: ${err.message}`);
|
|
287
259
|
}
|
|
@@ -293,36 +265,24 @@ function inferGitPatterns(dir, result) {
|
|
|
293
265
|
function generateGovernance(analysis, cwd) {
|
|
294
266
|
const sections = [];
|
|
295
267
|
|
|
296
|
-
// Identity
|
|
297
268
|
sections.push(`# Governance — ${analysis.name}`);
|
|
298
269
|
sections.push(`# Inferred by crag analyze — review and adjust as needed\n`);
|
|
299
270
|
sections.push('## Identity');
|
|
300
271
|
sections.push(`- Project: ${analysis.name}`);
|
|
301
272
|
if (analysis.description) sections.push(`- Description: ${analysis.description}`);
|
|
302
273
|
sections.push(`- Stack: ${analysis.stack.join(', ') || 'unknown'}`);
|
|
274
|
+
if (analysis.workspaceType) sections.push(`- Workspace: ${analysis.workspaceType}`);
|
|
303
275
|
sections.push('');
|
|
304
276
|
|
|
305
|
-
// Gates
|
|
306
277
|
sections.push('## Gates (run in order, stop on failure)');
|
|
307
278
|
|
|
308
|
-
// Group gates by type
|
|
309
279
|
const allGates = new Set();
|
|
310
280
|
|
|
311
|
-
//
|
|
281
|
+
// Lint
|
|
312
282
|
if (analysis.linters.length > 0) {
|
|
313
283
|
sections.push('### Lint');
|
|
314
|
-
for (const
|
|
315
|
-
|
|
316
|
-
switch (linter) {
|
|
317
|
-
case 'eslint': cmd = 'npx eslint . --max-warnings 0'; break;
|
|
318
|
-
case 'biome': cmd = 'npx biome check .'; break;
|
|
319
|
-
case 'ruff': cmd = 'ruff check .'; break;
|
|
320
|
-
case 'clippy': cmd = 'cargo clippy -- -D warnings'; break;
|
|
321
|
-
case 'mypy': cmd = 'mypy .'; break;
|
|
322
|
-
case 'typescript': cmd = 'npx tsc --noEmit'; break;
|
|
323
|
-
default: cmd = null;
|
|
324
|
-
}
|
|
325
|
-
if (cmd && !allGates.has(cmd)) {
|
|
284
|
+
for (const cmd of analysis.linters) {
|
|
285
|
+
if (!allGates.has(cmd)) {
|
|
326
286
|
sections.push(`- ${cmd}`);
|
|
327
287
|
allGates.add(cmd);
|
|
328
288
|
}
|
|
@@ -330,73 +290,73 @@ function generateGovernance(analysis, cwd) {
|
|
|
330
290
|
sections.push('');
|
|
331
291
|
}
|
|
332
292
|
|
|
333
|
-
//
|
|
293
|
+
// Test
|
|
334
294
|
if (analysis.testers.length > 0) {
|
|
335
295
|
sections.push('### Test');
|
|
336
|
-
for (const
|
|
337
|
-
if (!allGates.has(
|
|
338
|
-
sections.push(`- ${
|
|
339
|
-
allGates.add(
|
|
296
|
+
for (const cmd of analysis.testers) {
|
|
297
|
+
if (!allGates.has(cmd)) {
|
|
298
|
+
sections.push(`- ${cmd}`);
|
|
299
|
+
allGates.add(cmd);
|
|
340
300
|
}
|
|
341
301
|
}
|
|
342
302
|
sections.push('');
|
|
343
303
|
}
|
|
344
304
|
|
|
345
|
-
//
|
|
305
|
+
// Build
|
|
346
306
|
if (analysis.builders.length > 0) {
|
|
347
307
|
sections.push('### Build');
|
|
348
|
-
for (const
|
|
349
|
-
if (!
|
|
350
|
-
sections.push(`- ${
|
|
351
|
-
allGates.add(
|
|
308
|
+
for (const cmd of analysis.builders) {
|
|
309
|
+
if (!allGates.has(cmd)) {
|
|
310
|
+
sections.push(`- ${cmd}`);
|
|
311
|
+
allGates.add(cmd);
|
|
352
312
|
}
|
|
353
313
|
}
|
|
354
314
|
sections.push('');
|
|
355
315
|
}
|
|
356
316
|
|
|
357
|
-
//
|
|
317
|
+
// CI — only include gates not already captured by language inference
|
|
358
318
|
const uniqueCiGates = analysis.ciGates.filter(g => !allGates.has(g));
|
|
359
319
|
if (uniqueCiGates.length > 0) {
|
|
360
320
|
sections.push('### CI (inferred from workflow)');
|
|
361
321
|
for (const gate of uniqueCiGates) {
|
|
362
322
|
sections.push(`- ${gate}`);
|
|
323
|
+
allGates.add(gate);
|
|
363
324
|
}
|
|
364
325
|
sections.push('');
|
|
365
326
|
}
|
|
366
327
|
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
sections.push(
|
|
328
|
+
// Documentation-derived gates (advisory — need user confirmation)
|
|
329
|
+
const newDocGates = (analysis.docGates || [])
|
|
330
|
+
.filter(g => !allGates.has(g.command));
|
|
331
|
+
if (newDocGates.length > 0) {
|
|
332
|
+
sections.push('### Contributor docs (ADVISORY — confirm before enforcing)');
|
|
333
|
+
for (const { command, source } of newDocGates) {
|
|
334
|
+
sections.push(`- ${command} # from ${source}`);
|
|
335
|
+
allGates.add(command);
|
|
374
336
|
}
|
|
337
|
+
sections.push('');
|
|
375
338
|
}
|
|
376
339
|
|
|
377
|
-
//
|
|
378
|
-
if (analysis.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
sections.push(
|
|
340
|
+
// Workspace members — per-member gates
|
|
341
|
+
if (analysis.workspace && analysis.workspace.members.length > 0) {
|
|
342
|
+
for (const member of analysis.workspace.members) {
|
|
343
|
+
if (member.linters.length === 0 && member.testers.length === 0 && member.builders.length === 0) continue;
|
|
344
|
+
const pathAnnotation = member.relativePath ? ` (path: ${member.relativePath.replace(/\\/g, '/')})` : '';
|
|
345
|
+
sections.push(`### ${member.name}${pathAnnotation}`);
|
|
346
|
+
for (const cmd of [...member.linters, ...member.testers, ...member.builders]) {
|
|
347
|
+
sections.push(`- ${cmd}`);
|
|
348
|
+
}
|
|
383
349
|
sections.push('');
|
|
384
350
|
}
|
|
385
351
|
}
|
|
386
352
|
|
|
387
|
-
//
|
|
388
|
-
if (analysis.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
for (const bin of binFiles) {
|
|
395
|
-
sections.push(`- node --check ${bin}`);
|
|
396
|
-
}
|
|
397
|
-
sections.push('');
|
|
398
|
-
}
|
|
399
|
-
} catch { /* skip */ }
|
|
353
|
+
// Advisories
|
|
354
|
+
if (analysis.advisories && analysis.advisories.length > 0) {
|
|
355
|
+
sections.push('## Advisories (informational, not enforced)');
|
|
356
|
+
for (const a of analysis.advisories) {
|
|
357
|
+
sections.push(`- ${a} # [ADVISORY]`);
|
|
358
|
+
}
|
|
359
|
+
sections.push('');
|
|
400
360
|
}
|
|
401
361
|
|
|
402
362
|
// Branch Strategy
|
|
@@ -419,7 +379,7 @@ function generateGovernance(analysis, cwd) {
|
|
|
419
379
|
// Deployment
|
|
420
380
|
if (analysis.deployment.length > 0) {
|
|
421
381
|
sections.push('## Deployment');
|
|
422
|
-
sections.push(`- Target: ${analysis.deployment.join(', ')}`);
|
|
382
|
+
sections.push(`- Target: ${[...new Set(analysis.deployment)].join(', ')}`);
|
|
423
383
|
if (analysis.ci) sections.push(`- CI: ${analysis.ci}`);
|
|
424
384
|
sections.push('');
|
|
425
385
|
}
|
|
@@ -428,15 +388,11 @@ function generateGovernance(analysis, cwd) {
|
|
|
428
388
|
}
|
|
429
389
|
|
|
430
390
|
function mergeWithExisting(existing, generated) {
|
|
431
|
-
// Simple merge: keep existing content, append new sections that don't exist
|
|
432
391
|
const existingSections = new Set();
|
|
433
392
|
for (const match of existing.matchAll(/^## (.+)$/gm)) {
|
|
434
393
|
existingSections.add(match[1].trim().toLowerCase());
|
|
435
394
|
}
|
|
436
395
|
|
|
437
|
-
// Walk the generated file and collect new sections in their original order.
|
|
438
|
-
// Each new section becomes a self-contained block: the heading line plus all
|
|
439
|
-
// following lines until the next heading or EOF.
|
|
440
396
|
const newBlocks = [];
|
|
441
397
|
const genLines = generated.split('\n');
|
|
442
398
|
let currentBlock = null;
|
|
@@ -445,11 +401,9 @@ function mergeWithExisting(existing, generated) {
|
|
|
445
401
|
for (const line of genLines) {
|
|
446
402
|
const sectionMatch = line.match(/^## (.+)$/);
|
|
447
403
|
if (sectionMatch) {
|
|
448
|
-
// Flush previous block if it was new
|
|
449
404
|
if (currentBlock && blockIsNew) {
|
|
450
405
|
newBlocks.push(currentBlock.trimEnd());
|
|
451
406
|
}
|
|
452
|
-
// Start new block
|
|
453
407
|
const section = sectionMatch[1].trim().toLowerCase();
|
|
454
408
|
blockIsNew = !existingSections.has(section);
|
|
455
409
|
currentBlock = blockIsNew ? line + '\n' : null;
|
|
@@ -457,7 +411,6 @@ function mergeWithExisting(existing, generated) {
|
|
|
457
411
|
currentBlock += line + '\n';
|
|
458
412
|
}
|
|
459
413
|
}
|
|
460
|
-
// Flush final block
|
|
461
414
|
if (currentBlock && blockIsNew) {
|
|
462
415
|
newBlocks.push(currentBlock.trimEnd());
|
|
463
416
|
}
|
|
@@ -60,28 +60,84 @@ function extractRunCommands(content) {
|
|
|
60
60
|
*/
|
|
61
61
|
function isGateCommand(cmd) {
|
|
62
62
|
const patterns = [
|
|
63
|
+
// Node ecosystem
|
|
63
64
|
/\bnpm (run |ci|test|install)/,
|
|
64
65
|
/\bnpx /,
|
|
65
66
|
/\bnode /,
|
|
66
|
-
/\
|
|
67
|
+
/\byarn (test|lint|build|check)/,
|
|
68
|
+
/\bpnpm (run |test|lint|build|check|install|i\b)/,
|
|
69
|
+
/\bbun (test|run)/,
|
|
70
|
+
/\bdeno (test|lint|fmt|check)/,
|
|
71
|
+
// Rust
|
|
72
|
+
/\bcargo (test|build|check|clippy|fmt)/,
|
|
67
73
|
/\brustfmt/,
|
|
74
|
+
// Go
|
|
68
75
|
/\bgo (test|build|vet)/,
|
|
69
76
|
/\bgolangci-lint/,
|
|
77
|
+
// Python — direct + modern runner wrappers
|
|
70
78
|
/\bpytest/,
|
|
71
79
|
/\bpython -m/,
|
|
72
80
|
/\bruff/,
|
|
73
81
|
/\bmypy/,
|
|
74
82
|
/\bflake8/,
|
|
83
|
+
/\bblack\b/,
|
|
84
|
+
/\bisort\b/,
|
|
85
|
+
/\bpylint\b/,
|
|
86
|
+
/\btox\s+(run|r)/,
|
|
87
|
+
/\buv run /,
|
|
88
|
+
/\bpoetry run /,
|
|
89
|
+
/\bpdm run /,
|
|
90
|
+
/\bhatch run /,
|
|
91
|
+
/\brye run /,
|
|
92
|
+
/\bnox\b/,
|
|
93
|
+
// JVM
|
|
75
94
|
/\bgradle/,
|
|
76
95
|
/\bmvn /,
|
|
77
96
|
/\bmaven/,
|
|
97
|
+
/\.\/gradlew/,
|
|
98
|
+
/\.\/mvnw/,
|
|
99
|
+
// Ruby
|
|
100
|
+
/\bbundle exec /,
|
|
101
|
+
/\brake\b/,
|
|
102
|
+
/\brspec\b/,
|
|
103
|
+
/\brubocop/,
|
|
104
|
+
// PHP
|
|
105
|
+
/\bcomposer (test|lint|run|validate)/,
|
|
106
|
+
/\bvendor\/bin\/(phpunit|phpcs|phpstan|psalm|pest|php-cs-fixer|rector)/,
|
|
107
|
+
// .NET
|
|
108
|
+
/\bdotnet (test|build|format)/,
|
|
109
|
+
// Swift
|
|
110
|
+
/\bswift (test|build)/,
|
|
111
|
+
/\bswiftlint/,
|
|
112
|
+
// Elixir
|
|
113
|
+
/\bmix (test|format|credo|dialyzer)/,
|
|
114
|
+
// Node linters
|
|
78
115
|
/\beslint/,
|
|
79
116
|
/\bbiome/,
|
|
80
117
|
/\bprettier/,
|
|
81
118
|
/\btsc/,
|
|
82
|
-
/\
|
|
119
|
+
/\bxo\b/,
|
|
120
|
+
// Task runners
|
|
83
121
|
/\bmake /,
|
|
84
122
|
/\bjust /,
|
|
123
|
+
/\btask /,
|
|
124
|
+
// Containers / infra
|
|
125
|
+
/\bdocker (build|compose)/,
|
|
126
|
+
/\bterraform (fmt|validate|plan)/,
|
|
127
|
+
/\btflint/,
|
|
128
|
+
/\bhelm (lint|template)/,
|
|
129
|
+
/\bkubeconform/,
|
|
130
|
+
/\bkubeval/,
|
|
131
|
+
/\bhadolint/,
|
|
132
|
+
/\bactionlint/,
|
|
133
|
+
/\bmarkdownlint/,
|
|
134
|
+
/\byamllint/,
|
|
135
|
+
/\bbuf (lint|build)/,
|
|
136
|
+
/\bspectral lint/,
|
|
137
|
+
/\bshellcheck/,
|
|
138
|
+
/\bsemgrep/,
|
|
139
|
+
/\btrivy/,
|
|
140
|
+
/\bgitleaks/,
|
|
85
141
|
];
|
|
86
142
|
return patterns.some((p) => p.test(cmd));
|
|
87
143
|
}
|