@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.
- package/README.md +149 -51
- 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 +49 -33
- package/src/config.ts +50 -3
- package/src/index.ts +2 -0
- package/src/init.ts +36 -25
- package/src/install.ts +37 -26
- 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
|
```
|
|
@@ -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
|
|
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
|
-
|
|
325
|
-
|
|
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
|
|
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
|
|
335
|
-
`.adhere/org/repo/`, so `data/brand-ports` from
|
|
336
|
-
`.adhere/darkmatter/standards/data/brand-ports
|
|
337
|
-
`darkmatter/standards/data/brand-ports`, and two repos' rules never
|
|
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
|
|
350
|
-
edits and all.
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
465
|
-
`.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.
|
|
466
491
|
|
|
467
492
|
### Rules as Markdown files
|
|
468
493
|
|
|
469
|
-
A repo's own rules live in `.adhere/`,
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
942
|
+
Three skills teach an agent to work with adhere:
|
|
852
943
|
|
|
853
|
-
- [`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md)
|
|
854
|
-
|
|
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
|
|
862
|
-
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
|
|
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.
|
|
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
|
}
|