@drkmttr/adhere 0.18.1 → 0.21.2

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
@@ -1,142 +1,137 @@
1
1
  <div align="center">
2
2
  <h1>adhere</h1>
3
- <sup>A linter for non-deterministic rules. Powered by Typesafe</sup>
3
+ <br/>
4
+ <sup>A linter for non-deterministic rules. Powered by TypeSafe AI.</sup><br><br/>
5
+ <a href="https://dm.sh/adhere/"><img src="https://img.shields.io/website?url=https%3A%2F%2Fdm.sh%2Fadhere%2Fgetting-started%2F&up_message=healthy&logo=readme&logoColor=%23dddddd&label=docs&labelColor=%23000000" /></a>
6
+ &nbsp;
7
+ <img src="https://img.shields.io/npm/dm/%40drkmttr%2Fadhere?style=flat&logo=github&labelColor=%23181926&color=%23000000" />
8
+ &nbsp;
9
+ <a href="https://www.npmjs.com/package/@drkmttr/adhere"><img src="https://img.shields.io/npm/v/%40drkmttr%2Fadhere" /></a>
4
10
  </div>
5
11
 
6
- A linter for rules a normal linter cannot check, such as "Business logic should live in services", or "modules should expose lots of functionality behind a small API". Define rules as formatted markdown:
12
+ <br />
13
+
14
+ A linter for rules a normal linter cannot check, such as "business logic should
15
+ live in services" or "modules should expose lots of functionality behind a
16
+ small API". You can write a rule as a Markdown file, with code that shows it:
7
17
 
8
18
  ````md
9
19
  ---
10
- description: A third party API going offline should not also take our app down.
20
+ description: A third-party API going offline should not also take our app down.
11
21
  ---
12
22
 
13
- Our APIs should never be coupled to some third party API on a critical path. The user facing
14
- side should be unaffected if it goes offline, and ingestion should happen in a separate asynchronous process.
23
+ Our APIs should not depend on a third-party API on a critical path. The
24
+ user-facing side should be unaffected if it goes offline, and ingestion should
25
+ happen in a separate, asynchronous process.
15
26
 
16
27
  ## Should
17
28
 
18
29
  ```ts
19
- import googleMaps from "./lib/maps"
20
- import db from "./lib/db"
21
- import redis from "redis"
22
- import cron from "node-cron"
23
-
24
- // asume this is called by /api/restaurants/:zipcode
25
- export async function findRestaurants(zipCode: number) {
26
- const results = db.restaurants.find({ where: { zipCode }})
27
- return results
28
- }
30
+ import db from "./lib/db";
31
+ import googleMaps from "./lib/maps";
29
32
 
30
- export async startWorker() {
31
- cron("0 0 * * *", async () => {
32
- const since = Number(await redis.get("maps:sync:timestamp"))
33
- const items = await googlemaps.findAll({ since, limit: 100 })
34
- const syncItem = item => db.restaurants.upsert(item)
35
- const next = items.sort((a, b) => b.id < a.id).at(-1)?.timestamp
36
- const touch = () => redis.set("maps:sync:timestamp", next ?? Date.now())
33
+ // GET /api/restaurants/:zipCode reads only our own database.
34
+ export async function findRestaurants(zipCode: string) {
35
+ return db.restaurants.find({ where: { zipCode } });
36
+ }
37
37
 
38
- return Promise.all(items.map(item => syncItem(item))).then(touch)
39
- })
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)));
40
42
  }
41
43
  ```
42
44
 
43
- ## Never
45
+ ## Should not
44
46
 
45
47
  ```ts
46
48
  import googleMaps from "./lib/maps";
47
- import db from "./lib/db";
48
49
 
49
- // asume this is called by /api/restaurants/:zipcode
50
- export async function findRestaurants(zipCode: number) {
51
- // what happens when google maps goes down? how about latency? how about costs?
52
- const results = await googleMaps.findAll({ where: { zipCode }, limit: 100 });
53
- return results;
50
+ // GET /api/restaurants/:zipCode
51
+ export async function findRestaurants(zipCode: string) {
52
+ // When Google Maps is down, slow, or over quota, so is this endpoint.
53
+ return googleMaps.findAll({ where: { zipCode } });
54
54
  }
55
55
  ```
56
56
  ````
57
57
 
58
- Your rules are parsed, then adhere asks [Jev](https://typesafe.ai) (TypeSafe AI's
59
- System One model) whether the file breaks each rule, gets a calibrated
60
- probability per rule, and reports the ones above a threshold with the section of
61
- the file Jev points at. The report uses the same frame as `vp lint`.
62
-
63
- Here's a demo of what the output looks like.
58
+ 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
60
+ 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:
64
63
 
65
- <details>
66
- <summary>
67
- <h3>Demo 🎬</h3>
68
- </summary>
69
- <img alt="demo" align="center" src="https://github.com/darkmatter/adhere/raw/main/eval/demo.gif" />
70
- </details>
71
-
72
- Presets for TypeScript, React, security, Effect, and alchemy are included.
73
- Run them to quickly adhere your agent to recommended rules.
74
-
75
- ```sh
76
- bun add -g @drkmttr/adhere
77
- adhere login # save your typesafe api key
78
- adhere lint --preset typescript,react
79
- ```
64
+ ![adhere lint demo](https://github.com/darkmatter/adhere/raw/main/eval/demo.gif)
80
65
 
81
66
  Rules a normal linter can check exactly, such as a banned import or a type
82
- error, belong in that linter. While it lints, adhere asks Jev whether a normal
83
- linter could check each of your rules. [`adhere validate`](#check-for-contradictions) lists the
84
- ones it thinks belong in a regular linter, and detects contradictions between
85
- your rules.
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.
86
70
 
87
71
  ## Contents
88
72
 
89
73
  - [Install](#install)
90
- - [Quick start](#quick-start)
91
- - [Generate rules from your repo](#generate-rules-from-your-repo)
92
- - [Install rules from other repos](#install-rules-from-other-repos)
93
- - [Check for contradictions](#check-for-contradictions)
94
- - [Live evals](#live-evals)
95
- - [Writing good rules](#writing-good-rules)
96
- - [Commands](#commands)
97
- - [Reading the report](#reading-the-report)
98
- - [Rule format](#rule-format)
99
- - [Config](#config)
100
- - [Other Providers](#providers)
101
- - [Running lint](#running-lint)
102
- - [Comments and suppressions](#comments-and-suppressions)
103
- - [Cache](#cache)
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)
104
93
  - [Agent skills](#agent-skills)
105
- - [How a file is judged](#how-a-file-is-judged)
106
- - [Upgrading](#upgrading)
107
- - [Development](#development)
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)
108
99
  - [License](#license)
109
100
 
110
101
  ## Install
111
102
 
112
- Add adhere to your repo's dev dependencies, and run it with `npx adhere` or
113
- `bunx adhere`:
103
+ Install the standalone executable. It needs no Node or Bun, and runs in any
104
+ directory, inside a repo or not:
114
105
 
115
106
  ```sh
116
- npm install -D @drkmttr/adhere
107
+ curl -fsSL https://github.com/darkmatter/adhere/raw/main/scripts/install.sh | sh
117
108
  ```
118
109
 
119
- Or download the standalone executable, which needs no Node or Bun:
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:
120
115
 
121
116
  ```sh
122
- curl -fsSL --create-dirs -o ~/.local/bin/adhere \
123
- https://github.com/darkmatter/adhere/releases/latest/download/adhere-$(uname -s)-$(uname -m)
124
- chmod +x ~/.local/bin/adhere
117
+ bun add -d @drkmttr/adhere
118
+ bun run adhere lint
125
119
  ```
126
120
 
127
- - The npm package brings its own TypeScript compiler. Opt in with
128
- `--type-aware` or `typeAware: true` to add both
129
- [`symbols` and `references`](#what-jev-reads); type awareness is off by default.
130
- No other compiler setup is needed.
131
- - The standalone executable has no compiler. It runs the default, compiler-free
132
- audit with workspace context, or code alone with `reads: []`. Type-aware audits
133
- require the npm package or a checkout with its development dependencies.
134
- It suits a global install that your agent config can always call.
135
- - It is built for macOS and Linux, on arm64 and x64, and for Windows x64:
136
- download `adhere-Windows-x86_64.exe` from the
137
- [latest release](https://github.com/darkmatter/adhere/releases/latest).
138
- - adhere sends each file it judges, with what it [reads](#what-jev-reads)
139
- beside it, to Jev at `api.typesafe.ai`, authenticated with a TypeSafe AI API
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
140
135
  key.
141
136
 
142
137
  ### Login
@@ -154,298 +149,62 @@ adhere logout --openai # deletes it, and only it
154
149
  saved beside the TypeSafe AI one, for a config whose provider is
155
150
  `openai()`; see [Providers](#providers).
156
151
  - `TYPESAFE_API_KEY`, when set, takes precedence over the saved key, so CI can
157
- pass a key without a login. `OPENAI_API_KEY` does the same for OpenAI's.
158
- - The key is read only when a request is about to be sent. A run the cache
159
- answers in full needs no key and no network.
160
-
161
- ## Quick start
162
-
163
- ```sh
164
- npx adhere init # .adhere/config.ts, two example rules, and the dependency
165
- npx adhere login # save your TypeSafe AI API key, once
166
- npx adhere validate # check your rules' wording, and ask Jev whether any contradict
167
- npx adhere lint # audit the working directory
168
- git add .adhere # commit the rules, and the judgments they cost
169
- ```
170
-
171
- ### Init
172
-
173
- `adhere init` creates a config file, some example rules, and adds adhere as a
174
- dev dependency.
175
-
176
- It is safe to rerun: existing files are reported as skipped, and only `--force`
177
- overwrites them.
178
-
179
- ## Generate rules from your repo
180
-
181
- Let an agent write your first rules with you. The
182
- [`adhere-setup`](./skills/adhere-setup/SKILL.md) skill walks it through setting
183
- adhere up in your repo:
184
-
185
- 1. It reads where your conventions live: `AGENTS.md`, `CONTRIBUTING.md`, docs
186
- and ADRs, your lint config, recent review comments, and the code others copy
187
- from.
188
- 2. It drafts the conventions a normal linter cannot check as rules, each with
189
- real code from your repo, and sets aside the ones a linter could check.
190
- 3. It adds the presets that fit your stack, and rules from your organization's
191
- [shared repo](#install-rules-from-other-repos), if you have one.
192
- 4. It shows every candidate in one list, with its evidence, and imports the ones
193
- you choose.
194
- 5. It validates them, walks you through the first lint and what it costs, and
195
- adds adhere to CI.
196
-
197
- It stops to ask you at each of those decisions, and never asks for your API key.
198
-
199
- ```sh
200
- skills add darkmatter/adhere # install adhere's skills for your agent
201
- ```
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.
202
156
 
203
- Then ask your agent to set adhere up.
204
-
205
- ## Install rules from other repos
157
+ ## Getting Started
206
158
 
207
- An organization's rules can live in one repo's `.adhere/rules/`, and other repos
208
- copy them in with `adhere install`:
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:
209
162
 
210
163
  ```sh
211
- adhere list darkmatter/standards # each rule there, with its description
212
- adhere install darkmatter/standards # every rule
213
- adhere install darkmatter/standards/data # the data topic
214
- adhere install darkmatter/standards/data/brand-ports # one rule
215
- adhere install darkmatter/standards#v3 # every rule, as tagged v3
164
+ adhere login
165
+ adhere lint --preset typescript,react
216
166
  ```
217
167
 
218
- - Copies land in this repo's `.adhere/rules/org/repo/`. So `data/brand-ports`
219
- from darkmatter/standards is
220
- `.adhere/rules/darkmatter/standards/data/brand-ports/`, the rule
221
- `darkmatter/standards/data/brand-ports`, and two repos' rules never collide.
222
- - The copies are the repo's own rules from then on: commit them, edit them, or
223
- start new rules from them. Nothing tracks where they came from.
224
- - A later install skips a rule already there, as init does. `--force` replaces
225
- its directory, edits and all.
226
- - A rule's whole directory is copied. The source's config, its inline rules, and
227
- its cache are not.
228
-
229
- > **Read what you install.** A `RULE.ts` is copied with the helpers beside it,
230
- > and runs whenever lint does, in CI too. `adhere list` names a `RULE.ts`
231
- > without its description, since reading it would run it.
232
-
233
- The source is cloned with `git` from `https://github.com/org/repo.git`, so a
234
- private repo needs git's credentials for GitHub, as `gh auth setup-git` sets up.
235
- To clone over SSH instead, let git rewrite the URL:
236
- `git config --global url.git@github.com:.insteadOf https://github.com/`.
237
-
238
- ### Creating a shared repo
239
-
240
- `adhere init --shared org/repo` scaffolds a repo whose rules other repos copy in
241
- with `adhere install`, instead of one that lints itself. It writes the two
242
- example rules and no config, since install copies only rule files, and with
243
- them:
244
-
245
- | File | What it is for |
246
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
247
- | `README.md` | Says what the repo is for, how to install its rules, and how to add one. |
248
- | `.github/workflows/adhere.yaml` | Runs `adhere validate` on every push to main and every pull request, with the `TYPESAFE_API_KEY` secret. A pull request from a fork gets no secrets, so validate refuses there. |
249
- | `alchemy.run.ts` | An [alchemy](https://alchemy.run) stack that creates the GitHub repo, or adopts it, and sets that secret from `TYPESAFE_API_KEY` when deployed with `npx alchemy deploy`. |
250
- | `package.json`, `.gitignore` | alchemy and effect, pinned to versions that work together, which init installs, and a `.gitignore` for them and alchemy's state. |
251
-
252
- The repo is public unless you answer yes when init asks, at a terminal, whether
253
- to make it private. Other repos then need git's credentials for it to list and
254
- install its rules. Without a terminal it is public: change `visibility` in the
255
- stack to make it private.
256
-
257
- ## Check for contradictions
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).
258
171
 
259
- Two rules contradict when no code can follow both, such as one that says errors
260
- must be thrown and one that says they must be returned. Each rule reads fine on
261
- its own, so a contradiction shows up only as code that breaks one rule or the
262
- other whatever you do. `adhere validate` finds them before lint does:
172
+ To set up rules of your own in a repo:
263
173
 
264
174
  ```sh
265
- adhere validate # your rules
266
- adhere validate --preset effect # your rules beside a preset's
267
- ```
268
-
269
- ```text
270
- Found 1 contradiction among configured rules:
271
-
272
- 1. No code can follow both (0.91):
273
- - errors/throw-tagged-errors (/repo/.adhere/rules/errors/throw-tagged-errors/RULE.md)
274
- A function that fails must throw a tagged error, never return an error value.
275
- - errors/return-results (/repo/packages/api/.adhere/rules/errors/return-results/RULE.md)
276
- A function that fails must return a Result, never throw.
277
-
278
- 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.
279
- ```
280
-
281
- It asks Jev about each pair of rules that apply to the same files, so it needs
282
- the API key, as `lint` does. A contradiction fails it, with exit code 1, so it
283
- can gate a pull request that adds a rule, as the workflow
284
- [`adhere init --shared`](#creating-a-shared-repo) writes does.
285
-
286
- It also runs two checks that are advice and never fail it:
287
-
288
- - **Wording**: each rule worded otherwise than the
289
- [rule writing tips](#rule-writing-tips) recommend, with its file and how.
290
- - **The linter check**: while `lint` judges a file against one of your rules, it
291
- also asks Jev whether a linter or type checker could decide the rule exactly,
292
- on 10 files per rule. A rule flagged on 7 or more of them probably belongs in
293
- a regular linter. One flagged on 3 to 6 reads differently from file to file,
294
- so its description should say more precisely what it applies to. Preset rules
295
- are left out.
296
-
297
- Two rules with the same id are never compared: a nested rule that shares an id
298
- shadows the other on purpose.
299
-
300
- ## Live evals
301
-
302
- Try a change to a rule on your own code before you rely on it: run the current
303
- rule and the changed one side by side, and compare what each flags.
304
-
305
- 1. Copy the rule's directory under a new id:
306
-
307
- ```sh
308
- cp -r .adhere/rules/data/brand-ports .adhere/rules/data/brand-ports-next
309
- ```
310
-
311
- 2. Give the copy `level: warning` in its front matter. Its findings show in
312
- amber and never fail the run, so the experiment can sit in the repo, and in
313
- CI, while you watch it.
314
- 3. Change the copy: its description, its examples, its `appliesTo` or
315
- `excludeIf`. To try giving Jev more to read, make the copy a `RULE.ts` with
316
- an [`appendState`](#rules-as-typescript-files) hook.
317
- 4. Run `adhere lint`, or `adhere lint --filter '<glob>'` to try it on part of
318
- the repo first, and compare the two rules' findings on the same files.
319
-
320
- Judge the two by recall, the share of real violations each catches, and
321
- precision, the share of its flags that are real. Check the findings by hand, or
322
- with the [`adhere-fix`](./skills/adhere-fix/SKILL.md) skill, which verifies each
323
- one against its rule and the code. Then keep the better version under the
324
- original id, and delete the other.
325
-
326
- - **The copy costs nothing until you change it.** Judgments are cached by the
327
- rule's text, not by its id, its level, or its threshold, so a copy that reads
328
- the same as the original answers from the original's cache. Each change after
329
- that judges the copy, and only the copy, once per file.
330
- - **Keeping the winner is free.** Moving the winning text to the original id
331
- reuses its cached judgments.
332
- - **Counting each rule's findings** takes a pipe:
333
- `adhere lint --yes | grep -c 'data/brand-ports-next:'`.
334
-
335
- ## Writing good rules
336
-
337
- We've evaluated how Jev reads a rule, on the presets and on other repos, to
338
- catch the most violations with the fewest false positives. The
339
- [eval](eval/README.md#studies) has every study; these are the lessons for
340
- writing a rule.
341
-
342
- ### Rule writing tips
343
-
344
- - **Say "must" and "never"**, in the description and in the headings over the
345
- code, or "should" and "should not" for a guideline. Rewording the presets'
346
- descriptions this way, with a bad example per rule, was the largest gain of
347
- any study: at 0.8, as many caught, and 1 wrong where there had been 5 or 6
348
- ([study](eval/studies/must-never.md)).
349
- - **Give one example of each kind**: one `must` block and one `never` block. A
350
- bad example helped every wording, and at 0.9 caught 24 violations with none
351
- wrong, where there had been 17
352
- ([study](eval/studies/rule-vocabulary.md)). Either kind alone did worse, and
353
- three of each did no better, for 1.58 times the tokens
354
- ([study](eval/studies/example-count.md)).
355
-
356
- `adhere validate` lists each rule worded otherwise.
357
-
358
- ### What else the studies found
359
-
360
- | Lesson | What the study found |
361
- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
362
- | "Should" is for people, not a weaker check. | Rules written with "should" and with "must" scored nearly the same ([study](eval/studies/rule-vocabulary.md)). |
363
- | A wrong finding is usually the rule's fault. | Above 0.8, 31 of 35 wrong findings repeated within a rule: a scope its words reach past, or the same misreading again. Fixing the wording fixes them together. Below 0.8, one-off misreads grow, to about one finding in six at 0.6 to 0.7 ([study](eval/studies/presets.md)). |
364
- | Narrow a rule in its description. | Jev scores an `excludeIf` near 0.5 for real and false findings alike, so it thins a rule's findings about as much as a higher threshold ([study](eval/studies/presets.md)). Narrowing the description moves the judgment itself: scoping alchemy's idempotent-delete rule to providers' delete handlers dropped its findings elsewhere from 0.67–0.88 to 0.08–0.21, with nothing real lost ([study](eval/studies/idempotent-delete.md)). |
365
- | Beware rules that turn on what the file does not show. | The weakest preset rules hinged on facts outside the file: whether a client has a default timeout, whether a script is an entry point, whether a key is public ([study](eval/studies/presets.md)). Warning below a context of 0.7 marked one finding in five, and those were real about half the time, where the rest were real three times in four ([study](eval/studies/sufficiency.md)). Make such a rule a warning, or give Jev the fact with [`typeAware` or workspace `reads`](#what-jev-reads), an [`appendState`](#rules-as-typescript-files) hook, or an `@adhere` note. |
366
- | Jev believes comments. | Eight of nine real violations of the idempotent-delete rule carried a comment wrongly saying they were fine, and scored 0.31 to 0.62; without comments, all nine scored 0.81 or more ([study](eval/studies/idempotent-delete.md)). That is why adhere takes comments out. A fact Jev needs, such as an exemption, goes in an `@adhere` note, which stays ([study](eval/studies/comments.md)). |
367
-
368
- ## Commands
369
-
370
- | Command | What it does |
371
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
372
- | `adhere lint` | Audit the working directory. See [Running lint](#running-lint). |
373
- | `adhere validate` | Check the rules' wording, and ask Jev whether any contradict. See [Check for contradictions](#check-for-contradictions). |
374
- | `adhere init [--force]` | Scaffold `.adhere/config.ts` and two example rules. See [Init](#init). |
375
- | `adhere init --shared org/repo` | Scaffold a repo of rules other repos install. See [Creating a shared repo](#creating-a-shared-repo). |
376
- | `adhere list org/repo` | List the rules in another repo's `.adhere/rules/`. See [Install rules from other repos](#install-rules-from-other-repos). |
377
- | `adhere install org/repo` | Copy them into this repo's `.adhere/rules/org/repo/`. |
378
- | `adhere login`, `adhere logout` | Save or delete a TypeSafe AI API key, or with `--openai`, an OpenAI one. See [Login](#login). |
379
- | `adhere skill [docs\|setup\|fix]` | Print an agent skill. See [Agent skills](#agent-skills). |
380
-
381
- Bare `adhere` prints the help. `adhere <command> --help` lists a command's
382
- flags, and `adhere --completions <shell>` prints a completion script.
383
-
384
- ## Reading the report
385
-
386
- A finding looks like this:
387
-
388
- ```text
389
- × data/brand-ports: A port must be a branded, range-checked integer, never a bare number.
390
- confidence 0.93 · context 0.88
391
- ╭─[src/server.ts:6:3]
392
- 1 │ import { Effect } from "effect";
393
- 2 │ import { listen } from "./listen.ts";
394
- 3 │
395
- 4 │ export const serve = Effect.gen(function* () {
396
- 5 │ const host = process.env.HOST ?? "localhost";
397
- 6 │ const port: number = Number(process.env.PORT ?? 3000);
398
- · ──────────────────────────────────────────────────────
399
- 7 │ yield* listen({ host, port });
400
- 8 │ yield* Effect.log(`Listening on ${host}:${port}`);
401
- 9 │ });
402
- ╰────
403
- hint: const Port = Schema.Int.pipe(
404
- Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
405
- Schema.brand("Port"),
406
- );
407
-
408
- confidence 0–0.74 < 0.8 < 0.85–1 Jev's probability that the file breaks the rule
409
- context 0–0.49 < 0.6 < 0.70–1 its probability that the file shows enough to decide
410
-
411
- Found 1 error.
412
- 42 files, 3 judged, 39 cached.
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
413
179
  ```
414
180
 
415
- | Part | What it tells you |
416
- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
417
- | `×` or `⚠` | An error, or a warning, in amber, from a rule whose `level` is `warning`. Only errors fail the run. |
418
- | `data/brand-ports` | The rule's id, then its description. A preset's rule has the preset's name first, as in `effect/basics/gen-for-sequencing`. |
419
- | `confidence` | Jev's probability that the file breaks the rule. |
420
- | `context` | Jev's probability that it sees enough to decide that. |
421
- | The excerpt | The section of the file Jev points at. The line it names is underlined, or the lines, when it names several, are marked with a bar beside the code. |
422
- | `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:`. |
423
- | The legend | Under the last finding: each score's threshold, between its range in red and its range in green. |
424
-
425
- **Score colors.** On a terminal, each score is colored by where it sits around
426
- its threshold:
427
-
428
- - If you're near the threshold (orange), consider adjustments.
429
- - Red is below the threshold, so lint never reports it: only the legend shows
430
- that range.
181
+ ### Init
431
182
 
432
- **Low context.** Jev sees the file and what adhere [reads](#what-jev-reads)
433
- beside it, not your whole repo. So we also ask whether that shows enough to
434
- decide the rule at all. When `context` is below 0.6, it means Jev thinks it
435
- needs more info, which you can add with an `@adhere` comment. You may also see a
436
- hint:
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.
437
185
 
438
- ```text
439
- warning: this file may not show enough to check this rule
440
- help: add what the code relies on outside this file as a note Jev reads:
441
- // @adhere <the fact>, and how you know it
442
- ```
186
+ It is safe to rerun: existing files are reported as skipped, and only `--force`
187
+ overwrites them.
443
188
 
444
- - On the eval, most findings with this warning were false, where about one in
445
- five of all findings was ([study](eval/studies/sufficiency.md)). Check what
446
- the code relies on outside the file before acting on it.
447
- - If you feel the threshold is too low/high, override it using `sufficiencyThreshold`
448
- in the config.
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).
449
208
 
450
209
  ## Rule format
451
210
 
@@ -500,7 +259,7 @@ const port: number = Number(process.env.PORT);
500
259
 
501
260
  | Front matter | Meaning |
502
261
  | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
503
- | `description` | Required. One sentence saying what code must be, and never be. |
262
+ | `description` | Required. One sentence that says what code must be and what it must never be. |
504
263
  | `threshold` | The rule's own cutoff, in place of the config's. |
505
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. |
506
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. |
@@ -517,8 +276,8 @@ const port: number = Number(process.env.PORT);
517
276
  not"), and cannot mix the two.
518
277
  - A rule with only a `never` block suits a rule with no single correct form to
519
278
  show, such as a hand-rolled retry loop or an error caught and dropped.
520
- - Neither goes in the other's place: code that must never be written, under
521
- `must`, reads to Jev as the pattern to follow.
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.
522
281
  - Prose around the code is the rule's details: Jev reads it after the
523
282
  description, so use it to say why the rule holds or where it does not apply.
524
283
  The report shows the description alone.
@@ -532,7 +291,7 @@ const port: number = Number(process.env.PORT);
532
291
  - Only a heading that is the word alone names code, in any case and with or
533
292
  without a colon: `## Never:` does, `## Never do this` does not.
534
293
  - A fence can instead name its code after its language, as in `ts never`, which
535
- GitHub does not show. A fence's own word wins over its heading's.
294
+ GitHub does not show. A fence's own word wins over its heading's word.
536
295
  - A rule inline in a config takes the same keys, with its code under `must`,
537
296
  `never`, `should`, and `shouldNot`, and its prose under `details`.
538
297
  - `loadRules(directory)` from the package root does the same load for your own
@@ -571,215 +330,296 @@ finding its matchers dropped, with their scores.
571
330
  > scored an `excludeIf` near 0.5 for real and false findings alike. See
572
331
  > [Writing good rules](#writing-good-rules).
573
332
 
574
- ### What Jev reads
333
+ ### Rules as TypeScript files
575
334
 
576
- By default, Jev reads the file's code and its imported workspace packages, with
577
- no native compiler startup. Opt in with `typeAware: true` in the config or
578
- `adhere lint --type-aware` to add both compiler context maps for every rule:
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.
579
340
 
580
- | Name | What Jev reads |
581
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
582
- | `workspacePackages` | The repo's own packages the file imports, each name with its directory, as in `{ "orders-core": "packages/orders" }`. A package it imports that is not listed is an installed one. |
583
- | `symbols` | The compiler's type blocks for each name, plus compiler documentation and JSDoc tags; not literal editor hover text. |
584
- | `references` | Whole declarations of symbols the code uses from other files of the repo, keyed by `symbol:repository-relative pathname`: a function with its body, a method without the rest of its class. |
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.
585
347
 
586
- `symbols` omits standard-library and Node/Bun runtime built-ins, including their
587
- methods. Project and third-party symbols remain, even when they share a built-in
588
- name. Built-in type names can still appear inside those symbols' type signatures.
348
+ ### Where rules live
589
349
 
590
- `references` is a flat string-to-string map. Keys keep the symbol's name as used
591
- in the target code, including import aliases, and the pathname's original casing.
592
- Values are complete declaration excerpts, not incoming reference call sites.
593
- Overlapping declarations under one key are merged; disjoint excerpts are joined
594
- with `…` between them. For example, code importing `twice as scale` can receive:
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).
595
359
 
596
- ```json
597
- {
598
- "references": {
599
- "__comment": "Declarations of symbols used by the target code, provided only as context. Keys are symbol:repository-relative pathname; values are source excerpts, not incoming call sites.",
600
- "scale:src/Math.ts": "export function twice(value: number): number {\n return value * 2;\n}"
601
- }
602
- }
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" });
603
365
  ```
604
366
 
605
- When sent to Jev, each enabled `references`, `symbols`, and `workspacePackages`
606
- map includes a reserved `__comment` explaining its contents. These comments are
607
- included before `appendState` hooks and restored after hooks. Top-level guidance
608
- also tells Jev that these maps are context only: judge the target `code`, not the
609
- additional declarations or metadata.
367
+ That directory holds a directory per rule, as `.adhere/rules/` does, and its
368
+ rules apply project-wide.
610
369
 
611
- Each symbol's complete resolved content is truncated to 256 characters by
612
- default. Set `maxSymbolLength` in the config to change this limit. Documentation
613
- and JSDoc tags are first capped separately by `maxSymbolDocumentationLength`, also
614
- 256 by default; `0` omits documentation and tags. Smaller limits reduce request
615
- size but can cut off type signatures, overloads, useful details, or examples.
370
+ ## Writing good rules
616
371
 
617
- `typeAware` defaults to `false`. When enabled, it adds **both** `symbols` and
618
- `references`, regardless of `reads`. The `reads` setting now chooses workspace
619
- context only: its default is `["workspacePackages"]`; `[]` omits that map.
620
- Legacy `"symbols"` or `"references"` entries in `reads` alone do not enable native
621
- context, and leaving them out does not disable it when `typeAware` is true.
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.
622
375
 
623
- ```ts
624
- // .adhere/config.ts
625
- import { defineConfig } from "@drkmttr/adhere";
376
+ ### Rule writing tips
626
377
 
627
- export default defineConfig({
628
- typeAware: true, // opt in to both symbols and references
629
- reads: [], // omit workspacePackages, not compiler context
630
- });
631
- ```
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)).
632
389
 
633
- For code alone, use `reads: []` with type awareness off; do not pass
634
- `--type-aware`. To retain workspace context without a compiler, leave the
635
- settings at their defaults.
390
+ `adhere validate` lists each rule worded otherwise.
636
391
 
637
- Each name is the key Jev reads it under, so a rule's description can name it in
638
- backticks, as the questions name `code`:
392
+ ### What else the studies found
639
393
 
640
- ````md
641
- ---
642
- description: A function imported from one of this repository's own packages, which the file's `workspacePackages` names, must be called through the package's namespace, never imported by its own name.
643
- ---
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)).
644
424
 
645
- ## Must
425
+ ## Generate rules from your repo
646
426
 
647
- ```ts
648
- import * as Orders from "orders-core";
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
649
447
  ```
650
448
 
651
- ## Never
449
+ Then ask your agent to set adhere up.
652
450
 
653
- ```ts
654
- import { parse } from "orders-core";
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
655
462
  ```
656
- ````
657
463
 
658
- - `symbols` and `references` use the SDK and native compiler pinned to exactly
659
- `7.1.0-dev.20261009.1`, which the npm package installs. This prerelease contains
660
- the tuple-reference serialization fix from [microsoft/TypeScript#64080](https://github.com/microsoft/TypeScript/issues/64080);
661
- SDK and native executable must match that exact version. They use each file's
662
- `tsconfig.json`, and work without one.
663
- - `symbols` is still a name-to-string map. Distinct binding/narrowing types get
664
- distinct blocks; docs and JSDoc tags appear once per canonical symbol per file.
665
- It has no IDE labels or selected-call overload presentation. Symbol/type queries
666
- are batched, with independent SDK requests chunked to at most 8.
667
- - A name adhere does not have refuses the run, so a misspelled one does not go
668
- unnoticed. So does `reads` in a rule: it is the config's alone.
669
- - What adhere has no name for, such as a file of the repository's, a rule in
670
- TypeScript adds with [`appendState`](#rules-as-typescript-files).
671
-
672
- **Cost and privacy.** Opting into type awareness sends Jev more than the file it
673
- judges: whole declarations from other files, plus compiler-provided documentation
674
- and JSDoc tags in `symbols`, even with `includeComments` off. It also starts the
675
- native compiler during execution. The plan's estimate leaves those tokens out.
676
- Keep `typeAware` off to avoid that extra context and compiler work; changing
677
- `reads` only changes workspace context.
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.
678
474
 
679
- <details>
680
- <summary>Where the packages come from</summary>
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.
681
478
 
682
- - The packages are read on every run from the working directory's
683
- `package.json`, whose `workspaces` npm, Bun, and Yarn read, and from
684
- `pnpm-workspace.yaml`'s `packages`.
685
- - In a pattern, `*` is any directory and `**` any depth of them, and one that
686
- starts with `!` leaves out what it matches. Each matched directory's
687
- `package.json` gives the name, and its directory is given from the working
688
- directory.
689
- - Nothing need be installed, so a run in CI reads the same packages as one on a
690
- laptop.
691
- - A working directory that is not a workspace's root has none.
692
- - Only an import whose path is a plain string counts, and one of a subpath, such
693
- as `orders-core/client`, counts as its package's.
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/`.
694
483
 
695
- </details>
484
+ ### Creating a shared repo
696
485
 
697
- ### Rules as TypeScript files
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:
698
490
 
699
- A rule's directory can hold it as a `RULE.ts` instead, which default-exports
700
- `defineRule({...})`, taking the fields a config's inline rule does. What
701
- TypeScript adds is `appendState`, a hook on what Jev reads for the rule:
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.
702
502
 
703
- ```ts
704
- // .adhere/rules/data/columns/RULE.ts
705
- import { defineRule } from "@drkmttr/adhere";
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.
706
507
 
707
- export default defineRule({
708
- description:
709
- "A query must name only columns its table has in `schema`, and never a column it lacks.",
710
- must: 'db.select("id", "email").from("users")',
711
- never: 'db.select("mail").from("users")',
712
- appendState: async (state, file, Bun) => ({
713
- schema: await Bun.file("db/schema.sql").text(),
714
- }),
715
- });
716
- ```
508
+ ## Presets
717
509
 
718
- - The hook runs right before each request about the rule goes out. It gets the
719
- request's state, the file the request is about, and Bun's API, and what it
720
- returns is spread over the state.
721
- - The description can name a key the hook adds, in backticks, as the questions
722
- name `code`.
723
- - A `RULE.ts` is imported, as a config is, so its code runs on every lint.
724
- - A rule with a hook goes to Jev in requests of its own, so only that rule reads
725
- what its hook adds. Each of those requests sends the file and its context
726
- again.
727
- - What the hook reads outside the file is not part of the cache: when
728
- `db/schema.sql` changes, a file already judged is not judged again until it or
729
- the rule changes.
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.
730
532
 
731
533
  <details>
732
- <summary>More on the hook</summary>
534
+ <summary>What every preset does</summary>
733
535
 
734
- - The state is what Jev reads beside each question: `code`, the file's sections
735
- as Jev reads them, under their numbers from 1, as in
736
- `{ code: { "1": "import …", "2": "export const …" } }`, beside what the
737
- config's [`reads` and `typeAware`](#what-jev-reads) enable.
738
- - `file` is the file's absolute `path`, and its `contents` as written, comments
739
- and all.
740
- - Nothing the hook returns is checked, so it can change `code` too, or replace
741
- it: a hook can break its own rule's answers, and adhere does not stop it.
742
- - It can be async, and one that throws refuses the run, naming the rule. A
743
- `RULE.ts` that default-exports no rule refuses the run too.
744
- - The plan counts the rule's requests, but not the tokens a hook adds, which
745
- are not known until it runs. A request over Jev's context skips the file, as a
746
- file too long does.
747
- - A `RULE.ts` can import other files by relative path, such as a helper beside
748
- it in its directory, but the executable resolves no packages besides
749
- `@drkmttr/adhere`.
750
- - `Bun` is typed when `@types/bun` is installed and `.adhere/tsconfig.json`
751
- lists it, as `"types": ["bun"]`, and is `unknown` otherwise.
752
- - A config's inline rules can have `appendState` too.
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.
753
545
 
754
546
  </details>
755
547
 
756
- ### Where rules live
548
+ ## Reading the report
757
549
 
758
- - Rules in the root `.adhere/rules/` apply project-wide.
759
- - A nested `.adhere/` scopes its rules to the directory that contains it. A rule
760
- in `packages/api/.adhere/rules/data/brand-ports/RULE.md` has the same id,
761
- `data/brand-ports`, but applies only to files under `packages/api/`.
762
- - If a nested rule has the same id as a root rule, the nearest containing
763
- `.adhere/` shadows the less-specific rule for that subtree. Outside that
764
- subtree, the root rule still applies.
765
- - Nested `.adhere/` directories are not discovered under `node_modules/`,
766
- `dist/`, and the other [skipped directories](#what-gets-read).
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
+ );
767
571
 
768
- A repo that would rather keep its rules with the rest of its documentation can
769
- point `rules` at a directory such as `docs/adhere/`:
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
770
574
 
771
- ```ts
772
- export default defineConfig({ presets: ["effect"], rules: "./docs/adhere" });
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
773
609
  ```
774
610
 
775
- That directory holds its rules as `.adhere/rules/` does, a directory per rule,
776
- and they apply project-wide.
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.
777
617
 
778
618
  ## Config
779
619
 
780
620
  A config file is optional. Without one, adhere reads the rules in
781
621
  `.adhere/rules/` and any `--preset`. A config names presets, sets the model,
782
- the thresholds, and what Jev reads, or gives rules inline. It sits in the
622
+ the thresholds, and what Jev reads, or gives rules inline. Put it in the
783
623
  working directory of the repo being audited, at one of these paths (keep one):
784
624
 
785
625
  - `.adhere/config.ts`, the default, next to the rules
@@ -790,14 +630,8 @@ working directory of the repo being audited, at one of these paths (keep one):
790
630
  import { defineConfig } from "@drkmttr/adhere";
791
631
 
792
632
  export default defineConfig({
793
- model: "jev-latest",
794
633
  threshold: 0.8,
795
- sufficiencyThreshold: 0.6,
796
- reads: ["workspacePackages"],
797
- typeAware: true, // optional; false by default
798
634
  presets: ["effect"],
799
- maxSymbolDocumentationLength: 256,
800
- maxSymbolLength: 256,
801
635
  exclude: ["**/generated/**"],
802
636
  overrides: { "effect/basics/instrument-with-pipe": "off" },
803
637
  rules: {
@@ -815,25 +649,54 @@ type UserId = typeof UserId.Type
815
649
  });
816
650
  ```
817
651
 
818
- | Key | Default | Meaning |
819
- | ------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
820
- | `model` | `"jev-latest"` | The model id sent to TypeSafe. |
821
- | `threshold` | `0.8` | Report a rule when Jev's probability is above this. |
822
- | `sufficiencyThreshold` | `0.6` | Below it, a finding warns that Jev may not see enough to decide. |
823
- | `presets` | none | Built-in rule sets, or their topics. See [Presets](#presets). |
824
- | `rules` | `.adhere/rules/` | Rules inline, as above, or a directory (see [Where rules live](#where-rules-live)). Either replaces `.adhere/rules/`: none of its rules, root or nested, is read then. |
825
- | `exclude` | none | Globs, relative to the working directory, of files no rule judges. |
826
- | `overrides` | none | Settings for a rule by its id. See [Overrides](#overrides). |
827
- | `includeComments` | `false` | Send every comment to Jev. See [Comments](#comments). |
828
- | `reads` | `["workspacePackages"]` | Chooses workspace context only. `[]` omits it; native read entries do not enable type awareness. See [What Jev reads](#what-jev-reads). |
829
- | `typeAware` | `false` | Opt in to both compiler `symbols` and declaration `references`, regardless of `reads`. Also enabled by `--type-aware`. |
830
- | `maxSymbolDocumentationLength` | `256` | Maximum characters of each symbol's combined documentation and JSDoc tags, before `maxSymbolLength` applies. A non-negative integer; `0` omits them. |
831
- | `maxSymbolLength` | `256` | Maximum characters of each complete `symbols[name]` string, including type signatures and documentation. A non-negative integer; `0` leaves names with empty content. |
832
- | `provider` | TypeSafe's Jev | Where the questions go instead, with its own model. See [Providers](#providers). |
833
-
834
- - An invalid shape refuses the run.
835
- - A config can import other files by relative path, but no packages besides
836
- `@drkmttr/adhere`: the executable does not resolve `node_modules`.
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`.
837
700
 
838
701
  <details>
839
702
  <summary>More on types in the editor</summary>
@@ -853,9 +716,531 @@ type UserId = typeof UserId.Type
853
716
 
854
717
  </details>
855
718
 
856
- ### Providers
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
+ ```
825
+
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:
828
+
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.
835
+
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
+ ```
857
1226
 
858
- adhere asks TypeSafe's Jev by default. A provider asks another decision model
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
859
1244
  instead, and adhere exports one for each model
860
1245
  [the providers study](eval/studies/providers.md) measured:
861
1246
 
@@ -868,13 +1253,13 @@ export default defineConfig({ provider: openai() });
868
1253
 
869
1254
  | Provider | Asks | Options |
870
1255
  | ------------ | --------------------------------------------------------------- | -------------------------------- |
871
- | `jev` | TypeSafe's Jev, `jev-latest` by default | `apiKey`, `model` |
1256
+ | `jev` | TypeSafe AI's Jev, `jev-latest` by default | `apiKey`, `model` |
872
1257
  | `openai` | OpenAI's `gpt-6-luna`, on its Decisions API | `apiKey`, `model` |
873
1258
  | `cloudflare` | Cloudflare's `clef`, by default, or `clef-flash`, on Workers AI | `accountId`, `apiToken`, `model` |
874
1259
 
875
1260
  - Without an `apiKey`, `jev` and `openai` ask with the key from
876
1261
  [Login](#login).
877
- - Each also takes `fetch`, what sends its requests.
1262
+ - Each provider also takes `fetch`, the function that sends its requests.
878
1263
  - Clef cuts a request too long for it short rather than refuse it, so
879
1264
  `cloudflare` skips a file whose request reaches clef's limit: 64,000 tokens,
880
1265
  or 24,000 for clef-flash.
@@ -910,20 +1295,20 @@ export default defineConfig({ provider: mine });
910
1295
  - `state` is the file's code by section under `code`, beside what adhere read
911
1296
  for it.
912
1297
  - Each question has an `id`, the `question` as adhere phrases it, and the
913
- `fields` it names in backticks, such as `rule`, `must`, and `never`. A `noul`
914
- asks whether something holds. A `choice` asks for the key of one of its
915
- `options`, and can have just one.
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.
916
1301
  - `ask` returns `{ answers }`: by id, a `noul`'s probability from 0 to 1, or a
917
1302
  `choice`'s key. An id left out goes unanswered: its rule is not reported, and
918
1303
  is asked again on the next run.
919
1304
  - A rejection stops the run, but for three errors adhere exports:
920
- `ContextOverflow` gets one retry without `references` when that map is present;
921
- if the request still overflows, the file is skipped. `RequestBlocked` reports
922
- it blocked, and `RequestFailed` is tried again, up to three times, when its
923
- `status` is 0, for no answer, or a 408, 429, or 5xx.
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.
924
1309
  - adhere paces calls with `--rpm`, aborts `signal` after 30 seconds, and splits
925
- requests to fit Jev's 64,000 tokens. A model that takes less splits further in
926
- its `ask`.
1310
+ requests to fit Jev's 64,000 tokens. A model that takes less splits further
1311
+ in its `ask`.
927
1312
  - `model` replaces the config's `model`. The cache keeps answers by it, so a
928
1313
  new model is asked again.
929
1314
  - A provider that sets `savedKey` to `"typesafe"` or `"openai"` gets that key
@@ -933,334 +1318,17 @@ export default defineConfig({ provider: mine });
933
1318
 
934
1319
  </details>
935
1320
 
936
- ### Presets
937
-
938
- | Preset | Rules | For | What it asks for |
939
- | ------------------------------------- | ----- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
940
- | [`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. |
941
- | [`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. |
942
- | [`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. |
943
- | [`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`. |
944
- | [`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. |
945
-
946
- - Name presets in the config's `presets`, or on the command line with
947
- `--preset`, repeated or separated by commas, as in `--preset effect,alchemy`,
948
- in which case the config file is optional. Presets named in both places apply
949
- together.
950
- - A topic, one of a preset's subdirectories, is a preset of its own:
951
- `--preset effect/basics` applies only the rules under
952
- `presets/effect/basics/`, and `alchemy/secrets` only alchemy's secrets rules.
953
- `security` has no topics.
954
- - To change a preset's rule without copying it, use [Overrides](#overrides).
955
-
956
- <details>
957
- <summary>What every preset does</summary>
958
-
959
- - It leaves out conventions a linter checks exactly. The notes on each preset
960
- say which linter checks them.
961
- - A preset rule says "must" only where its source makes a requirement, and
962
- "should" where the source gives advice.
963
- - A topic's rules keep the ids they have in the whole preset, so a topic and its
964
- preset share cached judgments, and naming both applies each rule once.
965
- - Where effect or alchemy had a rule that `typescript` or `security` has, that
966
- one is kept and the other is gone. typescript's
967
- `async/network-calls-have-timeouts` replaced effect's
968
- `basics/external-calls-are-resilient`, and `security/parameterized-queries`
969
- and `security/no-secrets-in-output` replaced alchemy's
970
- `data/parameterized-sql` and `secrets/never-logged-returned-or-output`. A
971
- project on Effect or alchemy names `typescript` and `security` too, for those
972
- rules.
973
-
974
- </details>
975
-
976
- <details>
977
- <summary>Notes on <code>typescript</code></summary>
978
-
979
- - **Source.** The TypeScript Handbook and the Google TypeScript Style Guide.
980
- - **Left to a linter.** What typescript-eslint and ESLint check exactly:
981
- unhandled promises (`no-floating-promises`), throwing non-errors
982
- (`only-throw-error`), `any` (`no-explicit-any`), exhaustive switches
983
- (`switch-exhaustiveness-check`), a lost `cause` (`preserve-caught-error`), and
984
- empty catch blocks (`no-empty`).
985
- - **Warnings.** Four of its rules report warnings. Three are advice that code
986
- often has reason to set aside: assertions that claim only what the code
987
- established, types derived rather than restated, and independent awaits run
988
- concurrently. The fourth, that HTTP requests carry a timeout, was right on
989
- fewer than half its findings above 0.8 on the eval: many of the rest were
990
- calls through a client with a default timeout that the file does not show.
991
- - **Tests.** `testing/assert-outcomes` and `testing/independent-tests` say
992
- `tests: only`: they judge tests and nothing else.
993
- - **Gone.** Two rules the eval found mostly wrong: that tests use fake time
994
- rather than real waits, and that importing a module does no work.
995
-
996
- </details>
997
-
998
- <details>
999
- <summary>Notes on <code>react</code></summary>
1000
-
1001
- - **Source.** react.dev, mostly
1002
- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
1003
- and
1004
- [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure).
1005
- - **Left to a linter.** What the React Compiler's lint rules check, in
1006
- [eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks)'s
1007
- recommended set and in Oxlint: pure render (`purity`), props and state never
1008
- mutated (`immutability`), refs not read during render (`refs`), no `setState`
1009
- in render or synchronously in an effect (`set-state-in-render`,
1010
- `set-state-in-effect`), which covers state derived in an effect, and no
1011
- component defined inside another (`static-components`).
1012
- - **Files.** Its rules judge `.ts` files as well as `.tsx`, and apply only to
1013
- components, hooks, effects, and Server Functions, so other files cost their
1014
- checks and find nothing.
1015
- - **Warnings.** `server/no-private-data-to-client` reports warnings: a file does
1016
- not show whether the component it passes a record to is a Client Component. So
1017
- does `state/store-ids-not-copies`, which also flags state that copies a string
1018
- or an option from a fixed list, where a copy cannot go stale.
1019
-
1020
- </details>
1021
-
1022
- <details>
1023
- <summary>Notes on <code>effect</code></summary>
1024
-
1025
- - **Source.** The
1026
- [effect-solutions](https://github.com/kitlangton/effect-solutions) docs and
1027
- the [effect/platform](https://effect.website/docs/platform/introduction/)
1028
- docs.
1029
- - **Tests.** Three rules say `tests: only`, so they judge tests and nothing else:
1030
- `testing/test-clock-for-time`, `services/fresh-layer-per-test`, and
1031
- `config/tests-provide-values-directly`.
1032
- - **Guidelines.** One rule says "should": a test of code that consumes config
1033
- provides it through a layer.
1034
- - **Domain types.** The data rules judge the types services exchange, return,
1035
- or store. A shape that mirrors a payload until it is mapped, and a view's
1036
- props or state, are out of their scope.
1037
- - **What the rules allow.** Config validation accepts `Config.mapEffect` as well
1038
- as `Config.schema`, and the variants rule does not rule out a `switch`.
1039
- - **Gone.** The rule that a command handler only parses input: the docs show
1040
- that pattern but do not ask for it. The rule that test layers are in memory,
1041
- which flagged tests of real services: none of its findings labeled on the eval
1042
- or on darkmatter/agents was real. And the rule that service operations have no
1043
- requirements, which `leakingRequirements` below checks exactly.
1044
- - **Left to a linter.** Effect's language service,
1045
- [`@effect/tsgo`](https://github.com/Effect-TS/tsgo) on TypeScript 7, checks
1046
- seven conventions exactly, which the preset checked through 0.7. Most are off
1047
- by default. `npx @effect/tsgo setup` installs it, and these lines in its
1048
- plugin options in `tsconfig.json` turn them on:
1049
-
1050
- ```jsonc
1051
- "diagnosticSeverity": {
1052
- "strictEffectProvide": "warning", // layers are provided once, at the entry point
1053
- "leakingRequirements": "warning", // service methods have no requirements
1054
- "effectFnOpportunity": "warning", // a named function returning an Effect uses Effect.fn
1055
- "preferSchemaOverJson": "warning", // JSON is decoded with Schema, not JSON.parse
1056
- "nodeBuiltinImport": "warning", // platform services, not node: builtins
1057
- "globalFetch": "warning",
1058
- "globalFetchInEffect": "warning",
1059
- "processEnv": "warning",
1060
- "processEnvInEffect": "warning",
1061
- "globalRandom": "warning", // the Random service, not Math.random
1062
- "globalRandomInEffect": "warning",
1063
- "globalErrorInEffectFailure": "warning", // tagged errors, not Error
1064
- "extendsNativeError": "warning"
1065
- }
1066
- ```
1067
-
1068
- `npx @effect/tsgo diagnostics --project tsconfig.json` runs them in CI. Bun
1069
- globals, which the old platform rule also ruled out, need a lint rule of their
1070
- own, such as `no-restricted-globals`.
1071
-
1072
- - **What the linter misses.** `leakingRequirements` sees a requirement in an
1073
- operation's type, but not a service built by a factory function that takes its
1074
- dependencies as arguments, whose types have no requirements to find. The
1075
- preset's `services/dependencies-through-layers` asks for that.
1076
-
1077
- </details>
1078
-
1079
- <details>
1080
- <summary>Notes on <code>alchemy</code></summary>
1081
-
1082
- - **Source.** alchemy's docs and blog.
1083
- - **Warnings.** `providers/idempotent-delete` reports warnings. Whether a delete
1084
- fails on a resource that is already gone is a fact about the API, which the
1085
- file does not show, so the rule also flags deletes of APIs that succeed
1086
- anyway. See [the study](eval/studies/idempotent-delete.md).
1087
-
1088
- </details>
1089
-
1090
- ### Overrides
1091
-
1092
- A config's `overrides` changes a rule by the id a report names it with, a
1093
- preset's with the preset first, without copying its text into `rules`:
1094
-
1095
- ```ts
1096
- export default defineConfig({
1097
- presets: ["effect", "alchemy"],
1098
- overrides: {
1099
- "alchemy/providers/idempotent-delete": { level: "warning", threshold: 0.9 },
1100
- "effect/basics/instrument-with-pipe": "off",
1101
- },
1102
- });
1103
- ```
1104
-
1105
- | Value | Effect |
1106
- | ---------------------- | -------------------- |
1107
- | `"off"` | drops the rule |
1108
- | `"warning"`, `"error"` | sets its level |
1109
- | `{ level, threshold }` | sets either, or both |
1110
-
1111
- An id that no preset or project rule has refuses the run, so a typo does not go
1112
- unnoticed. A rule of a preset the run does not name is accepted.
1113
-
1114
- ### Precedence
1115
-
1116
- For the model and the threshold, highest first:
1117
-
1118
- 1. `--threshold` on the command line
1119
- 2. the config file
1120
- 3. presets, in order (a later preset wins)
1121
- 4. the defaults, `jev-latest` and `0.8`
1122
-
1123
- For one rule:
1124
-
1125
- - A rule's own `threshold` beats all of the above for that rule.
1126
- - An entry in `overrides` beats the rule's own `threshold` and `level`.
1127
- - A rule in `rules` replaces a preset rule with the same id.
1128
-
1129
- `--sufficiency-threshold` beats the config's `sufficiencyThreshold`, which beats
1130
- the default, `0.6`.
1131
-
1132
- ## Running lint
1133
-
1134
- ### Flags
1135
-
1136
- | Flag | What it does |
1137
- | ---------------------------------- | ---------------------------------------------------------------------------------------------- |
1138
- | `--preset <names>` | Add built-in rule sets, or their topics. The config becomes optional. See [Presets](#presets). |
1139
- | `--threshold <0 to 1>` | Replace the config's threshold. Per-rule thresholds still apply. |
1140
- | `--sufficiency-threshold <0 to 1>` | Replace the config's `sufficiencyThreshold`: a finding warns when its `context` is below it. |
1141
- | `--type-aware` | Enable both compiler `symbols` and declaration `references`; off by default. |
1142
- | `--yes`, `-y` | Send the requests without asking first. |
1143
- | `--limit <files>` | Send requests for at most that many files. See [Limiting a run](#limiting-a-run). |
1144
- | `--rpm <requests>` | Send at most that many requests a minute. |
1145
- | `--filter <glob>` | Judge only the files a glob matches. |
1146
- | `--deny-warnings` | Fail on warnings as well as errors. |
1147
- | `--log-level <level>` | Log the run's work on stderr. See [Logging a run](#logging-a-run). |
1148
-
1149
- ### What gets read
1150
-
1151
- - `adhere lint` reads the TypeScript files under the working directory: `.ts`,
1152
- `.tsx`, `.mts`, and `.cts`. It does not read declaration files such as
1153
- `.d.ts`, adhere's own config files, or JavaScript.
1154
- - When the working directory contains `agents/`, `apps/`, or `packages/`, only
1155
- those trees are read.
1156
- - Below the working directory, anything under `node_modules/`, `dist/`,
1157
- `coverage/`, `vendor/`, `e2e/`, `references/`, `.adhere/`, `.agents/`,
1158
- `.claude/`, `.direnv/`, `.alchemy/`, or `.vite/` is skipped. The directories
1159
- above the working directory do not count.
1160
- - `.gitignore` is not consulted.
1161
- - A config's `exclude` lists globs, relative to the working directory, of files
1162
- no rule judges, such as generated code. It leaves them out of every run, as
1163
- `--filter '!<glob>'` leaves them out of one:
1164
-
1165
- ```ts
1166
- export default defineConfig({ exclude: ["**/generated/**"] });
1167
- ```
1168
-
1169
- **Tests** are judged only by rules about tests, whose front matter says `tests`
1170
- (see [Rules as Markdown files](#rules-as-markdown-files)). A test is:
1171
-
1172
- - a `.test` or `.spec` file, such as `a.test.ts` or `Button.spec.tsx`;
1173
- - a fixture or test helper named as one, such as `fixtures.ts`,
1174
- `GitHubHttpFixtures.ts`, `ledger-copy.fixture.ts`, `linear-test-helpers.ts`,
1175
- or `test-utils.ts`;
1176
- - any file under a `test/`, `tests/`, `__tests__/`, `testing/`, `fixtures/`,
1177
- `fixture/`, `__fixtures__/`, or `__mocks__/` directory.
1178
-
1179
- ### Before and during a run
1180
-
1181
- Before it judges anything, `lint` says on stderr what it found and what judging
1182
- takes, and asks:
1183
-
1184
- ```text
1185
- 200 files and 14 rules: 2800 checks, 1400 cached.
1186
- Judging 100 files takes an estimated 100 requests to Jev, plus 1 or more for each file with a finding.
1187
- Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, and more for locating findings.
1188
- Estimates cover only code and workspace-package context; symbol/reference input, possible extra request splits, and size-skips are determined at send time.
1189
- ? 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)
1190
- ```
1191
-
1192
- - It asks only with a terminal on stdin and stdout, and only when there is
1193
- something to send. `--yes` sends without asking.
1194
- - The cost is adhere's estimate of the input tokens times the model's price. Jev
1195
- charges only for input tokens: $0.042 a million for `jev-latest`, per
1196
- [TypeSafe AI's models page](https://docs.typesafe.ai/models) in
1197
- September 2026. When type awareness is enabled, the estimate leaves out
1198
- `symbols` and `references`, which adhere reads as it sends each file, so a run
1199
- can cost more than it says.
1200
- - A run that stops loses nothing: files finished before it stopped are cached,
1201
- so running again continues from there.
1202
-
1203
- <details>
1204
- <summary>More on unknown prices and blocked files</summary>
1205
-
1206
- - For a model adhere has no price for, the plan gives the tokens alone.
1207
- - The firewall in front of Jev's API can refuse a request whose code reads to it
1208
- as an attack, with a 403 and an HTML page rather than Jev's JSON. It refuses
1209
- the same request every run, so that file is skipped, the run goes on, and the
1210
- report lists each such file with Cloudflare's Ray ID, which TypeSafe AI can
1211
- look the block up by.
1212
- - When only the question that finds the line is refused, the file's judgments
1213
- are kept, and a rerun sends that question alone.
1214
-
1215
- </details>
1216
-
1217
- ### Limiting a run
1218
-
1219
- Three flags bound what a run does:
1220
-
1221
- | Flag | Effect |
1222
- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1223
- | `--limit <files>` | Sends requests for at most that many files, taken in path order. The rest wait, and since judgments are cached, the next run with the same limit picks up where this one stopped. `--limit 0` shows the plan and sends nothing. |
1224
- | `--rpm <requests>` | Sends at most that many requests to Jev a minute, evenly spaced, retries included. The plan says about how long they take. |
1225
- | `--filter <glob>` | Judges only the files whose path from the working directory matches, such as `src/**` or `**/*.service.ts`. Wrap a glob in single quotes so the shell does not expand it. Repeat the flag for more, and start a pattern with `!` to leave out what it matches. |
1226
-
1227
- ```sh
1228
- adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 100 --rpm 30
1229
- ```
1230
-
1231
- `--limit` counts files, not requests: one file can take several.
1232
-
1233
- ### Logging a run
1234
-
1235
- | Level | What it logs, on stderr |
1236
- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1237
- | `--log-level debug` | The config it loaded, how many paths it listed and how many of them it reads, and each request to Jev, with the file it is for, about how many tokens it carries, and how long it took. A failed attempt is logged even when a retry hides it, with the HTTP status, Cloudflare's Ray ID, and the start of what the API said. |
1238
- | `--log-level trace` | Also each file as it is read, planned, and answered from the cache. |
1239
-
1240
- stdout still carries only the report, so the log can go to a file of its own:
1241
-
1242
- ```sh
1243
- adhere lint --log-level debug 2> adhere.log
1244
- ```
1245
-
1246
- ### Exit codes
1247
-
1248
- | Code | When |
1249
- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
1250
- | 0 | Nothing is reported, or only warnings. |
1251
- | 1 | An error is reported. With `--deny-warnings`, a warning too. |
1252
- | 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. |
1253
-
1254
1321
  ## Comments and suppressions
1255
1322
 
1256
1323
  ### Comments
1257
1324
 
1258
1325
  Jev reads a file's code, not its comments. adhere takes every comment out before
1259
1326
  it hashes or sends a file, or a declaration from
1260
- [`references`](#what-jev-reads), blanking it so every line keeps its number.
1261
- The report's excerpts still show them. Compiler-provided documentation and
1262
- JSDoc tags still reach Jev in the type/docs strings [`symbols`](#what-jev-reads)
1263
- sends when type awareness is enabled; these are not editor hovers.
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.
1264
1332
 
1265
1333
  Jev takes what comments claim at their word, which hid real violations in the
1266
1334
  studies: see [Writing good rules](#writing-good-rules).
@@ -1283,7 +1351,7 @@ checked it, with how you know:
1283
1351
 
1284
1352
  ### Suppressing a finding
1285
1353
 
1286
- A finding Jev got wrong is suppressed in the code, with a comment saying why:
1354
+ Suppress a finding Jev got wrong with a comment in the code that says why:
1287
1355
 
1288
1356
  ```ts
1289
1357
  // adhere-ignore alchemy/providers/idempotent-delete -- DeleteActivity succeeds on a missing activity; probed 2026-09-25
@@ -1304,22 +1372,81 @@ delete: Effect.fn(function* ({ output }) {
1304
1372
  - Jev never reads these comments, even with `includeComments: true`.
1305
1373
  - The summary counts the findings they suppress.
1306
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
+
1307
1433
  ## Cache
1308
1434
 
1309
1435
  Judgments are cached in `.adhere/cache/`. **Commit it.** Every judgment is a
1310
1436
  paid request, and Jev's answers vary a little from run to run, so a committed
1311
- cache gives everyone and CI the same findings without paying for them again, and
1312
- a pull request changes the judgments of only the files it changes.
1313
-
1314
- | What changed | What is judged again |
1315
- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
1316
- | A file's code | Every rule, for that file. |
1317
- | A file's comments alone | Nothing. |
1318
- | A file is moved or copied | Nothing. Identical files share their judgments. |
1319
- | A rule's text, a matcher, or the source of its `appendState` | That rule, for every file. |
1320
- | Anything else Jev read: a file it imports, the workspace's packages, or the config's `reads` | Nothing, so a judgment can go stale. Delete the cache to judge every file again. |
1321
- | The threshold is lowered | Nothing. Cached judgments newly above it are located. |
1322
- | adhere asks its questions differently, after an upgrade | Every rule, once. |
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. |
1323
1450
 
1324
1451
  To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1325
1452
 
@@ -1339,11 +1466,11 @@ To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1339
1466
  <details>
1340
1467
  <summary>How the cache is stored and pruned</summary>
1341
1468
 
1342
- - `files/` holds Jev's answers by the content they are about. Each cache file is
1343
- named for the hash of a source file's content as Jev reads it, without its
1344
- comments.
1345
- - Each answer sits under a fingerprint of the model and the rule: its text, its
1346
- matchers, and its `appendState`'s source.
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.
1347
1474
  - `tallies/` keeps the linter check's answers, one tally per rule, under a key
1348
1475
  that changes with the rule's text and the model.
1349
1476
  - A cache file is written once and never changed: it is also named for the hash
@@ -1361,56 +1488,6 @@ To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1361
1488
 
1362
1489
  </details>
1363
1490
 
1364
- ## Agent skills
1365
-
1366
- Three skills teach an agent to work with adhere:
1367
-
1368
- | Skill | Print it with | What it does |
1369
- | ------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1370
- | [`adhere`](./skills/adhere/SKILL.md) | `adhere skill` | The reference: the commands, how to write and word a rule, the config, presets, shared rules, comments and suppressions, the cache, reading a report, and tuning a rule. |
1371
- | [`adhere-setup`](./skills/adhere-setup/SKILL.md) | `adhere skill setup` | Sets adhere up in a repo with you. See [Generate rules from your repo](#generate-rules-from-your-repo). |
1372
- | [`adhere-fix`](./skills/adhere-fix/SKILL.md) | `adhere skill fix` | Verifies and fixes the findings `lint` reports: it checks each against its rule and the code, fixes the real ones, reports the false ones with the evidence, and says when a rule is wrong more often than right. |
1373
-
1374
- Install them with the [skills](https://github.com/vercel-labs/skills) CLI:
1375
- `skills add darkmatter/adhere`. The binary carries all three, so
1376
- `adhere skill fix > .agents/skills/adhere-fix/SKILL.md` works without a
1377
- checkout.
1378
-
1379
- ### In CI
1380
-
1381
- Pipe the fix skill into an agent that runs without asking. The skill tells it
1382
- that invoking the skill permits every `adhere lint` run and its cost, and
1383
- `adhere skill fix` prints the skill of the adhere that runs, so its flags match:
1384
-
1385
- ```sh
1386
- adhere skill fix | codex exec --yolo
1387
- adhere skill fix | claude -p --dangerously-skip-permissions
1388
- ```
1389
-
1390
- Both turn off the agent's own permission checks, which suits a runner thrown
1391
- away after the job. To keep Codex in its sandbox, let the sandbox reach
1392
- `api.typesafe.ai`:
1393
-
1394
- ```sh
1395
- adhere skill fix | codex exec --sandbox workspace-write \
1396
- -c sandbox_workspace_write.network_access=true -c approval_policy=never
1397
- ```
1398
-
1399
- - Set `TYPESAFE_API_KEY` from a secret.
1400
- - The agent adds no `adhere-ignore` comments or `@adhere` notes unless
1401
- `ADHERE_ALLOW_SUPPRESSIONS=1` is set in its environment, as in
1402
- `adhere skill fix | ADHERE_ALLOW_SUPPRESSIONS=1 codex exec --yolo`. It then
1403
- suppresses a finding it shows is false, or that three fixes did not clear,
1404
- with the evidence in the comment. Without it, the agent leaves those findings,
1405
- and its report ends by naming them and the variable.
1406
- - End the job with `adhere lint --yes`. The agent exits 0 whatever it left, so
1407
- this step passes or fails the job. It judges only the files the agent changed,
1408
- and reads the rest from the cache.
1409
-
1410
- > **Do not run it on pull requests from forks.** The agent reads their code with
1411
- > your secrets in reach and the network open, so code written to steer it can
1412
- > send them out.
1413
-
1414
1491
  ## How a file is judged
1415
1492
 
1416
1493
  1. **Split.** adhere splits the file into sections of whole statements, in runs
@@ -1437,38 +1514,40 @@ statements of its body. Jev reads the file as these sections.
1437
1514
  `{ code: { "1": "…", "2": "…" } }`, without their comments (see
1438
1515
  [Comments](#comments)), beside what the config [`reads`](#what-jev-reads).
1439
1516
  - Each rule is one `noul` (yes/no probability) question that carries the rule's
1440
- description and details, and its code under its words, `must` and `never`, or a guideline's
1441
- `should` and `should_not`. It asks whether the file diverges from the pattern
1442
- `must` shows, with `never` as an example of diverging, or, for a rule with
1443
- only code that must never be written, whether the file contains that code.
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.
1444
1522
  - Criteria for each answer draw the line at the rule's scope, so a file with no
1445
1523
  code the rule is about is a no.
1446
1524
  - No rule sits in the state, so a rule's probability depends only on the state
1447
1525
  and that rule, not on which other rules share the request. Every question
1448
1526
  shares the state cost of the request.
1449
1527
  - On up to 10 files per project rule, the rule's question has a second one
1450
- beside it, the linter check (see
1451
- [Check for contradictions](#check-for-contradictions)), with the same fields.
1528
+ beside it, the linter check (see [Validating rules](#validating-rules)),
1529
+ with the same fields.
1452
1530
  - A rule with `appendState` has requests of its own, here and below, whose
1453
1531
  state adds to this one.
1454
1532
 
1455
1533
  **Locate: a second request, only when at least one rule's probability is above
1456
1534
  its threshold.** Per flagged rule:
1457
1535
 
1458
- - When the file has more than one section, one `choice` question among the
1459
- sections' numbers, which yields the section to report. The options carry no
1460
- text of their own, since each is a key of the state: describing each section
1461
- by its first line, or by what it declares, located no better on the eval.
1462
- - For each section, two `choice` questions among its lines, each option the
1463
- line's text: the line the violation starts on and the one it ends on. The
1464
- chosen section's are underlined. On the eval, lines asked for in this request
1465
- were named as well as lines asked for on their own, and they held a violation
1466
- for 94% of real findings, most often as one line
1467
- ([study](eval/studies/pinpoint.md)).
1468
- - One `noul` carrying the rule: whether you can tell if `code` breaks it from
1469
- the state alone, without knowing what other files, libraries, services, or
1470
- configuration do beyond what it shows. It yields the `context` score and its
1471
- warning.
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.
1472
1551
 
1473
1552
  **Limits.** Jev reads at most 32k tokens of state and one question together, and
1474
1553
  64k tokens in a request. adhere estimates tokens from the JSON it sends, at
@@ -1483,121 +1562,53 @@ about three bytes a token, and leaves 1k of room for what Jev wraps around it.
1483
1562
  the request too large, in which case the file is skipped and counted in the
1484
1563
  summary. A file with more than 255 code sections is also skipped, and the rest
1485
1564
  of the run goes on.
1486
- - So is a file Jev itself counts as over its context, which text denser than the
1487
- estimate, such as CJK, can cause.
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.
1488
1567
 
1489
1568
  </details>
1490
1569
 
1491
- ## Upgrading
1492
-
1493
- Names and layouts from older versions:
1570
+ ## Compose your AGENTS.md
1494
1571
 
1495
- | Version | What it had | What happens now |
1496
- | ----------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1497
- | before 0.7 | A rule's examples were `reference` and `avoid` in a config, and a fence could be tagged `avoid`. | They still read as `must` and `never`. |
1498
- | through 0.7 | The `effect` preset checked seven conventions a linter checks exactly. | They are left to `@effect/tsgo`. The notes on `effect` under [Presets](#presets) say how to turn them on. |
1499
- | before 0.13 | A rule was any `*.md` file in `.adhere/`, as `.adhere/data/brand-ports.md`. | A `*.md` file outside every rule's directory, but a `README.md`, refuses the run. The refusal says where each such file goes to keep its id, so `adhere-ignore` comments and `overrides` still name it. |
1500
- | before 0.13 | `.adhere/tsconfig.json` included only `config.ts`. | Make its `include` `["**/*.ts"]` to give rules written in TypeScript their types. |
1501
- | 0.13.1 | The workspace's packages had a key of their own, `includeWorkspacePackages: true`. | adhere no longer reads it. Jev reads the workspace's packages by default. |
1502
- | 0.13.1 and before | Prose around a `RULE.md`'s code was ignored. | Jev reads it after the description, as the rule's details, so a rule with prose is judged again once. Keep prose about the rule: why it holds, or where it does not apply. |
1503
- | 0.15.0 and before | A rule named what Jev reads beside its code, in `reads`. | `reads` is the config's, for every rule, and a rule that has it refuses the run: delete it. Workspace context is on by default; compiler context requires `typeAware: true`. See [What Jev reads](#what-jev-reads). |
1504
- | 0.15.0 and before | `--limit` counted rule checks. | It counts files. |
1505
- | 0.14.0 and before | The `effect` preset had `services/test-layers-are-in-memory` and `services/operations-have-no-requirements`. | They are gone, and an `overrides` entry naming either refuses the run: remove it. `@effect/tsgo`'s `leakingRequirements` checks the second. `adhere-ignore` comments naming them suppress nothing. |
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/`.
1506
1576
 
1507
- Configs that previously enabled compiler context with `reads: ["symbols", "references"]`
1508
- must now opt in with `typeAware: true`, or use `adhere lint --type-aware`.
1509
- That switch enables both maps; `reads` only controls workspace context.
1577
+ To run it after installing dependencies, add it to your package scripts, or
1578
+ append it to your existing `postinstall` script:
1510
1579
 
1511
- ## Development
1580
+ ```json
1581
+ {
1582
+ "scripts": {
1583
+ "postinstall": "adhere postinstall"
1584
+ }
1585
+ }
1586
+ ```
1512
1587
 
1513
- From a checkout, with Bun:
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:
1514
1590
 
1515
- ```sh
1516
- bun install
1517
- bun link # puts `adhere` on PATH
1518
- adhere lint --preset effect
1519
- ```
1591
+ ```markdown
1592
+ <!-- adhere:instructions:start -->
1520
1593
 
1521
- - In a checkout no platform package is installed, so `bin/adhere.js` runs
1522
- `src/main.ts` with Bun instead. Type-aware audits use the checkout's own
1523
- TypeScript; the default audit does not start it.
1524
- - `bun run typecheck` runs `tsc` and the linter.
1525
- - `bun run test` runs the tests.
1526
- - `bun scripts/verify-npm.ts`, after `bun run build:npm`, installs the built
1527
- packages in a scratch project and explicitly enables `typeAware: true` to
1528
- check `symbols` and `references`. It needs Node 24 or later, and sends Jev nothing.
1529
-
1530
- ### Native executable
1531
-
1532
- | Command | What it builds |
1533
- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1534
- | `bun run build` | `dist/adhere`, a single binary with Bun and the presets inside it, through Bun's [`--compile`](https://bun.sh/docs/bundler/executables) with `--asset ./presets`. It runs without Bun or `node_modules` on the target machine and still loads the repo's `config.ts` and Markdown rules from disk. It has no TypeScript compiler, so it reads no `symbols` or `references`. |
1535
- | `bun run build:npm` | The same for every published platform, each into its package under `dist/npm/`, with a copy for the GitHub release in `dist/release/` ([`scripts/npm-packages.ts`](./scripts/npm-packages.ts)). |
1536
-
1537
- Each platform's executable is its own npm package,
1538
- `@drkmttr/adhere-<platform>-<arch>`, limited by `os` and `cpu`, and an optional
1539
- dependency of `@drkmttr/adhere`, so an install fetches only its own machine's.
1540
- Each depends on TypeScript's native compiler for its platform,
1541
- `@typescript/typescript-<platform>-<arch>`, pinned to exactly
1542
- `7.1.0-dev.20261009.1` to match the SDK. The package's
1543
- `bin/adhere.js` finds both, and runs the executable with Node. The same
1544
- executables are attached to each GitHub release, named for `uname -s` and
1545
- `uname -m` as in `adhere-Linux-x86_64`, with `LICENSE` and
1546
- `THIRD_PARTY_NOTICES.txt`, which go with any copy you redistribute.
1547
-
1548
- `adhere --version` tells the builds apart:
1549
-
1550
- | Build | What it prints |
1551
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1552
- | A release | Its version, as `adhere v0.10.0`. |
1553
- | `bun run build` | `git describe --tags --dirty` where it was built, as `adhere v0.10.0-16-g9a41328`: 16 commits after v0.10.0, at `9a41328`, ending in `-dirty` when the tree had changes. |
1554
- | From source | The checkout's version, marked `(source)`. |
1555
-
1556
- ### Release
1557
-
1558
- Releases are cut by CI from pushed version tags. From a clean, up-to-date
1559
- `main`, push the release tag:
1594
+ Generated instructions
1560
1595
 
1561
- ```sh
1562
- git tag v0.3.0
1563
- git push origin v0.3.0
1596
+ <!-- adhere:instructions:end -->
1564
1597
  ```
1565
1598
 
1566
- 1. The tag push starts `.github/workflows/release.yaml`, which checks out
1567
- `main`, derives `0.3.0` from `v0.3.0`, and runs
1568
- `bun run release -- --ci 0.3.0`.
1569
- 2. Release-it bumps `package.json`, commits `chore: release v0.3.0` back to
1570
- `main`, skips npm publish, and creates the GitHub Release for the existing
1571
- tag.
1572
- 3. `release.yaml` then calls the reusable publish workflow,
1573
- `.github/workflows/publish.yaml`, directly, avoiding a chained GitHub Release
1574
- event created by `GITHUB_TOKEN`.
1575
- 4. The publish workflow verifies that `package.json` matches the release tag,
1576
- builds the platform packages, attaches their executables to the GitHub
1577
- Release, and publishes each package before `@drkmttr/adhere`, which it first
1578
- lists them in as `optionalDependencies` at the same version.
1579
-
1580
- CI, and the publish workflow before it publishes, run `scripts/verify-npm.ts`
1581
- on Linux and on both Macs. Windows is built, but not run.
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.
1582
1604
 
1583
- <details>
1584
- <summary>More on publishing</summary>
1585
-
1586
- - Every publish is `npm publish --access public --provenance`.
1587
- - No npm token is used, so a new package has to be published once by hand before
1588
- it can be given trusted publishers on npmjs.com.
1589
- - npm checks the workflow that started the run, so every package has two trusted
1590
- publishers: `release.yaml`, which calls the publish workflow for tag releases,
1591
- and `publish.yaml`, for manual runs.
1592
- - The publish workflow can be run manually for an already-created release tag if
1593
- a publish needs to be retried. It skips every package whose version is already
1594
- on npm, and replaces the executables on the release with ones it builds from
1595
- `main`.
1596
- - The main package's `files` list keeps it to `bin/`, `src/`, `presets/`, and
1597
- `skills/`.
1605
+ ## Contributing
1598
1606
 
1599
- </details>
1607
+ To build, test, or release adhere from a checkout, see
1608
+ [CONTRIBUTING.md](.github/CONTRIBUTING.md).
1600
1609
 
1601
1610
  ## License
1602
1611
 
1603
- MIT. See [LICENSE](LICENSE).
1612
+ MIT. See [LICENSE](LICENSE). The executables attached to each GitHub release
1613
+ come with `LICENSE` and `THIRD_PARTY_NOTICES.txt`, which go with any copy you
1614
+ redistribute.