@drkmttr/adhere 0.18.1 → 0.23.0

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