@drkmttr/adhere 0.9.5 → 0.10.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 +245 -65
- package/package.json +6 -6
- 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/parameterized-sql.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/never-logged-returned-or-output.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/external-calls-are-resilient.md +6 -2
- package/presets/effect/basics/gen-for-sequencing.md +6 -2
- 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 +6 -2
- package/presets/effect/data/variants-are-tagged-unions.md +6 -2
- 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 +6 -2
- 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/skills/adhere/SKILL.md +40 -12
- package/skills/adhere-fix/SKILL.md +174 -0
- package/src/cli.ts +51 -14
- package/src/comments.ts +36 -0
- package/src/config.ts +98 -14
- package/src/contradictions.ts +10 -2
- package/src/excerpt.ts +11 -1
- package/src/index.ts +1 -1
- package/src/init.ts +119 -12
- package/src/main.ts +0 -1
- package/src/markdown.ts +77 -14
- package/src/models/Audit.ts +2 -0
- package/src/presets.ts +6 -3
- package/src/rules.ts +35 -2
- package/src/services/AdhereConfig.ts +36 -13
- package/src/services/SourceWalker.ts +32 -10
- package/src/suppress.ts +113 -0
- package/src/workflows/audit.ts +99 -17
- package/src/workflows/format.ts +20 -13
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>adhere</h1>
|
|
3
|
+
</div>
|
|
2
4
|
|
|
3
5
|
A linter for rules a normal linter cannot check. A rule is a one-sentence
|
|
4
6
|
description in RFC 2119's words, like "business logic must live in services",
|
|
@@ -8,8 +10,11 @@ model) whether the file breaks each rule, gets a calibrated probability per
|
|
|
8
10
|
rule, and reports the ones above a threshold with the line Jev points at. The
|
|
9
11
|
report uses the same frame as `vp lint`.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
Rules a normal linter can check exactly, such as a banned import or a type
|
|
14
|
+
error, belong in that linter. While it lints, adhere asks Jev whether a normal
|
|
15
|
+
linter could check each of your rules, and [`adhere validate`](#validate) lists
|
|
16
|
+
rules that it thinks belong in a regular linter, and also detects any
|
|
17
|
+
contradictions in your rules.
|
|
13
18
|
|
|
14
19
|
A preset for effect is included which can be run without setup:
|
|
15
20
|
|
|
@@ -27,7 +32,7 @@ To pin the version in a repo, for CI or scripts, add it as a dev dependency
|
|
|
27
32
|
and run `npx adhere`.
|
|
28
33
|
|
|
29
34
|
`adhere` is a prebuilt executable for macOS and Linux on arm64 and x64 and for
|
|
30
|
-
Windows on x64.
|
|
35
|
+
Windows on x64.
|
|
31
36
|
|
|
32
37
|
adhere sends each file it judges to Jev at `api.typesafe.ai`, authenticated
|
|
33
38
|
with a TypeSafe AI API key: the one `adhere login` saved, or
|
|
@@ -36,7 +41,7 @@ with a TypeSafe AI API key: the one `adhere login` saved, or
|
|
|
36
41
|
## Quick start
|
|
37
42
|
|
|
38
43
|
```sh
|
|
39
|
-
adhere init # .adhere/config.ts
|
|
44
|
+
adhere init # .adhere/config.ts, two example rules, and the dependency
|
|
40
45
|
adhere login # save your TypeSafe AI API key, once
|
|
41
46
|
adhere validate # Checks your rules' wording, and contradictions using Jev
|
|
42
47
|
adhere lint # audit the working directory
|
|
@@ -74,11 +79,13 @@ Under it is the line Jev points at, underlined, with the code around it: the
|
|
|
74
79
|
largest statement around the line that is 30 lines or fewer, usually the whole
|
|
75
80
|
function, and up to 3 lines of whole statements on either side. The
|
|
76
81
|
hint is the rule's code that must be written; a rule with only code that must
|
|
77
|
-
never be written shows that code, labeled `never:`, instead.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
never be written shows that code, labeled `never:`, instead. A rule whose
|
|
83
|
+
level is `warning` reports its findings with `⚠` in place of `×`, in amber. On
|
|
84
|
+
a terminal, the report is in color and the code in it is highlighted. The exit
|
|
85
|
+
code is 1 when an error is reported, and 0 when nothing is, or only warnings;
|
|
86
|
+
`--deny-warnings` fails the run on warnings too. It is also 1 when the run
|
|
87
|
+
refuses, for example on an invalid config or rule file, a missing API key, or
|
|
88
|
+
an unknown command or flag; a refusal prints its reason.
|
|
82
89
|
|
|
83
90
|
## Usage
|
|
84
91
|
|
|
@@ -90,6 +97,7 @@ adhere lint --yes # send the requests without asking first
|
|
|
90
97
|
adhere lint --limit 500 # judge at most 500 checks; the rest wait for the next run
|
|
91
98
|
adhere lint --rpm 30 # send at most 30 requests a minute
|
|
92
99
|
adhere lint --filter 'src/**' # read only the files a glob matches
|
|
100
|
+
adhere lint --deny-warnings # fail on warnings as well as errors
|
|
93
101
|
adhere lint --log-level debug # log each request to Jev on stderr
|
|
94
102
|
adhere validate # check the rules' wording; ask Jev whether any contradict
|
|
95
103
|
adhere init [--force] # scaffold .adhere/config.ts and two example rules
|
|
@@ -102,6 +110,10 @@ Bare `adhere` prints the help, which lists the commands, and
|
|
|
102
110
|
`adhere <command> --help` lists a command's flags. `adhere --completions <shell>`
|
|
103
111
|
prints a completion script.
|
|
104
112
|
|
|
113
|
+
### Demo
|
|
114
|
+
|
|
115
|
+
<a href="https://asciinema.org/a/1266511" target="_blank"><img width="400" src="https://asciinema.org/a/1266511.svg" /></a>
|
|
116
|
+
|
|
105
117
|
### Before and during a run
|
|
106
118
|
|
|
107
119
|
Until it knows what judging takes, a status line on stderr says what `lint` is
|
|
@@ -121,15 +133,10 @@ Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, a
|
|
|
121
133
|
The cost is adhere's estimate of the input tokens times the model's price.
|
|
122
134
|
Jev charges only for input tokens: $0.042 a million for `jev-latest`, per
|
|
123
135
|
[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.
|
|
136
|
+
For a model adhere has no price for, the plan gives the tokens alone.
|
|
127
137
|
|
|
128
138
|
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
|
|
139
|
+
answers every check; `--yes` sends without asking. Files finished before it are cached, so
|
|
133
140
|
running again continues from there.
|
|
134
141
|
|
|
135
142
|
The firewall in front of Jev's API can refuse a request whose code reads to
|
|
@@ -147,13 +154,60 @@ next run with the same limit picks up where this one stopped. `--limit 0` shows
|
|
|
147
154
|
the plan and judges nothing. `--rpm <requests>` sends at most that many requests
|
|
148
155
|
to Jev a minute, evenly spaced, retries included, and the plan says about how
|
|
149
156
|
long they take. `--filter <glob>` reads only the files whose path from the
|
|
150
|
-
working directory matches, such as `src/**` or `**/*.service.ts
|
|
151
|
-
|
|
157
|
+
working directory matches, such as `src/**` or `**/*.service.ts` (if using globs,
|
|
158
|
+
wrap with single quotes to avoid expansion); repeat it for more, and start a
|
|
159
|
+
pattern with `!` to leave out what it matches. Together:
|
|
152
160
|
|
|
153
161
|
```sh
|
|
154
162
|
adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 200 --rpm 30
|
|
155
163
|
```
|
|
156
164
|
|
|
165
|
+
### Comments
|
|
166
|
+
|
|
167
|
+
Jev reads a file's code, not its comments. A comment changes nothing the code
|
|
168
|
+
does, so it should not change the report, yet Jev takes what comments claim at
|
|
169
|
+
their word: in a study of one alchemy rule, comments saying a delete succeeds
|
|
170
|
+
on a missing resource hid 8 of its 9 real violations, and taking comments out
|
|
171
|
+
found all nine. adhere takes every comment out before it hashes or sends a
|
|
172
|
+
file, blanking it so every line keeps its number; the report's excerpts still
|
|
173
|
+
show them.
|
|
174
|
+
|
|
175
|
+
A comment that says `@adhere` anywhere in it is a note for Jev, and stays.
|
|
176
|
+
Write one for a fact the code relies on that the file cannot show, once you
|
|
177
|
+
have checked it, with how you know:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
/**
|
|
181
|
+
* Deletes the activity. @adhere DeleteActivity succeeds on a missing
|
|
182
|
+
* activity, so no not-found error needs catching; probed 2026-09-25.
|
|
183
|
+
*/
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A note informs Jev; it suppresses nothing, and Jev still judges the code
|
|
187
|
+
around it. `includeComments: true` in the config sends every comment, for a
|
|
188
|
+
project whose rules are about comments, such as doc comments on exports.
|
|
189
|
+
|
|
190
|
+
### Suppressing a finding
|
|
191
|
+
|
|
192
|
+
A finding Jev got wrong is suppressed in the code, with a comment saying why:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// adhere-ignore alchemy/providers/idempotent-delete -- DeleteActivity succeeds on a missing activity; probed 2026-09-25
|
|
196
|
+
delete: Effect.fn(function* ({ output }) {
|
|
197
|
+
yield* sfn.deleteActivity({ activityArn: output.activityArn });
|
|
198
|
+
}),
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
On a line of its own, `adhere-ignore` covers the statement that starts on the
|
|
202
|
+
next line of code: here the whole handler, wherever in it Jev points, since the
|
|
203
|
+
line it points at can move between runs. At the end of a line of code, it
|
|
204
|
+
covers the statement that starts on that line. `adhere-ignore-file`, anywhere
|
|
205
|
+
in a file, covers the whole file, and the rules it names are not judged there
|
|
206
|
+
at all. A comment names rules as a report does, a preset's with its preset
|
|
207
|
+
first, separated by commas; what follows `--` is the reason, for whoever
|
|
208
|
+
reads the code next. Jev never reads these comments, even with
|
|
209
|
+
`includeComments: true`, and the summary counts the findings they suppress.
|
|
210
|
+
|
|
157
211
|
### Logging a run
|
|
158
212
|
|
|
159
213
|
`--log-level debug` logs a run's work on stderr: the config it loaded, how many
|
|
@@ -172,21 +226,47 @@ adhere lint --log-level debug 2> adhere.log
|
|
|
172
226
|
|
|
173
227
|
### What gets read
|
|
174
228
|
|
|
175
|
-
`adhere lint` reads the `.ts` files under the working directory, except `.d.ts
|
|
176
|
-
|
|
177
|
-
`
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`.
|
|
229
|
+
`adhere lint` reads the `.ts` files under the working directory, except `.d.ts`
|
|
230
|
+
and config files. When the working directory contains `agents/`, `apps/`, or
|
|
231
|
+
`packages/`, only those trees are read. Below the working directory, anything
|
|
232
|
+
under `node_modules/`, `dist/`, `coverage/`, `vendor/`, `e2e/`, `references/`,
|
|
233
|
+
`.adhere/`, `.agents/`, `.claude/`, `.direnv/`, `.alchemy/`, or `.vite/` is
|
|
234
|
+
skipped; the directories above it do not count. `.tsx` files are not read, and
|
|
235
|
+
`.gitignore` is not consulted.
|
|
236
|
+
|
|
237
|
+
Tests are judged only by rules about tests. A test is a `.test.ts` or
|
|
238
|
+
`.spec.ts` file, or any file under a `test/`, `tests/`, `__tests__/`, or
|
|
239
|
+
`fixtures/` directory, and only a rule whose front matter says `tests` judges
|
|
240
|
+
one (see [Rules as Markdown files](#rules-as-markdown-files)). On alchemy, 38%
|
|
241
|
+
of the findings from rules not about tests were in test helpers and fixtures.
|
|
242
|
+
|
|
243
|
+
A config's `exclude` lists globs, relative to the working directory, of files
|
|
244
|
+
no rule judges, such as generated code. It leaves them out of every run, as
|
|
245
|
+
`--filter '!<glob>'` leaves them out of one:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
export default defineConfig({ exclude: ["**/generated/**"] });
|
|
249
|
+
```
|
|
182
250
|
|
|
183
251
|
### Init
|
|
184
252
|
|
|
185
|
-
`adhere init` writes `.adhere/config.ts
|
|
186
|
-
|
|
253
|
+
`adhere init` writes `.adhere/config.ts`, the `.adhere/tsconfig.json` that gives
|
|
254
|
+
the editor its types (see [Config](#config)), and two example rules. It is safe
|
|
255
|
+
to rerun: by default it reports existing files as skipped and does not clobber
|
|
187
256
|
them. `--force` overwrites them. A config already at another accepted path is
|
|
188
257
|
kept, and no second one is added, even with `--force`.
|
|
189
258
|
|
|
259
|
+
When there is a `package.json`, init also adds `@drkmttr/adhere` to its
|
|
260
|
+
`devDependencies`, so the config's `defineConfig` has its types in the editor. It runs
|
|
261
|
+
`<manager> add -D @drkmttr/adhere` with the package manager whose lockfile
|
|
262
|
+
(`bun.lock`, `bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`)
|
|
263
|
+
sits beside the `package.json`, or npm when there is none. With pnpm it adds
|
|
264
|
+
`--ignore-workspace-root-check`, so the install also works at a workspace root,
|
|
265
|
+
and `--config.strict-dep-builds=false`, so pnpm 11 skips the build script of
|
|
266
|
+
msgpackr-extract, an optional native add-on that comes with `effect` and that
|
|
267
|
+
adhere never uses, with a warning instead of failing.
|
|
268
|
+
It skips the install when `package.json` already lists adhere.
|
|
269
|
+
|
|
190
270
|
### Validate
|
|
191
271
|
|
|
192
272
|
`adhere validate` loads the config, rule files, and presets the way `lint`
|
|
@@ -252,12 +332,14 @@ repo being audited, at one of these paths (keep one):
|
|
|
252
332
|
- `.adhere.config.ts`
|
|
253
333
|
|
|
254
334
|
```ts
|
|
255
|
-
import
|
|
335
|
+
import { defineConfig } from "@drkmttr/adhere";
|
|
256
336
|
|
|
257
|
-
export default {
|
|
337
|
+
export default defineConfig({
|
|
258
338
|
model: "jev-latest", // optional, default "jev-latest"
|
|
259
339
|
threshold: 0.8, // optional, default 0.8
|
|
260
340
|
presets: ["effect"], // optional, built-in rule sets
|
|
341
|
+
exclude: ["**/generated/**"], // optional, files no rule judges
|
|
342
|
+
overrides: { "effect/basics/instrument-with-pipe": "off" }, // optional, see Presets
|
|
261
343
|
rules: {
|
|
262
344
|
"data/brand-meaningful-primitives": {
|
|
263
345
|
description:
|
|
@@ -270,16 +352,24 @@ type UserId = typeof UserId.Type
|
|
|
270
352
|
threshold: 0.8, // optional per-rule override
|
|
271
353
|
},
|
|
272
354
|
},
|
|
273
|
-
}
|
|
355
|
+
});
|
|
274
356
|
```
|
|
275
357
|
|
|
276
358
|
The default export is decoded with Effect `Schema`. An invalid shape refuses
|
|
277
|
-
the run.
|
|
278
|
-
|
|
279
|
-
supplies
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
359
|
+
the run. `defineConfig` types the config for the editor and returns it as it
|
|
360
|
+
is; `satisfies Config`, with `import type { Config }`, does the same. The
|
|
361
|
+
executable supplies `@drkmttr/adhere` to the config whether or not the repo has
|
|
362
|
+
the package installed, and the editor's types come from the package, which
|
|
363
|
+
`adhere init` adds as a dev dependency. A config can import other files by
|
|
364
|
+
relative path, but no packages besides `@drkmttr/adhere`: the executable does
|
|
365
|
+
not resolve `node_modules`.
|
|
366
|
+
|
|
367
|
+
A tsconfig's globs skip dot directories, so the editor opens
|
|
368
|
+
`.adhere/config.ts` outside any project, where it cannot resolve the package's
|
|
369
|
+
types: they are reached only by `bundler`, `node16`, or `nodenext` resolution.
|
|
370
|
+
`adhere init` writes `.adhere/tsconfig.json`, a project for the config alone
|
|
371
|
+
with `bundler` resolution. For a config written by hand, add that file, or
|
|
372
|
+
include `.adhere/config.ts` in a tsconfig that resolves the same way.
|
|
283
373
|
|
|
284
374
|
`rules` in the config, inline as above or as a directory (below), replaces the
|
|
285
375
|
`.adhere/` rule files: none of them, root or nested, is read then.
|
|
@@ -291,12 +381,19 @@ config and the cache (neither is read as a rule). The path without `.md` is the
|
|
|
291
381
|
rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
|
|
292
382
|
directory exists, it is read without any config.
|
|
293
383
|
|
|
294
|
-
A file is front matter, then a body. The body's code goes in fences
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
384
|
+
A file is front matter, then a body. The body's code goes in fences under
|
|
385
|
+
headings of RFC 2119's words: `## Must` for code that must be written, and
|
|
386
|
+
`## Never` for code that must never be, which is what a violation looks like.
|
|
387
|
+
A heading names the code in its section, which runs to the next heading at its
|
|
388
|
+
level or higher, so a deeper heading such as `### A bare number` stays inside
|
|
389
|
+
it. Only a heading that is the word alone names code, in any case and with or
|
|
390
|
+
without a colon: `## Never:` does, `## Never do this` does not. Prose around
|
|
391
|
+
the code renders on GitHub and is ignored.
|
|
392
|
+
|
|
393
|
+
A fence can instead name its code after its language, as in `ts never`, which
|
|
394
|
+
GitHub does not show; a fence's own word wins over its heading's. An untagged
|
|
395
|
+
fence outside those sections is code that must be written, and without a fence
|
|
396
|
+
the whole body is. `avoid`, the tag before 0.7, reads as `never`.
|
|
300
397
|
|
|
301
398
|
````md
|
|
302
399
|
---
|
|
@@ -306,22 +403,31 @@ threshold: 0.8
|
|
|
306
403
|
|
|
307
404
|
Why: a bare `number` accepts 70000 and -1.
|
|
308
405
|
|
|
309
|
-
|
|
406
|
+
## Must
|
|
407
|
+
|
|
408
|
+
```ts
|
|
310
409
|
const Port = Schema.Int.pipe(
|
|
311
410
|
Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
|
|
312
411
|
Schema.brand("Port"),
|
|
313
412
|
);
|
|
314
413
|
```
|
|
315
414
|
|
|
316
|
-
|
|
415
|
+
## Never
|
|
416
|
+
|
|
417
|
+
```ts
|
|
317
418
|
const port: number = Number(process.env.PORT);
|
|
318
419
|
```
|
|
319
420
|
````
|
|
320
421
|
|
|
321
422
|
`description` is required, with code under `must`, `never`, or both.
|
|
322
|
-
`threshold` is optional
|
|
323
|
-
|
|
324
|
-
|
|
423
|
+
`threshold` is optional, and so is `tests`, for a rule that judges tests, which
|
|
424
|
+
rules otherwise skip: `tests: only` for a rule about tests, which judges
|
|
425
|
+
nothing else, and `tests: include` for one that holds in tests as well. So is
|
|
426
|
+
`level`: `level: warning` for a nit, or for a rule that tends to flag code
|
|
427
|
+
wrongly, reports its findings as warnings, which do not fail the run. A rule
|
|
428
|
+
inline in a config takes the same `tests` and `level`. A file that fails validation refuses
|
|
429
|
+
the run with its path in the message. `loadRules(directory)` from the package
|
|
430
|
+
root does the same load for your own tooling.
|
|
325
431
|
|
|
326
432
|
Jev reads the code under the same words, and is asked whether the file
|
|
327
433
|
diverges from the pattern `must` shows, with `never` as an example of
|
|
@@ -334,7 +440,7 @@ dropped. Neither goes in the other's place: code that must never be written,
|
|
|
334
440
|
under `must`, reads to Jev as the pattern to follow.
|
|
335
441
|
|
|
336
442
|
A rule that is a guideline rather than a requirement says "should" instead:
|
|
337
|
-
in its description, and in
|
|
443
|
+
in its description, and in `## Should` and `## Should not` headings (`should`
|
|
338
444
|
and `shouldNot` inline in a config). Jev then reads the code under `should`
|
|
339
445
|
and `should_not`. A rule is one or the other, so it cannot mix `must` or
|
|
340
446
|
`never` with `should` or `should not`. In a config, `reference` and `avoid`,
|
|
@@ -360,7 +466,7 @@ cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
|
|
|
360
466
|
documentation can point `rules` at `docs/adhere/`:
|
|
361
467
|
|
|
362
468
|
```ts
|
|
363
|
-
export default { presets: ["effect"], rules: "./docs/adhere" }
|
|
469
|
+
export default defineConfig({ presets: ["effect"], rules: "./docs/adhere" });
|
|
364
470
|
```
|
|
365
471
|
|
|
366
472
|
Rules read through `rules` apply project-wide.
|
|
@@ -371,7 +477,7 @@ We've evaluated different ways of giving Jev a rule, to catch the most
|
|
|
371
477
|
violations with the fewest false positives. In general:
|
|
372
478
|
|
|
373
479
|
- Say "must" for what code must do and "never" for what it must not, in the
|
|
374
|
-
description and
|
|
480
|
+
description and in the headings over the code.
|
|
375
481
|
- Give one example of each: one `must` block and one `never` block. Three of
|
|
376
482
|
each did no better, and several of one kind alone did worse.
|
|
377
483
|
- For a guideline rather than a requirement, say "should" and "should not"
|
|
@@ -407,6 +513,32 @@ of its own: `--preset effect/basics` applies only the rules under
|
|
|
407
513
|
A topic's rules keep the ids they have in the whole preset, so a topic and its
|
|
408
514
|
preset share cached judgments, and naming both applies each rule once.
|
|
409
515
|
|
|
516
|
+
A config's `overrides` changes a rule by the id a report names it with, a
|
|
517
|
+
preset's with the preset first, without copying its text into `rules`: `off`
|
|
518
|
+
drops it, `warning` or `error` sets its level, and `{ level, threshold }` sets
|
|
519
|
+
either. An id that no preset or project rule has refuses the run, so a typo
|
|
520
|
+
does not go unnoticed; a rule of a preset the run does not name is accepted.
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
export default defineConfig({
|
|
524
|
+
presets: ["effect", "alchemy"],
|
|
525
|
+
overrides: {
|
|
526
|
+
"alchemy/providers/idempotent-delete": { level: "warning", threshold: 0.9 },
|
|
527
|
+
"effect/basics/instrument-with-pipe": "off",
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
`effect`'s four rules about tests, `testing/test-clock-for-time`,
|
|
533
|
+
`services/fresh-layer-per-test`, `services/test-layers-are-in-memory`, and
|
|
534
|
+
`config/tests-provide-values-directly`, say `tests: only`: they judge tests and
|
|
535
|
+
nothing else, and every other preset rule skips tests.
|
|
536
|
+
|
|
537
|
+
`alchemy`'s `providers/idempotent-delete` reports warnings. Whether a delete
|
|
538
|
+
fails on a resource that is already gone is a fact about the API, which the
|
|
539
|
+
file does not show, so the rule also flags deletes of APIs that succeed
|
|
540
|
+
anyway; see [the study](eval/studies/idempotent-delete.md).
|
|
541
|
+
|
|
410
542
|
A preset rule says "must" only where its source makes a requirement, and
|
|
411
543
|
"should" where the source gives advice. In `effect`, three rules are
|
|
412
544
|
guidelines: network calls carry a timeout and a retry schedule, unless their
|
|
@@ -455,12 +587,13 @@ whose types have no requirements to find. The preset's
|
|
|
455
587
|
Highest first: `--threshold` on the command line, the config file, presets in
|
|
456
588
|
order (a later preset wins), then the defaults `jev-latest` and `0.8`. A rule
|
|
457
589
|
in `rules` replaces a preset rule with the same id. A rule's own `threshold`
|
|
458
|
-
beats all of the above for that rule
|
|
590
|
+
beats all of the above for that rule, and an entry in `overrides` beats the
|
|
591
|
+
rule's own `threshold` and `level`.
|
|
459
592
|
|
|
460
593
|
## How a file is judged
|
|
461
594
|
|
|
462
|
-
1. One Jev request per file. The state is the file's numbered lines
|
|
463
|
-
nothing else. Each rule is one `noul` (yes/no probability) question that
|
|
595
|
+
1. One Jev request per file. The state is the file's numbered lines, without
|
|
596
|
+
their comments (see [Comments](#comments)), and nothing else. Each rule is one `noul` (yes/no probability) question that
|
|
464
597
|
carries the rule's description and its code under its words, `must` and
|
|
465
598
|
`never` (or a guideline's `should` and `should_not`): whether the file
|
|
466
599
|
diverges from the pattern `must` shows, with `never` as an example of
|
|
@@ -493,8 +626,9 @@ gives everyone and CI the same findings without paying for them again, and a
|
|
|
493
626
|
pull request changes the judgments of only the files it changes.
|
|
494
627
|
|
|
495
628
|
`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
|
-
|
|
629
|
+
named for the hash of a source file's content as Jev reads it, without its
|
|
630
|
+
comments, so a moved or copied file keeps its judgments, identical files share
|
|
631
|
+
them, and a change to comments alone re-judges nothing. Each answer sits under a
|
|
498
632
|
fingerprint of the model and the question asked for the rule, which carries
|
|
499
633
|
the rule's text. A changed file re-judges every rule for that file. An edited
|
|
500
634
|
rule re-judges only that rule, and an adhere that asks its questions
|
|
@@ -510,9 +644,12 @@ every file, adhere prunes the cache: it deletes answers about content no file
|
|
|
510
644
|
has, and folds what is left into one file per content. Answers to rules the
|
|
511
645
|
run left out stay, so a run with other presets or rules, or with one topic,
|
|
512
646
|
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
|
-
|
|
647
|
+
until the content they are about is gone. Answers about a file the run read
|
|
648
|
+
but judged against no rule, such as a test when no rule judges tests, stay as
|
|
649
|
+
well, while a file the config excludes is not read, so answers about it go. A
|
|
650
|
+
run narrowed by `--filter` does not prune.
|
|
651
|
+
|
|
652
|
+
To keep the cache out of diffs, mark it as generated in `.gitattributes`:
|
|
516
653
|
|
|
517
654
|
```text
|
|
518
655
|
.adhere/cache/** linguist-generated -diff
|
|
@@ -532,12 +669,55 @@ every file is cached needs no key and no network.
|
|
|
532
669
|
|
|
533
670
|
## Agent skill
|
|
534
671
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
672
|
+
Two skills teach an agent to work with adhere:
|
|
673
|
+
|
|
674
|
+
- [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) gathers a repo's
|
|
675
|
+
conventions into rule files, configures adhere, and calibrates thresholds.
|
|
676
|
+
- [`skills/adhere-fix/SKILL.md`](./skills/adhere-fix/SKILL.md) verifies and
|
|
677
|
+
fixes the findings `lint` reports: it checks each against its rule and the
|
|
678
|
+
code, fixes the real ones, reports the false ones with the evidence, and says
|
|
679
|
+
when a rule is wrong more often than right.
|
|
680
|
+
|
|
681
|
+
Install them with the [skills](https://github.com/vercel-labs/skills) CLI:
|
|
682
|
+
`skills add darkmatter/adhere`. The binary carries both: `adhere skill` prints
|
|
683
|
+
the first and `adhere skill fix` the second, so
|
|
684
|
+
`adhere skill fix > .agents/skills/adhere-fix/SKILL.md` works without a
|
|
685
|
+
checkout.
|
|
686
|
+
|
|
687
|
+
### In CI
|
|
688
|
+
|
|
689
|
+
Pipe the fix skill into an agent that runs without asking. The skill tells it
|
|
690
|
+
that invoking the skill permits every `adhere lint` run and its cost, and
|
|
691
|
+
`adhere skill fix` prints the skill of the adhere that runs, so its flags
|
|
692
|
+
match:
|
|
693
|
+
|
|
694
|
+
```sh
|
|
695
|
+
adhere skill fix | codex exec --yolo
|
|
696
|
+
adhere skill fix | claude -p --dangerously-skip-permissions
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Both turn off the agent's own permission checks, which suits a runner thrown
|
|
700
|
+
away after the job. To keep Codex in its sandbox, let the sandbox reach
|
|
701
|
+
`api.typesafe.ai`:
|
|
702
|
+
|
|
703
|
+
```sh
|
|
704
|
+
adhere skill fix | codex exec --sandbox workspace-write \
|
|
705
|
+
-c sandbox_workspace_write.network_access=true -c approval_policy=never
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
- Set `TYPESAFE_API_KEY` from a secret.
|
|
709
|
+
- The agent adds no `adhere-ignore` comments or `@adhere` notes unless
|
|
710
|
+
`ADHERE_ALLOW_SUPPRESSIONS=1` is set in its environment, as in
|
|
711
|
+
`adhere skill fix | ADHERE_ALLOW_SUPPRESSIONS=1 codex exec --yolo`. It then
|
|
712
|
+
suppresses a finding it shows is false, or that three fixes did not clear,
|
|
713
|
+
with the evidence in the comment. Without it, the agent leaves those
|
|
714
|
+
findings, and its report ends by naming them and the variable.
|
|
715
|
+
- End the job with `adhere lint --yes`. The agent exits 0 whatever it left,
|
|
716
|
+
so this step passes or fails the job; it judges only the files the agent
|
|
717
|
+
changed, and reads the rest from the cache.
|
|
718
|
+
- Do not run it on pull requests from forks. The agent reads their code with
|
|
719
|
+
your secrets in reach and the network open, so code written to steer it can
|
|
720
|
+
send them out.
|
|
541
721
|
|
|
542
722
|
## Development
|
|
543
723
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@drkmttr/adhere-darwin-arm64": "0.
|
|
72
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
73
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
74
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
75
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.10.0",
|
|
72
|
+
"@drkmttr/adhere-darwin-x64": "0.10.0",
|
|
73
|
+
"@drkmttr/adhere-linux-arm64": "0.10.0",
|
|
74
|
+
"@drkmttr/adhere-linux-x64": "0.10.0",
|
|
75
|
+
"@drkmttr/adhere-win32-x64": "0.10.0"
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
description: Errors that crossed RPC or a workflow replay must be matched by their tag and data fields, never with instanceof, prototype methods, or object identity, which do not survive serialization, and RPC arguments must be serializable values, never functions or class instances.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const value = yield* stub.get(key).pipe(
|
|
7
9
|
Effect.catchTag("KeyMissing", (error) => Effect.succeed(`missing: ${error.key}`)),
|
|
8
10
|
);
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
## Never
|
|
14
|
+
|
|
15
|
+
```ts
|
|
12
16
|
const value = yield* stub.get(key).pipe(
|
|
13
17
|
Effect.catchAll((error) => (error instanceof KeyMissing ? Effect.succeed("missing") : Effect.fail(error))),
|
|
14
18
|
);
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
description: Input from outside the stack, such as browsers, partners, or public URLs, should be validated with a schema through Effect RPC or HttpApi, while calls between the stack's own runtimes should use schemaless RPC through bindings, and unvalidated payloads should not cross a trust boundary.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Should
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const Tasks = HttpApiGroup.make("tasks").add(
|
|
7
9
|
HttpApiEndpoint.post("create", "/tasks", { payload: Schema.Struct({ title: Schema.String }) }),
|
|
8
10
|
);
|
|
9
11
|
const greeter = yield* Cloudflare.Workers.bindWorker(Greeter); // internal call
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
## Should not
|
|
15
|
+
|
|
16
|
+
```ts
|
|
13
17
|
fetch: Effect.gen(function* () {
|
|
14
18
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
15
19
|
const body = (yield* request.json) as { title: string; ownerId: string }; // public URL, no validation
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
description: Migrations must be generated and reviewed ahead of time, committed with the schema, and applied from the committed files; a deploy, deploy hook, or Action must never generate migration SQL, and schema changes must never run from an HTTP route.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
// package.json: "db:generate": "drizzle-kit generate" (run locally, then commit ./drizzle)
|
|
7
9
|
const db = yield* Cloudflare.D1.Database("AppDb", { migrations: "./drizzle" });
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
## Never
|
|
13
|
+
|
|
14
|
+
```ts
|
|
11
15
|
// package.json: "deploy": "drizzle-kit generate && alchemy deploy"
|
|
12
16
|
fetch: Effect.gen(function* () {
|
|
13
17
|
if (url.pathname === "/setup") yield* sql`CREATE TABLE users (id text primary key)`;
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
description: A Hyperdrive connection's origin should be the database's direct endpoint, and clients that connect without Hyperdrive should use the provider's pooled endpoint; Hyperdrive should not point at a provider pooler, which stacks two poolers.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Should
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("AppHyperdrive", {
|
|
7
9
|
origin: branch.origin,
|
|
8
10
|
dev: branch.pooledOrigin,
|
|
9
11
|
});
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
## Should not
|
|
15
|
+
|
|
16
|
+
```ts
|
|
13
17
|
const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("AppHyperdrive", { origin: role.pooledOrigin });
|
|
14
18
|
```
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
description: A migration runner must connect with the database's direct URL, never Hyperdrive's runtime connection string or an Accelerate or pooled URL.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
export default defineConfig({ dialect: "postgresql", dbCredentials: { url: process.env.DIRECT_DATABASE_URL! } });
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Never
|
|
12
|
+
|
|
13
|
+
```ts
|
|
10
14
|
export default defineConfig({ dialect: "postgresql", dbCredentials: { url: process.env.ACCELERATE_URL! } });
|
|
11
15
|
```
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
description: A database client or migration config must verify the server's TLS certificate, or connect inside the private network, never setting rejectUnauthorized: false or sslmode=no-verify to make a connection work.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const url = new URL(connection.connectionString);
|
|
7
9
|
url.searchParams.set("sslmode", "verify-full");
|
|
8
10
|
const db = yield* Drizzle.Postgres(url.toString());
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
## Never
|
|
14
|
+
|
|
15
|
+
```ts
|
|
12
16
|
export default defineConfig({
|
|
13
17
|
dialect: "postgresql",
|
|
14
18
|
dbCredentials: { url: process.env.DATABASE_URL!, ssl: { rejectUnauthorized: false } },
|