@alex-dbs/coderuler 1.4.0 → 1.6.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.
- package/README.md +3 -1
- package/dist/adapter/cli/commands/check.js +21 -10
- package/dist/adapter/cli/program.js +2 -2
- package/dist/adapter/cli/reports.js +1 -1
- package/dist/adapter/cli/services.d.ts +4 -2
- package/dist/adapter/decision/jev.js +32 -15
- package/dist/adapter/decision/openai.d.ts +11 -0
- package/dist/adapter/decision/openai.js +160 -0
- package/dist/adapter/filesystem/rule-test-lsp.d.ts +6 -0
- package/dist/adapter/filesystem/rule-test-lsp.js +189 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +5 -0
- package/dist/adapter/filesystem/rule-test-worker.js +48 -26
- package/dist/adapter/filesystem/rule-tests.d.ts +2 -2
- package/dist/adapter/filesystem/rule-tests.js +170 -29
- package/dist/adapter/git/changes.d.ts +2 -1
- package/dist/adapter/git/changes.js +369 -35
- package/dist/adapter/git/rules.d.ts +5 -0
- package/dist/adapter/git/rules.js +126 -0
- package/dist/adapter/github/pull-request.d.ts +2 -0
- package/dist/adapter/github/pull-request.js +9 -3
- package/dist/adapter/lsp/backend.d.ts +12 -0
- package/dist/adapter/lsp/backend.js +826 -0
- package/dist/app/concurrency.d.ts +1 -1
- package/dist/app/concurrency.js +12 -8
- package/dist/app/execute.js +84 -22
- package/dist/app/lsp.d.ts +6 -0
- package/dist/app/lsp.js +141 -0
- package/dist/app/ports.d.ts +13 -1
- package/dist/app/rule.d.ts +52 -10
- package/dist/app/rule.js +14 -0
- package/dist/app/validate.js +3 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/infra/runtime.js +27 -8
- package/dist/infra/settings.js +19 -8
- package/package.json +3 -1
- package/skills/coderuler-new-rule/SKILL.md +7 -3
- package/skills/coderuler-new-rule/reference.md +80 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function mapConcurrent<T, R>(items: readonly T[], concurrency: number, mapper: (item: T, index: number, reportFailure: (error: unknown) => void) => Promise<R>): Promise<R[]>;
|
|
1
|
+
export declare function mapConcurrent<T, R>(items: readonly T[], concurrency: number, mapper: (item: T, index: number, reportFailure: (error: unknown) => void) => Promise<R>, scheduler?: (work: Promise<R>) => Promise<R>): Promise<R[]>;
|
package/dist/app/concurrency.js
CHANGED
|
@@ -6,7 +6,7 @@ function requirePositiveSafeInteger(concurrency) {
|
|
|
6
6
|
throw invalidConcurrency(String(concurrency));
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
export async function mapConcurrent(items, concurrency, mapper) {
|
|
9
|
+
export async function mapConcurrent(items, concurrency, mapper, scheduler = work => work) {
|
|
10
10
|
requirePositiveSafeInteger(concurrency);
|
|
11
11
|
const results = new Array(items.length);
|
|
12
12
|
if (items.length === 0)
|
|
@@ -14,12 +14,12 @@ export async function mapConcurrent(items, concurrency, mapper) {
|
|
|
14
14
|
const workerCount = Math.min(items.length, concurrency);
|
|
15
15
|
let nextIndex = 0;
|
|
16
16
|
let failureObserved = false;
|
|
17
|
-
|
|
18
|
-
// A mapper may
|
|
17
|
+
const { promise: failureSignal, resolve: signalFailure } = Promise.withResolvers();
|
|
18
|
+
// A mapper may report failure before its own cleanup finishes.
|
|
19
19
|
const reportFailure = (error) => {
|
|
20
20
|
if (!failureObserved) {
|
|
21
21
|
failureObserved = true;
|
|
22
|
-
|
|
22
|
+
signalFailure({ error });
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
const runWorker = async () => {
|
|
@@ -29,7 +29,8 @@ export async function mapConcurrent(items, concurrency, mapper) {
|
|
|
29
29
|
if (index >= items.length)
|
|
30
30
|
return;
|
|
31
31
|
try {
|
|
32
|
-
|
|
32
|
+
const work = scheduler(Promise.resolve().then(() => mapper(items[index], index, reportFailure)));
|
|
33
|
+
results[index] = await work;
|
|
33
34
|
}
|
|
34
35
|
catch (error) {
|
|
35
36
|
reportFailure(error);
|
|
@@ -37,8 +38,11 @@ export async function mapConcurrent(items, concurrency, mapper) {
|
|
|
37
38
|
}
|
|
38
39
|
};
|
|
39
40
|
const workers = Array.from({ length: workerCount }, () => runWorker());
|
|
40
|
-
await Promise.
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const outcome = await Promise.race([
|
|
42
|
+
Promise.all(workers).then(() => ({ error: undefined, failed: false })),
|
|
43
|
+
failureSignal.then(({ error }) => ({ error, failed: true })),
|
|
44
|
+
]);
|
|
45
|
+
if (outcome.failed)
|
|
46
|
+
throw outcome.error;
|
|
43
47
|
return results;
|
|
44
48
|
}
|
package/dist/app/execute.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { matchesGlob } from 'node:path';
|
|
2
|
+
import { createRunLsp } from './lsp.js';
|
|
2
3
|
import { mapConcurrent } from './concurrency.js';
|
|
3
4
|
import { evaluateAi } from './evaluators.js';
|
|
4
5
|
import { wholeFileScope } from './selection.js';
|
|
5
6
|
import { addUsage } from './usage.js';
|
|
6
7
|
import { validateCheckResult, validatePredicateResult, validateScopeMatch, validateScopeSubjects, } from './validate.js';
|
|
8
|
+
function trackWork(pending, work) {
|
|
9
|
+
pending.add(work);
|
|
10
|
+
void work.then(() => { pending.delete(work); }, () => { pending.delete(work); });
|
|
11
|
+
return work;
|
|
12
|
+
}
|
|
7
13
|
function isEligibleRelativePath(filepath) {
|
|
8
14
|
if (filepath === '' || filepath.startsWith('/'))
|
|
9
15
|
return false;
|
|
@@ -26,12 +32,62 @@ function folderPath(filepath, level) {
|
|
|
26
32
|
return segments.slice(0, keep).join('/');
|
|
27
33
|
}
|
|
28
34
|
export async function executeRules(rules, context) {
|
|
35
|
+
const goLsp = createRunLsp(context.project, context.createLspBackend);
|
|
36
|
+
const rubyLsp = createRunLsp(context.project, context.createRubyLspBackend);
|
|
37
|
+
const typescriptLsp = createRunLsp(context.project, context.createTypescriptLspBackend);
|
|
38
|
+
const closeLspSessions = async () => {
|
|
39
|
+
const results = await Promise.allSettled([goLsp.close(), rubyLsp.close(), typescriptLsp.close()]);
|
|
40
|
+
const goResult = results[0];
|
|
41
|
+
if (goResult.status === 'rejected')
|
|
42
|
+
throw goResult.reason;
|
|
43
|
+
const rubyResult = results[1];
|
|
44
|
+
if (rubyResult.status === 'rejected')
|
|
45
|
+
throw rubyResult.reason;
|
|
46
|
+
const typescriptResult = results[2];
|
|
47
|
+
if (typescriptResult.status === 'rejected')
|
|
48
|
+
throw typescriptResult.reason;
|
|
49
|
+
};
|
|
50
|
+
const abortLsp = () => { void closeLspSessions().catch(() => undefined); };
|
|
51
|
+
const startedWork = new Set();
|
|
52
|
+
const scheduleWork = (work) => {
|
|
53
|
+
startedWork.add(work);
|
|
54
|
+
void work.then(() => { startedWork.delete(work); }, () => { startedWork.delete(work); });
|
|
55
|
+
return work;
|
|
56
|
+
};
|
|
57
|
+
const drainStartedWork = async () => {
|
|
58
|
+
while (startedWork.size > 0)
|
|
59
|
+
await Promise.allSettled([...startedWork]);
|
|
60
|
+
};
|
|
61
|
+
let failed = false;
|
|
62
|
+
try {
|
|
63
|
+
const reports = await executeRun(rules, context, goLsp.tools, rubyLsp.tools, typescriptLsp.tools, scheduleWork, abortLsp);
|
|
64
|
+
await drainStartedWork();
|
|
65
|
+
return reports;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
failed = true;
|
|
69
|
+
abortLsp();
|
|
70
|
+
await drainStartedWork();
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
try {
|
|
75
|
+
await closeLspSessions();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!failed)
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function executeRun(rules, context, goLsp, rubyLsp, typescriptLsp, scheduleWork, abortLsp) {
|
|
29
84
|
let completedSelections = 0;
|
|
30
85
|
let completedEvaluations = 0;
|
|
31
86
|
const emit = (event) => {
|
|
32
87
|
context.emit?.(event);
|
|
33
88
|
};
|
|
34
89
|
const changedFiles = context.changedFiles;
|
|
90
|
+
const changedLines = context.changedLines;
|
|
35
91
|
const changedPaths = changedFiles === undefined
|
|
36
92
|
? undefined
|
|
37
93
|
: [...changedFiles].filter(filepath => isEligibleRelativePath(filepath) && context.isTarget(filepath));
|
|
@@ -44,9 +100,14 @@ export async function executeRules(rules, context) {
|
|
|
44
100
|
...(details === undefined ? {} : { details }),
|
|
45
101
|
});
|
|
46
102
|
};
|
|
47
|
-
const projectContextFor = (name) => ({
|
|
103
|
+
const projectContextFor = (name, evaluationId) => ({
|
|
48
104
|
project: context.project,
|
|
49
|
-
|
|
105
|
+
goLsp,
|
|
106
|
+
rubyLsp,
|
|
107
|
+
typescriptLsp,
|
|
108
|
+
...(changedFiles === undefined ? {} : { changedFiles }),
|
|
109
|
+
...(changedLines === undefined ? {} : { changedLines }),
|
|
110
|
+
trace: traceFor(name, evaluationId),
|
|
50
111
|
});
|
|
51
112
|
const fail = (name, stage, error, evaluationId) => {
|
|
52
113
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -80,7 +141,7 @@ export async function executeRules(rules, context) {
|
|
|
80
141
|
}
|
|
81
142
|
}
|
|
82
143
|
};
|
|
83
|
-
const selectFromRule = async (name, selection, evaluationId, changedOnly = false) => {
|
|
144
|
+
const selectFromRule = async (name, selection, evaluationId, changedOnly = false, pendingWork) => {
|
|
84
145
|
const exclude = selection.exclude ?? [];
|
|
85
146
|
const candidates = await context.project.glob(selection.files);
|
|
86
147
|
const chosen = [];
|
|
@@ -102,11 +163,14 @@ export async function executeRules(rules, context) {
|
|
|
102
163
|
chosen.push(filepath);
|
|
103
164
|
}
|
|
104
165
|
const scope = selection.scope;
|
|
166
|
+
const scheduler = pendingWork === undefined
|
|
167
|
+
? scheduleWork
|
|
168
|
+
: (work) => trackWork(pendingWork, work);
|
|
105
169
|
const collected = await mapConcurrent(chosen, context.concurrency, async (filepath) => {
|
|
106
170
|
const source = await context.project.readText(filepath);
|
|
107
171
|
const matches = scope === undefined
|
|
108
172
|
? wholeFileScope({ filepath, source })
|
|
109
|
-
: (await scope({ filepath, source }));
|
|
173
|
+
: (await scope({ filepath, source }, projectContextFor(name, evaluationId)));
|
|
110
174
|
if (!Array.isArray(matches)) {
|
|
111
175
|
throw new Error(`Rule "${name}" scope selector must return an array of scopes`);
|
|
112
176
|
}
|
|
@@ -119,12 +183,11 @@ export async function executeRules(rules, context) {
|
|
|
119
183
|
subjects.push({ kind: 'scope', filepath, ...valid });
|
|
120
184
|
}
|
|
121
185
|
return subjects;
|
|
122
|
-
});
|
|
186
|
+
}, scheduler);
|
|
123
187
|
return collected.flat();
|
|
124
188
|
};
|
|
125
189
|
const selectionContextFor = (name, changedOnly) => ({
|
|
126
|
-
|
|
127
|
-
trace: traceFor(name),
|
|
190
|
+
...projectContextFor(name),
|
|
128
191
|
select: selection => selectFromRule(name, selection, undefined, changedOnly),
|
|
129
192
|
});
|
|
130
193
|
const runtimeOf = (plan) => {
|
|
@@ -139,15 +202,15 @@ export async function executeRules(rules, context) {
|
|
|
139
202
|
plan.evaluationStartedAt ??= performance.now();
|
|
140
203
|
const { name, rule } = plan;
|
|
141
204
|
emit({ type: 'evaluation', evaluationId, name, subject });
|
|
142
|
-
const trace = traceFor(name, evaluationId);
|
|
143
205
|
let trackedUsage;
|
|
144
|
-
let
|
|
206
|
+
let pendingWork;
|
|
145
207
|
let accountingFailure;
|
|
146
208
|
const retainAccountingFailure = (error) => {
|
|
147
209
|
if (accountingFailure !== undefined)
|
|
148
210
|
return;
|
|
149
211
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
150
212
|
accountingFailure = { error: failure };
|
|
213
|
+
abortLsp();
|
|
151
214
|
reportFailure(failure);
|
|
152
215
|
};
|
|
153
216
|
const throwIfAccountingFailed = () => {
|
|
@@ -180,17 +243,15 @@ export async function executeRules(rules, context) {
|
|
|
180
243
|
}
|
|
181
244
|
};
|
|
182
245
|
const checkContext = {
|
|
183
|
-
|
|
246
|
+
...projectContextFor(name, evaluationId),
|
|
184
247
|
ruleName: name,
|
|
185
|
-
|
|
186
|
-
|
|
248
|
+
// Let the check catch a nested selection rejection. If it escapes, the
|
|
249
|
+
// enclosing check handler records it before aborting sibling work.
|
|
250
|
+
select: selection => selectFromRule(name, selection, evaluationId, false, pendingWork ??= new Set()),
|
|
187
251
|
ai: options => {
|
|
188
|
-
const call = evaluateAi(subject, options, {
|
|
189
|
-
const pending =
|
|
190
|
-
pending
|
|
191
|
-
// Observe both outcomes immediately; the check still receives the original promise.
|
|
192
|
-
void call.then(() => { pending.delete(call); }, () => { pending.delete(call); });
|
|
193
|
-
return call;
|
|
252
|
+
const call = evaluateAi(subject, options, { ...projectContextFor(name, evaluationId), ruleName: name }, runtimeOf(plan), observer);
|
|
253
|
+
const pending = pendingWork ??= new Set();
|
|
254
|
+
return trackWork(pending, call);
|
|
194
255
|
},
|
|
195
256
|
};
|
|
196
257
|
let result;
|
|
@@ -223,12 +284,13 @@ export async function executeRules(rules, context) {
|
|
|
223
284
|
const cause = accountingFailure === undefined ? error : accountingFailure.error;
|
|
224
285
|
const failure = cause instanceof Error ? cause : new Error(String(cause));
|
|
225
286
|
reportFailure(failure);
|
|
287
|
+
abortLsp();
|
|
226
288
|
throw failure;
|
|
227
289
|
}
|
|
228
290
|
finally {
|
|
229
|
-
//
|
|
230
|
-
while (
|
|
231
|
-
await Promise.allSettled(
|
|
291
|
+
// The check can settle while sibling AI calls or selector workers still produce usage.
|
|
292
|
+
while (pendingWork !== undefined && pendingWork.size > 0) {
|
|
293
|
+
await Promise.allSettled(pendingWork);
|
|
232
294
|
}
|
|
233
295
|
}
|
|
234
296
|
// A custom check may catch context.ai failures, but cannot discard accounting failures.
|
|
@@ -372,7 +434,7 @@ export async function executeRules(rules, context) {
|
|
|
372
434
|
}
|
|
373
435
|
}
|
|
374
436
|
context.emit?.({ type: 'progress', phase: 'evaluation', completed: 0, total: tasks.length });
|
|
375
|
-
const results = await mapConcurrent(tasks, context.concurrency, (task, _index, reportFailure) => evaluateSubject(task.plan, task.subject, task.evaluationId, reportFailure));
|
|
437
|
+
const results = await mapConcurrent(tasks, context.concurrency, (task, _index, reportFailure) => evaluateSubject(task.plan, task.subject, task.evaluationId, reportFailure), scheduleWork);
|
|
376
438
|
for (let index = 0; index < tasks.length; index += 1) {
|
|
377
439
|
const task = tasks[index];
|
|
378
440
|
task.plan.evaluations.push({ subject: task.subject, result: results[index] });
|
package/dist/app/lsp.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export function createRunLsp(project, createBackend) {
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
const pending = new Set();
|
|
4
|
+
let startup;
|
|
5
|
+
let backend;
|
|
6
|
+
let backendClose;
|
|
7
|
+
let closeFailure;
|
|
8
|
+
let hasCloseFailure = false;
|
|
9
|
+
let closePromise;
|
|
10
|
+
let closed = false;
|
|
11
|
+
const closedError = () => new Error('LSP session is closed');
|
|
12
|
+
const closeBackend = (owned) => {
|
|
13
|
+
if (backendClose === undefined) {
|
|
14
|
+
backendClose = Promise.resolve().then(() => owned.close());
|
|
15
|
+
void backendClose.catch(error => {
|
|
16
|
+
closeFailure = error;
|
|
17
|
+
hasCloseFailure = true;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return backendClose;
|
|
21
|
+
};
|
|
22
|
+
const getBackend = () => {
|
|
23
|
+
if (closed)
|
|
24
|
+
return Promise.reject(closedError());
|
|
25
|
+
if (startup !== undefined)
|
|
26
|
+
return startup;
|
|
27
|
+
if (createBackend === undefined) {
|
|
28
|
+
startup = Promise.reject(new Error('LSP is not configured for this run'));
|
|
29
|
+
void startup.catch(() => undefined);
|
|
30
|
+
return startup;
|
|
31
|
+
}
|
|
32
|
+
startup = Promise.resolve().then(async () => {
|
|
33
|
+
if (controller.signal.aborted)
|
|
34
|
+
throw closedError();
|
|
35
|
+
const created = await createBackend(project, controller.signal);
|
|
36
|
+
backend = created;
|
|
37
|
+
if (closed) {
|
|
38
|
+
await closeBackend(created);
|
|
39
|
+
throw closedError();
|
|
40
|
+
}
|
|
41
|
+
return created;
|
|
42
|
+
});
|
|
43
|
+
void startup.catch(() => undefined);
|
|
44
|
+
return startup;
|
|
45
|
+
};
|
|
46
|
+
const observe = (promise) => {
|
|
47
|
+
pending.add(promise);
|
|
48
|
+
void promise.then(() => pending.delete(promise), () => pending.delete(promise));
|
|
49
|
+
return promise;
|
|
50
|
+
};
|
|
51
|
+
const rejectCall = (error) => observe(Promise.reject(error));
|
|
52
|
+
const invoke = (operation) => observe(Promise.resolve().then(async () => {
|
|
53
|
+
if (closed)
|
|
54
|
+
throw closedError();
|
|
55
|
+
const active = await getBackend();
|
|
56
|
+
if (closed || controller.signal.aborted)
|
|
57
|
+
throw closedError();
|
|
58
|
+
return operation(active);
|
|
59
|
+
}));
|
|
60
|
+
const validateTarget = (target) => {
|
|
61
|
+
if (!Number.isSafeInteger(target.line) || target.line < 1) {
|
|
62
|
+
throw new Error('LSP line must be a positive safe integer');
|
|
63
|
+
}
|
|
64
|
+
if (target.column !== undefined && (!Number.isSafeInteger(target.column) || target.column < 1)) {
|
|
65
|
+
throw new Error('LSP column must be a positive safe integer');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const withTarget = (target, operation) => {
|
|
69
|
+
if (closed)
|
|
70
|
+
return rejectCall(closedError());
|
|
71
|
+
try {
|
|
72
|
+
validateTarget(target);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return rejectCall(error);
|
|
76
|
+
}
|
|
77
|
+
return invoke(operation);
|
|
78
|
+
};
|
|
79
|
+
const withGraph = (target, options, operation) => {
|
|
80
|
+
if (closed)
|
|
81
|
+
return rejectCall(closedError());
|
|
82
|
+
if (!Number.isSafeInteger(options.depth) || options.depth < 0) {
|
|
83
|
+
return rejectCall(new Error('LSP call depth must be a nonnegative safe integer'));
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
validateTarget(target);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return rejectCall(error);
|
|
90
|
+
}
|
|
91
|
+
if (options.depth === 0)
|
|
92
|
+
return Promise.resolve([]);
|
|
93
|
+
return invoke(operation);
|
|
94
|
+
};
|
|
95
|
+
const tools = {
|
|
96
|
+
definitions: (target, options) => withTarget(target, active => active.definitions(target, options)),
|
|
97
|
+
declarations: (target, options) => withTarget(target, active => active.declarations(target, options)),
|
|
98
|
+
typeDefinitions: (target, options) => withTarget(target, active => active.typeDefinitions(target, options)),
|
|
99
|
+
implementations: (target, options) => withTarget(target, active => active.implementations(target, options)),
|
|
100
|
+
references: (target, options) => withTarget(target, active => active.references(target, options)),
|
|
101
|
+
hover: (target, options) => withTarget(target, active => active.hover(target, options)),
|
|
102
|
+
documentSymbols: (filepath, options) => invoke(active => active.documentSymbols(filepath, options)),
|
|
103
|
+
workspaceSymbols: (query, options) => {
|
|
104
|
+
if (closed)
|
|
105
|
+
return rejectCall(closedError());
|
|
106
|
+
return query.length === 0 ? Promise.resolve([]) : invoke(active => active.workspaceSymbols(query, options));
|
|
107
|
+
},
|
|
108
|
+
callers: (target, options) => withGraph(target, options, active => active.callers(target, options)),
|
|
109
|
+
callees: (target, options) => withGraph(target, options, active => active.callees(target, options)),
|
|
110
|
+
};
|
|
111
|
+
const close = () => {
|
|
112
|
+
if (closePromise !== undefined)
|
|
113
|
+
return closePromise;
|
|
114
|
+
closed = true;
|
|
115
|
+
controller.abort();
|
|
116
|
+
closePromise = Promise.resolve().then(async () => {
|
|
117
|
+
if (startup !== undefined) {
|
|
118
|
+
try {
|
|
119
|
+
backend = await startup;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// The request reports startup failures. There is no backend to close.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (backend !== undefined) {
|
|
126
|
+
try {
|
|
127
|
+
await closeBackend(backend);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Preserve the close failure after pending calls settle.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await Promise.allSettled([...pending]);
|
|
134
|
+
if (hasCloseFailure)
|
|
135
|
+
throw closeFailure;
|
|
136
|
+
});
|
|
137
|
+
void closePromise.catch(() => undefined);
|
|
138
|
+
return closePromise;
|
|
139
|
+
};
|
|
140
|
+
return { tools, close };
|
|
141
|
+
}
|
package/dist/app/ports.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { AiUsage, CheckResult, GroupSubject, JsonValue, Rule, RuleSettings, ScopeSubject, Subject, TokenPricing, TokenUsage } from './rule.js';
|
|
1
|
+
import type { AiUsage, Awaitable, CheckResult, GroupSubject, JsonValue, LineRange, LspTools, Rule, RuleSettings, ScopeSubject, Subject, TokenPricing, TokenUsage } from './rule.js';
|
|
2
|
+
export interface DiffChanges {
|
|
3
|
+
readonly changedFiles: ReadonlySet<string>;
|
|
4
|
+
readonly changedLines: ReadonlyMap<string, readonly LineRange[]>;
|
|
5
|
+
}
|
|
2
6
|
export interface Project {
|
|
3
7
|
readonly root: string;
|
|
4
8
|
glob(patterns: string | readonly string[]): Promise<readonly string[]>;
|
|
@@ -11,6 +15,10 @@ export interface Project {
|
|
|
11
15
|
readYaml(path: string): Promise<readonly JsonValue[]>;
|
|
12
16
|
readCsv(path: string, options?: CsvOptions): Promise<readonly CsvRow[]>;
|
|
13
17
|
}
|
|
18
|
+
export interface LspBackend extends LspTools {
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export type LspBackendFactory = (project: Project, signal: AbortSignal) => Awaitable<LspBackend>;
|
|
14
22
|
export interface CsvOptions {
|
|
15
23
|
readonly headers?: boolean;
|
|
16
24
|
readonly delimiter?: string;
|
|
@@ -71,7 +79,11 @@ export interface DecisionRuntime {
|
|
|
71
79
|
export interface RunContext {
|
|
72
80
|
readonly project: Project;
|
|
73
81
|
readonly isTarget: (filepath: string) => boolean;
|
|
82
|
+
readonly createLspBackend?: LspBackendFactory;
|
|
83
|
+
readonly createRubyLspBackend?: LspBackendFactory;
|
|
84
|
+
readonly createTypescriptLspBackend?: LspBackendFactory;
|
|
74
85
|
readonly changedFiles?: ReadonlySet<string>;
|
|
86
|
+
readonly changedLines?: ReadonlyMap<string, readonly LineRange[]>;
|
|
75
87
|
readonly concurrency: number;
|
|
76
88
|
readonly resolveDecision: (settings: RuleSettings) => DecisionRuntime;
|
|
77
89
|
readonly emit?: (event: RunEvent) => void;
|
package/dist/app/rule.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { DocumentSymbol, Hover, Location, SymbolInformation, WorkspaceSymbol } from 'vscode-languageserver-protocol';
|
|
1
2
|
import type { Project } from './ports.js';
|
|
3
|
+
export type { DocumentSymbol, Hover, Location, SymbolInformation, WorkspaceSymbol, } from 'vscode-languageserver-protocol';
|
|
2
4
|
export type Awaitable<T> = T | Promise<T>;
|
|
3
5
|
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
|
|
4
6
|
readonly [key: string]: JsonValue;
|
|
@@ -11,15 +13,49 @@ export interface SourceFile {
|
|
|
11
13
|
readonly filepath: string;
|
|
12
14
|
readonly source: string;
|
|
13
15
|
}
|
|
16
|
+
export interface LineRange {
|
|
17
|
+
readonly start: number;
|
|
18
|
+
readonly end: number;
|
|
19
|
+
}
|
|
20
|
+
export interface LspTarget {
|
|
21
|
+
readonly filepath: string;
|
|
22
|
+
readonly line: number;
|
|
23
|
+
readonly column?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface LspOptions {
|
|
26
|
+
readonly includeDependencies?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface LspReferenceOptions extends LspOptions {
|
|
29
|
+
readonly includeDeclaration?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface LspCallOptions extends LspOptions {
|
|
32
|
+
readonly depth: number;
|
|
33
|
+
}
|
|
34
|
+
export interface LspCall {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly filepath: string;
|
|
37
|
+
readonly source: string;
|
|
38
|
+
readonly lines: LineRange;
|
|
39
|
+
readonly depth: number;
|
|
40
|
+
}
|
|
41
|
+
export interface LspTools {
|
|
42
|
+
definitions(target: LspTarget, options?: LspOptions): Promise<readonly Location[]>;
|
|
43
|
+
declarations(target: LspTarget, options?: LspOptions): Promise<readonly Location[]>;
|
|
44
|
+
typeDefinitions(target: LspTarget, options?: LspOptions): Promise<readonly Location[]>;
|
|
45
|
+
implementations(target: LspTarget, options?: LspOptions): Promise<readonly Location[]>;
|
|
46
|
+
references(target: LspTarget, options?: LspReferenceOptions): Promise<readonly Location[]>;
|
|
47
|
+
hover(target: LspTarget, options?: LspOptions): Promise<Hover | null>;
|
|
48
|
+
documentSymbols(filepath: string, options?: LspOptions): Promise<readonly DocumentSymbol[] | readonly SymbolInformation[]>;
|
|
49
|
+
workspaceSymbols(query: string, options?: LspOptions): Promise<readonly (SymbolInformation | WorkspaceSymbol)[]>;
|
|
50
|
+
callers(target: LspTarget, options: LspCallOptions): Promise<readonly LspCall[]>;
|
|
51
|
+
callees(target: LspTarget, options: LspCallOptions): Promise<readonly LspCall[]>;
|
|
52
|
+
}
|
|
14
53
|
export interface ScopeMatch<T = JsonValue> {
|
|
15
54
|
readonly id: string;
|
|
16
55
|
readonly source: string;
|
|
17
56
|
readonly data?: T;
|
|
18
57
|
readonly captures?: Readonly<Record<string, string | readonly string[]>>;
|
|
19
|
-
readonly lines?:
|
|
20
|
-
readonly start: number;
|
|
21
|
-
readonly end: number;
|
|
22
|
-
};
|
|
58
|
+
readonly lines?: LineRange;
|
|
23
59
|
}
|
|
24
60
|
export interface ScopeSubject<T = JsonValue> extends ScopeMatch<T> {
|
|
25
61
|
readonly kind: 'scope';
|
|
@@ -34,13 +70,15 @@ export interface GroupSubject<T = JsonValue> {
|
|
|
34
70
|
readonly members: readonly ScopeSubject<T>[];
|
|
35
71
|
}
|
|
36
72
|
export type Subject<T = JsonValue> = ScopeSubject<T> | GroupSubject<T>;
|
|
37
|
-
export type ScopeSelector<T = JsonValue> =
|
|
73
|
+
export type ScopeSelector<T = JsonValue> = {
|
|
74
|
+
bivarianceHack(file: SourceFile, context?: ProjectContext): Awaitable<readonly ScopeMatch<T>[]>;
|
|
75
|
+
}['bivarianceHack'];
|
|
38
76
|
export type Grouping = 'scope' | 'file' | 'folder' | 'project' | {
|
|
39
77
|
readonly by: 'folder';
|
|
40
78
|
readonly level: number;
|
|
41
79
|
};
|
|
42
80
|
export interface RuleSettings {
|
|
43
|
-
readonly backend?: 'jev';
|
|
81
|
+
readonly backend?: 'jev' | 'openai';
|
|
44
82
|
readonly model?: string;
|
|
45
83
|
readonly threshold?: number;
|
|
46
84
|
readonly inputCostPerMillion?: number;
|
|
@@ -63,10 +101,7 @@ export interface AiUsage extends TokenUsage {
|
|
|
63
101
|
export interface ViolationLocation {
|
|
64
102
|
readonly filepath: string;
|
|
65
103
|
readonly scopeId?: string;
|
|
66
|
-
readonly lines?:
|
|
67
|
-
readonly start: number;
|
|
68
|
-
readonly end: number;
|
|
69
|
-
};
|
|
104
|
+
readonly lines?: LineRange;
|
|
70
105
|
}
|
|
71
106
|
export interface CheckResult {
|
|
72
107
|
readonly status: 'pass' | 'violation';
|
|
@@ -96,6 +131,11 @@ export type AiOptions<T = JsonValue> = {
|
|
|
96
131
|
export type Evaluator<T = JsonValue> = (subject: Subject<T>, context: CheckContext<T>) => Awaitable<CheckResult>;
|
|
97
132
|
export interface ProjectContext {
|
|
98
133
|
readonly project: Project;
|
|
134
|
+
readonly goLsp: LspTools;
|
|
135
|
+
readonly rubyLsp: LspTools;
|
|
136
|
+
readonly typescriptLsp: LspTools;
|
|
137
|
+
readonly changedFiles?: ReadonlySet<string>;
|
|
138
|
+
readonly changedLines?: ReadonlyMap<string, readonly LineRange[]>;
|
|
99
139
|
trace(message: string, details?: JsonValue): void;
|
|
100
140
|
}
|
|
101
141
|
export interface Selection<T = JsonValue> {
|
|
@@ -110,6 +150,8 @@ export interface CheckContext<T = JsonValue> extends SelectionContext {
|
|
|
110
150
|
readonly ruleName: string;
|
|
111
151
|
ai(options: AiOptions<T>): Promise<CheckResult>;
|
|
112
152
|
}
|
|
153
|
+
export declare function isDiffMode(context: ProjectContext): boolean;
|
|
154
|
+
export declare function isInDiff(subject: ScopeSubject<unknown>, context: ProjectContext): boolean;
|
|
113
155
|
export declare const RULE_BRAND: symbol;
|
|
114
156
|
export declare abstract class Rule<T = JsonValue> {
|
|
115
157
|
enabled: boolean;
|
package/dist/app/rule.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
export function isDiffMode(context) {
|
|
2
|
+
return context.changedFiles !== undefined || context.changedLines !== undefined;
|
|
3
|
+
}
|
|
4
|
+
export function isInDiff(subject, context) {
|
|
5
|
+
const lines = subject.lines;
|
|
6
|
+
const ranges = context.changedLines?.get(subject.filepath);
|
|
7
|
+
if (lines === undefined || ranges === undefined)
|
|
8
|
+
return false;
|
|
9
|
+
for (const range of ranges) {
|
|
10
|
+
if (range.start <= lines.end && lines.start <= range.end)
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
1
15
|
export const RULE_BRAND = Symbol.for('coderuler.Rule');
|
|
2
16
|
export class Rule {
|
|
3
17
|
enabled = true;
|
package/dist/app/validate.js
CHANGED
|
@@ -24,8 +24,9 @@ function isStringList(value, allowEmpty) {
|
|
|
24
24
|
}
|
|
25
25
|
function validateSettings(settings, at) {
|
|
26
26
|
const backend = settings['backend'];
|
|
27
|
-
if (backend !== undefined && backend !== 'jev')
|
|
28
|
-
invalid(at, 'settings.backend must be "jev"');
|
|
27
|
+
if (backend !== undefined && backend !== 'jev' && backend !== 'openai') {
|
|
28
|
+
invalid(at, 'settings.backend must be "jev" or "openai"');
|
|
29
|
+
}
|
|
29
30
|
const model = settings['model'];
|
|
30
31
|
if (model !== undefined && (typeof model !== 'string' || model.trim() === '')) {
|
|
31
32
|
invalid(at, 'settings.model must be a non-blank string');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { Rule } from './app/rule.js';
|
|
1
|
+
export { Rule, isDiffMode, isInDiff } from './app/rule.js';
|
|
2
2
|
export { test } from './app/rule-tests.js';
|
|
3
3
|
export type { RuleTestCase } from './app/rule-tests.js';
|
|
4
|
-
export type { AiExamples, AiOptions, AiUsage, Awaitable, CheckContext, CheckResult, Evaluator, Grouping, GroupSubject, JsonValue, PredicateResult, ProjectContext, RuleSettings, ScopeMatch, ScopeSelector, ScopeSubject, Selection, SelectionContext, SourceFile, Subject, TokenPricing, TokenUsage, ViolationLocation, } from './app/rule.js';
|
|
4
|
+
export type { AiExamples, AiOptions, AiUsage, Awaitable, CheckContext, CheckResult, DocumentSymbol, Evaluator, Grouping, GroupSubject, Hover, JsonValue, LineRange, Location, LspCall, LspCallOptions, LspOptions, LspReferenceOptions, LspTarget, LspTools, PredicateResult, ProjectContext, RuleSettings, ScopeMatch, ScopeSelector, ScopeSubject, Selection, SelectionContext, SourceFile, Subject, SymbolInformation, TokenPricing, TokenUsage, ViolationLocation, WorkspaceSymbol, } from './app/rule.js';
|
|
5
5
|
export { ai, pass, regex, violation } from './app/evaluators.js';
|
|
6
6
|
export type { RegexOptions } from './app/evaluators.js';
|
|
7
|
-
export type { CsvOptions, CsvRow, DecisionBackend, DecisionExchange, DecisionInstructions, DecisionObserver, DecisionOutcome, DecisionRequest, DecisionRuntime, LoadedRule, Project, RuleEvaluation, RuleLoader, RuleReport, RuleSelection, RunContext, RunEvent, } from './app/ports.js';
|
|
7
|
+
export type { CsvOptions, DiffChanges, CsvRow, DecisionBackend, DecisionExchange, DecisionInstructions, DecisionObserver, DecisionOutcome, DecisionRequest, DecisionRuntime, LoadedRule, LspBackend, LspBackendFactory, Project, RuleEvaluation, RuleLoader, RuleReport, RuleSelection, RunContext, RunEvent, } from './app/ports.js';
|
|
8
8
|
import { astGrep } from './adapter/scope/astgrep.js';
|
|
9
9
|
import { csv } from './adapter/scope/csv.js';
|
|
10
10
|
import { file } from './adapter/scope/file.js';
|
package/dist/index.js
CHANGED