@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.
Files changed (38) hide show
  1. package/README.md +3 -1
  2. package/dist/adapter/cli/commands/check.js +21 -10
  3. package/dist/adapter/cli/program.js +2 -2
  4. package/dist/adapter/cli/reports.js +1 -1
  5. package/dist/adapter/cli/services.d.ts +4 -2
  6. package/dist/adapter/decision/jev.js +32 -15
  7. package/dist/adapter/decision/openai.d.ts +11 -0
  8. package/dist/adapter/decision/openai.js +160 -0
  9. package/dist/adapter/filesystem/rule-test-lsp.d.ts +6 -0
  10. package/dist/adapter/filesystem/rule-test-lsp.js +189 -0
  11. package/dist/adapter/filesystem/rule-test-worker.d.ts +5 -0
  12. package/dist/adapter/filesystem/rule-test-worker.js +48 -26
  13. package/dist/adapter/filesystem/rule-tests.d.ts +2 -2
  14. package/dist/adapter/filesystem/rule-tests.js +170 -29
  15. package/dist/adapter/git/changes.d.ts +2 -1
  16. package/dist/adapter/git/changes.js +369 -35
  17. package/dist/adapter/git/rules.d.ts +5 -0
  18. package/dist/adapter/git/rules.js +126 -0
  19. package/dist/adapter/github/pull-request.d.ts +2 -0
  20. package/dist/adapter/github/pull-request.js +9 -3
  21. package/dist/adapter/lsp/backend.d.ts +12 -0
  22. package/dist/adapter/lsp/backend.js +826 -0
  23. package/dist/app/concurrency.d.ts +1 -1
  24. package/dist/app/concurrency.js +12 -8
  25. package/dist/app/execute.js +84 -22
  26. package/dist/app/lsp.d.ts +6 -0
  27. package/dist/app/lsp.js +141 -0
  28. package/dist/app/ports.d.ts +13 -1
  29. package/dist/app/rule.d.ts +52 -10
  30. package/dist/app/rule.js +14 -0
  31. package/dist/app/validate.js +3 -2
  32. package/dist/index.d.ts +3 -3
  33. package/dist/index.js +1 -1
  34. package/dist/infra/runtime.js +27 -8
  35. package/dist/infra/settings.js +19 -8
  36. package/package.json +3 -1
  37. package/skills/coderuler-new-rule/SKILL.md +7 -3
  38. 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[]>;
@@ -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
- let failure;
18
- // A mapper may need to drain started work after it has already failed.
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
- failure = error;
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
- results[index] = await mapper(items[index], index, reportFailure);
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.all(workers);
41
- if (failureObserved)
42
- throw failure;
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
  }
@@ -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
- trace: traceFor(name),
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
- project: context.project,
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 pendingAi;
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
- project: context.project,
246
+ ...projectContextFor(name, evaluationId),
184
247
  ruleName: name,
185
- trace,
186
- select: selection => selectFromRule(name, selection, evaluationId),
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, { project: context.project, ruleName: name, trace }, runtimeOf(plan), observer);
189
- const pending = pendingAi ??= new Set();
190
- pending.add(call);
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
- // Promise.all may reject while sibling calls are still producing usage events.
230
- while (pendingAi !== undefined && pendingAi.size > 0) {
231
- await Promise.allSettled(pendingAi);
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] });
@@ -0,0 +1,6 @@
1
+ import type { LspTools } from './rule.js';
2
+ import type { LspBackendFactory, Project } from './ports.js';
3
+ export declare function createRunLsp(project: Project, createBackend?: LspBackendFactory): {
4
+ readonly tools: LspTools;
5
+ close(): Promise<void>;
6
+ };
@@ -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
+ }
@@ -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;
@@ -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> = (file: SourceFile) => Awaitable<readonly ScopeMatch<T>[]>;
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;
@@ -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
@@ -1,4 +1,4 @@
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 { ai, pass, regex, violation } from './app/evaluators.js';
4
4
  import { astGrep } from './adapter/scope/astgrep.js';