@alex-dbs/coderuler 1.5.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/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 +4 -0
- package/dist/adapter/filesystem/rule-test-worker.js +48 -28
- package/dist/adapter/filesystem/rule-tests.d.ts +2 -2
- package/dist/adapter/filesystem/rule-tests.js +115 -12
- 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 +75 -13
- package/dist/app/lsp.d.ts +6 -0
- package/dist/app/lsp.js +141 -0
- package/dist/app/ports.d.ts +8 -1
- package/dist/app/rule.d.ts +40 -0
- package/dist/app/rule.js +14 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/infra/runtime.js +14 -3
- package/package.json +2 -1
- package/skills/coderuler-new-rule/reference.md +53 -0
|
@@ -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,6 +32,55 @@ 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) => {
|
|
@@ -47,6 +102,9 @@ export async function executeRules(rules, context) {
|
|
|
47
102
|
};
|
|
48
103
|
const projectContextFor = (name, evaluationId) => ({
|
|
49
104
|
project: context.project,
|
|
105
|
+
goLsp,
|
|
106
|
+
rubyLsp,
|
|
107
|
+
typescriptLsp,
|
|
50
108
|
...(changedFiles === undefined ? {} : { changedFiles }),
|
|
51
109
|
...(changedLines === undefined ? {} : { changedLines }),
|
|
52
110
|
trace: traceFor(name, evaluationId),
|
|
@@ -83,7 +141,7 @@ export async function executeRules(rules, context) {
|
|
|
83
141
|
}
|
|
84
142
|
}
|
|
85
143
|
};
|
|
86
|
-
const selectFromRule = async (name, selection, evaluationId, changedOnly = false) => {
|
|
144
|
+
const selectFromRule = async (name, selection, evaluationId, changedOnly = false, pendingWork) => {
|
|
87
145
|
const exclude = selection.exclude ?? [];
|
|
88
146
|
const candidates = await context.project.glob(selection.files);
|
|
89
147
|
const chosen = [];
|
|
@@ -105,6 +163,9 @@ export async function executeRules(rules, context) {
|
|
|
105
163
|
chosen.push(filepath);
|
|
106
164
|
}
|
|
107
165
|
const scope = selection.scope;
|
|
166
|
+
const scheduler = pendingWork === undefined
|
|
167
|
+
? scheduleWork
|
|
168
|
+
: (work) => trackWork(pendingWork, work);
|
|
108
169
|
const collected = await mapConcurrent(chosen, context.concurrency, async (filepath) => {
|
|
109
170
|
const source = await context.project.readText(filepath);
|
|
110
171
|
const matches = scope === undefined
|
|
@@ -122,7 +183,7 @@ export async function executeRules(rules, context) {
|
|
|
122
183
|
subjects.push({ kind: 'scope', filepath, ...valid });
|
|
123
184
|
}
|
|
124
185
|
return subjects;
|
|
125
|
-
});
|
|
186
|
+
}, scheduler);
|
|
126
187
|
return collected.flat();
|
|
127
188
|
};
|
|
128
189
|
const selectionContextFor = (name, changedOnly) => ({
|
|
@@ -142,13 +203,14 @@ export async function executeRules(rules, context) {
|
|
|
142
203
|
const { name, rule } = plan;
|
|
143
204
|
emit({ type: 'evaluation', evaluationId, name, subject });
|
|
144
205
|
let trackedUsage;
|
|
145
|
-
let
|
|
206
|
+
let pendingWork;
|
|
146
207
|
let accountingFailure;
|
|
147
208
|
const retainAccountingFailure = (error) => {
|
|
148
209
|
if (accountingFailure !== undefined)
|
|
149
210
|
return;
|
|
150
211
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
151
212
|
accountingFailure = { error: failure };
|
|
213
|
+
abortLsp();
|
|
152
214
|
reportFailure(failure);
|
|
153
215
|
};
|
|
154
216
|
const throwIfAccountingFailed = () => {
|
|
@@ -183,14 +245,13 @@ export async function executeRules(rules, context) {
|
|
|
183
245
|
const checkContext = {
|
|
184
246
|
...projectContextFor(name, evaluationId),
|
|
185
247
|
ruleName: name,
|
|
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
252
|
const call = evaluateAi(subject, options, { ...projectContextFor(name, evaluationId), ruleName: name }, runtimeOf(plan), observer);
|
|
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;
|
|
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,4 @@
|
|
|
1
|
-
import type { AiUsage, CheckResult, GroupSubject, JsonValue, LineRange, 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
2
|
export interface DiffChanges {
|
|
3
3
|
readonly changedFiles: ReadonlySet<string>;
|
|
4
4
|
readonly changedLines: ReadonlyMap<string, readonly LineRange[]>;
|
|
@@ -15,6 +15,10 @@ export interface Project {
|
|
|
15
15
|
readYaml(path: string): Promise<readonly JsonValue[]>;
|
|
16
16
|
readCsv(path: string, options?: CsvOptions): Promise<readonly CsvRow[]>;
|
|
17
17
|
}
|
|
18
|
+
export interface LspBackend extends LspTools {
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export type LspBackendFactory = (project: Project, signal: AbortSignal) => Awaitable<LspBackend>;
|
|
18
22
|
export interface CsvOptions {
|
|
19
23
|
readonly headers?: boolean;
|
|
20
24
|
readonly delimiter?: string;
|
|
@@ -75,6 +79,9 @@ export interface DecisionRuntime {
|
|
|
75
79
|
export interface RunContext {
|
|
76
80
|
readonly project: Project;
|
|
77
81
|
readonly isTarget: (filepath: string) => boolean;
|
|
82
|
+
readonly createLspBackend?: LspBackendFactory;
|
|
83
|
+
readonly createRubyLspBackend?: LspBackendFactory;
|
|
84
|
+
readonly createTypescriptLspBackend?: LspBackendFactory;
|
|
78
85
|
readonly changedFiles?: ReadonlySet<string>;
|
|
79
86
|
readonly changedLines?: ReadonlyMap<string, readonly LineRange[]>;
|
|
80
87
|
readonly concurrency: number;
|
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;
|
|
@@ -15,6 +17,39 @@ export interface LineRange {
|
|
|
15
17
|
readonly start: number;
|
|
16
18
|
readonly end: number;
|
|
17
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
|
+
}
|
|
18
53
|
export interface ScopeMatch<T = JsonValue> {
|
|
19
54
|
readonly id: string;
|
|
20
55
|
readonly source: string;
|
|
@@ -96,6 +131,9 @@ 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;
|
|
99
137
|
readonly changedFiles?: ReadonlySet<string>;
|
|
100
138
|
readonly changedLines?: ReadonlyMap<string, readonly LineRange[]>;
|
|
101
139
|
trace(message: string, details?: JsonValue): void;
|
|
@@ -112,6 +150,8 @@ export interface CheckContext<T = JsonValue> extends SelectionContext {
|
|
|
112
150
|
readonly ruleName: string;
|
|
113
151
|
ai(options: AiOptions<T>): Promise<CheckResult>;
|
|
114
152
|
}
|
|
153
|
+
export declare function isDiffMode(context: ProjectContext): boolean;
|
|
154
|
+
export declare function isInDiff(subject: ScopeSubject<unknown>, context: ProjectContext): boolean;
|
|
115
155
|
export declare const RULE_BRAND: symbol;
|
|
116
156
|
export declare abstract class Rule<T = JsonValue> {
|
|
117
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/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,
|
|
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, DiffChanges, 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
package/dist/infra/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
+
import { createLspBackend, createRubyLspBackend, createTypescriptLspBackend } from '../adapter/lsp/backend.js';
|
|
4
5
|
import { runProgram } from '../adapter/cli/program.js';
|
|
5
6
|
import { terminalCapabilities } from '../adapter/cli/progress.js';
|
|
6
7
|
import { createRedactor, escapeDiagnostic } from '../adapter/cli/text.js';
|
|
@@ -67,7 +68,12 @@ export function createServices(cwd, env) {
|
|
|
67
68
|
...ruleServices,
|
|
68
69
|
createProject: project.createProject,
|
|
69
70
|
resolveTarget: project.resolveTarget,
|
|
70
|
-
runRules: execution.executeRules,
|
|
71
|
+
runRules: (rules, runContext) => execution.executeRules(rules, {
|
|
72
|
+
...runContext,
|
|
73
|
+
createLspBackend: runContext.createLspBackend ?? createLspBackend,
|
|
74
|
+
createRubyLspBackend: runContext.createRubyLspBackend ?? createRubyLspBackend,
|
|
75
|
+
createTypescriptLspBackend: runContext.createTypescriptLspBackend ?? createTypescriptLspBackend,
|
|
76
|
+
}),
|
|
71
77
|
decisionResolver: (thresholdOverride, pricingOverrides) => settings.createDecisionResolver(env, thresholdOverride, pricingOverrides),
|
|
72
78
|
async collectChanges(root, base) {
|
|
73
79
|
const { collectChanges } = await import('../adapter/git/changes.js');
|
|
@@ -98,7 +104,12 @@ export function createServices(cwd, env) {
|
|
|
98
104
|
...ruleServices,
|
|
99
105
|
createProject: project.createProject,
|
|
100
106
|
resolveTarget: project.resolveTarget,
|
|
101
|
-
runRules: execution.executeRules,
|
|
107
|
+
runRules: (rules, runContext) => execution.executeRules(rules, {
|
|
108
|
+
...runContext,
|
|
109
|
+
createLspBackend: runContext.createLspBackend ?? createLspBackend,
|
|
110
|
+
createRubyLspBackend: runContext.createRubyLspBackend ?? createRubyLspBackend,
|
|
111
|
+
createTypescriptLspBackend: runContext.createTypescriptLspBackend ?? createTypescriptLspBackend,
|
|
112
|
+
}),
|
|
102
113
|
decisionResolver: (thresholdOverride, pricingOverrides) => settings.createDecisionResolver(env, thresholdOverride, pricingOverrides),
|
|
103
114
|
};
|
|
104
115
|
},
|
|
@@ -107,7 +118,7 @@ export function createServices(cwd, env) {
|
|
|
107
118
|
const { runRuleTests } = await import('../adapter/filesystem/rule-tests.js');
|
|
108
119
|
return {
|
|
109
120
|
...ruleServices,
|
|
110
|
-
testRules: (rules, onResult, timeoutMs, concurrency, includeAI) => runRuleTests(rules, onResult, timeoutMs, concurrency, includeAI),
|
|
121
|
+
testRules: (rules, onResult, timeoutMs, concurrency, includeAI) => runRuleTests(rules, onResult, timeoutMs, concurrency, includeAI, false, createLspBackend, createRubyLspBackend, createTypescriptLspBackend),
|
|
111
122
|
};
|
|
112
123
|
},
|
|
113
124
|
async update(_context) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alex-dbs/coderuler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"skills": "1.7.2",
|
|
96
96
|
"ssri": "14.0.0",
|
|
97
97
|
"string-width": "8.3.0",
|
|
98
|
+
"vscode-languageserver-protocol": "3.18.5",
|
|
98
99
|
"yaml": "^2.9.1"
|
|
99
100
|
},
|
|
100
101
|
"devDependencies": {
|
|
@@ -334,9 +334,62 @@ Results validate; supplied messages survive reporting. Violations without a mess
|
|
|
334
334
|
|
|
335
335
|
`changedFiles` contains paths that changed in the project. `changedLines` maps changed paths to added or replaced lines. These values are absent outside `--diff` and `--pr` runs. In those runs, an empty set means no changed files. An empty map means no exact line ranges are available. A missing entry for a path means line data is unavailable, for example for binary or conversion-attributed files. An entry with `[]` means the file changed but has no added lines.
|
|
336
336
|
|
|
337
|
+
`isDiffMode(context)` returns `true` when either `changedFiles` or `changedLines` is defined, even if empty; it returns `false` when both are absent. `isInDiff(subject, context)` returns `true` when the subject's line span overlaps a known changed range; a missing span or range returns `false`.
|
|
338
|
+
|
|
339
|
+
Use both helpers to keep full scans outside diff mode:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import {
|
|
343
|
+
Rule,
|
|
344
|
+
isDiffMode,
|
|
345
|
+
isInDiff,
|
|
346
|
+
type ProjectContext,
|
|
347
|
+
type ScopeSubject,
|
|
348
|
+
} from '@alex-dbs/coderuler';
|
|
349
|
+
|
|
350
|
+
export default class Local extends Rule {
|
|
351
|
+
filter = (subject: ScopeSubject, context: ProjectContext) =>
|
|
352
|
+
!isDiffMode(context) || isInDiff(subject, context);
|
|
353
|
+
}
|
|
354
|
+
```
|
|
337
355
|
|
|
338
356
|
Use `trace` for decision evidence visible during inspection; keep normal execution free of ad-hoc console output. AI/regex projection callbacks receive only `ProjectContext`, not the full check context.
|
|
339
357
|
|
|
358
|
+
`context.goLsp` provides Go-to-definition, declaration, type-definition, implementation, reference, hover, document-symbol, workspace-symbol, caller, and callee queries. `context.rubyLsp` provides Ruby definitions, references, hover, document symbols, and workspace symbols. `context.typescriptLsp` provides TypeScript definitions, type definitions, implementations, references, hover, document symbols, workspace symbols, callers, and callees; declaration lookup is not supported. LSP use is optional. Install Go and `gopls`, Shopify Ruby LSP (`ruby-lsp`), or native TypeScript 7's `tsc` on `PATH` only when a rule uses that language's helpers. CodeRuler does not install these servers. An unsupported operation rejects with a capability error; for example, gopls v0.24.0 does not support declaration lookup.
|
|
359
|
+
|
|
360
|
+
Pass file paths and positive, 1-based line and UTF-16 column coordinates. A column counts UTF-16 code units. Navigation and hover use column 1 when omitted. Caller and callee queries (Go and TypeScript) use the enclosing named function or method when the column is omitted. Their required `depth` is a nonnegative integer; depth 1 returns direct neighbors. Set `includeDependencies: true` to include available dependency or standard-library source outside the project; the default is `false`. Ruby dependency definitions are excluded by default and can be requested with `includeDependencies: true`.
|
|
361
|
+
|
|
362
|
+
Caller and callee results are a static call graph. They can omit dynamic calls and do not load every possible package consumer. Use them as the server's known call relationships, not as a complete runtime graph.
|
|
363
|
+
|
|
364
|
+
Call graph helpers from an async custom check:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { Rule, pass, scopes, violation } from '@alex-dbs/coderuler';
|
|
368
|
+
import type { Evaluator } from '@alex-dbs/coderuler';
|
|
369
|
+
|
|
370
|
+
export default class CallGraphCheck extends Rule {
|
|
371
|
+
files = ['**/*.go'];
|
|
372
|
+
scope = scopes.astGrep({
|
|
373
|
+
language: 'go',
|
|
374
|
+
rule: { any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }] },
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
check: Evaluator = async (subject, context) => {
|
|
378
|
+
if (subject.kind !== 'scope' || subject.lines === undefined) return pass();
|
|
379
|
+
const target = { filepath: subject.filepath, line: subject.lines.start };
|
|
380
|
+
const [callers, callees] = await Promise.all([
|
|
381
|
+
context.goLsp.callers(target, { depth: 2 }),
|
|
382
|
+
context.goLsp.callees(target, { depth: 2, includeDependencies: true }),
|
|
383
|
+
]);
|
|
384
|
+
return callers.length + callees.length === 0
|
|
385
|
+
? pass()
|
|
386
|
+
: violation('Review callers and callees');
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The example assumes `pass` and `violation` are imported from the SDK.
|
|
392
|
+
|
|
340
393
|
## Regex: matching means passing
|
|
341
394
|
|
|
342
395
|
`regex<T = JsonValue>(options: RegexOptions<T>): Evaluator<T>` takes `{ readonly pattern: RegExp; readonly violationMessage?: string; readonly input?: (subject: ScopeSubject<T>, context: ProjectContext) => Awaitable<string> }`.
|