clckernel 1.2.6 → 1.2.9

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.
Files changed (47) hide show
  1. package/README.es.md +103 -110
  2. package/README.md +100 -108
  3. package/package.json +14 -2
  4. package/src/adapters/agent-os.js +206 -0
  5. package/src/catalog.js +16 -0
  6. package/src/detector.js +2 -0
  7. package/src/generator.js +39 -62
  8. package/src/index.js +11 -0
  9. package/src/ui.js +79 -35
  10. package/tools/audit.js +0 -0
  11. package/tools/audit.py +0 -0
  12. package/tools/check_custom.js +0 -0
  13. package/tools/check_custom.py +0 -0
  14. package/tools/check_db_efficiency.py +0 -0
  15. package/tools/check_domain_data.js +100 -0
  16. package/tools/check_hitl.js +93 -0
  17. package/tools/check_responsive.js +0 -0
  18. package/tools/check_seo.js +1 -1
  19. package/tools/check_symlinks.js +76 -0
  20. package/tools/check_test_calendar_sync.js +111 -0
  21. package/tools/scan_secrets.js +12 -1
  22. package/tools/scan_secrets.py +0 -0
  23. package/AGENTS.md +0 -124
  24. package/docs/Journal/001-adr-clckernel-governance.md +0 -18
  25. package/test/adapters/astro.test.js +0 -102
  26. package/test/adapters/base.test.js +0 -86
  27. package/test/adapters/django.test.js +0 -95
  28. package/test/adapters/fastapi.test.js +0 -88
  29. package/test/adapters/go.test.js +0 -81
  30. package/test/adapters/laravel.test.js +0 -106
  31. package/test/adapters/nextjs.test.js +0 -113
  32. package/test/adapters/rails.test.js +0 -108
  33. package/test/adapters/rust.test.js +0 -83
  34. package/test/catalog.test.js +0 -170
  35. package/test/config.test.js +0 -512
  36. package/test/detector.test.js +0 -345
  37. package/test/doctor.test.js +0 -302
  38. package/test/generator.test.js +0 -419
  39. package/test/guards_new.test.js +0 -95
  40. package/test/helpers.js +0 -36
  41. package/test/integration/cli-flow.test.js +0 -270
  42. package/test/technologies/celery_guard.test.js +0 -185
  43. package/test/technologies/detector.test.js +0 -342
  44. package/test/technologies/docker_guard.test.js +0 -187
  45. package/test/technologies/integration.test.js +0 -123
  46. package/test/technologies/postgres_guard.test.js +0 -87
  47. package/test/technologies/redis_guard.test.js +0 -128
@@ -1,170 +0,0 @@
1
- /**
2
- * CLC Forge — Catalog tests for the central safeguard registry (src/catalog.js).
3
- * Covers: 24-entry completeness (cross-checked against design §1.2 and the
4
- * actual tools/ directory), full shape validation, guard-file correlation,
5
- * uniqueness, and the applyTo/severityFor/gateFor lookup helpers.
6
- */
7
-
8
- const { describe, it } = require('node:test');
9
- const assert = require('node:assert');
10
- const fs = require('fs');
11
- const path = require('path');
12
-
13
- const {
14
- SAFEGUARD_CATALOG,
15
- VALID_SEVERITIES,
16
- VALID_GATE_MODES,
17
- VALID_PHASES,
18
- VALID_APPLIES_WHEN,
19
- applyTo,
20
- severityFor,
21
- gateFor,
22
- } = require('../src/catalog.js');
23
-
24
- const TOOLS_DIR = path.join(__dirname, '..', 'tools');
25
-
26
- function guardFileExists(file) {
27
- if (!file) return false;
28
- return file.split('+').every(variant => fs.existsSync(path.join(TOOLS_DIR, variant)));
29
- }
30
-
31
- describe('SAFEGUARD_CATALOG', () => {
32
- it('exposes exactly 26 entries, one per safeguard the framework knows about', () => {
33
- assert.strictEqual(SAFEGUARD_CATALOG.length, 26, `expected 26, got ${SAFEGUARD_CATALOG.length}`);
34
- const names = SAFEGUARD_CATALOG.map(e => e.name);
35
- assert.deepStrictEqual(names, [
36
- 'scan_secrets', 'check_architecture', 'check_custom', 'check_a11y',
37
- 'check_ui_reuse', 'check_performance', 'check_responsive', 'check_seo', 'check_storybook', 'check_api_contracts',
38
- 'check_migrations', 'verify_tdd', 'check_scope', 'celery_guard', 'redis_guard',
39
- 'postgres_guard', 'docker_guard', 'ruff', 'pytest', 'tsc', 'lint', 'graphify',
40
- 'gentle_ai_review', 'check_openapi_drift', 'check_db_efficiency', 'check_observability',
41
- ]);
42
- });
43
-
44
- it('has unique names and no duplicate alias labels', () => {
45
- const names = SAFEGUARD_CATALOG.map(e => e.name);
46
- assert.strictEqual(new Set(names).size, names.length, 'names must be unique');
47
- const allLabels = SAFEGUARD_CATALOG.flatMap(e => e.labels);
48
- assert.strictEqual(new Set(allLabels).size, allLabels.length, 'labels must be globally unique');
49
- });
50
-
51
- it('validates every required field on every entry (shape loop)', () => {
52
- const required = ['name', 'labels', 'checks', 'verify', 'applies_when', 'severity', 'gate_mode', 'config_keys', 'phase', 'file', 'manifest', 'planned'];
53
- for (const entry of SAFEGUARD_CATALOG) {
54
- for (const key of required) {
55
- assert.ok(key in entry, `${entry.name} is missing required field "${key}"`);
56
- }
57
- assert.ok(typeof entry.name === 'string' && entry.name.length > 0, `${entry.name}: name`);
58
- assert.ok(Array.isArray(entry.labels) && entry.labels.length > 0, `${entry.name}: labels`);
59
- assert.ok(entry.labels.includes(entry.name), `${entry.name}: labels must include the name`);
60
- assert.ok(typeof entry.checks === 'string' && entry.checks.length > 0, `${entry.name}: checks`);
61
- assert.ok(Array.isArray(entry.verify) && entry.verify.length > 0, `${entry.name}: verify`);
62
- assert.ok(VALID_APPLIES_WHEN.includes(entry.applies_when), `${entry.name}: applies_when "${entry.applies_when}"`);
63
- assert.ok(VALID_SEVERITIES.includes(entry.severity), `${entry.name}: severity "${entry.severity}"`);
64
- assert.ok(VALID_GATE_MODES.includes(entry.gate_mode), `${entry.name}: gate_mode "${entry.gate_mode}"`);
65
- assert.ok(Array.isArray(entry.config_keys) && entry.config_keys.every(k => typeof k === 'string'), `${entry.name}: config_keys`);
66
- assert.ok(VALID_PHASES.includes(entry.phase), `${entry.name}: phase "${entry.phase}"`);
67
- assert.ok(entry.file === null || typeof entry.file === 'string', `${entry.name}: file`);
68
- assert.strictEqual(typeof entry.manifest, 'boolean', `${entry.name}: manifest`);
69
- assert.strictEqual(typeof entry.planned, 'boolean', `${entry.name}: planned`);
70
- }
71
- });
72
-
73
- it('every verify token references a plausible verify_* audit goal', () => {
74
- for (const entry of SAFEGUARD_CATALOG) {
75
- for (const token of entry.verify) {
76
- assert.match(token, /^verify_[a-z0-9_]+$/, `${entry.name}: verify token "${token}"`);
77
- }
78
- }
79
- });
80
-
81
- it('every non-null file exists in bundled tools/ unless the entry is planned', () => {
82
- for (const entry of SAFEGUARD_CATALOG) {
83
- if (entry.file === null) continue;
84
- const exists = guardFileExists(entry.file);
85
- if (entry.planned) {
86
- assert.ok(!exists, `${entry.name}: planned file "${entry.file}" should NOT exist yet`);
87
- } else {
88
- assert.ok(exists, `${entry.name}: file "${entry.file}" missing from tools/`);
89
- }
90
- }
91
- });
92
-
93
- it('flags exactly the 2 Slice C guards as planned', () => {
94
- const planned = SAFEGUARD_CATALOG.filter(e => e.planned).map(e => e.name);
95
- assert.deepStrictEqual(planned, ['check_openapi_drift', 'check_observability']);
96
- });
97
-
98
- it('keeps design defaults for the new observability guard (warning/advisory)', () => {
99
- const observability = applyTo('check_observability');
100
- assert.strictEqual(observability.severity, 'warning');
101
- assert.strictEqual(observability.gate_mode, 'advisory');
102
- });
103
-
104
- it('maps stack-specific guards to their applicability predicates', () => {
105
- assert.strictEqual(applyTo('redis_guard').applies_when, 'redis');
106
- assert.strictEqual(applyTo('postgres_guard').applies_when, 'postgresql');
107
- assert.strictEqual(applyTo('docker_guard').applies_when, 'docker');
108
- assert.strictEqual(applyTo('celery_guard').applies_when, 'celery');
109
- assert.strictEqual(applyTo('check_openapi_drift').applies_when, 'fastapi');
110
- assert.strictEqual(applyTo('check_db_efficiency').applies_when, 'django|sqlalchemy');
111
- assert.strictEqual(applyTo('check_migrations').applies_when, 'backend,fastapi,django');
112
- });
113
-
114
- it('external CLI entries have file null and contribute no tool files', () => {
115
- for (const name of ['ruff', 'pytest', 'tsc', 'lint', 'graphify', 'gentle_ai_review']) {
116
- const entry = applyTo(name);
117
- assert.strictEqual(entry.file, null, `${name}: external row must have file null`);
118
- }
119
- });
120
- });
121
-
122
- describe('applyTo', () => {
123
- it('resolves the entry by exact name', () => {
124
- assert.strictEqual(applyTo('check_custom').name, 'check_custom');
125
- assert.strictEqual(applyTo('redis_guard').name, 'redis_guard');
126
- assert.strictEqual(applyTo('check_observability').name, 'check_observability');
127
- });
128
-
129
- it('resolves alias labels (js/py variants and CLI aliases)', () => {
130
- assert.strictEqual(applyTo('check_custom.js').name, 'check_custom');
131
- assert.strictEqual(applyTo('check_custom.py').name, 'check_custom');
132
- assert.strictEqual(applyTo('scan_secrets.py').name, 'scan_secrets');
133
- assert.strictEqual(applyTo('lint-staged').name, 'lint');
134
- assert.strictEqual(applyTo('gentle-ai').name, 'gentle_ai_review');
135
- });
136
-
137
- it('normalizes case and whitespace', () => {
138
- assert.strictEqual(applyTo(' Check_Custom ').name, 'check_custom');
139
- });
140
-
141
- it('returns null for unknown names (no throw)', () => {
142
- assert.strictEqual(applyTo('check_white'), null);
143
- assert.strictEqual(applyTo('check_black'), null);
144
- assert.strictEqual(applyTo('check_memory'), null);
145
- assert.strictEqual(applyTo('check_audit'), null);
146
- assert.strictEqual(applyTo('nonexistent_guard'), null);
147
- assert.strictEqual(applyTo(''), null);
148
- assert.strictEqual(applyTo(undefined), null);
149
- assert.strictEqual(applyTo(null), null);
150
- });
151
- });
152
-
153
- describe('severityFor / gateFor', () => {
154
- it('resolves catalog defaults for representative entries', () => {
155
- assert.strictEqual(severityFor('check_custom'), 'error');
156
- assert.strictEqual(severityFor('check_observability'), 'warning');
157
- assert.strictEqual(severityFor('redis_guard'), 'error');
158
- assert.strictEqual(severityFor('check_ui_reuse'), 'warning');
159
- assert.strictEqual(gateFor('check_custom'), 'hard');
160
- assert.strictEqual(gateFor('check_observability'), 'advisory');
161
- assert.strictEqual(gateFor('redis_guard'), 'hard');
162
- });
163
-
164
- it('returns null for unknown names (no throw)', () => {
165
- assert.strictEqual(severityFor('check_black'), null);
166
- assert.strictEqual(gateFor('check_black'), null);
167
- assert.strictEqual(severityFor(undefined), null);
168
- assert.strictEqual(gateFor(''), null);
169
- });
170
- });
@@ -1,512 +0,0 @@
1
- /**
2
- * CLC Forge — Config tests for the extracted zero-dependency YAML parser.
3
- * Covers: parser reuse (src/yaml.js), unsupported anchor/alias syntax,
4
- * existing custom-rules behavior via tools/check_custom.js subprocess,
5
- * and `../src/yaml.js` resolution from a generated-style layout.
6
- */
7
-
8
- const { describe, it } = require('node:test');
9
- const assert = require('node:assert');
10
- const { spawn, spawnSync } = require('child_process');
11
- const path = require('path');
12
- const fs = require('fs');
13
- const { createTmpProject, teardown } = require('./helpers');
14
-
15
- const { parseYaml } = require('../src/yaml.js');
16
-
17
- const {
18
- loadConfig,
19
- validateConfig,
20
- resolveConfig,
21
- ConfigError,
22
- HARNESS_PHASES,
23
- ENV_SEVERITIES,
24
- } = require('../src/config.js');
25
- const { generateHarness } = require('../src/generator.js');
26
-
27
- const CHECK_CUSTOM = path.join(__dirname, '..', 'tools', 'check_custom.js');
28
-
29
- const VALID_CONFIG = `rules:
30
- - name: no-debugger
31
- pattern: 'debugger'
32
- message: 'No debugger statements'
33
- severity: error
34
- scope: '**/*.js'
35
- meta: {owner: team, tier: 1}
36
- - name: no-todo
37
- pattern: 'TODO'
38
- message: 'No TODOs'
39
- severity: warning
40
- exclude: ['**/docs/**']
41
- `;
42
-
43
- const EXPECTED_CONFIG = {
44
- rules: [
45
- {
46
- name: 'no-debugger',
47
- pattern: 'debugger',
48
- message: 'No debugger statements',
49
- severity: 'error',
50
- scope: '**/*.js',
51
- meta: { owner: 'team', tier: 1 },
52
- },
53
- {
54
- name: 'no-todo',
55
- pattern: 'TODO',
56
- message: 'No TODOs',
57
- severity: 'warning',
58
- exclude: ['**/docs/**'],
59
- },
60
- ],
61
- };
62
-
63
- /**
64
- * Run tools/check_custom.js (or a copy at the given path) against a
65
- * target directory. Resolves with { parsed, exitCode }.
66
- * @param {string} script - Absolute path to check_custom.js
67
- * @param {string} targetDir - Directory to scan
68
- * @returns {Promise<{parsed: object, exitCode: number}>}
69
- */
70
- function runCheckCustom(script, targetDir) {
71
- return new Promise((resolve, reject) => {
72
- const proc = spawn(process.execPath, [script, targetDir], { cwd: targetDir });
73
- let stdout = '';
74
- proc.stdout.on('data', (d) => (stdout += d));
75
- proc.on('close', (code) => {
76
- try {
77
- resolve({ parsed: JSON.parse(stdout), exitCode: code });
78
- } catch {
79
- reject(new Error(`Failed to parse output: ${stdout}`));
80
- }
81
- });
82
- proc.on('error', reject);
83
- });
84
- }
85
-
86
- /**
87
- * Initialize a git repo with an initial commit, so
88
- * `git diff --name-only HEAD~1` fails and check_custom
89
- * falls back to `git diff --name-only --cached`.
90
- * @param {string} dir - Target directory
91
- */
92
- function initGitRepo(dir) {
93
- spawnSync('git', ['init'], { cwd: dir });
94
- spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
95
- spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
96
- spawnSync('git', ['add', '.'], { cwd: dir });
97
- spawnSync('git', ['commit', '-m', 'initial'], { cwd: dir });
98
- }
99
-
100
- describe('config: src/yaml.js parser reuse', () => {
101
- it('parses a valid .clc-forge.yml with scalars, block sequence, flow mapping', () => {
102
- assert.deepStrictEqual(parseYaml(VALID_CONFIG), EXPECTED_CONFIG);
103
- });
104
-
105
- it('parses quoted and unquoted scalars into their typed values', () => {
106
- const cfg = 'enabled: true\ncount: 42\nratio: 1.5\nnothing: null\n';
107
- assert.deepStrictEqual(parseYaml(cfg), {
108
- enabled: true,
109
- count: 42,
110
- ratio: 1.5,
111
- nothing: null,
112
- });
113
- });
114
-
115
- it('strips comments and skips blank lines', () => {
116
- const cfg = '# top comment\nkey: value # trailing\n\nother: x\n';
117
- assert.deepStrictEqual(parseYaml(cfg), { key: 'value', other: 'x' });
118
- });
119
- });
120
-
121
- describe('config: unsupported YAML syntax', () => {
122
- it('throws on anchor syntax (&name)', () => {
123
- assert.throws(
124
- () => parseYaml('defaults: &defaults\n a: 1\n'),
125
- /Unsupported YAML syntax at line 1: anchors\/aliases are not supported/
126
- );
127
- });
128
-
129
- it('throws on alias syntax (*name)', () => {
130
- assert.throws(
131
- () => parseYaml('db: *defaults\n'),
132
- /Unsupported YAML syntax at line 1: anchors\/aliases are not supported/
133
- );
134
- });
135
-
136
- it('throws on anchor inside a sequence item', () => {
137
- assert.throws(
138
- () => parseYaml('rules:\n - &item name: x\n'),
139
- /Unsupported YAML syntax at line 2: anchors\/aliases are not supported/
140
- );
141
- });
142
-
143
- it('reports the exact line number for nested content', () => {
144
- assert.throws(
145
- () => parseYaml('rules:\n - name: a\n template: &tpl shared\n'),
146
- /Unsupported YAML syntax at line 3/
147
- );
148
- });
149
-
150
- it('does not false-positive on regex patterns or globs', () => {
151
- const cfg = `rules:
152
- - name: r
153
- pattern: 'a*b'
154
- message: 'm'
155
- scope: '**/*.js'
156
- exclude: ['**/docs/**', '*.min.js']
157
- `;
158
- assert.deepStrictEqual(parseYaml(cfg), {
159
- rules: [
160
- {
161
- name: 'r',
162
- pattern: 'a*b',
163
- message: 'm',
164
- scope: '**/*.js',
165
- exclude: ['**/docs/**', '*.min.js'],
166
- },
167
- ],
168
- });
169
- });
170
- });
171
-
172
- describe('config: check_custom behavior preserved (subprocess)', () => {
173
- it('produces the same output shape (name, exitCode, skipped, data)', async () => {
174
- const dir = createTmpProject({
175
- '.clc-forge.yml': VALID_CONFIG,
176
- 'src/app.js': 'module.exports = 1;\n',
177
- });
178
- initGitRepo(dir);
179
-
180
- try {
181
- // Introduce a violation, then stage it so check_custom's
182
- // fallback diff (`git diff --name-only --cached`) sees it.
183
- fs.writeFileSync(path.join(dir, 'src/app.js'), 'debugger;\nmodule.exports = 1;\n');
184
- spawnSync('git', ['add', 'src/app.js'], { cwd: dir });
185
-
186
- const { parsed, exitCode } = await runCheckCustom(CHECK_CUSTOM, dir);
187
-
188
- assert.strictEqual(parsed.name, 'check_custom');
189
- assert.strictEqual(parsed.skipped, false);
190
- assert.strictEqual(parsed.exitCode, 1);
191
- assert.strictEqual(exitCode, 1);
192
- assert.ok(parsed.data, 'data key present');
193
- assert.ok(parsed.data.violations, 'violations key present');
194
- assert.strictEqual(parsed.data.violations['src/app.js'].length, 1);
195
- assert.strictEqual(parsed.data.violations['src/app.js'][0].rule, 'no-debugger');
196
- assert.strictEqual(
197
- parsed.data.violations['src/app.js'][0].message,
198
- 'No debugger statements'
199
- );
200
- assert.strictEqual(parsed.data.violations['src/app.js'][0].severity, 'error');
201
- assert.strictEqual(parsed.data.violations['src/app.js'][0].lineNo, 1);
202
- assert.strictEqual(parsed.data.message, '1 custom rule violation(s) in 1 file(s)');
203
- } finally {
204
- teardown(dir);
205
- }
206
- });
207
-
208
- it('ships src/yaml.js to generated projects so check_custom resolves', async () => {
209
- const { autoDetectStack } = require('../src/detector');
210
- const { generateHarness } = require('../src/generator');
211
-
212
- // Real end-to-end path: detect + generate a Next.js project.
213
- const dir = createTmpProject({
214
- 'package.json': JSON.stringify({ dependencies: { next: '14.0.0' } }),
215
- });
216
- const config = autoDetectStack(dir);
217
- assert.strictEqual(config.framework, 'Next.js (App Router)');
218
- generateHarness(dir, config);
219
-
220
- try {
221
- // The generated project must ship src/yaml.js next to tools/check_custom.js.
222
- assert.ok(fs.existsSync(path.join(dir, 'src', 'yaml.js')), 'src/yaml.js shipped');
223
-
224
- const generatedCheck = path.join(dir, 'tools', 'check_custom.js');
225
- assert.ok(fs.existsSync(generatedCheck), 'tools/check_custom.js shipped');
226
-
227
- fs.writeFileSync(path.join(dir, '.clc-forge.yml'), VALID_CONFIG);
228
- fs.mkdirSync(path.join(dir, 'src', 'app'), { recursive: true });
229
- fs.writeFileSync(path.join(dir, 'src', 'app.js'), 'module.exports = 1;\n');
230
- initGitRepo(dir);
231
-
232
- fs.writeFileSync(path.join(dir, 'src', 'app.js'), 'debugger;\nmodule.exports = 1;\n');
233
- spawnSync('git', ['add', 'src/app.js'], { cwd: dir });
234
-
235
- const { parsed, exitCode } = await runCheckCustom(generatedCheck, dir);
236
-
237
- assert.strictEqual(parsed.name, 'check_custom');
238
- assert.strictEqual(parsed.skipped, false);
239
- assert.strictEqual(parsed.exitCode, 1);
240
- assert.strictEqual(exitCode, 1);
241
- assert.strictEqual(parsed.data.violations['src/app.js'][0].rule, 'no-debugger');
242
- } finally {
243
- teardown(dir);
244
- }
245
- });
246
-
247
- it('returns skipped:true with parse error message on unsupported anchor/alias config', async () => {
248
- const dir = createTmpProject({
249
- '.clc-forge.yml': 'rules:\n - name: r\n template: &tpl shared\n',
250
- 'src/app.js': 'module.exports = 1;\n',
251
- });
252
- initGitRepo(dir);
253
-
254
- try {
255
- fs.writeFileSync(path.join(dir, 'src/app.js'), 'debugger;\n');
256
- spawnSync('git', ['add', 'src/app.js'], { cwd: dir });
257
-
258
- const { parsed, exitCode } = await runCheckCustom(CHECK_CUSTOM, dir);
259
-
260
- assert.strictEqual(parsed.name, 'check_custom');
261
- assert.strictEqual(parsed.skipped, true);
262
- assert.strictEqual(parsed.exitCode, 0);
263
- assert.ok(
264
- parsed.data.message.includes('Config parse error: Unsupported YAML syntax'),
265
- `unexpected message: ${parsed.data.message}`
266
- );
267
- } finally {
268
- teardown(dir);
269
- }
270
- });
271
- });
272
-
273
-
274
- // ═══════════════════════════════════════════════════════════════════════════
275
- // A-03: .clc-forge.yml loader / validator / resolver (extended schema)
276
- // ═══════════════════════════════════════════════════════════════════════════
277
-
278
- const FULL_VALID_YAML = `active_guards:
279
- - check_custom
280
- - scan_secrets
281
- phases: [research, plan, test-scenarios, hit, tdd-red, green, audit, rdd-review, handover, close]
282
- gate_mode: advisory
283
- severities:
284
- check_custom: warning
285
- scan_secrets: error
286
- layers: [domain, application]
287
- scope: 'src'
288
- exclude_paths: ['**/*.min.js']
289
- rules:
290
- - name: no-debugger
291
- pattern: 'debugger'
292
- severity: error
293
- `;
294
-
295
- function assertConfigError(fn, pathPattern, fixPattern) {
296
- assert.throws(fn, (err) => {
297
- assert.ok(err instanceof ConfigError, `expected ConfigError, got ${err.constructor.name}: ${err.message}`);
298
- assert.ok(err instanceof Error, 'ConfigError must extend Error');
299
- assert.match(err.path, pathPattern);
300
- assert.ok(typeof err.fix === 'string' && err.fix.length > 0, 'fix message present');
301
- if (fixPattern) assert.match(err.fix, fixPattern);
302
- return true;
303
- });
304
- }
305
-
306
- describe('config A-03: loadConfig', () => {
307
- it('parses yaml into a raw object and returns {} for empty input', () => {
308
- const raw = loadConfig(FULL_VALID_YAML);
309
- assert.deepStrictEqual(raw.active_guards, ['check_custom', 'scan_secrets']);
310
- assert.deepStrictEqual(raw.phases, HARNESS_PHASES);
311
- assert.deepStrictEqual(raw.severities, { check_custom: 'warning', scan_secrets: 'error' });
312
- assert.strictEqual(raw.scope, 'src');
313
- assert.deepStrictEqual(raw.exclude_paths, ['**/*.min.js']);
314
- assert.strictEqual(raw.rules.length, 1);
315
- assert.deepStrictEqual(loadConfig(''), {});
316
- assert.deepStrictEqual(loadConfig(undefined), {});
317
- assert.deepStrictEqual(loadConfig(null), {});
318
- });
319
-
320
- it('throws ConfigError on parse failure (unsupported anchor syntax)', () => {
321
- assertConfigError(() => loadConfig('base: &anchor\n a: 1\n'), /<root>/, /parse/i);
322
- });
323
- });
324
-
325
- describe('config A-03: validateConfig', () => {
326
- it('accepts full valid and empty configs, returning them unchanged', () => {
327
- const raw = loadConfig(FULL_VALID_YAML);
328
- assert.strictEqual(validateConfig(raw), raw);
329
- assert.deepStrictEqual(validateConfig({}), {});
330
- });
331
-
332
- it('rejects unknown guard names in active_guards and severities', () => {
333
- assertConfigError(
334
- () => validateConfig({ active_guards: ['check_custom', 'does_not_exist'] }),
335
- /active_guards\[1\]/, /does_not_exist/
336
- );
337
- assertConfigError(() => validateConfig({ severities: { bogus_guard: 'error' } }), /severities\.bogus_guard/);
338
- });
339
-
340
- it('rejects invalid severity and gate_mode values', () => {
341
- assertConfigError(
342
- () => validateConfig({ severities: { check_custom: 'fatal' } }),
343
- /severities\.check_custom/, /to one of: error, warning, advisory/
344
- );
345
- assertConfigError(() => validateConfig({ gate_mode: 'soft' }), /gate_mode/, /to one of: hard, advisory/);
346
- });
347
-
348
- it('rejects unknown/out-of-order/duplicate phases; accepts ordered subset', () => {
349
- assertConfigError(() => validateConfig({ phases: ['research', 'plan', 'nonsense'] }), /phases\[2\]/, /nonsense/);
350
- assertConfigError(() => validateConfig({ phases: ['research', 'audit', 'plan'] }), /phases\[2\]/, /out of order/);
351
- assertConfigError(() => validateConfig({ phases: ['research', 'research'] }), /phases\[1\]/);
352
- assert.deepStrictEqual(
353
- validateConfig({ phases: ['research', 'plan', 'tdd-red', 'green'] }).phases,
354
- ['research', 'plan', 'tdd-red', 'green']
355
- );
356
- });
357
-
358
- it('validates scope / exclude_paths / layers types', () => {
359
- assert.strictEqual(validateConfig({ scope: 'src' }).scope, 'src');
360
- assert.deepStrictEqual(validateConfig({ scope: ['src', 'lib'] }).scope, ['src', 'lib']);
361
- assert.deepStrictEqual(validateConfig({ exclude_paths: ['**/*.min.js'] }).exclude_paths, ['**/*.min.js']);
362
- assert.deepStrictEqual(validateConfig({ layers: ['domain'] }).layers, ['domain']);
363
- assertConfigError(() => validateConfig({ scope: [42] }), /scope\[0\]/);
364
- assertConfigError(() => validateConfig({ exclude_paths: 'src' }), /exclude_paths/);
365
- assertConfigError(() => validateConfig({ layers: [12] }), /layers\[0\]/);
366
- });
367
-
368
- it('preserves legacy rules with unique required names', () => {
369
- assert.strictEqual(validateConfig(loadConfig(FULL_VALID_YAML)).rules[0].name, 'no-debugger');
370
- assertConfigError(
371
- () => validateConfig({ rules: [{ name: 'a', pattern: 'x' }, { name: 'a', pattern: 'y' }] }),
372
- /rules\[1\]\.name/, /duplicate/
373
- );
374
- assertConfigError(() => validateConfig({ rules: [{ pattern: 'x' }] }), /rules\[0\]/, /name/);
375
- assertConfigError(() => validateConfig({ rules: [{ name: 'a', severity: 'fatal' }] }), /rules\[0\]\.severity/);
376
- });
377
- });
378
-
379
- const DETECTED = { projectType: 'backend', framework: 'FastAPI', orm: 'Framework Native', testRunner: 'Pytest', techTools: [] };
380
-
381
- describe('config A-03: resolveConfig', () => {
382
- it('resolves a valid full config with catalog defaults merged', () => {
383
- const resolved = resolveConfig(loadConfig(FULL_VALID_YAML), DETECTED);
384
- assert.deepStrictEqual(resolved.activeGuards, ['check_custom', 'scan_secrets']);
385
- assert.strictEqual(resolved.gateMode, 'advisory');
386
- assert.deepStrictEqual(resolved.phases, HARNESS_PHASES);
387
- assert.strictEqual(resolved.scope, 'src');
388
- assert.deepStrictEqual(resolved.excludePaths, ['**/*.min.js']);
389
- assert.deepStrictEqual(resolved.layers, ['domain', 'application']);
390
- assert.strictEqual(resolved.framework, 'FastAPI');
391
- // Config severities override catalog defaults.
392
- assert.strictEqual(resolved.effectiveSeverities.check_custom, 'warning');
393
- assert.strictEqual(resolved.effectiveSeverities.scan_secrets, 'error');
394
- // __catalog pipeline with override + advisory downgrade-everything.
395
- assert.strictEqual(resolved.__catalog.length, 2);
396
- assert.strictEqual(resolved.__catalog[0].name, 'check_custom');
397
- assert.strictEqual(resolved.__catalog[0].severity, 'warning');
398
- assert.strictEqual(resolved.__catalog[0].file, 'check_custom.js+check_custom.py');
399
- assert.strictEqual(resolved.__catalog[0].gate_mode, 'advisory');
400
- assert.strictEqual(resolved.__catalog[1].gate_mode, 'advisory');
401
- });
402
-
403
- it('defaults active_guards to catalog rows applicable to the detected stack', () => {
404
- const resolved = resolveConfig({}, { projectType: 'frontend', framework: 'Next.js (App Router)', orm: 'Framework Native', testRunner: 'Vitest', techTools: [] });
405
- const names = new Set(resolved.activeGuards);
406
- assert.ok(names.has('scan_secrets'), 'always-applicable guard active');
407
- assert.ok(names.has('check_a11y'), 'frontend-applicable guard active');
408
- assert.ok(!names.has('check_migrations'), 'backend-only guard NOT active');
409
- assert.ok(!names.has('celery_guard'), 'tech guard NOT active without techTools');
410
- });
411
-
412
- it('activates tech guards whose names are detected in techTools', () => {
413
- const resolved = resolveConfig({}, { ...DETECTED, framework: 'Django', techTools: [{ name: 'celery_guard', path: 'celery_guard.py', command: 'python3 tools/celery_guard.py' }] });
414
- assert.ok(resolved.activeGuards.includes('celery_guard'));
415
- assert.ok(resolved.activeGuards.includes('check_migrations'));
416
- });
417
-
418
- it('applies severity precedence: env override > config severities > catalog default', () => {
419
- const prev = process.env[ENV_SEVERITIES];
420
- try {
421
- // catalog default for check_ui_reuse is 'warning'.
422
- assert.strictEqual(resolveConfig({}, { projectType: 'frontend' }).effectiveSeverities.check_ui_reuse, 'warning');
423
- // env override wins over config severities and catalog default.
424
- process.env[ENV_SEVERITIES] = JSON.stringify({ check_ui_reuse: 'error' });
425
- assert.strictEqual(
426
- resolveConfig({ severities: { check_ui_reuse: 'warning' } }, { projectType: 'frontend' }).effectiveSeverities.check_ui_reuse,
427
- 'error'
428
- );
429
- // unknown guard / invalid value / malformed JSON in env are ignored.
430
- process.env[ENV_SEVERITIES] = JSON.stringify({ bogus_guard: 'error' });
431
- assert.strictEqual(resolveConfig({}, { projectType: 'frontend' }).effectiveSeverities.check_ui_reuse, 'warning');
432
- process.env[ENV_SEVERITIES] = JSON.stringify({ check_ui_reuse: 'fatal' });
433
- assert.strictEqual(resolveConfig({}, { projectType: 'frontend' }).effectiveSeverities.check_ui_reuse, 'warning');
434
- process.env[ENV_SEVERITIES] = '{not json';
435
- assert.strictEqual(resolveConfig({}, { projectType: 'frontend' }).effectiveSeverities.check_ui_reuse, 'warning');
436
- } finally {
437
- if (prev === undefined) delete process.env[ENV_SEVERITIES];
438
- else process.env[ENV_SEVERITIES] = prev;
439
- }
440
- });
441
-
442
- it('does not silently fall back on invalid raw config', () => {
443
- assertConfigError(() => resolveConfig({ active_guards: ['nope'] }, {}), /active_guards\[0\]/);
444
- });
445
- });
446
-
447
- describe('config A-03: generateHarness integration', () => {
448
- it('generateHarness(config) writes .clc-forge.resolved.json metadata', () => {
449
- const dir = createTmpProject({});
450
- try {
451
- const resolved = generateHarness(dir, loadConfig(FULL_VALID_YAML));
452
- const meta = JSON.parse(fs.readFileSync(path.join(dir, '.clc-forge.resolved.json'), 'utf-8'));
453
- assert.deepStrictEqual(meta.activeGuards, ['check_custom', 'scan_secrets']);
454
- assert.strictEqual(meta.gateMode, 'advisory');
455
- assert.deepStrictEqual(meta.phases, HARNESS_PHASES);
456
- assert.strictEqual(meta.scope, 'src');
457
- assert.deepStrictEqual(meta.excludePaths, ['**/*.min.js']);
458
- assert.deepStrictEqual(meta.layers, ['domain', 'application']);
459
- assert.strictEqual(meta.framework, 'unknown');
460
- assert.strictEqual(meta.__catalog.length, 2);
461
- assert.deepStrictEqual(resolved.activeGuards, meta.activeGuards);
462
- } finally {
463
- teardown(dir);
464
- }
465
- });
466
-
467
- it('generateHarness with a pre-resolved config does not re-resolve', () => {
468
- const dir = createTmpProject({});
469
- try {
470
- const resolved = resolveConfig(loadConfig(FULL_VALID_YAML), { ...DETECTED });
471
- resolved.__clcForgeConfigPath = true;
472
- generateHarness(dir, resolved);
473
- const meta = JSON.parse(fs.readFileSync(path.join(dir, '.clc-forge.resolved.json'), 'utf-8'));
474
- assert.strictEqual(meta.framework, 'FastAPI', 'detected metadata preserved (no re-resolve)');
475
- } finally {
476
- teardown(dir);
477
- }
478
- });
479
-
480
- it('generateHarness(null/undefined/adapter-config) keeps the legacy path, no metadata', () => {
481
- const dir = createTmpProject({});
482
- try {
483
- generateHarness(dir, null);
484
- assert.ok(fs.existsSync(path.join(dir, 'AGENTS.md')), 'AGENTS.md still written');
485
- assert.ok(fs.existsSync(path.join(dir, 'tools', 'scan_secrets.py')), 'fallback tools still installed');
486
- assert.strictEqual(fs.existsSync(path.join(dir, '.clc-forge.resolved.json')), false, 'no metadata for the legacy/null path');
487
- } finally {
488
- teardown(dir);
489
- }
490
-
491
- const dir2 = createTmpProject({ 'package.json': JSON.stringify({ dependencies: { next: '14.0.0' } }) });
492
- try {
493
- const { autoDetectStack } = require('../src/detector');
494
- generateHarness(dir2, autoDetectStack(dir2));
495
- assert.strictEqual(fs.existsSync(path.join(dir2, '.clc-forge.resolved.json')), false, 'adapter-detected config does not emit metadata');
496
- generateHarness(dir2, undefined);
497
- assert.strictEqual(fs.existsSync(path.join(dir2, '.clc-forge.resolved.json')), false);
498
- } finally {
499
- teardown(dir2);
500
- }
501
- });
502
-
503
- it('generateHarness with an invalid config throws ConfigError (no partial harness)', () => {
504
- const dir = createTmpProject({});
505
- try {
506
- assertConfigError(() => generateHarness(dir, { active_guards: ['not_a_guard'] }), /active_guards\[0\]/);
507
- assert.strictEqual(fs.existsSync(path.join(dir, '.clc-forge.resolved.json')), false);
508
- } finally {
509
- teardown(dir);
510
- }
511
- });
512
- });