@alex-dbs/coderuler 0.0.0-stage → 1.3.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 (121) hide show
  1. package/LICENSE +175 -0
  2. package/README.md +218 -2
  3. package/dist/adapter/cli/commands/check.d.ts +3 -0
  4. package/dist/adapter/cli/commands/check.js +93 -0
  5. package/dist/adapter/cli/commands/deps.d.ts +3 -0
  6. package/dist/adapter/cli/commands/deps.js +4 -0
  7. package/dist/adapter/cli/commands/inspect.d.ts +3 -0
  8. package/dist/adapter/cli/commands/inspect.js +59 -0
  9. package/dist/adapter/cli/commands/rules.d.ts +10 -0
  10. package/dist/adapter/cli/commands/rules.js +17 -0
  11. package/dist/adapter/cli/commands/skill.d.ts +4 -0
  12. package/dist/adapter/cli/commands/skill.js +7 -0
  13. package/dist/adapter/cli/commands/test.d.ts +3 -0
  14. package/dist/adapter/cli/commands/test.js +78 -0
  15. package/dist/adapter/cli/commands/update.d.ts +3 -0
  16. package/dist/adapter/cli/commands/update.js +7 -0
  17. package/dist/adapter/cli/commands/validate.d.ts +3 -0
  18. package/dist/adapter/cli/commands/validate.js +12 -0
  19. package/dist/adapter/cli/exit.d.ts +7 -0
  20. package/dist/adapter/cli/exit.js +10 -0
  21. package/dist/adapter/cli/inspect-json.d.ts +9 -0
  22. package/dist/adapter/cli/inspect-json.js +160 -0
  23. package/dist/adapter/cli/inspect-usage.d.ts +9 -0
  24. package/dist/adapter/cli/inspect-usage.js +14 -0
  25. package/dist/adapter/cli/program.d.ts +4 -0
  26. package/dist/adapter/cli/program.js +216 -0
  27. package/dist/adapter/cli/progress.d.ts +22 -0
  28. package/dist/adapter/cli/progress.js +79 -0
  29. package/dist/adapter/cli/reports.d.ts +47 -0
  30. package/dist/adapter/cli/reports.js +735 -0
  31. package/dist/adapter/cli/self-update.d.ts +1 -0
  32. package/dist/adapter/cli/self-update.js +89 -0
  33. package/dist/adapter/cli/services.d.ts +101 -0
  34. package/dist/adapter/cli/services.js +1 -0
  35. package/dist/adapter/cli/text.d.ts +5 -0
  36. package/dist/adapter/cli/text.js +36 -0
  37. package/dist/adapter/cli/version.d.ts +1 -0
  38. package/dist/adapter/cli/version.js +3 -0
  39. package/dist/adapter/decision/jev.d.ts +11 -0
  40. package/dist/adapter/decision/jev.js +138 -0
  41. package/dist/adapter/filesystem/dependencies.d.ts +26 -0
  42. package/dist/adapter/filesystem/dependencies.js +577 -0
  43. package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
  44. package/dist/adapter/filesystem/dependency-installation.js +373 -0
  45. package/dist/adapter/filesystem/project.d.ts +14 -0
  46. package/dist/adapter/filesystem/project.js +387 -0
  47. package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
  48. package/dist/adapter/filesystem/rule-resolution.js +516 -0
  49. package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
  50. package/dist/adapter/filesystem/rule-test-worker.js +47 -0
  51. package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
  52. package/dist/adapter/filesystem/rule-tests.js +264 -0
  53. package/dist/adapter/filesystem/rules.d.ts +13 -0
  54. package/dist/adapter/filesystem/rules.js +242 -0
  55. package/dist/adapter/git/changes.d.ts +1 -0
  56. package/dist/adapter/git/changes.js +54 -0
  57. package/dist/adapter/git/environment.d.ts +1 -0
  58. package/dist/adapter/git/environment.js +10 -0
  59. package/dist/adapter/github/api.d.ts +5 -0
  60. package/dist/adapter/github/api.js +40 -0
  61. package/dist/adapter/github/comments.d.ts +3 -0
  62. package/dist/adapter/github/comments.js +235 -0
  63. package/dist/adapter/github/pull-request.d.ts +16 -0
  64. package/dist/adapter/github/pull-request.js +139 -0
  65. package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
  66. package/dist/adapter/scope/astgrep-languages.js +119 -0
  67. package/dist/adapter/scope/astgrep.d.ts +14 -0
  68. package/dist/adapter/scope/astgrep.js +183 -0
  69. package/dist/adapter/scope/csv.d.ts +4 -0
  70. package/dist/adapter/scope/csv.js +158 -0
  71. package/dist/adapter/scope/file.d.ts +2 -0
  72. package/dist/adapter/scope/file.js +4 -0
  73. package/dist/adapter/scope/json.d.ts +7 -0
  74. package/dist/adapter/scope/json.js +132 -0
  75. package/dist/adapter/scope/lines.d.ts +10 -0
  76. package/dist/adapter/scope/lines.js +42 -0
  77. package/dist/adapter/scope/protobuf-options.d.ts +29 -0
  78. package/dist/adapter/scope/protobuf-options.js +64 -0
  79. package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
  80. package/dist/adapter/scope/protobuf-parser.js +772 -0
  81. package/dist/adapter/scope/protobuf.d.ts +5 -0
  82. package/dist/adapter/scope/protobuf.js +53 -0
  83. package/dist/adapter/scope/structured.d.ts +10 -0
  84. package/dist/adapter/scope/structured.js +84 -0
  85. package/dist/adapter/scope/yaml.d.ts +7 -0
  86. package/dist/adapter/scope/yaml.js +224 -0
  87. package/dist/adapter/skills/cli.d.ts +10 -0
  88. package/dist/adapter/skills/cli.js +77 -0
  89. package/dist/app/concurrency.d.ts +1 -0
  90. package/dist/app/concurrency.js +44 -0
  91. package/dist/app/evaluators.d.ts +14 -0
  92. package/dist/app/evaluators.js +175 -0
  93. package/dist/app/execute.d.ts +2 -0
  94. package/dist/app/execute.js +427 -0
  95. package/dist/app/jsonpath.d.ts +1 -0
  96. package/dist/app/jsonpath.js +8 -0
  97. package/dist/app/ports.d.ts +165 -0
  98. package/dist/app/ports.js +1 -0
  99. package/dist/app/rule-tests.d.ts +20 -0
  100. package/dist/app/rule-tests.js +11 -0
  101. package/dist/app/rule.d.ts +121 -0
  102. package/dist/app/rule.js +24 -0
  103. package/dist/app/selection.d.ts +2 -0
  104. package/dist/app/selection.js +23 -0
  105. package/dist/app/usage.d.ts +2 -0
  106. package/dist/app/usage.js +13 -0
  107. package/dist/app/validate.d.ts +11 -0
  108. package/dist/app/validate.js +415 -0
  109. package/dist/app/violation-comments.d.ts +10 -0
  110. package/dist/app/violation-comments.js +1 -0
  111. package/dist/cli.d.ts +2 -0
  112. package/dist/cli.js +3 -0
  113. package/dist/index.d.ts +22 -0
  114. package/dist/index.js +11 -0
  115. package/dist/infra/runtime.d.ts +3 -0
  116. package/dist/infra/runtime.js +168 -0
  117. package/dist/infra/settings.d.ts +5 -0
  118. package/dist/infra/settings.js +91 -0
  119. package/package.json +86 -4
  120. package/skills/coderuler-new-rule/SKILL.md +141 -0
  121. package/skills/coderuler-new-rule/reference.md +447 -0
@@ -0,0 +1,516 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, mkdtempSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
2
+ import { createRequire, isBuiltin, registerHooks } from 'node:module';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import npa from 'npm-package-arg';
7
+ import { ruleDependencyLockKey } from './dependencies.js';
8
+ const SDK = '@alex-dbs/coderuler';
9
+ const SDK_ENTRY = new URL(import.meta.url.endsWith('.ts') ? '../../index.ts' : '../../index.js', import.meta.url).href;
10
+ // This prevents accidental ambient dependency borrowing, not deliberate escape:
11
+ // trusted rule code can still use Node APIs, absolute paths, or custom loaders.
12
+ const contexts = new Map();
13
+ const commonJSDirectories = new Map();
14
+ const sdkModules = new Set();
15
+ const requireFromHere = createRequire(import.meta.url);
16
+ const ownerKeys = new Map();
17
+ const installations = new Map();
18
+ const importScopes = new Map();
19
+ const temporaryScopes = new Set();
20
+ function record(value, label) {
21
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
22
+ throw new Error(`Invalid ${label}`);
23
+ return value;
24
+ }
25
+ function json(path) {
26
+ return record(JSON.parse(readFileSync(path, 'utf8')), path);
27
+ }
28
+ function inside(root, filename) {
29
+ const path = relative(root, filename);
30
+ return path === '' || (!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`));
31
+ }
32
+ function filename(url) {
33
+ if (!url?.startsWith('file:'))
34
+ return undefined;
35
+ const path = fileURLToPath(url);
36
+ try {
37
+ return realpathSync(path);
38
+ }
39
+ catch (error) {
40
+ // createRequire's synthetic parent need not exist on disk.
41
+ if (error.code === 'ENOENT')
42
+ return path;
43
+ throw error;
44
+ }
45
+ }
46
+ function nameOf(specifier) {
47
+ return specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0];
48
+ }
49
+ function bare(specifier) {
50
+ return !specifier.startsWith('.') && !specifier.startsWith('#') && !isAbsolute(specifier) && !/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
51
+ }
52
+ function contextKey(dependencies) {
53
+ return `${dependencies.directory}\0${dependencies.key}`;
54
+ }
55
+ function stale(dependencies, detail) {
56
+ return new Error(`Rule dependencies changed or installation is in progress in ${dependencies.directory}: ${detail}; rerun the command after coderuler deps sync completes. If the write mutex is stale, remove it only after confirming its owner exited`);
57
+ }
58
+ function regularText(path) {
59
+ if (!lstatSync(path).isFile())
60
+ throw new Error(`Not a regular, non-symlinked file: ${path}`);
61
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
62
+ try {
63
+ if (!fstatSync(fd).isFile())
64
+ throw new Error(`Not a regular file: ${path}`);
65
+ return readFileSync(fd, 'utf8');
66
+ }
67
+ finally {
68
+ closeSync(fd);
69
+ }
70
+ }
71
+ function assertIdle(dependencies) {
72
+ const mutex = join(dependencies.directory, '.coderuler-deps-write');
73
+ try {
74
+ lstatSync(mutex);
75
+ }
76
+ catch (error) {
77
+ if (error.code === 'ENOENT')
78
+ return;
79
+ throw stale(dependencies, mutex);
80
+ }
81
+ throw stale(dependencies, mutex);
82
+ }
83
+ function snapshot(dependencies) {
84
+ const registeredKey = ownerKeys.get(dependencies.directory);
85
+ if (registeredKey !== undefined && registeredKey !== dependencies.key)
86
+ throw stale(dependencies, 'this process already loaded a different dependency snapshot for this owner');
87
+ assertIdle(dependencies);
88
+ try {
89
+ let text = null;
90
+ try {
91
+ text = regularText(join(dependencies.directory, 'coderuler.lock'));
92
+ }
93
+ catch (error) {
94
+ if (error.code !== 'ENOENT')
95
+ throw error;
96
+ }
97
+ const lock = text === null ? null : record(JSON.parse(text), 'rule dependency lock');
98
+ if (ruleDependencyLockKey(lock, dependencies.directory) !== dependencies.key)
99
+ throw new Error('committed lock no longer matches the prepared context');
100
+ if (dependencies.key === 'empty') {
101
+ if (dependencies.directDependencies.length !== 0)
102
+ throw new Error('empty context has declared packages');
103
+ assertIdle(dependencies);
104
+ return undefined;
105
+ }
106
+ const packages = record(lock['packages'], 'rule dependency lock packages');
107
+ const direct = Object.keys(record(record(packages[''], 'root package')['dependencies'] ?? {}, 'direct dependencies')).sort();
108
+ if (JSON.stringify(direct) !== JSON.stringify([...dependencies.directDependencies].sort()))
109
+ throw new Error('direct dependencies no longer match');
110
+ const modules = join(dependencies.directory, 'node_modules');
111
+ if (!lstatSync(modules).isDirectory() || realpathSync(modules) !== modules)
112
+ throw new Error(`invalid local package directory ${modules}`);
113
+ const marker = record(JSON.parse(regularText(join(dependencies.directory, 'node_modules', '.coderuler-install.json'))), 'rule dependency installation marker');
114
+ if (marker['version'] !== 1 || marker['key'] !== dependencies.key || !Array.isArray(marker['installedLocations']))
115
+ throw new Error('local installation marker no longer matches');
116
+ if (direct.some(name => !marker['installedLocations'].includes(`node_modules/${name}`)))
117
+ throw new Error('local installation omits a direct dependency');
118
+ const installed = installations.get(contextKey(dependencies));
119
+ if (installed !== undefined && installed.marker !== JSON.stringify(marker))
120
+ throw new Error('local installation metadata changed during this command');
121
+ assertIdle(dependencies);
122
+ return { lock: lock, marker };
123
+ }
124
+ catch (error) {
125
+ throw stale(dependencies, error instanceof Error ? error.message : String(error));
126
+ }
127
+ }
128
+ export function assertRuleDependenciesCurrent(dependencies) {
129
+ snapshot(dependencies);
130
+ }
131
+ function installationFor(dependencies) {
132
+ const current = snapshot(dependencies);
133
+ if (current === undefined)
134
+ return undefined;
135
+ const key = contextKey(dependencies);
136
+ const existing = installations.get(key);
137
+ if (existing !== undefined)
138
+ return existing;
139
+ const root = join(dependencies.directory, 'node_modules');
140
+ const packages = record(current.lock['packages'], 'rule dependency lock packages');
141
+ const installed = [];
142
+ for (const location of current.marker['installedLocations']) {
143
+ if (typeof location !== 'string' || !/^node_modules\/(?:@[^/]+\/)?[^/]+(?:\/node_modules\/(?:@[^/]+\/)?[^/]+)*$/.test(location)) {
144
+ throw stale(dependencies, 'invalid installed package location');
145
+ }
146
+ const packageRoot = join(dependencies.directory, location);
147
+ if (!inside(root, packageRoot) || realpathSync(packageRoot) !== packageRoot)
148
+ throw stale(dependencies, `invalid package directory ${packageRoot}`);
149
+ const manifest = record(packages[location], `lock package ${location}`);
150
+ const declarations = new Set();
151
+ for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
152
+ if (manifest[field] !== undefined) {
153
+ for (const name of Object.keys(record(manifest[field], `${location} ${field}`)))
154
+ declarations.add(name);
155
+ }
156
+ }
157
+ const segments = location.split('/');
158
+ const last = segments.at(-1);
159
+ const scope = segments.at(-2);
160
+ const name = scope.startsWith('@') ? `${scope}/${last}` : last;
161
+ const actual = json(join(packageRoot, 'package.json'));
162
+ if (actual['name'] !== name || actual['version'] !== manifest['version'])
163
+ throw stale(dependencies, `package ${name} does not match the lock`);
164
+ installed.push({ root: packageRoot, name, declarations });
165
+ }
166
+ installed.sort((left, right) => right.root.length - left.root.length);
167
+ // Node stops package-scope/self-reference lookup at node_modules. Keep the
168
+ // synthetic parent there so an ancestor application's exports cannot shadow
169
+ // a declared dependency; Node still performs normal package/exports lookup.
170
+ const parentURL = pathToFileURL(join(root, '.coderuler-resolve.cjs')).href;
171
+ const installation = { dependencies, marker: JSON.stringify(current.marker), root, packages: installed, require: createRequire(parentURL), parentURL };
172
+ installations.set(key, installation);
173
+ return installation;
174
+ }
175
+ function managedPackage(installation, path) {
176
+ return installation.packages.find(pkg => inside(pkg.root, path));
177
+ }
178
+ function packageContext(path) {
179
+ for (const installation of installations.values()) {
180
+ if (!inside(installation.root, path))
181
+ continue;
182
+ assertRuleDependenciesCurrent(installation.dependencies);
183
+ const pkg = managedPackage(installation, path);
184
+ if (pkg !== undefined)
185
+ return { installation, pkg };
186
+ }
187
+ return undefined;
188
+ }
189
+ function conflict(path) {
190
+ return new Error(`Rule helper ${path} has conflicting dependency contexts; use separate helper directories for rules with different locks`);
191
+ }
192
+ function reserveCommonJS(path, dependencies) {
193
+ const directory = dirname(path);
194
+ const previous = commonJSDirectories.get(directory);
195
+ if (previous !== undefined && previous !== contextKey(dependencies))
196
+ throw conflict(directory);
197
+ for (const cached of Object.keys(requireFromHere.cache)) {
198
+ const physical = filename(pathToFileURL(cached).href);
199
+ if (dirname(physical) !== directory || sdkModules.has(physical))
200
+ continue;
201
+ const owner = contexts.get(physical);
202
+ if (owner === undefined) {
203
+ throw new Error(`Rule helper directory ${directory} contains an already-loaded unowned CommonJS module ${physical}; use a separate helper directory`);
204
+ }
205
+ if (contextKey(owner) !== contextKey(dependencies))
206
+ throw conflict(directory);
207
+ }
208
+ commonJSDirectories.set(directory, contextKey(dependencies));
209
+ }
210
+ // Absolute public SDK entries are supported too (for example a caller-selected
211
+ // SDK copy). Only the public entry is exempted here, not every file in its
212
+ // containing package, which may itself contain authored rules.
213
+ function sdkEntry(path) {
214
+ if (sdkModules.has(path))
215
+ return true;
216
+ if (path === filename(SDK_ENTRY)) {
217
+ sdkModules.add(path);
218
+ return true;
219
+ }
220
+ let directory = dirname(path);
221
+ for (;;) {
222
+ let manifest;
223
+ try {
224
+ manifest = json(join(directory, 'package.json'));
225
+ }
226
+ catch (error) {
227
+ if (error.code !== 'ENOENT')
228
+ throw error;
229
+ const above = dirname(directory);
230
+ if (above === directory)
231
+ return false;
232
+ directory = above;
233
+ continue;
234
+ }
235
+ if (manifest['name'] !== SDK)
236
+ return false;
237
+ const exports = manifest['exports'];
238
+ const targets = [exports !== null && typeof exports === 'object' && !Array.isArray(exports) && '.' in exports ? exports['.'] : exports ?? manifest['main']];
239
+ while (targets.length > 0) {
240
+ const target = targets.pop();
241
+ if (typeof target === 'string' && filename(pathToFileURL(join(directory, target)).href) === path) {
242
+ sdkModules.add(path);
243
+ return true;
244
+ }
245
+ if (target !== null && typeof target === 'object')
246
+ targets.push(...Object.values(target));
247
+ }
248
+ return false;
249
+ }
250
+ }
251
+ function attach(path, dependencies) {
252
+ const previous = contexts.get(path);
253
+ if (previous === undefined && (sdkEntry(path) || packageContext(path) !== undefined))
254
+ return;
255
+ if (previous !== undefined && contextKey(previous) !== contextKey(dependencies))
256
+ throw conflict(path);
257
+ if (previous === undefined && requireFromHere.cache[path] !== undefined) {
258
+ throw new Error(`Rule helper ${path} is an already-loaded unowned CommonJS module; use a separate helper directory`);
259
+ }
260
+ contexts.set(path, dependencies);
261
+ if (path.endsWith('.cjs') || requireFromHere.cache[path] !== undefined)
262
+ reserveCommonJS(path, dependencies);
263
+ }
264
+ export function registerRuleDependencies(ruleFile, dependencies) {
265
+ installationFor(dependencies);
266
+ attach(realpathSync(ruleFile), dependencies);
267
+ ownerKeys.set(dependencies.directory, dependencies.key);
268
+ }
269
+ export function getRuleDependencies(ruleFile) {
270
+ return contexts.get(realpathSync(ruleFile));
271
+ }
272
+ function undeclared(name, dependencies) {
273
+ return new Error(`Rule dependency ${name} is not declared in ${join(dependencies.directory, 'coderuler.lock')}; run coderuler deps add ${name}`);
274
+ }
275
+ function requireCondition(conditions) {
276
+ const requiring = conditions.includes('require');
277
+ if (requiring && conditions.includes('import'))
278
+ throw new Error('Ambiguous rule dependency resolution: both import and require conditions are active');
279
+ return requiring;
280
+ }
281
+ const resolveSdk = (specifier, context, nextResolve) => {
282
+ let result;
283
+ try {
284
+ result = nextResolve(specifier, context);
285
+ }
286
+ catch (error) {
287
+ if (error.code !== 'ERR_MODULE_NOT_FOUND' && error.code !== 'MODULE_NOT_FOUND')
288
+ throw error;
289
+ // CommonJS nextResolve expects a filename, not a file URL. Only missing
290
+ // packages use the running SDK fallback; export-condition errors propagate.
291
+ result = nextResolve(requireCondition(context.conditions) ? fileURLToPath(SDK_ENTRY) : SDK_ENTRY, context);
292
+ }
293
+ const path = filename(result.url);
294
+ if (path !== undefined)
295
+ sdkModules.add(path);
296
+ return result;
297
+ };
298
+ // Node, rather than a second implementation of its imports/conditions algorithm,
299
+ // resolves #imports in a package scope with managed dependencies. Source links
300
+ // canonicalize back to the real helpers without modifying the owner.
301
+ function importScope(parent, installation, dependencies) {
302
+ let directory = dirname(parent);
303
+ let manifest;
304
+ for (;;) {
305
+ try {
306
+ manifest = json(join(directory, 'package.json'));
307
+ break;
308
+ }
309
+ catch (error) {
310
+ if (error.code !== 'ENOENT')
311
+ throw error;
312
+ }
313
+ const above = dirname(directory);
314
+ if (above === directory)
315
+ return undefined;
316
+ directory = above;
317
+ }
318
+ if (manifest['imports'] === undefined)
319
+ return undefined;
320
+ const key = `${directory}\0${contextKey(dependencies)}`;
321
+ const existing = importScopes.get(key);
322
+ if (existing !== undefined)
323
+ return existing;
324
+ // Resolution metadata is process-owned, including for read-only owners.
325
+ const scope = mkdtempSync(join(realpathSync(tmpdir()), 'coderuler-resolution-'));
326
+ temporaryScopes.add(scope);
327
+ try {
328
+ const denied = new Map();
329
+ const sourceRoots = [];
330
+ const ownName = manifest['exports'] == null ? undefined : manifest['name'];
331
+ let targetsDirectory;
332
+ let sdkTarget;
333
+ const restrictTargets = (value) => {
334
+ if (typeof value === 'string') {
335
+ if (value === SDK) {
336
+ // Let Node select this exact public target through its normal
337
+ // conditions/arrays/patterns, then resolve it from the real importer
338
+ // to retain local SDK preference and the source-checkout fallback.
339
+ targetsDirectory ??= mkdtempSync(join(scope, '.coderuler-targets-'));
340
+ if (sdkTarget === undefined) {
341
+ sdkTarget = join(targetsDirectory, 'sdk');
342
+ writeFileSync(sdkTarget, '');
343
+ }
344
+ return `./${relative(scope, sdkTarget).split(sep).join('/')}`;
345
+ }
346
+ const name = nameOf(value);
347
+ if (!bare(value) || isBuiltin(value) || name === ownName || dependencies.directDependencies.includes(name))
348
+ return value;
349
+ try {
350
+ const candidate = name.replaceAll('*', 'coderuler-pattern');
351
+ const parsed = npa(candidate);
352
+ if (parsed.name !== candidate || !parsed.registry || parsed.rawSpec !== '*')
353
+ return value;
354
+ }
355
+ catch {
356
+ return value;
357
+ }
358
+ // Preserve Node's condition/array/pattern selection, but turn an
359
+ // undeclared external target into an identifiable local resolution.
360
+ // It can never reach application, NODE_PATH, or ancestor packages.
361
+ targetsDirectory ??= mkdtempSync(join(scope, '.coderuler-targets-'));
362
+ const marker = join(targetsDirectory, String(denied.size));
363
+ writeFileSync(marker, '');
364
+ denied.set(marker, name);
365
+ return `./${relative(scope, marker).split(sep).join('/')}`;
366
+ }
367
+ if (Array.isArray(value))
368
+ return value.map(restrictTargets);
369
+ if (value !== null && typeof value === 'object') {
370
+ return Object.fromEntries(Object.entries(value).map(([name, target]) => [name, restrictTargets(target)]));
371
+ }
372
+ return value;
373
+ };
374
+ const metadata = {};
375
+ for (const field of ['name', 'type', 'imports', 'exports']) {
376
+ if (manifest[field] !== undefined)
377
+ metadata[field] = field === 'imports' ? restrictTargets(manifest[field]) : manifest[field];
378
+ }
379
+ writeFileSync(join(scope, 'package.json'), JSON.stringify(metadata));
380
+ if (installation === undefined)
381
+ mkdirSync(join(scope, 'node_modules'));
382
+ else
383
+ symlinkSync(installation.root, join(scope, 'node_modules'), 'junction');
384
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
385
+ if (entry.name === 'node_modules' || entry.name === 'package.json')
386
+ continue;
387
+ // Avoid mirroring process-owned resolution storage back into itself.
388
+ if (inside(join(directory, entry.name), scope))
389
+ continue;
390
+ symlinkSync(join(directory, entry.name), join(scope, entry.name), entry.isDirectory() ? 'junction' : 'file');
391
+ sourceRoots.push(filename(pathToFileURL(join(directory, entry.name)).href));
392
+ }
393
+ const parentURL = pathToFileURL(join(scope, '.coderuler-imports.cjs')).href;
394
+ const prepared = {
395
+ parentURL, require: createRequire(parentURL),
396
+ metadata: join(scope, 'package.json'), sourceMetadata: join(directory, 'package.json'),
397
+ denied, sdkTarget, sourceRoots,
398
+ };
399
+ importScopes.set(key, prepared);
400
+ return prepared;
401
+ }
402
+ catch (error) {
403
+ rmSync(scope, { recursive: true, force: true });
404
+ temporaryScopes.delete(scope);
405
+ throw error;
406
+ }
407
+ }
408
+ process.once('exit', () => {
409
+ for (const scope of temporaryScopes) {
410
+ try {
411
+ rmSync(scope, { recursive: true, force: true });
412
+ }
413
+ catch { /* Best-effort process-owned resolution cleanup. */ }
414
+ }
415
+ });
416
+ registerHooks({
417
+ resolve(specifier, context, nextResolve) {
418
+ const parent = filename(context.parentURL);
419
+ const dependencies = parent === undefined || sdkModules.has(parent) ? undefined : contexts.get(parent);
420
+ const owner = parent === undefined || sdkModules.has(parent) ? undefined : packageContext(parent);
421
+ if (dependencies !== undefined)
422
+ assertRuleDependenciesCurrent(dependencies);
423
+ if (isBuiltin(specifier))
424
+ return nextResolve(specifier, context);
425
+ if (specifier === SDK)
426
+ return resolveSdk(specifier, context, nextResolve);
427
+ if (parent === undefined || (dependencies === undefined && owner === undefined))
428
+ return nextResolve(specifier, context);
429
+ const requiring = requireCondition(context.conditions);
430
+ if (owner !== undefined) {
431
+ if (bare(specifier)) {
432
+ const name = nameOf(specifier);
433
+ if (name !== owner.pkg.name && !owner.pkg.declarations.has(name)) {
434
+ throw new Error(`Managed package ${owner.pkg.name} imports undeclared dependency ${name}`);
435
+ }
436
+ }
437
+ const result = nextResolve(specifier, context);
438
+ const target = filename(result.url);
439
+ if (target !== undefined && !inside(owner.installation.root, target)) {
440
+ throw new Error(`Managed package ${owner.pkg.name} resolved ${specifier} outside its rule dependency node_modules`);
441
+ }
442
+ if (specifier.startsWith('#') && target !== undefined) {
443
+ const imported = managedPackage(owner.installation, target);
444
+ if (imported !== undefined && imported.name !== owner.pkg.name && !owner.pkg.declarations.has(imported.name)) {
445
+ throw new Error(`Managed package ${owner.pkg.name} imports undeclared dependency ${imported.name}`);
446
+ }
447
+ }
448
+ return result;
449
+ }
450
+ const prepared = dependencies;
451
+ const installation = installationFor(prepared);
452
+ let result;
453
+ if (bare(specifier)) {
454
+ const name = nameOf(specifier);
455
+ if (!prepared.directDependencies.includes(name) || installation === undefined)
456
+ throw undeclared(name, prepared);
457
+ result = requiring
458
+ ? nextResolve(installation.require.resolve(specifier), context)
459
+ : nextResolve(specifier, { ...context, parentURL: installation.parentURL });
460
+ const target = filename(result.url);
461
+ if (target === undefined || !inside(installation.root, target))
462
+ throw new Error(`Rule dependency ${name} resolved outside its prepared node_modules`);
463
+ return result;
464
+ }
465
+ let scope;
466
+ if (specifier.startsWith('#')) {
467
+ // Node merges nextResolve's context into this hook's context in place.
468
+ // Retain the real importer before resolving through the temporary scope.
469
+ const importerContext = { ...context };
470
+ scope = importScope(parent, installation, prepared);
471
+ result = scope === undefined ? nextResolve(specifier, context) : requiring
472
+ ? nextResolve(scope.require.resolve(specifier), context)
473
+ : nextResolve(specifier, { ...context, parentURL: scope.parentURL });
474
+ const denied = scope === undefined ? undefined : scope.denied.get(filename(result.url));
475
+ if (denied !== undefined)
476
+ throw undeclared(denied, prepared);
477
+ if (scope?.sdkTarget !== undefined && filename(result.url) === scope.sdkTarget) {
478
+ return resolveSdk(SDK, importerContext, nextResolve);
479
+ }
480
+ if (scope !== undefined && filename(result.url) === scope.metadata) {
481
+ result = nextResolve(requiring ? scope.sourceMetadata : pathToFileURL(scope.sourceMetadata).href, context);
482
+ }
483
+ }
484
+ else
485
+ result = nextResolve(specifier, context);
486
+ const target = filename(result.url);
487
+ if (target === undefined)
488
+ return result;
489
+ const pkg = installation === undefined ? undefined : managedPackage(installation, target);
490
+ if (pkg !== undefined) {
491
+ if (specifier.startsWith('#') && !prepared.directDependencies.includes(pkg.name))
492
+ throw undeclared(pkg.name, prepared);
493
+ return result;
494
+ }
495
+ if (scope !== undefined && target !== scope.sourceMetadata && !scope.sourceRoots.some(root => inside(root, target))) {
496
+ throw new Error(`Rule import ${specifier} resolved outside its source helpers and prepared node_modules`);
497
+ }
498
+ attach(target, prepared);
499
+ return result;
500
+ },
501
+ load(url, context, nextLoad) {
502
+ const path = filename(url);
503
+ const dependencies = path === undefined ? undefined : contexts.get(path);
504
+ if (dependencies !== undefined)
505
+ assertRuleDependenciesCurrent(dependencies);
506
+ if (path !== undefined)
507
+ packageContext(path);
508
+ const result = nextLoad(url, context);
509
+ if (dependencies !== undefined)
510
+ assertRuleDependenciesCurrent(dependencies);
511
+ if (path !== undefined && dependencies !== undefined && result.format?.startsWith('commonjs')) {
512
+ reserveCommonJS(path, dependencies);
513
+ }
514
+ return result;
515
+ },
516
+ });
@@ -0,0 +1,17 @@
1
+ import type { RuleTestCase } from '../../app/rule-tests.js';
2
+ import type { PreparedRuleDependencies } from './dependencies.js';
3
+ export interface RuleTestWorkerRequest {
4
+ readonly id: number;
5
+ readonly root: string;
6
+ readonly ruleFile: string;
7
+ readonly files: RuleTestCase['files'];
8
+ readonly ai: boolean;
9
+ readonly dependencies?: PreparedRuleDependencies;
10
+ }
11
+ export type RuleTestWorkerMessage = {
12
+ readonly id: number;
13
+ readonly actual: RuleTestCase['expected'];
14
+ } | {
15
+ readonly id: number;
16
+ readonly error: string;
17
+ };
@@ -0,0 +1,47 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, resolve, sep, win32 } from 'node:path';
3
+ import { parentPort } from 'node:worker_threads';
4
+ import { executeRules } from '../../app/execute.js';
5
+ import { createDecisionResolver } from '../../infra/settings.js';
6
+ import { createProject } from './project.js';
7
+ import { createRuleLoader } from './rules.js';
8
+ function fixturePath(root, filepath) {
9
+ const target = resolve(root, filepath);
10
+ if (isAbsolute(filepath) || win32.isAbsolute(filepath) || !target.startsWith(`${root}${sep}`)) {
11
+ throw new Error(`Fixture path "${filepath}" must be relative and stay inside the test project`);
12
+ }
13
+ return target;
14
+ }
15
+ async function runCase({ root, ruleFile, files, ai, dependencies }) {
16
+ for (const [filepath, source] of Object.entries(files)) {
17
+ const target = fixturePath(root, filepath);
18
+ await mkdir(dirname(target), { recursive: true });
19
+ await writeFile(target, typeof source === 'string' ? source : JSON.stringify(source, null, 2), 'utf8');
20
+ }
21
+ const rules = await createRuleLoader(dependencies === undefined ? {} : { preparedDependencies: dependencies }).load({ root, explicit: ruleFile, invocationCwd: root });
22
+ if (rules.length !== 1)
23
+ throw new Error('Expected exactly one freshly loaded rule');
24
+ const reports = await executeRules(rules, {
25
+ project: createProject(root, { excludeFiles: [ruleFile] }),
26
+ isTarget: () => true,
27
+ concurrency: 1,
28
+ resolveDecision: ai ? createDecisionResolver(process.env) : () => {
29
+ throw new Error('Inline rule tests must not call the decision backend');
30
+ },
31
+ });
32
+ const report = reports[0];
33
+ if (reports.length !== 1 || report === undefined)
34
+ throw new Error('Expected exactly one rule report');
35
+ return report.status;
36
+ }
37
+ const port = parentPort;
38
+ if (port === null)
39
+ throw new Error('Rule test cases must run in a worker');
40
+ port.start();
41
+ port.ref();
42
+ port.on('message', (request) => {
43
+ void runCase(request).then(actual => port.postMessage({ id: request.id, actual }), error => port.postMessage({
44
+ id: request.id,
45
+ error: error instanceof Error ? error.message : String(error),
46
+ }));
47
+ });
@@ -0,0 +1,3 @@
1
+ import type { LoadedRule } from '../../app/ports.js';
2
+ import type { RuleTestResult } from '../../app/rule-tests.js';
3
+ export declare function runRuleTests(rules: readonly LoadedRule[], onResult?: (result: RuleTestResult, lastInRule: boolean) => void, timeoutMs?: number, concurrency?: number, includeAI?: boolean): Promise<readonly RuleTestResult[]>;