clckernel 1.2.18 → 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.
Files changed (78) hide show
  1. package/.agents/skills/clckernel_cli/SKILL.md +23 -6
  2. package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
  3. package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
  4. package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
  5. package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
  6. package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
  7. package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
  8. package/.agents/skills/clckernel_cli/reference/start.md +14 -4
  9. package/.agents/skills/clckernel_cli/reference/test.md +1 -1
  10. package/.agents/skills/clckernel_cli/reference/update.md +19 -19
  11. package/CHANGELOG.md +244 -0
  12. package/LICENSE +21 -0
  13. package/README.es.md +280 -51
  14. package/README.md +274 -50
  15. package/bin/cli.js +8 -0
  16. package/package.json +12 -2
  17. package/src/adapters/agent-os.js +161 -84
  18. package/src/adapters/astro.js +8 -6
  19. package/src/adapters/base.js +27 -46
  20. package/src/adapters/django.js +14 -13
  21. package/src/adapters/fastapi.js +16 -15
  22. package/src/adapters/go.js +11 -9
  23. package/src/adapters/laravel.js +12 -10
  24. package/src/adapters/nextjs.js +7 -5
  25. package/src/adapters/rails.js +13 -11
  26. package/src/adapters/rust.js +11 -9
  27. package/src/catalog.js +58 -49
  28. package/src/config.js +38 -240
  29. package/src/detector.js +243 -44
  30. package/src/doctor.js +18 -9
  31. package/src/generator.js +737 -309
  32. package/src/gitignore.js +144 -0
  33. package/src/index.js +257 -18
  34. package/src/io.js +409 -0
  35. package/src/known-hashes.json +309 -0
  36. package/src/onboarding.js +15 -0
  37. package/src/technologies/detector.js +1 -1
  38. package/src/technologies/guards/_shared.js +1 -1
  39. package/src/ui.js +233 -40
  40. package/src/updater.js +271 -36
  41. package/tools/_changes.js +279 -0
  42. package/tools/_changes.py +233 -0
  43. package/tools/_config.js +294 -0
  44. package/tools/_config.py +271 -0
  45. package/tools/_yaml.py +489 -0
  46. package/tools/audit.js +511 -111
  47. package/tools/audit.py +506 -128
  48. package/tools/celery_guard.py +13 -35
  49. package/tools/check_a11y.js +8 -28
  50. package/tools/check_api_contracts.js +13 -39
  51. package/tools/check_architecture.js +35 -44
  52. package/tools/check_architecture.py +172 -105
  53. package/tools/check_custom.js +108 -61
  54. package/tools/check_custom.py +108 -271
  55. package/tools/check_data_files.js +88 -0
  56. package/tools/check_db_efficiency.py +80 -65
  57. package/tools/check_hitl.js +112 -69
  58. package/tools/check_links.js +138 -0
  59. package/tools/check_migrations.py +143 -94
  60. package/tools/check_performance.js +35 -44
  61. package/tools/check_placeholders.js +104 -0
  62. package/tools/check_responsive.js +8 -28
  63. package/tools/check_scope.py +40 -31
  64. package/tools/check_seo.js +8 -28
  65. package/tools/check_skills.js +158 -0
  66. package/tools/check_storybook.js +8 -21
  67. package/tools/check_symlinks.js +40 -22
  68. package/tools/check_ui_reuse.js +17 -29
  69. package/tools/docker_guard.py +19 -41
  70. package/tools/postgres_guard.py +15 -36
  71. package/tools/redis_guard.py +13 -35
  72. package/tools/scan_secrets.js +134 -67
  73. package/tools/scan_secrets.py +163 -72
  74. package/tools/verify_tdd.js +867 -0
  75. package/tools/verify_tdd.py +407 -165
  76. package/tools/yaml.js +404 -132
  77. package/tools/check_domain_data.js +0 -100
  78. package/tools/check_test_calendar_sync.js +0 -111
@@ -1,47 +1,71 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Custom Rules Guard
4
- * Reads .clc-forge.yml and executes user-defined regex rules against changed files.
4
+ * Reads .clckernel.yaml and executes user-defined regex rules against changed files.
5
5
  * Self-contained: uses only Node.js built-ins (inline YAML parser, no npm deps).
6
6
  *
7
7
  * @param {string} targetDir - Directory to scan (defaults to cwd)
8
8
  * @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
9
9
  */
10
10
  const { parseYaml, globToRegex } = require('./yaml.js');
11
+ const { changedFiles, readFile } = require('./_changes');
12
+
13
+ // Lookup order: the canonical name first, then older names kept for compatibility.
14
+ const CONFIG_NAMES = ['.clckernel.yaml', '.clckernel.yml', '.clc-forge.yaml', '.clc-forge.yml'];
15
+ const CANONICAL_CONFIG = CONFIG_NAMES[0];
16
+ const DEPRECATED_PREFIX = '.clc-forge.';
17
+
11
18
  const check = async function checkCustom(targetDir) {
12
19
  const fs = require('fs');
13
20
  const path = require('path');
14
- const { execSync } = require('child_process');
15
21
 
16
22
  const dir = targetDir || process.cwd();
17
23
 
18
24
  // ── 1. Load config ──────────────────────────────────────────────────
19
- const candidates = ['.clckernel.yml', '.clckernel.yaml', '.clc-forge.yml', '.clc-forge.yaml'];
20
- const configPath = candidates.map(c => path.join(dir, c)).find(p => fs.existsSync(p));
25
+ const configPath = CONFIG_NAMES.map(c => path.join(dir, c)).find(p => fs.existsSync(p));
21
26
  if (!configPath) {
22
27
  return {
23
28
  exitCode: 0, skipped: true, name: 'check_custom',
24
- data: { violations: {}, message: 'No .clckernel.yml or .clc-forge.yml found' },
29
+ data: { violations: {}, message: `No ${CANONICAL_CONFIG} found` },
25
30
  };
26
31
  }
27
32
 
33
+ const configName = path.basename(configPath);
34
+ const result = runRules(dir, configPath, configName);
35
+ if (configName.startsWith(DEPRECATED_PREFIX)) {
36
+ result.data.warnings = [`${configName}: deprecated config name, rename to ${CANONICAL_CONFIG}`];
37
+ }
38
+ return result;
39
+ };
40
+ module.exports = check;
41
+
42
+ /**
43
+ * Validate the rules in one config file and run them against changed files.
44
+ * @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
45
+ */
46
+ function runRules(dir, configPath, configName) {
47
+ const fs = require('fs');
48
+
49
+ const configError = (message) => ({
50
+ exitCode: 1, skipped: false, name: 'check_custom',
51
+ data: { violations: {}, message: `Config error in ${configName}: ${message}` },
52
+ });
53
+
28
54
  let config;
29
55
  try {
30
56
  const raw = fs.readFileSync(configPath, 'utf-8');
31
57
  config = parseYaml(raw);
32
58
  } catch (e) {
59
+ // Fail closed: rules we cannot read must not turn into a silent PASS.
33
60
  return {
34
- exitCode: 0, skipped: true, name: 'check_custom',
35
- data: { violations: {}, message: `Config parse error: ${e.message}` },
61
+ exitCode: 1, skipped: false, name: 'check_custom',
62
+ data: { violations: {}, message: `Config parse error in ${configName}: ${e.message}` },
36
63
  };
37
64
  }
38
65
 
39
66
  const rules = config && config.rules;
40
- if (rules !== undefined && !Array.isArray(rules)) {
41
- return {
42
- exitCode: 0, skipped: true, name: 'check_custom',
43
- data: { violations: {}, message: 'Config error: "rules" must be a list' },
44
- };
67
+ if (rules !== undefined && rules !== null && !Array.isArray(rules)) {
68
+ return configError('"rules" must be a list');
45
69
  }
46
70
  if (!Array.isArray(rules) || rules.length === 0) {
47
71
  return {
@@ -50,54 +74,47 @@ const check = async function checkCustom(targetDir) {
50
74
  };
51
75
  }
52
76
 
53
- // ── 2. Validate rules: compile regexes, check required fields ───────
77
+ // ── 2. Validate rules: shape and regex; any invalid rule fails the guard ──
54
78
  const validated = [];
55
- for (const rule of rules) {
56
- if (!rule.name || !rule.pattern || !rule.message) continue;
79
+ const errors = [];
80
+ rules.forEach((rule, i) => {
81
+ const problems = validateRule(rule);
82
+ const label = rule && typeof rule.name === 'string' ? `"${rule.name}"` : `#${i + 1}`;
83
+ if (problems.length > 0) {
84
+ errors.push(`rule ${label}: ${problems.join(', ')}`);
85
+ return;
86
+ }
87
+ let regex;
57
88
  try {
58
- validated.push({
59
- name: rule.name,
60
- pattern: rule.pattern,
61
- message: rule.message,
62
- severity: rule.severity || 'error',
63
- exclude: rule.exclude || [],
64
- regex: new RegExp(rule.pattern, 'gm'),
65
- scopeRe: globToRegex(rule.scope || '**/*'),
66
- excludeRe: (rule.exclude || []).map(globToRegex),
67
- });
68
- } catch {
69
- // Invalid regex — skip this rule silently
89
+ regex = new RegExp(rule.pattern, 'gm');
90
+ } catch (e) {
91
+ errors.push(`rule ${label}: invalid pattern: ${e.message}`);
92
+ return;
70
93
  }
71
- }
94
+ validated.push({
95
+ name: rule.name,
96
+ pattern: rule.pattern,
97
+ message: rule.message,
98
+ severity: rule.severity || 'error',
99
+ exclude: rule.exclude || [],
100
+ regex,
101
+ scopeRe: globToRegex(rule.scope || '**/*'),
102
+ excludeRe: (rule.exclude || []).map(globToRegex),
103
+ });
104
+ });
72
105
 
73
- if (validated.length === 0) {
106
+ if (errors.length > 0) return configError(errors.join('; '));
107
+
108
+ // ── 3. Changed files of the snapshot being verified ──────────────
109
+ // The index, or HEAD when nothing is staged; never the working copy.
110
+ const { mode, files } = changedFiles(dir);
111
+ if (mode === null) {
74
112
  return {
75
- exitCode: 0, skipped: false, name: 'check_custom',
76
- data: { violations: {}, message: 'No valid custom rules after validation' },
113
+ exitCode: 0, skipped: true, name: 'check_custom',
114
+ data: { violations: {}, message: 'No git diff available' },
77
115
  };
78
116
  }
79
117
 
80
- // ── 3. Get changed files via git diff ───────────────────────────────
81
- let files;
82
- try {
83
- const out = execSync('git diff --name-only HEAD~1', {
84
- cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
85
- });
86
- files = out.split('\n').filter(f => f.trim());
87
- } catch {
88
- try {
89
- const out = execSync('git diff --name-only --cached', {
90
- cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
91
- });
92
- files = out.split('\n').filter(f => f.trim());
93
- } catch {
94
- return {
95
- exitCode: 0, skipped: true, name: 'check_custom',
96
- data: { violations: {}, message: 'No git diff available' },
97
- };
98
- }
99
- }
100
-
101
118
  if (!files || files.length === 0) {
102
119
  return {
103
120
  exitCode: 0, skipped: false, name: 'check_custom',
@@ -110,13 +127,8 @@ const check = async function checkCustom(targetDir) {
110
127
  let hasError = false;
111
128
 
112
129
  for (const file of files) {
113
- const filePath = path.join(dir, file);
114
- let content;
115
- try {
116
- content = fs.readFileSync(filePath, 'utf-8');
117
- } catch {
118
- continue; // permission denied or unreadable — skip
119
- }
130
+ const content = readFile(dir, file, mode);
131
+ if (content === null) continue; // missing or binary in the snapshot — skip
120
132
 
121
133
  for (const rule of validated) {
122
134
  if (!rule.scopeRe.test(file)) continue;
@@ -125,6 +137,9 @@ const check = async function checkCustom(targetDir) {
125
137
  rule.regex.lastIndex = 0;
126
138
  let match;
127
139
  while ((match = rule.regex.exec(content)) !== null) {
140
+ // An empty match does not move lastIndex: step over one code point
141
+ // (never between surrogates) so the loop ends, like Python's finditer.
142
+ if (match[0] === '') rule.regex.lastIndex = nextCodePointIndex(content, rule.regex.lastIndex);
128
143
  const lineNo = content.slice(0, match.index).split('\n').length;
129
144
  if (!violations[file]) violations[file] = [];
130
145
  violations[file].push({
@@ -151,11 +166,43 @@ const check = async function checkCustom(targetDir) {
151
166
  : 'No custom rule violations',
152
167
  },
153
168
  };
154
- };
155
- module.exports = check;
169
+ }
170
+
171
+ /**
172
+ * Index just past the code point at `i` (two UTF-16 units for a surrogate pair).
173
+ * @param {string} s
174
+ * @param {number} i
175
+ * @returns {number}
176
+ */
177
+ function nextCodePointIndex(s, i) {
178
+ const code = s.codePointAt(i);
179
+ return i + (code !== undefined && code > 0xffff ? 2 : 1);
180
+ }
181
+
182
+ /**
183
+ * Shape problems of one rule (empty list when valid).
184
+ * @param {*} rule
185
+ * @returns {string[]}
186
+ */
187
+ function validateRule(rule) {
188
+ if (!rule || typeof rule !== 'object' || Array.isArray(rule)) return ['must be a mapping'];
189
+ const problems = [];
190
+ for (const field of ['name', 'pattern', 'message']) {
191
+ if (typeof rule[field] !== 'string' || rule[field] === '') problems.push(`"${field}" must be a non-empty string`);
192
+ }
193
+ if (rule.severity != null && !['error', 'warning'].includes(rule.severity)) {
194
+ problems.push('"severity" must be "error" or "warning"');
195
+ }
196
+ if (rule.scope != null && typeof rule.scope !== 'string') problems.push('"scope" must be a string');
197
+ if (rule.exclude != null && !(Array.isArray(rule.exclude) && rule.exclude.every(e => typeof e === 'string'))) {
198
+ problems.push('"exclude" must be a list of strings');
199
+ }
200
+ return problems;
201
+ }
156
202
 
157
203
  if (require.main === module) {
158
204
  check(process.argv[2]).then(result => {
205
+ for (const warning of result.data.warnings || []) console.error(`[check_custom] warning: ${warning}`);
159
206
  console.log(JSON.stringify(result, null, 2));
160
207
  process.exit(result.exitCode);
161
208
  });