@drkmttr/adhere 0.9.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +406 -90
- package/package.json +7 -7
- package/presets/alchemy/apis/match-remote-errors-by-tag.md +6 -2
- package/presets/alchemy/apis/schemas-at-trust-boundaries.md +6 -2
- package/presets/alchemy/data/commit-migrations-then-deploy.md +6 -2
- package/presets/alchemy/data/hyperdrive-direct-origin.md +6 -2
- package/presets/alchemy/data/migrations-use-direct-url.md +6 -2
- package/presets/alchemy/data/never-disable-tls-verification.md +6 -2
- package/presets/alchemy/data/private-endpoint-at-runtime.md +6 -2
- package/presets/alchemy/data/single-migration-owner.md +6 -2
- package/presets/alchemy/durable/idempotent-event-handlers.md +6 -2
- package/presets/alchemy/durable/io-inside-steps.md +6 -2
- package/presets/alchemy/durable/no-self-triggering-bucket-writes.md +6 -2
- package/presets/alchemy/durable/stable-step-names.md +6 -2
- package/presets/alchemy/durable/webhook-state-is-truth.md +6 -2
- package/presets/alchemy/providers/diff-guards-unresolved-props.md +6 -2
- package/presets/alchemy/providers/idempotent-delete.md +8 -3
- package/presets/alchemy/providers/lazy-layer-construction.md +6 -2
- package/presets/alchemy/providers/reconcile-from-observed-state.md +6 -2
- package/presets/alchemy/resources/anchor-paths-with-import-meta.md +6 -2
- package/presets/alchemy/resources/idempotent-actions-and-seeds.md +6 -2
- package/presets/alchemy/resources/previews-reference-shared-resources.md +6 -2
- package/presets/alchemy/resources/protect-irreplaceable-data.md +6 -2
- package/presets/alchemy/resources/remote-state-for-ci.md +6 -2
- package/presets/alchemy/resources/stable-logical-ids.md +6 -2
- package/presets/alchemy/resources/stage-unique-physical-names.md +6 -2
- package/presets/alchemy/runtime/bind-tag-not-implementation.md +6 -2
- package/presets/alchemy/runtime/deterministic-config-defaults.md +6 -2
- package/presets/alchemy/runtime/disposables-in-request-scope.md +6 -2
- package/presets/alchemy/runtime/lambda-no-post-response-work.md +6 -2
- package/presets/alchemy/runtime/no-per-request-work-at-construction.md +6 -2
- package/presets/alchemy/runtime/resolve-config-and-bindings-at-construction.md +6 -2
- package/presets/alchemy/secrets/let-alchemy-mint-infra-secrets.md +6 -2
- package/presets/alchemy/secrets/none-in-browser-env.md +6 -2
- package/presets/alchemy/secrets/redacted-until-use.md +6 -2
- package/presets/alchemy/secrets/unwrap-before-compare-or-send.md +6 -2
- package/presets/alchemy/security/authorize-public-endpoints.md +6 -2
- package/presets/alchemy/security/github-secret-not-variable.md +6 -2
- package/presets/alchemy/security/identity-from-verified-session.md +6 -2
- package/presets/alchemy/security/keep-auth-protections-on.md +6 -2
- package/presets/alchemy/security/least-privilege-bindings.md +6 -2
- package/presets/alchemy/security/least-privilege-ci-credentials.md +6 -2
- package/presets/alchemy/security/lookup-failure-is-not-anonymous.md +6 -2
- package/presets/effect/basics/gen-for-sequencing.md +7 -3
- package/presets/effect/basics/instrument-with-pipe.md +6 -2
- package/presets/effect/config/business-logic-depends-on-config-service.md +6 -2
- package/presets/effect/config/secrets-are-redacted.md +6 -2
- package/presets/effect/config/tests-provide-values-directly.md +7 -2
- package/presets/effect/config/validate-with-schema.md +6 -2
- package/presets/effect/data/brand-meaningful-primitives.md +7 -3
- package/presets/effect/data/variants-are-tagged-unions.md +7 -3
- package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
- package/presets/effect/errors/defects-for-bugs.md +6 -2
- package/presets/effect/errors/wrap-external-errors.md +6 -2
- package/presets/effect/services/dependencies-through-layers.md +7 -3
- package/presets/effect/services/fresh-layer-per-test.md +7 -2
- package/presets/effect/services/memoize-parameterized-layers.md +6 -2
- package/presets/effect/services/no-mutable-state.md +6 -2
- package/presets/effect/services/operations-have-no-requirements.md +6 -2
- package/presets/effect/services/test-layers-are-in-memory.md +7 -2
- package/presets/effect/testing/test-clock-for-time.md +7 -2
- package/presets/react/effects/clean-up-what-they-start.md +23 -0
- package/presets/react/effects/event-logic-in-handlers.md +29 -0
- package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
- package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
- package/presets/react/effects/notify-parents-in-handlers.md +29 -0
- package/presets/react/server/no-private-data-to-client.md +23 -0
- package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
- package/presets/react/state/no-contradictory-state.md +17 -0
- package/presets/react/state/no-props-copied-into-state.md +23 -0
- package/presets/react/state/store-ids-not-copies.md +20 -0
- package/presets/security/constant-time-secret-compare.md +22 -0
- package/presets/security/no-secrets-in-output.md +19 -0
- package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
- package/presets/security/parameterized-queries.md +16 -0
- package/presets/typescript/async/concurrent-independent-work.md +23 -0
- package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
- package/presets/typescript/design/no-argument-mutation.md +20 -0
- package/presets/typescript/errors/no-swallowed-errors.md +29 -0
- package/presets/typescript/errors/release-on-every-path.md +29 -0
- package/presets/typescript/testing/assert-outcomes.md +22 -0
- package/presets/typescript/testing/independent-tests.md +41 -0
- package/presets/typescript/types/assertions-only-when-proven.md +20 -0
- package/presets/typescript/types/derive-dont-restate.md +24 -0
- package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
- package/presets/typescript/types/validate-external-data.md +21 -0
- package/skills/adhere/SKILL.md +41 -12
- package/skills/adhere-fix/SKILL.md +180 -0
- package/src/cli.ts +59 -14
- package/src/comments.ts +36 -0
- package/src/config.ts +126 -14
- package/src/contradictions.ts +10 -2
- package/src/excerpt.ts +78 -58
- package/src/highlight.ts +2 -0
- package/src/index.ts +1 -1
- package/src/init.ts +119 -12
- package/src/main.ts +0 -1
- package/src/markdown.ts +83 -14
- package/src/models/Audit.ts +2 -0
- package/src/presets.ts +12 -3
- package/src/rules.ts +36 -3
- package/src/services/AdhereConfig.ts +36 -13
- package/src/services/AuditCache.ts +31 -5
- package/src/services/Jev.http.ts +76 -11
- package/src/services/Jev.ts +191 -73
- package/src/services/SourceWalker.ts +51 -13
- package/src/suppress.ts +119 -0
- package/src/version.ts +25 -1
- package/src/workflows/audit.ts +192 -40
- package/src/workflows/format.ts +71 -24
- package/presets/alchemy/data/parameterized-sql.md +0 -13
- package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -15
- package/presets/effect/basics/external-calls-are-resilient.md +0 -17
package/README.md
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>adhere</h1>
|
|
3
|
+
<sup>A linter for non-deterministic rules. Powered by Typesafe</sup>
|
|
4
|
+
</div>
|
|
2
5
|
|
|
3
|
-
A linter for rules a normal linter cannot check.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
A linter for rules a normal linter cannot check. Describe rules using RFC2119
|
|
7
|
+
language, like "business logic MUST live in services", or "A 3rd-party API going down
|
|
8
|
+
SHOULD NOT also take us down". A rule also includes a good example, a bad example, or
|
|
9
|
+
both (which is highly recommended according to [our studies](eval/studies/example-count.md).)
|
|
10
|
+
|
|
11
|
+
For each source file, adhere asks [Jev](https://typesafe.ai) (TypeSafe AI's System One
|
|
7
12
|
model) whether the file breaks each rule, gets a calibrated probability per
|
|
8
|
-
rule, and reports the ones above a threshold with the
|
|
9
|
-
report uses the same frame as `vp lint`.
|
|
13
|
+
rule, and reports the ones above a threshold with the section of the file Jev
|
|
14
|
+
points at. The report uses the same frame as `vp lint`.
|
|
15
|
+
|
|
16
|
+
Here's a demo of what the output looks like:
|
|
17
|
+
|
|
18
|
+
<a href="https://asciinema.org/a/1266511" target="_blank"><img width="400" src="https://asciinema.org/a/1266511.svg" /></a>
|
|
19
|
+
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
Rules a normal linter can check exactly, such as a banned import or a type
|
|
22
|
+
error, belong in that linter. While it lints, adhere asks Jev whether a normal
|
|
23
|
+
linter could check each of your rules, and [`adhere validate`](#validate) lists
|
|
24
|
+
rules that it thinks belong in a regular linter, and also detects any
|
|
25
|
+
contradictions in your rules.
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
Presets for TypeScript, React, security, Effect, and alchemy are included, and run without setup:
|
|
15
28
|
|
|
16
29
|
```sh
|
|
17
|
-
TYPESAFE_API_KEY=xxx npx @drkmttr/adhere lint --preset
|
|
30
|
+
TYPESAFE_API_KEY=xxx npx @drkmttr/adhere lint --preset typescript
|
|
18
31
|
```
|
|
19
32
|
|
|
20
33
|
## Install
|
|
@@ -27,7 +40,7 @@ To pin the version in a repo, for CI or scripts, add it as a dev dependency
|
|
|
27
40
|
and run `npx adhere`.
|
|
28
41
|
|
|
29
42
|
`adhere` is a prebuilt executable for macOS and Linux on arm64 and x64 and for
|
|
30
|
-
Windows on x64.
|
|
43
|
+
Windows on x64.
|
|
31
44
|
|
|
32
45
|
adhere sends each file it judges to Jev at `api.typesafe.ai`, authenticated
|
|
33
46
|
with a TypeSafe AI API key: the one `adhere login` saved, or
|
|
@@ -36,7 +49,7 @@ with a TypeSafe AI API key: the one `adhere login` saved, or
|
|
|
36
49
|
## Quick start
|
|
37
50
|
|
|
38
51
|
```sh
|
|
39
|
-
adhere init # .adhere/config.ts
|
|
52
|
+
adhere init # .adhere/config.ts, two example rules, and the dependency
|
|
40
53
|
adhere login # save your TypeSafe AI API key, once
|
|
41
54
|
adhere validate # Checks your rules' wording, and contradictions using Jev
|
|
42
55
|
adhere lint # audit the working directory
|
|
@@ -49,6 +62,9 @@ built-in Effect rules. A finding looks like this:
|
|
|
49
62
|
```text
|
|
50
63
|
× data/brand-ports (0.93): A port must be a branded, range-checked integer, never a bare number.
|
|
51
64
|
╭─[src/server.ts:6:3]
|
|
65
|
+
1 │ import { Effect } from "effect";
|
|
66
|
+
2 │ import { listen } from "./listen.ts";
|
|
67
|
+
3 │
|
|
52
68
|
4 │ export const serve = Effect.gen(function* () {
|
|
53
69
|
5 │ const host = process.env.HOST ?? "localhost";
|
|
54
70
|
6 │ const port: number = Number(process.env.PORT ?? 3000);
|
|
@@ -68,17 +84,45 @@ Found 1 error.
|
|
|
68
84
|
|
|
69
85
|
The header is the rule id, Jev's probability, and the rule's description. A
|
|
70
86
|
preset's rule has the preset's name first, as in
|
|
71
|
-
`effect/basics/
|
|
87
|
+
`effect/basics/gen-for-sequencing`, so a report that mixes presets
|
|
72
88
|
with a repo's own rules says where each came from.
|
|
73
|
-
Under it is the
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
Under it is the section of the file Jev points at, with the line it names
|
|
90
|
+
underlined, or the lines, when it names several, marked with a bar beside the
|
|
91
|
+
code. adhere splits each file into sections of whole statements, such as
|
|
92
|
+
its imports, constants, and functions, packed in order into runs of at most 30
|
|
93
|
+
lines; a class or function longer than that splits into its methods or the
|
|
94
|
+
statements of its body. Jev reads the file as these sections and chooses one,
|
|
95
|
+
and in the same request the line in it where the violation starts and the one
|
|
96
|
+
where it ends: on the eval it judged as well that way, and the lines it named
|
|
97
|
+
held a violation for 94% of real findings, most often as one line. When it
|
|
98
|
+
names no lines, the section is shown alone, at its first line.
|
|
99
|
+
|
|
100
|
+
Jev is also asked whether the file shows enough to decide the rule at all:
|
|
101
|
+
whether it turns on something the file does not show, such as what another
|
|
102
|
+
file, a library, or a service does. When Jev's probability that it does is
|
|
103
|
+
below 0.6, the finding gets a warning under its code, with the probability
|
|
104
|
+
that it does not, and help on adding what is missing:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
warning: this file may not show enough to check this rule (Jev: 0.55 that it does not)
|
|
108
|
+
help: add what the code relies on outside this file as a note Jev reads:
|
|
109
|
+
// @adhere <the fact>, and how you know it
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
On the eval, most findings with this warning were false, where about one in
|
|
113
|
+
five of all findings was: check what the code relies on outside the file
|
|
114
|
+
before acting on it. What you find can go in an `@adhere` note, which Jev
|
|
115
|
+
reads (see [Comments](#comments)). `sufficiencyThreshold` in the config, or `--sufficiency-threshold`, sets
|
|
116
|
+
the cutoff. The warning changes neither the finding's level nor the exit code.
|
|
117
|
+
|
|
118
|
+
The hint is the rule's code that must be written; a rule with only code that must
|
|
119
|
+
never be written shows that code, labeled `never:`, instead. A rule whose
|
|
120
|
+
level is `warning` reports its findings with `⚠` in place of `×`, in amber. On
|
|
121
|
+
a terminal, the report is in color and the code in it is highlighted. The exit
|
|
122
|
+
code is 1 when an error is reported, and 0 when nothing is, or only warnings;
|
|
123
|
+
`--deny-warnings` fails the run on warnings too. It is also 1 when the run
|
|
124
|
+
refuses, for example on an invalid config or rule file, a missing API key, or
|
|
125
|
+
an unknown command or flag; a refusal prints its reason.
|
|
82
126
|
|
|
83
127
|
## Usage
|
|
84
128
|
|
|
@@ -86,10 +130,12 @@ refusal prints its reason.
|
|
|
86
130
|
adhere lint # audit the working directory
|
|
87
131
|
adhere lint --preset effect # add a built-in rule set; the config becomes optional
|
|
88
132
|
adhere lint --threshold 0.9 # replace the config's threshold; per-rule thresholds still apply
|
|
133
|
+
adhere lint --sufficiency-threshold 0.5 # warn on fewer findings that may turn on other files
|
|
89
134
|
adhere lint --yes # send the requests without asking first
|
|
90
135
|
adhere lint --limit 500 # judge at most 500 checks; the rest wait for the next run
|
|
91
136
|
adhere lint --rpm 30 # send at most 30 requests a minute
|
|
92
137
|
adhere lint --filter 'src/**' # read only the files a glob matches
|
|
138
|
+
adhere lint --deny-warnings # fail on warnings as well as errors
|
|
93
139
|
adhere lint --log-level debug # log each request to Jev on stderr
|
|
94
140
|
adhere validate # check the rules' wording; ask Jev whether any contradict
|
|
95
141
|
adhere init [--force] # scaffold .adhere/config.ts and two example rules
|
|
@@ -121,15 +167,10 @@ Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, a
|
|
|
121
167
|
The cost is adhere's estimate of the input tokens times the model's price.
|
|
122
168
|
Jev charges only for input tokens: $0.042 a million for `jev-latest`, per
|
|
123
169
|
[TypeSafe AI's models page](https://docs.typesafe.ai/models) in September 2026.
|
|
124
|
-
|
|
125
|
-
what judging finds, comes on top. For a model adhere has no price for, the plan
|
|
126
|
-
gives the tokens alone.
|
|
170
|
+
For a model adhere has no price for, the plan gives the tokens alone.
|
|
127
171
|
|
|
128
172
|
It asks only with a terminal on stdin and stdout, and never when the cache
|
|
129
|
-
answers every check; `--yes` sends without asking.
|
|
130
|
-
stderr shows the files done, the requests sent, and the findings so far, and
|
|
131
|
-
the report goes to stdout. A request Jev refuses stops the run with the file,
|
|
132
|
-
the HTTP status, and Jev's reason. Files finished before it are cached, so
|
|
173
|
+
answers every check; `--yes` sends without asking. Files finished before it are cached, so
|
|
133
174
|
running again continues from there.
|
|
134
175
|
|
|
135
176
|
The firewall in front of Jev's API can refuse a request whose code reads to
|
|
@@ -147,13 +188,61 @@ next run with the same limit picks up where this one stopped. `--limit 0` shows
|
|
|
147
188
|
the plan and judges nothing. `--rpm <requests>` sends at most that many requests
|
|
148
189
|
to Jev a minute, evenly spaced, retries included, and the plan says about how
|
|
149
190
|
long they take. `--filter <glob>` reads only the files whose path from the
|
|
150
|
-
working directory matches, such as `src/**` or `**/*.service.ts
|
|
151
|
-
|
|
191
|
+
working directory matches, such as `src/**` or `**/*.service.ts` (if using globs,
|
|
192
|
+
wrap with single quotes to avoid expansion); repeat it for more, and start a
|
|
193
|
+
pattern with `!` to leave out what it matches. Together:
|
|
152
194
|
|
|
153
195
|
```sh
|
|
154
196
|
adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 200 --rpm 30
|
|
155
197
|
```
|
|
156
198
|
|
|
199
|
+
### Comments
|
|
200
|
+
|
|
201
|
+
Jev reads a file's code, not its comments. A comment changes nothing the code
|
|
202
|
+
does, so it should not change the report, yet Jev takes what comments claim at
|
|
203
|
+
their word: in a study of one alchemy rule, comments saying a delete succeeds
|
|
204
|
+
on a missing resource hid 8 of its 9 real violations, and taking comments out
|
|
205
|
+
found all nine. adhere takes every comment out before it hashes or sends a
|
|
206
|
+
file, blanking it so every line keeps its number; the report's excerpts still
|
|
207
|
+
show them.
|
|
208
|
+
|
|
209
|
+
A comment that says `@adhere` anywhere in it is a note for Jev, and stays.
|
|
210
|
+
Write one for a fact the code relies on that the file cannot show, once you
|
|
211
|
+
have checked it, with how you know:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
/**
|
|
215
|
+
* Deletes the activity. @adhere DeleteActivity succeeds on a missing
|
|
216
|
+
* activity, so no not-found error needs catching; probed 2026-09-25.
|
|
217
|
+
*/
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A note informs Jev; it suppresses nothing, and Jev still judges the code
|
|
221
|
+
around it. `includeComments: true` in the config sends every comment, for a
|
|
222
|
+
project whose rules are about comments, such as doc comments on exports.
|
|
223
|
+
|
|
224
|
+
### Suppressing a finding
|
|
225
|
+
|
|
226
|
+
A finding Jev got wrong is suppressed in the code, with a comment saying why:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// adhere-ignore alchemy/providers/idempotent-delete -- DeleteActivity succeeds on a missing activity; probed 2026-09-25
|
|
230
|
+
delete: Effect.fn(function* ({ output }) {
|
|
231
|
+
yield* sfn.deleteActivity({ activityArn: output.activityArn });
|
|
232
|
+
}),
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
On a line of its own, `adhere-ignore` covers the statement that starts on the
|
|
236
|
+
next line of code: here the whole handler, wherever in it Jev points, since the
|
|
237
|
+
lines it points at can move between runs. A finding without lines is covered
|
|
238
|
+
when its section overlaps the statement. At the end of a line of code, it
|
|
239
|
+
covers the statement that starts on that line. `adhere-ignore-file`, anywhere
|
|
240
|
+
in a file, covers the whole file, and the rules it names are not judged there
|
|
241
|
+
at all. A comment names rules as a report does, a preset's with its preset
|
|
242
|
+
first, separated by commas; what follows `--` is the reason, for whoever
|
|
243
|
+
reads the code next. Jev never reads these comments, even with
|
|
244
|
+
`includeComments: true`, and the summary counts the findings they suppress.
|
|
245
|
+
|
|
157
246
|
### Logging a run
|
|
158
247
|
|
|
159
248
|
`--log-level debug` logs a run's work on stderr: the config it loaded, how many
|
|
@@ -172,21 +261,52 @@ adhere lint --log-level debug 2> adhere.log
|
|
|
172
261
|
|
|
173
262
|
### What gets read
|
|
174
263
|
|
|
175
|
-
`adhere lint` reads the
|
|
176
|
-
`.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`
|
|
180
|
-
`.alchemy/`, or `.vite/` is
|
|
181
|
-
|
|
264
|
+
`adhere lint` reads the TypeScript files under the working directory, `.ts`,
|
|
265
|
+
`.tsx`, `.mts`, and `.cts`, except declaration files such as `.d.ts` and
|
|
266
|
+
config files. When the working directory contains `agents/`, `apps/`, or
|
|
267
|
+
`packages/`, only those trees are read. Below the working directory, anything
|
|
268
|
+
under `node_modules/`, `dist/`, `coverage/`, `vendor/`, `e2e/`, `references/`,
|
|
269
|
+
`.adhere/`, `.agents/`, `.claude/`, `.direnv/`, `.alchemy/`, or `.vite/` is
|
|
270
|
+
skipped; the directories above it do not count. JavaScript files are not read,
|
|
271
|
+
and `.gitignore` is not consulted.
|
|
272
|
+
|
|
273
|
+
Tests are judged only by rules about tests. A test is a `.test` or `.spec`
|
|
274
|
+
file, such as `a.test.ts` or `Button.spec.tsx`; a fixture or test helper named
|
|
275
|
+
as one, such as `fixtures.ts`, `GitHubHttpFixtures.ts`,
|
|
276
|
+
`ledger-copy.fixture.ts`, `linear-test-helpers.ts`, or `test-utils.ts`; or any
|
|
277
|
+
file under a `test/`, `tests/`, `__tests__/`,
|
|
278
|
+
`testing/`, `fixtures/`, `fixture/`, `__fixtures__/`, or `__mocks__/`
|
|
279
|
+
directory, and only a rule whose front matter says `tests` judges
|
|
280
|
+
one (see [Rules as Markdown files](#rules-as-markdown-files)). On alchemy, 38%
|
|
281
|
+
of the findings from rules not about tests were in test helpers and fixtures.
|
|
282
|
+
|
|
283
|
+
A config's `exclude` lists globs, relative to the working directory, of files
|
|
284
|
+
no rule judges, such as generated code. It leaves them out of every run, as
|
|
285
|
+
`--filter '!<glob>'` leaves them out of one:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
export default defineConfig({ exclude: ["**/generated/**"] });
|
|
289
|
+
```
|
|
182
290
|
|
|
183
291
|
### Init
|
|
184
292
|
|
|
185
|
-
`adhere init` writes `.adhere/config.ts
|
|
186
|
-
|
|
293
|
+
`adhere init` writes `.adhere/config.ts`, the `.adhere/tsconfig.json` that gives
|
|
294
|
+
the editor its types (see [Config](#config)), and two example rules. It is safe
|
|
295
|
+
to rerun: by default it reports existing files as skipped and does not clobber
|
|
187
296
|
them. `--force` overwrites them. A config already at another accepted path is
|
|
188
297
|
kept, and no second one is added, even with `--force`.
|
|
189
298
|
|
|
299
|
+
When there is a `package.json`, init also adds `@drkmttr/adhere` to its
|
|
300
|
+
`devDependencies`, so the config's `defineConfig` has its types in the editor. It runs
|
|
301
|
+
`<manager> add -D @drkmttr/adhere` with the package manager whose lockfile
|
|
302
|
+
(`bun.lock`, `bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`)
|
|
303
|
+
sits beside the `package.json`, or npm when there is none. With pnpm it adds
|
|
304
|
+
`--ignore-workspace-root-check`, so the install also works at a workspace root,
|
|
305
|
+
and `--config.strict-dep-builds=false`, so pnpm 11 skips the build script of
|
|
306
|
+
msgpackr-extract, an optional native add-on that comes with `effect` and that
|
|
307
|
+
adhere never uses, with a warning instead of failing.
|
|
308
|
+
It skips the install when `package.json` already lists adhere.
|
|
309
|
+
|
|
190
310
|
### Validate
|
|
191
311
|
|
|
192
312
|
`adhere validate` loads the config, rule files, and presets the way `lint`
|
|
@@ -252,12 +372,15 @@ repo being audited, at one of these paths (keep one):
|
|
|
252
372
|
- `.adhere.config.ts`
|
|
253
373
|
|
|
254
374
|
```ts
|
|
255
|
-
import
|
|
375
|
+
import { defineConfig } from "@drkmttr/adhere";
|
|
256
376
|
|
|
257
|
-
export default {
|
|
377
|
+
export default defineConfig({
|
|
258
378
|
model: "jev-latest", // optional, default "jev-latest"
|
|
259
379
|
threshold: 0.8, // optional, default 0.8
|
|
380
|
+
sufficiencyThreshold: 0.6, // optional, default 0.6: below it, a finding warns to check outside the file
|
|
260
381
|
presets: ["effect"], // optional, built-in rule sets
|
|
382
|
+
exclude: ["**/generated/**"], // optional, files no rule judges
|
|
383
|
+
overrides: { "effect/basics/instrument-with-pipe": "off" }, // optional, see Presets
|
|
261
384
|
rules: {
|
|
262
385
|
"data/brand-meaningful-primitives": {
|
|
263
386
|
description:
|
|
@@ -270,16 +393,24 @@ type UserId = typeof UserId.Type
|
|
|
270
393
|
threshold: 0.8, // optional per-rule override
|
|
271
394
|
},
|
|
272
395
|
},
|
|
273
|
-
}
|
|
396
|
+
});
|
|
274
397
|
```
|
|
275
398
|
|
|
276
399
|
The default export is decoded with Effect `Schema`. An invalid shape refuses
|
|
277
|
-
the run.
|
|
278
|
-
|
|
279
|
-
supplies
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
400
|
+
the run. `defineConfig` types the config for the editor and returns it as it
|
|
401
|
+
is; `satisfies Config`, with `import type { Config }`, does the same. The
|
|
402
|
+
executable supplies `@drkmttr/adhere` to the config whether or not the repo has
|
|
403
|
+
the package installed, and the editor's types come from the package, which
|
|
404
|
+
`adhere init` adds as a dev dependency. A config can import other files by
|
|
405
|
+
relative path, but no packages besides `@drkmttr/adhere`: the executable does
|
|
406
|
+
not resolve `node_modules`.
|
|
407
|
+
|
|
408
|
+
A tsconfig's globs skip dot directories, so the editor opens
|
|
409
|
+
`.adhere/config.ts` outside any project, where it cannot resolve the package's
|
|
410
|
+
types: they are reached only by `bundler`, `node16`, or `nodenext` resolution.
|
|
411
|
+
`adhere init` writes `.adhere/tsconfig.json`, a project for the config alone
|
|
412
|
+
with `bundler` resolution. For a config written by hand, add that file, or
|
|
413
|
+
include `.adhere/config.ts` in a tsconfig that resolves the same way.
|
|
283
414
|
|
|
284
415
|
`rules` in the config, inline as above or as a directory (below), replaces the
|
|
285
416
|
`.adhere/` rule files: none of them, root or nested, is read then.
|
|
@@ -291,12 +422,19 @@ config and the cache (neither is read as a rule). The path without `.md` is the
|
|
|
291
422
|
rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
|
|
292
423
|
directory exists, it is read without any config.
|
|
293
424
|
|
|
294
|
-
A file is front matter, then a body. The body's code goes in fences
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
425
|
+
A file is front matter, then a body. The body's code goes in fences under
|
|
426
|
+
headings of RFC 2119's words: `## Must` for code that must be written, and
|
|
427
|
+
`## Never` for code that must never be, which is what a violation looks like.
|
|
428
|
+
A heading names the code in its section, which runs to the next heading at its
|
|
429
|
+
level or higher, so a deeper heading such as `### A bare number` stays inside
|
|
430
|
+
it. Only a heading that is the word alone names code, in any case and with or
|
|
431
|
+
without a colon: `## Never:` does, `## Never do this` does not. Prose around
|
|
432
|
+
the code renders on GitHub and is ignored.
|
|
433
|
+
|
|
434
|
+
A fence can instead name its code after its language, as in `ts never`, which
|
|
435
|
+
GitHub does not show; a fence's own word wins over its heading's. An untagged
|
|
436
|
+
fence outside those sections is code that must be written, and without a fence
|
|
437
|
+
the whole body is. `avoid`, the tag before 0.7, reads as `never`.
|
|
300
438
|
|
|
301
439
|
````md
|
|
302
440
|
---
|
|
@@ -306,22 +444,31 @@ threshold: 0.8
|
|
|
306
444
|
|
|
307
445
|
Why: a bare `number` accepts 70000 and -1.
|
|
308
446
|
|
|
309
|
-
|
|
447
|
+
## Must
|
|
448
|
+
|
|
449
|
+
```ts
|
|
310
450
|
const Port = Schema.Int.pipe(
|
|
311
451
|
Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
|
|
312
452
|
Schema.brand("Port"),
|
|
313
453
|
);
|
|
314
454
|
```
|
|
315
455
|
|
|
316
|
-
|
|
456
|
+
## Never
|
|
457
|
+
|
|
458
|
+
```ts
|
|
317
459
|
const port: number = Number(process.env.PORT);
|
|
318
460
|
```
|
|
319
461
|
````
|
|
320
462
|
|
|
321
463
|
`description` is required, with code under `must`, `never`, or both.
|
|
322
|
-
`threshold` is optional
|
|
323
|
-
|
|
324
|
-
|
|
464
|
+
`threshold` is optional, and so is `tests`, for a rule that judges tests, which
|
|
465
|
+
rules otherwise skip: `tests: only` for a rule about tests, which judges
|
|
466
|
+
nothing else, and `tests: include` for one that holds in tests as well. So is
|
|
467
|
+
`level`: `level: warning` for a nit, or for a rule that tends to flag code
|
|
468
|
+
wrongly, reports its findings as warnings, which do not fail the run. A rule
|
|
469
|
+
inline in a config takes the same `tests` and `level`. A file that fails validation refuses
|
|
470
|
+
the run with its path in the message. `loadRules(directory)` from the package
|
|
471
|
+
root does the same load for your own tooling.
|
|
325
472
|
|
|
326
473
|
Jev reads the code under the same words, and is asked whether the file
|
|
327
474
|
diverges from the pattern `must` shows, with `never` as an example of
|
|
@@ -334,7 +481,7 @@ dropped. Neither goes in the other's place: code that must never be written,
|
|
|
334
481
|
under `must`, reads to Jev as the pattern to follow.
|
|
335
482
|
|
|
336
483
|
A rule that is a guideline rather than a requirement says "should" instead:
|
|
337
|
-
in its description, and in
|
|
484
|
+
in its description, and in `## Should` and `## Should not` headings (`should`
|
|
338
485
|
and `shouldNot` inline in a config). Jev then reads the code under `should`
|
|
339
486
|
and `should_not`. A rule is one or the other, so it cannot mix `must` or
|
|
340
487
|
`never` with `should` or `should not`. In a config, `reference` and `avoid`,
|
|
@@ -360,18 +507,45 @@ cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
|
|
|
360
507
|
documentation can point `rules` at `docs/adhere/`:
|
|
361
508
|
|
|
362
509
|
```ts
|
|
363
|
-
export default { presets: ["effect"], rules: "./docs/adhere" }
|
|
510
|
+
export default defineConfig({ presets: ["effect"], rules: "./docs/adhere" });
|
|
364
511
|
```
|
|
365
512
|
|
|
366
513
|
Rules read through `rules` apply project-wide.
|
|
367
514
|
|
|
515
|
+
### Scoping a rule
|
|
516
|
+
|
|
517
|
+
A rule can say where it applies, apart from what it asks for, with
|
|
518
|
+
`appliesTo` and `excludeIf`: each a list of descriptions of code, written in
|
|
519
|
+
front matter as a JSON array on one line, or as an array inline in a config.
|
|
520
|
+
|
|
521
|
+
```md
|
|
522
|
+
---
|
|
523
|
+
description: Structured variants must be Schema.TaggedClass members of a Schema.Union, never hand-written object types joined by a tag field.
|
|
524
|
+
appliesTo: ["a declared union type or schema"]
|
|
525
|
+
excludeIf: ["a type that mirrors a third-party format whose tag key that format fixes, such as a Slack Block Kit block"]
|
|
526
|
+
---
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Each goes to Jev as a question of its own, in the same request as the rule,
|
|
530
|
+
about the code that breaks the rule rather than the whole file: for
|
|
531
|
+
`appliesTo`, whether any of that code is as described, and for `excludeIf`,
|
|
532
|
+
whether all of it is. A finding stands only when Jev says yes, above 0.5, to
|
|
533
|
+
every `appliesTo` and to no `excludeIf`, so a file with a real violation
|
|
534
|
+
beside code an `excludeIf` describes keeps its finding. The scores are cached
|
|
535
|
+
with the rule's judgment, and editing a matcher judges the rule again, as
|
|
536
|
+
editing its description does. At `--log-level debug`, lint logs each finding
|
|
537
|
+
its matchers dropped, with their scores.
|
|
538
|
+
|
|
539
|
+
Keeping the scope out of the description keeps the description to what the
|
|
540
|
+
rule asks for, which the report shows, and gives each part a score of its own.
|
|
541
|
+
|
|
368
542
|
### Rule writing tips
|
|
369
543
|
|
|
370
544
|
We've evaluated different ways of giving Jev a rule, to catch the most
|
|
371
545
|
violations with the fewest false positives. In general:
|
|
372
546
|
|
|
373
547
|
- Say "must" for what code must do and "never" for what it must not, in the
|
|
374
|
-
description and
|
|
548
|
+
description and in the headings over the code.
|
|
375
549
|
- Give one example of each: one `must` block and one `never` block. Three of
|
|
376
550
|
each did no better, and several of one kind alone did worse.
|
|
377
551
|
- For a guideline rather than a requirement, say "should" and "should not"
|
|
@@ -389,13 +563,30 @@ A preset has the shape of a config without `presets`: `rules`, and optionally
|
|
|
389
563
|
command line with `--preset`, repeated or separated by commas, as in
|
|
390
564
|
`--preset effect,alchemy`, in which case the config file is optional. Presets
|
|
391
565
|
named in both places apply together. Reports name a preset's rule with the
|
|
392
|
-
preset first, as in `alchemy/secrets/…`. There are
|
|
393
|
-
|
|
566
|
+
preset first, as in `alchemy/secrets/…`. There are five:
|
|
567
|
+
|
|
568
|
+
- `typescript`: 11 rules in [`presets/typescript/`](./presets/typescript/) for
|
|
569
|
+
any TypeScript project, drawn from the TypeScript Handbook and the Google
|
|
570
|
+
TypeScript Style Guide: data from outside checked at runtime, invalid states
|
|
571
|
+
unrepresentable, errors never swallowed, resources released on every path,
|
|
572
|
+
arguments not mutated, and tests that assert outcomes and stand alone.
|
|
573
|
+
- `react`: 10 rules in [`presets/react/`](./presets/react/) for components
|
|
574
|
+
and hooks, lifted from react.dev, mostly
|
|
575
|
+
[You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
|
576
|
+
and [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure):
|
|
577
|
+
logic for an event in its handler rather than an effect, effects that clean
|
|
578
|
+
up and ignore stale fetches, `useSyncExternalStore` for outside stores, state
|
|
579
|
+
that neither copies props nor contradicts itself, and Server Functions and
|
|
580
|
+
Server Components that guard what crosses to the client.
|
|
581
|
+
- `security`: 4 rules in [`presets/security/`](./presets/security/), drawn
|
|
582
|
+
from OWASP's cheat sheets: secrets kept out of logs, error messages, and
|
|
583
|
+
responses; parameterized SQL; no untrusted input in shell commands, `eval`,
|
|
584
|
+
or file paths; and secrets compared in constant time. It has no topics.
|
|
394
585
|
- `effect`: Markdown rules in [`presets/effect/`](./presets/effect/), lifted
|
|
395
586
|
from the [effect-solutions](https://github.com/kitlangton/effect-solutions)
|
|
396
587
|
docs and the
|
|
397
588
|
[effect/platform](https://effect.website/docs/platform/introduction/) docs.
|
|
398
|
-
- `alchemy`:
|
|
589
|
+
- `alchemy`: 41 rules in [`presets/alchemy/`](./presets/alchemy/) for code that
|
|
399
590
|
deploys with [alchemy](https://alchemy.run), lifted from its docs and blog:
|
|
400
591
|
where Config and bindings are read, which resources keep their data, how
|
|
401
592
|
secrets stay out of bundles and logs, authorization on public URLs,
|
|
@@ -407,11 +598,36 @@ of its own: `--preset effect/basics` applies only the rules under
|
|
|
407
598
|
A topic's rules keep the ids they have in the whole preset, so a topic and its
|
|
408
599
|
preset share cached judgments, and naming both applies each rule once.
|
|
409
600
|
|
|
601
|
+
A config's `overrides` changes a rule by the id a report names it with, a
|
|
602
|
+
preset's with the preset first, without copying its text into `rules`: `off`
|
|
603
|
+
drops it, `warning` or `error` sets its level, and `{ level, threshold }` sets
|
|
604
|
+
either. An id that no preset or project rule has refuses the run, so a typo
|
|
605
|
+
does not go unnoticed; a rule of a preset the run does not name is accepted.
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
export default defineConfig({
|
|
609
|
+
presets: ["effect", "alchemy"],
|
|
610
|
+
overrides: {
|
|
611
|
+
"alchemy/providers/idempotent-delete": { level: "warning", threshold: 0.9 },
|
|
612
|
+
"effect/basics/instrument-with-pipe": "off",
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`effect`'s four rules about tests, `testing/test-clock-for-time`,
|
|
618
|
+
`services/fresh-layer-per-test`, `services/test-layers-are-in-memory`, and
|
|
619
|
+
`config/tests-provide-values-directly`, say `tests: only`: they judge tests and
|
|
620
|
+
nothing else, and every other preset rule skips tests.
|
|
621
|
+
|
|
622
|
+
`alchemy`'s `providers/idempotent-delete` reports warnings. Whether a delete
|
|
623
|
+
fails on a resource that is already gone is a fact about the API, which the
|
|
624
|
+
file does not show, so the rule also flags deletes of APIs that succeed
|
|
625
|
+
anyway; see [the study](eval/studies/idempotent-delete.md).
|
|
626
|
+
|
|
410
627
|
A preset rule says "must" only where its source makes a requirement, and
|
|
411
|
-
"should" where the source gives advice. In `effect`,
|
|
412
|
-
guidelines:
|
|
413
|
-
|
|
414
|
-
tests; and tests provide config through a layer. Config validation accepts
|
|
628
|
+
"should" where the source gives advice. In `effect`, two rules are
|
|
629
|
+
guidelines: test layers are in memory, outside integration tests, and tests
|
|
630
|
+
provide config through a layer. Config validation accepts
|
|
415
631
|
`Config.mapOrFail` as well as `Config.schema`, and the variants rule does not
|
|
416
632
|
rule out a `switch`. The rule that a command handler only parses input is
|
|
417
633
|
gone: the docs show that pattern but do not ask for it.
|
|
@@ -450,17 +666,56 @@ whose types have no requirements to find. The preset's
|
|
|
450
666
|
`services/dependencies-through-layers` asks for that, and
|
|
451
667
|
`services/operations-have-no-requirements` for the style it goes with.
|
|
452
668
|
|
|
669
|
+
`typescript` leaves out what typescript-eslint and ESLint check exactly:
|
|
670
|
+
unhandled promises (`no-floating-promises`), throwing non-errors
|
|
671
|
+
(`only-throw-error`), `any` (`no-explicit-any`), exhaustive switches
|
|
672
|
+
(`switch-exhaustiveness-check`), a lost `cause` (`preserve-caught-error`), and
|
|
673
|
+
empty catch blocks (`no-empty`). Four of its rules report warnings. Three are
|
|
674
|
+
advice that code often has reason to set aside: assertions that claim only
|
|
675
|
+
what the code established, types derived rather than restated, and
|
|
676
|
+
independent awaits run concurrently. The fourth, that HTTP requests carry a
|
|
677
|
+
timeout, was right on fewer than half its findings above 0.8 on the eval:
|
|
678
|
+
many of the rest were calls through a client with a default timeout that the
|
|
679
|
+
file does not show.
|
|
680
|
+
Two rules the eval found mostly wrong are gone: that tests use fake time
|
|
681
|
+
rather than real waits, and that importing a module does no work.
|
|
682
|
+
|
|
683
|
+
Where effect or alchemy had a rule that `typescript` or `security` has, that
|
|
684
|
+
one is kept and the other is gone: typescript's
|
|
685
|
+
`async/network-calls-have-timeouts` replaced effect's
|
|
686
|
+
`basics/external-calls-are-resilient`, and `security/parameterized-queries`
|
|
687
|
+
and `security/no-secrets-in-output` replaced alchemy's
|
|
688
|
+
`data/parameterized-sql` and `secrets/never-logged-returned-or-output`. A
|
|
689
|
+
project on Effect or alchemy names those presets too, for those rules.
|
|
690
|
+
|
|
691
|
+
`react` leaves out what the React Compiler's lint rules check, in
|
|
692
|
+
[eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks)'s
|
|
693
|
+
recommended set and in Oxlint: pure render (`purity`), props and state never
|
|
694
|
+
mutated (`immutability`), refs not read during render (`refs`), no `setState`
|
|
695
|
+
in render or synchronously in an effect (`set-state-in-render`,
|
|
696
|
+
`set-state-in-effect`), which covers state derived in an effect, and no
|
|
697
|
+
component defined inside another (`static-components`). Its rules judge `.ts`
|
|
698
|
+
files as well as `.tsx`, and apply only to components, hooks, effects, and
|
|
699
|
+
Server Functions, so other files cost their checks and find nothing.
|
|
700
|
+
`server/no-private-data-to-client` reports warnings: a file does not show
|
|
701
|
+
whether the component it passes a record to is a Client Component. So does
|
|
702
|
+
`state/store-ids-not-copies`, which also flags state that copies a string or
|
|
703
|
+
an option from a fixed list, where a copy cannot go stale.
|
|
704
|
+
|
|
453
705
|
### Precedence
|
|
454
706
|
|
|
455
707
|
Highest first: `--threshold` on the command line, the config file, presets in
|
|
456
708
|
order (a later preset wins), then the defaults `jev-latest` and `0.8`. A rule
|
|
457
709
|
in `rules` replaces a preset rule with the same id. A rule's own `threshold`
|
|
458
|
-
beats all of the above for that rule
|
|
710
|
+
beats all of the above for that rule, and an entry in `overrides` beats the
|
|
711
|
+
rule's own `threshold` and `level`. `--sufficiency-threshold` beats the
|
|
712
|
+
config's `sufficiencyThreshold`, which beats the default, `0.6`.
|
|
459
713
|
|
|
460
714
|
## How a file is judged
|
|
461
715
|
|
|
462
|
-
1. One Jev request per file. The state is the file's
|
|
463
|
-
|
|
716
|
+
1. One Jev request per file. The state is the file's sections (see the
|
|
717
|
+
report, above) under their numbers from 1, as `{ "1": "…", "2": "…" }`,
|
|
718
|
+
without their comments (see [Comments](#comments)), and nothing else. Each rule is one `noul` (yes/no probability) question that
|
|
464
719
|
carries the rule's description and its code under its words, `must` and
|
|
465
720
|
`never` (or a guideline's `should` and `should_not`): whether the file
|
|
466
721
|
diverges from the pattern `must` shows, with `never` as an example of
|
|
@@ -473,14 +728,22 @@ beats all of the above for that rule.
|
|
|
473
728
|
project rule, the rule's question has a second one beside it, the linter
|
|
474
729
|
check (see [Validate](#validate)), with the same fields.
|
|
475
730
|
2. A second request only when at least one rule's probability is above its
|
|
476
|
-
threshold
|
|
477
|
-
|
|
478
|
-
|
|
731
|
+
threshold. Per flagged rule, when the file has more than one section, one
|
|
732
|
+
`choice` question among the sections' numbers, which yields the section to
|
|
733
|
+
report. The options carry no text of their own, since each is a key of the
|
|
734
|
+
state: describing each section by its first line, or by what it declares,
|
|
735
|
+
located no better on the eval. For each section, two `choice` questions
|
|
736
|
+
among its lines, each option the line's text, of the line the violation
|
|
737
|
+
starts on and the one it ends on; the chosen section's are underlined.
|
|
738
|
+
Beside them, one `noul` carrying the rule:
|
|
739
|
+
whether you can tell if `code` breaks it from `code` alone, without knowing
|
|
740
|
+
what other files, libraries, services, or configuration do. It yields the
|
|
741
|
+
warning (see the report, above).
|
|
479
742
|
3. Jev reads at most 32k tokens of state and one question together, and 64k
|
|
480
743
|
tokens in a request. adhere estimates tokens from the JSON it sends, at
|
|
481
744
|
about three bytes a token. When a file's questions would not fit in one
|
|
482
745
|
request, they are split across several. A file whose code and longest
|
|
483
|
-
question would not fit together, or that has more than
|
|
746
|
+
question would not fit together, or that has more than 255 sections, is
|
|
484
747
|
skipped and counted in the summary, and the rest of the run goes on. So is
|
|
485
748
|
a file Jev itself counts as over its context, which text denser than the
|
|
486
749
|
estimate, such as CJK, can cause.
|
|
@@ -493,8 +756,9 @@ gives everyone and CI the same findings without paying for them again, and a
|
|
|
493
756
|
pull request changes the judgments of only the files it changes.
|
|
494
757
|
|
|
495
758
|
`files/` holds Jev's answers by the content they are about: each cache file is
|
|
496
|
-
named for the hash of a source file's content
|
|
497
|
-
|
|
759
|
+
named for the hash of a source file's content as Jev reads it, without its
|
|
760
|
+
comments, so a moved or copied file keeps its judgments, identical files share
|
|
761
|
+
them, and a change to comments alone re-judges nothing. Each answer sits under a
|
|
498
762
|
fingerprint of the model and the question asked for the rule, which carries
|
|
499
763
|
the rule's text. A changed file re-judges every rule for that file. An edited
|
|
500
764
|
rule re-judges only that rule, and an adhere that asks its questions
|
|
@@ -510,9 +774,12 @@ every file, adhere prunes the cache: it deletes answers about content no file
|
|
|
510
774
|
has, and folds what is left into one file per content. Answers to rules the
|
|
511
775
|
run left out stay, so a run with other presets or rules, or with one topic,
|
|
512
776
|
loses nothing another run still asks; answers to a rule's old texts stay too,
|
|
513
|
-
until the content they are about is gone.
|
|
514
|
-
|
|
515
|
-
|
|
777
|
+
until the content they are about is gone. Answers about a file the run read
|
|
778
|
+
but judged against no rule, such as a test when no rule judges tests, stay as
|
|
779
|
+
well, while a file the config excludes is not read, so answers about it go. A
|
|
780
|
+
run narrowed by `--filter` does not prune.
|
|
781
|
+
|
|
782
|
+
To keep the cache out of diffs, mark it as generated in `.gitattributes`:
|
|
516
783
|
|
|
517
784
|
```text
|
|
518
785
|
.adhere/cache/** linguist-generated -diff
|
|
@@ -532,12 +799,55 @@ every file is cached needs no key and no network.
|
|
|
532
799
|
|
|
533
800
|
## Agent skill
|
|
534
801
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
802
|
+
Two skills teach an agent to work with adhere:
|
|
803
|
+
|
|
804
|
+
- [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) gathers a repo's
|
|
805
|
+
conventions into rule files, configures adhere, and calibrates thresholds.
|
|
806
|
+
- [`skills/adhere-fix/SKILL.md`](./skills/adhere-fix/SKILL.md) verifies and
|
|
807
|
+
fixes the findings `lint` reports: it checks each against its rule and the
|
|
808
|
+
code, fixes the real ones, reports the false ones with the evidence, and says
|
|
809
|
+
when a rule is wrong more often than right.
|
|
810
|
+
|
|
811
|
+
Install them with the [skills](https://github.com/vercel-labs/skills) CLI:
|
|
812
|
+
`skills add darkmatter/adhere`. The binary carries both: `adhere skill` prints
|
|
813
|
+
the first and `adhere skill fix` the second, so
|
|
814
|
+
`adhere skill fix > .agents/skills/adhere-fix/SKILL.md` works without a
|
|
815
|
+
checkout.
|
|
816
|
+
|
|
817
|
+
### In CI
|
|
818
|
+
|
|
819
|
+
Pipe the fix skill into an agent that runs without asking. The skill tells it
|
|
820
|
+
that invoking the skill permits every `adhere lint` run and its cost, and
|
|
821
|
+
`adhere skill fix` prints the skill of the adhere that runs, so its flags
|
|
822
|
+
match:
|
|
823
|
+
|
|
824
|
+
```sh
|
|
825
|
+
adhere skill fix | codex exec --yolo
|
|
826
|
+
adhere skill fix | claude -p --dangerously-skip-permissions
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
Both turn off the agent's own permission checks, which suits a runner thrown
|
|
830
|
+
away after the job. To keep Codex in its sandbox, let the sandbox reach
|
|
831
|
+
`api.typesafe.ai`:
|
|
832
|
+
|
|
833
|
+
```sh
|
|
834
|
+
adhere skill fix | codex exec --sandbox workspace-write \
|
|
835
|
+
-c sandbox_workspace_write.network_access=true -c approval_policy=never
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
- Set `TYPESAFE_API_KEY` from a secret.
|
|
839
|
+
- The agent adds no `adhere-ignore` comments or `@adhere` notes unless
|
|
840
|
+
`ADHERE_ALLOW_SUPPRESSIONS=1` is set in its environment, as in
|
|
841
|
+
`adhere skill fix | ADHERE_ALLOW_SUPPRESSIONS=1 codex exec --yolo`. It then
|
|
842
|
+
suppresses a finding it shows is false, or that three fixes did not clear,
|
|
843
|
+
with the evidence in the comment. Without it, the agent leaves those
|
|
844
|
+
findings, and its report ends by naming them and the variable.
|
|
845
|
+
- End the job with `adhere lint --yes`. The agent exits 0 whatever it left,
|
|
846
|
+
so this step passes or fails the job; it judges only the files the agent
|
|
847
|
+
changed, and reads the rest from the cache.
|
|
848
|
+
- Do not run it on pull requests from forks. The agent reads their code with
|
|
849
|
+
your secrets in reach and the network open, so code written to steer it can
|
|
850
|
+
send them out.
|
|
541
851
|
|
|
542
852
|
## Development
|
|
543
853
|
|
|
@@ -556,7 +866,7 @@ and `bun run test` runs the tests.
|
|
|
556
866
|
### Native executable
|
|
557
867
|
|
|
558
868
|
`bun run build` compiles `dist/adhere`, a single binary with Bun and the
|
|
559
|
-
|
|
869
|
+
presets inside it, through Bun's
|
|
560
870
|
[`--compile`](https://bun.sh/docs/bundler/executables) with
|
|
561
871
|
`--asset ./presets`. It runs without Bun or `node_modules` on the target
|
|
562
872
|
machine and still loads the repo's `config.ts` and Markdown rules from disk.
|
|
@@ -564,6 +874,12 @@ machine and still loads the repo's `config.ts` and Markdown rules from disk.
|
|
|
564
874
|
into its package under `dist/npm/`
|
|
565
875
|
([`scripts/npm-packages.ts`](./scripts/npm-packages.ts)).
|
|
566
876
|
|
|
877
|
+
`adhere --version` tells the builds apart. A release prints its version, as
|
|
878
|
+
`adhere v0.10.0`. `bun run build` prints `git describe --tags --dirty` where
|
|
879
|
+
it was built, as `adhere v0.10.0-16-g9a41328`: 16 commits after v0.10.0, at
|
|
880
|
+
`9a41328`, ending in `-dirty` when the tree had changes. Run from source, it
|
|
881
|
+
prints the checkout's version marked `(source)`.
|
|
882
|
+
|
|
567
883
|
Each platform's executable is its own npm package,
|
|
568
884
|
`@drkmttr/adhere-<platform>-<arch>`, limited by `os` and `cpu`, and an
|
|
569
885
|
optional dependency of `@drkmttr/adhere`, so an install fetches only its own
|