@alex-dbs/coderuler 1.3.1 → 1.5.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 +8 -3
- package/dist/adapter/cli/commands/check.js +21 -10
- package/dist/adapter/cli/commands/test.js +2 -2
- package/dist/adapter/cli/program.js +2 -1
- package/dist/adapter/cli/reports.d.ts +1 -1
- package/dist/adapter/cli/reports.js +5 -2
- package/dist/adapter/cli/services.d.ts +5 -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-worker.d.ts +6 -1
- package/dist/adapter/filesystem/rule-test-worker.js +13 -3
- package/dist/adapter/filesystem/rule-tests.d.ts +1 -1
- package/dist/adapter/filesystem/rule-tests.js +69 -21
- 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/app/evaluators.js +11 -4
- package/dist/app/execute.js +9 -9
- package/dist/app/ports.d.ts +6 -1
- package/dist/app/rule-tests.d.ts +1 -0
- package/dist/app/rule.d.ts +21 -13
- package/dist/app/validate.js +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/infra/runtime.js +13 -5
- package/dist/infra/settings.js +19 -8
- package/package.json +20 -1
- package/skills/coderuler-new-rule/SKILL.md +11 -5
- package/skills/coderuler-new-rule/reference.md +121 -53
|
@@ -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
|
|
|
@@ -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,9 +328,11 @@ 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
|
-
| `CheckContext<T = JsonValue> extends SelectionContext` | `readonly ruleName: string`; `ai(options: AiOptions<T>): Promise<CheckResult>`.
|
|
333
|
+
| `CheckContext<T = JsonValue> extends SelectionContext` | `readonly ruleName: string`; `ai(options: AiOptions<T>): Promise<CheckResult>`. |
|
|
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.
|
|
322
336
|
|
|
323
337
|
|
|
324
338
|
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 +358,139 @@ Default input is `String(data)` for scalar/null data, otherwise source text. `in
|
|
|
344
358
|
|
|
345
359
|
## AI options, examples, and settings
|
|
346
360
|
|
|
347
|
-
`ai<T = JsonValue>(options: AiOptions<T>): Evaluator<T>` accepts
|
|
361
|
+
`ai<T = JsonValue>(options: AiOptions<T>): Evaluator<T>` accepts exactly one of `rule` or `instruction`:
|
|
348
362
|
|
|
349
|
-
- `rule: string`: nonempty
|
|
363
|
+
- `rule: string`: nonempty policy text. This option is deprecated; use `instruction` for new rules.
|
|
364
|
+
- `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.
|
|
350
365
|
- `examples?: { readonly good?: readonly string[]; readonly bad?: readonly string[] }`: conditioning examples, dense arrays of nonempty strings; omitted/empty lists are valid.
|
|
351
366
|
- `violationMessage?: string`: nonempty reporting-only message, not sent as instructions.
|
|
352
367
|
- `input?: (subject: Subject<T>, context: ProjectContext) => Awaitable<JsonValue>`: replace the default projection; output validates as JSON-compatible.
|
|
353
368
|
|
|
354
|
-
|
|
355
|
-
import { Rule, ai, test } from '@alex-dbs/coderuler';
|
|
356
|
-
import type { RuleSettings } from '@alex-dbs/coderuler';
|
|
369
|
+
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.
|
|
357
370
|
|
|
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
|
-
```
|
|
371
|
+
### AI instruction style
|
|
373
372
|
|
|
374
|
-
|
|
373
|
+
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
374
|
|
|
376
|
-
|
|
375
|
+
Weak: "Report a violation when a Go pointer is dereferenced without a nil check."
|
|
377
376
|
|
|
378
|
-
|
|
377
|
+
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
378
|
|
|
380
|
-
|
|
379
|
+
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.
|
|
381
380
|
|
|
382
|
-
|
|
381
|
+
Compare wording with the policy and dataset held fixed:
|
|
383
382
|
|
|
384
|
-
|
|
383
|
+
1. Save the current instruction and its scores as the baseline.
|
|
384
|
+
2. Change only the instruction. Keep the model version, examples, input, fixtures, and threshold unchanged.
|
|
385
|
+
3. Run every affected `aiTests` case with `--ai --ai-scores`. Include compliant cases and violations, especially policy boundaries.
|
|
386
|
+
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.
|
|
387
|
+
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.
|
|
388
|
+
|
|
389
|
+
`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.
|
|
385
390
|
|
|
386
|
-
```
|
|
387
|
-
import { Rule, ai, scopes, test } from '@alex-dbs/coderuler';
|
|
388
391
|
|
|
389
|
-
|
|
392
|
+
This rule checks Go pointer dereferences, including nested fields:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import { Rule, ai, scopes, test, type ScopeSubject } from '@alex-dbs/coderuler';
|
|
396
|
+
|
|
397
|
+
export default class GoDereferenceGuard extends Rule {
|
|
390
398
|
files = ['**/*.go'];
|
|
399
|
+
settings = { threshold: 0.7 };
|
|
391
400
|
|
|
392
401
|
scope = scopes.astGrep({
|
|
393
402
|
language: 'go',
|
|
394
403
|
rule: {
|
|
395
404
|
any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }],
|
|
396
405
|
has: {
|
|
397
|
-
kind: '
|
|
406
|
+
kind: 'unary_expression',
|
|
398
407
|
stopBy: 'end',
|
|
399
408
|
has: {
|
|
400
|
-
|
|
401
|
-
|
|
409
|
+
field: 'operator',
|
|
410
|
+
regex: '^\\*$',
|
|
402
411
|
},
|
|
403
412
|
},
|
|
404
413
|
},
|
|
405
414
|
});
|
|
406
415
|
|
|
407
416
|
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.`,
|
|
417
|
+
instruction: `Check each Go pointer for nil before dereferencing it. For nested fields, check every pointer in the field path.`,
|
|
418
418
|
examples: {
|
|
419
|
-
good: [
|
|
420
|
-
|
|
419
|
+
good: [
|
|
420
|
+
`func configName(config *Config) string {
|
|
421
|
+
if config != nil {
|
|
422
|
+
return (*config).Name
|
|
423
|
+
}
|
|
424
|
+
return ""
|
|
425
|
+
}`,
|
|
426
|
+
`func (config *Config) name() string {
|
|
427
|
+
if config == nil {
|
|
428
|
+
return ""
|
|
429
|
+
}
|
|
430
|
+
return (*config).Name
|
|
431
|
+
}`
|
|
432
|
+
],
|
|
433
|
+
bad: [
|
|
434
|
+
`func configName(config *Config) string {
|
|
435
|
+
return (*config).Name
|
|
436
|
+
}`,
|
|
437
|
+
`func configName(config *Config) string {
|
|
438
|
+
if config == nil {
|
|
439
|
+
logMissingConfig()
|
|
440
|
+
}
|
|
441
|
+
return (*config).Name
|
|
442
|
+
}`
|
|
443
|
+
],
|
|
421
444
|
},
|
|
445
|
+
input: async (subject, { project }) => {
|
|
446
|
+
const scope = subject as ScopeSubject;
|
|
447
|
+
return {
|
|
448
|
+
function: scope.source,
|
|
449
|
+
file: await project.readText(scope.filepath),
|
|
450
|
+
};
|
|
451
|
+
},
|
|
452
|
+
violationMessage: 'Check each pointer for nil before using *.',
|
|
422
453
|
});
|
|
423
454
|
}
|
|
455
|
+
|
|
456
|
+
export const aiTests = [
|
|
457
|
+
test('an early return guards a pointer parameter').withFiles({
|
|
458
|
+
'quota.go': `package example
|
|
459
|
+
|
|
460
|
+
func remainingQuota(quota *int, used int) int {
|
|
461
|
+
if quota == nil {
|
|
462
|
+
return 0
|
|
463
|
+
}
|
|
464
|
+
return *quota - used
|
|
465
|
+
}`,
|
|
466
|
+
}).passes(),
|
|
467
|
+
test('a root nil check does not guard a nested pointer field').withFiles({
|
|
468
|
+
'config.go': `package example
|
|
469
|
+
|
|
470
|
+
type Config struct { Child *Child }
|
|
471
|
+
type Child struct { Value *int }
|
|
472
|
+
|
|
473
|
+
func nestedValue(config *Config) int {
|
|
474
|
+
if config == nil {
|
|
475
|
+
return 0
|
|
476
|
+
}
|
|
477
|
+
return *config.Child.Value
|
|
478
|
+
}`,
|
|
479
|
+
}).violates(),
|
|
480
|
+
] as const;
|
|
424
481
|
```
|
|
425
482
|
|
|
483
|
+
`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.
|
|
484
|
+
|
|
485
|
+
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`.
|
|
486
|
+
|
|
487
|
+
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.
|
|
488
|
+
|
|
489
|
+
`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.
|
|
490
|
+
|
|
491
|
+
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.
|
|
492
|
+
|
|
493
|
+
|
|
426
494
|
## Project readers
|
|
427
495
|
|
|
428
496
|
Public signatures (import `Project`, `CsvOptions`, `CsvRow`, and `JsonValue` as types from `@alex-dbs/coderuler`):
|