@drkmttr/adhere 0.11.0 → 0.13.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.
Files changed (102) hide show
  1. package/README.md +184 -37
  2. package/package.json +9 -9
  3. package/presets/effect/basics/{instrument-with-pipe.md → instrument-with-pipe/RULE.md} +1 -1
  4. package/presets/effect/config/{business-logic-depends-on-config-service.md → business-logic-depends-on-config-service/RULE.md} +3 -3
  5. package/presets/effect/config/{secrets-are-redacted.md → secrets-are-redacted/RULE.md} +3 -3
  6. package/presets/effect/config/{validate-with-schema.md → validate-with-schema/RULE.md} +4 -4
  7. package/skills/adhere/SKILL.md +206 -118
  8. package/skills/adhere-fix/SKILL.md +9 -8
  9. package/skills/adhere-setup/SKILL.md +244 -0
  10. package/src/cli.ts +114 -25
  11. package/src/config.ts +50 -3
  12. package/src/index.ts +2 -0
  13. package/src/init.ts +193 -27
  14. package/src/install.ts +158 -0
  15. package/src/main.ts +1 -1
  16. package/src/rules.ts +135 -56
  17. package/src/services/AdhereConfig.ts +2 -2
  18. package/src/services/Credentials.ts +4 -4
  19. package/src/services/Jev.http.ts +76 -10
  20. package/src/services/Jev.ts +39 -10
  21. package/src/workflows/audit.ts +23 -12
  22. package/src/workflows/format.ts +172 -35
  23. /package/presets/alchemy/apis/{match-remote-errors-by-tag.md → match-remote-errors-by-tag/RULE.md} +0 -0
  24. /package/presets/alchemy/apis/{schemas-at-trust-boundaries.md → schemas-at-trust-boundaries/RULE.md} +0 -0
  25. /package/presets/alchemy/data/{commit-migrations-then-deploy.md → commit-migrations-then-deploy/RULE.md} +0 -0
  26. /package/presets/alchemy/data/{hyperdrive-direct-origin.md → hyperdrive-direct-origin/RULE.md} +0 -0
  27. /package/presets/alchemy/data/{migrations-use-direct-url.md → migrations-use-direct-url/RULE.md} +0 -0
  28. /package/presets/alchemy/data/{never-disable-tls-verification.md → never-disable-tls-verification/RULE.md} +0 -0
  29. /package/presets/alchemy/data/{private-endpoint-at-runtime.md → private-endpoint-at-runtime/RULE.md} +0 -0
  30. /package/presets/alchemy/data/{single-migration-owner.md → single-migration-owner/RULE.md} +0 -0
  31. /package/presets/alchemy/durable/{idempotent-event-handlers.md → idempotent-event-handlers/RULE.md} +0 -0
  32. /package/presets/alchemy/durable/{io-inside-steps.md → io-inside-steps/RULE.md} +0 -0
  33. /package/presets/alchemy/durable/{no-self-triggering-bucket-writes.md → no-self-triggering-bucket-writes/RULE.md} +0 -0
  34. /package/presets/alchemy/durable/{stable-step-names.md → stable-step-names/RULE.md} +0 -0
  35. /package/presets/alchemy/durable/{webhook-state-is-truth.md → webhook-state-is-truth/RULE.md} +0 -0
  36. /package/presets/alchemy/providers/{diff-guards-unresolved-props.md → diff-guards-unresolved-props/RULE.md} +0 -0
  37. /package/presets/alchemy/providers/{idempotent-delete.md → idempotent-delete/RULE.md} +0 -0
  38. /package/presets/alchemy/providers/{lazy-layer-construction.md → lazy-layer-construction/RULE.md} +0 -0
  39. /package/presets/alchemy/providers/{reconcile-from-observed-state.md → reconcile-from-observed-state/RULE.md} +0 -0
  40. /package/presets/alchemy/resources/{anchor-paths-with-import-meta.md → anchor-paths-with-import-meta/RULE.md} +0 -0
  41. /package/presets/alchemy/resources/{idempotent-actions-and-seeds.md → idempotent-actions-and-seeds/RULE.md} +0 -0
  42. /package/presets/alchemy/resources/{previews-reference-shared-resources.md → previews-reference-shared-resources/RULE.md} +0 -0
  43. /package/presets/alchemy/resources/{protect-irreplaceable-data.md → protect-irreplaceable-data/RULE.md} +0 -0
  44. /package/presets/alchemy/resources/{remote-state-for-ci.md → remote-state-for-ci/RULE.md} +0 -0
  45. /package/presets/alchemy/resources/{stable-logical-ids.md → stable-logical-ids/RULE.md} +0 -0
  46. /package/presets/alchemy/resources/{stage-unique-physical-names.md → stage-unique-physical-names/RULE.md} +0 -0
  47. /package/presets/alchemy/runtime/{bind-tag-not-implementation.md → bind-tag-not-implementation/RULE.md} +0 -0
  48. /package/presets/alchemy/runtime/{deterministic-config-defaults.md → deterministic-config-defaults/RULE.md} +0 -0
  49. /package/presets/alchemy/runtime/{disposables-in-request-scope.md → disposables-in-request-scope/RULE.md} +0 -0
  50. /package/presets/alchemy/runtime/{lambda-no-post-response-work.md → lambda-no-post-response-work/RULE.md} +0 -0
  51. /package/presets/alchemy/runtime/{no-per-request-work-at-construction.md → no-per-request-work-at-construction/RULE.md} +0 -0
  52. /package/presets/alchemy/runtime/{resolve-config-and-bindings-at-construction.md → resolve-config-and-bindings-at-construction/RULE.md} +0 -0
  53. /package/presets/alchemy/secrets/{let-alchemy-mint-infra-secrets.md → let-alchemy-mint-infra-secrets/RULE.md} +0 -0
  54. /package/presets/alchemy/secrets/{none-in-browser-env.md → none-in-browser-env/RULE.md} +0 -0
  55. /package/presets/alchemy/secrets/{redacted-until-use.md → redacted-until-use/RULE.md} +0 -0
  56. /package/presets/alchemy/secrets/{unwrap-before-compare-or-send.md → unwrap-before-compare-or-send/RULE.md} +0 -0
  57. /package/presets/alchemy/security/{authorize-public-endpoints.md → authorize-public-endpoints/RULE.md} +0 -0
  58. /package/presets/alchemy/security/{github-secret-not-variable.md → github-secret-not-variable/RULE.md} +0 -0
  59. /package/presets/alchemy/security/{identity-from-verified-session.md → identity-from-verified-session/RULE.md} +0 -0
  60. /package/presets/alchemy/security/{keep-auth-protections-on.md → keep-auth-protections-on/RULE.md} +0 -0
  61. /package/presets/alchemy/security/{least-privilege-bindings.md → least-privilege-bindings/RULE.md} +0 -0
  62. /package/presets/alchemy/security/{least-privilege-ci-credentials.md → least-privilege-ci-credentials/RULE.md} +0 -0
  63. /package/presets/alchemy/security/{lookup-failure-is-not-anonymous.md → lookup-failure-is-not-anonymous/RULE.md} +0 -0
  64. /package/presets/effect/basics/{gen-for-sequencing.md → gen-for-sequencing/RULE.md} +0 -0
  65. /package/presets/effect/config/{tests-provide-values-directly.md → tests-provide-values-directly/RULE.md} +0 -0
  66. /package/presets/effect/data/{brand-meaningful-primitives.md → brand-meaningful-primitives/RULE.md} +0 -0
  67. /package/presets/effect/data/{variants-are-tagged-unions.md → variants-are-tagged-unions/RULE.md} +0 -0
  68. /package/presets/effect/errors/{catch-defects-at-boundaries-only.md → catch-defects-at-boundaries-only/RULE.md} +0 -0
  69. /package/presets/effect/errors/{defects-for-bugs.md → defects-for-bugs/RULE.md} +0 -0
  70. /package/presets/effect/errors/{wrap-external-errors.md → wrap-external-errors/RULE.md} +0 -0
  71. /package/presets/effect/services/{dependencies-through-layers.md → dependencies-through-layers/RULE.md} +0 -0
  72. /package/presets/effect/services/{fresh-layer-per-test.md → fresh-layer-per-test/RULE.md} +0 -0
  73. /package/presets/effect/services/{memoize-parameterized-layers.md → memoize-parameterized-layers/RULE.md} +0 -0
  74. /package/presets/effect/services/{no-mutable-state.md → no-mutable-state/RULE.md} +0 -0
  75. /package/presets/effect/services/{operations-have-no-requirements.md → operations-have-no-requirements/RULE.md} +0 -0
  76. /package/presets/effect/services/{test-layers-are-in-memory.md → test-layers-are-in-memory/RULE.md} +0 -0
  77. /package/presets/effect/testing/{test-clock-for-time.md → test-clock-for-time/RULE.md} +0 -0
  78. /package/presets/react/effects/{clean-up-what-they-start.md → clean-up-what-they-start/RULE.md} +0 -0
  79. /package/presets/react/effects/{event-logic-in-handlers.md → event-logic-in-handlers/RULE.md} +0 -0
  80. /package/presets/react/effects/{external-stores-use-sync-external-store.md → external-stores-use-sync-external-store/RULE.md} +0 -0
  81. /package/presets/react/effects/{fetches-ignore-stale-responses.md → fetches-ignore-stale-responses/RULE.md} +0 -0
  82. /package/presets/react/effects/{notify-parents-in-handlers.md → notify-parents-in-handlers/RULE.md} +0 -0
  83. /package/presets/react/server/{no-private-data-to-client.md → no-private-data-to-client/RULE.md} +0 -0
  84. /package/presets/react/server/{server-functions-authorize-and-validate.md → server-functions-authorize-and-validate/RULE.md} +0 -0
  85. /package/presets/react/state/{no-contradictory-state.md → no-contradictory-state/RULE.md} +0 -0
  86. /package/presets/react/state/{no-props-copied-into-state.md → no-props-copied-into-state/RULE.md} +0 -0
  87. /package/presets/react/state/{store-ids-not-copies.md → store-ids-not-copies/RULE.md} +0 -0
  88. /package/presets/security/{constant-time-secret-compare.md → constant-time-secret-compare/RULE.md} +0 -0
  89. /package/presets/security/{no-secrets-in-output.md → no-secrets-in-output/RULE.md} +0 -0
  90. /package/presets/security/{no-untrusted-input-in-commands-or-paths.md → no-untrusted-input-in-commands-or-paths/RULE.md} +0 -0
  91. /package/presets/security/{parameterized-queries.md → parameterized-queries/RULE.md} +0 -0
  92. /package/presets/typescript/async/{concurrent-independent-work.md → concurrent-independent-work/RULE.md} +0 -0
  93. /package/presets/typescript/async/{network-calls-have-timeouts.md → network-calls-have-timeouts/RULE.md} +0 -0
  94. /package/presets/typescript/design/{no-argument-mutation.md → no-argument-mutation/RULE.md} +0 -0
  95. /package/presets/typescript/errors/{no-swallowed-errors.md → no-swallowed-errors/RULE.md} +0 -0
  96. /package/presets/typescript/errors/{release-on-every-path.md → release-on-every-path/RULE.md} +0 -0
  97. /package/presets/typescript/testing/{assert-outcomes.md → assert-outcomes/RULE.md} +0 -0
  98. /package/presets/typescript/testing/{independent-tests.md → independent-tests/RULE.md} +0 -0
  99. /package/presets/typescript/types/{assertions-only-when-proven.md → assertions-only-when-proven/RULE.md} +0 -0
  100. /package/presets/typescript/types/{derive-dont-restate.md → derive-dont-restate/RULE.md} +0 -0
  101. /package/presets/typescript/types/{unrepresentable-invalid-states.md → unrepresentable-invalid-states/RULE.md} +0 -0
  102. /package/presets/typescript/types/{validate-external-data.md → validate-external-data/RULE.md} +0 -0
package/README.md CHANGED
@@ -60,7 +60,8 @@ Without a config or rules, `adhere lint --preset effect` audits against the
60
60
  built-in Effect rules. A finding looks like this:
61
61
 
62
62
  ```text
63
- × data/brand-ports (0.93): A port must be a branded, range-checked integer, never a bare number.
63
+ × data/brand-ports: A port must be a branded, range-checked integer, never a bare number.
64
+ confidence 0.93 · context 0.88
64
65
  ╭─[src/server.ts:6:3]
65
66
  1 │ import { Effect } from "effect";
66
67
  2 │ import { listen } from "./listen.ts";
@@ -78,12 +79,28 @@ built-in Effect rules. A finding looks like this:
78
79
  Schema.brand("Port"),
79
80
  );
80
81
 
82
+ confidence 0–0.74 < 0.8 < 0.85–1 Jev's probability that the file breaks the rule
83
+ context 0–0.49 < 0.6 < 0.70–1 its probability that the file shows enough to decide
84
+
81
85
  Found 1 error.
82
86
  42 files, 3 judged, 39 cached.
83
87
  ```
84
88
 
85
- The header is the rule id, Jev's probability, and the rule's description. A
86
- preset's rule has the preset's name first, as in
89
+ The header is the rule id and the rule's description, and two scores have
90
+ the next line. `confidence` is Jev's probability that the file breaks the
91
+ rule. `context` is its probability that the file shows enough to decide that
92
+ (see below). On a terminal a confidence is amber near its rule's threshold,
93
+ and green above that. Near is within a quarter of the room above the
94
+ threshold, on either side: 0.05 at a threshold of 0.80, so 0.75 to 0.84. A
95
+ rule whose findings are mostly amber turns on where its threshold sits, which
96
+ may be too low. Below the amber a score is red: lint reports no finding with
97
+ a confidence there. A context is colored the same way around the sufficiency
98
+ threshold, so at 0.60 it is amber from 0.50 to 0.69 and red below 0.50, the
99
+ finding to doubt first. A legend under the last finding gives each score's
100
+ threshold between the range in red and the range in green. The threshold and
101
+ each `<` are amber, for the scores between them. Confidence's threshold there is the run's;
102
+ a rule with a threshold of its own has its ranges around that.
103
+ A preset's rule has the preset's name first, as in
87
104
  `effect/basics/gen-for-sequencing`, so a report that mixes presets
88
105
  with a repo's own rules says where each came from.
89
106
  Under it is the section of the file Jev points at, with the line it names
@@ -99,12 +116,12 @@ names no lines, the section is shown alone, at its first line.
99
116
 
100
117
  Jev is also asked whether the file shows enough to decide the rule at all:
101
118
  whether it turns on something the file does not show, such as what another
102
- file, a library, or a service does. When Jev's probability that it does is
103
- below 0.6, the finding gets a warning under its code, with the probability
104
- that it does not, and help on adding what is missing:
119
+ file, a library, or a service does. Its probability that the file does show
120
+ enough is the finding's `context`. When that is below 0.6, the finding gets a
121
+ warning under its code, and help on adding what is missing:
105
122
 
106
123
  ```text
107
- warning: this file may not show enough to check this rule (Jev: 0.55 that it does not)
124
+ warning: this file may not show enough to check this rule
108
125
  help: add what the code relies on outside this file as a note Jev reads:
109
126
  // @adhere <the fact>, and how you know it
110
127
  ```
@@ -139,9 +156,12 @@ adhere lint --deny-warnings # fail on warnings as well as errors
139
156
  adhere lint --log-level debug # log each request to Jev on stderr
140
157
  adhere validate # check the rules' wording; ask Jev whether any contradict
141
158
  adhere init [--force] # scaffold .adhere/config.ts and two example rules
159
+ adhere init --shared org/repo # scaffold a repo of rules other repos install
160
+ adhere list org/repo # list the rules in another repo's .adhere/rules/
161
+ adhere install org/repo # copy them into this repo's .adhere/rules/org/repo/
142
162
  adhere login # save a TypeSafe AI API key for later runs
143
163
  adhere logout # delete the saved key
144
- adhere skill # print the agent skill (below)
164
+ adhere skill [setup|fix] # print an agent skill (below)
145
165
  ```
146
166
 
147
167
  Bare `adhere` prints the help, which lists the commands, and
@@ -301,12 +321,63 @@ When there is a `package.json`, init also adds `@drkmttr/adhere` to its
301
321
  `<manager> add -D @drkmttr/adhere` with the package manager whose lockfile
302
322
  (`bun.lock`, `bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`)
303
323
  sits beside the `package.json`, or npm when there is none. With pnpm it adds
304
- `--ignore-workspace-root-check`, so the install also works at a workspace root,
305
- and `--config.strict-dep-builds=false`, so pnpm 11 skips the build script of
306
- msgpackr-extract, an optional native add-on that comes with `effect` and that
307
- adhere never uses, with a warning instead of failing.
324
+ `--ignore-workspace-root-check`, so the install also works at a workspace root.
308
325
  It skips the install when `package.json` already lists adhere.
309
326
 
327
+ `adhere init --shared org/repo` scaffolds a repo whose rules other repos copy
328
+ in with `adhere install` (see [Sharing rules](#sharing-rules)) instead of one
329
+ that lints itself. It writes the two example rules and no config, since
330
+ install copies only rule files, and with them:
331
+
332
+ - `README.md`, which says what the repo is for, how to install its rules, and
333
+ how to add one.
334
+ - `.github/workflows/adhere.yaml`, which runs `adhere validate` on every push
335
+ to main and every pull request, with the `TYPESAFE_API_KEY` secret. A pull
336
+ request from a fork gets no secrets, so validate refuses there.
337
+ - `alchemy.run.ts`, an [alchemy](https://alchemy.run) stack that creates the
338
+ GitHub repo, or adopts it, and sets that secret from `TYPESAFE_API_KEY`
339
+ when deployed with `npx alchemy deploy`. The repo is public unless you
340
+ answer yes when init asks, at a terminal, whether to make it private. Other
341
+ repos then need git's credentials for it to list and install its rules.
342
+ Without a terminal it is public; change `visibility` in the stack to make it
343
+ private.
344
+ - `package.json`, with alchemy and effect pinned to versions that work
345
+ together, which init installs, and a `.gitignore` for them and alchemy's
346
+ state.
347
+
348
+ ### Sharing rules
349
+
350
+ An organization's rules can live in one repo's `.adhere/rules/`, and other
351
+ repos copy them in. `adhere list org/repo` prints each rule there with its
352
+ description. `adhere install org/repo` copies every rule's directory into this
353
+ repo's `.adhere/rules/org/repo/`, so `data/brand-ports` from
354
+ darkmatter/standards is `.adhere/rules/darkmatter/standards/data/brand-ports/`,
355
+ the rule `darkmatter/standards/data/brand-ports`, and two repos' rules never
356
+ collide.
357
+ A topic or a rule after the repo copies only that part, and `#` picks a branch
358
+ or tag:
359
+
360
+ ```sh
361
+ adhere install darkmatter/standards/data # the data topic
362
+ adhere install darkmatter/standards/data/brand-ports # one rule
363
+ adhere install darkmatter/standards#v3 # every rule, as tagged v3
364
+ ```
365
+
366
+ The copies are the repo's own rules from then on: commit them, edit them, or
367
+ start new rules from them. Nothing tracks where they came from, so a later
368
+ install skips a rule already there, as init does, and `--force` replaces its
369
+ directory, edits and all. A rule's whole directory is copied, so a `RULE.ts`
370
+ comes with the helpers beside it, and runs whenever lint does, in CI too: read
371
+ what you install. `adhere list` names a `RULE.ts` without its description,
372
+ since reading it would run it. The source's config, its inline rules, and its
373
+ cache are not copied. A config with inline `rules` replaces `.adhere/rules/`,
374
+ so it replaces copied rules too.
375
+
376
+ The source is cloned with `git` from `https://github.com/org/repo.git`, so a
377
+ private repo needs git's credentials for GitHub, as `gh auth setup-git` sets
378
+ up. To clone over SSH instead, let git rewrite the URL:
379
+ `git config --global url.git@github.com:.insteadOf https://github.com/`.
380
+
310
381
  ### Validate
311
382
 
312
383
  `adhere validate` loads the config, rule files, and presets the way `lint`
@@ -362,8 +433,8 @@ login. `adhere logout` deletes the saved key.
362
433
 
363
434
  ## Config
364
435
 
365
- A config file is optional. Without one, adhere reads the rule files in
366
- `.adhere/` and any `--preset`. A config names presets, sets the model and
436
+ A config file is optional. Without one, adhere reads the rules in
437
+ `.adhere/rules/` and any `--preset`. A config names presets, sets the model and
367
438
  thresholds, or gives rules inline. It sits in the working directory of the
368
439
  repo being audited, at one of these paths (keep one):
369
440
 
@@ -408,21 +479,35 @@ not resolve `node_modules`.
408
479
  A tsconfig's globs skip dot directories, so the editor opens
409
480
  `.adhere/config.ts` outside any project, where it cannot resolve the package's
410
481
  types: they are reached only by `bundler`, `node16`, or `nodenext` resolution.
411
- `adhere init` writes `.adhere/tsconfig.json`, a project for the config alone
412
- with `bundler` resolution. For a config written by hand, add that file, or
413
- include `.adhere/config.ts` in a tsconfig that resolves the same way.
482
+ `adhere init` writes `.adhere/tsconfig.json`, a project for the config and any
483
+ [rules written in TypeScript](#rules-as-typescript-files), with `bundler`
484
+ resolution. For a config written by hand, add that file, or include
485
+ `.adhere/config.ts` in a tsconfig that resolves the same way. One written
486
+ before rules could be TypeScript includes only `config.ts`; make its `include`
487
+ `["**/*.ts"]` to give rule files their types.
414
488
 
415
- `rules` in the config, inline as above or as a directory (below), replaces the
416
- `.adhere/` rule files: none of them, root or nested, is read then.
489
+ `rules` in the config, inline as above or as a directory (below), replaces
490
+ `.adhere/rules/`: none of its rules, root or nested, is read then.
417
491
 
418
492
  ### Rules as Markdown files
419
493
 
420
- A repo's own rules live in `.adhere/`, one `*.md` file per rule, next to the
421
- config and the cache (neither is read as a rule). The path without `.md` is the
422
- rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
423
- directory exists, it is read without any config.
424
-
425
- A file is front matter, then a body. The body's code goes in fences under
494
+ A repo's own rules live in `.adhere/rules/`, a directory per rule, which holds
495
+ the rule as a `RULE.md`, or as a `RULE.ts` (see
496
+ [Rules as TypeScript files](#rules-as-typescript-files)). The directory's path
497
+ is the rule id, so `.adhere/rules/data/brand-ports/RULE.md` is
498
+ `data/brand-ports`, and a directory that holds no rule, such as `data/`, is a
499
+ topic. Nothing else in a rule's directory, such as notes or a helper its
500
+ `RULE.ts` imports, is read as a rule, and a directory with both a `RULE.md` and
501
+ a `RULE.ts` refuses the run. When `.adhere/rules/` exists, it is read without
502
+ any config.
503
+
504
+ Rules used to be any `*.md` file in `.adhere/`, as
505
+ `.adhere/data/brand-ports.md`. A `*.md` file outside every rule's directory,
506
+ but a `README.md`, now refuses the run, and the refusal says where each such
507
+ file goes to keep its id, so `adhere-ignore` comments and `overrides` still
508
+ name it.
509
+
510
+ A `RULE.md` is front matter, then a body. The body's code goes in fences under
426
511
  headings of RFC 2119's words: `## Must` for code that must be written, and
427
512
  `## Never` for code that must never be, which is what a violation looks like.
428
513
  A heading names the code in its section, which runs to the next heading at its
@@ -489,7 +574,7 @@ the names before 0.7, read as `must` and `never`.
489
574
 
490
575
  Nested `.adhere/` directories are also discovered, except under
491
576
  `node_modules/`, `dist/`, and the other skipped directories (above). A rule in
492
- `packages/api/.adhere/data/brand-ports.md` has the same id,
577
+ `packages/api/.adhere/rules/data/brand-ports/RULE.md` has the same id,
493
578
  `data/brand-ports`, but applies only to files under `packages/api/`. Root
494
579
  `.adhere/` rules apply project-wide. If a nested rule has the same id as a root
495
580
  rule, the nearest containing `.adhere/` shadows the less-specific rule for that
@@ -498,8 +583,8 @@ priority system: presets are global, project rules override preset rules with
498
583
  the same id, and nested project rules override less-specific project rules with
499
584
  the same id for files in their subtree.
500
585
 
501
- `.adhere/` is the default rather than `docs/adhere/` because it keeps
502
- everything adhere owns in one directory: the config and the cache are tool
586
+ `.adhere/rules/` is the default rather than `docs/adhere/` because it keeps
587
+ everything adhere owns in `.adhere/`: the config and the cache are tool
503
588
  state, not documentation, and would stay in `.adhere/` anyway. A nested
504
589
  `.adhere/` also scopes its rules to the directory that contains it (above). The
505
590
  cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
@@ -510,7 +595,59 @@ documentation can point `rules` at `docs/adhere/`:
510
595
  export default defineConfig({ presets: ["effect"], rules: "./docs/adhere" });
511
596
  ```
512
597
 
513
- Rules read through `rules` apply project-wide.
598
+ That directory holds its rules as `.adhere/rules/` does, a directory per rule,
599
+ and they apply project-wide.
600
+
601
+ ### Rules as TypeScript files
602
+
603
+ A rule's directory can hold it as a `RULE.ts` instead, which default-exports
604
+ `defineRule({...})`, taking the fields a config's inline rule does. What
605
+ TypeScript adds is `appendState`, a hook on what Jev reads for the rule:
606
+
607
+ ```ts
608
+ // .adhere/rules/data/columns/RULE.ts
609
+ import { defineRule } from "@drkmttr/adhere";
610
+
611
+ export default defineRule({
612
+ description:
613
+ "A query must name only columns its table has in `schema`, and never a column it lacks.",
614
+ must: 'db.select("id", "email").from("users")',
615
+ never: 'db.select("mail").from("users")',
616
+ appendState: async (state, file, Bun) => ({
617
+ schema: await Bun.file("db/schema.sql").text(),
618
+ }),
619
+ });
620
+ ```
621
+
622
+ Right before each request about the rule goes out, `appendState` gets the
623
+ request's state, the file it is about, and Bun's API, and what it returns is
624
+ spread over the state. The state is what Jev reads beside each question:
625
+ `code`, the file's sections as Jev reads them, under their numbers from 1, as
626
+ in `{ code: { "1": "import …", "2": "export const …" } }`. `file` is the file's
627
+ absolute `path`, and its `contents` as written, comments and all. The
628
+ description can name a key the hook adds, in backticks, as the questions name
629
+ `code`. Nothing the hook returns is checked, so it can change `code` too, or
630
+ replace it: a hook can break its own rule's answers, and adhere does not stop
631
+ it. It can be async, and one that throws refuses the run, naming the rule.
632
+
633
+ A rule with a hook goes to Jev in requests of its own, to judge a file and to
634
+ locate a finding, so only that rule reads what its hook adds, and no other
635
+ rule's answer depends on it. Each of those requests sends the file again. The
636
+ plan counts them, but not the tokens a hook adds, which are not known until it
637
+ runs; a request over Jev's context skips the file, as a file too long does.
638
+
639
+ The cache keys a judgment by the file's content and the rule, and the hook is
640
+ part of the rule by its source: editing the hook judges the rule again. What
641
+ the hook reads outside the file is not, so when `db/schema.sql` changes, a file
642
+ already judged is not judged again until it or the rule changes.
643
+
644
+ A `RULE.ts` is imported, as a config is, so its code runs on every lint, and
645
+ one that default-exports no rule refuses the run. It can import other files by
646
+ relative path, such as a helper beside it in its directory, but the executable
647
+ resolves no packages besides `@drkmttr/adhere`. `Bun` is typed when
648
+ `@types/bun` is installed and `.adhere/tsconfig.json` lists it, as
649
+ `"types": ["bun"]`, and is `unknown` otherwise. A config's inline rules can
650
+ have `appendState` too.
514
651
 
515
652
  ### Scoping a rule
516
653
 
@@ -628,7 +765,7 @@ A preset rule says "must" only where its source makes a requirement, and
628
765
  "should" where the source gives advice. In `effect`, two rules are
629
766
  guidelines: test layers are in memory, outside integration tests, and tests
630
767
  provide config through a layer. Config validation accepts
631
- `Config.mapOrFail` as well as `Config.schema`, and the variants rule does not
768
+ `Config.mapEffect` as well as `Config.schema`, and the variants rule does not
632
769
  rule out a `switch`. The rule that a command handler only parses input is
633
770
  gone: the docs show that pattern but do not ask for it.
634
771
 
@@ -726,7 +863,10 @@ config's `sufficiencyThreshold`, which beats the default, `0.6`.
726
863
  file and that rule, not on which other rules share the request. Every
727
864
  question shares the state cost of the request. On up to 10 files per
728
865
  project rule, the rule's question has a second one beside it, the linter
729
- check (see [Validate](#validate)), with the same fields.
866
+ check (see [Validate](#validate)), with the same fields. A rule with
867
+ `appendState` has requests of its own, here and below, whose state is what
868
+ its hook makes of this one (see
869
+ [Rules as TypeScript files](#rules-as-typescript-files)).
730
870
  2. A second request only when at least one rule's probability is above its
731
871
  threshold. Per flagged rule, when the file has more than one section, one
732
872
  `choice` question among the sections' numbers, which yields the section to
@@ -760,7 +900,7 @@ named for the hash of a source file's content as Jev reads it, without its
760
900
  comments, so a moved or copied file keeps its judgments, identical files share
761
901
  them, and a change to comments alone re-judges nothing. Each answer sits under a
762
902
  fingerprint of the model and the question asked for the rule, which carries
763
- the rule's text. A changed file re-judges every rule for that file. An edited
903
+ the rule's text, and of the source of the rule's `appendState`, if it has one. A changed file re-judges every rule for that file. An edited
764
904
  rule re-judges only that rule, and an adhere that asks its questions
765
905
  differently re-judges every rule once. A lowered threshold locates cached
766
906
  judgments that are newly above it without judging again. `tallies/` keeps the
@@ -799,18 +939,25 @@ every file is cached needs no key and no network.
799
939
 
800
940
  ## Agent skill
801
941
 
802
- Two skills teach an agent to work with adhere:
942
+ Three skills teach an agent to work with adhere:
803
943
 
804
- - [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) gathers a repo's
805
- conventions into rule files, configures adhere, and calibrates thresholds.
944
+ - [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) is the reference: the
945
+ commands, how to write and word a rule, the config, presets, shared rules,
946
+ comments and suppressions, the cache, reading a report, and tuning a rule.
947
+ - [`skills/adhere-setup/SKILL.md`](./skills/adhere-setup/SKILL.md) sets adhere
948
+ up in a repo with the user. It drafts rules from the repo's conventions,
949
+ offers fitting presets and a shared org repo's rules, shows them all in one
950
+ list to choose from, imports and validates the chosen ones, walks through
951
+ the first lint, and adds a CI workflow.
806
952
  - [`skills/adhere-fix/SKILL.md`](./skills/adhere-fix/SKILL.md) verifies and
807
953
  fixes the findings `lint` reports: it checks each against its rule and the
808
954
  code, fixes the real ones, reports the false ones with the evidence, and says
809
955
  when a rule is wrong more often than right.
810
956
 
811
957
  Install them with the [skills](https://github.com/vercel-labs/skills) CLI:
812
- `skills add darkmatter/adhere`. The binary carries both: `adhere skill` prints
813
- the first and `adhere skill fix` the second, so
958
+ `skills add darkmatter/adhere`. The binary carries all three: `adhere skill`
959
+ prints the reference, `adhere skill setup` the setup skill, and
960
+ `adhere skill fix` the fix skill, so
814
961
  `adhere skill fix > .agents/skills/adhere-fix/SKILL.md` works without a
815
962
  checkout.
816
963
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drkmttr/adhere",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "description": "Lints a repository against the rules in .adhere/, judged by Jev.",
6
6
  "license": "MIT",
@@ -37,10 +37,10 @@
37
37
  "release": "release-it"
38
38
  },
39
39
  "dependencies": {
40
- "effect": "4.0.0-rc.112"
40
+ "effect": "4.0.0-rc.118"
41
41
  },
42
42
  "devDependencies": {
43
- "@effect/platform-bun": "4.0.0-rc.112",
43
+ "@effect/platform-bun": "4.0.0-rc.118",
44
44
  "@types/bun": "^1.4.2",
45
45
  "@types/node": "^24.13.6",
46
46
  "release-it": "^21.1.0",
@@ -49,7 +49,7 @@
49
49
  "vitest": "4.1.11"
50
50
  },
51
51
  "overrides": {
52
- "@effect/platform-node-shared": "4.0.0-rc.112"
52
+ "@effect/platform-node-shared": "4.0.0-rc.118"
53
53
  },
54
54
  "release-it": {
55
55
  "$schema": "https://unpkg.com/release-it@21.1.0/schema/release-it.json",
@@ -68,10 +68,10 @@
68
68
  }
69
69
  },
70
70
  "optionalDependencies": {
71
- "@drkmttr/adhere-darwin-arm64": "0.11.0",
72
- "@drkmttr/adhere-darwin-x64": "0.11.0",
73
- "@drkmttr/adhere-linux-arm64": "0.11.0",
74
- "@drkmttr/adhere-linux-x64": "0.11.0",
75
- "@drkmttr/adhere-win32-x64": "0.11.0"
71
+ "@drkmttr/adhere-darwin-arm64": "0.13.0",
72
+ "@drkmttr/adhere-darwin-x64": "0.13.0",
73
+ "@drkmttr/adhere-linux-arm64": "0.13.0",
74
+ "@drkmttr/adhere-linux-x64": "0.13.0",
75
+ "@drkmttr/adhere-win32-x64": "0.13.0"
76
76
  }
77
77
  }
@@ -7,7 +7,7 @@ description: Timeouts, retries, logging, and spans must be attached with .pipe,
7
7
  ```ts
8
8
  const program = fetchData.pipe(
9
9
  Effect.timeout("5 seconds"),
10
- Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)))),
10
+ Effect.retry(Schedule.max([Schedule.exponential("100 millis"), Schedule.recurs(3)])),
11
11
  Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)),
12
12
  Effect.withSpan("fetchData"),
13
13
  );
@@ -15,8 +15,8 @@ class ApiConfig extends Context.Service<
15
15
  static readonly layer = Layer.effect(
16
16
  ApiConfig,
17
17
  Effect.gen(function* () {
18
- const apiKey = yield* Config.redacted("API_KEY");
19
- const baseUrl = yield* Config.string("API_BASE_URL");
18
+ const apiKey = yield* Config.Redacted("API_KEY");
19
+ const baseUrl = yield* Config.String("API_BASE_URL");
20
20
  return { apiKey, baseUrl };
21
21
  }),
22
22
  );
@@ -27,7 +27,7 @@ class ApiConfig extends Context.Service<
27
27
 
28
28
  ```ts
29
29
  const sendInvoice = Effect.fn("sendInvoice")(function* (invoice: Invoice) {
30
- const apiUrl = yield* Config.string("BILLING_API_URL");
30
+ const apiUrl = yield* Config.String("BILLING_API_URL");
31
31
  return yield* post(apiUrl, invoice);
32
32
  });
33
33
  ```
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: A token, password, or key must be read with Config.redacted, never with Config.string.
2
+ description: A token, password, or key must be read with Config.Redacted, never with Config.String.
3
3
  ---
4
4
 
5
5
  ## Must
6
6
 
7
7
  ```ts
8
8
  const program = Effect.gen(function* () {
9
- const apiKey = yield* Config.redacted("API_KEY");
9
+ const apiKey = yield* Config.Redacted("API_KEY");
10
10
 
11
11
  const headers = {
12
12
  Authorization: `Bearer ${Redacted.value(apiKey)}`,
@@ -19,5 +19,5 @@ const program = Effect.gen(function* () {
19
19
  ## Never
20
20
 
21
21
  ```ts
22
- const token = yield* Config.string("GITHUB_TOKEN");
22
+ const token = yield* Config.String("GITHUB_TOKEN");
23
23
  ```
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: A config value with constraints must be validated as it is read, with Config.schema or Config.mapOrFail, never checked by hand after reading.
2
+ description: A config value with constraints must be validated as it is read, with Config.schema or Config.mapEffect, never checked by hand after reading.
3
3
  ---
4
4
 
5
5
  ## Must
@@ -14,8 +14,8 @@ const Environment = Schema.Literals(["development", "staging", "production"]);
14
14
  const program = Effect.gen(function* () {
15
15
  const port = yield* Config.schema(Port, "PORT");
16
16
  const env = yield* Config.schema(Environment, "ENV");
17
- const timeoutMs = yield* Config.int("TIMEOUT_MS").pipe(
18
- Config.mapOrFail((ms) =>
17
+ const timeoutMs = yield* Config.Int("TIMEOUT_MS").pipe(
18
+ Config.mapEffect((ms) =>
19
19
  ms > 0 && ms <= 60_000
20
20
  ? Effect.succeed(ms)
21
21
  : Effect.fail(
@@ -33,7 +33,7 @@ const program = Effect.gen(function* () {
33
33
  ## Never
34
34
 
35
35
  ```ts
36
- const timeoutMs = yield* Config.number("TIMEOUT_MS");
36
+ const timeoutMs = yield* Config.Number("TIMEOUT_MS");
37
37
  if (timeoutMs <= 0 || timeoutMs > 60_000) {
38
38
  return yield* Effect.die("TIMEOUT_MS out of range");
39
39
  }