@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 CHANGED
@@ -114,14 +114,17 @@ export default class SafeResponseHeaders extends Rule {
114
114
  });
115
115
 
116
116
  check = ai({
117
- rule: 'Report a violation only when this function writes request data into an HTTP response header without rejecting CR and LF first. Judge only this function. Do not assume unseen callers or helpers validate the data.',
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));
@@ -44,6 +44,7 @@ export interface TestOptions {
44
44
  readonly concurrency: number;
45
45
  readonly ai?: boolean;
46
46
  readonly failuresOnly?: boolean;
47
+ readonly aiScores?: boolean;
47
48
  }
48
49
  export interface DependencyOptions {
49
50
  readonly global?: boolean;
@@ -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 actual: RuleTestCase['expected'];
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
- return report.status;
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(actual => port.postMessage({ id: request.id, actual }), error => port.postMessage({
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.actual);
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
- actual = await active.run({
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)
@@ -23,8 +23,11 @@ function assertExampleList(value, label) {
23
23
  }
24
24
  }
25
25
  function assertAiOptions(options) {
26
- if (typeof options.rule !== 'string' || options.rule === '') {
27
- fail('ai rule must be a non-empty string');
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 { rule: options.rule, question: QUESTION };
82
- return { rule: options.rule, question: QUESTION, examples: { good, bad } };
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);
@@ -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): {
@@ -81,12 +81,18 @@ export interface AiExamples {
81
81
  readonly good?: readonly string[];
82
82
  readonly bad?: readonly string[];
83
83
  }
84
- export interface AiOptions<T = JsonValue> {
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.0",
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
- Add a named `tests` export in the same file. Use `.passes()` and `.violates()` cases that exercise the policy. Omit `.skips()` assertions when creating a rule: skipped inputs never reach the check and provide no evidence of policy behavior.
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/k8s-semantic-policy.rule.ts --ai --timeout 45s
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 may incur charges. Outcomes can vary; provider errors are errors, not passes.
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 semantic policy; include exceptions and what visible evidence permits a violation.
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
- ```ts
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
- export default class ActionableInstructions extends Rule {
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
- `examples` condition the model; `aiTests` assert outcomes on independent datasets. Ordinary `tests` are offline; live `aiTests` require explicit AI opt-in as described in [SKILL.md](SKILL.md). Keep deterministic applicability out of the prompt.
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
- 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`.
361
+ Weak: "Report a violation when a Go pointer is dereferenced without a nil check."
377
362
 
378
- Inside a custom check, `return context.ai({ rule: '...' })` evaluates its current subject with effective settings and preserves AI result metadata. 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.
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
- `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.
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
- 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.
367
+ Compare wording with the policy and dataset held fixed:
383
368
 
384
- AI rules, under the hood, are using TypeSafe Jev model and `noul` question (yes/no probabilistc answer). The rule must clearly explain what is checked. A global/generic rule won't match. A dedicated rule will perform better. The example bellow will check a particular usage of go slice `append` function.
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
- import { Rule, ai, scopes, test } from '@alex-dbs/coderuler';
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 GoAppendSlice extends Rule {
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: 'for_statement',
392
+ kind: 'unary_expression',
398
393
  stopBy: 'end',
399
394
  has: {
400
- pattern: 'append($SLICE,$$$ARGS)',
401
- stopBy: 'end',
395
+ field: 'operator',
396
+ regex: '^\\*$',
402
397
  },
403
398
  },
404
399
  },
405
400
  });
406
401
 
407
402
  check = ai({
408
- rule: `A slice freshly allocated for a loop whose final element count is exactly known must be allocated at that final length and filled by index or with copy;
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: [/* Some examples, not shown here to keep it short*/],
420
- bad: [/* Some examples, not shown here to keep it short*/],
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`):