@alex-dbs/coderuler 1.3.0 → 1.4.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 +5 -2
- package/dist/adapter/cli/commands/test.js +2 -2
- package/dist/adapter/cli/program.js +1 -0
- package/dist/adapter/cli/reports.d.ts +1 -1
- package/dist/adapter/cli/reports.js +5 -2
- package/dist/adapter/cli/services.d.ts +1 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +5 -1
- package/dist/adapter/filesystem/rule-test-worker.js +10 -2
- package/dist/adapter/filesystem/rule-tests.js +12 -2
- package/dist/app/evaluators.js +11 -4
- package/dist/app/rule-tests.d.ts +1 -0
- package/dist/app/rule.d.ts +9 -3
- package/package.json +19 -1
- package/skills/coderuler-new-rule/SKILL.md +5 -3
- package/skills/coderuler-new-rule/reference.md +100 -46
package/README.md
CHANGED
|
@@ -114,14 +114,17 @@ export default class SafeResponseHeaders extends Rule {
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
check = ai({
|
|
117
|
-
|
|
117
|
+
instruction: `Reject CR and LF in untrusted values before inserting them into
|
|
118
|
+
HTTP response headers or status lines.
|
|
119
|
+
This policy covers response construction and writes visible in the selected function. Hidden helper behavior and unseen callers are outside its scope.
|
|
120
|
+
Treat serialization arguments and statusMessage as untrusted. Fixed values and values checked for both CR and LF meet the requirement. Preserve fixed HTTP delimiters.`,
|
|
118
121
|
input: subject => (subject as ScopeSubject).source,
|
|
119
122
|
violationMessage: 'Reject CR and LF in request data before writing a response header.',
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
This example shows an AI check for CWE-113.
|
|
127
|
+
This example shows an AI check for CWE-113.
|
|
125
128
|
|
|
126
129
|
AI rules require a configured service and can incur charges.
|
|
127
130
|
|
|
@@ -22,7 +22,7 @@ async function runTest(rulePath, options, context, services) {
|
|
|
22
22
|
context.write(renderRuleTestHeading(ruleName, context.redact, color));
|
|
23
23
|
previousRuleName = ruleName;
|
|
24
24
|
for (let index = 0; index < failures.length; index += 1) {
|
|
25
|
-
context.write(renderRuleTestResult(failures[index], context.redact, color, index === failures.length - 1));
|
|
25
|
+
context.write(renderRuleTestResult(failures[index], context.redact, color, index === failures.length - 1, options.aiScores === true));
|
|
26
26
|
}
|
|
27
27
|
failures.length = 0;
|
|
28
28
|
};
|
|
@@ -53,7 +53,7 @@ async function runTest(rulePath, options, context, services) {
|
|
|
53
53
|
context.write(renderRuleTestHeading(result.ruleName, context.redact, color));
|
|
54
54
|
previousRuleName = result.ruleName;
|
|
55
55
|
}
|
|
56
|
-
context.write(renderRuleTestResult(result, context.redact, color, lastInRule));
|
|
56
|
+
context.write(renderRuleTestResult(result, context.redact, color, lastInRule, options.aiScores === true));
|
|
57
57
|
}
|
|
58
58
|
progress?.message(`Running tests (${passed + failed} completed)`);
|
|
59
59
|
}, options.timeout, options.concurrency, options.ai === true);
|
|
@@ -128,6 +128,7 @@ export function createProgram(services) {
|
|
|
128
128
|
.description('Run the declared test cases for the selected rules')
|
|
129
129
|
.argument('[rule]', 'rule name, module path, or directory')
|
|
130
130
|
.option('--ai', 'include aiTests that use the configured decision backend')
|
|
131
|
+
.option('--ai-scores', 'show Jev scores for aiTests')
|
|
131
132
|
.option('--failures-only', 'show only failed test cases')
|
|
132
133
|
.addOption(new Option('--timeout <duration>', 'per-test timeout (ms, s, or m)')
|
|
133
134
|
.argParser(parseTimeout)
|
|
@@ -4,7 +4,7 @@ import type { ViolationComment } from '../../app/violation-comments.js';
|
|
|
4
4
|
import { type TextRedactor, type TextWriter } from './text.js';
|
|
5
5
|
export type RuleHeadings = (name: string) => readonly string[];
|
|
6
6
|
export declare function renderRuleTestHeading(ruleName: string, redact: TextRedactor, color: boolean): string;
|
|
7
|
-
export declare function renderRuleTestResult(result: RuleTestResult, redact: TextRedactor, color: boolean, lastInRule: boolean): string;
|
|
7
|
+
export declare function renderRuleTestResult(result: RuleTestResult, redact: TextRedactor, color: boolean, lastInRule: boolean, showAIScores?: boolean): string;
|
|
8
8
|
export declare function renderRuleTestSummary(passed: number, failed: number, durationMs: number, redact: TextRedactor, color: boolean): string;
|
|
9
9
|
export declare function createRuleHeadings(rules: readonly LoadedRule[]): RuleHeadings;
|
|
10
10
|
export declare function createMetadataRenderer(write: TextWriter, redact: TextRedactor): (event: RunEvent) => void;
|
|
@@ -36,7 +36,7 @@ function sourceText(line) {
|
|
|
36
36
|
export function renderRuleTestHeading(ruleName, redact, color) {
|
|
37
37
|
return `${paint(escapeText(redact(ruleName)), 'bold', color)}\n`;
|
|
38
38
|
}
|
|
39
|
-
export function renderRuleTestResult(result, redact, color, lastInRule) {
|
|
39
|
+
export function renderRuleTestResult(result, redact, color, lastInRule, showAIScores = false) {
|
|
40
40
|
const safe = (text) => escapeText(redact(text));
|
|
41
41
|
const matches = result.expected === result.actual;
|
|
42
42
|
const label = paint(safe(matches ? 'PASS' : 'FAIL'), matches ? 'green' : 'red', color);
|
|
@@ -44,8 +44,11 @@ export function renderRuleTestResult(result, redact, color, lastInRule) {
|
|
|
44
44
|
const detail = matches
|
|
45
45
|
? ''
|
|
46
46
|
: safe(` (expected ${result.expected}, actual ${result.actual})`);
|
|
47
|
+
const aiScores = showAIScores && result.aiScores !== undefined && result.aiScores.length > 0
|
|
48
|
+
? ` (${result.aiScores.length === 1 ? 'Jev score' : 'Jev scores'}: ${result.aiScores.join(', ')})`
|
|
49
|
+
: '';
|
|
47
50
|
const duration = paint(safe(` (${formatDuration(result.durationMs)})`), 'dim', color);
|
|
48
|
-
return `${branch}${label} ${safe(result.name)}${detail}${duration}\n`;
|
|
51
|
+
return `${branch}${label} ${safe(result.name)}${detail}${aiScores}${duration}\n`;
|
|
49
52
|
}
|
|
50
53
|
export function renderRuleTestSummary(passed, failed, durationMs, redact, color) {
|
|
51
54
|
const safe = (text) => escapeText(redact(text));
|
|
@@ -8,9 +8,13 @@ export interface RuleTestWorkerRequest {
|
|
|
8
8
|
readonly ai: boolean;
|
|
9
9
|
readonly dependencies?: PreparedRuleDependencies;
|
|
10
10
|
}
|
|
11
|
+
export interface RuleTestWorkerOutcome {
|
|
12
|
+
readonly actual: RuleTestCase['expected'];
|
|
13
|
+
readonly aiScores?: readonly number[];
|
|
14
|
+
}
|
|
11
15
|
export type RuleTestWorkerMessage = {
|
|
12
16
|
readonly id: number;
|
|
13
|
-
readonly
|
|
17
|
+
readonly outcome: RuleTestWorkerOutcome;
|
|
14
18
|
} | {
|
|
15
19
|
readonly id: number;
|
|
16
20
|
readonly error: string;
|
|
@@ -32,7 +32,15 @@ async function runCase({ root, ruleFile, files, ai, dependencies }) {
|
|
|
32
32
|
const report = reports[0];
|
|
33
33
|
if (reports.length !== 1 || report === undefined)
|
|
34
34
|
throw new Error('Expected exactly one rule report');
|
|
35
|
-
|
|
35
|
+
let aiScores;
|
|
36
|
+
if (ai) {
|
|
37
|
+
for (const evaluation of report.evaluations) {
|
|
38
|
+
const score = evaluation.result.probability;
|
|
39
|
+
if (score !== undefined)
|
|
40
|
+
(aiScores ??= []).push(score);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { actual: report.status, ...(aiScores === undefined ? {} : { aiScores }) };
|
|
36
44
|
}
|
|
37
45
|
const port = parentPort;
|
|
38
46
|
if (port === null)
|
|
@@ -40,7 +48,7 @@ if (port === null)
|
|
|
40
48
|
port.start();
|
|
41
49
|
port.ref();
|
|
42
50
|
port.on('message', (request) => {
|
|
43
|
-
void runCase(request).then(
|
|
51
|
+
void runCase(request).then(outcome => port.postMessage({ id: request.id, outcome }), error => port.postMessage({
|
|
44
52
|
id: request.id,
|
|
45
53
|
error: error instanceof Error ? error.message : String(error),
|
|
46
54
|
}));
|
|
@@ -75,7 +75,7 @@ class RuleTestWorkerSession {
|
|
|
75
75
|
else if ('error' in message)
|
|
76
76
|
result.reject(new Error(message.error));
|
|
77
77
|
else
|
|
78
|
-
result.resolve(message.
|
|
78
|
+
result.resolve(message.outcome);
|
|
79
79
|
};
|
|
80
80
|
this.rejectPending = error => {
|
|
81
81
|
if (performance.now() >= deadline)
|
|
@@ -129,6 +129,7 @@ async function runCase(entry, example, ai, timeoutMs, session, onFailure) {
|
|
|
129
129
|
try {
|
|
130
130
|
const root = await mkdtemp(join(await realpath(tmpdir()), 'coderuler-test-'));
|
|
131
131
|
let actual;
|
|
132
|
+
let aiScores;
|
|
132
133
|
let active;
|
|
133
134
|
try {
|
|
134
135
|
if (session.cancelled)
|
|
@@ -138,10 +139,16 @@ async function runCase(entry, example, ai, timeoutMs, session, onFailure) {
|
|
|
138
139
|
else {
|
|
139
140
|
active = session.current ??= new RuleTestWorkerSession();
|
|
140
141
|
const dependencies = getRuleDependencies(entry.ruleFile);
|
|
141
|
-
|
|
142
|
+
const outcome = await active.run({
|
|
142
143
|
root, ruleFile: entry.ruleFile, files: example.files, ai,
|
|
143
144
|
...(dependencies === undefined ? {} : { dependencies }),
|
|
144
145
|
}, deadline, failCase);
|
|
146
|
+
if (typeof outcome === 'string')
|
|
147
|
+
actual = outcome;
|
|
148
|
+
else {
|
|
149
|
+
actual = outcome.actual;
|
|
150
|
+
aiScores = outcome.aiScores;
|
|
151
|
+
}
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
154
|
catch (error) {
|
|
@@ -164,6 +171,7 @@ async function runCase(entry, example, ai, timeoutMs, session, onFailure) {
|
|
|
164
171
|
expected: example.expected,
|
|
165
172
|
actual,
|
|
166
173
|
durationMs: performance.now() - started,
|
|
174
|
+
...(aiScores === undefined ? {} : { aiScores }),
|
|
167
175
|
};
|
|
168
176
|
}
|
|
169
177
|
catch (error) {
|
|
@@ -196,6 +204,8 @@ export async function runRuleTests(rules, onResult, timeoutMs = 10_000, concurre
|
|
|
196
204
|
for (const rule of rules) {
|
|
197
205
|
if (failureObserved)
|
|
198
206
|
throw failure;
|
|
207
|
+
if (!rule.rule.enabled)
|
|
208
|
+
continue;
|
|
199
209
|
// Discovery selects the rule module path at runtime.
|
|
200
210
|
const { tests, aiTests } = await import(pathToFileURL(rule.ruleFile).href);
|
|
201
211
|
if (failureObserved)
|
package/dist/app/evaluators.js
CHANGED
|
@@ -23,8 +23,11 @@ function assertExampleList(value, label) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
function assertAiOptions(options) {
|
|
26
|
-
if (
|
|
27
|
-
fail('ai rule
|
|
26
|
+
if (options.rule !== undefined && options.instruction !== undefined)
|
|
27
|
+
fail('ai rule and instruction are mutually exclusive');
|
|
28
|
+
const policy = options.instruction ?? options.rule;
|
|
29
|
+
if (typeof policy !== 'string' || policy === '') {
|
|
30
|
+
fail(`ai ${options.instruction === undefined ? 'rule' : 'instruction'} must be a non-empty string`);
|
|
28
31
|
}
|
|
29
32
|
const examples = options.examples;
|
|
30
33
|
if (examples !== undefined) {
|
|
@@ -77,9 +80,13 @@ function projectState(subject) {
|
|
|
77
80
|
function instructionsOf(options) {
|
|
78
81
|
const good = options.examples?.good ?? [];
|
|
79
82
|
const bad = options.examples?.bad ?? [];
|
|
83
|
+
const instructions = {
|
|
84
|
+
rule: options.instruction === undefined ? options.rule : options.instruction,
|
|
85
|
+
question: QUESTION,
|
|
86
|
+
};
|
|
80
87
|
if (good.length === 0 && bad.length === 0)
|
|
81
|
-
return
|
|
82
|
-
return {
|
|
88
|
+
return instructions;
|
|
89
|
+
return { ...instructions, examples: { good, bad } };
|
|
83
90
|
}
|
|
84
91
|
export async function evaluateAi(subject, options, context, runtime, observer) {
|
|
85
92
|
assertAiOptions(options);
|
package/dist/app/rule-tests.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface RuleTestResult {
|
|
|
9
9
|
readonly name: string;
|
|
10
10
|
readonly expected: RuleTestCase['expected'];
|
|
11
11
|
readonly actual: RuleTestCase['expected'] | 'timeout';
|
|
12
|
+
readonly aiScores?: readonly number[];
|
|
12
13
|
readonly durationMs: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function test(name: string): {
|
package/dist/app/rule.d.ts
CHANGED
|
@@ -81,12 +81,18 @@ export interface AiExamples {
|
|
|
81
81
|
readonly good?: readonly string[];
|
|
82
82
|
readonly bad?: readonly string[];
|
|
83
83
|
}
|
|
84
|
-
export
|
|
85
|
-
readonly rule: string;
|
|
84
|
+
export type AiOptions<T = JsonValue> = {
|
|
86
85
|
readonly examples?: AiExamples;
|
|
87
86
|
readonly violationMessage?: string;
|
|
88
87
|
readonly input?: (subject: Subject<T>, context: ProjectContext) => Awaitable<JsonValue>;
|
|
89
|
-
}
|
|
88
|
+
} & ({
|
|
89
|
+
/** @deprecated Use `instruction` instead. */
|
|
90
|
+
readonly rule: string;
|
|
91
|
+
readonly instruction?: never;
|
|
92
|
+
} | {
|
|
93
|
+
readonly instruction: string;
|
|
94
|
+
readonly rule?: never;
|
|
95
|
+
});
|
|
90
96
|
export type Evaluator<T = JsonValue> = (subject: Subject<T>, context: CheckContext<T>) => Awaitable<CheckResult>;
|
|
91
97
|
export interface ProjectContext {
|
|
92
98
|
readonly project: Project;
|
package/package.json
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alex-dbs/coderuler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"ai-agent",
|
|
8
|
+
"agent",
|
|
9
|
+
"rule",
|
|
10
|
+
"rules",
|
|
11
|
+
"coding-standards",
|
|
12
|
+
"standard",
|
|
13
|
+
"code-quality",
|
|
14
|
+
"code-review",
|
|
15
|
+
"security",
|
|
16
|
+
"linter",
|
|
17
|
+
"checker",
|
|
18
|
+
"static-analysis",
|
|
19
|
+
"policy-as-code",
|
|
20
|
+
"typescript",
|
|
21
|
+
"developer-tools"
|
|
22
|
+
],
|
|
5
23
|
"repository": {
|
|
6
24
|
"type": "git",
|
|
7
25
|
"url": "git+https://github.com/alex-dbs/coderuler.git"
|
|
@@ -29,7 +29,9 @@ 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
|
+
For a deterministic rule, add a named `tests` export 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
|
+
|
|
34
|
+
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).
|
|
33
35
|
|
|
34
36
|
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.
|
|
35
37
|
|
|
@@ -97,10 +99,10 @@ Useful test options:
|
|
|
97
99
|
```sh
|
|
98
100
|
coderuler test . --failures-only
|
|
99
101
|
coderuler test . --concurrency 4 --timeout 30s
|
|
100
|
-
coderuler test .coderuler/
|
|
102
|
+
coderuler test .coderuler/semantic-policy.rule.ts --ai --ai-scores --timeout 45s
|
|
101
103
|
```
|
|
102
104
|
|
|
103
|
-
Replace the semantic-policy filename with the actual AI rule. `--ai` authorizes live provider calls for `aiTests`, requires provider configuration, and
|
|
105
|
+
Replace the semantic-policy filename with the actual AI rule. `--ai` authorizes live provider calls for `aiTests`, requires provider configuration, and can incur charges. `--ai-scores` displays each Jev probability; use it with `--ai`. A test marked `PASS` means its expectation matched, not that its code is compliant. Expected violations need scores near `0`; compliant cases need scores near `1`. For wording changes, compare both groups on the same fixtures and configuration. Follow [the comparison procedure](reference.md#ai-instruction-style) rather than changing fixtures or thresholds to make a candidate pass.
|
|
104
106
|
|
|
105
107
|
## 4. Inspect the rule on the project
|
|
106
108
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Rule API reference
|
|
1
|
+
# Rule API reference
|
|
2
2
|
|
|
3
3
|
Read the branch needed for the rule; use [SKILL.md](SKILL.md) for the authoring, test, and inspection workflow. All imports below use the public `@alex-dbs/coderuler` SDK.
|
|
4
4
|
|
|
@@ -318,7 +318,7 @@ Results validate; supplied messages survive reporting. Violations without a mess
|
|
|
318
318
|
| ------------------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
319
319
|
| `ProjectContext` | `readonly project: Project`; `trace(message: string, details?: JsonValue): void`. |
|
|
320
320
|
| `SelectionContext extends ProjectContext` | `select<T>(selection: Selection<T>): Promise<readonly ScopeSubject<T>[]>`. |
|
|
321
|
-
| `CheckContext<T = JsonValue> extends SelectionContext` | `readonly ruleName: string`; `ai(options: AiOptions<T>): Promise<CheckResult>`.
|
|
321
|
+
| `CheckContext<T = JsonValue> extends SelectionContext` | `readonly ruleName: string`; `ai(options: AiOptions<T>): Promise<CheckResult>`. |
|
|
322
322
|
|
|
323
323
|
|
|
324
324
|
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.
|
|
@@ -344,85 +344,139 @@ Default input is `String(data)` for scalar/null data, otherwise source text. `in
|
|
|
344
344
|
|
|
345
345
|
## AI options, examples, and settings
|
|
346
346
|
|
|
347
|
-
`ai<T = JsonValue>(options: AiOptions<T>): Evaluator<T>` accepts
|
|
347
|
+
`ai<T = JsonValue>(options: AiOptions<T>): Evaluator<T>` accepts exactly one of `rule` or `instruction`:
|
|
348
348
|
|
|
349
|
-
- `rule: string`: nonempty
|
|
349
|
+
- `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 Jev can judge as respected or broken.
|
|
350
351
|
- `examples?: { readonly good?: readonly string[]; readonly bad?: readonly string[] }`: conditioning examples, dense arrays of nonempty strings; omitted/empty lists are valid.
|
|
351
352
|
- `violationMessage?: string`: nonempty reporting-only message, not sent as instructions.
|
|
352
353
|
- `input?: (subject: Subject<T>, context: ProjectContext) => Awaitable<JsonValue>`: replace the default projection; output validates as JSON-compatible.
|
|
353
354
|
|
|
354
|
-
|
|
355
|
-
import { Rule, ai, test } from '@alex-dbs/coderuler';
|
|
356
|
-
import type { RuleSettings } from '@alex-dbs/coderuler';
|
|
355
|
+
Jev uses the fixed question "Is the rule respected?" Yes/true means no problems. No/false means at least one violation. The configured threshold decides pass or violation.
|
|
357
356
|
|
|
358
|
-
|
|
359
|
-
files = ['guides/**/*.md'];
|
|
360
|
-
settings: RuleSettings = { model: 'jev-latest', threshold: 0.5 };
|
|
361
|
-
check = ai({
|
|
362
|
-
rule: 'Instructions name a concrete action and its observable completion condition.',
|
|
363
|
-
examples: { good: ['Run the migration until it exits successfully.'], bad: ['Handle the migration.'] },
|
|
364
|
-
violationMessage: 'Name the action and how to recognize completion',
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
export const aiTests = [
|
|
368
|
-
test('an independent concrete instruction passes').withFiles({
|
|
369
|
-
'guides/cache.md': 'Delete the cache directory; finish when it no longer exists.',
|
|
370
|
-
}).passes(),
|
|
371
|
-
];
|
|
372
|
-
```
|
|
357
|
+
### AI instruction style
|
|
373
358
|
|
|
374
|
-
|
|
359
|
+
Write the instruction as a requirement for the code, not directions for the reviewer. A direct requirement names the behavior compliant code must have. Add a boundary only when it says where the policy applies; include exceptions only when they are genuinely allowed. Keep each rule to one policy.
|
|
375
360
|
|
|
376
|
-
|
|
361
|
+
Weak: "Report a violation when a Go pointer is dereferenced without a nil check."
|
|
377
362
|
|
|
378
|
-
|
|
363
|
+
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."
|
|
379
364
|
|
|
380
|
-
|
|
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, and include enough input for the semantic decision.
|
|
381
366
|
|
|
382
|
-
|
|
367
|
+
Compare wording with the policy and dataset held fixed:
|
|
383
368
|
|
|
384
|
-
|
|
369
|
+
1. Save the current instruction and its scores as the baseline.
|
|
370
|
+
2. Change only the instruction. Keep the model version, examples, input, fixtures, and threshold unchanged.
|
|
371
|
+
3. Run every affected `aiTests` case with `--ai --ai-scores`. Include compliant cases and violations, especially policy boundaries.
|
|
372
|
+
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
|
+
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.
|
|
385
374
|
|
|
386
|
-
|
|
387
|
-
|
|
375
|
+
`PASS` in test output means the expected result matched. A passing violation test still needs a low Jev 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
|
+
|
|
377
|
+
|
|
378
|
+
This rule checks Go pointer dereferences, including nested fields:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { Rule, ai, scopes, test, type ScopeSubject } from '@alex-dbs/coderuler';
|
|
388
382
|
|
|
389
|
-
export default class
|
|
383
|
+
export default class GoDereferenceGuard extends Rule {
|
|
390
384
|
files = ['**/*.go'];
|
|
385
|
+
settings = { threshold: 0.7 };
|
|
391
386
|
|
|
392
387
|
scope = scopes.astGrep({
|
|
393
388
|
language: 'go',
|
|
394
389
|
rule: {
|
|
395
390
|
any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }],
|
|
396
391
|
has: {
|
|
397
|
-
kind: '
|
|
392
|
+
kind: 'unary_expression',
|
|
398
393
|
stopBy: 'end',
|
|
399
394
|
has: {
|
|
400
|
-
|
|
401
|
-
|
|
395
|
+
field: 'operator',
|
|
396
|
+
regex: '^\\*$',
|
|
402
397
|
},
|
|
403
398
|
},
|
|
404
399
|
},
|
|
405
400
|
});
|
|
406
401
|
|
|
407
402
|
check = ai({
|
|
408
|
-
|
|
409
|
-
growing it with append is a violation even when capacity was preallocated with make([]T, 0, n)
|
|
410
|
-
and even when it was first created with make([]T, n) and then reset to length zero.
|
|
411
|
-
|
|
412
|
-
Append in a loop is valid only when the final count is not known before the loop,
|
|
413
|
-
such as elements appended under a condition so the count varies within an upper bound that capacity merely reserves,
|
|
414
|
-
or when the slice already holds a deliberately assigned prefix written before the loop.
|
|
415
|
-
|
|
416
|
-
Appending after a length that was allocated but never assigned,
|
|
417
|
-
or assigning elements by index and also appending them, is a violation.`,
|
|
403
|
+
instruction: `Check each Go pointer for nil before dereferencing it. For nested fields, check every pointer in the field path.`,
|
|
418
404
|
examples: {
|
|
419
|
-
good: [
|
|
420
|
-
|
|
405
|
+
good: [
|
|
406
|
+
`func configName(config *Config) string {
|
|
407
|
+
if config != nil {
|
|
408
|
+
return (*config).Name
|
|
409
|
+
}
|
|
410
|
+
return ""
|
|
411
|
+
}`,
|
|
412
|
+
`func (config *Config) name() string {
|
|
413
|
+
if config == nil {
|
|
414
|
+
return ""
|
|
415
|
+
}
|
|
416
|
+
return (*config).Name
|
|
417
|
+
}`
|
|
418
|
+
],
|
|
419
|
+
bad: [
|
|
420
|
+
`func configName(config *Config) string {
|
|
421
|
+
return (*config).Name
|
|
422
|
+
}`,
|
|
423
|
+
`func configName(config *Config) string {
|
|
424
|
+
if config == nil {
|
|
425
|
+
logMissingConfig()
|
|
426
|
+
}
|
|
427
|
+
return (*config).Name
|
|
428
|
+
}`
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
input: async (subject, { project }) => {
|
|
432
|
+
const scope = subject as ScopeSubject;
|
|
433
|
+
return {
|
|
434
|
+
function: scope.source,
|
|
435
|
+
file: await project.readText(scope.filepath),
|
|
436
|
+
};
|
|
421
437
|
},
|
|
438
|
+
violationMessage: 'Check each pointer for nil before using *.',
|
|
422
439
|
});
|
|
423
440
|
}
|
|
441
|
+
|
|
442
|
+
export const aiTests = [
|
|
443
|
+
test('an early return guards a pointer parameter').withFiles({
|
|
444
|
+
'quota.go': `package example
|
|
445
|
+
|
|
446
|
+
func remainingQuota(quota *int, used int) int {
|
|
447
|
+
if quota == nil {
|
|
448
|
+
return 0
|
|
449
|
+
}
|
|
450
|
+
return *quota - used
|
|
451
|
+
}`,
|
|
452
|
+
}).passes(),
|
|
453
|
+
test('a root nil check does not guard a nested pointer field').withFiles({
|
|
454
|
+
'config.go': `package example
|
|
455
|
+
|
|
456
|
+
type Config struct { Child *Child }
|
|
457
|
+
type Child struct { Value *int }
|
|
458
|
+
|
|
459
|
+
func nestedValue(config *Config) int {
|
|
460
|
+
if config == nil {
|
|
461
|
+
return 0
|
|
462
|
+
}
|
|
463
|
+
return *config.Child.Value
|
|
464
|
+
}`,
|
|
465
|
+
}).violates(),
|
|
466
|
+
] as const;
|
|
424
467
|
```
|
|
425
468
|
|
|
469
|
+
`examples` condition the model; `aiTests` assert outcomes on independent datasets. Put every test case for an AI rule in `aiTests`, without a separate `tests` export. Ordinary `tests` are offline; live `aiTests` require `--ai`. Use `instruction` and follow [AI instruction style](#ai-instruction-style). Add `--ai-scores` to inspect probabilities, not just test outcomes.
|
|
470
|
+
|
|
471
|
+
Default AI input is `{ filepath, scope: { id, data, captures? } }` when data exists, or `{ filepath, scope: { id, source, captures? } }` otherwise. Data presence includes `null`, `false`, `0`, and `''`. Groups become `{ files: [{ filepath, scopes: [...] }] }`. Good: select enough context to judge meaning, or project a deliberate JSON input. Bad: expect source text alongside structured data without providing it via `input`.
|
|
472
|
+
|
|
473
|
+
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
|
+
|
|
475
|
+
`RuleSettings` supports backend `'jev'`, model (default `'jev-latest'`), finite threshold in `[0, 1]` (default `0.4`), and finite nonnegative `inputCostPerMillion` / `outputCostPerMillion` prices (USD per million tokens; defaults `0.042` / `0`). Threshold precedence is CLI override → rule setting → `RK_THRESHOLD` → default; backend/model use rule setting → environment (`RK_BACKEND` / `TYPESAFE_DEFAULT_MODEL`) → default. AI passes only when probability is **strictly greater** than threshold; equality violates. Results include the effective threshold. Provider environment/key resolution is lazy at the first AI call; deterministic/skipped/filtered paths need no key. Live calls require `TYPESAFE_API_KEY`; use release configuration guidance for transport setup.
|
|
476
|
+
|
|
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. Configure prices through rule settings or `RK_INPUT_COST_PER_MILLION` / `RK_OUTPUT_COST_PER_MILLION`; inspect flags `--input-cost-per-million` / `--output-cost-per-million` override both. Prices change estimates, not decisions.
|
|
478
|
+
|
|
479
|
+
|
|
426
480
|
## Project readers
|
|
427
481
|
|
|
428
482
|
Public signatures (import `Project`, `CsvOptions`, `CsvRow`, and `JsonValue` as types from `@alex-dbs/coderuler`):
|