@drkmttr/adhere 0.18.1 → 0.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1099 -1088
- package/package.json +15 -15
- package/presets/README.md +132 -0
- package/skills/adhere/SKILL.md +4 -3
- package/skills/adhere-setup/SKILL.md +11 -10
- package/src/cli.ts +121 -14
- package/src/config.ts +147 -139
- package/src/create/agents.ts +149 -0
- package/src/create/ask.ts +147 -0
- package/src/create/look.ts +140 -0
- package/src/create/wizard.ts +432 -0
- package/src/index.ts +2 -0
- package/src/init.ts +2 -1
- package/src/install.ts +1 -1
- package/src/instructions.ts +76 -0
- package/src/markdown.ts +3 -9
- package/src/rules.ts +42 -45
- package/src/services/AdhereConfig.ts +31 -18
- package/src/services/Credentials.ts +14 -8
- package/src/services/Jev.reads.test.ts +22 -0
- package/src/services/Jev.ts +35 -13
- package/src/wording.ts +1 -1
- package/src/workflows/audit.ts +82 -14
- package/src/workflows/format.ts +97 -0
- package/src/workspaces.test.ts +37 -1
- package/src/workspaces.ts +121 -12
package/README.md
CHANGED
|
@@ -1,142 +1,137 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>adhere</h1>
|
|
3
|
-
<
|
|
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
|
+
|
|
7
|
+
<img src="https://img.shields.io/npm/dm/%40drkmttr%2Fadhere?style=flat&logo=github&labelColor=%23181926&color=%23000000" />
|
|
8
|
+
|
|
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
|
-
|
|
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
|
|
20
|
+
description: A third-party API going offline should not also take our app down.
|
|
11
21
|
---
|
|
12
22
|
|
|
13
|
-
Our APIs should
|
|
14
|
-
side should be unaffected if it goes offline, and ingestion should
|
|
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
|
|
20
|
-
import
|
|
21
|
-
import redis from "redis"
|
|
22
|
-
import cron from "node-cron"
|
|
23
|
-
|
|
24
|
-
// asume this is called by /api/restaurants/:zipcode
|
|
25
|
-
export async function findRestaurants(zipCode: number) {
|
|
26
|
-
const results = db.restaurants.find({ where: { zipCode }})
|
|
27
|
-
return results
|
|
28
|
-
}
|
|
30
|
+
import db from "./lib/db";
|
|
31
|
+
import googleMaps from "./lib/maps";
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const syncItem = item => db.restaurants.upsert(item)
|
|
35
|
-
const next = items.sort((a, b) => b.id < a.id).at(-1)?.timestamp
|
|
36
|
-
const touch = () => redis.set("maps:sync:timestamp", next ?? Date.now())
|
|
33
|
+
// GET /api/restaurants/:zipCode reads only our own database.
|
|
34
|
+
export async function findRestaurants(zipCode: string) {
|
|
35
|
+
return db.restaurants.find({ where: { zipCode } });
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// A scheduled job copies new restaurants in, off the request path.
|
|
39
|
+
export async function syncRestaurants(since: Date) {
|
|
40
|
+
const restaurants = await googleMaps.findAll({ updatedSince: since });
|
|
41
|
+
await Promise.all(restaurants.map((restaurant) => db.restaurants.upsert(restaurant)));
|
|
40
42
|
}
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
##
|
|
45
|
+
## Should not
|
|
44
46
|
|
|
45
47
|
```ts
|
|
46
48
|
import googleMaps from "./lib/maps";
|
|
47
|
-
import db from "./lib/db";
|
|
48
49
|
|
|
49
|
-
//
|
|
50
|
-
export async function findRestaurants(zipCode:
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
return results;
|
|
50
|
+
// GET /api/restaurants/:zipCode
|
|
51
|
+
export async function findRestaurants(zipCode: string) {
|
|
52
|
+
// When Google Maps is down, slow, or over quota, so is this endpoint.
|
|
53
|
+
return googleMaps.findAll({ where: { zipCode } });
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
````
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
the file Jev points at. The report
|
|
62
|
-
|
|
63
|
-
Here's a demo of what the output looks like.
|
|
58
|
+
adhere asks [Jev](https://typesafe.ai), TypeSafe AI's System One model, whether
|
|
59
|
+
each file breaks each rule. Jev answers with a calibrated probability. adhere
|
|
60
|
+
reports each rule whose probability is above a threshold, and shows the part of
|
|
61
|
+
the file Jev points at. The report looks like Oxlint's, the linter `vp lint`
|
|
62
|
+
runs:
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
<summary>
|
|
67
|
-
<h3>Demo 🎬</h3>
|
|
68
|
-
</summary>
|
|
69
|
-
<img alt="demo" align="center" src="https://github.com/darkmatter/adhere/raw/main/eval/demo.gif" />
|
|
70
|
-
</details>
|
|
71
|
-
|
|
72
|
-
Presets for TypeScript, React, security, Effect, and alchemy are included.
|
|
73
|
-
Run them to quickly adhere your agent to recommended rules.
|
|
74
|
-
|
|
75
|
-
```sh
|
|
76
|
-
bun add -g @drkmttr/adhere
|
|
77
|
-
adhere login # save your typesafe api key
|
|
78
|
-
adhere lint --preset typescript,react
|
|
79
|
-
```
|
|
64
|
+

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