@drkmttr/adhere 0.12.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 +149 -51
  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 +49 -33
  11. package/src/config.ts +50 -3
  12. package/src/index.ts +2 -0
  13. package/src/init.ts +36 -25
  14. package/src/install.ts +37 -26
  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
  ```
@@ -140,11 +157,11 @@ 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
142
159
  adhere init --shared org/repo # scaffold a repo of rules other repos install
143
- adhere list org/repo # list the rules in another repo's .adhere/
144
- adhere install org/repo # copy them into this repo's .adhere/org/repo/
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/
145
162
  adhere login # save a TypeSafe AI API key for later runs
146
163
  adhere logout # delete the saved key
147
- adhere skill # print the agent skill (below)
164
+ adhere skill [setup|fix] # print an agent skill (below)
148
165
  ```
149
166
 
150
167
  Bare `adhere` prints the help, which lists the commands, and
@@ -304,10 +321,7 @@ When there is a `package.json`, init also adds `@drkmttr/adhere` to its
304
321
  `<manager> add -D @drkmttr/adhere` with the package manager whose lockfile
305
322
  (`bun.lock`, `bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`)
306
323
  sits beside the `package.json`, or npm when there is none. With pnpm it adds
307
- `--ignore-workspace-root-check`, so the install also works at a workspace root,
308
- and `--config.strict-dep-builds=false`, so pnpm 11 skips the build script of
309
- msgpackr-extract, an optional native add-on that comes with `effect` and that
310
- adhere never uses, with a warning instead of failing.
324
+ `--ignore-workspace-root-check`, so the install also works at a workspace root.
311
325
  It skips the install when `package.json` already lists adhere.
312
326
 
313
327
  `adhere init --shared org/repo` scaffolds a repo whose rules other repos copy
@@ -321,20 +335,25 @@ install copies only rule files, and with them:
321
335
  to main and every pull request, with the `TYPESAFE_API_KEY` secret. A pull
322
336
  request from a fork gets no secrets, so validate refuses there.
323
337
  - `alchemy.run.ts`, an [alchemy](https://alchemy.run) stack that creates the
324
- private GitHub repo, or adopts it, and sets that secret from
325
- `TYPESAFE_API_KEY` when deployed with `npx alchemy deploy`.
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.
326
344
  - `package.json`, with alchemy and effect pinned to versions that work
327
345
  together, which init installs, and a `.gitignore` for them and alchemy's
328
346
  state.
329
347
 
330
348
  ### Sharing rules
331
349
 
332
- An organization's rules can live in one repo's `.adhere/`, and other repos
333
- copy them in. `adhere list org/repo` prints each rule there with its
334
- description. `adhere install org/repo` copies every rule into this repo's
335
- `.adhere/org/repo/`, so `data/brand-ports` from darkmatter/standards is
336
- `.adhere/darkmatter/standards/data/brand-ports.md`, the rule
337
- `darkmatter/standards/data/brand-ports`, and two repos' rules never collide.
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.
338
357
  A topic or a rule after the repo copies only that part, and `#` picks a branch
339
358
  or tag:
340
359
 
@@ -346,10 +365,13 @@ adhere install darkmatter/standards#v3 # every rule, as tagged
346
365
 
347
366
  The copies are the repo's own rules from then on: commit them, edit them, or
348
367
  start new rules from them. Nothing tracks where they came from, so a later
349
- install skips a file already there, as init does, and `--force` overwrites it,
350
- edits and all. Only Markdown rule files are copied; the source's config, its
351
- inline rules, and its cache are not. A config with inline `rules` replaces
352
- the `.adhere/` rule files, so it replaces copied ones too.
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.
353
375
 
354
376
  The source is cloned with `git` from `https://github.com/org/repo.git`, so a
355
377
  private repo needs git's credentials for GitHub, as `gh auth setup-git` sets
@@ -411,8 +433,8 @@ login. `adhere logout` deletes the saved key.
411
433
 
412
434
  ## Config
413
435
 
414
- A config file is optional. Without one, adhere reads the rule files in
415
- `.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
416
438
  thresholds, or gives rules inline. It sits in the working directory of the
417
439
  repo being audited, at one of these paths (keep one):
418
440
 
@@ -457,21 +479,35 @@ not resolve `node_modules`.
457
479
  A tsconfig's globs skip dot directories, so the editor opens
458
480
  `.adhere/config.ts` outside any project, where it cannot resolve the package's
459
481
  types: they are reached only by `bundler`, `node16`, or `nodenext` resolution.
460
- `adhere init` writes `.adhere/tsconfig.json`, a project for the config alone
461
- with `bundler` resolution. For a config written by hand, add that file, or
462
- 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.
463
488
 
464
- `rules` in the config, inline as above or as a directory (below), replaces the
465
- `.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.
466
491
 
467
492
  ### Rules as Markdown files
468
493
 
469
- A repo's own rules live in `.adhere/`, one `*.md` file per rule, next to the
470
- config and the cache (neither is read as a rule). The path without `.md` is the
471
- rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
472
- directory exists, it is read without any config.
473
-
474
- 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
475
511
  headings of RFC 2119's words: `## Must` for code that must be written, and
476
512
  `## Never` for code that must never be, which is what a violation looks like.
477
513
  A heading names the code in its section, which runs to the next heading at its
@@ -538,7 +574,7 @@ the names before 0.7, read as `must` and `never`.
538
574
 
539
575
  Nested `.adhere/` directories are also discovered, except under
540
576
  `node_modules/`, `dist/`, and the other skipped directories (above). A rule in
541
- `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,
542
578
  `data/brand-ports`, but applies only to files under `packages/api/`. Root
543
579
  `.adhere/` rules apply project-wide. If a nested rule has the same id as a root
544
580
  rule, the nearest containing `.adhere/` shadows the less-specific rule for that
@@ -547,8 +583,8 @@ priority system: presets are global, project rules override preset rules with
547
583
  the same id, and nested project rules override less-specific project rules with
548
584
  the same id for files in their subtree.
549
585
 
550
- `.adhere/` is the default rather than `docs/adhere/` because it keeps
551
- 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
552
588
  state, not documentation, and would stay in `.adhere/` anyway. A nested
553
589
  `.adhere/` also scopes its rules to the directory that contains it (above). The
554
590
  cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
@@ -559,7 +595,59 @@ documentation can point `rules` at `docs/adhere/`:
559
595
  export default defineConfig({ presets: ["effect"], rules: "./docs/adhere" });
560
596
  ```
561
597
 
562
- 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.
563
651
 
564
652
  ### Scoping a rule
565
653
 
@@ -677,7 +765,7 @@ A preset rule says "must" only where its source makes a requirement, and
677
765
  "should" where the source gives advice. In `effect`, two rules are
678
766
  guidelines: test layers are in memory, outside integration tests, and tests
679
767
  provide config through a layer. Config validation accepts
680
- `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
681
769
  rule out a `switch`. The rule that a command handler only parses input is
682
770
  gone: the docs show that pattern but do not ask for it.
683
771
 
@@ -775,7 +863,10 @@ config's `sufficiencyThreshold`, which beats the default, `0.6`.
775
863
  file and that rule, not on which other rules share the request. Every
776
864
  question shares the state cost of the request. On up to 10 files per
777
865
  project rule, the rule's question has a second one beside it, the linter
778
- 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)).
779
870
  2. A second request only when at least one rule's probability is above its
780
871
  threshold. Per flagged rule, when the file has more than one section, one
781
872
  `choice` question among the sections' numbers, which yields the section to
@@ -809,7 +900,7 @@ named for the hash of a source file's content as Jev reads it, without its
809
900
  comments, so a moved or copied file keeps its judgments, identical files share
810
901
  them, and a change to comments alone re-judges nothing. Each answer sits under a
811
902
  fingerprint of the model and the question asked for the rule, which carries
812
- 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
813
904
  rule re-judges only that rule, and an adhere that asks its questions
814
905
  differently re-judges every rule once. A lowered threshold locates cached
815
906
  judgments that are newly above it without judging again. `tallies/` keeps the
@@ -848,18 +939,25 @@ every file is cached needs no key and no network.
848
939
 
849
940
  ## Agent skill
850
941
 
851
- Two skills teach an agent to work with adhere:
942
+ Three skills teach an agent to work with adhere:
852
943
 
853
- - [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) gathers a repo's
854
- 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.
855
952
  - [`skills/adhere-fix/SKILL.md`](./skills/adhere-fix/SKILL.md) verifies and
856
953
  fixes the findings `lint` reports: it checks each against its rule and the
857
954
  code, fixes the real ones, reports the false ones with the evidence, and says
858
955
  when a rule is wrong more often than right.
859
956
 
860
957
  Install them with the [skills](https://github.com/vercel-labs/skills) CLI:
861
- `skills add darkmatter/adhere`. The binary carries both: `adhere skill` prints
862
- 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
863
961
  `adhere skill fix > .agents/skills/adhere-fix/SKILL.md` works without a
864
962
  checkout.
865
963
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drkmttr/adhere",
3
- "version": "0.12.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.12.0",
72
- "@drkmttr/adhere-darwin-x64": "0.12.0",
73
- "@drkmttr/adhere-linux-arm64": "0.12.0",
74
- "@drkmttr/adhere-linux-x64": "0.12.0",
75
- "@drkmttr/adhere-win32-x64": "0.12.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
  }