@drkmttr/adhere 0.15.0 → 0.18.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 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,66 @@ 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` | Whole declarations of symbols the code uses from other files of the repo, keyed by `symbol:repository-relative pathname`: a function with its body, a method without the rest of its class. |
580
+
581
+ `symbols` omits standard-library and Node/Bun runtime built-ins, including their
582
+ methods. Project and third-party symbols remain, even when they share a built-in
583
+ name. Built-in type names can still appear inside those symbols' type signatures.
584
+
585
+ `references` is a flat string-to-string map. Keys keep the symbol's name as used
586
+ in the target code, including import aliases, and the pathname's original casing.
587
+ Values are complete declaration excerpts, not incoming reference call sites.
588
+ Overlapping declarations under one key are merged; disjoint excerpts are joined
589
+ with `…` between them. For example, code importing `twice as scale` can receive:
590
+
591
+ ```json
592
+ {
593
+ "references": {
594
+ "__comment": "Declarations of symbols used by the target code, provided only as context. Keys are symbol:repository-relative pathname; values are source excerpts, not incoming call sites.",
595
+ "scale:src/Math.ts": "export function twice(value: number): number {\n return value * 2;\n}"
596
+ }
597
+ }
598
+ ```
599
+
600
+ When sent to Jev, each selected `references`, `symbols`, and `workspacePackages`
601
+ map includes a reserved `__comment` explaining its contents. These comments are
602
+ included before `appendState` hooks and restored after hooks. Top-level guidance
603
+ also tells Jev that these maps are context only: judge the target `code`, not the
604
+ additional declarations or metadata.
605
+
606
+ Each symbol's complete resolved content is truncated to 256 characters by
607
+ default. Set `maxSymbolLength` in the config to change this limit. Documentation
608
+ and JSDoc tags are first capped separately by `maxSymbolDocumentationLength`, also
609
+ 256 by default; `0` omits documentation and tags. Smaller limits reduce request
610
+ size but can cut off type signatures, overloads, useful details, or examples.
611
+
612
+ To read fewer, list the ones you want in the config's `reads`:
613
+
614
+ ```ts
615
+ // .adhere/config.ts
616
+ import { defineConfig } from "@drkmttr/adhere";
617
+
618
+ export default defineConfig({
619
+ reads: ["workspacePackages"], // no symbols or references
620
+ });
621
+ ```
622
+
623
+ `reads: []` sends the code alone.
558
624
 
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.
625
+ Each name is the key Jev reads it under, so a rule's description can name it in
626
+ backticks, as the questions name `code`:
563
627
 
564
628
  ````md
565
629
  ---
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"]
630
+ 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
631
  ---
569
632
 
570
633
  ## Must
@@ -580,25 +643,31 @@ import { parse } from "orders-core";
580
643
  ```
581
644
  ````
582
645
 
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`.
646
+ - `symbols` and `references` use the SDK and native compiler pinned to exactly
647
+ `7.1.0-dev.20261009.1`, which the npm package installs. This prerelease contains
648
+ the tuple-reference serialization fix from [microsoft/TypeScript#64080](https://github.com/microsoft/TypeScript/issues/64080);
649
+ SDK and native executable must match that exact version. They use each file's
650
+ `tsconfig.json`, and work without one.
651
+ - `symbols` is still a name-to-string map. Distinct binding/narrowing types get
652
+ distinct blocks; docs and JSDoc tags appear once per canonical symbol per file.
653
+ It has no IDE labels or selected-call overload presentation. Symbol/type queries
654
+ are batched, with independent SDK requests chunked to at most 8.
591
655
  - A name adhere does not have refuses the run, so a misspelled one does not go
592
- unnoticed.
656
+ unnoticed. So does `reads` in a rule: it is the config's alone.
593
657
  - What adhere has no name for, such as a file of the repository's, a rule in
594
658
  TypeScript adds with [`appendState`](#rules-as-typescript-files).
595
659
 
660
+ **Cost and privacy.** `symbols` and `references` send Jev more than the file it
661
+ judges: whole declarations from other files, plus compiler-provided documentation
662
+ and JSDoc tags in `symbols`, even with `includeComments` off. The plan's estimate leaves their tokens out. Leave them
663
+ out of `reads` to send less.
664
+
596
665
  <details>
597
- <summary>More on requests and where the packages come from</summary>
666
+ <summary>Where the packages come from</summary>
598
667
 
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`.
668
+ - The packages are read on every run from the working directory's
669
+ `package.json`, whose `workspaces` npm, Bun, and Yarn read, and from
670
+ `pnpm-workspace.yaml`'s `packages`.
602
671
  - In a pattern, `*` is any directory and `**` any depth of them, and one that
603
672
  starts with `!` leaves out what it matches. Each matched directory's
604
673
  `package.json` gives the name, and its directory is given from the working
@@ -606,6 +675,8 @@ There is one name so far:
606
675
  - Nothing need be installed, so a run in CI reads the same packages as one on a
607
676
  laptop.
608
677
  - A working directory that is not a workspace's root has none.
678
+ - Only an import whose path is a plain string counts, and one of a subpath, such
679
+ as `orders-core/client`, counts as its package's.
609
680
 
610
681
  </details>
611
682
 
@@ -637,7 +708,8 @@ export default defineRule({
637
708
  name `code`.
638
709
  - A `RULE.ts` is imported, as a config is, so its code runs on every lint.
639
710
  - 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.
711
+ what its hook adds. Each of those requests sends the file and its context
712
+ again.
641
713
  - What the hook reads outside the file is not part of the cache: when
642
714
  `db/schema.sql` changes, a file already judged is not judged again until it or
643
715
  the rule changes.
@@ -647,7 +719,8 @@ export default defineRule({
647
719
 
648
720
  - The state is what Jev reads beside each question: `code`, the file's sections
649
721
  as Jev reads them, under their numbers from 1, as in
650
- `{ code: { "1": "import …", "2": "export const …" } }`.
722
+ `{ code: { "1": "import …", "2": "export const …" } }`, beside what the
723
+ config [`reads`](#what-jev-reads).
651
724
  - `file` is the file's absolute `path`, and its `contents` as written, comments
652
725
  and all.
653
726
  - Nothing the hook returns is checked, so it can change `code` too, or replace
@@ -691,9 +764,9 @@ and they apply project-wide.
691
764
  ## Config
692
765
 
693
766
  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):
767
+ `.adhere/rules/` and any `--preset`. A config names presets, sets the model,
768
+ the thresholds, and what Jev reads, or gives rules inline. It sits in the
769
+ working directory of the repo being audited, at one of these paths (keep one):
697
770
 
698
771
  - `.adhere/config.ts`, the default, next to the rules
699
772
  - `adhere.config.ts`
@@ -706,7 +779,10 @@ export default defineConfig({
706
779
  model: "jev-latest",
707
780
  threshold: 0.8,
708
781
  sufficiencyThreshold: 0.6,
782
+ reads: ["workspacePackages", "symbols", "references"],
709
783
  presets: ["effect"],
784
+ maxSymbolDocumentationLength: 256,
785
+ maxSymbolLength: 256,
710
786
  exclude: ["**/generated/**"],
711
787
  overrides: { "effect/basics/instrument-with-pipe": "off" },
712
788
  rules: {
@@ -724,16 +800,20 @@ type UserId = typeof UserId.Type
724
800
  });
725
801
  ```
726
802
 
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). |
803
+ | Key | Default | Meaning |
804
+ | ------------------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
805
+ | `model` | `"jev-latest"` | The model id sent to TypeSafe. |
806
+ | `threshold` | `0.8` | Report a rule when Jev's probability is above this. |
807
+ | `sufficiencyThreshold` | `0.6` | Below it, a finding warns that Jev may not see enough to decide. |
808
+ | `presets` | none | Built-in rule sets, or their topics. See [Presets](#presets). |
809
+ | `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. |
810
+ | `exclude` | none | Globs, relative to the working directory, of files no rule judges. |
811
+ | `overrides` | none | Settings for a rule by its id. See [Overrides](#overrides). |
812
+ | `includeComments` | `false` | Send every comment to Jev. See [Comments](#comments). |
813
+ | `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). |
814
+ | `maxSymbolDocumentationLength` | `256` | Maximum characters of each symbol's combined documentation and JSDoc tags, before `maxSymbolLength` applies. A non-negative integer; `0` omits them. |
815
+ | `maxSymbolLength` | `256` | Maximum characters of each complete `symbols[name]` string, including type signatures and documentation. A non-negative integer; `0` leaves names with empty content. |
816
+ | `provider` | TypeSafe's Jev | Where the questions go instead, with its own model. See [Providers](#providers). |
737
817
 
738
818
  - An invalid shape refuses the run.
739
819
  - A config can import other files by relative path, but no packages besides
@@ -757,6 +837,86 @@ type UserId = typeof UserId.Type
757
837
 
758
838
  </details>
759
839
 
840
+ ### Providers
841
+
842
+ adhere asks TypeSafe's Jev by default. A provider asks another decision model
843
+ instead, and adhere exports one for each model
844
+ [the providers study](eval/studies/providers.md) measured:
845
+
846
+ ```ts
847
+ import { defineConfig, openai } from "@drkmttr/adhere";
848
+
849
+ // Asks with OPENAI_API_KEY, or the key `adhere login --openai` saved.
850
+ export default defineConfig({ provider: openai() });
851
+ ```
852
+
853
+ | Provider | Asks | Options |
854
+ | ------------ | --------------------------------------------------------------- | -------------------------------- |
855
+ | `jev` | TypeSafe's Jev, `jev-latest` by default | `apiKey`, `model` |
856
+ | `openai` | OpenAI's `gpt-6-luna`, on its Decisions API | `apiKey`, `model` |
857
+ | `cloudflare` | Cloudflare's `clef`, by default, or `clef-flash`, on Workers AI | `accountId`, `apiToken`, `model` |
858
+
859
+ - Without an `apiKey`, `jev` and `openai` ask with the key from
860
+ [Login](#login).
861
+ - Each also takes `fetch`, what sends its requests.
862
+ - Clef cuts a request too long for it short rather than refuse it, so
863
+ `cloudflare` skips a file whose request reaches clef's limit: 64,000 tokens,
864
+ or 24,000 for clef-flash.
865
+
866
+ <details>
867
+ <summary>Writing a provider</summary>
868
+
869
+ A provider is a model's name, and an `ask` that answers adhere's questions
870
+ about a state:
871
+
872
+ ```ts
873
+ import { defineConfig, type Provider, RequestFailed } from "@drkmttr/adhere";
874
+
875
+ const mine: Provider = {
876
+ model: "my-decision-model",
877
+ ask: async (state, questions, { signal }) => {
878
+ const response = await fetch("https://decisions.example.com/v1/decide", {
879
+ method: "POST",
880
+ headers: { "Content-Type": "application/json" },
881
+ body: JSON.stringify({ state, questions }),
882
+ signal,
883
+ });
884
+ if (!response.ok) {
885
+ throw new RequestFailed(`My model answered HTTP ${response.status}`, response.status);
886
+ }
887
+ return { answers: await response.json() };
888
+ },
889
+ };
890
+
891
+ export default defineConfig({ provider: mine });
892
+ ```
893
+
894
+ - `state` is the file's code by section under `code`, beside what adhere read
895
+ for it.
896
+ - Each question has an `id`, the `question` as adhere phrases it, and the
897
+ `fields` it names in backticks, such as `rule`, `must`, and `never`. A `noul`
898
+ asks whether something holds. A `choice` asks for the key of one of its
899
+ `options`, and can have just one.
900
+ - `ask` returns `{ answers }`: by id, a `noul`'s probability from 0 to 1, or a
901
+ `choice`'s key. An id left out goes unanswered: its rule is not reported, and
902
+ is asked again on the next run.
903
+ - A rejection stops the run, but for three errors adhere exports:
904
+ `ContextOverflow` gets one retry without `references` when that map is present;
905
+ if the request still overflows, the file is skipped. `RequestBlocked` reports
906
+ it blocked, and `RequestFailed` is tried again, up to three times, when its
907
+ `status` is 0, for no answer, or a 408, 429, or 5xx.
908
+ - adhere paces calls with `--rpm`, aborts `signal` after 30 seconds, and splits
909
+ requests to fit Jev's 64,000 tokens. A model that takes less splits further in
910
+ its `ask`.
911
+ - `model` replaces the config's `model`. The cache keeps answers by it, so a
912
+ new model is asked again.
913
+ - A provider that sets `savedKey` to `"typesafe"` or `"openai"` gets that key
914
+ as `apiKey`.
915
+ - The executable resolves no packages besides `@drkmttr/adhere`, so an `ask`
916
+ uses `fetch` rather than an SDK.
917
+
918
+ </details>
919
+
760
920
  ### Presets
761
921
 
762
922
  | Preset | Rules | For | What it asks for |
@@ -963,9 +1123,9 @@ the default, `0.6`.
963
1123
  | `--threshold <0 to 1>` | Replace the config's threshold. Per-rule thresholds still apply. |
964
1124
  | `--sufficiency-threshold <0 to 1>` | Replace the config's `sufficiencyThreshold`: a finding warns when its `context` is below it. |
965
1125
  | `--yes`, `-y` | Send the requests without asking first. |
966
- | `--limit <checks>` | Judge at most that many checks. See [Limiting a run](#limiting-a-run). |
1126
+ | `--limit <files>` | Send requests for at most that many files. See [Limiting a run](#limiting-a-run). |
967
1127
  | `--rpm <requests>` | Send at most that many requests a minute. |
968
- | `--filter <glob>` | Read only the files a glob matches. |
1128
+ | `--filter <glob>` | Judge only the files a glob matches. |
969
1129
  | `--deny-warnings` | Fail on warnings as well as errors. |
970
1130
  | `--log-level <level>` | Log the run's work on stderr. See [Logging a run](#logging-a-run). |
971
1131
 
@@ -990,9 +1150,7 @@ the default, `0.6`.
990
1150
  ```
991
1151
 
992
1152
  **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:
1153
+ (see [Rules as Markdown files](#rules-as-markdown-files)). A test is:
996
1154
 
997
1155
  - a `.test` or `.spec` file, such as `a.test.ts` or `Button.spec.tsx`;
998
1156
  - a fixture or test helper named as one, such as `fixtures.ts`,
@@ -1008,17 +1166,19 @@ takes, and asks:
1008
1166
 
1009
1167
  ```text
1010
1168
  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.
1169
+ Judging 100 files takes an estimated 100 requests to Jev, plus 1 or more for each file with a finding.
1012
1170
  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)
1171
+ Estimates cover only code and workspace-package context; symbol/reference input, possible extra request splits, and size-skips are determined at send time.
1172
+ ? 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
1173
  ```
1015
1174
 
1016
- - It asks only with a terminal on stdin and stdout, and never when the cache
1017
- answers every check. `--yes` sends without asking.
1175
+ - It asks only with a terminal on stdin and stdout, and only when there is
1176
+ something to send. `--yes` sends without asking.
1018
1177
  - The cost is adhere's estimate of the input tokens times the model's price. Jev
1019
1178
  charges only for input tokens: $0.042 a million for `jev-latest`, per
1020
1179
  [TypeSafe AI's models page](https://docs.typesafe.ai/models) in
1021
- September 2026.
1180
+ September 2026. The estimate leaves out `symbols` and `references`, which
1181
+ adhere reads as it sends each file, so a run can cost more than it says.
1022
1182
  - A run that stops loses nothing: files finished before it stopped are cached,
1023
1183
  so running again continues from there.
1024
1184
 
@@ -1040,22 +1200,24 @@ Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, a
1040
1200
 
1041
1201
  Three flags bound what a run does:
1042
1202
 
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. |
1203
+ | Flag | Effect |
1204
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1205
+ | `--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. |
1206
+ | `--rpm <requests>` | Sends at most that many requests to Jev a minute, evenly spaced, retries included. The plan says about how long they take. |
1207
+ | `--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
1208
 
1049
1209
  ```sh
1050
- adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 200 --rpm 30
1210
+ adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 100 --rpm 30
1051
1211
  ```
1052
1212
 
1213
+ `--limit` counts files, not requests: one file can take several.
1214
+
1053
1215
  ### Logging a run
1054
1216
 
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. |
1217
+ | Level | What it logs, on stderr |
1218
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1219
+ | `--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. |
1220
+ | `--log-level trace` | Also each file as it is read, planned, and answered from the cache. |
1059
1221
 
1060
1222
  stdout still carries only the report, so the log can go to a file of its own:
1061
1223
 
@@ -1076,8 +1238,11 @@ adhere lint --log-level debug 2> adhere.log
1076
1238
  ### Comments
1077
1239
 
1078
1240
  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.
1241
+ it hashes or sends a file, or a declaration from
1242
+ [`references`](#what-jev-reads), blanking it so every line keeps its number.
1243
+ The report's excerpts still show them. Compiler-provided documentation and
1244
+ JSDoc tags still reach Jev in the type/docs strings [`symbols`](#what-jev-reads)
1245
+ sends; these are not editor hovers.
1081
1246
 
1082
1247
  Jev takes what comments claim at their word, which hid real violations in the
1083
1248
  studies: see [Writing good rules](#writing-good-rules).
@@ -1128,15 +1293,15 @@ paid request, and Jev's answers vary a little from run to run, so a committed
1128
1293
  cache gives everyone and CI the same findings without paying for them again, and
1129
1294
  a pull request changes the judgments of only the files it changes.
1130
1295
 
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. |
1296
+ | What changed | What is judged again |
1297
+ | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
1298
+ | A file's code | Every rule, for that file. |
1299
+ | A file's comments alone | Nothing. |
1300
+ | A file is moved or copied | Nothing. Identical files share their judgments. |
1301
+ | A rule's text, a matcher, or the source of its `appendState` | That rule, for every file. |
1302
+ | 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. |
1303
+ | The threshold is lowered | Nothing. Cached judgments newly above it are located. |
1304
+ | adhere asks its questions differently, after an upgrade | Every rule, once. |
1140
1305
 
1141
1306
  To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1142
1307
 
@@ -1159,8 +1324,8 @@ To keep the cache out of diffs, mark it as generated in `.gitattributes`:
1159
1324
  - `files/` holds Jev's answers by the content they are about. Each cache file is
1160
1325
  named for the hash of a source file's content as Jev reads it, without its
1161
1326
  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.
1327
+ - Each answer sits under a fingerprint of the model and the rule: its text, its
1328
+ matchers, and its `appendState`'s source.
1164
1329
  - `tallies/` keeps the linter check's answers, one tally per rule, under a key
1165
1330
  that changes with the rule's text and the model.
1166
1331
  - A cache file is written once and never changed: it is also named for the hash
@@ -1235,7 +1400,7 @@ adhere skill fix | codex exec --sandbox workspace-write \
1235
1400
  2. **Judge.** One request per file asks Jev, for each rule, how likely the file
1236
1401
  is to break it.
1237
1402
  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
1403
+ which section and lines break the rule, and whether Jev sees enough to
1239
1404
  decide.
1240
1405
 
1241
1406
  A file too long for Jev's context is skipped and counted in the summary.
@@ -1250,9 +1415,9 @@ statements of its body. Jev reads the file as these sections.
1250
1415
 
1251
1416
  **Judge: one request per file.**
1252
1417
 
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.
1418
+ - The state is `code`, the file's sections under their numbers from 1, as
1419
+ `{ code: { "1": "…", "2": "…" } }`, without their comments (see
1420
+ [Comments](#comments)), beside what the config [`reads`](#what-jev-reads).
1256
1421
  - Each rule is one `noul` (yes/no probability) question that carries the rule's
1257
1422
  description and details, and its code under its words, `must` and `never`, or a guideline's
1258
1423
  `should` and `should_not`. It asks whether the file diverges from the pattern
@@ -1260,14 +1425,14 @@ statements of its body. Jev reads the file as these sections.
1260
1425
  only code that must never be written, whether the file contains that code.
1261
1426
  - Criteria for each answer draw the line at the rule's scope, so a file with no
1262
1427
  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
1428
+ - No rule sits in the state, so a rule's probability depends only on the state
1429
+ and that rule, not on which other rules share the request. Every question
1265
1430
  shares the state cost of the request.
1266
1431
  - On up to 10 files per project rule, the rule's question has a second one
1267
1432
  beside it, the linter check (see
1268
1433
  [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.
1434
+ - A rule with `appendState` has requests of its own, here and below, whose
1435
+ state adds to this one.
1271
1436
 
1272
1437
  **Locate: a second request, only when at least one rule's probability is above
1273
1438
  its threshold.** Per flagged rule:
@@ -1283,18 +1448,23 @@ its threshold.** Per flagged rule:
1283
1448
  for 94% of real findings, most often as one line
1284
1449
  ([study](eval/studies/pinpoint.md)).
1285
1450
  - 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.
1451
+ the state alone, without knowing what other files, libraries, services, or
1452
+ configuration do beyond what it shows. It yields the `context` score and its
1453
+ warning.
1288
1454
 
1289
1455
  **Limits.** Jev reads at most 32k tokens of state and one question together, and
1290
1456
  64k tokens in a request. adhere estimates tokens from the JSON it sends, at
1291
- about three bytes a token.
1457
+ about three bytes a token, and leaves 1k of room for what Jev wraps around it.
1292
1458
 
1293
1459
  - When a file's questions would not fit in one request, they are split across
1294
1460
  several.
1295
- - A file whose code and longest question would not fit together, or that has
1296
- more than 255 sections, is skipped and counted in the summary, and the rest of
1297
- the run goes on.
1461
+ - If a file's state and longest question would not fit together, or a provider
1462
+ reports a context overflow, adhere retries once without `references` when that
1463
+ map is present. Target code, `symbols`, and other context remain intact; no
1464
+ declaration excerpt is shortened to fit. Removing references may still leave
1465
+ the request too large, in which case the file is skipped and counted in the
1466
+ summary. A file with more than 255 code sections is also skipped, and the rest
1467
+ of the run goes on.
1298
1468
  - So is a file Jev itself counts as over its context, which text denser than the
1299
1469
  estimate, such as CJK, can cause.
1300
1470
 
@@ -1310,8 +1480,10 @@ Names and layouts from older versions:
1310
1480
  | 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
1481
  | 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
1482
  | 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. |
1483
+ | 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
1484
  | 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. |
1485
+ | 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). |
1486
+ | 0.15.0 and before | `--limit` counted rule checks. | It counts files. |
1315
1487
  | 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
1488
 
1317
1489
  ## Development
@@ -1325,24 +1497,31 @@ adhere lint --preset effect
1325
1497
  ```
1326
1498
 
1327
1499
  - In a checkout no platform package is installed, so `bin/adhere.js` runs
1328
- `src/main.ts` with Bun instead.
1500
+ `src/main.ts` with Bun instead, and `symbols` and `references` use the
1501
+ checkout's own TypeScript.
1329
1502
  - `bun run typecheck` runs `tsc` and the linter.
1330
1503
  - `bun run test` runs the tests.
1504
+ - `bun scripts/verify-npm.ts`, after `bun run build:npm`, installs the built
1505
+ packages in a scratch project and checks that they read `symbols` and
1506
+ `references`. It needs Node 24 or later, and sends Jev nothing.
1331
1507
 
1332
1508
  ### Native executable
1333
1509
 
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)). |
1510
+ | Command | What it builds |
1511
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1512
+ | `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`. |
1513
+ | `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
1514
 
1339
1515
  Each platform's executable is its own npm package,
1340
1516
  `@drkmttr/adhere-<platform>-<arch>`, limited by `os` and `cpu`, and an optional
1341
1517
  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
1518
+ Each depends on TypeScript's native compiler for its platform,
1519
+ `@typescript/typescript-<platform>-<arch>`, pinned to exactly
1520
+ `7.1.0-dev.20261009.1` to match the SDK. The package's
1521
+ `bin/adhere.js` finds both, and runs the executable with Node. The same
1343
1522
  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.
1523
+ `uname -m` as in `adhere-Linux-x86_64`, with `LICENSE` and
1524
+ `THIRD_PARTY_NOTICES.txt`, which go with any copy you redistribute.
1346
1525
 
1347
1526
  `adhere --version` tells the builds apart:
1348
1527
 
@@ -1376,6 +1555,9 @@ git push origin v0.3.0
1376
1555
  Release, and publishes each package before `@drkmttr/adhere`, which it first
1377
1556
  lists them in as `optionalDependencies` at the same version.
1378
1557
 
1558
+ CI, and the publish workflow before it publishes, run `scripts/verify-npm.ts`
1559
+ on Linux and on both Macs. Windows is built, but not run.
1560
+
1379
1561
  <details>
1380
1562
  <summary>More on publishing</summary>
1381
1563