@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.
- package/README.md +184 -37
- package/package.json +9 -9
- package/presets/effect/basics/{instrument-with-pipe.md → instrument-with-pipe/RULE.md} +1 -1
- package/presets/effect/config/{business-logic-depends-on-config-service.md → business-logic-depends-on-config-service/RULE.md} +3 -3
- package/presets/effect/config/{secrets-are-redacted.md → secrets-are-redacted/RULE.md} +3 -3
- package/presets/effect/config/{validate-with-schema.md → validate-with-schema/RULE.md} +4 -4
- package/skills/adhere/SKILL.md +206 -118
- package/skills/adhere-fix/SKILL.md +9 -8
- package/skills/adhere-setup/SKILL.md +244 -0
- package/src/cli.ts +114 -25
- package/src/config.ts +50 -3
- package/src/index.ts +2 -0
- package/src/init.ts +193 -27
- package/src/install.ts +158 -0
- package/src/main.ts +1 -1
- package/src/rules.ts +135 -56
- package/src/services/AdhereConfig.ts +2 -2
- package/src/services/Credentials.ts +4 -4
- package/src/services/Jev.http.ts +76 -10
- package/src/services/Jev.ts +39 -10
- package/src/workflows/audit.ts +23 -12
- package/src/workflows/format.ts +172 -35
- /package/presets/alchemy/apis/{match-remote-errors-by-tag.md → match-remote-errors-by-tag/RULE.md} +0 -0
- /package/presets/alchemy/apis/{schemas-at-trust-boundaries.md → schemas-at-trust-boundaries/RULE.md} +0 -0
- /package/presets/alchemy/data/{commit-migrations-then-deploy.md → commit-migrations-then-deploy/RULE.md} +0 -0
- /package/presets/alchemy/data/{hyperdrive-direct-origin.md → hyperdrive-direct-origin/RULE.md} +0 -0
- /package/presets/alchemy/data/{migrations-use-direct-url.md → migrations-use-direct-url/RULE.md} +0 -0
- /package/presets/alchemy/data/{never-disable-tls-verification.md → never-disable-tls-verification/RULE.md} +0 -0
- /package/presets/alchemy/data/{private-endpoint-at-runtime.md → private-endpoint-at-runtime/RULE.md} +0 -0
- /package/presets/alchemy/data/{single-migration-owner.md → single-migration-owner/RULE.md} +0 -0
- /package/presets/alchemy/durable/{idempotent-event-handlers.md → idempotent-event-handlers/RULE.md} +0 -0
- /package/presets/alchemy/durable/{io-inside-steps.md → io-inside-steps/RULE.md} +0 -0
- /package/presets/alchemy/durable/{no-self-triggering-bucket-writes.md → no-self-triggering-bucket-writes/RULE.md} +0 -0
- /package/presets/alchemy/durable/{stable-step-names.md → stable-step-names/RULE.md} +0 -0
- /package/presets/alchemy/durable/{webhook-state-is-truth.md → webhook-state-is-truth/RULE.md} +0 -0
- /package/presets/alchemy/providers/{diff-guards-unresolved-props.md → diff-guards-unresolved-props/RULE.md} +0 -0
- /package/presets/alchemy/providers/{idempotent-delete.md → idempotent-delete/RULE.md} +0 -0
- /package/presets/alchemy/providers/{lazy-layer-construction.md → lazy-layer-construction/RULE.md} +0 -0
- /package/presets/alchemy/providers/{reconcile-from-observed-state.md → reconcile-from-observed-state/RULE.md} +0 -0
- /package/presets/alchemy/resources/{anchor-paths-with-import-meta.md → anchor-paths-with-import-meta/RULE.md} +0 -0
- /package/presets/alchemy/resources/{idempotent-actions-and-seeds.md → idempotent-actions-and-seeds/RULE.md} +0 -0
- /package/presets/alchemy/resources/{previews-reference-shared-resources.md → previews-reference-shared-resources/RULE.md} +0 -0
- /package/presets/alchemy/resources/{protect-irreplaceable-data.md → protect-irreplaceable-data/RULE.md} +0 -0
- /package/presets/alchemy/resources/{remote-state-for-ci.md → remote-state-for-ci/RULE.md} +0 -0
- /package/presets/alchemy/resources/{stable-logical-ids.md → stable-logical-ids/RULE.md} +0 -0
- /package/presets/alchemy/resources/{stage-unique-physical-names.md → stage-unique-physical-names/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{bind-tag-not-implementation.md → bind-tag-not-implementation/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{deterministic-config-defaults.md → deterministic-config-defaults/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{disposables-in-request-scope.md → disposables-in-request-scope/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{lambda-no-post-response-work.md → lambda-no-post-response-work/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{no-per-request-work-at-construction.md → no-per-request-work-at-construction/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{resolve-config-and-bindings-at-construction.md → resolve-config-and-bindings-at-construction/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{let-alchemy-mint-infra-secrets.md → let-alchemy-mint-infra-secrets/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{none-in-browser-env.md → none-in-browser-env/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{redacted-until-use.md → redacted-until-use/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{unwrap-before-compare-or-send.md → unwrap-before-compare-or-send/RULE.md} +0 -0
- /package/presets/alchemy/security/{authorize-public-endpoints.md → authorize-public-endpoints/RULE.md} +0 -0
- /package/presets/alchemy/security/{github-secret-not-variable.md → github-secret-not-variable/RULE.md} +0 -0
- /package/presets/alchemy/security/{identity-from-verified-session.md → identity-from-verified-session/RULE.md} +0 -0
- /package/presets/alchemy/security/{keep-auth-protections-on.md → keep-auth-protections-on/RULE.md} +0 -0
- /package/presets/alchemy/security/{least-privilege-bindings.md → least-privilege-bindings/RULE.md} +0 -0
- /package/presets/alchemy/security/{least-privilege-ci-credentials.md → least-privilege-ci-credentials/RULE.md} +0 -0
- /package/presets/alchemy/security/{lookup-failure-is-not-anonymous.md → lookup-failure-is-not-anonymous/RULE.md} +0 -0
- /package/presets/effect/basics/{gen-for-sequencing.md → gen-for-sequencing/RULE.md} +0 -0
- /package/presets/effect/config/{tests-provide-values-directly.md → tests-provide-values-directly/RULE.md} +0 -0
- /package/presets/effect/data/{brand-meaningful-primitives.md → brand-meaningful-primitives/RULE.md} +0 -0
- /package/presets/effect/data/{variants-are-tagged-unions.md → variants-are-tagged-unions/RULE.md} +0 -0
- /package/presets/effect/errors/{catch-defects-at-boundaries-only.md → catch-defects-at-boundaries-only/RULE.md} +0 -0
- /package/presets/effect/errors/{defects-for-bugs.md → defects-for-bugs/RULE.md} +0 -0
- /package/presets/effect/errors/{wrap-external-errors.md → wrap-external-errors/RULE.md} +0 -0
- /package/presets/effect/services/{dependencies-through-layers.md → dependencies-through-layers/RULE.md} +0 -0
- /package/presets/effect/services/{fresh-layer-per-test.md → fresh-layer-per-test/RULE.md} +0 -0
- /package/presets/effect/services/{memoize-parameterized-layers.md → memoize-parameterized-layers/RULE.md} +0 -0
- /package/presets/effect/services/{no-mutable-state.md → no-mutable-state/RULE.md} +0 -0
- /package/presets/effect/services/{operations-have-no-requirements.md → operations-have-no-requirements/RULE.md} +0 -0
- /package/presets/effect/services/{test-layers-are-in-memory.md → test-layers-are-in-memory/RULE.md} +0 -0
- /package/presets/effect/testing/{test-clock-for-time.md → test-clock-for-time/RULE.md} +0 -0
- /package/presets/react/effects/{clean-up-what-they-start.md → clean-up-what-they-start/RULE.md} +0 -0
- /package/presets/react/effects/{event-logic-in-handlers.md → event-logic-in-handlers/RULE.md} +0 -0
- /package/presets/react/effects/{external-stores-use-sync-external-store.md → external-stores-use-sync-external-store/RULE.md} +0 -0
- /package/presets/react/effects/{fetches-ignore-stale-responses.md → fetches-ignore-stale-responses/RULE.md} +0 -0
- /package/presets/react/effects/{notify-parents-in-handlers.md → notify-parents-in-handlers/RULE.md} +0 -0
- /package/presets/react/server/{no-private-data-to-client.md → no-private-data-to-client/RULE.md} +0 -0
- /package/presets/react/server/{server-functions-authorize-and-validate.md → server-functions-authorize-and-validate/RULE.md} +0 -0
- /package/presets/react/state/{no-contradictory-state.md → no-contradictory-state/RULE.md} +0 -0
- /package/presets/react/state/{no-props-copied-into-state.md → no-props-copied-into-state/RULE.md} +0 -0
- /package/presets/react/state/{store-ids-not-copies.md → store-ids-not-copies/RULE.md} +0 -0
- /package/presets/security/{constant-time-secret-compare.md → constant-time-secret-compare/RULE.md} +0 -0
- /package/presets/security/{no-secrets-in-output.md → no-secrets-in-output/RULE.md} +0 -0
- /package/presets/security/{no-untrusted-input-in-commands-or-paths.md → no-untrusted-input-in-commands-or-paths/RULE.md} +0 -0
- /package/presets/security/{parameterized-queries.md → parameterized-queries/RULE.md} +0 -0
- /package/presets/typescript/async/{concurrent-independent-work.md → concurrent-independent-work/RULE.md} +0 -0
- /package/presets/typescript/async/{network-calls-have-timeouts.md → network-calls-have-timeouts/RULE.md} +0 -0
- /package/presets/typescript/design/{no-argument-mutation.md → no-argument-mutation/RULE.md} +0 -0
- /package/presets/typescript/errors/{no-swallowed-errors.md → no-swallowed-errors/RULE.md} +0 -0
- /package/presets/typescript/errors/{release-on-every-path.md → release-on-every-path/RULE.md} +0 -0
- /package/presets/typescript/testing/{assert-outcomes.md → assert-outcomes/RULE.md} +0 -0
- /package/presets/typescript/testing/{independent-tests.md → independent-tests/RULE.md} +0 -0
- /package/presets/typescript/types/{assertions-only-when-proven.md → assertions-only-when-proven/RULE.md} +0 -0
- /package/presets/typescript/types/{derive-dont-restate.md → derive-dont-restate/RULE.md} +0 -0
- /package/presets/typescript/types/{unrepresentable-invalid-states.md → unrepresentable-invalid-states/RULE.md} +0 -0
- /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
|
|
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
|
|
86
|
-
|
|
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.
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
416
|
-
`.adhere
|
|
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/`,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
942
|
+
Three skills teach an agent to work with adhere:
|
|
803
943
|
|
|
804
|
-
- [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md)
|
|
805
|
-
|
|
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
|
|
813
|
-
the
|
|
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.
|
|
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.
|
|
40
|
+
"effect": "4.0.0-rc.118"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@effect/platform-bun": "4.0.0-rc.
|
|
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.
|
|
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.
|
|
72
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
73
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
74
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
75
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.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")
|
|
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.
|
|
19
|
-
const baseUrl = yield* Config.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
18
|
-
Config.
|
|
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.
|
|
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
|
}
|