clckernel 1.2.6
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.md +124 -0
- package/README.es.md +166 -0
- package/README.md +167 -0
- package/bin/cli.js +12 -0
- package/docs/Journal/001-adr-clckernel-governance.md +18 -0
- package/package.json +44 -0
- package/src/adapters/astro.js +67 -0
- package/src/adapters/base.js +109 -0
- package/src/adapters/django.js +70 -0
- package/src/adapters/fastapi.js +69 -0
- package/src/adapters/go.js +50 -0
- package/src/adapters/laravel.js +52 -0
- package/src/adapters/nextjs.js +72 -0
- package/src/adapters/rails.js +54 -0
- package/src/adapters/rust.js +51 -0
- package/src/catalog.js +217 -0
- package/src/config.js +250 -0
- package/src/detector.js +85 -0
- package/src/doctor.js +125 -0
- package/src/generator.js +473 -0
- package/src/index.js +52 -0
- package/src/technologies/detector.js +243 -0
- package/src/technologies/guards/_shared.js +66 -0
- package/src/ui.js +101 -0
- package/src/yaml.js +173 -0
- package/test/adapters/astro.test.js +102 -0
- package/test/adapters/base.test.js +86 -0
- package/test/adapters/django.test.js +95 -0
- package/test/adapters/fastapi.test.js +88 -0
- package/test/adapters/go.test.js +81 -0
- package/test/adapters/laravel.test.js +106 -0
- package/test/adapters/nextjs.test.js +113 -0
- package/test/adapters/rails.test.js +108 -0
- package/test/adapters/rust.test.js +83 -0
- package/test/catalog.test.js +170 -0
- package/test/config.test.js +512 -0
- package/test/detector.test.js +345 -0
- package/test/doctor.test.js +302 -0
- package/test/generator.test.js +419 -0
- package/test/guards_new.test.js +95 -0
- package/test/helpers.js +36 -0
- package/test/integration/cli-flow.test.js +270 -0
- package/test/technologies/celery_guard.test.js +185 -0
- package/test/technologies/detector.test.js +342 -0
- package/test/technologies/docker_guard.test.js +187 -0
- package/test/technologies/integration.test.js +123 -0
- package/test/technologies/postgres_guard.test.js +87 -0
- package/test/technologies/redis_guard.test.js +128 -0
- package/tools/audit.js +219 -0
- package/tools/audit.py +273 -0
- package/tools/celery_guard.py +209 -0
- package/tools/check_a11y.js +109 -0
- package/tools/check_api_contracts.js +139 -0
- package/tools/check_architecture.js +139 -0
- package/tools/check_architecture.py +264 -0
- package/tools/check_custom.js +163 -0
- package/tools/check_custom.py +395 -0
- package/tools/check_db_efficiency.py +190 -0
- package/tools/check_migrations.py +223 -0
- package/tools/check_performance.js +142 -0
- package/tools/check_responsive.js +131 -0
- package/tools/check_scope.py +180 -0
- package/tools/check_seo.js +139 -0
- package/tools/check_storybook.js +108 -0
- package/tools/check_ui_reuse.js +135 -0
- package/tools/docker_guard.py +210 -0
- package/tools/postgres_guard.py +192 -0
- package/tools/redis_guard.py +197 -0
- package/tools/scan_secrets.js +109 -0
- package/tools/scan_secrets.py +153 -0
- package/tools/verify_tdd.py +212 -0
package/src/catalog.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Kernel — Central Safeguard Catalog
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth describing every safeguard/guard the clckernel
|
|
5
|
+
* framework knows about. Drives config validation (src/config.js),
|
|
6
|
+
* config-driven harness generation (src/generator.js), doctor checks, and
|
|
7
|
+
* the future safeguard audit.
|
|
8
|
+
*
|
|
9
|
+
* Entry shape (design §1.2, additive fields noted):
|
|
10
|
+
* name {string} guard script basename WITHOUT extension
|
|
11
|
+
* labels {string[]} alias labels (yaml/js/python variants) for config matching
|
|
12
|
+
* checks {string} short description of what the safeguard verifies (design §1.2)
|
|
13
|
+
* verify {string[]} plausible verify_* audit-goal tokens this safeguard covers
|
|
14
|
+
* applies_when{string} applicability predicate key (stack/project conditions)
|
|
15
|
+
* severity {string} 'error' | 'warning' | 'advisory' (catalog default)
|
|
16
|
+
* gate_mode {string} 'hard' | 'advisory' (catalog default)
|
|
17
|
+
* config_keys {string[]} .clc-forge.yml keys this safeguard consumes
|
|
18
|
+
* phase {string} 10-phase harness phase: 'audit' | 'test' | 'review' | 'handover'
|
|
19
|
+
* file {string|null} bundled tools/ file(s), '+' separated; null = external CLI
|
|
20
|
+
* manifest {boolean} true = declared by >=1 adapter manifest (design §1.2)
|
|
21
|
+
* planned {boolean} true = tool file not yet materialized (future slice)
|
|
22
|
+
*
|
|
23
|
+
* The catalog is a JOIN, not a rewrite: the no-config adapter path never
|
|
24
|
+
* consults it; only the config-driven generation path does.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const SAFEGUARD_CATALOG = [
|
|
28
|
+
{ name: 'scan_secrets', labels: ['scan_secrets', 'scan_secrets.js', 'scan_secrets.py'],
|
|
29
|
+
checks: 'Secret & private-key leak scan', verify: ['verify_secret_leaks', 'verify_no_hardcoded_secrets'],
|
|
30
|
+
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
31
|
+
phase: 'audit', file: 'scan_secrets.js+scan_secrets.py', manifest: true, planned: false },
|
|
32
|
+
|
|
33
|
+
{ name: 'check_architecture', labels: ['check_architecture', 'check_architecture.js', 'check_architecture.py'],
|
|
34
|
+
checks: 'Clean Architecture layer / server-component boundaries', verify: ['verify_architecture_layers', 'verify_server_component_boundaries'],
|
|
35
|
+
applies_when: 'frontend,backend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
36
|
+
phase: 'audit', file: 'check_architecture.js+check_architecture.py', manifest: true, planned: false },
|
|
37
|
+
|
|
38
|
+
{ name: 'check_custom', labels: ['check_custom', 'check_custom.js', 'check_custom.py'],
|
|
39
|
+
checks: 'User-defined regex rules from `.clc-forge.yml`', verify: ['verify_custom_rules', 'verify_user_defined_regex_rules'],
|
|
40
|
+
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
41
|
+
phase: 'audit', file: 'check_custom.js+check_custom.py', manifest: true, planned: false },
|
|
42
|
+
|
|
43
|
+
{ name: 'check_a11y', labels: ['check_a11y', 'check_a11y.js'],
|
|
44
|
+
checks: 'ARIA + alt accessibility', verify: ['verify_a11y', 'verify_accessibility'],
|
|
45
|
+
applies_when: 'frontend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
46
|
+
phase: 'audit', file: 'check_a11y.js', manifest: true, planned: false },
|
|
47
|
+
|
|
48
|
+
{ name: 'check_ui_reuse', labels: ['check_ui_reuse', 'check_ui_reuse.js'],
|
|
49
|
+
checks: 'UI primitive reuse from components/ui/', verify: ['verify_ui_reuse', 'verify_component_reuse'],
|
|
50
|
+
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
51
|
+
phase: 'audit', file: 'check_ui_reuse.js', manifest: true, planned: false },
|
|
52
|
+
|
|
53
|
+
{ name: 'check_performance', labels: ['check_performance', 'check_performance.js'],
|
|
54
|
+
checks: 'next/image + tree-shaking', verify: ['verify_performance', 'verify_image_optimization'],
|
|
55
|
+
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
56
|
+
phase: 'audit', file: 'check_performance.js', manifest: true, planned: false },
|
|
57
|
+
|
|
58
|
+
{ name: 'check_responsive', labels: ['check_responsive', 'check_responsive.js'],
|
|
59
|
+
checks: 'Mobile-first responsiveness, fixed-width overflow & touch target size', verify: ['verify_responsive', 'verify_mobile_adaptability'],
|
|
60
|
+
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
61
|
+
phase: 'audit', file: 'check_responsive.js', manifest: true, planned: false },
|
|
62
|
+
|
|
63
|
+
{ name: 'check_seo', labels: ['check_seo', 'check_seo.js'],
|
|
64
|
+
checks: 'SEO metadata, OpenGraph, GEO tags, semantic HTML & LCP priority', verify: ['verify_seo', 'verify_geo_metadata', 'verify_core_web_vitals'],
|
|
65
|
+
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
66
|
+
phase: 'audit', file: 'check_seo.js', manifest: true, planned: false },
|
|
67
|
+
|
|
68
|
+
{ name: 'check_storybook', labels: ['check_storybook', 'check_storybook.js'],
|
|
69
|
+
checks: 'Storybook coverage (`.stories.tsx`)', verify: ['verify_storybook_coverage'],
|
|
70
|
+
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
71
|
+
phase: 'audit', file: 'check_storybook.js', manifest: true, planned: false },
|
|
72
|
+
|
|
73
|
+
{ name: 'check_api_contracts', labels: ['check_api_contracts', 'check_api_contracts.js'],
|
|
74
|
+
checks: 'Zod response-schema parsing', verify: ['verify_api_contracts', 'verify_response_schemas'],
|
|
75
|
+
applies_when: 'frontend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
76
|
+
phase: 'audit', file: 'check_api_contracts.js', manifest: true, planned: false },
|
|
77
|
+
|
|
78
|
+
{ name: 'check_migrations', labels: ['check_migrations', 'check_migrations.py'],
|
|
79
|
+
checks: 'Alembic/Django migration idempotency', verify: ['verify_migration_idempotency'],
|
|
80
|
+
applies_when: 'backend,fastapi,django', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
81
|
+
phase: 'audit', file: 'check_migrations.py', manifest: true, planned: false },
|
|
82
|
+
|
|
83
|
+
{ name: 'verify_tdd', labels: ['verify_tdd', 'verify_tdd.py'],
|
|
84
|
+
checks: 'Real red→green TDD validation', verify: ['verify_tdd_red_green', 'verify_tdd_evidence'],
|
|
85
|
+
applies_when: 'backend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
86
|
+
phase: 'audit', file: 'verify_tdd.py', manifest: false, planned: false },
|
|
87
|
+
|
|
88
|
+
{ name: 'check_scope', labels: ['check_scope', 'check_scope.py'],
|
|
89
|
+
checks: 'git-diff vs SDD-authorization scope guardrail', verify: ['verify_scope_authorization', 'verify_sdd_scope'],
|
|
90
|
+
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
91
|
+
phase: 'audit', file: 'check_scope.py', manifest: false, planned: false },
|
|
92
|
+
|
|
93
|
+
{ name: 'celery_guard', labels: ['celery_guard', 'celery_guard.py'],
|
|
94
|
+
checks: 'Broker URL exposure, ignore_result, result backend', verify: ['verify_celery_broker_config'],
|
|
95
|
+
applies_when: 'celery', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
96
|
+
phase: 'audit', file: 'celery_guard.py', manifest: true, planned: false },
|
|
97
|
+
|
|
98
|
+
{ name: 'redis_guard', labels: ['redis_guard', 'redis_guard.py'],
|
|
99
|
+
checks: 'decode_responses, connect timeout, hardcoded URLs', verify: ['verify_redis_config'],
|
|
100
|
+
applies_when: 'redis', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
101
|
+
phase: 'audit', file: 'redis_guard.py', manifest: true, planned: false },
|
|
102
|
+
|
|
103
|
+
{ name: 'postgres_guard', labels: ['postgres_guard', 'postgres_guard.py'],
|
|
104
|
+
checks: 'Raw SQL string formatting, hstore misuse', verify: ['verify_postgres_sql_usage'],
|
|
105
|
+
applies_when: 'postgresql', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
106
|
+
phase: 'audit', file: 'postgres_guard.py', manifest: true, planned: false },
|
|
107
|
+
|
|
108
|
+
{ name: 'docker_guard', labels: ['docker_guard', 'docker_guard.py'],
|
|
109
|
+
checks: 'Non-root USER, ENV secrets, missing HEALTHCHECK', verify: ['verify_dockerfile_security'],
|
|
110
|
+
applies_when: 'docker', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
111
|
+
phase: 'audit', file: 'docker_guard.py', manifest: true, planned: false },
|
|
112
|
+
|
|
113
|
+
{ name: 'ruff', labels: ['ruff'],
|
|
114
|
+
checks: 'Python AST linter (external CLI)', verify: ['verify_python_lint'],
|
|
115
|
+
applies_when: 'backend', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
116
|
+
phase: 'test', file: null, manifest: true, planned: false },
|
|
117
|
+
|
|
118
|
+
{ name: 'pytest', labels: ['pytest'],
|
|
119
|
+
checks: 'Test runner (external CLI)', verify: ['verify_test_coverage'],
|
|
120
|
+
applies_when: 'backend', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
121
|
+
phase: 'test', file: null, manifest: true, planned: false },
|
|
122
|
+
|
|
123
|
+
{ name: 'tsc', labels: ['tsc'],
|
|
124
|
+
checks: 'TypeScript typecheck (external)', verify: ['verify_typescript_types'],
|
|
125
|
+
applies_when: 'frontend', severity: 'error', gate_mode: 'hard', config_keys: [],
|
|
126
|
+
phase: 'test', file: null, manifest: true, planned: false },
|
|
127
|
+
|
|
128
|
+
{ name: 'lint', labels: ['lint', 'lint-staged'],
|
|
129
|
+
checks: 'lint-staged (external)', verify: ['verify_lint_staged'],
|
|
130
|
+
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
131
|
+
phase: 'test', file: null, manifest: true, planned: false },
|
|
132
|
+
|
|
133
|
+
{ name: 'graphify', labels: ['graphify'],
|
|
134
|
+
checks: 'Memory graph (external, graceful-skip)', verify: ['verify_memory_graph'],
|
|
135
|
+
applies_when: 'always', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
136
|
+
phase: 'review', file: null, manifest: false, planned: false },
|
|
137
|
+
|
|
138
|
+
{ name: 'gentle_ai_review', labels: ['gentle_ai_review', 'gentle-ai'],
|
|
139
|
+
checks: 'Native bounded review gate (external, graceful-skip)', verify: ['verify_rdd_review_gate'],
|
|
140
|
+
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: [],
|
|
141
|
+
phase: 'review', file: null, manifest: false, planned: false },
|
|
142
|
+
|
|
143
|
+
{ name: 'check_openapi_drift', labels: ['check_openapi_drift', 'check_openapi_drift.py'],
|
|
144
|
+
checks: 'FastAPI OpenAPI schema drift vs app routes', verify: ['verify_openapi_drift'],
|
|
145
|
+
applies_when: 'fastapi', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
146
|
+
phase: 'audit', file: 'check_openapi_drift.py', manifest: true, planned: true },
|
|
147
|
+
|
|
148
|
+
{ name: 'check_db_efficiency', labels: ['check_db_efficiency', 'check_db_efficiency.py'],
|
|
149
|
+
checks: 'ORM N+1 / select_related / prefetch_related', verify: ['verify_db_efficiency', 'verify_no_n_plus_1'],
|
|
150
|
+
applies_when: 'django|sqlalchemy', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
151
|
+
phase: 'audit', file: 'check_db_efficiency.py', manifest: true, planned: false },
|
|
152
|
+
|
|
153
|
+
{ name: 'check_observability', labels: ['check_observability', 'check_observability.py'],
|
|
154
|
+
checks: 'structlog structured logging; bare print/logger ban', verify: ['verify_structured_logging'],
|
|
155
|
+
applies_when: 'backend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
156
|
+
phase: 'audit', file: 'check_observability.py', manifest: true, planned: true },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// Freeze the registry so config validation and generation can rely on it.
|
|
160
|
+
Object.freeze(SAFEGUARD_CATALOG);
|
|
161
|
+
for (const entry of SAFEGUARD_CATALOG) {
|
|
162
|
+
Object.freeze(entry.labels);
|
|
163
|
+
Object.freeze(entry.verify);
|
|
164
|
+
Object.freeze(entry.config_keys);
|
|
165
|
+
Object.freeze(entry);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Valid value sets exported for validators (src/config.js, tests). */
|
|
169
|
+
const VALID_SEVERITIES = ['error', 'warning', 'advisory'];
|
|
170
|
+
const VALID_GATE_MODES = ['hard', 'advisory'];
|
|
171
|
+
const VALID_PHASES = ['audit', 'test', 'review', 'handover'];
|
|
172
|
+
const VALID_APPLIES_WHEN = [
|
|
173
|
+
'always', 'frontend', 'backend', 'fastapi', 'django', 'django|sqlalchemy',
|
|
174
|
+
'celery', 'redis', 'postgresql', 'docker', 'frontend,backend', 'backend,fastapi,django',
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
/** Normalize an input label for catalog lookup. */
|
|
178
|
+
function normalizeLabel(name) {
|
|
179
|
+
return typeof name === 'string' ? name.trim().toLowerCase() : '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve a guard name (or any alias label) to its catalog entry.
|
|
184
|
+
* @param {string} name - entry name or alias label (e.g. 'check_custom.js', 'lint-staged')
|
|
185
|
+
* @returns {object|null} the catalog entry, or null when unknown
|
|
186
|
+
*/
|
|
187
|
+
function applyTo(name) {
|
|
188
|
+
const key = normalizeLabel(name);
|
|
189
|
+
if (!key) return null;
|
|
190
|
+
for (const entry of SAFEGUARD_CATALOG) {
|
|
191
|
+
if (entry.labels.includes(key)) return entry;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Catalog default severity for a guard name/label; null when unknown. */
|
|
197
|
+
function severityFor(name) {
|
|
198
|
+
const entry = applyTo(name);
|
|
199
|
+
return entry ? entry.severity : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Catalog default gate mode for a guard name/label; null when unknown. */
|
|
203
|
+
function gateFor(name) {
|
|
204
|
+
const entry = applyTo(name);
|
|
205
|
+
return entry ? entry.gate_mode : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
SAFEGUARD_CATALOG,
|
|
210
|
+
VALID_SEVERITIES,
|
|
211
|
+
VALID_GATE_MODES,
|
|
212
|
+
VALID_PHASES,
|
|
213
|
+
VALID_APPLIES_WHEN,
|
|
214
|
+
applyTo,
|
|
215
|
+
severityFor,
|
|
216
|
+
gateFor,
|
|
217
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Kernel - extended .clckernel.yml / .clc-forge.yml loader / validator / resolver.
|
|
3
|
+
* Backbone = src/catalog.js. Invalid config -> actionable ConfigError;
|
|
4
|
+
* never a silent fallback.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { parseYaml } = require('./yaml.js');
|
|
8
|
+
const {
|
|
9
|
+
SAFEGUARD_CATALOG,
|
|
10
|
+
VALID_SEVERITIES,
|
|
11
|
+
VALID_GATE_MODES,
|
|
12
|
+
applyTo,
|
|
13
|
+
severityFor,
|
|
14
|
+
} = require('./catalog.js');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Canonical 10-phase workflow order (design §3.1). NOTE: catalog VALID_PHASES
|
|
18
|
+
* is the per-entry `phase` field, NOT this workflow — the `phases` config key
|
|
19
|
+
* validates against this list.
|
|
20
|
+
*/
|
|
21
|
+
const HARNESS_PHASES = [
|
|
22
|
+
'research', 'plan', 'test-scenarios', 'hit', 'tdd-red',
|
|
23
|
+
'green', 'audit', 'rdd-review', 'handover', 'close',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const ENV_SEVERITIES = 'CLCKERNEL_SEVERITIES';
|
|
27
|
+
const LEGACY_ENV_SEVERITIES = 'CLC_FORGE_SEVERITIES';
|
|
28
|
+
const DEFAULT_SCOPE = '.';
|
|
29
|
+
|
|
30
|
+
class ConfigError extends Error {
|
|
31
|
+
constructor(path, message, allowed) {
|
|
32
|
+
const list = Array.isArray(allowed) ? allowed : allowed != null ? [allowed] : [];
|
|
33
|
+
super(
|
|
34
|
+
`Invalid .clckernel.yml: key "${path}" — ${message}` +
|
|
35
|
+
(list.length ? ` (allowed: ${list.join(', ')})` : '')
|
|
36
|
+
);
|
|
37
|
+
this.name = 'ConfigError';
|
|
38
|
+
this.path = path;
|
|
39
|
+
this.allowed = list;
|
|
40
|
+
this.fix =
|
|
41
|
+
list.length
|
|
42
|
+
? `Set "${path}" to one of: ${list.join(', ')}. ${message}`
|
|
43
|
+
: `Correct the value of "${path}". ${message}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function arrOrThrow(raw, path, what) {
|
|
48
|
+
if (!Array.isArray(raw)) throw new ConfigError(path, `expected an array, got ${raw === null ? 'null' : typeof raw}`, what);
|
|
49
|
+
if (!raw.length) throw new ConfigError(path, 'must not be empty', what);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function strItems(arr, base) {
|
|
53
|
+
arr.forEach((v, i) => {
|
|
54
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
55
|
+
throw new ConfigError(`${base}[${i}]`, 'each item must be a non-empty string');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return arr;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listGuards() {
|
|
62
|
+
return SAFEGUARD_CATALOG.map((e) => e.name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateConfig(raw) {
|
|
66
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
67
|
+
throw new ConfigError('<root>', 'expected a YAML mapping (object)');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { active_guards, gate_mode, severities, phases, scope, exclude_paths, layers, rules } = raw;
|
|
71
|
+
|
|
72
|
+
if (active_guards !== undefined) {
|
|
73
|
+
arrOrThrow(active_guards, 'active_guards', 'a list of catalog guard names');
|
|
74
|
+
active_guards.forEach((name, i) => {
|
|
75
|
+
if (typeof name !== 'string' || name.trim() === '') throw new ConfigError(`active_guards[${i}]`, 'each active guard must be a non-empty string');
|
|
76
|
+
if (!applyTo(name)) throw new ConfigError(`active_guards[${i}]`, `unknown guard "${name}"`, listGuards());
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (gate_mode !== undefined && (typeof gate_mode !== 'string' || !VALID_GATE_MODES.includes(gate_mode))) {
|
|
81
|
+
throw new ConfigError('gate_mode', `invalid value "${gate_mode}"`, VALID_GATE_MODES);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (severities !== undefined) {
|
|
85
|
+
if (!severities || typeof severities !== 'object' || Array.isArray(severities)) {
|
|
86
|
+
throw new ConfigError('severities', 'expected an object mapping guard -> severity');
|
|
87
|
+
}
|
|
88
|
+
for (const [name, severity] of Object.entries(severities)) {
|
|
89
|
+
if (!applyTo(name)) throw new ConfigError(`severities.${name}`, `unknown guard "${name}"`, listGuards());
|
|
90
|
+
if (!VALID_SEVERITIES.includes(severity)) throw new ConfigError(`severities.${name}`, `invalid severity "${severity}"`, VALID_SEVERITIES);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (phases !== undefined) {
|
|
95
|
+
arrOrThrow(phases, 'phases', 'the canonical 10-phase workflow names');
|
|
96
|
+
let last = -1;
|
|
97
|
+
phases.forEach((phase, i) => {
|
|
98
|
+
if (typeof phase !== 'string' || phase.trim() === '') throw new ConfigError(`phases[${i}]`, 'each phase must be a non-empty string');
|
|
99
|
+
const idx = HARNESS_PHASES.indexOf(phase);
|
|
100
|
+
if (idx === -1) throw new ConfigError(`phases[${i}]`, `unknown phase "${phase}"`, HARNESS_PHASES);
|
|
101
|
+
if (idx <= last) throw new ConfigError(`phases[${i}]`, `phase "${phase}" is out of order — canonical order is ${HARNESS_PHASES.join(' → ')}`);
|
|
102
|
+
last = idx;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (scope !== undefined) {
|
|
107
|
+
const items = typeof scope === 'string' ? [scope] : scope;
|
|
108
|
+
arrOrThrow(items, 'scope', 'a glob string or array of glob strings');
|
|
109
|
+
strItems(items, 'scope');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (exclude_paths !== undefined) { arrOrThrow(exclude_paths, 'exclude_paths', 'an array of glob strings'); strItems(exclude_paths, 'exclude_paths'); }
|
|
113
|
+
if (layers !== undefined) { arrOrThrow(layers, 'layers', 'an array of layer name strings'); strItems(layers, 'layers'); }
|
|
114
|
+
|
|
115
|
+
if (rules !== undefined) {
|
|
116
|
+
arrOrThrow(rules, 'rules', 'an array of legacy rule objects');
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
rules.forEach((rule, i) => {
|
|
119
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) throw new ConfigError(`rules[${i}]`, 'each rule must be an object');
|
|
120
|
+
if (typeof rule.name !== 'string' || rule.name.trim() === '') throw new ConfigError(`rules[${i}]`, 'each rule requires a non-empty "name"');
|
|
121
|
+
if (seen.has(rule.name)) throw new ConfigError(`rules[${i}].name`, `duplicate rule name "${rule.name}"`);
|
|
122
|
+
seen.add(rule.name);
|
|
123
|
+
if (rule.severity !== undefined && !['error', 'warning'].includes(rule.severity)) {
|
|
124
|
+
throw new ConfigError(`rules[${i}].severity`, `invalid severity "${rule.severity}"`, ['error', 'warning']);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return raw;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const loadConfig = (yamlString) => {
|
|
133
|
+
if (yamlString === undefined || yamlString === null || yamlString.trim() === '') return {};
|
|
134
|
+
let raw;
|
|
135
|
+
try { raw = parseYaml(yamlString); } catch (err) { throw new ConfigError('<root>', `YAML parse error: ${err.message}`); }
|
|
136
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new ConfigError('<root>', 'expected a YAML mapping (object) at the top level');
|
|
137
|
+
return raw;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** Env override map (tolerant: malformed JSON/non-object ignored). */
|
|
141
|
+
function envSeverities() {
|
|
142
|
+
const raw =
|
|
143
|
+
process.env[ENV_SEVERITIES] ||
|
|
144
|
+
process.env.CLC_KERNEL_SEVERITIES ||
|
|
145
|
+
process.env[LEGACY_ENV_SEVERITIES];
|
|
146
|
+
if (!raw) return {};
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(raw);
|
|
149
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
150
|
+
} catch { /* ignore malformed env override */ }
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Catalog names among detected.techTools (design §1.3 JOIN rule). */
|
|
155
|
+
function detectedToolNames(detected) {
|
|
156
|
+
const names = new Set();
|
|
157
|
+
if (!detected || !Array.isArray(detected.techTools)) return names;
|
|
158
|
+
for (const tool of detected.techTools) {
|
|
159
|
+
const name = tool && typeof tool === 'object' ? tool.name : tool;
|
|
160
|
+
const entry = applyTo(name);
|
|
161
|
+
if (entry) names.add(entry.name);
|
|
162
|
+
}
|
|
163
|
+
return names;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** True when a catalog row applies to the detected stack. */
|
|
167
|
+
function rowApplies(row, detected, detectedTools) {
|
|
168
|
+
if (row.applies_when === 'always') return true;
|
|
169
|
+
if (!detected || typeof detected !== 'object') return false;
|
|
170
|
+
if (detectedTools && detectedTools.has(row.name)) return true;
|
|
171
|
+
const deps = new Set(
|
|
172
|
+
[detected.projectType, detected.framework, detected.orm]
|
|
173
|
+
.filter((x) => typeof x === 'string')
|
|
174
|
+
.map((x) => x.toLowerCase())
|
|
175
|
+
);
|
|
176
|
+
for (const alt of row.applies_when.split('|')) {
|
|
177
|
+
for (const tok of alt.split(',')) {
|
|
178
|
+
const t = tok.trim();
|
|
179
|
+
if (t && deps.has(t)) return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generation-time severity precedence: env CLC_FORGE_SEVERITIES > config
|
|
187
|
+
* severities > catalog default > 'error'. (Guard-result still wins at runtime
|
|
188
|
+
* audit — slice C.)
|
|
189
|
+
*/
|
|
190
|
+
function resolveSeverity(name, configSeverities, envMap) {
|
|
191
|
+
if (envMap[name] && VALID_SEVERITIES.includes(envMap[name])) return envMap[name];
|
|
192
|
+
if (configSeverities[name] && VALID_SEVERITIES.includes(configSeverities[name])) return configSeverities[name];
|
|
193
|
+
const cat = severityFor(name);
|
|
194
|
+
if (cat && VALID_SEVERITIES.includes(cat)) return cat;
|
|
195
|
+
return 'error';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Effective resolved config (catalog defaults merged with overrides).
|
|
200
|
+
* @param {object} raw - validated raw config.
|
|
201
|
+
* @param {object} [detected] - autoDetectStack output (optional; {} when unknown).
|
|
202
|
+
*/
|
|
203
|
+
function resolveConfig(raw, detected) {
|
|
204
|
+
const validated = validateConfig(raw);
|
|
205
|
+
const gateMode = validated.gate_mode === undefined ? 'hard' : validated.gate_mode;
|
|
206
|
+
const phases = validated.phases === undefined ? HARNESS_PHASES.slice() : validated.phases;
|
|
207
|
+
const detectedTools = detectedToolNames(detected);
|
|
208
|
+
const base = SAFEGUARD_CATALOG.filter((row) => rowApplies(row, detected, detectedTools));
|
|
209
|
+
const active =
|
|
210
|
+
Array.isArray(validated.active_guards) && validated.active_guards.length
|
|
211
|
+
? validated.active_guards
|
|
212
|
+
: base.map((row) => row.name);
|
|
213
|
+
const configSeverities = validated.severities || {};
|
|
214
|
+
const envMap = envSeverities();
|
|
215
|
+
|
|
216
|
+
const effectiveSeverities = {};
|
|
217
|
+
const __catalog = active.map((name) => {
|
|
218
|
+
const entry = applyTo(name);
|
|
219
|
+
const severity = resolveSeverity(name, configSeverities, envMap);
|
|
220
|
+
effectiveSeverities[name] = severity;
|
|
221
|
+
return {
|
|
222
|
+
name,
|
|
223
|
+
checks: entry ? entry.checks : undefined,
|
|
224
|
+
applies_when: entry ? entry.applies_when : undefined,
|
|
225
|
+
severity,
|
|
226
|
+
gate_mode: gateMode === 'advisory' ? 'advisory' : entry ? entry.gate_mode : 'hard',
|
|
227
|
+
file: entry ? entry.file : null,
|
|
228
|
+
manifest: entry ? entry.manifest : false,
|
|
229
|
+
planned: entry ? entry.planned : false,
|
|
230
|
+
config_keys: entry ? entry.config_keys : [],
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
activeGuards: active,
|
|
236
|
+
phases,
|
|
237
|
+
gateMode,
|
|
238
|
+
effectiveSeverities,
|
|
239
|
+
layers: validated.layers || [],
|
|
240
|
+
scope: validated.scope === undefined ? DEFAULT_SCOPE : validated.scope,
|
|
241
|
+
excludePaths: validated.exclude_paths || [],
|
|
242
|
+
framework: detected && detected.framework ? detected.framework : 'unknown',
|
|
243
|
+
projectType: detected && detected.projectType ? detected.projectType : 'unknown',
|
|
244
|
+
testRunner: detected && detected.testRunner ? detected.testRunner : 'unknown',
|
|
245
|
+
rules: validated.rules || [],
|
|
246
|
+
__catalog,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { loadConfig, validateConfig, resolveConfig, ConfigError, HARNESS_PHASES, ENV_SEVERITIES, DEFAULT_SCOPE };
|
package/src/detector.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Universal Auto-Discovery Engine
|
|
3
|
+
* Inspects repository signatures to auto-match the right polyglot Stack Adapter
|
|
4
|
+
* (Next.js, FastAPI, Django, Astro, Rails, Go, Rust, Laravel).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const NextjsAdapter = require('./adapters/nextjs');
|
|
11
|
+
const FastApiAdapter = require('./adapters/fastapi');
|
|
12
|
+
const DjangoAdapter = require('./adapters/django');
|
|
13
|
+
const AstroAdapter = require('./adapters/astro');
|
|
14
|
+
const RailsAdapter = require('./adapters/rails');
|
|
15
|
+
const GoAdapter = require('./adapters/go');
|
|
16
|
+
const RustAdapter = require('./adapters/rust');
|
|
17
|
+
const LaravelAdapter = require('./adapters/laravel');
|
|
18
|
+
const { TechnologyDetector } = require('./technologies/detector');
|
|
19
|
+
|
|
20
|
+
const adapters = [
|
|
21
|
+
new NextjsAdapter(),
|
|
22
|
+
new FastApiAdapter(),
|
|
23
|
+
new DjangoAdapter(),
|
|
24
|
+
new AstroAdapter(),
|
|
25
|
+
new RailsAdapter(),
|
|
26
|
+
new GoAdapter(),
|
|
27
|
+
new RustAdapter(),
|
|
28
|
+
new LaravelAdapter()
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function autoDetectStack(targetDir) {
|
|
32
|
+
// 1. Check registered polyglot adapters
|
|
33
|
+
for (const adapter of adapters) {
|
|
34
|
+
if (adapter.detect(targetDir)) {
|
|
35
|
+
// Detect Phase-1 technologies (Celery, Redis, PostgreSQL, Docker)
|
|
36
|
+
const techDetector = new TechnologyDetector();
|
|
37
|
+
const detectedTechs = techDetector.detect(targetDir);
|
|
38
|
+
const techTools = techDetector.getToolsForTech(detectedTechs);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
projectType: adapter.projectType,
|
|
42
|
+
framework: adapter.name,
|
|
43
|
+
styling: adapter.projectType === 'frontend' ? 'Tailwind CSS v4' : 'N/A',
|
|
44
|
+
uiLibrary: adapter.projectType === 'frontend' ? 'Component Primitives' : 'N/A',
|
|
45
|
+
testRunner: adapter.testRunner,
|
|
46
|
+
gitHooks: fs.existsSync(path.join(targetDir, '.husky')) ? 'husky' : 'githooks',
|
|
47
|
+
orm: 'Framework Native',
|
|
48
|
+
adapter,
|
|
49
|
+
techTools
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Fallback detection
|
|
55
|
+
const techDetector = new TechnologyDetector();
|
|
56
|
+
const detectedTechs = techDetector.detect(targetDir);
|
|
57
|
+
const techTools = techDetector.getToolsForTech(detectedTechs);
|
|
58
|
+
|
|
59
|
+
const config = {
|
|
60
|
+
projectType: 'unknown',
|
|
61
|
+
framework: 'unknown',
|
|
62
|
+
styling: 'none',
|
|
63
|
+
uiLibrary: 'none',
|
|
64
|
+
testRunner: 'unknown',
|
|
65
|
+
gitHooks: fs.existsSync(path.join(targetDir, '.husky')) ? 'husky' : 'githooks',
|
|
66
|
+
orm: 'none',
|
|
67
|
+
adapter: null,
|
|
68
|
+
techTools
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(path.join(targetDir, 'src', 'app')) || fs.existsSync(path.join(targetDir, 'src', 'pages'))) {
|
|
72
|
+
config.projectType = 'frontend';
|
|
73
|
+
config.framework = 'Next.js';
|
|
74
|
+
config.testRunner = 'Vitest';
|
|
75
|
+
config.styling = 'Tailwind CSS v4';
|
|
76
|
+
} else {
|
|
77
|
+
config.projectType = 'backend';
|
|
78
|
+
config.framework = 'FastAPI';
|
|
79
|
+
config.testRunner = 'Pytest';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { autoDetectStack };
|