clckernel 1.2.19 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/clckernel_cli/SKILL.md +23 -6
- package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
- package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
- package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
- package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
- package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
- package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
- package/.agents/skills/clckernel_cli/reference/start.md +14 -4
- package/.agents/skills/clckernel_cli/reference/test.md +1 -1
- package/.agents/skills/clckernel_cli/reference/update.md +19 -19
- package/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.es.md +280 -51
- package/README.md +274 -50
- package/bin/cli.js +8 -0
- package/package.json +12 -2
- package/src/adapters/agent-os.js +161 -84
- package/src/adapters/astro.js +8 -6
- package/src/adapters/base.js +23 -38
- package/src/adapters/django.js +14 -13
- package/src/adapters/fastapi.js +16 -15
- package/src/adapters/go.js +11 -9
- package/src/adapters/laravel.js +12 -10
- package/src/adapters/nextjs.js +7 -5
- package/src/adapters/rails.js +13 -11
- package/src/adapters/rust.js +11 -9
- package/src/catalog.js +58 -49
- package/src/config.js +38 -240
- package/src/detector.js +243 -44
- package/src/doctor.js +18 -9
- package/src/generator.js +734 -302
- package/src/gitignore.js +60 -14
- package/src/index.js +257 -18
- package/src/io.js +409 -0
- package/src/known-hashes.json +309 -0
- package/src/onboarding.js +15 -0
- package/src/technologies/detector.js +1 -1
- package/src/technologies/guards/_shared.js +1 -1
- package/src/ui.js +233 -40
- package/src/updater.js +271 -38
- package/tools/_changes.js +279 -0
- package/tools/_changes.py +233 -0
- package/tools/_config.js +294 -0
- package/tools/_config.py +271 -0
- package/tools/_yaml.py +489 -0
- package/tools/audit.js +511 -111
- package/tools/audit.py +506 -128
- package/tools/celery_guard.py +13 -35
- package/tools/check_a11y.js +8 -28
- package/tools/check_api_contracts.js +13 -39
- package/tools/check_architecture.js +35 -44
- package/tools/check_architecture.py +172 -105
- package/tools/check_custom.js +108 -61
- package/tools/check_custom.py +108 -271
- package/tools/check_data_files.js +88 -0
- package/tools/check_db_efficiency.py +80 -65
- package/tools/check_hitl.js +112 -69
- package/tools/check_links.js +138 -0
- package/tools/check_migrations.py +143 -94
- package/tools/check_performance.js +35 -44
- package/tools/check_placeholders.js +104 -0
- package/tools/check_responsive.js +8 -28
- package/tools/check_scope.py +40 -31
- package/tools/check_seo.js +8 -28
- package/tools/check_skills.js +158 -0
- package/tools/check_storybook.js +8 -21
- package/tools/check_symlinks.js +40 -22
- package/tools/check_ui_reuse.js +17 -29
- package/tools/docker_guard.py +19 -41
- package/tools/postgres_guard.py +15 -36
- package/tools/redis_guard.py +13 -35
- package/tools/scan_secrets.js +134 -67
- package/tools/scan_secrets.py +163 -72
- package/tools/verify_tdd.js +867 -0
- package/tools/verify_tdd.py +407 -165
- package/tools/yaml.js +404 -132
- package/tools/check_domain_data.js +0 -100
- package/tools/check_test_calendar_sync.js +0 -111
package/tools/audit.js
CHANGED
|
@@ -1,19 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Modular Audit Orchestrator — JS
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The single place that decides which guards run (sdds/unified-discovery):
|
|
5
|
+
* `clckernel audit` and agents run it, the pre-commit hook runs it with --hook.
|
|
6
|
+
* Same discovery, same guards, each exactly once:
|
|
7
|
+
* - top-level tools/ guards: check_*, scan_*, verify_* or *_guard, .js/.py;
|
|
8
|
+
* - every tools/guards/*.js and *.py (project guards).
|
|
9
|
+
* audit.js runs the .js guards; the .py ones belong to audit.py, unless
|
|
10
|
+
* tools/audit.py is not installed: then audit.js runs them too, with python3.
|
|
11
|
+
* A top-level guard of this language that exports a function is called in
|
|
12
|
+
* process; every other guard runs as a process (exit code decides, a JSON
|
|
13
|
+
* result is optional). A guard listed in the managed registry but missing on
|
|
14
|
+
* disk fails the run. Results are grouped into 4 lenses.
|
|
15
|
+
*
|
|
16
|
+
* CLI (sdds/audit-cli): audit.js [targetDir] [--hook] [--change <name>] [--staged]
|
|
17
|
+
* (none) print the 4-lens summary; write nothing
|
|
18
|
+
* --change <name> also write sdds/<name>/05-audit-report.md and
|
|
19
|
+
* audit-receipt.json (<name>: one safe path segment);
|
|
20
|
+
* 05-audit-report.js.md / audit-receipt.js.json when
|
|
21
|
+
* tools/audit.py is installed too (mixed stack)
|
|
22
|
+
* --hook one line per guard; write nothing (not with --change)
|
|
23
|
+
* --staged the default snapshot, spelled out: the staged changes,
|
|
24
|
+
* or the last commit when nothing is staged (_changes)
|
|
25
|
+
* Unknown input exits 2. A failing guard whose level is 'warning' (its result's
|
|
26
|
+
* `level`, overridden by `severities` in .clckernel.yaml) is reported but does
|
|
27
|
+
* not fail the run; a guard that crashes always does. Self-contained: Node.js
|
|
28
|
+
* built-ins plus the sibling _config.js (optional).
|
|
9
29
|
*/
|
|
10
30
|
|
|
11
31
|
const fs = require('fs');
|
|
12
32
|
const path = require('path');
|
|
33
|
+
const { spawnSync } = require('child_process');
|
|
13
34
|
|
|
14
35
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
36
|
+
const LENSES = ['correctness', 'security', 'architecture', 'performance'];
|
|
37
|
+
const PROJECT_GUARD_RUNNERS = { '.js': process.execPath, '.py': 'python3' };
|
|
38
|
+
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
39
|
+
const OWN_EXT = '.js';
|
|
40
|
+
const SIBLING = { ext: '.py', orchestrator: 'audit.py' };
|
|
41
|
+
const REGISTRY_FILE = '.clckernel-managed.json';
|
|
42
|
+
const USAGE = 'usage: audit.js [targetDir] [--hook] [--change <name>] [--staged]';
|
|
43
|
+
const CHANGE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
44
|
|
|
16
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Runs a Python guard that defines check() (module contract) as a process:
|
|
47
|
+
* prints its result as JSON and exits with its exit_code.
|
|
48
|
+
*/
|
|
49
|
+
const PY_MODULE_RUNNER = [
|
|
50
|
+
'import importlib.util, json, os, sys',
|
|
51
|
+
'p = os.path.abspath(sys.argv[1])',
|
|
52
|
+
'sys.path.insert(0, os.path.dirname(p))',
|
|
53
|
+
'spec = importlib.util.spec_from_file_location(os.path.splitext(os.path.basename(p))[0], p)',
|
|
54
|
+
'm = importlib.util.module_from_spec(spec)',
|
|
55
|
+
'spec.loader.exec_module(m)',
|
|
56
|
+
'r = m.check(os.getcwd())',
|
|
57
|
+
'if not isinstance(r, dict):',
|
|
58
|
+
' print("check() returned " + type(r).__name__, file=sys.stderr)',
|
|
59
|
+
' sys.exit(1)',
|
|
60
|
+
'print(json.dumps(r))',
|
|
61
|
+
'sys.exit(r.get("exit_code", 0) or 0)',
|
|
62
|
+
].join('\n');
|
|
63
|
+
|
|
64
|
+
// ── Static name → lens mapping (4 RDD lenses) ──────────────────────────────────────────
|
|
17
65
|
|
|
18
66
|
const LENS_MAP = {
|
|
19
67
|
// Correctness
|
|
@@ -26,6 +74,10 @@ const LENS_MAP = {
|
|
|
26
74
|
tsc: 'correctness',
|
|
27
75
|
lint: 'correctness',
|
|
28
76
|
ruff: 'correctness',
|
|
77
|
+
check_skills: 'correctness',
|
|
78
|
+
check_links: 'correctness',
|
|
79
|
+
check_placeholders: 'correctness',
|
|
80
|
+
check_data_files: 'correctness',
|
|
29
81
|
// Security
|
|
30
82
|
scan_secrets: 'security',
|
|
31
83
|
docker_guard: 'security',
|
|
@@ -38,7 +90,6 @@ const LENS_MAP = {
|
|
|
38
90
|
check_ui_reuse: 'architecture',
|
|
39
91
|
check_symlinks: 'architecture',
|
|
40
92
|
check_hitl: 'architecture',
|
|
41
|
-
check_domain_data: 'architecture',
|
|
42
93
|
check_migrations: 'architecture',
|
|
43
94
|
graphify: 'architecture',
|
|
44
95
|
gentle_ai_review: 'architecture',
|
|
@@ -48,7 +99,6 @@ const LENS_MAP = {
|
|
|
48
99
|
check_seo: 'performance',
|
|
49
100
|
check_responsive: 'performance',
|
|
50
101
|
check_storybook: 'performance',
|
|
51
|
-
check_test_calendar_sync: 'performance',
|
|
52
102
|
check_observability: 'performance',
|
|
53
103
|
};
|
|
54
104
|
|
|
@@ -63,42 +113,210 @@ function getLensForGuard(guardName) {
|
|
|
63
113
|
|
|
64
114
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
65
115
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return
|
|
116
|
+
/** True for a top-level guard file name: check_*, scan_*, verify_* or *_guard, .js or .py. */
|
|
117
|
+
function isGuardFile(name) {
|
|
118
|
+
const ext = path.extname(name);
|
|
119
|
+
if (ext !== '.js' && ext !== '.py') return false;
|
|
120
|
+
const stem = path.basename(name, ext);
|
|
121
|
+
return /^(check|scan|verify)_/.test(stem) || stem.endsWith('_guard');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Regular files of dir (names, sorted); [] when it cannot be read. */
|
|
125
|
+
function regularFiles(dir) {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
128
|
+
.filter(entry => entry.isFile())
|
|
129
|
+
.map(entry => entry.name)
|
|
130
|
+
.sort();
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
78
133
|
}
|
|
79
|
-
const defaultDir = path.join(sddsDir, 'audit-run');
|
|
80
|
-
fs.mkdirSync(defaultDir, { recursive: true });
|
|
81
|
-
return defaultDir;
|
|
82
134
|
}
|
|
83
135
|
|
|
84
136
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
137
|
+
* Top-level guards of this orchestrator's language (.js) in tools/,
|
|
138
|
+
* alphabetical. Snapshot helpers (_*), yaml.js and the orchestrators never
|
|
139
|
+
* match the guard name rule.
|
|
140
|
+
* @param {string} [toolsDirParam] - The tools/ directory
|
|
141
|
+
* @returns {string[]} Absolute paths
|
|
87
142
|
*/
|
|
88
143
|
function discoverGuards(toolsDirParam) {
|
|
89
144
|
const toolsDir = toolsDirParam || __dirname;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.filter(f => (f.startsWith('check_') || f.startsWith('scan_')) && f.endsWith('.js') && f !== path.basename(__filename || 'audit.js'))
|
|
93
|
-
.sort()
|
|
145
|
+
return regularFiles(toolsDir)
|
|
146
|
+
.filter(f => isGuardFile(f) && path.extname(f) === OWN_EXT)
|
|
94
147
|
.map(f => path.join(toolsDir, f));
|
|
95
148
|
}
|
|
96
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Project guards (sdds/domain-agnostic, decision 3): every *.js and *.py file
|
|
152
|
+
* in tools/guards/, alphabetical.
|
|
153
|
+
* @param {string} toolsDir - The project's tools/ directory
|
|
154
|
+
* @returns {string[]} Absolute paths
|
|
155
|
+
*/
|
|
156
|
+
function discoverProjectGuards(toolsDir) {
|
|
157
|
+
const dir = path.join(toolsDir, 'guards');
|
|
158
|
+
return regularFiles(dir)
|
|
159
|
+
.filter(name => PROJECT_GUARD_RUNNERS[path.extname(name)])
|
|
160
|
+
.map(name => path.join(dir, name));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Paths (relative to the project root) the managed registry lists, in
|
|
165
|
+
* either format: a list of paths, or { files: [{ path }] }.
|
|
166
|
+
* @param {string} toolsDir - The tools/ directory
|
|
167
|
+
* @returns {string[]}
|
|
168
|
+
*/
|
|
169
|
+
function registeredPaths(toolsDir) {
|
|
170
|
+
let data;
|
|
171
|
+
try {
|
|
172
|
+
data = JSON.parse(fs.readFileSync(path.join(toolsDir, REGISTRY_FILE), 'utf-8'));
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const entries = Array.isArray(data) ? data : (data && Array.isArray(data.files) ? data.files.map(f => f && f.path) : []);
|
|
177
|
+
return entries.filter(p => typeof p === 'string');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** True when a JS file follows the module contract (it exports something). */
|
|
181
|
+
function exportsModule(file) {
|
|
182
|
+
try {
|
|
183
|
+
return /\bmodule\.exports\b|^\s*exports\.[\w$]+\s*=/m.test(fs.readFileSync(file, 'utf-8'));
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** True when a Python file follows the module contract (a top-level check()). */
|
|
190
|
+
function definesCheck(file) {
|
|
191
|
+
try {
|
|
192
|
+
return /^def check\s*\(/m.test(fs.readFileSync(file, 'utf-8'));
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The guards this orchestrator runs (sdds/unified-discovery), in order:
|
|
200
|
+
* top-level guards, then tools/guards/*. Each entry says how it runs:
|
|
201
|
+
* 'module' — a top-level .js guard that exports a function: in process;
|
|
202
|
+
* 'process' — any other guard of a language this orchestrator owns.
|
|
203
|
+
* Languages owned: .js, plus .py when tools/audit.py is not installed.
|
|
204
|
+
* Registered guards missing on disk come back as 'missing'.
|
|
205
|
+
* @param {string} toolsDir - The project's tools/ directory
|
|
206
|
+
* @returns {Array<{ file: string, label: string, mode: 'module'|'process'|'missing' }>}
|
|
207
|
+
*/
|
|
208
|
+
function planGuards(toolsDir) {
|
|
209
|
+
const exts = new Set([OWN_EXT]);
|
|
210
|
+
if (!fs.existsSync(path.join(toolsDir, SIBLING.orchestrator))) exts.add(SIBLING.ext);
|
|
211
|
+
const plan = [];
|
|
212
|
+
const top = regularFiles(toolsDir).filter(f => isGuardFile(f) && exts.has(path.extname(f)));
|
|
213
|
+
for (const name of top) {
|
|
214
|
+
const file = path.join(toolsDir, name);
|
|
215
|
+
const module = path.extname(name) === OWN_EXT && exportsModule(file);
|
|
216
|
+
plan.push({ file, label: name, mode: module ? 'module' : 'process' });
|
|
217
|
+
}
|
|
218
|
+
for (const file of discoverProjectGuards(toolsDir).filter(f => exts.has(path.extname(f)))) {
|
|
219
|
+
plan.push({ file, label: `guards/${path.basename(file)}`, mode: 'process' });
|
|
220
|
+
}
|
|
221
|
+
const present = new Set(top);
|
|
222
|
+
for (const rel of registeredPaths(toolsDir)) {
|
|
223
|
+
const name = rel.startsWith('tools/') ? rel.slice('tools/'.length) : null;
|
|
224
|
+
if (!name || name.includes('/') || !isGuardFile(name) || !exts.has(path.extname(name)) || present.has(name)) continue;
|
|
225
|
+
plan.push({ file: path.join(toolsDir, name), label: name, mode: 'missing' });
|
|
226
|
+
}
|
|
227
|
+
return plan;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Result for a registered guard that is not on disk (fail closed). */
|
|
231
|
+
function missingGuardResult(entry) {
|
|
232
|
+
const name = path.basename(entry.label, path.extname(entry.label));
|
|
233
|
+
return {
|
|
234
|
+
exitCode: 1, skipped: false, crashed: true, name, lens: getLensForGuard(name),
|
|
235
|
+
data: { message: `Kernel guard missing: tools/${entry.label} is listed in tools/${REGISTRY_FILE} but not on disk; restore it with \`clckernel update\`` },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The guard's JSON result when its stdout (or its last line) is a JSON object. */
|
|
240
|
+
function parseGuardJson(stdout) {
|
|
241
|
+
const text = String(stdout || '').trim();
|
|
242
|
+
const lines = text.split('\n');
|
|
243
|
+
for (const candidate of [text, lines[lines.length - 1]]) {
|
|
244
|
+
try {
|
|
245
|
+
const value = JSON.parse(candidate);
|
|
246
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Last non-empty line of a process stream. */
|
|
253
|
+
function lastLine(text) {
|
|
254
|
+
const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean);
|
|
255
|
+
return lines.length > 0 ? lines[lines.length - 1] : '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Runs one guard as a process with cwd = targetDir, the way a project guard
|
|
260
|
+
* runs. The exit code decides (non-zero fails); a JSON result with the
|
|
261
|
+
* standard shape ({ name, skipped, lens?, data: { message, violations } }) is
|
|
262
|
+
* used for the report, plain output is reported by its last line. A .py guard
|
|
263
|
+
* that defines check() is called through that contract; when it exits non-zero
|
|
264
|
+
* without a JSON result it crashed. Lens: the JSON's, else the guard name's,
|
|
265
|
+
* else architecture; level: the JSON's `level`.
|
|
266
|
+
* @param {string} guardPath - Absolute path to the guard
|
|
267
|
+
* @param {string} targetDir - Project root
|
|
268
|
+
* @returns {object} Normalized result (`output`: the guard's raw output)
|
|
269
|
+
*/
|
|
270
|
+
function runProjectGuard(guardPath, targetDir) {
|
|
271
|
+
const ext = path.extname(guardPath);
|
|
272
|
+
const fallbackName = path.basename(guardPath, ext);
|
|
273
|
+
const moduleContract = ext === '.py' && definesCheck(guardPath);
|
|
274
|
+
const args = moduleContract ? ['-c', PY_MODULE_RUNNER, guardPath] : [guardPath];
|
|
275
|
+
const res = spawnSync(PROJECT_GUARD_RUNNERS[ext], args, { cwd: targetDir, encoding: 'utf-8', maxBuffer: MAX_BUFFER });
|
|
276
|
+
const fallbackLens = getLensForGuard(fallbackName);
|
|
277
|
+
if (res.error) {
|
|
278
|
+
return {
|
|
279
|
+
exitCode: 1, skipped: false, crashed: true, name: fallbackName, lens: fallbackLens,
|
|
280
|
+
data: { message: `Guard crashed: ${res.error.message}` },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const output = `${res.stdout || ''}${res.stderr || ''}`;
|
|
284
|
+
if (typeof res.status !== 'number') {
|
|
285
|
+
return {
|
|
286
|
+
exitCode: 1, skipped: false, crashed: true, name: fallbackName, lens: fallbackLens,
|
|
287
|
+
data: { message: `Guard crashed: killed by ${res.signal || 'a signal'}` },
|
|
288
|
+
output,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const exitCode = res.status;
|
|
292
|
+
const json = parseGuardJson(res.stdout);
|
|
293
|
+
if (json) {
|
|
294
|
+
const data = json.data && typeof json.data === 'object' ? json.data : json;
|
|
295
|
+
return {
|
|
296
|
+
exitCode,
|
|
297
|
+
skipped: exitCode === 0 && Boolean(json.skipped),
|
|
298
|
+
crashed: false,
|
|
299
|
+
level: json.level,
|
|
300
|
+
name: typeof json.name === 'string' && json.name ? json.name : fallbackName,
|
|
301
|
+
lens: LENSES.includes(json.lens) ? json.lens : fallbackLens,
|
|
302
|
+
data,
|
|
303
|
+
output,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const line = exitCode === 0 ? lastLine(res.stdout) : (lastLine(res.stderr) || lastLine(res.stdout));
|
|
307
|
+
const crashed = moduleContract && exitCode !== 0;
|
|
308
|
+
return {
|
|
309
|
+
exitCode, skipped: false, crashed, name: fallbackName, lens: fallbackLens,
|
|
310
|
+
data: { message: crashed ? `Guard crashed: ${line || `exited with code ${exitCode}`}` : (line || (exitCode === 0 ? 'Passed' : `Exited with code ${exitCode}`)) },
|
|
311
|
+
output,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
97
315
|
/**
|
|
98
316
|
* Run a single guard module with try/catch.
|
|
99
317
|
* @param {string} guardPath - Absolute path to the guard script
|
|
100
318
|
* @param {string} targetDir - Directory to scan
|
|
101
|
-
* @returns {object} Result from the guard, or a
|
|
319
|
+
* @returns {object} Result from the guard, or a FAILED result if it cannot run
|
|
102
320
|
*/
|
|
103
321
|
async function runGuard(guardPath, targetDir) {
|
|
104
322
|
const guardName = path.basename(guardPath, '.js');
|
|
@@ -108,36 +326,110 @@ async function runGuard(guardPath, targetDir) {
|
|
|
108
326
|
const fn = typeof mod === 'function' ? mod : Object.values(mod)[0];
|
|
109
327
|
if (typeof fn !== 'function') {
|
|
110
328
|
return {
|
|
111
|
-
exitCode:
|
|
112
|
-
data: { message: `Guard ${guardName} does not export a function` },
|
|
329
|
+
exitCode: 1, skipped: false, crashed: true, name: guardName, lens,
|
|
330
|
+
data: { message: `Guard crashed: ${guardName} does not export a function` },
|
|
113
331
|
};
|
|
114
332
|
}
|
|
115
333
|
const result = await fn(targetDir);
|
|
334
|
+
if (!result || typeof result !== 'object') {
|
|
335
|
+
return {
|
|
336
|
+
exitCode: 1, skipped: false, crashed: true, name: guardName, lens,
|
|
337
|
+
data: { message: `Guard crashed: ${guardName} returned ${result === null ? 'null' : typeof result}` },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
116
340
|
// Normalize result shape
|
|
117
341
|
return {
|
|
118
342
|
exitCode: result.exitCode || 0,
|
|
119
343
|
skipped: Boolean(result.skipped),
|
|
344
|
+
crashed: false,
|
|
345
|
+
level: result.level,
|
|
120
346
|
name: result.name || guardName,
|
|
121
347
|
lens,
|
|
122
348
|
data: result.data || result,
|
|
123
349
|
};
|
|
124
350
|
} catch (e) {
|
|
351
|
+
// A guard that cannot run must never approve a commit (fail closed).
|
|
125
352
|
return {
|
|
126
|
-
exitCode:
|
|
127
|
-
data: { message: `Guard
|
|
353
|
+
exitCode: 1, skipped: false, crashed: true, name: guardName, lens,
|
|
354
|
+
data: { message: `Guard crashed: ${e.message}` },
|
|
128
355
|
};
|
|
129
356
|
}
|
|
130
357
|
}
|
|
131
358
|
|
|
132
359
|
// ── Report Generation ────────────────────────────────────────────────────────
|
|
133
360
|
|
|
361
|
+
/** One markdown bullet per violation (or finding) of a failed guard. */
|
|
362
|
+
function violationLines(result) {
|
|
363
|
+
const lines = [];
|
|
364
|
+
const violations = result.data?.violations || result.data?.findings || {};
|
|
365
|
+
if (Array.isArray(violations)) {
|
|
366
|
+
for (const item of violations) {
|
|
367
|
+
lines.push(`- ${item.file ? `\`${item.file}\`: ` : ''}${item.message || item.reason || JSON.stringify(item)}`);
|
|
368
|
+
}
|
|
369
|
+
return lines;
|
|
370
|
+
}
|
|
371
|
+
if (!violations || typeof violations !== 'object') return lines;
|
|
372
|
+
for (const file of Object.keys(violations)) {
|
|
373
|
+
const items = Array.isArray(violations[file]) ? violations[file] : [];
|
|
374
|
+
for (const item of items) {
|
|
375
|
+
const loc = item.lineNo ? ` (line ${item.lineNo})` : '';
|
|
376
|
+
const label = item.label || item.rule || item.module || '';
|
|
377
|
+
const msg = item.message || item.reason || '';
|
|
378
|
+
lines.push(`- \`${file}\`${loc}: ${label ? `[${label}] ` : ''}${msg}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return lines;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Result levels (sdds/audit-cli) ───────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/** A guard that ran and did not pass (skipped guards never fail). */
|
|
387
|
+
const isFailed = (r) => !r.skipped && r.exitCode !== 0;
|
|
388
|
+
/** A failure reported but not blocking: effective level 'warning'. */
|
|
389
|
+
const isWarning = (r) => isFailed(r) && r.level === 'warning';
|
|
390
|
+
/** A failure that fails the run. */
|
|
391
|
+
const isBlocking = (r) => isFailed(r) && r.level !== 'warning';
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Sets each result's effective level: severities[name] (or [file stem]) from
|
|
395
|
+
* .clckernel.yaml, else the guard's own `level` ('warning' only), else
|
|
396
|
+
* 'error'. A crash is always 'error' (fail closed).
|
|
397
|
+
* @param {object[]} results - Normalized results (with `stem`)
|
|
398
|
+
* @param {Record<string, 'error'|'warning'>} severities
|
|
399
|
+
* @returns {object[]} The same results
|
|
400
|
+
*/
|
|
401
|
+
function applyLevels(results, severities = {}) {
|
|
402
|
+
const has = (key) => typeof key === 'string' && Object.prototype.hasOwnProperty.call(severities, key);
|
|
403
|
+
for (const result of results) {
|
|
404
|
+
let level = result.level === 'warning' ? 'warning' : 'error';
|
|
405
|
+
if (has(result.name)) level = severities[result.name];
|
|
406
|
+
else if (has(result.stem)) level = severities[result.stem];
|
|
407
|
+
result.level = result.crashed ? 'error' : level;
|
|
408
|
+
}
|
|
409
|
+
return results;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** [icon, STATUS] of a result. */
|
|
413
|
+
function statusOf(result) {
|
|
414
|
+
if (result.skipped) return ['⏭️', 'SKIPPED'];
|
|
415
|
+
if (!isFailed(result)) return ['✅', 'PASSED'];
|
|
416
|
+
return isWarning(result) ? ['⚠️', 'WARNING'] : ['❌', 'FAILED'];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Lens/verdict status: FAILED on a blocking failure, PASSED_WITH_WARNINGS on warnings only. */
|
|
420
|
+
function verdictOf(results) {
|
|
421
|
+
if (results.some(isBlocking)) return 'FAILED';
|
|
422
|
+
return results.some(isWarning) ? 'PASSED_WITH_WARNINGS' : 'PASSED';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Report Generation ────────────────────────────────────────────────────────
|
|
426
|
+
|
|
134
427
|
/**
|
|
135
428
|
* Format a single guard result into a markdown section.
|
|
136
429
|
*/
|
|
137
430
|
function formatGuardSection(name, result) {
|
|
138
431
|
const lines = [];
|
|
139
|
-
const icon = result
|
|
140
|
-
const status = result.skipped ? 'SKIPPED' : result.exitCode === 0 ? 'PASSED' : 'FAILED';
|
|
432
|
+
const [icon, status] = statusOf(result);
|
|
141
433
|
const lensBadge = (result.lens || 'architecture').toUpperCase();
|
|
142
434
|
|
|
143
435
|
lines.push(`### ${icon} ${name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())} [${lensBadge}] — ${status}`);
|
|
@@ -150,25 +442,7 @@ function formatGuardSection(name, result) {
|
|
|
150
442
|
} else {
|
|
151
443
|
lines.push(`> ${result.data?.message || 'Issues found'}`);
|
|
152
444
|
lines.push('');
|
|
153
|
-
|
|
154
|
-
if (Array.isArray(violations)) {
|
|
155
|
-
for (const item of violations) {
|
|
156
|
-
lines.push(`- ${item.file ? `\`${item.file}\`: ` : ''}${item.message || item.reason || JSON.stringify(item)}`);
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
const files = Object.keys(violations);
|
|
160
|
-
if (files.length > 0) {
|
|
161
|
-
for (const file of files) {
|
|
162
|
-
const items = violations[file];
|
|
163
|
-
for (const item of items) {
|
|
164
|
-
const loc = item.lineNo ? ` (line ${item.lineNo})` : '';
|
|
165
|
-
const label = item.label || item.rule || item.module || '';
|
|
166
|
-
const msg = item.message || item.reason || '';
|
|
167
|
-
lines.push(`- \`${file}\`${loc}: ${label ? `[${label}] ` : ''}${msg}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
445
|
+
lines.push(...violationLines(result));
|
|
172
446
|
}
|
|
173
447
|
|
|
174
448
|
return lines.join('\n');
|
|
@@ -180,7 +454,8 @@ function formatGuardSection(name, result) {
|
|
|
180
454
|
function generateReport(results, sddDir) {
|
|
181
455
|
const taskName = path.basename(sddDir);
|
|
182
456
|
const passed = results.filter(r => !r.skipped && r.exitCode === 0).length;
|
|
183
|
-
const failed = results.filter(
|
|
457
|
+
const failed = results.filter(isBlocking).length;
|
|
458
|
+
const warnings = results.filter(isWarning);
|
|
184
459
|
const skipped = results.filter(r => r.skipped).length;
|
|
185
460
|
const total = results.length;
|
|
186
461
|
|
|
@@ -189,7 +464,9 @@ function generateReport(results, sddDir) {
|
|
|
189
464
|
sections.push(`# Code Audit Report: ${taskName}`);
|
|
190
465
|
sections.push('');
|
|
191
466
|
sections.push('> **Review-Driven Development (RDD) Report:**');
|
|
192
|
-
sections.push('>
|
|
467
|
+
sections.push('> Results grouped into 4 lenses (Correctness, Security, Architecture, Performance).');
|
|
468
|
+
sections.push('');
|
|
469
|
+
sections.push(`**Verdict:** ${verdictOf(results)}`);
|
|
193
470
|
sections.push('');
|
|
194
471
|
sections.push('---');
|
|
195
472
|
sections.push('');
|
|
@@ -200,12 +477,23 @@ function generateReport(results, sddDir) {
|
|
|
200
477
|
sections.push(`| Total guards | ${total} |`);
|
|
201
478
|
sections.push(`| Passed | ${passed} |`);
|
|
202
479
|
sections.push(`| Failed | ${failed} |`);
|
|
480
|
+
sections.push(`| Warnings | ${warnings.length} |`);
|
|
203
481
|
sections.push(`| Skipped | ${skipped} |`);
|
|
204
482
|
sections.push('');
|
|
205
483
|
sections.push('---');
|
|
206
484
|
sections.push('');
|
|
207
485
|
|
|
208
|
-
|
|
486
|
+
if (warnings.length > 0) {
|
|
487
|
+
sections.push('## ⚠️ Warnings');
|
|
488
|
+
sections.push('');
|
|
489
|
+
sections.push('_Failed, but not blocking (level `warning`)._');
|
|
490
|
+
sections.push('');
|
|
491
|
+
for (const result of warnings) sections.push(`- **${result.name}** [${result.lens.toUpperCase()}]: ${result.data?.message || 'Issues found'}`);
|
|
492
|
+
sections.push('');
|
|
493
|
+
sections.push('---');
|
|
494
|
+
sections.push('');
|
|
495
|
+
}
|
|
496
|
+
|
|
209
497
|
const lensHeaders = {
|
|
210
498
|
correctness: '## 🎯 Correctness Lens',
|
|
211
499
|
security: '## 🛡️ Security Lens',
|
|
@@ -213,7 +501,7 @@ function generateReport(results, sddDir) {
|
|
|
213
501
|
performance: '## ⚡ Performance Lens',
|
|
214
502
|
};
|
|
215
503
|
|
|
216
|
-
for (const lens of
|
|
504
|
+
for (const lens of LENSES) {
|
|
217
505
|
sections.push(lensHeaders[lens]);
|
|
218
506
|
sections.push('');
|
|
219
507
|
const lensResults = results.filter(r => r.lens === lens);
|
|
@@ -234,24 +522,20 @@ function generateReport(results, sddDir) {
|
|
|
234
522
|
|
|
235
523
|
/**
|
|
236
524
|
* Generate an immutable machine-readable audit receipt (audit-receipt.json).
|
|
525
|
+
* verdict: FAILED | PASSED_WITH_WARNINGS | PASSED; `failed` counts blocking
|
|
526
|
+
* failures, `warnings` the non-blocking ones (listed in `warnings`).
|
|
237
527
|
*/
|
|
238
528
|
function generateReceipt(results, targetDir) {
|
|
239
|
-
const lenses = ['correctness', 'security', 'architecture', 'performance'];
|
|
240
529
|
const lensSummary = {};
|
|
241
530
|
|
|
242
|
-
|
|
243
|
-
for (const lens of lenses) {
|
|
531
|
+
for (const lens of LENSES) {
|
|
244
532
|
const lensResults = results.filter(r => r.lens === lens);
|
|
245
|
-
const passed = lensResults.filter(r => !r.skipped && r.exitCode === 0).length;
|
|
246
|
-
const failed = lensResults.filter(r => !r.skipped && r.exitCode !== 0).length;
|
|
247
|
-
const skipped = lensResults.filter(r => r.skipped).length;
|
|
248
|
-
if (failed > 0) hasFailure = true;
|
|
249
|
-
|
|
250
533
|
lensSummary[lens] = {
|
|
251
|
-
status:
|
|
252
|
-
passed,
|
|
253
|
-
failed,
|
|
254
|
-
|
|
534
|
+
status: verdictOf(lensResults),
|
|
535
|
+
passed: lensResults.filter(r => !r.skipped && r.exitCode === 0).length,
|
|
536
|
+
failed: lensResults.filter(isBlocking).length,
|
|
537
|
+
warnings: lensResults.filter(isWarning).length,
|
|
538
|
+
skipped: lensResults.filter(r => r.skipped).length,
|
|
255
539
|
};
|
|
256
540
|
}
|
|
257
541
|
|
|
@@ -273,8 +557,9 @@ function generateReceipt(results, targetDir) {
|
|
|
273
557
|
contract: 'clckernel.audit-receipt/v1',
|
|
274
558
|
timestamp: new Date().toISOString(),
|
|
275
559
|
commitHash,
|
|
276
|
-
verdict:
|
|
560
|
+
verdict: verdictOf(results),
|
|
277
561
|
lenses: lensSummary,
|
|
562
|
+
warnings: results.filter(isWarning).map(r => ({ name: r.name, lens: r.lens, message: r.data?.message || '' })),
|
|
278
563
|
generator: 'clckernel',
|
|
279
564
|
gentle_ai: {
|
|
280
565
|
compatible: true,
|
|
@@ -286,12 +571,15 @@ function generateReceipt(results, targetDir) {
|
|
|
286
571
|
|
|
287
572
|
// ── Console Summary ──────────────────────────────────────────────────────────
|
|
288
573
|
|
|
574
|
+
/**
|
|
575
|
+
* The 4-lens summary. reportPath/receiptPath: where they were written, or
|
|
576
|
+
* null when no --change was given (then a hint says how to write them).
|
|
577
|
+
*/
|
|
289
578
|
function printSummary(results, reportPath, receiptPath) {
|
|
290
579
|
console.log('\n=================================================================');
|
|
291
580
|
console.log(' CLC KERNEL — 4-LENS RDD AUDIT SUMMARY');
|
|
292
581
|
console.log('=================================================================\n');
|
|
293
582
|
|
|
294
|
-
const lenses = ['correctness', 'security', 'architecture', 'performance'];
|
|
295
583
|
const lensLabels = {
|
|
296
584
|
correctness: '🎯 CORRECTNESS LENS',
|
|
297
585
|
security: '🛡️ SECURITY LENS',
|
|
@@ -299,14 +587,14 @@ function printSummary(results, reportPath, receiptPath) {
|
|
|
299
587
|
performance: '⚡ PERFORMANCE LENS',
|
|
300
588
|
};
|
|
301
589
|
|
|
302
|
-
for (const lens of
|
|
590
|
+
for (const lens of LENSES) {
|
|
303
591
|
const lensResults = results.filter(r => r.lens === lens);
|
|
304
592
|
console.log(` ${lensLabels[lens]}:`);
|
|
305
593
|
if (lensResults.length === 0) {
|
|
306
594
|
console.log(' — No guards active for this lens');
|
|
307
595
|
} else {
|
|
308
596
|
for (const result of lensResults) {
|
|
309
|
-
const icon = result
|
|
597
|
+
const [icon] = statusOf(result);
|
|
310
598
|
const label = result.name.replace(/_/g, ' ');
|
|
311
599
|
const msg = result.data?.message || (result.exitCode === 0 ? 'Passed' : 'Failed');
|
|
312
600
|
console.log(` ${icon} ${label}: ${msg}`);
|
|
@@ -315,64 +603,176 @@ function printSummary(results, reportPath, receiptPath) {
|
|
|
315
603
|
console.log('');
|
|
316
604
|
}
|
|
317
605
|
|
|
318
|
-
console.log(
|
|
319
|
-
if (
|
|
320
|
-
console.log(
|
|
606
|
+
console.log(`Verdict: ${verdictOf(results)}`);
|
|
607
|
+
if (reportPath) {
|
|
608
|
+
console.log(`📄 Full report: ${reportPath}`);
|
|
609
|
+
if (receiptPath) console.log(`🧾 Audit receipt: ${receiptPath}`);
|
|
610
|
+
} else {
|
|
611
|
+
console.log('No report written: pass --change <name> to write sdds/<name>/05-audit-report.md and audit-receipt.json.');
|
|
321
612
|
}
|
|
322
613
|
console.log('=================================================================\n');
|
|
323
614
|
}
|
|
324
615
|
|
|
325
616
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
326
617
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
618
|
+
/** Runs every planned guard once, in plan order; each result keeps its file stem. */
|
|
619
|
+
async function runPlan(plan, dir) {
|
|
620
|
+
const results = [];
|
|
621
|
+
for (const entry of plan) {
|
|
622
|
+
let result;
|
|
623
|
+
if (entry.mode === 'missing') result = missingGuardResult(entry);
|
|
624
|
+
else if (entry.mode === 'module') result = await runGuard(entry.file, dir);
|
|
625
|
+
else result = runProjectGuard(entry.file, dir);
|
|
626
|
+
result.stem = path.basename(entry.file, path.extname(entry.file));
|
|
627
|
+
results.push(result);
|
|
628
|
+
}
|
|
629
|
+
return results;
|
|
630
|
+
}
|
|
330
631
|
|
|
331
|
-
|
|
632
|
+
/**
|
|
633
|
+
* --hook output: one line per guard (ok, skip, WARN, FAIL); a failed guard
|
|
634
|
+
* also prints its violations, or its raw output when it reported none.
|
|
635
|
+
*/
|
|
636
|
+
function printHookSummary(results) {
|
|
637
|
+
for (const result of results) {
|
|
638
|
+
const status = result.skipped ? 'skip' : !isFailed(result) ? 'ok ' : isWarning(result) ? 'WARN' : 'FAIL';
|
|
639
|
+
console.log(`clckernel: ${status} ${result.name}: ${result.data?.message || ''}`);
|
|
640
|
+
if (!isFailed(result)) continue;
|
|
641
|
+
const details = violationLines(result);
|
|
642
|
+
const lines = details.length > 0 ? details : String(result.output || '').trimEnd().split('\n').filter(Boolean);
|
|
643
|
+
for (const line of lines) console.log(` ${line}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
332
646
|
|
|
647
|
+
/**
|
|
648
|
+
* `severities` from the project's .clckernel.yaml, read with the sibling
|
|
649
|
+
* _config.js; {} when that module is not installed. An invalid config throws.
|
|
650
|
+
*/
|
|
651
|
+
function loadSeverities(dir) {
|
|
652
|
+
let configModule;
|
|
653
|
+
try {
|
|
654
|
+
configModule = require(path.join(__dirname, '_config.js'));
|
|
655
|
+
} catch (err) {
|
|
656
|
+
if (err && err.code === 'MODULE_NOT_FOUND') return {};
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
return configModule.loadConfig(dir).config.severities || {};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Report and receipt file names. With both orchestrators installed side by
|
|
664
|
+
* side (a mixed stack), each runs its own language's guards and writes its own
|
|
665
|
+
* pair (05-audit-report.js.md, audit-receipt.js.json), so the second run of
|
|
666
|
+
* `clckernel audit --change` does not overwrite the first. A single
|
|
667
|
+
* orchestrator keeps the plain names.
|
|
668
|
+
* @param {string} toolsDir - Directory holding the orchestrators
|
|
669
|
+
* @returns {{ report: string, receipt: string }}
|
|
670
|
+
*/
|
|
671
|
+
function reportNames(toolsDir) {
|
|
672
|
+
if (fs.existsSync(path.join(toolsDir, 'audit.py'))) {
|
|
673
|
+
return { report: '05-audit-report.js.md', receipt: 'audit-receipt.js.json' };
|
|
674
|
+
}
|
|
675
|
+
return { report: '05-audit-report.md', receipt: 'audit-receipt.json' };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* @param {string} [targetDir] - Project root (default: cwd)
|
|
680
|
+
* @param {{ hook?: boolean, change?: string|null }} [options] - hook: one line
|
|
681
|
+
* per guard; change: write the report and receipt to sdds/<change>/
|
|
682
|
+
* @returns {Promise<number>} 1 when a guard failed with level error, else 0
|
|
683
|
+
*/
|
|
684
|
+
async function main(targetDir, options = {}) {
|
|
685
|
+
const hook = Boolean(options.hook);
|
|
686
|
+
const change = options.change || null;
|
|
687
|
+
const dir = targetDir ? path.resolve(targetDir) : process.cwd();
|
|
333
688
|
const targetTools = path.join(dir, 'tools');
|
|
334
689
|
const toolsDir = fs.existsSync(targetTools) ? targetTools : __dirname;
|
|
335
|
-
const
|
|
690
|
+
const plan = planGuards(toolsDir);
|
|
336
691
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
692
|
+
let severities;
|
|
693
|
+
try {
|
|
694
|
+
severities = loadSeverities(dir);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
console.error(`clckernel: ${err.message}`);
|
|
697
|
+
return 1;
|
|
340
698
|
}
|
|
341
699
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const result = await runGuard(guardPath, dir);
|
|
347
|
-
results.push(result);
|
|
700
|
+
if (hook) {
|
|
701
|
+
const results = applyLevels(await runPlan(plan, dir), severities);
|
|
702
|
+
printHookSummary(results);
|
|
703
|
+
return results.some(isBlocking) ? 1 : 0;
|
|
348
704
|
}
|
|
349
705
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
706
|
+
console.log('\nRunning CLC Kernel audit...\n');
|
|
707
|
+
|
|
708
|
+
if (plan.length === 0) {
|
|
709
|
+
console.log('No guard tools found in tools/. Nothing to audit.');
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
354
712
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
713
|
+
const names = plan.map(entry => entry.label.replace(/\.js$/, ''));
|
|
714
|
+
console.log(`Discovered ${names.length} guard(s): ${names.join(', ')}\n`);
|
|
715
|
+
|
|
716
|
+
const results = applyLevels(await runPlan(plan, dir), severities);
|
|
717
|
+
|
|
718
|
+
let reportPath = null;
|
|
719
|
+
let receiptPath = null;
|
|
720
|
+
if (change) {
|
|
721
|
+
const sddDir = path.join(dir, 'sdds', change);
|
|
722
|
+
fs.mkdirSync(sddDir, { recursive: true });
|
|
723
|
+
const names = reportNames(toolsDir);
|
|
724
|
+
reportPath = path.join(sddDir, names.report);
|
|
725
|
+
fs.writeFileSync(reportPath, generateReport(results, sddDir), 'utf-8');
|
|
726
|
+
receiptPath = path.join(sddDir, names.receipt);
|
|
727
|
+
fs.writeFileSync(receiptPath, JSON.stringify(generateReceipt(results, dir), null, 2) + '\n', 'utf-8');
|
|
728
|
+
}
|
|
359
729
|
|
|
360
|
-
// Print summary
|
|
361
730
|
printSummary(results, reportPath, receiptPath);
|
|
362
731
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
732
|
+
return results.some(isBlocking) ? 1 : 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* argv: [targetDir] [--hook] [--change <name>] [--staged], in any order.
|
|
737
|
+
* @returns {{ targetDir?: string, hook: boolean, change: string|null, staged: boolean } | { error: string }}
|
|
738
|
+
*/
|
|
739
|
+
function parseArgs(argv) {
|
|
740
|
+
const out = { targetDir: undefined, hook: false, change: null, staged: false };
|
|
741
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
742
|
+
const arg = argv[i];
|
|
743
|
+
if (arg === '--hook') out.hook = true;
|
|
744
|
+
else if (arg === '--staged') out.staged = true;
|
|
745
|
+
else if (arg === '--change') {
|
|
746
|
+
if (i + 1 >= argv.length) return { error: '--change needs a name' };
|
|
747
|
+
const name = argv[i += 1];
|
|
748
|
+
if (!CHANGE_NAME.test(name)) return { error: `invalid --change name ${JSON.stringify(name)}: use one path segment (letters, digits, ., _, -)` };
|
|
749
|
+
out.change = name;
|
|
750
|
+
} else if (arg.startsWith('-')) return { error: `unknown option ${arg}` };
|
|
751
|
+
else if (out.targetDir !== undefined) return { error: `unexpected argument ${arg}` };
|
|
752
|
+
else out.targetDir = arg;
|
|
367
753
|
}
|
|
754
|
+
if (out.hook && out.change) return { error: '--hook writes nothing; it cannot be combined with --change' };
|
|
755
|
+
return out;
|
|
368
756
|
}
|
|
369
757
|
|
|
370
|
-
// Support both require() and direct execution
|
|
758
|
+
// Support both require() and direct execution. Any crash fails closed.
|
|
371
759
|
if (require.main === module) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
760
|
+
const args = parseArgs(process.argv.slice(2));
|
|
761
|
+
if (args.error) {
|
|
762
|
+
console.error(`audit.js: ${args.error}\n${USAGE}`);
|
|
763
|
+
process.exitCode = 2;
|
|
764
|
+
} else {
|
|
765
|
+
main(args.targetDir, { hook: args.hook, change: args.change }).then(code => {
|
|
766
|
+
// exitCode, not exit(): a piped stdout is flushed before the process ends.
|
|
767
|
+
process.exitCode = code;
|
|
768
|
+
}, err => {
|
|
769
|
+
console.error(`Audit orchestrator error: ${err && err.message}`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
376
773
|
}
|
|
377
774
|
|
|
378
|
-
module.exports = {
|
|
775
|
+
module.exports = {
|
|
776
|
+
main, parseArgs, reportNames, applyLevels, discoverGuards, discoverProjectGuards, planGuards, isGuardFile,
|
|
777
|
+
runGuard, runProjectGuard, generateReceipt,
|
|
778
|
+
};
|