@alex-dbs/coderuler 0.0.0-stage → 1.3.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/LICENSE +175 -0
- package/README.md +218 -2
- package/dist/adapter/cli/commands/check.d.ts +3 -0
- package/dist/adapter/cli/commands/check.js +93 -0
- package/dist/adapter/cli/commands/deps.d.ts +3 -0
- package/dist/adapter/cli/commands/deps.js +4 -0
- package/dist/adapter/cli/commands/inspect.d.ts +3 -0
- package/dist/adapter/cli/commands/inspect.js +59 -0
- package/dist/adapter/cli/commands/rules.d.ts +10 -0
- package/dist/adapter/cli/commands/rules.js +17 -0
- package/dist/adapter/cli/commands/skill.d.ts +4 -0
- package/dist/adapter/cli/commands/skill.js +7 -0
- package/dist/adapter/cli/commands/test.d.ts +3 -0
- package/dist/adapter/cli/commands/test.js +78 -0
- package/dist/adapter/cli/commands/update.d.ts +3 -0
- package/dist/adapter/cli/commands/update.js +7 -0
- package/dist/adapter/cli/commands/validate.d.ts +3 -0
- package/dist/adapter/cli/commands/validate.js +12 -0
- package/dist/adapter/cli/exit.d.ts +7 -0
- package/dist/adapter/cli/exit.js +10 -0
- package/dist/adapter/cli/inspect-json.d.ts +9 -0
- package/dist/adapter/cli/inspect-json.js +160 -0
- package/dist/adapter/cli/inspect-usage.d.ts +9 -0
- package/dist/adapter/cli/inspect-usage.js +14 -0
- package/dist/adapter/cli/program.d.ts +4 -0
- package/dist/adapter/cli/program.js +216 -0
- package/dist/adapter/cli/progress.d.ts +22 -0
- package/dist/adapter/cli/progress.js +79 -0
- package/dist/adapter/cli/reports.d.ts +47 -0
- package/dist/adapter/cli/reports.js +735 -0
- package/dist/adapter/cli/self-update.d.ts +1 -0
- package/dist/adapter/cli/self-update.js +89 -0
- package/dist/adapter/cli/services.d.ts +101 -0
- package/dist/adapter/cli/services.js +1 -0
- package/dist/adapter/cli/text.d.ts +5 -0
- package/dist/adapter/cli/text.js +36 -0
- package/dist/adapter/cli/version.d.ts +1 -0
- package/dist/adapter/cli/version.js +3 -0
- package/dist/adapter/decision/jev.d.ts +11 -0
- package/dist/adapter/decision/jev.js +138 -0
- package/dist/adapter/filesystem/dependencies.d.ts +26 -0
- package/dist/adapter/filesystem/dependencies.js +577 -0
- package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
- package/dist/adapter/filesystem/dependency-installation.js +373 -0
- package/dist/adapter/filesystem/project.d.ts +14 -0
- package/dist/adapter/filesystem/project.js +387 -0
- package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
- package/dist/adapter/filesystem/rule-resolution.js +516 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
- package/dist/adapter/filesystem/rule-test-worker.js +47 -0
- package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
- package/dist/adapter/filesystem/rule-tests.js +264 -0
- package/dist/adapter/filesystem/rules.d.ts +13 -0
- package/dist/adapter/filesystem/rules.js +242 -0
- package/dist/adapter/git/changes.d.ts +1 -0
- package/dist/adapter/git/changes.js +54 -0
- package/dist/adapter/git/environment.d.ts +1 -0
- package/dist/adapter/git/environment.js +10 -0
- package/dist/adapter/github/api.d.ts +5 -0
- package/dist/adapter/github/api.js +40 -0
- package/dist/adapter/github/comments.d.ts +3 -0
- package/dist/adapter/github/comments.js +235 -0
- package/dist/adapter/github/pull-request.d.ts +16 -0
- package/dist/adapter/github/pull-request.js +139 -0
- package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
- package/dist/adapter/scope/astgrep-languages.js +119 -0
- package/dist/adapter/scope/astgrep.d.ts +14 -0
- package/dist/adapter/scope/astgrep.js +183 -0
- package/dist/adapter/scope/csv.d.ts +4 -0
- package/dist/adapter/scope/csv.js +158 -0
- package/dist/adapter/scope/file.d.ts +2 -0
- package/dist/adapter/scope/file.js +4 -0
- package/dist/adapter/scope/json.d.ts +7 -0
- package/dist/adapter/scope/json.js +132 -0
- package/dist/adapter/scope/lines.d.ts +10 -0
- package/dist/adapter/scope/lines.js +42 -0
- package/dist/adapter/scope/protobuf-options.d.ts +29 -0
- package/dist/adapter/scope/protobuf-options.js +64 -0
- package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
- package/dist/adapter/scope/protobuf-parser.js +772 -0
- package/dist/adapter/scope/protobuf.d.ts +5 -0
- package/dist/adapter/scope/protobuf.js +53 -0
- package/dist/adapter/scope/structured.d.ts +10 -0
- package/dist/adapter/scope/structured.js +84 -0
- package/dist/adapter/scope/yaml.d.ts +7 -0
- package/dist/adapter/scope/yaml.js +224 -0
- package/dist/adapter/skills/cli.d.ts +10 -0
- package/dist/adapter/skills/cli.js +77 -0
- package/dist/app/concurrency.d.ts +1 -0
- package/dist/app/concurrency.js +44 -0
- package/dist/app/evaluators.d.ts +14 -0
- package/dist/app/evaluators.js +175 -0
- package/dist/app/execute.d.ts +2 -0
- package/dist/app/execute.js +427 -0
- package/dist/app/jsonpath.d.ts +1 -0
- package/dist/app/jsonpath.js +8 -0
- package/dist/app/ports.d.ts +165 -0
- package/dist/app/ports.js +1 -0
- package/dist/app/rule-tests.d.ts +20 -0
- package/dist/app/rule-tests.js +11 -0
- package/dist/app/rule.d.ts +121 -0
- package/dist/app/rule.js +24 -0
- package/dist/app/selection.d.ts +2 -0
- package/dist/app/selection.js +23 -0
- package/dist/app/usage.d.ts +2 -0
- package/dist/app/usage.js +13 -0
- package/dist/app/validate.d.ts +11 -0
- package/dist/app/validate.js +415 -0
- package/dist/app/violation-comments.d.ts +10 -0
- package/dist/app/violation-comments.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/infra/runtime.d.ts +3 -0
- package/dist/infra/runtime.js +168 -0
- package/dist/infra/settings.d.ts +5 -0
- package/dist/infra/settings.js +91 -0
- package/package.json +86 -4
- package/skills/coderuler-new-rule/SKILL.md +141 -0
- package/skills/coderuler-new-rule/reference.md +447 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Rule API reference
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Rule members and lifecycle
|
|
6
|
+
|
|
7
|
+
Default-export a constructible class extending `Rule<T>` (`T = JsonValue`). Extra class fields/methods are allowed; hooks and checks receive the rule instance as `this`.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
| Member / signature | Default | Contract |
|
|
11
|
+
| --------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ |
|
|
12
|
+
| `enabled: boolean` | `true` | `false` skips with reason `disabled`; structural validation still runs. |
|
|
13
|
+
| `files: readonly string[]` | `['**/*']` | Nonempty root-relative glob list. |
|
|
14
|
+
| `exclude: readonly string[]` | `[]` | Root-relative globs; exclusion wins. |
|
|
15
|
+
| `tags?: readonly string[]` | Absent | Labels for `check --tags` and `--exclude-tags`. |
|
|
16
|
+
| `scope?: ScopeSelector<T>` | Absent | One whole-file scope per eligible file unless supplied. |
|
|
17
|
+
| `settings: RuleSettings` | `{}` | Optional backend, model, threshold, and input/output USD-per-million token prices; see AI below. |
|
|
18
|
+
| `group: Grouping` | `'scope'` | Groups surviving scopes before evaluation. |
|
|
19
|
+
| `comment?: string` | Absent | Nonblank Markdown override for each GitHub finding body with `--auto-comment`; see below. |
|
|
20
|
+
| `when(context: ProjectContext): Awaitable<PredicateResult>` | `true` | Whole-rule applicability, once per enabled rule before parsing target sources. |
|
|
21
|
+
| `select(context: SelectionContext): Awaitable<readonly ScopeSubject<T>[]>` | Delegates | Calls `context.select({ files, exclude, scope })`; override for custom selection. |
|
|
22
|
+
| `filter(subject: ScopeSubject<T>, context: ProjectContext): Awaitable<PredicateResult>` | `true` | Per-scope applicability before grouping. |
|
|
23
|
+
| `check: Evaluator<T>` | Required | Function-valued property, such as `check = regex(...)` or `check: Evaluator<T> = ...`, rather than a method. |
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Execution: load and validate all rules → apply `check` tag filters → enabled → `when` → select → validate subjects → filter → group → check → report. `Awaitable<T>` means `T | Promise<T>`.
|
|
27
|
+
|
|
28
|
+
`files` and `exclude` must be dense arrays of nonempty strings, including on disabled rules. `tags`, when present, must be dense arrays of nonempty strings.
|
|
29
|
+
|
|
30
|
+
Good: explain an out-of-domain scope with a filter reason. Bad: return `pass()` from a check because required data is missing, or filter out a genuine violation. Keep instance/module state independent of evaluation ordering: checks can run concurrently, and test workers reuse imported modules.
|
|
31
|
+
|
|
32
|
+
## Built-in selectors
|
|
33
|
+
|
|
34
|
+
`scopes` exposes these factories. Each result is a `ScopeSelector<T>` except `file()` (`ScopeSelector<never>`) and `csv()` (`ScopeSelector<CsvRow>`).
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
| Factory | Options and behavior |
|
|
38
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `scopes.file()` | No options. One data-free `$` scope per file, including empty files (lines 1–1). |
|
|
40
|
+
| `scopes.json<T>({ path?, key? }?)` | Both options are strings; `path` defaults to `$`. Strict JSON: comments, trailing commas, empty input, duplicate keys, and nonfinite numbers error. ID is an RFC 6901 pointer (`$` at root), or the selected key. |
|
|
41
|
+
| `scopes.yaml<T>({ path?, key? }?)` | Same options/default as JSON. Selects every nonempty document in order; aliases/merge keys resolve with a bounded alias count. ID is `<doc-index>:<pointer-or-key>`. Empty/comment-only documents are omitted without renumbering. |
|
|
42
|
+
| `scopes.csv({ headers?, delimiter?, key? }?)` | `headers: boolean = true`, `delimiter: string = ','`, `key?: string`. Header mode yields string records; otherwise string arrays. IDs are `row:<n>` (1-based data record) or the nonblank, unique key-column value. `key` requires headers and an existing column. Quoting, multiline records, BOM, and line-ending variants are supported; record widths are strict. |
|
|
43
|
+
| `scopes.astGrep({ language, rule, constraints?, utils?, tsx? })` | Required language string and native ast-grep query; optional metavariable constraints and reusable query utilities. Returns source/captures/line spans, not parsed `data`; queries validate at factory construction. |
|
|
44
|
+
| `scopes.protobuf<T>({ path? }?)` | String `path` defaults to `$.rpcs[*]`. Reads declarations and their options from one file; see [Protobuf data](#protobuf-data). |
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
JSON/YAML `path` addresses the document; `key` is a JSONPath evaluated against each selected value and must produce exactly one string or finite number. All structured paths are nonempty `$`-rooted JSONPath; executable expressions are rejected and evaluation errors propagate. Protobuf paths may select any node of the protobuf document.
|
|
48
|
+
|
|
49
|
+
Omitted/`undefined` options retain defaults; explicit `null` is invalid for structured paths and CSV headers/delimiters. A CSV delimiter is one Unicode code point other than CR, LF, or a quote; CSV scopes preserve exact record source slices with multibyte delimiters too.
|
|
50
|
+
|
|
51
|
+
### ast-grep queries
|
|
52
|
+
|
|
53
|
+
Use `scopes.astGrep` to select code by syntax. The [official ast-grep documentation](https://ast-grep.github.io/) is authoritative for query syntax and behavior. This reference covers the CodeRuler integration.
|
|
54
|
+
|
|
55
|
+
- For patterns and captures, read [pattern syntax](https://ast-grep.github.io/guide/pattern-syntax). `$NAME` matches one node, and `$$$ARGS` matches zero or more nodes.
|
|
56
|
+
- For node kinds, relations, and logical composition, read the [rule object reference](https://ast-grep.github.io/reference/rule).
|
|
57
|
+
- For unfamiliar syntax, use the [playground](https://ast-grep.github.io/playground.html) to inspect node kinds and try the query against realistic code.
|
|
58
|
+
- For custom node traversal beyond built-in selection, read the [JavaScript API](https://ast-grep.github.io/guide/api-usage/js-api).
|
|
59
|
+
|
|
60
|
+
Choose the source language, then start with a `pattern` or `kind`. Add `has`/`inside` for structural context and `all`/`any`/`not` for composition. Relational searches default to immediate neighbors. Use `stopBy: 'end'` when the search must reach deeper descendants or ancestors.
|
|
61
|
+
|
|
62
|
+
Pass the query object as `rule`, with `constraints` and `utils` beside it when needed. CodeRuler supplies file selection and reporting, so omit ast-grep YAML fields such as `id`, `message`, and `severity`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
scope = scopes.astGrep({
|
|
66
|
+
language: 'typescript',
|
|
67
|
+
rule: { pattern: 'console.log($$$ARGS)' },
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This selects calls with any number of arguments. Read `subject.source` and `subject.captures.ARGS` in the check. CodeRuler omits empty captures, so `ARGS` is absent for `console.log()`. Use `coderuler inspect` to confirm matches on project files, then test the policy through the workflow in [SKILL.md](SKILL.md).
|
|
72
|
+
|
|
73
|
+
### AST and TypeScript/TSX
|
|
74
|
+
|
|
75
|
+
Choose a language matching the source; common JavaScript/TypeScript, markup, and bundled language grammars are supported, with case-insensitive names/aliases and lazy bundled registration. Use the installed release's language documentation when selecting a less common grammar.
|
|
76
|
+
|
|
77
|
+
`language: 'typescript'` (alias `'ts'`) uses TSX for `.tsx`, TypeScript otherwise (including `.ts`, `.mts`, `.cts`). Explicit `'tsx'` always uses TSX. Both TypeScript query variants validate eagerly; a TypeScript-only kind needs a TSX-compatible replacement:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { scopes } from '@alex-dbs/coderuler';
|
|
81
|
+
|
|
82
|
+
const assertions = scopes.astGrep({
|
|
83
|
+
language: 'typescript',
|
|
84
|
+
rule: { any: [{ kind: 'as_expression' }, { kind: 'type_assertion' }] },
|
|
85
|
+
tsx: { rule: { kind: 'as_expression' } },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`tsx: { rule, constraints?, utils? }` is valid only with TypeScript selection and replaces the **complete** query; repeat any needed constraints/utilities. Captures follow the selected query: metavariables provide strings or string arrays, and supported named declarations/Go receivers provide identity captures. Good: query syntax with AST structure. Bad: infer code semantics from line-oriented regular expressions or assume `tsx` merges options.
|
|
90
|
+
|
|
91
|
+
### Protobuf data
|
|
92
|
+
|
|
93
|
+
`scopes.protobuf` parses one file without resolving imports or checking types. Its document has this shape:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
type ProtoDeclaration = {
|
|
97
|
+
readonly id: string;
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly parent: string | null;
|
|
100
|
+
readonly options: { readonly [key: string]: JsonValue };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type ProtoDocument = {
|
|
104
|
+
readonly options: { readonly [key: string]: JsonValue };
|
|
105
|
+
readonly services: readonly ProtoDeclaration[];
|
|
106
|
+
readonly rpcs: readonly ProtoDeclaration[];
|
|
107
|
+
readonly messages: readonly ProtoDeclaration[];
|
|
108
|
+
readonly fields: readonly ProtoDeclaration[];
|
|
109
|
+
readonly oneofs: readonly ProtoDeclaration[];
|
|
110
|
+
readonly enums: readonly ProtoDeclaration[];
|
|
111
|
+
readonly enumValues: readonly ProtoDeclaration[];
|
|
112
|
+
readonly extensions: readonly ProtoDeclaration[];
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Each declaration has a fully qualified `id`, its source `name`, its enclosing declaration ID as `parent` (or `null` at file level), and an `options` object. Each list follows source order.
|
|
117
|
+
|
|
118
|
+
IDs follow these rules:
|
|
119
|
+
|
|
120
|
+
- Services use `Service`; RPCs use `Service.Rpc`.
|
|
121
|
+
- Top-level messages and enums use `Name`; nested messages and enums use `<enclosing message>.Name`.
|
|
122
|
+
- Fields and oneofs use `<message>.name`. A oneof member field has the message as its parent.
|
|
123
|
+
- Enum values use `<enum>.VALUE`.
|
|
124
|
+
- Extensions use `<enclosing message>.name`, or `name` at file level.
|
|
125
|
+
- A group creates a message with its written name and a lowercase field in the same scope. Fields in its body use the group message as parent.
|
|
126
|
+
- The package prefixes all IDs, regardless of where its statement appears.
|
|
127
|
+
|
|
128
|
+
Options include built-in and custom options. Custom names omit parentheses: `(corp.http.route)` becomes the key `corp.http.route`. Use bracket notation for dotted names, such as `$.rpcs[*].options['corp.http.route']`. Dotted assignments create nested objects. Extension keys such as `[acme.ext.note]` and `Any` keys such as `[type.googleapis.com/acme.Thing]` lose their brackets.
|
|
129
|
+
|
|
130
|
+
Adjacent string literals concatenate. Booleans stay booleans. Safe integers and finite decimal or exponent numbers become numbers; other number spellings, including hex, octal, suffixed, unsafe, and nonfinite values, remain source-text strings. Enum names become strings. Message literals become objects and lists become arrays. A key with multiple occurrences or any list occurrence becomes an array in source order; one list occurrence contributes its elements, including an empty list. A single non-list occurrence remains a plain value. Dotted paths cannot place a field inside a scalar or repeated value. Use JSONPath `..` to reach fields whether their containing option is a single object or an array. JSONPath unions return matches in union order.
|
|
131
|
+
|
|
132
|
+
Common paths:
|
|
133
|
+
|
|
134
|
+
| Data | Path |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| HTTP URLs | `$.rpcs[*].options['google.api.http']..['get','put','post','delete','patch']` |
|
|
137
|
+
| Custom HTTP path | `$.rpcs[*].options['google.api.http']..custom.path` |
|
|
138
|
+
| Field validation | `$.fields[*].options['validate.rules']` |
|
|
139
|
+
| Deprecated RPCs | `$.rpcs[*].options.deprecated` |
|
|
140
|
+
| File Go package | `$.options.go_package` |
|
|
141
|
+
|
|
142
|
+
IDs select declarations directly; selecting a nested node adds `:/<pointer suffix>` to its declaration ID. Other nodes use their JSONPath pointer as the ID, and the root ID is `$`. Source spans cover the whole file at the root; declarations run from their first token through `;` or `}`, with RPC semicolons included. Name spans cover the name token. Option spans cover their entries; scalar spans cover their tokens, message and list spans include their delimiters, and combined or repeated values span all contributing entries. A node without its own span uses the nearest ancestor span. Line spans are 1-based and inclusive. Malformed syntax fails selection with an error.
|
|
143
|
+
|
|
144
|
+
Use a protobuf option to enforce a route version:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { Rule, regex, scopes, test } from '@alex-dbs/coderuler';
|
|
148
|
+
|
|
149
|
+
export default class ProtoRouteVersion extends Rule {
|
|
150
|
+
files = ['proto/**/*.proto'];
|
|
151
|
+
scope = scopes.protobuf({ path: "$.rpcs[*].options['corp.http.route'].*" });
|
|
152
|
+
check = regex({ pattern: /^\/v\d+\//, violationMessage: 'Start the route with /v<N>/' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const service = (route: string) => `syntax = "proto3";
|
|
156
|
+
package acme.v3;
|
|
157
|
+
service Accounts {
|
|
158
|
+
rpc Close(Request) returns (Response) {
|
|
159
|
+
option (corp.http.route) = {post: "${route}"};
|
|
160
|
+
}
|
|
161
|
+
}`;
|
|
162
|
+
|
|
163
|
+
export const tests = [
|
|
164
|
+
test('a versioned route passes').withFiles({ 'proto/accounts.proto': service('/v3/accounts/:id/close') }).passes(),
|
|
165
|
+
test('an unversioned route violates').withFiles({ 'proto/accounts.proto': service('/accounts/:id/close') }).violates(),
|
|
166
|
+
];
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The rule name is `proto-route-version`, saved as `.coderuler/proto-route-version.rule.ts`.
|
|
170
|
+
|
|
171
|
+
## Custom scope, selection, and filters
|
|
172
|
+
|
|
173
|
+
The public shapes are:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import type { Awaitable, JsonValue, SourceFile, ScopeMatch } from '@alex-dbs/coderuler';
|
|
177
|
+
|
|
178
|
+
type ScopeSelector<T = JsonValue> =
|
|
179
|
+
(file: SourceFile) => Awaitable<readonly ScopeMatch<T>[]>;
|
|
180
|
+
// SourceFile: { readonly filepath: string; readonly source: string }
|
|
181
|
+
// ScopeMatch<T>: { readonly id: string; readonly source: string;
|
|
182
|
+
// readonly data?: T;
|
|
183
|
+
// readonly captures?: Readonly<Record<string, string | readonly string[]>>;
|
|
184
|
+
// readonly lines?: { readonly start: number; readonly end: number } }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Return a dense array; `[]` means no matches. Use stable nonempty IDs and actual source text; line spans are 1-based inclusive. `data` must be finite, acyclic, JSON-compatible (including `null`, `false`, `0`, and `''`); captures contain only strings/string arrays.
|
|
188
|
+
|
|
189
|
+
`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
|
+
|
|
191
|
+
This complete example selects owner annotations from two disjoint trees, gates asynchronously, and explains applicability. Adapt the policy and datasets together:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { Rule, pass, test, violation } from '@alex-dbs/coderuler';
|
|
195
|
+
import type {
|
|
196
|
+
Evaluator, PredicateResult, ProjectContext, ScopeSelector,
|
|
197
|
+
ScopeSubject, SelectionContext,
|
|
198
|
+
} from '@alex-dbs/coderuler';
|
|
199
|
+
|
|
200
|
+
const owners: ScopeSelector<string> = ({ source }) =>
|
|
201
|
+
source.split('\n').flatMap((line, index) => {
|
|
202
|
+
const owner = /^# owner: (\S+)\s*$/.exec(line)?.[1];
|
|
203
|
+
return owner === undefined ? [] : [{
|
|
204
|
+
id: `owner:${index + 1}`, source: line, data: owner,
|
|
205
|
+
lines: { start: index + 1, end: index + 1 },
|
|
206
|
+
}];
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
export default class AssignedOwner extends Rule<string> {
|
|
210
|
+
files = ['src/**/*.txt'];
|
|
211
|
+
scope = owners;
|
|
212
|
+
|
|
213
|
+
async when({ project }: ProjectContext): Promise<PredicateResult> {
|
|
214
|
+
return { applies: await project.hasFiles('package.json'), reason: 'Package projects only' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async select(context: SelectionContext): Promise<readonly ScopeSubject<string>[]> {
|
|
218
|
+
const source = await context.select({ files: this.files, exclude: this.exclude, scope: this.scope });
|
|
219
|
+
const docs = await context.select({ files: ['docs/**/*.md'], scope: this.scope });
|
|
220
|
+
return [...source, ...docs];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
filter({ data }: ScopeSubject<string>): PredicateResult {
|
|
224
|
+
return { applies: typeof data === 'string' && data !== 'generated', reason: 'Human-owned annotations only' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
check: Evaluator<string> = subject => {
|
|
228
|
+
const owner = (subject as ScopeSubject<string>).data!;
|
|
229
|
+
return owner === 'unassigned' ? violation('Assign a responsible team') : pass();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const tests = [
|
|
234
|
+
test('an unassigned annotation violates').withFiles({
|
|
235
|
+
'package.json': {}, 'src/job.txt': '# owner: unassigned',
|
|
236
|
+
}).violates(),
|
|
237
|
+
test('a named docs owner passes').withFiles({
|
|
238
|
+
'package.json': {}, 'docs/job.md': '# owner: platform',
|
|
239
|
+
}).passes(),
|
|
240
|
+
];
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The cast trusts this rule's default scope grouping and filter; revisit it when changing either. Preserve identity when transforming built-in subjects (`{ ...subject, ...changes }`). Every returned `(filepath, id)` must be unique within the rule; overlapping selections need an explicit identity strategy. Attribute synthetic scopes to a real eligible root-relative filepath. Custom selection still respects CLI target and reserved-path boundaries; use grouping for multi-file decisions rather than invented paths, hidden subjects, or fallback passes.
|
|
244
|
+
|
|
245
|
+
Selection excludes symlinks and directories named `.git`, `node_modules`, `.coderuler`, or `old`, plus root-only `dist`/`coverage`; explicit targets inside them error. Nested generated directories and regular files bearing reserved directory names remain eligible.
|
|
246
|
+
|
|
247
|
+
## Grouping and custom checks
|
|
248
|
+
|
|
249
|
+
`Grouping = 'scope' | 'file' | 'folder' | 'project' | { readonly by: 'folder'; readonly level: number }`.
|
|
250
|
+
|
|
251
|
+
- `'scope'`: one scope per invocation; `'file'`: one group per filepath.
|
|
252
|
+
- `'folder'`: containing folder (level 0); `{ by: 'folder', level }` requires a nonpositive integer; negative levels climb, clamping at `.`.
|
|
253
|
+
- `'project'`: one group at root `.`. Empty groups are never evaluated; member order is preserved.
|
|
254
|
+
|
|
255
|
+
Declare `group: Grouping = 'folder'` with `import type { Grouping } from '@alex-dbs/coderuler'` to retain the union type. One group produces exactly one decision; choose the smallest group containing the comparison's evidence.
|
|
256
|
+
|
|
257
|
+
Grouping helps you to evaluate scopes together. For instance, grouping per folder means that ALL scopes selected for that folder will be evaluated together. A grouping per project means that ALL scopes will be evaluated together ONCE for the project. An example could be to check that `id` in all JSON files of the project, are UNIQUE on the project.
|
|
258
|
+
|
|
259
|
+
`Subject<T> = ScopeSubject<T> | GroupSubject<T>`. Narrow with `subject.kind`; this evaluator enforces unique selected IDs across group files:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { pass, violation } from '@alex-dbs/coderuler';
|
|
263
|
+
import type { Evaluator } from '@alex-dbs/coderuler';
|
|
264
|
+
// GroupSubject<T>: { readonly kind: 'group';
|
|
265
|
+
// readonly group: { readonly type: 'file' | 'folder' | 'project'; readonly path: string };
|
|
266
|
+
// readonly members: readonly ScopeSubject<T>[] }
|
|
267
|
+
// Evaluator<T>: (subject: Subject<T>, context: CheckContext<T>) => Awaitable<CheckResult>
|
|
268
|
+
|
|
269
|
+
const uniqueIds: Evaluator = subject => {
|
|
270
|
+
const members = subject.kind === 'group' ? subject.members : [subject];
|
|
271
|
+
const ids = new Set<string>();
|
|
272
|
+
for (const { id, filepath } of members) {
|
|
273
|
+
if (ids.has(id)) {
|
|
274
|
+
return violation(`Duplicate scope ID: ${id}`, [{ filepath, scopeId: id }]);
|
|
275
|
+
}
|
|
276
|
+
ids.add(id);
|
|
277
|
+
}
|
|
278
|
+
return pass();
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Custom checks return `CheckResult`; `pass(): CheckResult` and `violation(message: string, locations?: readonly ViolationLocation[]): CheckResult` are convenience helpers.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
interface ViolationLocation {
|
|
286
|
+
readonly filepath: string;
|
|
287
|
+
readonly scopeId?: string;
|
|
288
|
+
readonly lines?: { readonly start: number; readonly end: number };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface CheckResult {
|
|
292
|
+
readonly status: 'pass' | 'violation';
|
|
293
|
+
readonly message?: string;
|
|
294
|
+
readonly locations?: readonly ViolationLocation[];
|
|
295
|
+
readonly probability?: number;
|
|
296
|
+
readonly threshold?: number;
|
|
297
|
+
readonly model?: string;
|
|
298
|
+
readonly usage?: AiUsage;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`locations` is allowed only for violations. Attribute only offending evaluated scopes or group members: use their `filepath` and `id` as `scopeId`, as above, rather than naming unrelated files. Each location requires a nonempty filepath and either a nonempty `scopeId`, `lines`, or both. A scope ID resolves that member's source lines when available.
|
|
303
|
+
|
|
304
|
+
Explicit `lines` are absolute file coordinates: positive safe integers with inclusive `start <= end`. Use a narrower subrange within the member's known lines; file-only locations must fit a member's range for that file. When the selected member has no line information, explicit lines may supply it.
|
|
305
|
+
|
|
306
|
+
With `locations` omitted, a scope violation uses its own source lines; a group violation does **not** automatically blame all members. Explicit `locations: []` disables automatic scope attribution. Without usable source lines (or a matching current PR diff anchor), `--auto-comment` reports the finding in an ordinary PR comment instead of guessing an inline location.
|
|
307
|
+
|
|
308
|
+
Set `comment` on the rule to replace each generated GitHub finding body with your Markdown. Omit it to keep the generated body. CodeRuler strips terminal controls and redacts credentials from overrides. Inline locations and check results stay unchanged. The fallback comment keeps its shared `CodeRuler violations` heading.
|
|
309
|
+
|
|
310
|
+
Scalar AI classification supplies no offending-member attribution: scope AI can use its subject's lines, but grouped AI needs explicit locations from a custom check wrapping `context.ai(...)`; otherwise it uses the fallback comment.
|
|
311
|
+
|
|
312
|
+
Results validate; supplied messages survive reporting. Violations without a message report rule/location without generated text. Return real probability/model/threshold metadata only; supplied probability/threshold must be finite in `[0, 1]`. Throw for execution errors. External libraries/providers may be called directly; no backend registry is required.
|
|
313
|
+
|
|
314
|
+
### Context helpers
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
| Context | Available members |
|
|
318
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
319
|
+
| `ProjectContext` | `readonly project: Project`; `trace(message: string, details?: JsonValue): void`. |
|
|
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>`. |
|
|
322
|
+
|
|
323
|
+
|
|
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.
|
|
325
|
+
|
|
326
|
+
## Regex: matching means passing
|
|
327
|
+
|
|
328
|
+
`regex<T = JsonValue>(options: RegexOptions<T>): Evaluator<T>` takes `{ readonly pattern: RegExp; readonly violationMessage?: string; readonly input?: (subject: ScopeSubject<T>, context: ProjectContext) => Awaitable<string> }`.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { Rule, regex, test } from '@alex-dbs/coderuler';
|
|
332
|
+
|
|
333
|
+
export default class InternalUrl extends Rule {
|
|
334
|
+
files = ['config/*.txt'];
|
|
335
|
+
check = regex({ pattern: /^\/internal\//, violationMessage: 'Use an /internal/ URL' });
|
|
336
|
+
}
|
|
337
|
+
export const tests = [
|
|
338
|
+
test('internal URL passes').withFiles({ 'config/url.txt': '/internal/jobs' }).passes(),
|
|
339
|
+
test('public URL violates').withFiles({ 'config/url.txt': '/public/jobs' }).violates(),
|
|
340
|
+
];
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Default input is `String(data)` for scalar/null data, otherwise source text. `input` replaces this **per scope member**, even for groups; every member must match for the group to pass. Regex automatically returns locations for the failing members only, using each member's filepath and scope ID. The pattern is cloned and `lastIndex` reset for deterministic `/g`/`/y` behavior. Good: express the allowed pattern. Bad: use a forbidden-pattern regex expecting matches to violate. Regex needs no provider/key and adds no AI metadata.
|
|
344
|
+
|
|
345
|
+
## AI options, examples, and settings
|
|
346
|
+
|
|
347
|
+
`ai<T = JsonValue>(options: AiOptions<T>): Evaluator<T>` accepts:
|
|
348
|
+
|
|
349
|
+
- `rule: string`: nonempty semantic policy; include exceptions and what visible evidence permits a violation.
|
|
350
|
+
- `examples?: { readonly good?: readonly string[]; readonly bad?: readonly string[] }`: conditioning examples, dense arrays of nonempty strings; omitted/empty lists are valid.
|
|
351
|
+
- `violationMessage?: string`: nonempty reporting-only message, not sent as instructions.
|
|
352
|
+
- `input?: (subject: Subject<T>, context: ProjectContext) => Awaitable<JsonValue>`: replace the default projection; output validates as JSON-compatible.
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
import { Rule, ai, test } from '@alex-dbs/coderuler';
|
|
356
|
+
import type { RuleSettings } from '@alex-dbs/coderuler';
|
|
357
|
+
|
|
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
|
+
```
|
|
373
|
+
|
|
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.
|
|
375
|
+
|
|
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`.
|
|
377
|
+
|
|
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.
|
|
379
|
+
|
|
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.
|
|
381
|
+
|
|
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.
|
|
383
|
+
|
|
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.
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
import { Rule, ai, scopes, test } from '@alex-dbs/coderuler';
|
|
388
|
+
|
|
389
|
+
export default class GoAppendSlice extends Rule {
|
|
390
|
+
files = ['**/*.go'];
|
|
391
|
+
|
|
392
|
+
scope = scopes.astGrep({
|
|
393
|
+
language: 'go',
|
|
394
|
+
rule: {
|
|
395
|
+
any: [{ kind: 'function_declaration' }, { kind: 'method_declaration' }],
|
|
396
|
+
has: {
|
|
397
|
+
kind: 'for_statement',
|
|
398
|
+
stopBy: 'end',
|
|
399
|
+
has: {
|
|
400
|
+
pattern: 'append($SLICE,$$$ARGS)',
|
|
401
|
+
stopBy: 'end',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
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.`,
|
|
418
|
+
examples: {
|
|
419
|
+
good: [/* Some examples, not shown here to keep it short*/],
|
|
420
|
+
bad: [/* Some examples, not shown here to keep it short*/],
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Project readers
|
|
427
|
+
|
|
428
|
+
Public signatures (import `Project`, `CsvOptions`, `CsvRow`, and `JsonValue` as types from `@alex-dbs/coderuler`):
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
interface Project {
|
|
432
|
+
readonly root: string;
|
|
433
|
+
glob(patterns: string | readonly string[]): Promise<readonly string[]>;
|
|
434
|
+
hasFiles(patterns: string | readonly string[]): Promise<boolean>;
|
|
435
|
+
stat(path: string): Promise<{ readonly kind: 'file' | 'directory' } | undefined>;
|
|
436
|
+
readText(path: string): Promise<string>;
|
|
437
|
+
readJson(path: string): Promise<JsonValue>;
|
|
438
|
+
readYaml(path: string): Promise<readonly JsonValue[]>;
|
|
439
|
+
readCsv(path: string, options?: CsvOptions): Promise<readonly CsvRow[]>;
|
|
440
|
+
}
|
|
441
|
+
// CsvOptions: { readonly headers?: boolean; readonly delimiter?: string; readonly key?: string }
|
|
442
|
+
// CsvRow: Readonly<Record<string, string>> | readonly string[]
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Use `glob` for existing eligible files in lexical order (`[]` when none), `hasFiles` for existence by glob, and `stat` for a file/directory distinction (`undefined` only for ENOENT). Globs use `node:path.matchesGlob` semantics; exclusion wins. `readText` decodes strict UTF-8; structured readers use the selectors' strict parsing, with YAML returning one value per nonempty document and CSV returning records/arrays according to `headers`. CSV reader `key` shares option validation but does not add IDs or enforce key-column uniqueness; that is selector behavior.
|
|
446
|
+
|
|
447
|
+
The project root is invocation cwd for `check`/`validate`, and resolved `[folder]` for `inspect`; an absolute/sibling inspected folder is its own root. Reader paths are root-relative with `/` separators; absolute paths, `..` escapes, and symlink traversal are rejected. Reads require real files and throw on missing/unreadable/invalid input. Good: gate optional projects with `hasFiles`/`stat`. Bad: catch a read/parse error and treat it as empty input or a pass. Source text is cached per run; parsed values are per-use, so mutating one parse cannot alter another rule's view.
|