@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,264 @@
1
+ import { mkdtemp, realpath, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { Worker } from 'node:worker_threads';
6
+ import { isRecord, validateJsonValue } from '../../app/validate.js';
7
+ import { getRuleDependencies } from './rule-resolution.js';
8
+ // A JavaScript entry lets inherited tsx hooks resolve the source worker's .js import.
9
+ const WORKER_MODULE = new URL('./rule-test-worker.js', import.meta.url);
10
+ const WORKER_ENTRY = new URL(`data:text/javascript,${encodeURIComponent(`import ${JSON.stringify(WORKER_MODULE.href)};`)}`);
11
+ function validateCase(value, label) {
12
+ if (!isRecord(value))
13
+ throw new Error(`${label}: expected a test case object`);
14
+ if (typeof value['name'] !== 'string' || value['name'].trim() === '') {
15
+ throw new Error(`${label}: name must be a non-blank string`);
16
+ }
17
+ const at = `${label} "${value['name']}"`;
18
+ if (!isRecord(value['files']))
19
+ throw new Error(`${at}: files must be a fixture path map`);
20
+ validateJsonValue(value['files'], `${at} files`);
21
+ const expected = value['expected'];
22
+ if (expected !== 'pass' && expected !== 'violation' && expected !== 'skipped') {
23
+ throw new Error(`${at}: expected must be "pass", "violation", or "skipped"`);
24
+ }
25
+ return value;
26
+ }
27
+ class RuleTestWorkerSession {
28
+ worker = new Worker(WORKER_ENTRY);
29
+ nextId = 0;
30
+ failure;
31
+ rejectPending;
32
+ onFailure;
33
+ terminating = false;
34
+ termination;
35
+ onError = (error) => {
36
+ if (this.terminating)
37
+ return;
38
+ this.failure ??= error;
39
+ if (this.rejectPending !== undefined)
40
+ this.rejectPending(error);
41
+ else
42
+ this.onFailure?.(error);
43
+ };
44
+ onExit = (code) => {
45
+ if (!this.terminating) {
46
+ this.onError(new Error(`Rule test worker exited before reporting a result (code ${code})`));
47
+ }
48
+ };
49
+ constructor() {
50
+ this.worker.on('error', this.onError);
51
+ this.worker.on('exit', this.onExit);
52
+ }
53
+ assertHealthy() {
54
+ if (this.failure !== undefined)
55
+ throw this.failure;
56
+ }
57
+ async run(request, deadline, onFailure) {
58
+ this.onFailure = onFailure;
59
+ this.assertHealthy();
60
+ const id = this.nextId++;
61
+ const result = Promise.withResolvers();
62
+ let timer;
63
+ const expire = () => {
64
+ const remaining = deadline - performance.now();
65
+ if (remaining <= 0)
66
+ result.resolve('timeout');
67
+ else
68
+ timer = setTimeout(expire, Math.min(remaining, 2_147_483_647));
69
+ };
70
+ const onMessage = (message) => {
71
+ if (message.id !== id)
72
+ return;
73
+ if (performance.now() >= deadline)
74
+ result.resolve('timeout');
75
+ else if ('error' in message)
76
+ result.reject(new Error(message.error));
77
+ else
78
+ result.resolve(message.actual);
79
+ };
80
+ this.rejectPending = error => {
81
+ if (performance.now() >= deadline)
82
+ result.resolve('timeout');
83
+ else
84
+ result.reject(error);
85
+ };
86
+ this.worker.on('message', onMessage);
87
+ try {
88
+ expire();
89
+ if (performance.now() < deadline) {
90
+ this.worker.postMessage({ ...request, id });
91
+ }
92
+ return await result.promise;
93
+ }
94
+ finally {
95
+ clearTimeout(timer);
96
+ this.worker.off('message', onMessage);
97
+ this.rejectPending = undefined;
98
+ }
99
+ }
100
+ async stop() {
101
+ if (this.termination === undefined) {
102
+ this.terminating = true;
103
+ this.rejectPending?.(new Error('Rule test worker stopped'));
104
+ this.termination = this.worker.terminate();
105
+ }
106
+ await this.termination;
107
+ this.worker.off('error', this.onError);
108
+ this.worker.off('exit', this.onExit);
109
+ }
110
+ }
111
+ async function stopSession(session) {
112
+ const current = session.current;
113
+ await current?.stop();
114
+ if (session.current === current)
115
+ session.current = undefined;
116
+ }
117
+ async function runCase(entry, example, ai, timeoutMs, session, onFailure) {
118
+ const started = performance.now();
119
+ const deadline = started + timeoutMs;
120
+ let failure;
121
+ const failCase = (error) => {
122
+ if (failure !== undefined)
123
+ return failure;
124
+ const detail = error instanceof Error ? error.message : String(error);
125
+ failure = new Error(`Rule "${entry.name}" test "${example.name}" failed: ${detail}`, { cause: error });
126
+ onFailure(failure);
127
+ return failure;
128
+ };
129
+ try {
130
+ const root = await mkdtemp(join(await realpath(tmpdir()), 'coderuler-test-'));
131
+ let actual;
132
+ let active;
133
+ try {
134
+ if (session.cancelled)
135
+ throw new Error('Rule test run cancelled');
136
+ if (performance.now() >= deadline)
137
+ actual = 'timeout';
138
+ else {
139
+ active = session.current ??= new RuleTestWorkerSession();
140
+ const dependencies = getRuleDependencies(entry.ruleFile);
141
+ actual = await active.run({
142
+ root, ruleFile: entry.ruleFile, files: example.files, ai,
143
+ ...(dependencies === undefined ? {} : { dependencies }),
144
+ }, deadline, failCase);
145
+ }
146
+ }
147
+ catch (error) {
148
+ throw failCase(error);
149
+ }
150
+ finally {
151
+ try {
152
+ if (actual === undefined || actual === 'timeout')
153
+ await stopSession(session);
154
+ }
155
+ finally {
156
+ await rm(root, { recursive: true, force: true });
157
+ }
158
+ }
159
+ if (actual !== 'timeout')
160
+ active?.assertHealthy();
161
+ return {
162
+ ruleName: entry.name,
163
+ name: example.name,
164
+ expected: example.expected,
165
+ actual,
166
+ durationMs: performance.now() - started,
167
+ };
168
+ }
169
+ catch (error) {
170
+ const cause = failCase(error);
171
+ await stopSession(session);
172
+ throw cause;
173
+ }
174
+ }
175
+ export async function runRuleTests(rules, onResult, timeoutMs = 10_000, concurrency = 1, includeAI = false) {
176
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
177
+ throw new RangeError(`--concurrency must be a positive safe integer, received ${String(concurrency)}`);
178
+ }
179
+ const results = [];
180
+ const sessions = [];
181
+ let failureObserved = false;
182
+ let hasExcludedAITests = false;
183
+ let failure;
184
+ let cancellation;
185
+ const fail = (error) => {
186
+ if (failureObserved)
187
+ return;
188
+ failureObserved = true;
189
+ failure = error;
190
+ cancellation = Promise.allSettled(sessions.map(session => {
191
+ session.cancelled = true;
192
+ return stopSession(session);
193
+ }));
194
+ };
195
+ try {
196
+ for (const rule of rules) {
197
+ if (failureObserved)
198
+ throw failure;
199
+ // Discovery selects the rule module path at runtime.
200
+ const { tests, aiTests } = await import(pathToFileURL(rule.ruleFile).href);
201
+ if (failureObserved)
202
+ throw failure;
203
+ if (!includeAI && aiTests !== undefined)
204
+ hasExcludedAITests = true;
205
+ const selectedAI = includeAI ? aiTests : undefined;
206
+ const label = `Rule "${rule.name}"`;
207
+ if (tests !== undefined && !Array.isArray(tests)) {
208
+ throw new Error(`${label} tests: expected an array of test cases`);
209
+ }
210
+ if (selectedAI !== undefined && !Array.isArray(selectedAI)) {
211
+ throw new Error(`${label} aiTests: expected an array of test cases`);
212
+ }
213
+ const regularCount = tests?.length ?? 0;
214
+ const count = regularCount + (selectedAI?.length ?? 0);
215
+ const completed = new Array(count);
216
+ let nextIndex = 0;
217
+ let nextResult = 0;
218
+ const runWorker = async (session) => {
219
+ while (!failureObserved) {
220
+ const index = nextIndex++;
221
+ if (index >= count)
222
+ return;
223
+ try {
224
+ const ai = index >= regularCount;
225
+ const caseIndex = ai ? index - regularCount : index;
226
+ const value = ai ? selectedAI[caseIndex] : tests[caseIndex];
227
+ const example = validateCase(value, `${label} ${ai ? 'aiTests' : 'tests'}[${caseIndex}]`);
228
+ completed[index] = await runCase(rule, example, ai, timeoutMs, session, fail);
229
+ if (failureObserved)
230
+ return;
231
+ while (completed[nextResult] !== undefined) {
232
+ const result = completed[nextResult];
233
+ results.push(result);
234
+ nextResult += 1;
235
+ onResult?.(result, nextResult === count);
236
+ }
237
+ }
238
+ catch (error) {
239
+ fail(error);
240
+ }
241
+ }
242
+ };
243
+ const workers = [];
244
+ const workerCount = Math.min(concurrency, count);
245
+ for (let index = 0; index < workerCount && !failureObserved; index += 1) {
246
+ const session = sessions[index] ??= { current: undefined, cancelled: false };
247
+ workers.push(runWorker(session));
248
+ }
249
+ await Promise.all(workers);
250
+ if (failureObserved)
251
+ throw failure;
252
+ }
253
+ if (results.length === 0) {
254
+ throw new Error(hasExcludedAITests
255
+ ? 'No ordinary rule test cases found in the selected rules; run with --ai to include aiTests'
256
+ : 'No rule test cases found in the selected rules');
257
+ }
258
+ return results;
259
+ }
260
+ finally {
261
+ await cancellation;
262
+ await Promise.all(sessions.map(stopSession));
263
+ }
264
+ }
@@ -0,0 +1,13 @@
1
+ import type { RuleLoader } from '../../app/ports.js';
2
+ import type { PreparedRuleDependencies, RuleDependencyManager } from './dependencies.js';
3
+ export interface RuleLoaderOptions {
4
+ readonly home?: string;
5
+ readonly dependencies?: RuleDependencyManager;
6
+ readonly preparedDependencies?: PreparedRuleDependencies;
7
+ }
8
+ export interface ResolvedRuleSelector {
9
+ readonly explicit?: string;
10
+ readonly name?: string;
11
+ }
12
+ export declare function resolveRuleSelector(selector: string, invocationCwd: string, allowDirectory?: boolean): Promise<ResolvedRuleSelector>;
13
+ export declare function createRuleLoader(options?: RuleLoaderOptions): RuleLoader;
@@ -0,0 +1,242 @@
1
+ import { lstat, readdir, realpath } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { basename, join, resolve, sep } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { isRecord, isRuleBranded, validateRule } from '../../app/validate.js';
6
+ import { createRuleDependencyManager, dependencyDirectory } from './dependencies.js';
7
+ import { assertRuleDependenciesCurrent, getRuleDependencies, registerRuleDependencies } from './rule-resolution.js';
8
+ const RULE_DIR = '.coderuler';
9
+ const RULE_MODULE = /\.rule\.(?:ts|mts|js|mjs)$/;
10
+ const PATH_LIKE = /[/\\]|\.rule\.(?:ts|mts|js|mjs)$/;
11
+ const ARCHIVED_DIRECTORY = 'old';
12
+ function describe(error) {
13
+ return error instanceof Error ? error.message : String(error);
14
+ }
15
+ function ruleNameOf(modulePath) {
16
+ return basename(modulePath).replace(RULE_MODULE, '');
17
+ }
18
+ async function collectModules(directory, files) {
19
+ const entries = await readdir(directory, { withFileTypes: true });
20
+ for (const entry of entries) {
21
+ if (entry.isSymbolicLink())
22
+ continue;
23
+ const child = join(directory, entry.name);
24
+ if (entry.isDirectory()) {
25
+ if (entry.name === ARCHIVED_DIRECTORY || entry.name === 'node_modules'
26
+ || entry.name === '.coderuler-deps-write'
27
+ || entry.name.startsWith('.coderuler-deps-stage-')
28
+ || entry.name.startsWith('.coderuler-sdk-'))
29
+ continue;
30
+ await collectModules(child, files);
31
+ }
32
+ else if (entry.isFile() && RULE_MODULE.test(entry.name)) {
33
+ files.push(child);
34
+ }
35
+ }
36
+ }
37
+ async function inspectRoot(root) {
38
+ let stats;
39
+ try {
40
+ stats = await lstat(root);
41
+ }
42
+ catch (error) {
43
+ if (error.code === 'ENOENT')
44
+ return undefined;
45
+ throw error;
46
+ }
47
+ if (stats.isSymbolicLink())
48
+ return undefined;
49
+ if (!stats.isDirectory()) {
50
+ throw new Error(`Rule discovery root ${root} is not a directory`);
51
+ }
52
+ const physical = await realpath(root);
53
+ if (physical.split(sep).includes(ARCHIVED_DIRECTORY)) {
54
+ throw new Error(`Rule path ${physical} is inside an archived "${ARCHIVED_DIRECTORY}" directory`);
55
+ }
56
+ const files = [];
57
+ await collectModules(physical, files);
58
+ return { physical, files: files.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)) };
59
+ }
60
+ function noRulesError(roots) {
61
+ return new Error(`No rule files found: expected a .rule.ts, .rule.mts, .rule.js, or .rule.mjs module under ${roots.join(' or ')}`);
62
+ }
63
+ function duplicatedNameError(name, paths) {
64
+ const listed = paths.map((path) => ` ${path}`).join('\n');
65
+ return new Error(`Rule name '${name}' matches ${paths.length} rule files:\n${listed}`);
66
+ }
67
+ function assertUniqueNames(entries) {
68
+ const byName = new Map();
69
+ for (const entry of entries) {
70
+ const paths = byName.get(entry.name);
71
+ if (paths === undefined)
72
+ byName.set(entry.name, [entry.modulePath]);
73
+ else
74
+ paths.push(entry.modulePath);
75
+ }
76
+ for (const [name, paths] of byName) {
77
+ if (paths.length > 1)
78
+ throw duplicatedNameError(name, paths);
79
+ }
80
+ }
81
+ async function discover(roots) {
82
+ const visitedRoots = new Set();
83
+ const modulePaths = [];
84
+ for (const root of roots) {
85
+ const found = await inspectRoot(root);
86
+ if (found === undefined || visitedRoots.has(found.physical))
87
+ continue;
88
+ visitedRoots.add(found.physical);
89
+ modulePaths.push(...found.files);
90
+ }
91
+ const seen = new Set();
92
+ const entries = [];
93
+ for (const modulePath of modulePaths) {
94
+ if (seen.has(modulePath))
95
+ continue;
96
+ seen.add(modulePath);
97
+ entries.push({ name: ruleNameOf(modulePath), modulePath });
98
+ }
99
+ if (entries.length === 0)
100
+ throw noRulesError(roots);
101
+ return entries;
102
+ }
103
+ async function canonicalRulePath(absolute) {
104
+ const physical = await realpath(absolute);
105
+ if (physical.split(sep).includes(ARCHIVED_DIRECTORY)) {
106
+ throw new Error(`Rule path ${physical} is inside an archived "${ARCHIVED_DIRECTORY}" directory`);
107
+ }
108
+ return physical;
109
+ }
110
+ async function collectExplicit(explicit, invocationCwd) {
111
+ const absolute = resolve(invocationCwd, explicit);
112
+ if (absolute.split(sep).includes(ARCHIVED_DIRECTORY)) {
113
+ throw new Error(`Rule path ${absolute} is inside an archived "${ARCHIVED_DIRECTORY}" directory`);
114
+ }
115
+ let stats;
116
+ try {
117
+ stats = await lstat(absolute);
118
+ }
119
+ catch (error) {
120
+ const code = error.code;
121
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
122
+ throw new Error(`Rule path ${absolute} does not exist`, { cause: error });
123
+ }
124
+ throw error;
125
+ }
126
+ if (stats.isSymbolicLink()) {
127
+ throw new Error(`Rule path ${absolute} is a symbolic link; point rule selection at the real file or directory`);
128
+ }
129
+ if (stats.isDirectory()) {
130
+ const files = [];
131
+ await collectModules(await canonicalRulePath(absolute), files);
132
+ files.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
133
+ if (files.length === 0)
134
+ throw noRulesError([absolute]);
135
+ return files;
136
+ }
137
+ if (stats.isFile()) {
138
+ if (!RULE_MODULE.test(basename(absolute))) {
139
+ throw new Error(`Unsupported rule module ${absolute}; expected a .rule.ts, .rule.mts, .rule.js, or .rule.mjs file`);
140
+ }
141
+ return [await canonicalRulePath(absolute)];
142
+ }
143
+ throw new Error(`Rule path ${absolute} is neither a file nor a directory`);
144
+ }
145
+ async function loadRule(modulePath) {
146
+ let imported;
147
+ try {
148
+ imported = await import(pathToFileURL(modulePath).href);
149
+ }
150
+ catch (error) {
151
+ throw new Error(`Rule module ${modulePath} failed to import: ${describe(error)}`, {
152
+ cause: error,
153
+ });
154
+ }
155
+ const candidate = isRecord(imported) ? imported['default'] : undefined;
156
+ if (typeof candidate !== 'function' || !isRuleBranded(candidate)) {
157
+ throw new Error(`Rule module ${modulePath} must default-export a constructible Rule class carrying the shared Rule brand`);
158
+ }
159
+ let instance;
160
+ try {
161
+ instance = new candidate();
162
+ }
163
+ catch (error) {
164
+ throw new Error(`Rule module ${modulePath} failed to construct: ${describe(error)}`, {
165
+ cause: error,
166
+ });
167
+ }
168
+ return validateRule(instance, modulePath);
169
+ }
170
+ async function loadEntries(entries, dependencies, preparedDependencies) {
171
+ const prepared = new Map();
172
+ for (const entry of entries) {
173
+ let context = preparedDependencies;
174
+ if (context === undefined) {
175
+ const directory = await dependencyDirectory(entry.modulePath);
176
+ context = prepared.get(directory) ?? await dependencies.prepare(directory);
177
+ prepared.set(directory, context);
178
+ }
179
+ registerRuleDependencies(entry.modulePath, context);
180
+ }
181
+ const loaded = [];
182
+ for (const entry of entries) {
183
+ const context = getRuleDependencies(entry.modulePath);
184
+ assertRuleDependenciesCurrent(context);
185
+ const rule = await loadRule(entry.modulePath);
186
+ assertRuleDependenciesCurrent(context);
187
+ loaded.push({ name: entry.name, ruleFile: entry.modulePath, rule });
188
+ }
189
+ return loaded;
190
+ }
191
+ export async function resolveRuleSelector(selector, invocationCwd, allowDirectory = false) {
192
+ const absolute = resolve(invocationCwd, selector);
193
+ let stats;
194
+ try {
195
+ stats = await lstat(absolute);
196
+ }
197
+ catch (error) {
198
+ const code = error.code;
199
+ if (code !== 'ENOENT' && code !== 'ENOTDIR')
200
+ throw error;
201
+ }
202
+ if (stats?.isDirectory() === true && !allowDirectory) {
203
+ throw new Error(`Rule selector ${selector} is a directory; inspecting requires one rule module or an exact rule name`);
204
+ }
205
+ if (stats !== undefined || PATH_LIKE.test(selector))
206
+ return { explicit: absolute };
207
+ return { name: selector };
208
+ }
209
+ export function createRuleLoader(options = {}) {
210
+ const home = options.home ?? homedir();
211
+ const dependencies = options.dependencies ?? createRuleDependencyManager();
212
+ return {
213
+ async load(selection) {
214
+ if (selection.explicit !== undefined && selection.name !== undefined) {
215
+ throw new Error('Rule selection cannot combine an explicit rule path with a rule name');
216
+ }
217
+ const roots = [resolve(selection.root, RULE_DIR), resolve(home, RULE_DIR)];
218
+ if (selection.explicit !== undefined) {
219
+ const modulePaths = await collectExplicit(selection.explicit, selection.invocationCwd);
220
+ const entries = modulePaths.map((modulePath) => ({
221
+ name: ruleNameOf(modulePath),
222
+ modulePath,
223
+ }));
224
+ assertUniqueNames(entries);
225
+ return loadEntries(entries, dependencies, options.preparedDependencies);
226
+ }
227
+ const discovered = await discover(roots);
228
+ if (selection.name !== undefined) {
229
+ const matches = discovered.filter((entry) => entry.name === selection.name);
230
+ if (matches.length === 0) {
231
+ throw new Error(`No rule named '${selection.name}' found under ${roots.join(' or ')}`);
232
+ }
233
+ if (matches.length > 1) {
234
+ throw duplicatedNameError(selection.name, matches.map((match) => match.modulePath));
235
+ }
236
+ return loadEntries(matches, dependencies, options.preparedDependencies);
237
+ }
238
+ assertUniqueNames(discovered);
239
+ return loadEntries(discovered, dependencies, options.preparedDependencies);
240
+ },
241
+ };
242
+ }
@@ -0,0 +1 @@
1
+ export declare function changedFiles(root: string, base: string, env?: Readonly<Record<string, string | undefined>>): Promise<ReadonlySet<string>>;
@@ -0,0 +1,54 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { traversablePath } from '../filesystem/project.js';
3
+ async function git(root, args, consume, env) {
4
+ await new Promise((resolve, reject) => {
5
+ const child = spawn('git', [...args], { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'] });
6
+ let stderr = '';
7
+ child.stdout.setEncoding('utf8');
8
+ child.stderr.setEncoding('utf8');
9
+ child.stdout.on('data', consume);
10
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
11
+ child.on('error', error => reject(new Error(`Cannot run Git: ${error.message}`, { cause: error })));
12
+ child.on('close', (code, signal) => {
13
+ if (code === 0)
14
+ resolve();
15
+ else
16
+ reject(new Error(`Git ${args[0]} failed: ${stderr.trim() || signal || `exit ${code}`}`));
17
+ });
18
+ });
19
+ }
20
+ export async function changedFiles(root, base, env) {
21
+ let tree = '';
22
+ try {
23
+ await git(root, ['rev-parse', '--verify', '--end-of-options', `${base}^{tree}`], chunk => {
24
+ tree += chunk;
25
+ }, env);
26
+ }
27
+ catch (error) {
28
+ throw new Error(`Cannot resolve Git base ${base}: ${error instanceof Error ? error.message : String(error)}`, {
29
+ cause: error,
30
+ });
31
+ }
32
+ const paths = new Set();
33
+ const collectPaths = async (args) => {
34
+ let pending = '';
35
+ await git(root, args, chunk => {
36
+ const records = (pending + chunk).split('\0');
37
+ pending = records.pop();
38
+ for (const path of records)
39
+ paths.add(path);
40
+ }, env);
41
+ };
42
+ await collectPaths([
43
+ 'diff', '--relative', '--name-only', '-z', '--no-renames', '--no-ext-diff', '--no-textconv',
44
+ tree.trim(), '--', '.',
45
+ ]);
46
+ await collectPaths(['ls-files', '--others', '--exclude-standard', '-z', '--', '.']);
47
+ const changed = new Set();
48
+ for (const path of paths) {
49
+ const physical = await traversablePath(root, path);
50
+ if (physical !== undefined)
51
+ changed.add(physical);
52
+ }
53
+ return changed;
54
+ }
@@ -0,0 +1 @@
1
+ export declare function isolatedGitEnvironment(env: Readonly<Record<string, string | undefined>>): NodeJS.ProcessEnv;
@@ -0,0 +1,10 @@
1
+ export function isolatedGitEnvironment(env) {
2
+ const isolated = { ...env };
3
+ for (const key of [
4
+ 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY',
5
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_SHALLOW_FILE', 'GIT_GRAFT_FILE', 'GIT_IMPLICIT_WORK_TREE',
6
+ 'GIT_PREFIX', 'GIT_REPLACE_REF_BASE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_NAMESPACE', 'GIT_CONFIG',
7
+ ])
8
+ delete isolated[key];
9
+ return isolated;
10
+ }
@@ -0,0 +1,5 @@
1
+ export declare function mapping(value: unknown): Record<string, unknown> | undefined;
2
+ export declare function createGithubApi(token?: string): {
3
+ request(path: string, method: 'GET' | 'POST', payload?: object): Promise<Response>;
4
+ json(path: string): Promise<unknown>;
5
+ };
@@ -0,0 +1,40 @@
1
+ export function mapping(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
3
+ ? value : undefined;
4
+ }
5
+ export function createGithubApi(token) {
6
+ const request = async (path, method, payload) => {
7
+ let response;
8
+ try {
9
+ response = await fetch(`https://api.github.com${path}`, {
10
+ method,
11
+ redirect: 'manual',
12
+ headers: {
13
+ ...(token === undefined ? {} : { Authorization: `Bearer ${token}` }),
14
+ Accept: 'application/vnd.github+json',
15
+ 'X-GitHub-Api-Version': '2022-11-28',
16
+ ...(payload === undefined ? {} : { 'Content-Type': 'application/json' }),
17
+ },
18
+ ...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
19
+ });
20
+ }
21
+ catch {
22
+ throw new Error('GitHub API request failed; check network connectivity to api.github.com.');
23
+ }
24
+ if (!response.ok) {
25
+ await response.body?.cancel().catch(() => undefined);
26
+ throw new Error(`GitHub API returned HTTP ${response.status}; check GITHUB_TOKEN permissions and pull request access.`);
27
+ }
28
+ return response;
29
+ };
30
+ const json = async (path) => {
31
+ const response = await request(path, 'GET');
32
+ try {
33
+ return await response.json();
34
+ }
35
+ catch {
36
+ throw new Error('GitHub API returned an invalid response.');
37
+ }
38
+ };
39
+ return { request, json };
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { ViolationComment } from '../../app/violation-comments.js';
2
+ import { type PullRequest } from './pull-request.js';
3
+ export declare function createGithubCommenter(env: Readonly<Record<string, string | undefined>>): (root: string, comments: readonly ViolationComment[], pullRequest?: PullRequest) => Promise<void>;