@drkmttr/adhere 0.15.0 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,11 +69,13 @@ Here's a demo of what the output looks like.
69
69
  <img alt="demo" align="center" src="https://github.com/darkmatter/adhere/raw/main/eval/demo.gif" />
70
70
  </details>
71
71
 
72
- Presets for TypeScript, React, security, Effect, and alchemy are included, and
73
- run without setup:
72
+ Presets for TypeScript, React, security, Effect, and alchemy are included.
73
+ Run them to quickly adhere your agent to recommended rules.
74
74
 
75
75
  ```sh
76
- TYPESAFE_API_KEY=xxx npx @drkmttr/adhere lint --preset typescript
76
+ bun add -g @drkmttr/adhere
77
+ adhere login # save your typesafe api key
78
+ adhere lint --preset typescript,react
77
79
  ```
78
80
 
79
81
  Rules a normal linter can check exactly, such as a banned import or a type
@@ -106,24 +108,33 @@ your rules.
106
108
 
107
109
  ## Install
108
110
 
111
+ Add adhere to your repo's dev dependencies, and run it with `npx adhere` or
112
+ `bunx adhere`:
113
+
114
+ ```sh
115
+ npm install -D @drkmttr/adhere
116
+ ```
117
+
118
+ Or download the standalone executable, which needs no Node or Bun:
119
+
109
120
  ```sh
110
121
  curl -fsSL --create-dirs -o ~/.local/bin/adhere \
111
122
  https://github.com/darkmatter/adhere/releases/latest/download/adhere-$(uname -s)-$(uname -m)
112
123
  chmod +x ~/.local/bin/adhere
113
124
  ```
114
125
 
115
- - `adhere` is a single executable for macOS and Linux (arm64 and x64) and for
116
- Windows (x64), and needs nothing else installed, Node included. Any directory
117
- on your `PATH` works in place of `~/.local/bin`. On Windows, download
118
- `adhere-Windows-x86_64.exe` from the
126
+ - The npm package brings its own TypeScript compiler, for the
127
+ [`symbols` and `references`](#what-jev-reads) Jev reads by default. No other
128
+ setup is needed.
129
+ - The standalone executable has no compiler, so it lints only with `reads: []`
130
+ or `reads: ["workspacePackages"]` in the [config](#config). It suits a global
131
+ install that your agent config can always call.
132
+ - It is built for macOS and Linux, on arm64 and x64, and for Windows x64:
133
+ download `adhere-Windows-x86_64.exe` from the
119
134
  [latest release](https://github.com/darkmatter/adhere/releases/latest).
120
- - Run the command again to update. `download/v0.14.0` in place of
121
- `latest/download` fetches that version.
122
- - To pin the version in a JavaScript repo, for CI or scripts, add
123
- `@drkmttr/adhere` as a dev dependency and run `npx adhere`. That `adhere`
124
- starts through Node, or through Bun under `bunx`.
125
- - adhere sends each file it judges to Jev at `api.typesafe.ai`, authenticated
126
- with a TypeSafe AI API key.
135
+ - adhere sends each file it judges, with what it [reads](#what-jev-reads)
136
+ beside it, to Jev at `api.typesafe.ai`, authenticated with a TypeSafe AI API
137
+ key.
127
138
 
128
139
  ### Login
129
140
 
@@ -131,23 +142,27 @@ chmod +x ~/.local/bin/adhere
131
142
  adhere login # prompts for the key, masking what you type
132
143
  adhere login < key.txt # reads it from stdin instead
133
144
  adhere logout # deletes the saved key
145
+ adhere login --openai # saves an OpenAI API key, for the openai provider
146
+ adhere logout --openai # deletes it, and only it
134
147
  ```
135
148
 
136
149
  - The key is saved to `~/.config/adhere/credentials.json`, or under
137
- `$XDG_CONFIG_HOME` when that is set, readable only by you.
150
+ `$XDG_CONFIG_HOME` when that is set, readable only by you. An OpenAI key is
151
+ saved beside the TypeSafe AI one, for a config whose provider is
152
+ `openai()`; see [Providers](#providers).
138
153
  - `TYPESAFE_API_KEY`, when set, takes precedence over the saved key, so CI can
139
- pass a key without a login.
140
- - The key is read only when a request is about to be sent. A run where every
141
- file is cached needs no key and no network.
154
+ pass a key without a login. `OPENAI_API_KEY` does the same for OpenAI's.
155
+ - The key is read only when a request is about to be sent. A run the cache
156
+ answers in full needs no key and no network.
142
157
 
143
158
  ## Quick start
144
159
 
145
160
  ```sh
146
- adhere init # .adhere/config.ts, two example rules, and the dependency
147
- adhere login # save your TypeSafe AI API key, once
148
- adhere validate # check your rules' wording, and ask Jev whether any contradict
149
- adhere lint # audit the working directory
150
- git add .adhere # commit the rules, and the judgments they cost
161
+ npx adhere init # .adhere/config.ts, two example rules, and the dependency
162
+ npx adhere login # save your TypeSafe AI API key, once
163
+ npx adhere validate # check your rules' wording, and ask Jev whether any contradict
164
+ npx adhere lint # audit the working directory
165
+ git add .adhere # commit the rules, and the judgments they cost
151
166
  ```
152
167
 
153
168
  ### Init
@@ -294,9 +309,8 @@ rule and the changed one side by side, and compare what each flags.
294
309
  amber and never fail the run, so the experiment can sit in the repo, and in
295
310
  CI, while you watch it.
296
311
  3. Change the copy: its description, its examples, its `appliesTo` or
297
- `excludeIf`, or what it [`reads`](#what-a-rule-reads). To try giving Jev more
298
- to read, make the copy a `RULE.ts` with an
299
- [`appendState`](#rules-as-typescript-files) hook.
312
+ `excludeIf`. To try giving Jev more to read, make the copy a `RULE.ts` with
313
+ an [`appendState`](#rules-as-typescript-files) hook.
300
314
  4. Run `adhere lint`, or `adhere lint --filter '<glob>'` to try it on part of
301
315
  the repo first, and compare the two rules' findings on the same files.
302
316
 
@@ -340,13 +354,13 @@ writing a rule.
340
354
 
341
355
  ### What else the studies found
342
356
 
343
- | Lesson | What the study found |
344
- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
345
- | "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)). |
346
- | 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)). |
347
- | 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)). |
348
- | 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 [`reads`](#what-a-rule-reads), an [`appendState`](#rules-as-typescript-files) hook, or an `@adhere` note. |
349
- | 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)). |
357
+ | Lesson | What the study found |
358
+ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
359
+ | "Should" is for people, not a weaker check. | Rules written with "should" and with "must" scored nearly the same ([study](eval/studies/rule-vocabulary.md)). |
360
+ | A wrong finding is usually the rule's fault. | Above 0.8, 31 of 35 wrong findings repeated within a rule: a scope its words reach past, or the same misreading again. Fixing the wording fixes them together. Below 0.8, one-off misreads grow, to about one finding in six at 0.6 to 0.7 ([study](eval/studies/presets.md)). |
361
+ | Narrow a rule in its description. | Jev scores an `excludeIf` near 0.5 for real and false findings alike, so it thins a rule's findings about as much as a higher threshold ([study](eval/studies/presets.md)). Narrowing the description moves the judgment itself: scoping alchemy's idempotent-delete rule to providers' delete handlers dropped its findings elsewhere from 0.67–0.88 to 0.08–0.21, with nothing real lost ([study](eval/studies/idempotent-delete.md)). |
362
+ | Beware rules that turn on what the file does not show. | The weakest preset rules hinged on facts outside the file: whether a client has a default timeout, whether a script is an entry point, whether a key is public ([study](eval/studies/presets.md)). Warning below a context of 0.7 marked one finding in five, and those were real about half the time, where the rest were real three times in four ([study](eval/studies/sufficiency.md)). Make such a rule a warning, or give Jev the fact with config-level [`reads`](#what-jev-reads), an [`appendState`](#rules-as-typescript-files) hook, or an `@adhere` note. |
363
+ | Jev believes comments. | Eight of nine real violations of the idempotent-delete rule carried a comment wrongly saying they were fine, and scored 0.31 to 0.62; without comments, all nine scored 0.81 or more ([study](eval/studies/idempotent-delete.md)). That is why adhere takes comments out. A fact Jev needs, such as an exemption, goes in an `@adhere` note, which stays ([study](eval/studies/comments.md)). |
350
364
 
351
365
  ## Commands
352
366
 
@@ -358,7 +372,7 @@ writing a rule.
358
372
  | `adhere init --shared org/repo` | Scaffold a repo of rules other repos install. See [Creating a shared repo](#creating-a-shared-repo). |
359
373
  | `adhere list org/repo` | List the rules in another repo's `.adhere/rules/`. See [Install rules from other repos](#install-rules-from-other-repos). |
360
374
  | `adhere install org/repo` | Copy them into this repo's `.adhere/rules/org/repo/`. |
361
- | `adhere login`, `adhere logout` | Save or delete a TypeSafe AI API key. See [Login](#login). |
375
+ | `adhere login`, `adhere logout` | Save or delete a TypeSafe AI API key, or with `--openai`, an OpenAI one. See [Login](#login). |
362
376
  | `adhere skill [docs\|setup\|fix]` | Print an agent skill. See [Agent skills](#agent-skills). |
363
377
 
364
378
  Bare `adhere` prints the help. `adhere <command> --help` lists a command's
@@ -400,7 +414,7 @@ Found 1 error.
400
414
  | `×` or `⚠` | An error, or a warning, in amber, from a rule whose `level` is `warning`. Only errors fail the run. |
401
415
  | `data/brand-ports` | The rule's id, then its description. A preset's rule has the preset's name first, as in `effect/basics/gen-for-sequencing`. |
402
416
  | `confidence` | Jev's probability that the file breaks the rule. |
403
- | `context` | Jev's probability that the file shows enough to decide that. |
417
+ | `context` | Jev's probability that it sees enough to decide that. |
404
418
  | The excerpt | The section of the file Jev points at. The line it names is underlined, or the lines, when it names several, are marked with a bar beside the code. |
405
419
  | `hint:` | The rule's code that must be written. A rule with only code that must never be written shows that code instead, labeled `never:`. |
406
420
  | The legend | Under the last finding: each score's threshold, between its range in red and its range in green. |
@@ -412,10 +426,11 @@ its threshold:
412
426
  - Red is below the threshold, so lint never reports it: only the legend shows
413
427
  that range.
414
428
 
415
- **Low context.** Jev sees one file at a time. So we also ask whether the file
416
- shows enough to decide the rule at all. When `context` is below 0.6, it means
417
- Jev thinks it needs more info, which you can add with an `@adhere` comment.
418
- You may also see a hint:
429
+ **Low context.** Jev sees the file and what adhere [reads](#what-jev-reads)
430
+ beside it, not your whole repo. So we also ask whether that shows enough to
431
+ decide the rule at all. When `context` is below 0.6, it means Jev thinks it
432
+ needs more info, which you can add with an `@adhere` comment. You may also see a
433
+ hint:
419
434
 
420
435
  ```text
421
436
  warning: this file may not show enough to check this rule
@@ -487,7 +502,6 @@ const port: number = Number(process.env.PORT);
487
502
  | `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. |
488
503
  | `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. |
489
504
  | `appliesTo`, `excludeIf` | Where the rule applies. See [Scoping a rule](#scoping-a-rule). |
490
- | `reads` | What Jev reads beside the code. See [What a rule reads](#what-a-rule-reads). |
491
505
 
492
506
  | Heading in the body | The code under it |
493
507
  | ---------------------------- | ----------------------------------------------------------- |
@@ -554,17 +568,35 @@ finding its matchers dropped, with their scores.
554
568
  > scored an `excludeIf` near 0.5 for real and false findings alike. See
555
569
  > [Writing good rules](#writing-good-rules).
556
570
 
557
- ### What a rule reads
571
+ ### What Jev reads
572
+
573
+ Beside the file's code, Jev reads three kinds of context, for every rule:
574
+
575
+ | Name | What Jev reads |
576
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
577
+ | `workspacePackages` | The repo's own packages the file imports, each name with its directory, as in `{ "orders-core": "packages/orders" }`. A package it imports that is not listed is an installed one. |
578
+ | `symbols` | The compiler's type blocks for each name, plus compiler documentation and JSDoc tags; not literal editor hover text. |
579
+ | `references` | The declaration of each name the file uses from another file of the repo, whole: a function with its body, a method without the rest of its class. |
580
+
581
+ To read fewer, list the ones you want in the config's `reads`:
582
+
583
+ ```ts
584
+ // .adhere/config.ts
585
+ import { defineConfig } from "@drkmttr/adhere";
586
+
587
+ export default defineConfig({
588
+ reads: ["workspacePackages"], // no symbols or references
589
+ });
590
+ ```
591
+
592
+ `reads: []` sends the code alone.
558
593
 
559
- Jev judges a file by its code alone. A rule that turns on something the file
560
- does not show can name it in `reads`, and adhere gives it to Jev beside the
561
- code, for that rule only. It is a JSON array on one line in front matter, or an
562
- array in a config or a TypeScript rule.
594
+ Each name is the key Jev reads it under, so a rule's description can name it in
595
+ backticks, as the questions name `code`:
563
596
 
564
597
  ````md
565
598
  ---
566
- description: A function from one of this repository's own packages, which `workspacePackages` names, must be called through the package's namespace, never imported by its own name.
567
- reads: ["workspacePackages"]
599
+ description: A function imported from one of this repository's own packages, which the file's `workspacePackages` names, must be called through the package's namespace, never imported by its own name.
568
600
  ---
569
601
 
570
602
  ## Must
@@ -580,25 +612,31 @@ import { parse } from "orders-core";
580
612
  ```
581
613
  ````
582
614
 
583
- There is one name so far:
584
-
585
- | Name | What Jev reads |
586
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
587
- | `workspacePackages` | The workspace's packages, each one's name with its directory, as in `{ "orders-core": "packages/orders" }`. A package that is not in it is an installed one. It is for a rule about whose package an import is of: the repository's own, or one installed from a registry. |
588
-
589
- - Each name is the key Jev reads it under, so the description can name it in
590
- backticks, as the questions name `code`.
615
+ - `symbols` and `references` use the SDK and native compiler pinned to exactly
616
+ `7.1.0-dev.20261009.1`, which the npm package installs. This prerelease contains
617
+ the tuple-reference serialization fix from [microsoft/TypeScript#64080](https://github.com/microsoft/TypeScript/issues/64080);
618
+ SDK and native executable must match that exact version. They use each file's
619
+ `tsconfig.json`, and work without one.
620
+ - `symbols` is still a name-to-string map. Distinct binding/narrowing types get
621
+ distinct blocks; docs and JSDoc tags appear once per canonical symbol per file.
622
+ It has no IDE labels or selected-call overload presentation. Symbol/type queries
623
+ are batched, with independent SDK requests chunked to at most 8.
591
624
  - A name adhere does not have refuses the run, so a misspelled one does not go
592
- unnoticed.
625
+ unnoticed. So does `reads` in a rule: it is the config's alone.
593
626
  - What adhere has no name for, such as a file of the repository's, a rule in
594
627
  TypeScript adds with [`appendState`](#rules-as-typescript-files).
595
628
 
629
+ **Cost and privacy.** `symbols` and `references` send Jev more than the file it
630
+ judges: whole declarations from other files, plus compiler-provided documentation
631
+ and JSDoc tags in `symbols`, even with `includeComments` off. The plan's estimate leaves their tokens out. Leave them
632
+ out of `reads` to send less.
633
+
596
634
  <details>
597
- <summary>More on requests and where the packages come from</summary>
635
+ <summary>Where the packages come from</summary>
598
636
 
599
- - The packages are read on every run, when a rule reads them, from the working
600
- directory's `package.json`, whose `workspaces` npm, Bun, and Yarn read, and
601
- from `pnpm-workspace.yaml`'s `packages`.
637
+ - The packages are read on every run from the working directory's
638
+ `package.json`, whose `workspaces` npm, Bun, and Yarn read, and from
639
+ `pnpm-workspace.yaml`'s `packages`.
602
640
  - In a pattern, `*` is any directory and `**` any depth of them, and one that
603
641
  starts with `!` leaves out what it matches. Each matched directory's
604
642
  `package.json` gives the name, and its directory is given from the working
@@ -606,6 +644,8 @@ There is one name so far:
606
644
  - Nothing need be installed, so a run in CI reads the same packages as one on a
607
645
  laptop.
608
646
  - A working directory that is not a workspace's root has none.
647
+ - Only an import whose path is a plain string counts, and one of a subpath, such
648
+ as `orders-core/client`, counts as its package's.
609
649
 
610
650
  </details>
611
651
 
@@ -637,7 +677,8 @@ export default defineRule({
637
677
  name `code`.
638
678
  - A `RULE.ts` is imported, as a config is, so its code runs on every lint.
639
679
  - A rule with a hook goes to Jev in requests of its own, so only that rule reads
640
- what its hook adds. Each of those requests sends the file again.
680
+ what its hook adds. Each of those requests sends the file and its context
681
+ again.
641
682
  - What the hook reads outside the file is not part of the cache: when
642
683
  `db/schema.sql` changes, a file already judged is not judged again until it or
643
684
  the rule changes.
@@ -647,7 +688,8 @@ export default defineRule({
647
688
 
648
689
  - The state is what Jev reads beside each question: `code`, the file's sections
649
690
  as Jev reads them, under their numbers from 1, as in
650
- `{ code: { "1": "import …", "2": "export const …" } }`.
691
+ `{ code: { "1": "import …", "2": "export const …" } }`, beside what the
692
+ config [`reads`](#what-jev-reads).
651
693
  - `file` is the file's absolute `path`, and its `contents` as written, comments
652
694
  and all.
653
695
  - Nothing the hook returns is checked, so it can change `code` too, or replace
@@ -691,9 +733,9 @@ and they apply project-wide.
691
733
  ## Config
692
734
 
693
735
  A config file is optional. Without one, adhere reads the rules in
694
- `.adhere/rules/` and any `--preset`. A config names presets, sets the model and
695
- thresholds, or gives rules inline. It sits in the working directory of the repo
696
- being audited, at one of these paths (keep one):
736
+ `.adhere/rules/` and any `--preset`. A config names presets, sets the model,
737
+ the thresholds, and what Jev reads, or gives rules inline. It sits in the
738
+ working directory of the repo being audited, at one of these paths (keep one):
697
739
 
698
740
  - `.adhere/config.ts`, the default, next to the rules
699
741
  - `adhere.config.ts`
@@ -706,6 +748,7 @@ export default defineConfig({
706
748
  model: "jev-latest",
707
749
  threshold: 0.8,
708
750
  sufficiencyThreshold: 0.6,
751
+ reads: ["workspacePackages", "symbols", "references"],
709
752
  presets: ["effect"],
710
753
  exclude: ["**/generated/**"],
711
754
  overrides: { "effect/basics/instrument-with-pipe": "off" },
@@ -724,16 +767,18 @@ type UserId = typeof UserId.Type
724
767
  });
725
768
  ```
726
769
 
727
- | Key | Default | Meaning |
728
- | ---------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
729
- | `model` | `"jev-latest"` | The model id sent to TypeSafe. |
730
- | `threshold` | `0.8` | Report a rule when Jev's probability is above this. |
731
- | `sufficiencyThreshold` | `0.6` | Below it, a finding warns that the file may not show enough. |
732
- | `presets` | none | Built-in rule sets, or their topics. See [Presets](#presets). |
733
- | `rules` | `.adhere/rules/` | Rules inline, as above, or a directory (see [Where rules live](#where-rules-live)). Either replaces `.adhere/rules/`: none of its rules, root or nested, is read then. |
734
- | `exclude` | none | Globs, relative to the working directory, of files no rule judges. |
735
- | `overrides` | none | Settings for a rule by its id. See [Overrides](#overrides). |
736
- | `includeComments` | `false` | Send every comment to Jev. See [Comments](#comments). |
770
+ | Key | Default | Meaning |
771
+ | ---------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
772
+ | `model` | `"jev-latest"` | The model id sent to TypeSafe. |
773
+ | `threshold` | `0.8` | Report a rule when Jev's probability is above this. |
774
+ | `sufficiencyThreshold` | `0.6` | Below it, a finding warns that Jev may not see enough to decide. |
775
+ | `presets` | none | Built-in rule sets, or their topics. See [Presets](#presets). |
776
+ | `rules` | `.adhere/rules/` | Rules inline, as above, or a directory (see [Where rules live](#where-rules-live)). Either replaces `.adhere/rules/`: none of its rules, root or nested, is read then. |
777
+ | `exclude` | none | Globs, relative to the working directory, of files no rule judges. |
778
+ | `overrides` | none | Settings for a rule by its id. See [Overrides](#overrides). |
779
+ | `includeComments` | `false` | Send every comment to Jev. See [Comments](#comments). |
780
+ | `reads` | `["workspacePackages", "symbols", "references"]` | What Jev reads beside each file's code, for every rule. `[]` is the code alone. See [What Jev reads](#what-jev-reads). |
781
+ | `provider` | TypeSafe's Jev | Where the questions go instead, with its own model. See [Providers](#providers). |
737
782
 
738
783
  - An invalid shape refuses the run.
739
784
  - A config can import other files by relative path, but no packages besides
@@ -757,6 +802,85 @@ type UserId = typeof UserId.Type
757
802
 
758
803
  </details>
759
804
 
805
+ ### Providers
806
+
807
+ adhere asks TypeSafe's Jev by default. A provider asks another decision model
808
+ instead, and adhere exports one for each model
809
+ [the providers study](eval/studies/providers.md) measured:
810
+
811
+ ```ts
812
+ import { defineConfig, openai } from "@drkmttr/adhere";
813
+
814
+ // Asks with OPENAI_API_KEY, or the key `adhere login --openai` saved.
815
+ export default defineConfig({ provider: openai() });
816
+ ```
817
+
818
+ | Provider | Asks | Options |
819
+ | ------------ | --------------------------------------------------------------- | -------------------------------- |
820
+ | `jev` | TypeSafe's Jev, `jev-latest` by default | `apiKey`, `model` |
821
+ | `openai` | OpenAI's `gpt-6-luna`, on its Decisions API | `apiKey`, `model` |
822
+ | `cloudflare` | Cloudflare's `clef`, by default, or `clef-flash`, on Workers AI | `accountId`, `apiToken`, `model` |
823
+
824
+ - Without an `apiKey`, `jev` and `openai` ask with the key from
825
+ [Login](#login).
826
+ - Each also takes `fetch`, what sends its requests.
827
+ - Clef cuts a request too long for it short rather than refuse it, so
828
+ `cloudflare` skips a file whose request reaches clef's limit: 64,000 tokens,
829
+ or 24,000 for clef-flash.
830
+
831
+ <details>
832
+ <summary>Writing a provider</summary>
833
+
834
+ A provider is a model's name, and an `ask` that answers adhere's questions
835
+ about a state:
836
+
837
+ ```ts
838
+ import { defineConfig, type Provider, RequestFailed } from "@drkmttr/adhere";
839
+
840
+ const mine: Provider = {
841
+ model: "my-decision-model",
842
+ ask: async (state, questions, { signal }) => {
843
+ const response = await fetch("https://decisions.example.com/v1/decide", {
844
+ method: "POST",
845
+ headers: { "Content-Type": "application/json" },
846
+ body: JSON.stringify({ state, questions }),
847
+ signal,
848
+ });
849
+ if (!response.ok) {
850
+ throw new RequestFailed(`My model answered HTTP ${response.status}`, response.status);
851
+ }
852
+ return { answers: await response.json() };
853
+ },
854
+ };
855
+
856
+ export default defineConfig({ provider: mine });
857
+ ```
858
+
859
+ - `state` is the file's code by section under `code`, beside what adhere read
860
+ for it.
861
+ - Each question has an `id`, the `question` as adhere phrases it, and the
862
+ `fields` it names in backticks, such as `rule`, `must`, and `never`. A `noul`
863
+ asks whether something holds. A `choice` asks for the key of one of its
864
+ `options`, and can have just one.
865
+ - `ask` returns `{ answers }`: by id, a `noul`'s probability from 0 to 1, or a
866
+ `choice`'s key. An id left out goes unanswered: its rule is not reported, and
867
+ is asked again on the next run.
868
+ - A rejection stops the run, but for three errors adhere exports:
869
+ `ContextOverflow` skips the file, `RequestBlocked` reports it blocked, and
870
+ `RequestFailed` is tried again, up to three times, when its `status` is 0,
871
+ for no answer, or a 408, 429, or 5xx.
872
+ - adhere paces calls with `--rpm`, aborts `signal` after 30 seconds, and splits
873
+ requests to fit Jev's 64,000 tokens. A model that takes less splits further in
874
+ its `ask`.
875
+ - `model` replaces the config's `model`. The cache keeps answers by it, so a
876
+ new model is asked again.
877
+ - A provider that sets `savedKey` to `"typesafe"` or `"openai"` gets that key
878
+ as `apiKey`.
879
+ - The executable resolves no packages besides `@drkmttr/adhere`, so an `ask`
880
+ uses `fetch` rather than an SDK.
881
+
882
+ </details>
883
+
760
884
  ### Presets
761
885
 
762
886
  | Preset | Rules | For | What it asks for |
@@ -963,9 +1087,9 @@ the default, `0.6`.
963
1087
  | `--threshold <0 to 1>` | Replace the config's threshold. Per-rule thresholds still apply. |
964
1088
  | `--sufficiency-threshold <0 to 1>` | Replace the config's `sufficiencyThreshold`: a finding warns when its `context` is below it. |
965
1089
  | `--yes`, `-y` | Send the requests without asking first. |
966
- | `--limit <checks>` | Judge at most that many checks. See [Limiting a run](#limiting-a-run). |
1090
+ | `--limit <files>` | Send requests for at most that many files. See [Limiting a run](#limiting-a-run). |
967
1091
  | `--rpm <requests>` | Send at most that many requests a minute. |
968
- | `--filter <glob>` | Read only the files a glob matches. |
1092
+ | `--filter <glob>` | Judge only the files a glob matches. |
969
1093
  | `--deny-warnings` | Fail on warnings as well as errors. |
970
1094
  | `--log-level <level>` | Log the run's work on stderr. See [Logging a run](#logging-a-run). |
971
1095
 
@@ -990,9 +1114,7 @@ the default, `0.6`.
990
1114
  ```
991
1115
 
992
1116
  **Tests** are judged only by rules about tests, whose front matter says `tests`
993
- (see [Rules as Markdown files](#rules-as-markdown-files)). On alchemy, 38% of
994
- the findings from rules not about tests were in test helpers and fixtures. A
995
- test is:
1117
+ (see [Rules as Markdown files](#rules-as-markdown-files)). A test is:
996
1118
 
997
1119
  - a `.test` or `.spec` file, such as `a.test.ts` or `Button.spec.tsx`;
998
1120
  - a fixture or test helper named as one, such as `fixtures.ts`,
@@ -1008,17 +1130,19 @@ takes, and asks:
1008
1130
 
1009
1131
  ```text
1010
1132
  200 files and 14 rules: 2800 checks, 1400 cached.
1011
- Judging the other 1400 takes 200 requests to Jev, plus 1 or more for each file with a finding.
1133
+ Judging 100 files takes an estimated 100 requests to Jev, plus 1 or more for each file with a finding.
1012
1134
  Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, and more for locating findings.
1013
- ? Send 200 requests to Jev, about $0.08? › (Y/n)
1135
+ Estimates cover only code and workspace-package context; symbol/reference input, possible extra request splits, and size-skips are determined at send time.
1136
+ ? 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)
1014
1137
  ```
1015
1138
 
1016
- - It asks only with a terminal on stdin and stdout, and never when the cache
1017
- answers every check. `--yes` sends without asking.
1139
+ - It asks only with a terminal on stdin and stdout, and only when there is
1140
+ something to send. `--yes` sends without asking.
1018
1141
  - The cost is adhere's estimate of the input tokens times the model's price. Jev
1019
1142
  charges only for input tokens: $0.042 a million for `jev-latest`, per
1020
1143
  [TypeSafe AI's models page](https://docs.typesafe.ai/models) in
1021
- September 2026.
1144
+ September 2026. The estimate leaves out `symbols` and `references`, which
1145
+ adhere reads as it sends each file, so a run can cost more than it says.
1022
1146
  - A run that stops loses nothing: files finished before it stopped are cached,
1023
1147
  so running again continues from there.
1024
1148
 
@@ -1040,22 +1164,24 @@ Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, a
1040
1164
 
1041
1165
  Three flags bound what a run does:
1042
1166
 
1043
- | Flag | Effect |
1044
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1045
- | `--limit <checks>` | Judges at most that many checks, 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 judges nothing. |
1046
- | `--rpm <requests>` | Sends at most that many requests to Jev a minute, evenly spaced, retries included. The plan says about how long they take. |
1047
- | `--filter <glob>` | Reads 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. |
1167
+ | Flag | Effect |
1168
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1169
+ | `--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. |
1170
+ | `--rpm <requests>` | Sends at most that many requests to Jev a minute, evenly spaced, retries included. The plan says about how long they take. |
1171
+ | `--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. |
1048
1172
 
1049
1173
  ```sh
1050
- adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 200 --rpm 30
1174
+ adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 100 --rpm 30
1051
1175
  ```
1052
1176
 
1177
+ `--limit` counts files, not requests: one file can take several.
1178
+
1053
1179
  ### Logging a run
1054
1180
 
1055
- | Level | What it logs, on stderr |
1056
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1057
- | `--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, the HTTP status, how long it took, and Cloudflare's Ray ID. A failed attempt is logged even when a retry hides it. |
1058
- | `--log-level trace` | Also each file as it is read, planned, and answered from the cache, and the first 4000 characters of any error Jev's API answers with. |
1181
+ | Level | What it logs, on stderr |
1182
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1183
+ | `--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. |
1184
+ | `--log-level trace` | Also each file as it is read, planned, and answered from the cache. |
1059
1185
 
1060
1186
  stdout still carries only the report, so the log can go to a file of its own:
1061
1187
 
@@ -1076,8 +1202,11 @@ adhere lint --log-level debug 2> adhere.log
1076
1202
  ### Comments
1077
1203
 
1078
1204
  Jev reads a file's code, not its comments. adhere takes every comment out before
1079
- it hashes or sends a file, blanking it so every line keeps its number. The
1080
- report's excerpts still show them.
1205
+ it hashes or sends a file, or a declaration from
1206
+ [`references`](#what-jev-reads), blanking it so every line keeps its number.
1207
+ The report's excerpts still show them. Compiler-provided documentation and
1208
+ JSDoc tags still reach Jev in the type/docs strings [`symbols`](#what-jev-reads)
1209
+ sends; these are not editor hovers.
1081
1210
 
1082
1211
  Jev takes what comments claim at their word, which hid real violations in the
1083
1212
  studies: see [Writing good rules](#writing-good-rules).
@@ -1128,15 +1257,15 @@ paid request, and Jev's answers vary a little from run to run, so a committed
1128
1257
  cache gives everyone and CI the same findings without paying for them again, and
1129
1258
  a pull request changes the judgments of only the files it changes.
1130
1259
 
1131
- | What changed | What is judged again |
1132
- | ------------------------------------------------------------ | ----------------------------------------------------- |
1133
- | A file's code | Every rule, for that file. |
1134
- | A file's comments alone | Nothing. |
1135
- | A file is moved or copied | Nothing. Identical files share their judgments. |
1136
- | A rule's text, a matcher, or the source of its `appendState` | That rule, for every file. |
1137
- | What a rule `reads`, such as the workspace's packages | That rule, for every file. |
1138
- | The threshold is lowered | Nothing. Cached judgments newly above it are located. |
1139
- | adhere asks its questions differently, after an upgrade | Every rule, once. |
1260
+ | What changed | What is judged again |
1261
+ | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
1262
+ | A file's code | Every rule, for that file. |
1263
+ | A file's comments alone | Nothing. |
1264
+ | A file is moved or copied | Nothing. Identical files share their judgments. |
1265
+ | A rule's text, a matcher, or the source of its `appendState` | That rule, for every file. |
1266
+ | Anything else Jev read: a file it imports, the workspace's packages, or the config's `reads` | Nothing, so a judgment can go stale. Delete the cache to judge every file again. |
1267
+ | The threshold is lowered | Nothing. Cached judgments newly above it are located. |
1268
+ | adhere asks its questions differently, after an upgrade | Every rule, once. |
1140
1269
 
1141
1270
  To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1142
1271
 
@@ -1159,8 +1288,8 @@ To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1159
1288
  - `files/` holds Jev's answers by the content they are about. Each cache file is
1160
1289
  named for the hash of a source file's content as Jev reads it, without its
1161
1290
  comments.
1162
- - Each answer sits under a fingerprint of the model and everything the rule
1163
- sends: its text, its matchers, what it reads, and its `appendState`'s source.
1291
+ - Each answer sits under a fingerprint of the model and the rule: its text, its
1292
+ matchers, and its `appendState`'s source.
1164
1293
  - `tallies/` keeps the linter check's answers, one tally per rule, under a key
1165
1294
  that changes with the rule's text and the model.
1166
1295
  - A cache file is written once and never changed: it is also named for the hash
@@ -1235,7 +1364,7 @@ adhere skill fix | codex exec --sandbox workspace-write \
1235
1364
  2. **Judge.** One request per file asks Jev, for each rule, how likely the file
1236
1365
  is to break it.
1237
1366
  3. **Locate.** A second request, only for rules above their threshold, asks
1238
- which section and lines break the rule, and whether the file shows enough to
1367
+ which section and lines break the rule, and whether Jev sees enough to
1239
1368
  decide.
1240
1369
 
1241
1370
  A file too long for Jev's context is skipped and counted in the summary.
@@ -1250,9 +1379,9 @@ statements of its body. Jev reads the file as these sections.
1250
1379
 
1251
1380
  **Judge: one request per file.**
1252
1381
 
1253
- - The state is the file's sections under their numbers from 1, as
1254
- `{ "1": "…", "2": "…" }`, without their comments (see [Comments](#comments)),
1255
- and nothing else.
1382
+ - The state is `code`, the file's sections under their numbers from 1, as
1383
+ `{ code: { "1": "…", "2": "…" } }`, without their comments (see
1384
+ [Comments](#comments)), beside what the config [`reads`](#what-jev-reads).
1256
1385
  - Each rule is one `noul` (yes/no probability) question that carries the rule's
1257
1386
  description and details, and its code under its words, `must` and `never`, or a guideline's
1258
1387
  `should` and `should_not`. It asks whether the file diverges from the pattern
@@ -1260,14 +1389,14 @@ statements of its body. Jev reads the file as these sections.
1260
1389
  only code that must never be written, whether the file contains that code.
1261
1390
  - Criteria for each answer draw the line at the rule's scope, so a file with no
1262
1391
  code the rule is about is a no.
1263
- - No rule sits in the shared state, so a rule's probability depends only on the
1264
- file and that rule, not on which other rules share the request. Every question
1392
+ - No rule sits in the state, so a rule's probability depends only on the state
1393
+ and that rule, not on which other rules share the request. Every question
1265
1394
  shares the state cost of the request.
1266
1395
  - On up to 10 files per project rule, the rule's question has a second one
1267
1396
  beside it, the linter check (see
1268
1397
  [Check for contradictions](#check-for-contradictions)), with the same fields.
1269
- - A rule with `appendState` or `reads` has requests of its own, here and below,
1270
- whose state adds to this one.
1398
+ - A rule with `appendState` has requests of its own, here and below, whose
1399
+ state adds to this one.
1271
1400
 
1272
1401
  **Locate: a second request, only when at least one rule's probability is above
1273
1402
  its threshold.** Per flagged rule:
@@ -1283,18 +1412,20 @@ its threshold.** Per flagged rule:
1283
1412
  for 94% of real findings, most often as one line
1284
1413
  ([study](eval/studies/pinpoint.md)).
1285
1414
  - One `noul` carrying the rule: whether you can tell if `code` breaks it from
1286
- `code` alone, without knowing what other files, libraries, services, or
1287
- configuration do. It yields the `context` score and its warning.
1415
+ the state alone, without knowing what other files, libraries, services, or
1416
+ configuration do beyond what it shows. It yields the `context` score and its
1417
+ warning.
1288
1418
 
1289
1419
  **Limits.** Jev reads at most 32k tokens of state and one question together, and
1290
1420
  64k tokens in a request. adhere estimates tokens from the JSON it sends, at
1291
- about three bytes a token.
1421
+ about three bytes a token, and leaves 1k of room for what Jev wraps around it.
1292
1422
 
1293
1423
  - When a file's questions would not fit in one request, they are split across
1294
1424
  several.
1295
- - A file whose code and longest question would not fit together, or that has
1425
+ - A file whose state and longest question would not fit together, or that has
1296
1426
  more than 255 sections, is skipped and counted in the summary, and the rest of
1297
- the run goes on.
1427
+ the run goes on. What adhere reads is never cut short to fit, so large
1428
+ `references` can skip a file.
1298
1429
  - So is a file Jev itself counts as over its context, which text denser than the
1299
1430
  estimate, such as CJK, can cause.
1300
1431
 
@@ -1310,8 +1441,10 @@ Names and layouts from older versions:
1310
1441
  | through 0.7 | The `effect` preset checked seven conventions a linter checks exactly. | They are left to `@effect/tsgo`. The notes on `effect` under [Presets](#presets) say how to turn them on. |
1311
1442
  | 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. |
1312
1443
  | before 0.13 | `.adhere/tsconfig.json` included only `config.ts`. | Make its `include` `["**/*.ts"]` to give rules written in TypeScript their types. |
1313
- | 0.13.1 | The workspace's packages had a key of their own, `includeWorkspacePackages: true`. | adhere no longer reads it, and a rule that still has it gets no packages. Write `reads: ["workspacePackages"]` in its place. |
1444
+ | 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. |
1314
1445
  | 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. |
1446
+ | 0.15.0 and before | A rule named what Jev reads beside its code, in `reads`. | `reads` is the config's, for every rule, and a rule that has it refuses the run: delete it. Jev reads all three by default. See [What Jev reads](#what-jev-reads). |
1447
+ | 0.15.0 and before | `--limit` counted rule checks. | It counts files. |
1315
1448
  | 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. |
1316
1449
 
1317
1450
  ## Development
@@ -1325,24 +1458,31 @@ adhere lint --preset effect
1325
1458
  ```
1326
1459
 
1327
1460
  - In a checkout no platform package is installed, so `bin/adhere.js` runs
1328
- `src/main.ts` with Bun instead.
1461
+ `src/main.ts` with Bun instead, and `symbols` and `references` use the
1462
+ checkout's own TypeScript.
1329
1463
  - `bun run typecheck` runs `tsc` and the linter.
1330
1464
  - `bun run test` runs the tests.
1465
+ - `bun scripts/verify-npm.ts`, after `bun run build:npm`, installs the built
1466
+ packages in a scratch project and checks that they read `symbols` and
1467
+ `references`. It needs Node 24 or later, and sends Jev nothing.
1331
1468
 
1332
1469
  ### Native executable
1333
1470
 
1334
- | Command | What it builds |
1335
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1336
- | `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. |
1337
- | `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)). |
1471
+ | Command | What it builds |
1472
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1473
+ | `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`. |
1474
+ | `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)). |
1338
1475
 
1339
1476
  Each platform's executable is its own npm package,
1340
1477
  `@drkmttr/adhere-<platform>-<arch>`, limited by `os` and `cpu`, and an optional
1341
1478
  dependency of `@drkmttr/adhere`, so an install fetches only its own machine's.
1342
- The package's `bin/adhere.js` finds it and runs it with Node. The same
1479
+ Each depends on TypeScript's native compiler for its platform,
1480
+ `@typescript/typescript-<platform>-<arch>`, pinned to exactly
1481
+ `7.1.0-dev.20261009.1` to match the SDK. The package's
1482
+ `bin/adhere.js` finds both, and runs the executable with Node. The same
1343
1483
  executables are attached to each GitHub release, named for `uname -s` and
1344
- `uname -m` as in `adhere-Linux-x86_64`, so [installing](#install) one needs no
1345
- Node.
1484
+ `uname -m` as in `adhere-Linux-x86_64`, with `LICENSE` and
1485
+ `THIRD_PARTY_NOTICES.txt`, which go with any copy you redistribute.
1346
1486
 
1347
1487
  `adhere --version` tells the builds apart:
1348
1488
 
@@ -1376,6 +1516,9 @@ git push origin v0.3.0
1376
1516
  Release, and publishes each package before `@drkmttr/adhere`, which it first
1377
1517
  lists them in as `optionalDependencies` at the same version.
1378
1518
 
1519
+ CI, and the publish workflow before it publishes, run `scripts/verify-npm.ts`
1520
+ on Linux and on both Macs. Windows is built, but not run.
1521
+
1379
1522
  <details>
1380
1523
  <summary>More on publishing</summary>
1381
1524