@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
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';
|
|
@@ -17,7 +18,7 @@ function loadEnvironment(cwd) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
export function createServices(cwd, env) {
|
|
20
|
-
const redact = createRedactor([env.TYPESAFE_API_KEY, env.GITHUB_TOKEN]);
|
|
21
|
+
const redact = createRedactor([env.TYPESAFE_API_KEY, env.OPENAI_API_KEY, env.GITHUB_TOKEN]);
|
|
21
22
|
let dependenciesPromise;
|
|
22
23
|
let ruleServicesPromise;
|
|
23
24
|
const getDependencies = (context) => dependenciesPromise ??= (async () => {
|
|
@@ -67,16 +68,25 @@ 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
|
-
async
|
|
73
|
-
const {
|
|
74
|
-
return
|
|
78
|
+
async collectChanges(root, base) {
|
|
79
|
+
const { collectChanges } = await import('../adapter/git/changes.js');
|
|
80
|
+
return collectChanges(root, base);
|
|
75
81
|
},
|
|
76
82
|
async checkoutPullRequest(selector, baseBranch) {
|
|
77
83
|
const { createPullRequestCheckout } = await import('../adapter/github/pull-request.js');
|
|
78
84
|
return createPullRequestCheckout(cwd, env)(selector, baseBranch);
|
|
79
85
|
},
|
|
86
|
+
async checkoutRuleRepository(selector) {
|
|
87
|
+
const { createRuleRepositoryCheckout } = await import('../adapter/git/rules.js');
|
|
88
|
+
return createRuleRepositoryCheckout(env)(selector);
|
|
89
|
+
},
|
|
80
90
|
async commentOnPullRequest(root, comments, pullRequest) {
|
|
81
91
|
const { createGithubCommenter } = await import('../adapter/github/comments.js');
|
|
82
92
|
return createGithubCommenter(env)(root, comments, pullRequest);
|
|
@@ -94,7 +104,12 @@ export function createServices(cwd, env) {
|
|
|
94
104
|
...ruleServices,
|
|
95
105
|
createProject: project.createProject,
|
|
96
106
|
resolveTarget: project.resolveTarget,
|
|
97
|
-
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
|
+
}),
|
|
98
113
|
decisionResolver: (thresholdOverride, pricingOverrides) => settings.createDecisionResolver(env, thresholdOverride, pricingOverrides),
|
|
99
114
|
};
|
|
100
115
|
},
|
|
@@ -103,7 +118,7 @@ export function createServices(cwd, env) {
|
|
|
103
118
|
const { runRuleTests } = await import('../adapter/filesystem/rule-tests.js');
|
|
104
119
|
return {
|
|
105
120
|
...ruleServices,
|
|
106
|
-
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),
|
|
107
122
|
};
|
|
108
123
|
},
|
|
109
124
|
async update(_context) {
|
|
@@ -160,7 +175,11 @@ export async function runCli(argv) {
|
|
|
160
175
|
}
|
|
161
176
|
catch (error) {
|
|
162
177
|
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
-
const redact = createRedactor([
|
|
178
|
+
const redact = createRedactor([
|
|
179
|
+
process.env.TYPESAFE_API_KEY,
|
|
180
|
+
process.env.OPENAI_API_KEY,
|
|
181
|
+
process.env.GITHUB_TOKEN,
|
|
182
|
+
]);
|
|
164
183
|
process.stderr.write(`coderuler: ${redact(escapeDiagnostic(message))}\n`);
|
|
165
184
|
return EXIT_FAILURE;
|
|
166
185
|
}
|
package/dist/infra/settings.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { JevAdapter } from '../adapter/decision/jev.js';
|
|
2
|
+
import { OpenAIAdapter } from '../adapter/decision/openai.js';
|
|
2
3
|
const DEFAULT_BACKEND = 'jev';
|
|
3
4
|
const DEFAULT_MODEL = 'jev-latest';
|
|
4
5
|
const DEFAULT_BASE_URL = 'https://api.typesafe.ai';
|
|
6
|
+
const OPENAI_DEFAULT_MODEL = 'gpt-6-luna';
|
|
7
|
+
const OPENAI_DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
5
8
|
const DEFAULT_THRESHOLD = 0.4;
|
|
6
9
|
const DEFAULT_INPUT_COST_PER_MILLION = 0.042;
|
|
10
|
+
const OPENAI_DEFAULT_INPUT_COST_PER_MILLION = 0.10;
|
|
7
11
|
const DEFAULT_OUTPUT_COST_PER_MILLION = 0;
|
|
8
12
|
const THRESHOLD_MESSAGE = 'must be a number between 0 and 1';
|
|
9
13
|
const DECIMAL = /^(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$/;
|
|
@@ -56,9 +60,10 @@ export function createDecisionResolver(env, thresholdOverride, pricingOverrides)
|
|
|
56
60
|
validatePrice(outputOverride, '--output-cost-per-million');
|
|
57
61
|
return settings => {
|
|
58
62
|
const backend = settings.backend ?? setting(env, 'RK_BACKEND') ?? DEFAULT_BACKEND;
|
|
59
|
-
if (backend !== 'jev') {
|
|
60
|
-
throw new Error(`Unknown decision backend '${backend}'; supported backends:
|
|
63
|
+
if (backend !== 'jev' && backend !== 'openai') {
|
|
64
|
+
throw new Error(`Unknown decision backend '${backend}'; supported backends: jev, openai`);
|
|
61
65
|
}
|
|
66
|
+
const isOpenAI = backend === 'openai';
|
|
62
67
|
let threshold;
|
|
63
68
|
if (flagThreshold !== undefined) {
|
|
64
69
|
threshold = flagThreshold;
|
|
@@ -77,14 +82,20 @@ export function createDecisionResolver(env, thresholdOverride, pricingOverrides)
|
|
|
77
82
|
validatePrice(settings.outputCostPerMillion, 'rule.settings.outputCostPerMillion');
|
|
78
83
|
}
|
|
79
84
|
const pricing = {
|
|
80
|
-
inputCostPerMillion: price(inputOverride, settings.inputCostPerMillion, env, 'RK_INPUT_COST_PER_MILLION', DEFAULT_INPUT_COST_PER_MILLION),
|
|
85
|
+
inputCostPerMillion: price(inputOverride, settings.inputCostPerMillion, env, 'RK_INPUT_COST_PER_MILLION', isOpenAI ? OPENAI_DEFAULT_INPUT_COST_PER_MILLION : DEFAULT_INPUT_COST_PER_MILLION),
|
|
81
86
|
outputCostPerMillion: price(outputOverride, settings.outputCostPerMillion, env, 'RK_OUTPUT_COST_PER_MILLION', DEFAULT_OUTPUT_COST_PER_MILLION),
|
|
82
87
|
};
|
|
83
|
-
const model = settings.model ??
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const model = settings.model ??
|
|
89
|
+
setting(env, isOpenAI ? 'OPENAI_DEFAULT_MODEL' : 'TYPESAFE_DEFAULT_MODEL') ??
|
|
90
|
+
(isOpenAI ? OPENAI_DEFAULT_MODEL : DEFAULT_MODEL);
|
|
91
|
+
const baseUrl = setting(env, isOpenAI ? 'OPENAI_BASE_URL' : 'TYPESAFE_BASE_URL') ??
|
|
92
|
+
(isOpenAI ? OPENAI_DEFAULT_BASE_URL : DEFAULT_BASE_URL);
|
|
93
|
+
const apiKeyName = isOpenAI ? 'OPENAI_API_KEY' : 'TYPESAFE_API_KEY';
|
|
94
|
+
const apiKey = setting(env, apiKeyName);
|
|
95
|
+
if (apiKey === undefined)
|
|
96
|
+
throw new Error(`No decision API key: set ${apiKeyName}`);
|
|
97
|
+
if (isOpenAI) {
|
|
98
|
+
return { backend: new OpenAIAdapter({ model, baseUrl, apiKey }), threshold, pricing };
|
|
88
99
|
}
|
|
89
100
|
return { backend: new JevAdapter({ model, baseUrl, apiKey }), threshold, pricing };
|
|
90
101
|
};
|
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",
|
|
@@ -90,10 +90,12 @@
|
|
|
90
90
|
"jsonpath-plus": "11.2.1",
|
|
91
91
|
"npm-install-checks": "9.0.0",
|
|
92
92
|
"npm-package-arg": "14.0.0",
|
|
93
|
+
"openai": "7.45.1",
|
|
93
94
|
"pacote": "22.0.0",
|
|
94
95
|
"skills": "1.7.2",
|
|
95
96
|
"ssri": "14.0.0",
|
|
96
97
|
"string-width": "8.3.0",
|
|
98
|
+
"vscode-languageserver-protocol": "3.18.5",
|
|
97
99
|
"yaml": "^2.9.1"
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: coderuler-new-rule
|
|
3
|
-
description: Create CodeRuler rules, including custom selection, filters and checks, inline tests, and inspection.
|
|
3
|
+
description: Create CodeRuler rules, including custom selection, filters and checks, inline or companion tests, and inspection.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create a CodeRuler rule
|
|
@@ -29,10 +29,14 @@ Start the name with its language or technology prefix, such as `go-`, `ts-`, or
|
|
|
29
29
|
|
|
30
30
|
Keep rules short, with only a few lines of policy code. Reuse built-in selectors, evaluators, and standard libraries before writing custom logic. Add helpers only for real reuse.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Test cases can live in the rule module or in a sibling `.test` module. For example, `sample.rule.ts` pairs with `sample.test.ts`; keep the same extension for `.mts`, `.js`, and `.mjs`. The `test` command combines exports from both modules. It runs ordinary `tests` by default; include `--ai` to run `aiTests`.
|
|
33
|
+
|
|
34
|
+
For a deterministic rule, export named `tests` with `.passes()` and `.violates()` cases. For an AI rule, put every test case in `aiTests` and do not add a separate `tests` export. Omit `.skips()` assertions when creating a rule: skipped inputs never reach the check and provide no evidence of policy behavior.
|
|
33
35
|
|
|
34
36
|
For an AI rule, use `instruction` to state what compliant code does, beginning with a direct requirement. Add only applicability boundaries or real exceptions needed to define the policy; keep one policy per rule. Keep reporting language in `violationMessage`, examples in `examples`, and test cases in `aiTests`—not in the instruction. See [AI instruction style](reference.md#ai-instruction-style).
|
|
35
37
|
|
|
38
|
+
Keep Jev's input to the smallest scope that still supports a correct decision. For function-local code rules, use `scopes.astGrep` to select one function or method per scope and filter for relevant syntax, such as pointer dereferences.
|
|
39
|
+
|
|
36
40
|
Run the relevant cases before the policy change and require an assertion mismatch for the intended violation, not a loading error. Implement the policy, then require every case to pass.
|
|
37
41
|
|
|
38
42
|
This complete example requires at least two replicas whenever `spec.replicas` is an integer:
|
|
@@ -92,7 +96,7 @@ coderuler validate
|
|
|
92
96
|
coderuler test
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
`validate` checks the module contract, not policy behavior or TypeScript semantics. `check` and `validate` do not
|
|
99
|
+
`validate` checks the module contract, not policy behavior or TypeScript semantics. `check` and `validate` do not run test cases or load sibling `.test` modules. `test` with no selected cases is an error, not success.
|
|
96
100
|
|
|
97
101
|
Useful test options:
|
|
98
102
|
|
|
@@ -173,18 +173,23 @@ The rule name is `proto-route-version`, saved as `.coderuler/proto-route-version
|
|
|
173
173
|
The public shapes are:
|
|
174
174
|
|
|
175
175
|
```ts
|
|
176
|
-
import type { Awaitable, JsonValue, SourceFile, ScopeMatch } from '@alex-dbs/coderuler';
|
|
176
|
+
import type { Awaitable, JsonValue, LineRange, ProjectContext, SourceFile, ScopeMatch } from '@alex-dbs/coderuler';
|
|
177
177
|
|
|
178
178
|
type ScopeSelector<T = JsonValue> =
|
|
179
|
-
(file: SourceFile) => Awaitable<readonly ScopeMatch<T>[]>;
|
|
179
|
+
(file: SourceFile, context?: ProjectContext) => Awaitable<readonly ScopeMatch<T>[]>;
|
|
180
180
|
// SourceFile: { readonly filepath: string; readonly source: string }
|
|
181
181
|
// ScopeMatch<T>: { readonly id: string; readonly source: string;
|
|
182
182
|
// readonly data?: T;
|
|
183
183
|
// readonly captures?: Readonly<Record<string, string | readonly string[]>>;
|
|
184
|
-
// readonly lines?:
|
|
184
|
+
// readonly lines?: LineRange }
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
For `--diff` and `--pr`, CodeRuler selects changed files for the rule. It does not narrow custom scopes to changed lines. Your custom selector must use `changedLines` if it needs line-level filtering. A selector that ignores line ranges still checks every selected scope in each changed file.
|
|
188
|
+
|
|
189
|
+
The engine passes `ProjectContext`; direct calls can omit it. Use stable nonempty IDs and actual source text. Line spans are 1-based and inclusive. `data` must be finite, acyclic, JSON-compatible (including `null`, `false`, `0`, and `''`); captures contain only strings/string arrays.
|
|
190
|
+
When a selector reads diff fields, annotate its `context` parameter as `ProjectContext`. This lets direct callers omit the optional parameter.
|
|
191
|
+
|
|
192
|
+
`changedFiles` and `changedLines` are `undefined` outside diff/PR checks. In those checks, an empty `changedFiles` set means no files changed. An empty `changedLines` map means there are no entries with line data. `changedLines.get(filepath)` returns added or replaced lines in that file. The ranges are 1-based and inclusive. A missing entry 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.
|
|
188
193
|
|
|
189
194
|
`Selection<T>` is `{ readonly files: readonly string[]; readonly exclude?: readonly string[]; readonly scope?: ScopeSelector<T> }`. `context.select<T>(selection)` returns `Promise<readonly ScopeSubject<T>[]>`. A scope subject adds `kind: 'scope'` and `filepath` to its match. Custom `select` returns flattened scope subjects, not group subjects.
|
|
190
195
|
|
|
@@ -197,14 +202,21 @@ import type {
|
|
|
197
202
|
ScopeSubject, SelectionContext,
|
|
198
203
|
} from '@alex-dbs/coderuler';
|
|
199
204
|
|
|
200
|
-
const owners: ScopeSelector<string> = ({ source }) =>
|
|
201
|
-
source.split('\n').flatMap((line, index) => {
|
|
205
|
+
const owners: ScopeSelector<string> = ({ filepath, source }, context: ProjectContext) => {
|
|
206
|
+
const candidates = source.split('\n').flatMap((line, index) => {
|
|
202
207
|
const owner = /^# owner: (\S+)\s*$/.exec(line)?.[1];
|
|
203
208
|
return owner === undefined ? [] : [{
|
|
204
209
|
id: `owner:${index + 1}`, source: line, data: owner,
|
|
205
210
|
lines: { start: index + 1, end: index + 1 },
|
|
206
211
|
}];
|
|
207
212
|
});
|
|
213
|
+
if (context.changedFiles === undefined) return candidates;
|
|
214
|
+
if (!context.changedFiles.has(filepath)) return [];
|
|
215
|
+
const ranges = context.changedLines?.get(filepath);
|
|
216
|
+
if (ranges === undefined) return candidates;
|
|
217
|
+
return candidates.filter(scope => scope.lines !== undefined && ranges.some(range =>
|
|
218
|
+
range.start <= scope.lines.end && scope.lines.start <= range.end));
|
|
219
|
+
};
|
|
208
220
|
|
|
209
221
|
export default class AssignedOwner extends Rule<string> {
|
|
210
222
|
files = ['src/**/*.txt'];
|
|
@@ -316,13 +328,68 @@ Results validate; supplied messages survive reporting. Violations without a mess
|
|
|
316
328
|
|
|
317
329
|
| Context | Available members |
|
|
318
330
|
| ------------------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
319
|
-
| `ProjectContext` | `
|
|
331
|
+
| `ProjectContext` | `project`; optional `changedFiles: ReadonlySet<string>` and `changedLines: ReadonlyMap<string, readonly LineRange[]>`; `trace(message, details?)`. |
|
|
320
332
|
| `SelectionContext extends ProjectContext` | `select<T>(selection: Selection<T>): Promise<readonly ScopeSubject<T>[]>`. |
|
|
321
333
|
| `CheckContext<T = JsonValue> extends SelectionContext` | `readonly ruleName: string`; `ai(options: AiOptions<T>): Promise<CheckResult>`. |
|
|
322
334
|
|
|
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
|
+
|
|
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
|
+
```
|
|
323
355
|
|
|
324
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.
|
|
325
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
|
+
|
|
326
393
|
## Regex: matching means passing
|
|
327
394
|
|
|
328
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> }`.
|
|
@@ -347,12 +414,12 @@ Default input is `String(data)` for scalar/null data, otherwise source text. `in
|
|
|
347
414
|
`ai<T = JsonValue>(options: AiOptions<T>): Evaluator<T>` accepts exactly one of `rule` or `instruction`:
|
|
348
415
|
|
|
349
416
|
- `rule: string`: nonempty policy text. This option is deprecated; use `instruction` for new rules.
|
|
350
|
-
- `instruction: string`: nonempty text describing the expected behavior of the selected code. Write a direct requirement that
|
|
417
|
+
- `instruction: string`: nonempty text describing the expected behavior of the selected code. Write a direct requirement that the selected provider can judge as respected or broken.
|
|
351
418
|
- `examples?: { readonly good?: readonly string[]; readonly bad?: readonly string[] }`: conditioning examples, dense arrays of nonempty strings; omitted/empty lists are valid.
|
|
352
419
|
- `violationMessage?: string`: nonempty reporting-only message, not sent as instructions.
|
|
353
420
|
- `input?: (subject: Subject<T>, context: ProjectContext) => Awaitable<JsonValue>`: replace the default projection; output validates as JSON-compatible.
|
|
354
421
|
|
|
355
|
-
Jev
|
|
422
|
+
Jev and OpenAI use the fixed question "Is the rule respected?" A higher probability means the rule is respected. The configured threshold decides pass or violation.
|
|
356
423
|
|
|
357
424
|
### AI instruction style
|
|
358
425
|
|
|
@@ -362,7 +429,7 @@ Weak: "Report a violation when a Go pointer is dereferenced without a nil check.
|
|
|
362
429
|
|
|
363
430
|
Better: "Check each Go pointer for nil before dereferencing it." For nested fields, add the needed boundary: "Check every pointer in the field path before dereferencing the field."
|
|
364
431
|
|
|
365
|
-
Keep reporting language in `violationMessage`, conditioning examples in `examples`, and test fixtures in `aiTests`; they do not belong in the instruction. Use selectors and filters for deterministic applicability
|
|
432
|
+
Keep reporting language in `violationMessage`, conditioning examples in `examples`, and test fixtures in `aiTests`; they do not belong in the instruction. Use selectors and filters for deterministic applicability. Keep provider input to the smallest scope that still supports a correct decision. For function-local code policies, use `scopes.astGrep` to select one function or method per scope, then filter for relevant syntax such as pointer dereferences. Include surrounding context only when needed to judge the policy.
|
|
366
433
|
|
|
367
434
|
Compare wording with the policy and dataset held fixed:
|
|
368
435
|
|
|
@@ -372,7 +439,7 @@ Compare wording with the policy and dataset held fixed:
|
|
|
372
439
|
4. Compare compliant scores toward `1` and violation scores toward `0` separately. Inspect individual regressions and the weakest score in each group, not just averages.
|
|
373
440
|
5. Keep wording that improves separation without changing the policy or introducing wrong decisions. Repeat a close comparison before treating a small gain as reliable.
|
|
374
441
|
|
|
375
|
-
`PASS` in test output means the expected result matched. A passing violation test still needs a low
|
|
442
|
+
`PASS` in test output means the expected result matched. A passing violation test still needs a low AI score. Do not lower the threshold, relabel fixtures, or copy test cases into the instruction to improve the comparison. Results on a tuning dataset do not prove performance on unseen code.
|
|
376
443
|
|
|
377
444
|
|
|
378
445
|
This rule checks Go pointer dereferences, including nested fields:
|
|
@@ -472,9 +539,9 @@ Default AI input is `{ filepath, scope: { id, data, captures? } }` when data exi
|
|
|
472
539
|
|
|
473
540
|
Inside a custom check, `return context.ai({ instruction: '...' })` evaluates its current subject with effective settings and preserves AI result metadata. The deprecated `rule` option remains accepted for existing rules. Replacing that result requires intentionally preserving probability/model/threshold metadata; token accounting is automatic across all `context.ai` calls, even when returning a different result. `ai(...)` validates options when constructed but performs no loading-time network I/O.
|
|
474
541
|
|
|
475
|
-
`RuleSettings` supports
|
|
542
|
+
`RuleSettings.backend` supports `'jev'` and `'openai'`; Jev is the default. Set `model`, threshold, `inputCostPerMillion`, or `outputCostPerMillion` in `settings` to override defaults. Both providers use a threshold of `0.4`; AI passes only when probability is **strictly greater** than threshold, and equality violates. Threshold precedence is CLI override → rule setting → `RK_THRESHOLD` → `0.4`. Backend precedence is rule setting → `RK_BACKEND` → `jev`. Model precedence is rule setting → selected provider's model variable → provider default. Jev uses `TYPESAFE_DEFAULT_MODEL` (default `jev-latest`) and `TYPESAFE_API_KEY`; OpenAI uses `OPENAI_DEFAULT_MODEL` (default `gpt-6-luna`) and `OPENAI_API_KEY`. The base URL variables are `TYPESAFE_BASE_URL` and `OPENAI_BASE_URL`, defaulting to `https://api.typesafe.ai` and `https://api.openai.com/v1`. Provider configuration and credentials are read only when an AI call selects that backend. Deterministic, skipped, and filtered paths need no key.
|
|
476
543
|
|
|
477
|
-
Inspection reports per-verification `usage: AiUsage` and a final recap: `calls`, `unreportedCalls`, `inputTokens`, `outputTokens`, `inputCost`, and `outputCost` (USD). Counts come from provider responses; missing counts remain unreported and totals cover reported calls only.
|
|
544
|
+
Inspection reports per-verification `usage: AiUsage` and a final recap: `calls`, `unreportedCalls`, `inputTokens`, `outputTokens`, `inputCost`, and `outputCost` (USD). Counts come from provider responses; missing counts remain unreported and totals cover reported calls only. Default input prices are `$0.042` per million tokens for Jev and `$0.10` for OpenAI; both output prices default to `$0`. Price precedence is inspection override → rule setting → `RK_INPUT_COST_PER_MILLION` / `RK_OUTPUT_COST_PER_MILLION` → selected provider default. Prices change estimates, not decisions.
|
|
478
545
|
|
|
479
546
|
|
|
480
547
|
## Project readers
|