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