@drkmttr/adhere 0.21.2 → 0.23.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
@@ -27,1588 +27,64 @@ happen in a separate, asynchronous process.
27
27
  ## Should
28
28
 
29
29
  ```ts
30
- import db from "./lib/db";
31
- import googleMaps from "./lib/maps";
32
-
33
30
  // GET /api/restaurants/:zipCode reads only our own database.
34
31
  export async function findRestaurants(zipCode: string) {
35
32
  return db.restaurants.find({ where: { zipCode } });
36
33
  }
37
-
38
- // A scheduled job copies new restaurants in, off the request path.
39
- export async function syncRestaurants(since: Date) {
40
- const restaurants = await googleMaps.findAll({ updatedSince: since });
41
- await Promise.all(restaurants.map((restaurant) => db.restaurants.upsert(restaurant)));
42
- }
43
34
  ```
44
35
 
45
36
  ## Should not
46
37
 
47
38
  ```ts
48
- import googleMaps from "./lib/maps";
49
-
50
- // GET /api/restaurants/:zipCode
39
+ // When Google Maps is down, slow, or over quota, so is this endpoint.
51
40
  export async function findRestaurants(zipCode: string) {
52
- // When Google Maps is down, slow, or over quota, so is this endpoint.
53
41
  return googleMaps.findAll({ where: { zipCode } });
54
42
  }
55
43
  ```
56
44
  ````
57
45
 
58
46
  adhere asks [Jev](https://typesafe.ai), TypeSafe AI's System One model, whether
59
- each file breaks each rule. Jev answers with a calibrated probability. adhere
47
+ each file breaks each rule which returns a calibrated probability. adhere
60
48
  reports each rule whose probability is above a threshold, and shows the part of
61
- the file Jev points at. The report looks like Oxlint's, the linter `vp lint`
62
- runs:
49
+ the file Jev points at. The report looks like Oxlint's:
63
50
 
64
51
  ![adhere lint demo](https://github.com/darkmatter/adhere/raw/main/eval/demo.gif)
65
52
 
66
53
  Rules a normal linter can check exactly, such as a banned import or a type
67
- error, belong in that linter. [`adhere validate`](#validating-rules) lists the
68
- rules Jev thinks a linter could check, and finds rules that contradict each
69
- other.
70
-
71
- ## Contents
72
-
73
- - [Install](#install)
74
- - [Getting Started](#getting-started)
75
- - **Rules**
76
- - [Rule format](#rule-format)
77
- - [Writing good rules](#writing-good-rules)
78
- - [Generate rules from your repo](#generate-rules-from-your-repo)
79
- - [Install rules from other repos](#install-rules-from-other-repos)
80
- - [Presets](#presets)
81
- - **Usage**
82
- - [Reading the report](#reading-the-report)
83
- - [Config](#config)
84
- - [Running lint](#running-lint)
85
- - [Commands](#commands)
86
- - **Validate & improve rules**
87
- - [Validating rules](#validating-rules)
88
- - [Live evals](#live-evals)
89
- - **Advanced features**
90
- - [State Hooks and Type-Awareness](#state-hooks-and-type-awareness)
91
- - [Providers](#providers)
92
- - [Comments and suppressions](#comments-and-suppressions)
93
- - [Agent skills](#agent-skills)
94
- - **Reference**
95
- - [Cache](#cache)
96
- - [How a file is judged](#how-a-file-is-judged)
97
- - [Compose your AGENTS.md](#compose-your-agentsmd)
98
- - [Contributing](#contributing)
99
- - [License](#license)
54
+ error, belong in that linter.
55
+ [`adhere validate`](https://dm.sh/adhere/validating-rules/) lists the rules Jev
56
+ thinks a linter could check, and finds rules that contradict each other.
100
57
 
101
- ## Install
102
-
103
- Install the standalone executable. It needs no Node or Bun, and runs in any
104
- directory, inside a repo or not:
58
+ ## Get started
105
59
 
106
60
  ```sh
107
61
  curl -fsSL https://github.com/darkmatter/adhere/raw/main/scripts/install.sh | sh
62
+ adhere login # save your TypeSafe AI API key
63
+ adhere lint --preset typescript,react # try the built-in presets on your code
64
+ adhere init # scaffold .adhere/config.ts and two example rules
65
+ adhere rules create # write a rule with a coding agent: claude, codex, or omp
108
66
  ```
109
67
 
110
- It installs to `~/.local/bin`. If your shell can't find `adhere` afterwards, add
111
- that directory to your `PATH`.
112
-
113
- To pin adhere's version in a repo, so the team and CI run the same one, add it
114
- as a dev dependency and run it from the package:
115
-
116
- ```sh
117
- bun add -d @drkmttr/adhere
118
- bun run adhere lint
119
- ```
120
-
121
- With npm or pnpm, add it the same way and run `npm exec adhere` or
122
- `pnpm exec adhere`. The package is `@drkmttr/adhere`: `adhere` on npm is an
123
- unrelated package.
124
-
125
- - The package includes the TypeScript compiler that
126
- [type awareness](#what-jev-reads) needs. The standalone executable does not,
127
- so it runs every audit except type-aware ones.
128
- - The [install script](scripts/install.sh) puts this machine's executable from
129
- the [latest release](https://github.com/darkmatter/adhere/releases/latest) in
130
- `~/.local/bin`. Run it again to update. The executable is built for macOS and
131
- Linux, on arm64 and x64, and for Windows x64: download
132
- `adhere-Windows-x86_64.exe` from the release.
133
- - adhere sends each file it judges to Jev at `api.typesafe.ai`, with what it
134
- [reads](#what-jev-reads) beside it, authenticated with a TypeSafe AI API
135
- key.
136
-
137
- ### Login
138
-
139
- ```sh
140
- adhere login # prompts for the key, masking what you type
141
- adhere login < key.txt # reads it from stdin instead
142
- adhere logout # deletes the saved key
143
- adhere login --openai # saves an OpenAI API key, for the openai provider
144
- adhere logout --openai # deletes it, and only it
145
- ```
146
-
147
- - The key is saved to `~/.config/adhere/credentials.json`, or under
148
- `$XDG_CONFIG_HOME` when that is set, readable only by you. An OpenAI key is
149
- saved beside the TypeSafe AI one, for a config whose provider is
150
- `openai()`; see [Providers](#providers).
151
- - `TYPESAFE_API_KEY`, when set, takes precedence over the saved key, so CI can
152
- pass a key without a login. `OPENAI_API_KEY` does the same for the saved
153
- OpenAI key.
154
- - The key is read only when a request is about to be sent. A run that the
155
- cache answers in full needs no key and no network.
156
-
157
- ## Getting Started
158
-
159
- Install the executable (see [Install](#install)) and save your TypeSafe AI API
160
- key once (see [Login](#login)), then try the built-in presets on your code
161
- before you write a rule of your own. The executable runs in any directory:
162
-
163
- ```sh
164
- adhere login
165
- adhere lint --preset typescript,react
166
- ```
167
-
168
- Without installing anything, `npx @drkmttr/adhere` or `bunx @drkmttr/adhere`
169
- runs the same commands. Presets cover TypeScript, React, security, Effect, and
170
- alchemy. See [Presets](#presets).
171
-
172
- To set up rules of your own in a repo:
173
-
174
- ```sh
175
- adhere init # .adhere/config.ts, two example rules, and the dev dependency
176
- bun run adhere validate # check your rules' wording, and ask Jev whether any contradict
177
- bun run adhere lint # audit the working directory
178
- git add .adhere # commit the rules, and the judgments they cost
179
- ```
180
-
181
- ### Init
182
-
183
- `adhere init` creates a config file and some example rules and, in a repo with
184
- a `package.json`, adds adhere as a dev dependency.
185
-
186
- It is safe to rerun: existing files are reported as skipped, and only `--force`
187
- overwrites them.
188
-
189
- ### The day-to-day loop
190
-
191
- After that, using adhere is a short loop:
192
-
193
- 1. **Write or edit a rule** in `.adhere/rules/`. A rule is one sentence saying
194
- what code must be, with code that shows it: see
195
- [Rule format](#rule-format).
196
- 2. **Run `adhere lint`.** It says what it found and what judging takes, and
197
- asks before it sends anything: see [Running lint](#running-lint).
198
- 3. **Read the findings.** Each finding names the rule, Jev's probability, and
199
- the lines it points at: see [Reading the report](#reading-the-report).
200
- 4. **Fix the code, the rule, or neither.** A wrong finding is usually the
201
- rule's wording reaching too far: see
202
- [Writing good rules](#writing-good-rules). Suppress a finding that is wrong
203
- for this code alone with a comment that says why: see
204
- [Comments and suppressions](#comments-and-suppressions).
205
- 5. **Commit `.adhere/`, cache included.** Judgments are cached by file content
206
- and rule text, so the next run, and CI, only pay for what changed: see
207
- [Cache](#cache).
208
-
209
- ## Rule format
210
-
211
- A repo's own rules live in `.adhere/rules/`, organized just like skills:
212
-
213
- ```text
214
- .adhere/
215
- config.ts optional
216
- cache/ commit it
217
- rules/
218
- data/ a topic: a directory that holds no rule
219
- brand-ports/
220
- RULE.md the rule data/brand-ports
221
- columns/
222
- RULE.ts a rule written in TypeScript
223
- schema.ts a helper it imports, not read as a rule
224
- ```
225
-
226
- - The directory's path is the rule id: `.adhere/rules/data/brand-ports/RULE.md`
227
- is `data/brand-ports`.
228
- - A rule can be markdown `RULE.md`, or typescript [`RULE.ts`](#rules-as-typescript-files). A
229
- directory with both refuses the run.
230
- - Files not named `RULE` are ignored.
231
-
232
- ### Rules as Markdown files
233
-
234
- A `RULE.md` is front matter, then a body that holds the rule's code:
235
-
236
- ````md
237
- ---
238
- description: A port must be a branded, range-checked integer, never a bare number.
239
- threshold: 0.8
240
- ---
241
-
242
- Why: a bare `number` accepts 70000 and -1.
243
-
244
- ## Must
245
-
246
- ```ts
247
- const Port = Schema.Int.pipe(
248
- Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
249
- Schema.brand("Port"),
250
- );
251
- ```
252
-
253
- ## Never
254
-
255
- ```ts
256
- const port: number = Number(process.env.PORT);
257
- ```
258
- ````
259
-
260
- | Front matter | Meaning |
261
- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
262
- | `description` | Required. One sentence that says what code must be and what it must never be. |
263
- | `threshold` | The rule's own cutoff, in place of the config's. |
264
- | `level` | `warning` reports the rule's findings as warnings, which do not fail the run. Use it for a nit, or for a rule that tends to flag code wrongly. |
265
- | `tests` | Rules skip tests by default. `only` is for a rule about tests, which judges nothing else. `include` is for one that holds in tests as well. |
266
- | `appliesTo`, `excludeIf` | Where the rule applies. See [Scoping a rule](#scoping-a-rule). |
267
-
268
- | Heading in the body | The code under it |
269
- | ---------------------------- | ----------------------------------------------------------- |
270
- | `## Must` | must be written |
271
- | `## Never` | must never be written, which is what a violation looks like |
272
- | `## Should`, `## Should not` | the same, for a guideline |
273
-
274
- - A rule needs code under at least one heading.
275
- - A rule is a requirement ("must", "never") or a guideline ("should", "should
276
- not"), and cannot mix the two.
277
- - A rule with only a `never` block suits a rule with no single correct form to
278
- show, such as a hand-rolled retry loop or an error caught and dropped.
279
- - Put neither kind of code in the other's place: code that must never be
280
- written, under `must`, reads to Jev as the pattern to follow.
281
- - Prose around the code is the rule's details: Jev reads it after the
282
- description, so use it to say why the rule holds or where it does not apply.
283
- The report shows the description alone.
284
-
285
- <details>
286
- <summary>More on headings, fences, and loading</summary>
287
-
288
- - A heading names the code in its section, which runs to the next heading at its
289
- level or higher, so a deeper heading such as `### A bare number` stays inside
290
- it.
291
- - Only a heading that is the word alone names code, in any case and with or
292
- without a colon: `## Never:` does, `## Never do this` does not.
293
- - A fence can instead name its code after its language, as in `ts never`, which
294
- GitHub does not show. A fence's own word wins over its heading's word.
295
- - A rule inline in a config takes the same keys, with its code under `must`,
296
- `never`, `should`, and `shouldNot`, and its prose under `details`.
297
- - `loadRules(directory)` from the package root does the same load for your own
298
- tooling.
299
-
300
- </details>
301
-
302
- ### Scoping a rule
303
-
304
- `appliesTo` and `excludeIf` say where a rule applies, apart from what it asks
305
- for. Each is a list of descriptions of code: a JSON array on one line in front
306
- matter, or an array in a config or a TypeScript rule.
307
-
308
- <!-- prettier-ignore -->
309
- ```md
310
- ---
311
- description: Structured variants must be Schema.TaggedClass members of a Schema.Union, never hand-written object types joined by a tag field.
312
- appliesTo: ["a declared union type or schema"]
313
- excludeIf: ["a type that mirrors a third-party format whose tag key that format fixes, such as a Slack Block Kit block"]
314
- ---
315
- ```
316
-
317
- | Key | Jev is asked, of the code that breaks the rule | A finding stands when |
318
- | ----------- | ---------------------------------------------- | ------------------------- |
319
- | `appliesTo` | whether any of it is as described | Jev says yes to every one |
320
- | `excludeIf` | whether all of it is as described | Jev says yes to none |
321
-
322
- The questions are about the code that breaks the rule, not the whole file, so a
323
- file with a real violation beside code an `excludeIf` describes keeps its
324
- finding.
325
-
326
- A yes is a probability above 0.5. At `--log-level debug`, lint logs each
327
- finding its matchers dropped, with their scores.
328
-
329
- > **Narrowing the description often works better.** In the preset study, Jev
330
- > scored an `excludeIf` near 0.5 for real and false findings alike. See
331
- > [Writing good rules](#writing-good-rules).
332
-
333
- ### Rules as TypeScript files
334
-
335
- A rule's directory can hold a `RULE.ts` instead of a `RULE.md`. It
336
- default-exports `defineRule({...})`, which takes the same fields as a config's
337
- inline rule. TypeScript adds what a `RULE.md` cannot have: a
338
- [state hook](#state-hooks), which adds to what Jev reads for the rule, and
339
- [rule options](#rule-options), which each project that uses the rule sets.
340
-
341
- - adhere imports a `RULE.ts`, as it imports a config, so its code runs on
342
- every lint.
343
- - It can import other files by relative path, such as a helper beside it in
344
- its directory. The executable resolves no packages besides
345
- `@drkmttr/adhere`.
346
- - A `RULE.ts` that default-exports no rule refuses the run.
347
-
348
- ### Where rules live
349
-
350
- - Rules in the root `.adhere/rules/` apply project-wide.
351
- - A nested `.adhere/` scopes its rules to the directory that contains it. A rule
352
- in `packages/api/.adhere/rules/data/brand-ports/RULE.md` has the same id,
353
- `data/brand-ports`, but applies only to files under `packages/api/`.
354
- - If a nested rule has the same id as a root rule, the nearest containing
355
- `.adhere/` shadows the less-specific rule for that subtree. Outside that
356
- subtree, the root rule still applies.
357
- - Nested `.adhere/` directories are not discovered under `node_modules/`,
358
- `dist/`, and the other [skipped directories](#what-gets-read).
359
-
360
- To keep your rules with the rest of your documentation, point `rules` at a
361
- directory such as `docs/adhere/`:
362
-
363
- ```ts
364
- export default defineConfig({ presets: ["effect"], rules: "./docs/adhere" });
365
- ```
366
-
367
- That directory holds a directory per rule, as `.adhere/rules/` does, and its
368
- rules apply project-wide.
369
-
370
- ## Writing good rules
371
-
372
- The [eval](eval/README.md#studies) measured how Jev reads a rule, on the
373
- presets and on other repos, to catch the most violations with the fewest false
374
- positives. It has every study. These are its lessons for writing a rule.
375
-
376
- ### Rule writing tips
377
-
378
- - **Say "must" and "never"**, in the description and in the headings over the
379
- code, or "should" and "should not" for a guideline. Rewording the presets'
380
- descriptions this way, with a bad example per rule, was the largest gain of
381
- any study: at 0.8 it caught as many violations as before, with 1 wrong
382
- finding where there had been 5 or 6 ([study](eval/studies/must-never.md)).
383
- - **Give one example of each kind**: one `must` block and one `never` block. A
384
- bad example helped every wording, and at 0.9 caught 24 violations with none
385
- wrong, where there had been 17
386
- ([study](eval/studies/rule-vocabulary.md)). Either kind alone did worse, and
387
- three of each did no better, for 1.58 times the tokens
388
- ([study](eval/studies/example-count.md)).
389
-
390
- `adhere validate` lists each rule worded otherwise.
391
-
392
- ### What else the studies found
393
-
394
- - **"Should" is for people, not a weaker check.** Rules written with "should"
395
- and with "must" scored nearly the same
396
- ([study](eval/studies/rule-vocabulary.md)).
397
- - **A wrong finding is usually the rule's fault.** Above 0.8, 31 of 35 wrong
398
- findings repeated within a rule: a scope its words reach past, or the same
399
- misreading again. Fixing the wording fixes them together. Below 0.8, one-off
400
- misreads grow, to about one finding in six at 0.6 to 0.7
401
- ([study](eval/studies/presets.md)).
402
- - **Narrow a rule in its description.** Jev scores an `excludeIf` near 0.5 for
403
- real and false findings alike, so it thins a rule's findings about as much as
404
- a higher threshold ([study](eval/studies/presets.md)). Narrowing the
405
- description moves the judgment itself: scoping alchemy's idempotent-delete
406
- rule to providers' delete handlers dropped its findings elsewhere from
407
- 0.67–0.88 to 0.08–0.21, with nothing real lost
408
- ([study](eval/studies/idempotent-delete.md)).
409
- - **Beware rules that turn on what the file does not show.** The weakest preset
410
- rules hinged on facts outside the file: whether a client has a default
411
- timeout, whether a script is an entry point, whether a key is public
412
- ([study](eval/studies/presets.md)). Warning below a context of 0.7 marked one
413
- finding in five, and those were real about half the time, where the rest were
414
- real three times in four ([study](eval/studies/sufficiency.md)). Make such a
415
- rule a warning, or give Jev the fact with
416
- [`typeAware` or workspace `reads`](#what-jev-reads), an
417
- [`appendState`](#state-hooks) hook, or an `@adhere` note.
418
- - **Jev believes comments.** Eight of nine real violations of the
419
- idempotent-delete rule carried a comment wrongly saying they were fine, and
420
- scored 0.31 to 0.62, while without comments all nine scored 0.81 or more
421
- ([study](eval/studies/idempotent-delete.md)). That is why adhere takes
422
- comments out. A fact Jev needs, such as an exemption, goes in an `@adhere`
423
- note, which stays ([study](eval/studies/comments.md)).
424
-
425
- ## Generate rules from your repo
426
-
427
- Let an agent write your first rules with you. The
428
- [`adhere-setup`](./skills/adhere-setup/SKILL.md) skill walks the agent through
429
- setting adhere up in your repo:
430
-
431
- 1. It reads where your conventions live: `AGENTS.md`, `CONTRIBUTING.md`, docs
432
- and ADRs, your lint config, recent review comments, and the code others copy
433
- from.
434
- 2. It drafts the conventions a normal linter cannot check as rules, each with
435
- real code from your repo, and sets aside the ones a linter could check.
436
- 3. It adds the presets that fit your stack, and rules from your organization's
437
- [shared repo](#install-rules-from-other-repos), if you have one.
438
- 4. It shows every candidate in one list, with its evidence, and imports the ones
439
- you choose.
440
- 5. It validates them, walks you through the first lint and what it costs, and
441
- adds adhere to CI.
442
-
443
- It stops to ask you at each of those decisions, and never asks for your API key.
444
-
445
- ```sh
446
- skills add darkmatter/adhere # install adhere's skills for your agent
447
- ```
448
-
449
- Then ask your agent to set adhere up.
450
-
451
- ## Install rules from other repos
452
-
453
- An organization's rules can live in one repo's `.adhere/rules/`, and other repos
454
- copy them in with `adhere install`:
455
-
456
- ```sh
457
- adhere list darkmatter/standards # each rule there, with its description
458
- adhere install darkmatter/standards # every rule
459
- adhere install darkmatter/standards/data # the data topic
460
- adhere install darkmatter/standards/data/brand-ports # one rule
461
- adhere install darkmatter/standards#v3 # every rule, as tagged v3
462
- ```
463
-
464
- - Copies land in this repo's `.adhere/rules/org/repo/`, so `data/brand-ports`
465
- from darkmatter/standards lands in
466
- `.adhere/rules/darkmatter/standards/data/brand-ports/` as the rule
467
- `darkmatter/standards/data/brand-ports`, and two repos' rules never collide.
468
- - The copies are the repo's own rules from then on: commit them, edit them, or
469
- start new rules from them. Nothing tracks where they came from.
470
- - A later install skips a rule already there, just as init does. `--force`
471
- replaces its directory, edits and all.
472
- - A rule's whole directory is copied. The source's config, its inline rules, and
473
- its cache are not.
474
-
475
- > **Read what you install.** A `RULE.ts` is copied with the helpers beside it,
476
- > and runs whenever lint does, in CI too. `adhere list` names a `RULE.ts`
477
- > without its description, since reading the description would run the file.
478
-
479
- The source is cloned with `git` from `https://github.com/org/repo.git`, so a
480
- private repo needs git's credentials for GitHub, which `gh auth setup-git` sets
481
- up. To clone over SSH instead, let git rewrite the URL:
482
- `git config --global url.git@github.com:.insteadOf https://github.com/`.
483
-
484
- ### Creating a shared repo
485
-
486
- `adhere init --shared org/repo` scaffolds a repo whose rules other repos copy in
487
- with `adhere install`, instead of one that lints itself. It writes the two
488
- example rules and no config, since install copies only rule files, and with
489
- them:
490
-
491
- - **`README.md`** says what the repo is for, how to install its rules, and how
492
- to add one.
493
- - **`.github/workflows/adhere.yaml`** runs `adhere validate` on every push to
494
- main and every pull request, with the `TYPESAFE_API_KEY` secret. A pull
495
- request from a fork gets no secrets, so validate refuses there.
496
- - **`alchemy.run.ts`** is an [alchemy](https://alchemy.run) stack that creates
497
- the GitHub repo, or adopts it, and sets that secret from `TYPESAFE_API_KEY`
498
- when deployed with `npx alchemy deploy`.
499
- - **`package.json`** pins alchemy and effect, which init installs, to versions
500
- that work together.
501
- - **`.gitignore`** keeps the installed packages and alchemy's state out of git.
502
-
503
- The repo is public unless you answer yes when init asks, at a terminal, whether
504
- to make it private. Other repos then need git's credentials for it to list and
505
- install its rules. Without a terminal it is public: change `visibility` in the
506
- stack to make it private.
507
-
508
- ## Presets
509
-
510
- Built-in rule sets. Name one, or one of its topics, in the config's `presets`
511
- or with `--preset`:
512
-
513
- | Preset | Rules | For | What it asks for |
514
- | ------------------------------------- | ----- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
515
- | [`typescript`](./presets/typescript/) | 11 | any TypeScript project | Data from outside checked at runtime, invalid states unrepresentable, errors never swallowed, resources released on every path, arguments not mutated, and tests that assert outcomes and stand alone. |
516
- | [`react`](./presets/react/) | 10 | components and hooks | Logic for an event in its handler rather than an effect, effects that clean up and ignore stale fetches, `useSyncExternalStore` for outside stores, state that neither copies props nor contradicts itself, and Server Functions and Server Components that guard what crosses to the client. |
517
- | [`security`](./presets/security/) | 4 | any project | Secrets kept out of logs, error messages, and responses; parameterized SQL; no untrusted input in shell commands, `eval`, or file paths; and secrets compared in constant time. From OWASP's cheat sheets. |
518
- | [`effect`](./presets/effect/) | 16 | code written with Effect | Steps sequenced with `Effect.gen`, instrumentation attached with `.pipe`, config read through a service and validated, secrets redacted, branded primitives and tagged unions, defects kept apart from typed errors, services built by layers, and tests with their own layers and `TestClock`. |
519
- | [`alchemy`](./presets/alchemy/) | 41 | code that deploys with [alchemy](https://alchemy.run) | Where Config and bindings are read, which resources keep their data, how secrets stay out of bundles and logs, authorization on public URLs, migrations, durable workflows, and custom providers. |
520
-
521
- - Name presets in the config's `presets`, or on the command line with
522
- `--preset`, repeated or separated by commas, as in `--preset effect,alchemy`.
523
- Naming presets on the command line makes the config file optional, and
524
- presets named in both places apply together.
525
- - A topic, one of a preset's subdirectories, is a preset of its own:
526
- `--preset effect/basics` applies only the rules under
527
- `presets/effect/basics/`, and `alchemy/secrets` applies only alchemy's
528
- secrets rules. `security` has no topics.
529
- - To change a preset's rule without copying it, use [Overrides](#overrides).
530
- - [presets/README.md](./presets/README.md) lists every rule in each preset,
531
- with a one-line summary and a link to the full rule.
532
-
533
- <details>
534
- <summary>What every preset does</summary>
535
-
536
- - It has no rules for what a linter checks exactly.
537
- [presets/README.md](./presets/README.md) names the lint rules to turn on
538
- instead.
539
- - A preset rule says "must" only where its source makes a requirement, and
540
- "should" where the source gives advice.
541
- - A topic's rules keep the ids they have in the whole preset, so a topic and its
542
- preset share cached judgments, and naming both applies each rule once.
543
- - `effect` and `alchemy` have no rule that `typescript` or `security` already
544
- has, so a project on Effect or alchemy names `typescript` and `security` too.
545
-
546
- </details>
547
-
548
- ## Reading the report
549
-
550
- A finding looks like this:
551
-
552
- ```text
553
- × data/brand-ports: A port must be a branded, range-checked integer, never a bare number.
554
- confidence 0.93 · context 0.88
555
- ╭─[src/server.ts:6:3]
556
- 1 │ import { Effect } from "effect";
557
- 2 │ import { listen } from "./listen.ts";
558
- 3 │
559
- 4 │ export const serve = Effect.gen(function* () {
560
- 5 │ const host = process.env.HOST ?? "localhost";
561
- 6 │ const port: number = Number(process.env.PORT ?? 3000);
562
- · ──────────────────────────────────────────────────────
563
- 7 │ yield* listen({ host, port });
564
- 8 │ yield* Effect.log(`Listening on ${host}:${port}`);
565
- 9 │ });
566
- ╰────
567
- hint: const Port = Schema.Int.pipe(
568
- Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
569
- Schema.brand("Port"),
570
- );
571
-
572
- confidence 0–0.74 < 0.8 < 0.85–1 Jev's probability that the file breaks the rule
573
- context 0–0.49 < 0.6 < 0.70–1 its probability that the file shows enough to decide
574
-
575
- Found 1 error.
576
- 42 files, 3 judged, 39 cached.
577
- ```
578
-
579
- | Part | What it tells you |
580
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
581
- | `×` or `⚠` | `×` marks an error. `⚠`, in amber, marks a warning from a rule whose `level` is `warning`. Only errors fail the run. |
582
- | `data/brand-ports` | The rule's id, then its description. A preset rule's id starts with the preset's name, as in `effect/basics/gen-for-sequencing`. |
583
- | `confidence` | Jev's probability that the file breaks the rule. |
584
- | `context` | Jev's probability that it sees enough to decide that. |
585
- | The excerpt | The section of the file Jev points at. When Jev names one line, it is underlined. When it names several, a bar beside the code marks them. |
586
- | `hint:` | The rule's code that must be written. A rule with only code that must never be written shows that code instead, labeled `never:`. |
587
- | The legend | Under the last finding: each score's threshold, between its red range and its green range. |
588
-
589
- **Score colors.** On a terminal, each score is colored by where it sits around
590
- its threshold. The legend shows each color's range:
591
-
592
- - **Green** is well above the threshold.
593
- - **Amber** is near it, on either side. When many scores are amber, the
594
- threshold may be too low or too high for the rule.
595
- - **Red** is well below it. `lint` never reports a `confidence` this low, so
596
- only the legend shows that range. A red `context` marks the finding to doubt
597
- first.
598
-
599
- **Low context.** Jev sees the file and what adhere [reads](#what-jev-reads)
600
- beside it, not your whole repo, so adhere also asks Jev whether that shows
601
- enough to decide the rule at all: the `context` score. When `context` is below
602
- the sufficiency threshold, 0.6 by default, Jev needs more than it sees, and the
603
- finding carries a warning:
604
-
605
- ```text
606
- warning: this file may not show enough to check this rule
607
- help: add what the code relies on outside this file as a note Jev reads:
608
- // @adhere <the fact>, and how you know it
609
- ```
610
-
611
- - On the eval, most findings with this warning were false, compared with about
612
- one in five of all findings ([study](eval/studies/sufficiency.md)). Check what
613
- the code relies on outside the file before acting on such a finding.
614
- - Give Jev a fact it lacks with an `@adhere` note (see [Comments](#comments)).
615
- - Change the threshold with `sufficiencyThreshold` in the config, or with
616
- `--sufficiency-threshold` for one run.
617
-
618
- ## Config
619
-
620
- A config file is optional. Without one, adhere reads the rules in
621
- `.adhere/rules/` and any `--preset`. A config names presets, sets the model,
622
- the thresholds, and what Jev reads, or gives rules inline. Put it in the
623
- working directory of the repo being audited, at one of these paths (keep one):
624
-
625
- - `.adhere/config.ts`, the default, next to the rules
626
- - `adhere.config.ts`
627
- - `.adhere.config.ts`
628
-
629
- ```ts
630
- import { defineConfig } from "@drkmttr/adhere";
631
-
632
- export default defineConfig({
633
- threshold: 0.8,
634
- presets: ["effect"],
635
- exclude: ["**/generated/**"],
636
- overrides: { "effect/basics/instrument-with-pipe": "off" },
637
- rules: {
638
- "data/brand-meaningful-primitives": {
639
- description:
640
- "A primitive with semantic meaning, such as an id, email, URL, port, or count, must be a branded schema.",
641
- must: `
642
- const UserId = Schema.String.pipe(Schema.brand("UserId"))
643
- type UserId = typeof UserId.Type
644
- `,
645
- never: "type UserId = string",
646
- threshold: 0.8,
647
- },
648
- },
649
- });
650
- ```
651
-
652
- Every key, grouped by what it changes:
653
-
654
- **What adhere loads**
655
-
656
- - `presets` — default: none. Built-in rule sets, or their topics. See
657
- [Presets](#presets).
658
- - `rules` — default: `.adhere/rules/`. Rules inline, as above, or a directory
659
- (see [Where rules live](#where-rules-live)). Either form replaces
660
- `.adhere/rules/`: none of its rules, root or nested, is read then.
661
- - `overrides` — default: none. Settings for a rule by its id. See
662
- [Overrides](#overrides).
663
- - `exclude` — default: none. Globs, relative to the working directory, for
664
- files no rule judges.
665
- - `instructions` — default: `.adhere/instructions/`, then
666
- `.agents/instructions/`. Instructions directory relative to the config file,
667
- used by `adhere postinstall`.
668
-
669
- **Thresholds**
670
-
671
- - `threshold` — default: `0.8`. Report a rule when Jev's probability is above
672
- this.
673
- - `sufficiencyThreshold` — default: `0.6`. Below it, a finding warns that Jev
674
- may not see enough to decide.
675
-
676
- **What Jev reads**
677
-
678
- - `reads` — default: `["workspacePackages"]`. The workspace context Jev
679
- reads. See [What Jev reads](#what-jev-reads).
680
- - `typeAware` — default: `false`. Add the compiler's `symbols` and
681
- `references`, as `--type-aware` does.
682
- - `includeComments` — default: `false`. Send every comment to Jev. See
683
- [Comments](#comments).
684
- - `maxSymbolDocumentationLength` — default: `256`. Maximum characters of each
685
- symbol's combined documentation and JSDoc tags, before `maxSymbolLength`
686
- applies. A non-negative integer; `0` omits them.
687
- - `maxSymbolLength` — default: `256`. Maximum characters of each complete
688
- `symbols[name]` string, including type signatures and documentation. A
689
- non-negative integer; `0` leaves names with empty content.
690
-
691
- **Model**
692
-
693
- - `model` — default: `"jev-latest"`. The model id sent to TypeSafe AI.
694
- - `provider` — default: TypeSafe AI's Jev. Where the questions go instead, with
695
- its own model. See [Providers](#providers).
696
-
697
- An invalid shape refuses the run. A config can import other files by relative
698
- path, but no packages besides `@drkmttr/adhere`: the executable does not
699
- resolve `node_modules`.
700
-
701
- <details>
702
- <summary>More on types in the editor</summary>
703
-
704
- - `defineConfig` types the config for the editor and returns it as it is.
705
- `satisfies Config`, with `import type { Config }`, does the same.
706
- - The executable supplies `@drkmttr/adhere` to the config whether or not the
707
- repo has the package installed. The editor's types come from the package,
708
- which `adhere init` adds as a dev dependency.
709
- - A tsconfig's globs skip dot directories, so the editor opens
710
- `.adhere/config.ts` outside any project, where it cannot resolve the package's
711
- types: they are reached only by `bundler`, `node16`, or `nodenext` resolution.
712
- `adhere init` writes `.adhere/tsconfig.json`, a project for the config and any
713
- [rules written in TypeScript](#rules-as-typescript-files), with `bundler`
714
- resolution. For a config written by hand, add that file, or include
715
- `.adhere/config.ts` in a tsconfig that resolves the same way.
716
-
717
- </details>
718
-
719
- ### Overrides
720
-
721
- Use `overrides` to change a rule without copying its text into `rules`. Name
722
- the rule by the id the report shows. A preset rule's id starts with the
723
- preset's name, as in `alchemy/providers/idempotent-delete`:
724
-
725
- ```ts
726
- export default defineConfig({
727
- presets: ["effect", "alchemy"],
728
- overrides: {
729
- "alchemy/providers/idempotent-delete": { level: "warning", threshold: 0.9 },
730
- "effect/basics/instrument-with-pipe": "off",
731
- },
732
- });
733
- ```
734
-
735
- | Value | Effect |
736
- | ------------------------------- | -------------------------------------------------------------------------------------- |
737
- | `"off"` | drops the rule |
738
- | `"warning"`, `"error"` | sets its level |
739
- | `{ level, threshold, options }` | sets any of them; `options` only for a rule that takes them ([options](#rule-options)) |
740
-
741
- An id that no preset or project rule has refuses the run, so a typo does not go
742
- unnoticed. A rule of a preset that the run does not name is accepted.
743
-
744
- ### Precedence
745
-
746
- For the model and the threshold, highest first:
747
-
748
- 1. `--threshold` on the command line
749
- 2. the config file
750
- 3. presets, in order (a later preset wins)
751
- 4. the defaults, `jev-latest` and `0.8`
752
-
753
- For one rule:
754
-
755
- - A rule's own `threshold` beats all of the above for that rule.
756
- - An entry in `overrides` beats the rule's own `threshold` and `level`.
757
- - A rule in `rules` replaces a preset rule with the same id.
758
-
759
- `--sufficiency-threshold` beats the config's `sufficiencyThreshold`, which beats
760
- the default, `0.6`.
761
-
762
- ## Running lint
763
-
764
- `adhere lint` judges the TypeScript files under the working directory against
765
- your rules, and prints a [report](#reading-the-report) of what it finds. It
766
- asks before it sends anything.
767
-
768
- ### Before and during a run
769
-
770
- Before it judges anything, `lint` says on stderr what it found and what judging
771
- takes, and asks:
772
-
773
- ```text
774
- 200 files and 14 rules: 2800 checks, 1400 cached.
775
- Judging 100 files takes an estimated 100 requests to Jev, plus 1 or more for each file with a finding.
776
- Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, and more for locating findings.
777
- Estimates cover only code and workspace-package context; symbol/reference input, possible extra request splits, and size-skips are determined at send time.
778
- ? Estimates cover only code and workspace-package context; symbol/reference input, possible extra request splits, and size-skips are determined at send time. Send an estimated 100 requests to Jev, about $0.08? › (Y/n)
779
- ```
780
-
781
- - It asks only when stdin and stdout are terminals, and only when there is
782
- something to send. `--yes` sends without asking.
783
- - The cost is adhere's estimate of the input tokens times the model's price.
784
- Jev charges only for input tokens: $0.042 a million for `jev-latest`, per
785
- [TypeSafe AI's models page](https://docs.typesafe.ai/models) in
786
- September 2026. When type awareness is enabled, the estimate leaves out
787
- `symbols` and `references`, which adhere reads as it sends each file, so a
788
- run can cost more than it says.
789
- - A run that stops loses nothing: files finished before it stopped are cached,
790
- so running again continues from there.
791
-
792
- <details>
793
- <summary>More on unknown prices and blocked files</summary>
794
-
795
- - For a model adhere has no price for, the plan gives the tokens alone.
796
- - The firewall in front of Jev's API can refuse a request whose code reads to
797
- it as an attack, with a 403 and an HTML page rather than Jev's JSON. It
798
- refuses the same request every run, so that file is skipped, the run goes on,
799
- and the report lists each such file with Cloudflare's Ray ID, which
800
- TypeSafe AI can look the block up by.
801
- - When only the question that finds the line is refused, the file's judgments
802
- are kept, and a rerun sends that question alone.
803
-
804
- </details>
805
-
806
- ### What gets read
807
-
808
- - `adhere lint` reads the TypeScript files under the working directory: `.ts`,
809
- `.tsx`, `.mts`, and `.cts`. It does not read declaration files such as
810
- `.d.ts`, adhere's own config files, or JavaScript.
811
- - When the working directory contains `agents/`, `apps/`, or `packages/`, only
812
- those trees are read.
813
- - Below the working directory, anything under `node_modules/`, `dist/`,
814
- `coverage/`, `vendor/`, `e2e/`, `references/`, `.adhere/`, `.agents/`,
815
- `.claude/`, `.direnv/`, `.alchemy/`, or `.vite/` is skipped. The directories
816
- above the working directory do not count.
817
- - `.gitignore` is not consulted.
818
- - A config's `exclude` lists globs, relative to the working directory, of files
819
- no rule judges, such as generated code. It leaves them out of every run, as
820
- `--filter '!<glob>'` leaves them out of one:
821
-
822
- ```ts
823
- export default defineConfig({ exclude: ["**/generated/**"] });
824
- ```
68
+ The script installs to `~/.local/bin`. If your shell can't find `adhere`
69
+ afterwards, add that directory to your `PATH`. `adhere lint` estimates what
70
+ judging will cost. When stdin and stdout are terminals, it asks before it sends
71
+ anything; otherwise, or with `--yes`, it sends without asking. The package is
72
+ `@drkmttr/adhere`: `adhere` on npm is an unrelated package.
825
73
 
826
- **Tests** are judged only by rules about tests, whose front matter says `tests`
827
- (see [Rules as Markdown files](#rules-as-markdown-files)). A test is:
74
+ ## Docs
828
75
 
829
- - a `.test` or `.spec` file, such as `a.test.ts` or `Button.spec.tsx`;
830
- - a fixture or test helper named as one, such as `fixtures.ts`,
831
- `GitHubHttpFixtures.ts`, `ledger-copy.fixture.ts`, `linear-test-helpers.ts`,
832
- or `test-utils.ts`;
833
- - any file under a `test/`, `tests/`, `__tests__/`, `testing/`, `fixtures/`,
834
- `fixture/`, `__fixtures__/`, or `__mocks__/` directory.
76
+ - **Start:** [Introduction](https://dm.sh/adhere/), [Install](https://dm.sh/adhere/install/), [Getting started](https://dm.sh/adhere/getting-started/)
77
+ - **Rules:** <a id="rules-as-markdown-files"></a>[Rule format](https://dm.sh/adhere/rule-format/), <a id="rule-writing-tips"></a>[Writing good rules](https://dm.sh/adhere/writing-good-rules/), [Generate rules from your repo](https://dm.sh/adhere/generate-rules/), [Install rules from other repos](https://dm.sh/adhere/install-rules/), [Presets](https://dm.sh/adhere/presets/)
78
+ - **Usage:** [Reading the report](https://dm.sh/adhere/reading-the-report/), [Config](https://dm.sh/adhere/config/), [Running lint](https://dm.sh/adhere/running-lint/)
79
+ - **Validate & improve rules:** [Validating rules](https://dm.sh/adhere/validating-rules/), [Live evals](https://dm.sh/adhere/live-evals/)
80
+ - **Advanced features:** [State Hooks and Type-Awareness](https://dm.sh/adhere/state-hooks/), [Providers](https://dm.sh/adhere/providers/), [Comments and suppressions](https://dm.sh/adhere/comments-and-suppressions/)
81
+ - [Agent skills](https://dm.sh/adhere/agent-skills/)
82
+ - **Reference:** [Commands](https://dm.sh/adhere/reference/commands/), [Config file](https://dm.sh/adhere/reference/config-file/), [Cache](https://dm.sh/adhere/cache/), [How a file is judged](https://dm.sh/adhere/how-a-file-is-judged/)
83
+ - [Compose your AGENTS.md](https://dm.sh/adhere/compose-agents-md/)
835
84
 
836
- ### Flags
837
-
838
- | Flag | What it does |
839
- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
840
- | `--preset <names>` | Add built-in rule sets, or their topics. The config becomes optional. See [Presets](#presets). |
841
- | `--threshold <0 to 1>` | Replace the config's threshold. Per-rule thresholds still apply. |
842
- | `--sufficiency-threshold <0 to 1>` | Replace the config's `sufficiencyThreshold`: a finding warns when its `context` is below it. |
843
- | `--type-aware` | Add the compiler's `symbols` and `references`. See [What Jev reads](#what-jev-reads). |
844
- | `--yes`, `-y` | Send the requests without asking first. |
845
- | `--limit <files>`, `--rpm <requests>`, `--filter <glob>` | Bound how many files a run judges, how fast it sends, and which files it judges. See [Limiting a run](#limiting-a-run). |
846
- | `--deny-warnings` | Fail on warnings as well as errors. |
847
- | `--log-level <level>` | Log the run's work on stderr. See [Logging a run](#logging-a-run). |
848
- | `--json`, `--md` | Write one JSON or Markdown file per violated rule to `.adhere/reports/`. |
849
- | `--stdout` | Print one combined report instead of files; use with `--json` or `--md`. |
850
-
851
- ### JSON and Markdown reports
852
-
853
- ```sh
854
- adhere lint --json
855
- adhere lint --md
856
- adhere lint --json --md
857
- adhere lint --json --stdout > report.json
858
- adhere lint --md --stdout > report.md
859
- ```
860
-
861
- `--json` and `--md` write one file per violated rule under `.adhere/reports/`
862
- by default. Both errors and warnings are included. Filenames URL-encode the
863
- rule id: `data/ports` becomes `data%2Fports.json` or `data%2Fports.md`.
864
- Use both flags together to write both formats. File output leaves stdout
865
- empty and prints the destination on stderr.
866
-
867
- Add `--stdout` to print all findings together as one JSON object or one
868
- Markdown document, without writing or replacing report files. Choose one
869
- format when using `--stdout`; mixing JSON and Markdown would make the JSON
870
- unparseable. Without a format flag, lint keeps its usual terminal report.
871
- Progress, prompts, and logs stay on stderr.
872
-
873
- JSON reports include `findings`, file counts (`files`, `judged`, `cached`,
874
- `skipped`, `waiting`), `blocked` files, the `suppressed` finding count, and
875
- `threshold` and `sufficiencyThreshold`. A rule file contains only that rule's
876
- findings, while the file counts describe the whole run. Each finding includes
877
- its rule id, severity (`level`), description, file, line ranges, excerpt,
878
- examples, confidence (`probability`), threshold, and context score when
879
- available. Markdown includes locations, scores, code excerpts, examples,
880
- and a summary. Paths are relative to the working directory; preset rule ids
881
- include the preset name.
882
-
883
- The generated `.adhere/reports/` directory is replaced on each completed
884
- file-output run, including filtered or limited runs. A run with no findings
885
- leaves it empty. Keep hand-written files elsewhere, and add `.adhere/reports/`
886
- to `.gitignore` if reports should stay local.
887
-
888
- All output modes preserve the usual exit codes; reports are produced even
889
- when findings fail the run. `--limit 0` only prints the plan on stderr,
890
- produces no report, and leaves existing report files untouched.
891
-
892
- ### Limiting a run
893
-
894
- Three flags bound what a run does:
895
-
896
- - **`--limit <files>`** sends requests for at most that many files, taken in
897
- path order. The rest wait. Since judgments are cached, the next run with the
898
- same limit picks up where this one stopped. `--limit 0` shows the plan and
899
- sends nothing. It counts files, not requests: one file can take several.
900
- - **`--rpm <requests>`** sends at most that many requests to Jev a minute,
901
- evenly spaced, retries included. The plan says about how long they take.
902
- - **`--filter <glob>`** judges only the files whose path from the working
903
- directory matches, such as `src/**` or `**/*.service.ts`. Wrap a glob in
904
- single quotes so the shell does not expand it. Repeat the flag for more
905
- globs, and start one with `!` to leave out what it matches.
906
-
907
- ```sh
908
- adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 100 --rpm 30
909
- ```
910
-
911
- ### Logging a run
912
-
913
- `--log-level` logs the run's work on stderr:
914
-
915
- - **`debug`** logs the config it loaded, how many paths it listed, and how many
916
- of them it reads. It logs each request to Jev, with the file it is for, about
917
- how many tokens it carries, and how long it took. A failed attempt is logged
918
- even when a retry hides it, with the HTTP status, Cloudflare's Ray ID, and
919
- the start of what the API said.
920
- - **`trace`** also logs each file as it is read, planned, and answered from the
921
- cache.
922
-
923
- stdout still carries only the report, so the log can go to a file of its own:
924
-
925
- ```sh
926
- adhere lint --log-level debug 2> adhere.log
927
- ```
928
-
929
- ### Exit codes
930
-
931
- | Code | When |
932
- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
933
- | 0 | Nothing is reported, or only warnings. |
934
- | 1 | An error is reported. With `--deny-warnings`, a warning too. |
935
- | 1 | The run refuses, for example on an invalid config or rule file, a missing API key, or an unknown command or flag. A refusal prints its reason. |
936
-
937
- ## Commands
938
-
939
- | Command | What it does |
940
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
941
- | `adhere lint` | Audit the working directory. See [Running lint](#running-lint). |
942
- | `adhere validate` | Check the rules' wording, list rules a linter could check, and ask Jev whether any contradict. See [Validating rules](#validating-rules). |
943
- | `adhere postinstall` | Render instruction files into a managed block in `AGENTS.md`. See [Compose your AGENTS.md](#compose-your-agentsmd). |
944
- | `adhere rules create [idea]` | Write a rule with a local coding agent's help (`claude`, `codex`, or `omp`): judge excerpts it finds, get a `RULE.md` or `RULE.ts`; org rules are pushed to the shared repo for a PR. `--setup` re-picks defaults. |
945
- | `adhere init [--force]` | Scaffold `.adhere/config.ts` and two example rules. See [Init](#init). |
946
- | `adhere init --shared org/repo` | Scaffold a repo of rules other repos install. See [Creating a shared repo](#creating-a-shared-repo). |
947
- | `adhere list org/repo` | List the rules in another repo's `.adhere/rules/`. See [Install rules from other repos](#install-rules-from-other-repos). |
948
- | `adhere install org/repo` | Copy those rules into this repo's `.adhere/rules/org/repo/`. |
949
- | `adhere login`, `adhere logout` | Save or delete a TypeSafe AI API key, or with `--openai`, an OpenAI one. See [Login](#login). |
950
- | `adhere skill [docs\|setup\|fix]` | Print an agent skill. See [Agent skills](#agent-skills). |
951
-
952
- Bare `adhere` prints the help. `adhere <command> --help` lists a command's
953
- flags, and `adhere --completions <shell>` prints a completion script.
954
-
955
- ## Validating rules
956
-
957
- `adhere validate` checks your rules three ways:
958
-
959
- ```sh
960
- adhere validate # your rules
961
- adhere validate --preset effect # your rules beside a preset's
962
- ```
963
-
964
- - **Wording.** It lists each rule whose wording differs from what the
965
- [rule writing tips](#rule-writing-tips) recommend, with its file and what
966
- differs.
967
- - **The linter check.** While `lint` judges a file against one of your rules,
968
- it also asks Jev whether a linter or type checker could decide the rule
969
- exactly. It stops after 10 files per rule, and `validate` reports a rule once
970
- it has all 10 answers. A rule flagged on 7 or more of them probably belongs in
971
- a regular linter. One flagged on 3 to 6 reads differently from file to file:
972
- make its description say more precisely what it applies to. Preset rules are
973
- left out.
974
- - **Contradictions.** It asks Jev about each pair of rules that apply to the
975
- same files. When there is such a pair, it needs the API key, as `lint` does.
976
-
977
- Wording and the linter check are advice and never fail `validate`. A
978
- contradiction fails it with exit code 1, so `validate` can gate a pull request
979
- that adds a rule. The workflow that
980
- [`adhere init --shared`](#creating-a-shared-repo) writes does this.
981
-
982
- Two rules contradict when no code can follow both, such as one that says errors
983
- must be thrown and one that says they must be returned. Each rule reads fine on
984
- its own, so a contradiction shows up only as code that breaks one rule or the
985
- other, whatever you do. `validate` finds it before `lint` does:
986
-
987
- ```text
988
- Found 1 contradiction among configured rules:
989
-
990
- 1. No code can follow both (0.91):
991
- - errors/throw-tagged-errors (/repo/.adhere/rules/errors/throw-tagged-errors/RULE.md)
992
- A function that fails must throw a tagged error, never return an error value.
993
- - errors/return-results (/repo/packages/api/.adhere/rules/errors/return-results/RULE.md)
994
- A function that fails must return a Result, never throw.
995
-
996
- Resolve by editing one rule, narrowing a nested rule's scope, or using the same rule id when the nested rule is meant to shadow the root rule.
997
- ```
998
-
999
- Two rules with the same id are never compared: a nested rule that shares an id
1000
- shadows the other on purpose.
1001
-
1002
- ## Live evals
1003
-
1004
- Try a change to a rule on your own code before you rely on it: run the current
1005
- rule and the changed one side by side, and compare what each flags.
1006
-
1007
- 1. Copy the rule's directory under a new id:
1008
-
1009
- ```sh
1010
- cp -r .adhere/rules/data/brand-ports .adhere/rules/data/brand-ports-next
1011
- ```
1012
-
1013
- 2. Give the copy `level: warning` in its front matter. Its findings show in
1014
- amber and never fail the run, so the experiment can sit in the repo, and in
1015
- CI, while you watch it.
1016
- 3. Change the copy: its description, its examples, its `appliesTo` or
1017
- `excludeIf`. To try giving Jev more to read, make the copy a `RULE.ts` with
1018
- an [`appendState`](#state-hooks) hook.
1019
- 4. Run `adhere lint`, or `adhere lint --filter '<glob>'` to try it on part of
1020
- the repo first, and compare the two rules' findings on the same files.
1021
-
1022
- Judge the two by recall, the share of real violations each catches, and
1023
- precision, the share of its flags that are real. Check the findings by hand, or
1024
- with the [`adhere-fix`](./skills/adhere-fix/SKILL.md) skill, which verifies each
1025
- one against its rule and the code. Then keep the better version under the
1026
- original id, and delete the other.
1027
-
1028
- - **The copy costs nothing until you change it.** Judgments are cached by the
1029
- rule's text, not by its id, its level, or its threshold, so a copy that reads
1030
- the same as the original answers from the original's cache. Each change after
1031
- that judges the copy, and only the copy, once per file.
1032
- - **Keeping the winner is free.** Moving the winning text to the original id
1033
- reuses its cached judgments.
1034
- - **Counting each rule's findings** takes a pipe:
1035
- `adhere lint --yes | grep -c 'data/brand-ports-next:'`.
1036
-
1037
- ## State Hooks and Type-Awareness
1038
-
1039
- Jev judges a file from its state: the file's `code`, and maps of context beside
1040
- it. The config chooses the maps every rule reads (see
1041
- [What Jev reads](#what-jev-reads)). A rule written in TypeScript can add state
1042
- of its own with a hook (see [State hooks](#state-hooks)), and take settings
1043
- from each project that uses it (see [Rule options](#rule-options)).
1044
-
1045
- ### What Jev reads
1046
-
1047
- The state sent for one file, `packages/orders/src/total.ts`, with every map
1048
- on and each map's `__comment` cut short:
1049
-
1050
- ````json
1051
- {
1052
- "code": {
1053
- "1": "import { formatCents } from \"money\";\nimport { sum } from \"./sum\";\n\nexport const total = (prices: number[]): string => formatCents(sum(prices));\n"
1054
- },
1055
- "workspacePackages": {
1056
- "__comment": "Workspace packages imported by `code`; …",
1057
- "money": "packages/money"
1058
- },
1059
- "dependents": {
1060
- "__comment": "What depends on the file `code` holds. …",
1061
- "packages": { "web": "packages/web" },
1062
- "files": ["packages/orders/src/index.ts"]
1063
- },
1064
- "symbols": {
1065
- "__comment": "Compiler-rendered types and documentation for symbols used in `code`; …",
1066
- "formatCents": "```typescript\n(cents: number) => string\n```\n\nFormats cents as dollars, as in `$12.50`.",
1067
- "prices": "```typescript\nnumber[]\n```",
1068
- "sum": "```typescript\n(values: number[]) => number\n```",
1069
- "total": "```typescript\n(prices: number[]) => string\n```"
1070
- },
1071
- "references": {
1072
- "__comment": "Declarations of symbols used in `code`. …",
1073
- "formatCents:packages/money/src/index.ts": "export const formatCents = (cents: number): string => `$${(cents / 100).toFixed(2)}`;",
1074
- "sum:packages/orders/src/sum.ts": "export const sum = (values: number[]): number => values.reduce((a, b) => a + b, 0);"
1075
- },
1076
- "__comment": "Only `code` is the audit target; it is split into numbered sections. All other state entries provide context, not additional code to audit."
1077
- }
1078
- ````
1079
-
1080
- - **`code`** is always sent: the file's [sections](#how-a-file-is-judged),
1081
- numbered from 1, without [comments](#comments) unless `includeComments` is
1082
- on.
1083
- - **`workspacePackages`**, on by default: the repo's own packages the file
1084
- imports, each with its directory. A package the file imports that is not
1085
- listed is an installed one.
1086
- - **`dependents`**, off by default: the workspace packages whose
1087
- `package.json` depends on the file's package, and up to 50 files that import
1088
- the file by a relative path, with `omittedFiles` counting the rest. Imports
1089
- by package name or `tsconfig` path alias do not count, nor do files that
1090
- `--filter` or `exclude` leaves out. To find them, lint reads every file once
1091
- more before it plans.
1092
- - **`symbols`**, with type awareness: the compiler's type of each name the
1093
- code uses, with its documentation and JSDoc tags, leaving out
1094
- standard-library and runtime built-ins. `maxSymbolLength` and
1095
- `maxSymbolDocumentationLength` cap each one (see [Config](#config)).
1096
- - **`references`**, with type awareness: the whole declaration of each symbol
1097
- the code uses from another file of the repo, keyed by the name the code uses
1098
- and the declaring file's path. A function comes with its body, a method
1099
- without the rest of its class.
1100
- - **`__comment`**, in each map and a rule's [`options`](#rule-options), tells
1101
- Jev what the map holds. The top-level one says only `code` is judged. adhere
1102
- adds them before a [state hook](#state-hooks) runs, and again to any map the
1103
- hook returns without one.
1104
-
1105
- `reads` in the config chooses the workspace maps, and `typeAware` adds the
1106
- compiler's:
1107
-
1108
- ```ts
1109
- // .adhere/config.ts
1110
- import { defineConfig } from "@drkmttr/adhere";
1111
-
1112
- export default defineConfig({
1113
- reads: ["workspacePackages", "dependents"], // default: ["workspacePackages"]
1114
- typeAware: true, // or --type-aware; off by default
1115
- });
1116
- ```
1117
-
1118
- - `reads: []`, with type awareness off, sends the code alone. Neither
1119
- workspace map needs the compiler.
1120
- - `typeAware` adds `symbols` and `references` whatever `reads` says. The
1121
- compiler uses each file's `tsconfig.json`, and works without one.
1122
- - An unknown name in `reads` refuses the run. Only the config has `reads`.
1123
- - A rule's description can name a map in backticks, as Jev's questions name
1124
- `code`. To give Jev something no map covers, such as another file, add it
1125
- with a [state hook](#state-hooks).
1126
- - `adhere lint --log-level debug` logs a run's first request in full.
1127
-
1128
- **Cost and privacy.** Type awareness sends Jev more than the file it judges:
1129
- whole declarations from other files, and the compiler's documentation and JSDoc
1130
- tags in `symbols`, even with `includeComments` off. It also starts the native
1131
- compiler during the run. The plan's estimate leaves those tokens out.
1132
-
1133
- <details>
1134
- <summary>Where the packages come from</summary>
1135
-
1136
- - The packages are read on every run from the working directory's
1137
- `package.json`, whose `workspaces` npm, Bun, and Yarn read, and from
1138
- `pnpm-workspace.yaml`'s `packages`.
1139
- - In a pattern, `*` is any directory and `**` any depth of them, and one that
1140
- starts with `!` leaves out what it matches. Each matched directory's
1141
- `package.json` gives the name, and its directory is given from the working
1142
- directory.
1143
- - Nothing need be installed, so a run in CI reads the same packages as one on a
1144
- laptop.
1145
- - A working directory that is not a workspace's root has none.
1146
- - Only an import whose path is a plain string counts, and one of a subpath, such
1147
- as `orders-core/client`, counts as its package's.
1148
-
1149
- </details>
1150
-
1151
- ### State hooks
1152
-
1153
- A rule can add to what Jev reads for it with `appendState`, a hook in its
1154
- [`RULE.ts`](#rules-as-typescript-files) or in a config's inline rule:
1155
-
1156
- ```ts
1157
- // .adhere/rules/data/columns/RULE.ts
1158
- import { defineRule } from "@drkmttr/adhere";
1159
-
1160
- export default defineRule({
1161
- description:
1162
- "A query must name only columns its table has in `schema`, and never a column it lacks.",
1163
- must: 'db.select("id", "email").from("users")',
1164
- never: 'db.select("mail").from("users")',
1165
- appendState: async (state, file, Bun) => ({
1166
- schema: await Bun.file("db/schema.sql").text(),
1167
- }),
1168
- });
1169
- ```
1170
-
1171
- - The hook runs right before each request about the rule goes out. It gets the
1172
- request's state, the file the request is about, and Bun's API. What it
1173
- returns is spread over the state.
1174
- - The description can name a key the hook adds, in backticks, as the questions
1175
- name `code`.
1176
- - A rule with a hook goes to Jev in requests of its own, so only that rule reads
1177
- what its hook adds. Each of those requests sends the file and its context
1178
- again.
1179
- - What the hook reads outside the file is not part of the cache. When
1180
- `db/schema.sql` changes, a file already judged is not judged again until it
1181
- or the rule changes.
1182
-
1183
- <details>
1184
- <summary>More on the hook</summary>
1185
-
1186
- - `state` is what Jev reads beside each question: `code`, the file's sections
1187
- as Jev reads them, under their numbers from 1, as in
1188
- `{ code: { "1": "import …", "2": "export const …" } }`, beside the maps
1189
- [What Jev reads](#what-jev-reads) lists.
1190
- - `file` is the file's absolute `path`, and its `contents` as written, comments
1191
- and all.
1192
- - Nothing the hook returns is checked, so it can change `code` too, or replace
1193
- it. A hook can break its own rule's answers, and adhere does not stop it.
1194
- - It can be async. A hook that throws refuses the run, naming the rule.
1195
- - The plan counts the rule's requests, but not the tokens a hook adds, which are
1196
- not known until it runs. A request over Jev's context skips the file, as a
1197
- file too long does.
1198
- - `Bun` is typed when `@types/bun` is installed and `.adhere/tsconfig.json`
1199
- lists it, as `"types": ["bun"]`. Otherwise it is `unknown`.
1200
-
1201
- </details>
1202
-
1203
- ### Rule options
1204
-
1205
- A `RULE.ts` can take settings from each project that uses it, such as which
1206
- paths run on the edge. It declares them as `options`, with an Effect Schema, and
1207
- a config sets them under the rule's id in [`overrides`](#overrides):
1208
-
1209
- ```ts
1210
- // .adhere/rules/edge/no-node/RULE.ts
1211
- import { defineRule, Schema } from "@drkmttr/adhere";
1212
-
1213
- export default defineRule({
1214
- description: "A file under a path in `options.edgePaths` must never import a `node:` module.",
1215
- never: 'import { readFileSync } from "node:fs";',
1216
- options: Schema.Struct({ edgePaths: Schema.Array(Schema.String) }),
1217
- });
1218
- ```
1219
-
1220
- ```ts
1221
- // .adhere/config.ts
1222
- export default defineConfig({
1223
- overrides: { "edge/no-node": { options: { edgePaths: ["src/edge", "workers"] } } },
1224
- });
1225
- ```
1226
-
1227
- - Jev reads the decoded settings as `options`, so the description can name them
1228
- in backticks. A rule's `appendState` gets them too, in its `state`.
1229
- - A key the schema does not make optional must be set. An unset or invalid
1230
- option refuses the run, naming the rule. Options set for a rule that takes
1231
- none also refuse it.
1232
- - A rule with options goes to Jev in requests of its own, as one with a hook
1233
- does. Each of those requests sends the file and its context again.
1234
- - Changing a rule's options judges that rule again, and no other.
1235
- - The plan counts the tokens options add. A file whose options leave no room
1236
- for a question is skipped before any request.
1237
- - `Schema` is Effect's, re-exported by adhere. A `RULE.ts` needs no `effect` of
1238
- its own, and decodes with the same copy adhere does.
1239
- - A `RULE.md` cannot take options.
1240
-
1241
- ## Providers
1242
-
1243
- adhere asks TypeSafe AI's Jev by default. A provider asks another decision model
1244
- instead, and adhere exports one for each model
1245
- [the providers study](eval/studies/providers.md) measured:
1246
-
1247
- ```ts
1248
- import { defineConfig, openai } from "@drkmttr/adhere";
1249
-
1250
- // Asks with OPENAI_API_KEY, or the key `adhere login --openai` saved.
1251
- export default defineConfig({ provider: openai() });
1252
- ```
1253
-
1254
- | Provider | Asks | Options |
1255
- | ------------ | --------------------------------------------------------------- | -------------------------------- |
1256
- | `jev` | TypeSafe AI's Jev, `jev-latest` by default | `apiKey`, `model` |
1257
- | `openai` | OpenAI's `gpt-6-luna`, on its Decisions API | `apiKey`, `model` |
1258
- | `cloudflare` | Cloudflare's `clef`, by default, or `clef-flash`, on Workers AI | `accountId`, `apiToken`, `model` |
1259
-
1260
- - Without an `apiKey`, `jev` and `openai` ask with the key from
1261
- [Login](#login).
1262
- - Each provider also takes `fetch`, the function that sends its requests.
1263
- - Clef cuts a request too long for it short rather than refuse it, so
1264
- `cloudflare` skips a file whose request reaches clef's limit: 64,000 tokens,
1265
- or 24,000 for clef-flash.
1266
-
1267
- <details>
1268
- <summary>Writing a provider</summary>
1269
-
1270
- A provider is a model's name, and an `ask` that answers adhere's questions
1271
- about a state:
1272
-
1273
- ```ts
1274
- import { defineConfig, type Provider, RequestFailed } from "@drkmttr/adhere";
1275
-
1276
- const mine: Provider = {
1277
- model: "my-decision-model",
1278
- ask: async (state, questions, { signal }) => {
1279
- const response = await fetch("https://decisions.example.com/v1/decide", {
1280
- method: "POST",
1281
- headers: { "Content-Type": "application/json" },
1282
- body: JSON.stringify({ state, questions }),
1283
- signal,
1284
- });
1285
- if (!response.ok) {
1286
- throw new RequestFailed(`My model answered HTTP ${response.status}`, response.status);
1287
- }
1288
- return { answers: await response.json() };
1289
- },
1290
- };
1291
-
1292
- export default defineConfig({ provider: mine });
1293
- ```
1294
-
1295
- - `state` is the file's code by section under `code`, beside what adhere read
1296
- for it.
1297
- - Each question has an `id`, the `question` as adhere phrases it, and the
1298
- `fields` it names in backticks, such as `rule`, `must`, and `never`. A
1299
- `noul` asks whether something holds. A `choice` asks for the key of one of
1300
- its `options`. Its `options` can hold just one entry.
1301
- - `ask` returns `{ answers }`: by id, a `noul`'s probability from 0 to 1, or a
1302
- `choice`'s key. An id left out goes unanswered: its rule is not reported, and
1303
- is asked again on the next run.
1304
- - A rejection stops the run, but for three errors adhere exports:
1305
- `ContextOverflow` gets one retry without `references` when that map is
1306
- present; if the request still overflows, the file is skipped.
1307
- `RequestBlocked` reports it blocked, and `RequestFailed` is tried again, up
1308
- to three times, when its `status` is 0, for no answer, or a 408, 429, or 5xx.
1309
- - adhere paces calls with `--rpm`, aborts `signal` after 30 seconds, and splits
1310
- requests to fit Jev's 64,000 tokens. A model that takes less splits further
1311
- in its `ask`.
1312
- - `model` replaces the config's `model`. The cache keeps answers by it, so a
1313
- new model is asked again.
1314
- - A provider that sets `savedKey` to `"typesafe"` or `"openai"` gets that key
1315
- as `apiKey`.
1316
- - The executable resolves no packages besides `@drkmttr/adhere`, so an `ask`
1317
- uses `fetch` rather than an SDK.
1318
-
1319
- </details>
1320
-
1321
- ## Comments and suppressions
1322
-
1323
- ### Comments
1324
-
1325
- Jev reads a file's code, not its comments. adhere takes every comment out before
1326
- it hashes or sends a file, or a declaration from
1327
- [`references`](#what-jev-reads), blanking each one so every line keeps its
1328
- number. The report's excerpts still show comments. Compiler-provided
1329
- documentation and JSDoc tags still reach Jev in the type and documentation
1330
- strings that [`symbols`](#what-jev-reads) sends when type awareness is on.
1331
- These strings are not editor hovers.
1332
-
1333
- Jev takes what comments claim at their word, which hid real violations in the
1334
- studies: see [Writing good rules](#writing-good-rules).
1335
-
1336
- A comment that says `@adhere` anywhere in it is a note for Jev, and stays. Write
1337
- one for a fact the code relies on that the file cannot show, once you have
1338
- checked it, with how you know:
1339
-
1340
- ```ts
1341
- /**
1342
- * Deletes the activity. @adhere DeleteActivity succeeds on a missing
1343
- * activity, so no not-found error needs catching; probed 2026-09-25.
1344
- */
1345
- ```
1346
-
1347
- - A note informs Jev. It suppresses nothing, and Jev still judges the code
1348
- around it.
1349
- - `includeComments: true` in the config sends every comment, for a project whose
1350
- rules are about comments, such as doc comments on exports.
1351
-
1352
- ### Suppressing a finding
1353
-
1354
- Suppress a finding Jev got wrong with a comment in the code that says why:
1355
-
1356
- ```ts
1357
- // adhere-ignore alchemy/providers/idempotent-delete -- DeleteActivity succeeds on a missing activity; probed 2026-09-25
1358
- delete: Effect.fn(function* ({ output }) {
1359
- yield* sfn.deleteActivity({ activityArn: output.activityArn });
1360
- }),
1361
- ```
1362
-
1363
- | Comment | What it covers |
1364
- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1365
- | `adhere-ignore`, on a line of its own | The statement that starts on the next line of code: above, the whole handler, wherever in it Jev points, since the lines it points at can move between runs. |
1366
- | `adhere-ignore`, at the end of a line of code | The statement that starts on that line. |
1367
- | `adhere-ignore-file`, anywhere in a file | The whole file. The rules it names are not judged there at all. |
1368
-
1369
- - A comment names rules as a report does, separated by commas.
1370
- - What follows `--` is the reason, for whoever reads the code next.
1371
- - A finding without lines is covered when its section overlaps the statement.
1372
- - Jev never reads these comments, even with `includeComments: true`.
1373
- - The summary counts the findings they suppress.
1374
-
1375
- ## Agent skills
1376
-
1377
- Three skills teach an agent to work with adhere:
1378
-
1379
- - **[`adhere`](./skills/adhere/SKILL.md)**, printed by `adhere skill`, is the
1380
- reference: the commands, how to write and word a rule, the config, presets,
1381
- shared rules, comments and suppressions, the cache, reading a report, and
1382
- tuning a rule.
1383
- - **[`adhere-setup`](./skills/adhere-setup/SKILL.md)**, printed by
1384
- `adhere skill setup`, sets adhere up in a repo with you. See
1385
- [Generate rules from your repo](#generate-rules-from-your-repo).
1386
- - **[`adhere-fix`](./skills/adhere-fix/SKILL.md)**, printed by
1387
- `adhere skill fix`, verifies and fixes the findings `lint` reports: it
1388
- checks each against its rule and the code, fixes the real ones, reports the
1389
- false ones with the evidence, and says when a rule is wrong more often than
1390
- right.
1391
-
1392
- Install them with the [skills](https://github.com/vercel-labs/skills) CLI:
1393
- `skills add darkmatter/adhere`. The binary carries all three, so
1394
- `adhere skill fix > .agents/skills/adhere-fix/SKILL.md` works without a
1395
- checkout.
1396
-
1397
- ### In CI
1398
-
1399
- Pipe the fix skill into an agent that runs without asking. The skill tells it
1400
- that invoking the skill permits every `adhere lint` run and its cost, and
1401
- `adhere skill fix` prints the skill of the adhere that runs, so its flags
1402
- match:
1403
-
1404
- ```sh
1405
- adhere skill fix | codex exec --yolo
1406
- adhere skill fix | claude -p --dangerously-skip-permissions
1407
- ```
1408
-
1409
- Both turn off the agent's own permission checks, which suits a runner thrown
1410
- away after the job. To keep Codex in its sandbox, let the sandbox reach
1411
- `api.typesafe.ai`:
1412
-
1413
- ```sh
1414
- adhere skill fix | codex exec --sandbox workspace-write \
1415
- -c sandbox_workspace_write.network_access=true -c approval_policy=never
1416
- ```
1417
-
1418
- - Set `TYPESAFE_API_KEY` from a secret.
1419
- - The agent adds no `adhere-ignore` comments or `@adhere` notes unless
1420
- `ADHERE_ALLOW_SUPPRESSIONS=1` is set in its environment, as in
1421
- `adhere skill fix | ADHERE_ALLOW_SUPPRESSIONS=1 codex exec --yolo`. It then
1422
- suppresses a finding it shows is false, or that three fixes did not clear,
1423
- with the evidence in the comment. Without it, the agent leaves those
1424
- findings, and its report ends by naming them and the variable.
1425
- - End the job with `adhere lint --yes`. The agent exits 0 whatever it left, so
1426
- this step passes or fails the job. It judges only the files the agent
1427
- changed, and reads the rest from the cache.
1428
-
1429
- > **Do not run it on pull requests from forks.** The agent reads their code
1430
- > with your secrets in reach and the network open, so code written to steer it
1431
- > can send them out.
1432
-
1433
- ## Cache
1434
-
1435
- Judgments are cached in `.adhere/cache/`. **Commit it.** Every judgment is a
1436
- paid request, and Jev's answers vary a little from run to run, so a committed
1437
- cache gives everyone and CI the same findings without paying for them again,
1438
- and a pull request changes the judgments of only the files it changes.
1439
-
1440
- | What changed | What is judged again |
1441
- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
1442
- | A file's code | Every rule, for that file. |
1443
- | A file's comments alone | Nothing. |
1444
- | A file is moved or copied | Nothing. Identical files share their judgments. |
1445
- | A rule's text, a matcher, the source of its `appendState`, or its options | That rule, for every file. |
1446
- | The config's `reads`, `typeAware`, `maxSymbolLength`, or `maxSymbolDocumentationLength`, and `includeComments` with `typeAware` | Every rule, once. |
1447
- | What the reads find: a file it imports, the workspace's packages, the file's `dependents` | Nothing, so a judgment can go stale. Delete the cache to judge every file again. |
1448
- | The threshold is lowered | Nothing. Cached judgments newly above it are located. |
1449
- | adhere asks its questions differently, after an upgrade | Every rule, once. |
1450
-
1451
- To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1452
-
1453
- ```text
1454
- .adhere/cache/** linguist-generated -diff
1455
- ```
1456
-
1457
- > **Where lint gates a merge.** Anyone who can commit can also write a cache
1458
- > file saying that code passes. Lint a pull request against the cache as merged,
1459
- > not as the pull request has it. Only the files it changes are judged again:
1460
- >
1461
- > ```sh
1462
- > rm -rf .adhere/cache && git checkout origin/main -- .adhere/cache
1463
- > adhere lint --yes
1464
- > ```
1465
-
1466
- <details>
1467
- <summary>How the cache is stored and pruned</summary>
1468
-
1469
- - `files/` holds Jev's answers by the content they are about. Each cache file
1470
- is named for the hash of a source file's content as Jev reads it, without
1471
- its comments.
1472
- - Each answer sits under a fingerprint of the model and the rule: its text,
1473
- its matchers, and its `appendState`'s source.
1474
- - `tallies/` keeps the linter check's answers, one tally per rule, under a key
1475
- that changes with the rule's text and the model.
1476
- - A cache file is written once and never changed: it is also named for the hash
1477
- of its own text. Two branches that judge the same code add files rather than
1478
- edit them, so git merges the cache without a conflict.
1479
- - After a run that reads every file, adhere prunes the cache: it deletes answers
1480
- about content no file has, and folds what is left into one file per content. A
1481
- run narrowed by `--filter` does not prune.
1482
- - Pruning keeps answers to rules the run left out, so a run with other presets
1483
- or rules, or with one topic, loses nothing another run still asks. It keeps
1484
- answers to a rule's old texts too, until the content they are about is gone.
1485
- - It also keeps answers about a file the run read but judged against no rule,
1486
- such as a test when no rule judges tests. A file the config excludes is not
1487
- read, so answers about it go.
1488
-
1489
- </details>
1490
-
1491
- ## How a file is judged
1492
-
1493
- 1. **Split.** adhere splits the file into sections of whole statements, in runs
1494
- of at most 30 lines.
1495
- 2. **Judge.** One request per file asks Jev, for each rule, how likely the file
1496
- is to break it.
1497
- 3. **Locate.** A second request, only for rules above their threshold, asks
1498
- which section and lines break the rule, and whether Jev sees enough to
1499
- decide.
1500
-
1501
- A file too long for Jev's context is skipped and counted in the summary.
1502
-
1503
- <details>
1504
- <summary>The steps in detail</summary>
1505
-
1506
- **Split.** adhere splits each file into sections of whole statements, such as
1507
- its imports, constants, and functions, packed in order into runs of at most 30
1508
- lines. A class or function longer than that splits into its methods or the
1509
- statements of its body. Jev reads the file as these sections.
1510
-
1511
- **Judge: one request per file.**
1512
-
1513
- - The state is `code`, the file's sections under their numbers from 1, as
1514
- `{ code: { "1": "…", "2": "…" } }`, without their comments (see
1515
- [Comments](#comments)), beside what the config [`reads`](#what-jev-reads).
1516
- - Each rule is one `noul` (yes/no probability) question that carries the rule's
1517
- description and details, and its code under its words, `must` and `never`, or
1518
- a guideline's `should` and `should_not`. It asks whether the file diverges
1519
- from the pattern `must` shows, with `never` as an example of diverging. For
1520
- a rule with only code that must never be written, it asks whether the file
1521
- contains that code.
1522
- - Criteria for each answer draw the line at the rule's scope, so a file with no
1523
- code the rule is about is a no.
1524
- - No rule sits in the state, so a rule's probability depends only on the state
1525
- and that rule, not on which other rules share the request. Every question
1526
- shares the state cost of the request.
1527
- - On up to 10 files per project rule, the rule's question has a second one
1528
- beside it, the linter check (see [Validating rules](#validating-rules)),
1529
- with the same fields.
1530
- - A rule with `appendState` has requests of its own, here and below, whose
1531
- state adds to this one.
1532
-
1533
- **Locate: a second request, only when at least one rule's probability is above
1534
- its threshold.** Per flagged rule:
1535
-
1536
- - When the file has more than one section, adhere asks one `choice` question
1537
- among the sections' numbers, which yields the section to report. The options
1538
- carry no text of their own, since each is a key of the state. On the eval,
1539
- options that described each section, by its first line or by what it
1540
- declares, located violations no better than options without text.
1541
- - For each section, adhere asks two `choice` questions among its lines, each
1542
- option the line's text: the line the violation starts on and the one it ends
1543
- on. The report underlines the lines chosen in the reported section. On the
1544
- eval, lines asked for in this request were named as accurately as lines asked
1545
- for in a request of their own, and they held a violation for 94% of real
1546
- findings, most often as one line ([study](eval/studies/pinpoint.md)).
1547
- - adhere asks one `noul` carrying the rule: whether you can tell if `code`
1548
- breaks it from the state alone, without knowing what other files, libraries,
1549
- services, or configuration do beyond what it shows. It yields the `context`
1550
- score and its warning.
1551
-
1552
- **Limits.** Jev reads at most 32k tokens of state and one question together, and
1553
- 64k tokens in a request. adhere estimates tokens from the JSON it sends, at
1554
- about three bytes a token, and leaves 1k of room for what Jev wraps around it.
1555
-
1556
- - When a file's questions would not fit in one request, they are split across
1557
- several.
1558
- - If a file's state and longest question would not fit together, or a provider
1559
- reports a context overflow, adhere retries once without `references` when that
1560
- map is present. Target code, `symbols`, and other context remain intact; no
1561
- declaration excerpt is shortened to fit. Removing references may still leave
1562
- the request too large, in which case the file is skipped and counted in the
1563
- summary. A file with more than 255 code sections is also skipped, and the rest
1564
- of the run goes on.
1565
- - A file is also skipped when Jev itself counts it as over its context. Text
1566
- denser than the estimate, such as CJK, can cause that.
1567
-
1568
- </details>
1569
-
1570
- ## Compose your AGENTS.md
1571
-
1572
- `adhere postinstall` renders the Markdown files in an instructions directory,
1573
- in name order, into a managed block in `AGENTS.md`. Set the directory with the
1574
- config's `instructions` key, relative to the config file. Without it, adhere
1575
- looks in `.adhere/instructions/`, then `.agents/instructions/`.
1576
-
1577
- To run it after installing dependencies, add it to your package scripts, or
1578
- append it to your existing `postinstall` script:
1579
-
1580
- ```json
1581
- {
1582
- "scripts": {
1583
- "postinstall": "adhere postinstall"
1584
- }
1585
- }
1586
- ```
1587
-
1588
- The command creates `AGENTS.md` if needed, and appends the block if it is
1589
- absent. Otherwise it replaces only the content between these markers:
1590
-
1591
- ```markdown
1592
- <!-- adhere:instructions:start -->
1593
-
1594
- Generated instructions
1595
-
1596
- <!-- adhere:instructions:end -->
1597
- ```
1598
-
1599
- - Everything outside the block is kept.
1600
- - A missing instructions directory changes nothing. An empty one clears the
1601
- generated content, but keeps the markers.
1602
- - Malformed or duplicate markers in `AGENTS.md`, or a marker inside an
1603
- instruction file, fail the command before it writes anything.
1604
-
1605
- ## Contributing
1606
-
1607
- To build, test, or release adhere from a checkout, see
1608
- [CONTRIBUTING.md](.github/CONTRIBUTING.md).
1609
-
1610
- ## License
85
+ ## Contributing and license
1611
86
 
1612
87
  MIT. See [LICENSE](LICENSE). The executables attached to each GitHub release
1613
88
  come with `LICENSE` and `THIRD_PARTY_NOTICES.txt`, which go with any copy you
1614
- redistribute.
89
+ redistribute. To build, test, or release adhere from a checkout, see
90
+ [CONTRIBUTING.md](.github/CONTRIBUTING.md).