@drkmttr/adhere 0.2.2 → 0.3.1
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 +179 -107
- package/package.json +12 -6
- package/skills/adhere/SKILL.md +4 -3
- package/src/cli.ts +61 -85
- package/src/config.ts +19 -0
- package/src/main.ts +13 -11
- package/src/rules.ts +14 -2
- package/src/services/SourceWalker.ts +14 -17
- package/.adhere/config.ts +0 -7
- package/.adhere/rules/refusals-name-the-file.md +0 -17
- package/.github/workflows/ci.yaml +0 -27
- package/.github/workflows/publish.yaml +0 -68
- package/.github/workflows/release.yaml +0 -71
- package/bun.lock +0 -773
- package/scripts/npm-packages.ts +0 -68
- package/test/audit.test.ts +0 -540
- package/test/cli.test.ts +0 -17
- package/tsconfig.json +0 -26
package/README.md
CHANGED
|
@@ -9,10 +9,105 @@ report uses the same frame as `vp lint`.
|
|
|
9
9
|
|
|
10
10
|
Deterministic rules (substring matches, type checks) belong in a normal linter.
|
|
11
11
|
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install --global @drkmttr/adhere # or: bun add --global @drkmttr/adhere
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
To pin the version in a repo, for CI or scripts, add it as a dev dependency
|
|
19
|
+
and run `npx adhere`. To try it without installing, run
|
|
20
|
+
`npx @drkmttr/adhere` or `bunx @drkmttr/adhere`.
|
|
21
|
+
|
|
22
|
+
`adhere` is a prebuilt executable for macOS and Linux on arm64 and x64 and for
|
|
23
|
+
Windows on x64. It needs Node to start, and no Bun.
|
|
24
|
+
|
|
25
|
+
adhere sends each file it judges to Jev at `api.typesafe.ai`, authenticated
|
|
26
|
+
with a TypeSafe AI API key in `TYPESAFE_API_KEY`.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
adhere init # .adhere/config.ts and two example rules
|
|
32
|
+
adhere validate # check the config and rules, without Jev
|
|
33
|
+
echo ".adhere/cache/" >> .gitignore
|
|
34
|
+
export TYPESAFE_API_KEY=...
|
|
35
|
+
adhere lint # audit the working directory
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Without a config or rules, `adhere lint --preset effect` audits against the
|
|
39
|
+
built-in Effect rules. A finding looks like this:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
× data/brand-ports (0.93): A port is a branded, range-checked integer, not a bare number.
|
|
43
|
+
╭─[src/server.ts:4:1]
|
|
44
|
+
4 │ const port: number = Number(process.env.PORT ?? 3000);
|
|
45
|
+
· ──────────────────────────────────────────────────────
|
|
46
|
+
╰────
|
|
47
|
+
hint: const Port = Schema.Int.pipe(
|
|
48
|
+
Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
|
|
49
|
+
Schema.brand("Port"),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
Found 1 error.
|
|
53
|
+
42 files, 3 judged, 39 cached.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The header is the rule id, Jev's probability, and the rule's description. The
|
|
57
|
+
hint is the rule's reference. The exit code is 0 when nothing is reported and 1
|
|
58
|
+
when something is. It is also 1 when the run refuses, for example on an invalid
|
|
59
|
+
config or rule file, a missing `TYPESAFE_API_KEY`, or an unknown command or
|
|
60
|
+
flag; a refusal prints its reason.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
adhere lint # audit the working directory
|
|
66
|
+
adhere lint --preset effect # add a built-in rule set; the config becomes optional
|
|
67
|
+
adhere lint --threshold 0.8 # replace the config's threshold; per-rule thresholds still apply
|
|
68
|
+
adhere validate # load the config and rules without Jev, check for contradictions
|
|
69
|
+
adhere init [--force] # scaffold .adhere/config.ts and two example rules
|
|
70
|
+
adhere skill # print the agent skill (below)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Bare `adhere` prints the help, which lists the commands, and
|
|
74
|
+
`adhere <command> --help` lists a command's flags. `adhere --completions <shell>`
|
|
75
|
+
prints a completion script.
|
|
76
|
+
|
|
77
|
+
### What gets read
|
|
78
|
+
|
|
79
|
+
`adhere lint` reads the `.ts` files under the working directory, except `.d.ts`,
|
|
80
|
+
`.test.ts`, and config files. When the working directory contains `agents/`,
|
|
81
|
+
`apps/`, or `packages/`, only those trees are read. Below the working
|
|
82
|
+
directory, anything under `node_modules/`, `dist/`, `coverage/`, `vendor/`,
|
|
83
|
+
`e2e/`, `references/`, `.adhere/`, `.agents/`, `.claude/`, `.direnv/`,
|
|
84
|
+
`.alchemy/`, or `.vite/` is skipped; the directories above it do not count.
|
|
85
|
+
`.tsx` files are not read, and `.gitignore` is not consulted.
|
|
86
|
+
|
|
87
|
+
### Init
|
|
88
|
+
|
|
89
|
+
`adhere init` writes `.adhere/config.ts` and two example rules. It is safe to
|
|
90
|
+
rerun: by default it reports existing files as skipped and does not clobber
|
|
91
|
+
them. `--force` overwrites them. A config already at another accepted path is
|
|
92
|
+
kept, and no second one is added, even with `--force`.
|
|
93
|
+
|
|
94
|
+
### Validate
|
|
95
|
+
|
|
96
|
+
`adhere validate` loads the config, rule files, and presets the way `lint`
|
|
97
|
+
does, without calling Jev, so a file that fails to decode refuses here too, and
|
|
98
|
+
it prints how many rules it loaded. `--preset` adds a built-in rule set, as for
|
|
99
|
+
`lint`. It then checks the rules for local textual contradictions: it exits 1
|
|
100
|
+
and prints the conflicting rule files when overlapping rule scopes contain
|
|
101
|
+
opposite textual directives for the same topic, such as "Use service classes
|
|
102
|
+
for IO" and "Do not use service classes for IO". Same-id nested rules are
|
|
103
|
+
treated as intentional shadowing rather than contradictions.
|
|
104
|
+
|
|
12
105
|
## Config
|
|
13
106
|
|
|
14
|
-
A config file
|
|
15
|
-
|
|
107
|
+
A config file is optional. Without one, adhere reads the rule files in
|
|
108
|
+
`.adhere/` and any `--preset`. A config names presets, sets the model and
|
|
109
|
+
thresholds, or gives rules inline. It sits in the working directory of the
|
|
110
|
+
repo being audited, at one of these paths (keep one):
|
|
16
111
|
|
|
17
112
|
- `.adhere/config.ts`, the default, next to the rules
|
|
18
113
|
- `adhere.config.ts`
|
|
@@ -40,9 +135,15 @@ type UserId = typeof UserId.Type
|
|
|
40
135
|
```
|
|
41
136
|
|
|
42
137
|
The default export is decoded with Effect `Schema`. An invalid shape refuses
|
|
43
|
-
the run. The `import type` is erased at runtime
|
|
44
|
-
|
|
45
|
-
|
|
138
|
+
the run. The `import type` is erased at runtime. `defineConfig({...})` from
|
|
139
|
+
`@drkmttr/adhere` does the same thing as `satisfies Config`, and the executable
|
|
140
|
+
supplies it whether or not the repo has the package installed; the editor's
|
|
141
|
+
types come from the package as a dev dependency. A config can import other
|
|
142
|
+
files by relative path, but no packages besides `@drkmttr/adhere`: the
|
|
143
|
+
executable does not resolve `node_modules`.
|
|
144
|
+
|
|
145
|
+
`rules` in the config, inline as above or as a directory (below), replaces the
|
|
146
|
+
`.adhere/` rule files: none of them, root or nested, is read then.
|
|
46
147
|
|
|
47
148
|
### Rules as Markdown files
|
|
48
149
|
|
|
@@ -51,32 +152,6 @@ config and the cache (neither is read as a rule). The path without `.md` is the
|
|
|
51
152
|
rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
|
|
52
153
|
directory exists, it is read without any config.
|
|
53
154
|
|
|
54
|
-
`.adhere/` is the default rather than `docs/adhere/` because it keeps
|
|
55
|
-
everything adhere owns in one directory: the config and the cache are tool
|
|
56
|
-
state, not documentation, and would stay in `.adhere/` anyway. A nested
|
|
57
|
-
`.adhere/` also scopes its rules to the directory that contains it (below). The
|
|
58
|
-
cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
|
|
59
|
-
`--hidden`. A repo that would rather keep its rules with the rest of its
|
|
60
|
-
documentation can point `rules` at `docs/adhere/`:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
export default { presets: ["effect"], rules: "./docs/adhere" } satisfies Config;
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Rules read through `rules` apply project-wide; nested `.adhere/` directories
|
|
67
|
-
are not read then. A config is otherwise needed only to name presets or set
|
|
68
|
-
thresholds.
|
|
69
|
-
|
|
70
|
-
Nested `.adhere/` directories are also discovered. A rule in
|
|
71
|
-
`packages/api/.adhere/data/brand-ports.md` has the same id,
|
|
72
|
-
`data/brand-ports`, but applies only to files under `packages/api/`. Root
|
|
73
|
-
`.adhere/` rules apply project-wide. If a nested rule has the same id as a root
|
|
74
|
-
rule, the nearest containing `.adhere/` shadows the less-specific rule for that
|
|
75
|
-
subtree; outside that subtree, the root rule still applies. There is no broader
|
|
76
|
-
priority system: presets are global, project rules override preset rules with
|
|
77
|
-
the same id, and nested project rules override less-specific project rules with
|
|
78
|
-
the same id for files in their subtree.
|
|
79
|
-
|
|
80
155
|
A file is front matter, then a body. The first fenced code block in the body
|
|
81
156
|
is the reference; prose around it renders on GitHub and is ignored. Without a
|
|
82
157
|
fence, the whole body is the reference.
|
|
@@ -101,6 +176,31 @@ const Port = Schema.Int.pipe(
|
|
|
101
176
|
validation refuses the run with its path in the message. `loadRules(directory)`
|
|
102
177
|
from the package root does the same load for your own tooling.
|
|
103
178
|
|
|
179
|
+
Nested `.adhere/` directories are also discovered, except under
|
|
180
|
+
`node_modules/`, `dist/`, and the other skipped directories (above). A rule in
|
|
181
|
+
`packages/api/.adhere/data/brand-ports.md` has the same id,
|
|
182
|
+
`data/brand-ports`, but applies only to files under `packages/api/`. Root
|
|
183
|
+
`.adhere/` rules apply project-wide. If a nested rule has the same id as a root
|
|
184
|
+
rule, the nearest containing `.adhere/` shadows the less-specific rule for that
|
|
185
|
+
subtree; outside that subtree, the root rule still applies. There is no broader
|
|
186
|
+
priority system: presets are global, project rules override preset rules with
|
|
187
|
+
the same id, and nested project rules override less-specific project rules with
|
|
188
|
+
the same id for files in their subtree.
|
|
189
|
+
|
|
190
|
+
`.adhere/` is the default rather than `docs/adhere/` because it keeps
|
|
191
|
+
everything adhere owns in one directory: the config and the cache are tool
|
|
192
|
+
state, not documentation, and would stay in `.adhere/` anyway. A nested
|
|
193
|
+
`.adhere/` also scopes its rules to the directory that contains it (above). The
|
|
194
|
+
cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
|
|
195
|
+
`--hidden`. A repo that would rather keep its rules with the rest of its
|
|
196
|
+
documentation can point `rules` at `docs/adhere/`:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export default { presets: ["effect"], rules: "./docs/adhere" } satisfies Config;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Rules read through `rules` apply project-wide.
|
|
203
|
+
|
|
104
204
|
### Presets
|
|
105
205
|
|
|
106
206
|
A preset has the shape of a config without `presets`: `rules`, and optionally
|
|
@@ -118,56 +218,52 @@ order (a later preset wins), then the defaults `jev-latest` and `0.7`. A rule
|
|
|
118
218
|
in `rules` replaces a preset rule with the same id. A rule's own `threshold`
|
|
119
219
|
beats all of the above for that rule.
|
|
120
220
|
|
|
121
|
-
##
|
|
122
|
-
|
|
123
|
-
```sh
|
|
124
|
-
npm install --global @drkmttr/adhere # or: bun add --global @drkmttr/adhere
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
`adhere` is the native executable (below), compiled for macOS and Linux on
|
|
128
|
-
arm64 and x64 and for Windows on x64. Each platform's is its own package,
|
|
129
|
-
`@drkmttr/adhere-<platform>-<arch>`, and an optional dependency of
|
|
130
|
-
`@drkmttr/adhere` that the install fetches only on that platform. The package's
|
|
131
|
-
`bin/adhere.js` runs it with Node, so neither Bun nor adhere's runtime
|
|
132
|
-
dependencies are needed.
|
|
133
|
-
|
|
134
|
-
## Run
|
|
221
|
+
## How a file is judged
|
|
135
222
|
|
|
136
|
-
|
|
223
|
+
1. One Jev request per file, with the file's numbered lines and every rule as
|
|
224
|
+
the state, and one `noul` (yes/no probability) question per rule. Every
|
|
225
|
+
question shares the state cost of the request.
|
|
226
|
+
2. A second request only when at least one rule's probability is above its
|
|
227
|
+
threshold: one `choice` question per flagged rule over the file's non-blank
|
|
228
|
+
lines, which yields the line to report. A file with more than 255 non-blank
|
|
229
|
+
lines is located in two steps (a block of 20 lines, then a line inside it).
|
|
230
|
+
Files longer than 5100 lines are skipped and counted in the summary.
|
|
137
231
|
|
|
138
|
-
|
|
139
|
-
bun install
|
|
140
|
-
bun link # puts `adhere` on PATH
|
|
141
|
-
adhere --preset effect
|
|
142
|
-
```
|
|
232
|
+
## Cache
|
|
143
233
|
|
|
144
|
-
|
|
145
|
-
|
|
234
|
+
Judgments are cached in `.adhere/cache/` (add it to `.gitignore`), one entry
|
|
235
|
+
per file. An entry stores the file's content hash and, per rule, the
|
|
236
|
+
probability, the located line, and a fingerprint of the rule's text and model.
|
|
237
|
+
A changed file re-judges every rule for that file. An edited rule re-judges
|
|
238
|
+
only that rule. A lowered threshold locates cached judgments that are newly
|
|
239
|
+
above it without judging again. Entries depend on content, not on the
|
|
240
|
+
machine, so restoring `.adhere/cache/` between CI runs skips unchanged files.
|
|
146
241
|
|
|
147
|
-
`
|
|
148
|
-
|
|
149
|
-
at least one finding.
|
|
242
|
+
`TYPESAFE_API_KEY` is read only when a request is about to be sent. A run
|
|
243
|
+
where every file is cached needs no key and no network.
|
|
150
244
|
|
|
151
|
-
|
|
245
|
+
## Agent skill
|
|
152
246
|
|
|
153
|
-
|
|
154
|
-
adhere
|
|
155
|
-
|
|
156
|
-
|
|
247
|
+
[`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) teaches an agent to
|
|
248
|
+
gather a repo's conventions into rule files, configure adhere, and calibrate
|
|
249
|
+
thresholds. Install it with the [skills](https://github.com/vercel-labs/skills)
|
|
250
|
+
CLI: `skills add darkmatter/adhere`. The binary carries the same file:
|
|
251
|
+
`adhere skill` prints it, so `adhere skill > .agents/skills/adhere/SKILL.md`
|
|
252
|
+
works without a checkout.
|
|
157
253
|
|
|
158
|
-
|
|
159
|
-
and does not clobber them.
|
|
254
|
+
## Development
|
|
160
255
|
|
|
161
|
-
|
|
256
|
+
From a checkout, with Bun:
|
|
162
257
|
|
|
163
258
|
```sh
|
|
164
|
-
|
|
259
|
+
bun install
|
|
260
|
+
bun link # puts `adhere` on PATH
|
|
261
|
+
adhere lint --preset effect
|
|
165
262
|
```
|
|
166
263
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
are treated as intentional shadowing rather than contradictions.
|
|
264
|
+
In a checkout no platform package is installed, so `bin/adhere.js` runs
|
|
265
|
+
`src/main.ts` with Bun instead. `bun run typecheck` runs `tsc` and the linter,
|
|
266
|
+
and `bun run test` runs the tests.
|
|
171
267
|
|
|
172
268
|
### Native executable
|
|
173
269
|
|
|
@@ -180,7 +276,12 @@ machine and still loads the repo's `config.ts` and Markdown rules from disk.
|
|
|
180
276
|
into its package under `dist/npm/`
|
|
181
277
|
([`scripts/npm-packages.ts`](./scripts/npm-packages.ts)).
|
|
182
278
|
|
|
183
|
-
|
|
279
|
+
Each platform's executable is its own npm package,
|
|
280
|
+
`@drkmttr/adhere-<platform>-<arch>`, limited by `os` and `cpu`, and an
|
|
281
|
+
optional dependency of `@drkmttr/adhere`, so an install fetches only its own
|
|
282
|
+
machine's. The package's `bin/adhere.js` finds it and runs it with Node.
|
|
283
|
+
|
|
284
|
+
### Release
|
|
184
285
|
|
|
185
286
|
Releases are cut by CI from pushed version tags. From a clean, up-to-date
|
|
186
287
|
`main`, push the release tag:
|
|
@@ -202,40 +303,11 @@ The publish workflow verifies that `package.json` matches the release tag,
|
|
|
202
303
|
builds the platform packages, and publishes each of them before
|
|
203
304
|
`@drkmttr/adhere`, which it first lists them in as `optionalDependencies` at
|
|
204
305
|
the same version. Every publish is `npm publish --access public --provenance`.
|
|
205
|
-
No npm token is used, so a new
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
1. One Jev request per file, with the file's numbered lines and every rule as
|
|
214
|
-
the state, and one `noul` (yes/no probability) question per rule. Every
|
|
215
|
-
question shares the state cost of the request.
|
|
216
|
-
2. A second request only when at least one rule's probability is above its
|
|
217
|
-
threshold: one `choice` question per flagged rule over the file's non-blank
|
|
218
|
-
lines, which yields the line to report. A file with more than 255 non-blank
|
|
219
|
-
lines is located in two steps (a block of 20 lines, then a line inside it).
|
|
220
|
-
Files longer than 5100 lines are skipped and counted in the summary.
|
|
221
|
-
|
|
222
|
-
## Cache
|
|
223
|
-
|
|
224
|
-
Judgments are cached in `.adhere/cache/` (add it to `.gitignore`), one entry
|
|
225
|
-
per file. An entry stores the file's content hash and, per rule, the
|
|
226
|
-
probability, the located line, and a fingerprint of the rule's text and model.
|
|
227
|
-
A changed file re-judges every rule for that file. An edited rule re-judges
|
|
228
|
-
only that rule. A lowered threshold locates cached judgments that are newly
|
|
229
|
-
above it without judging again.
|
|
230
|
-
|
|
231
|
-
`TYPESAFE_API_KEY` is read only when a request is about to be sent. A run
|
|
232
|
-
where every file is cached needs no key and no network.
|
|
233
|
-
|
|
234
|
-
## Agent skill
|
|
235
|
-
|
|
236
|
-
[`skills/adhere/SKILL.md`](./skills/adhere/SKILL.md) teaches an agent to
|
|
237
|
-
gather a repo's conventions into rule files, configure adhere, and calibrate
|
|
238
|
-
thresholds. Install it with the [skills](https://github.com/vercel-labs/skills)
|
|
239
|
-
CLI: `skills add darkmatter/adhere`. The binary carries the same file:
|
|
240
|
-
`adhere skill` prints it, so `adhere skill > .agents/skills/adhere/SKILL.md`
|
|
241
|
-
works without a checkout.
|
|
306
|
+
No npm token is used, so a new package has to be published once by hand
|
|
307
|
+
before it can be given trusted publishers on npmjs.com. npm checks the
|
|
308
|
+
workflow that started the run, so every package has two: `release.yaml`, which
|
|
309
|
+
calls the publish workflow for tag releases, and `publish.yaml`, for manual
|
|
310
|
+
runs. The publish workflow can be run manually for an already-created release
|
|
311
|
+
tag if a publish needs to be retried; it skips every package whose version is
|
|
312
|
+
already on npm. The main package's `files` list keeps it to `bin/`, `src/`,
|
|
313
|
+
`presets/`, and `skills/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against reference code in .adhere/, judged by Jev.",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./src/index.ts"
|
|
19
19
|
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src",
|
|
23
|
+
"presets",
|
|
24
|
+
"skills"
|
|
25
|
+
],
|
|
20
26
|
"publishConfig": {
|
|
21
27
|
"access": "public"
|
|
22
28
|
},
|
|
@@ -61,10 +67,10 @@
|
|
|
61
67
|
}
|
|
62
68
|
},
|
|
63
69
|
"optionalDependencies": {
|
|
64
|
-
"@drkmttr/adhere-darwin-arm64": "0.
|
|
65
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
66
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
67
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
68
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
70
|
+
"@drkmttr/adhere-darwin-arm64": "0.3.1",
|
|
71
|
+
"@drkmttr/adhere-darwin-x64": "0.3.1",
|
|
72
|
+
"@drkmttr/adhere-linux-arm64": "0.3.1",
|
|
73
|
+
"@drkmttr/adhere-linux-x64": "0.3.1",
|
|
74
|
+
"@drkmttr/adhere-win32-x64": "0.3.1"
|
|
69
75
|
}
|
|
70
76
|
}
|
package/skills/adhere/SKILL.md
CHANGED
|
@@ -38,7 +38,7 @@ Keep rules about intent, shape, and placement:
|
|
|
38
38
|
- "Business logic depends on a config service, not on `Config.*` reads."
|
|
39
39
|
- "Domain errors are `Schema.TaggedError` classes, not thrown `Error`."
|
|
40
40
|
|
|
41
|
-
Skip rules a preset already covers. Run `adhere --help` and read
|
|
41
|
+
Skip rules a preset already covers. Run `adhere lint --help` and read
|
|
42
42
|
`presets/effect/` in the adhere repo before writing Effect rules; extend with
|
|
43
43
|
`presets: ["effect"]` and add only what is specific to this repo.
|
|
44
44
|
|
|
@@ -95,8 +95,9 @@ uncached files; in this org it comes from SOPS via `just shell`.
|
|
|
95
95
|
## 5. Run and calibrate
|
|
96
96
|
|
|
97
97
|
```sh
|
|
98
|
-
adhere
|
|
99
|
-
adhere
|
|
98
|
+
adhere validate # decodes the config and rules, checks contradictions
|
|
99
|
+
adhere lint # or: adhere lint --preset effect
|
|
100
|
+
adhere lint --threshold 0.9 # re-reads the cache, sends nothing
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
Then, per rule, read the findings and decide:
|
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { render, runAudit } from "#workflows/audit.ts";
|
|
|
6
6
|
import type { Overrides } from "#config.ts";
|
|
7
7
|
import { findContradictions, formatContradictions } from "#contradictions.ts";
|
|
8
8
|
import { initProject } from "#init.ts";
|
|
9
|
-
import { presetNames } from "#presets.ts";
|
|
9
|
+
import { type PresetName, presetNames } from "#presets.ts";
|
|
10
10
|
import { globalRuleSet } from "#rules.ts";
|
|
11
11
|
import {
|
|
12
12
|
Console,
|
|
@@ -39,13 +39,6 @@ class ContradictionsReported extends Schema.TaggedError<ContradictionsReported>(
|
|
|
39
39
|
readonly [Runtime.errorReported] = false;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
class UsageReported extends Schema.TaggedError<UsageReported>()(
|
|
43
|
-
"UsageReported",
|
|
44
|
-
{ message: Schema.String },
|
|
45
|
-
) {
|
|
46
|
-
readonly [Runtime.errorReported] = false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
42
|
const preset = Flag.choice("preset", presetNames).pipe(
|
|
50
43
|
Flag.optional,
|
|
51
44
|
Flag.withDescription("Add a built-in rule set. With a preset, .adhere/config.ts is optional."),
|
|
@@ -58,6 +51,14 @@ const threshold = Flag.float("threshold").pipe(
|
|
|
58
51
|
),
|
|
59
52
|
);
|
|
60
53
|
|
|
54
|
+
const force = Flag.boolean("force").pipe(
|
|
55
|
+
Flag.withDefault(false),
|
|
56
|
+
Flag.withDescription("Overwrite existing scaffold files."),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const chosenPresets = (flag: Option.Option<PresetName>): ReadonlyArray<PresetName> =>
|
|
60
|
+
Option.isSome(flag) ? [flag.value] : [];
|
|
61
|
+
|
|
61
62
|
export const auditLayer = (overrides: Overrides) =>
|
|
62
63
|
Layer.mergeAll(
|
|
63
64
|
SourceWalkerLive,
|
|
@@ -65,14 +66,8 @@ export const auditLayer = (overrides: Overrides) =>
|
|
|
65
66
|
JevLive.pipe(Layer.provide(FetchHttpClient.layer)),
|
|
66
67
|
).pipe(Layer.provideMerge(AdhereConfigLive(overrides)));
|
|
67
68
|
|
|
68
|
-
/** `adhere
|
|
69
|
-
export const
|
|
70
|
-
Command.withDescription(
|
|
71
|
-
"Print the agent skill that gathers a repo's conventions into rules and configures adhere. Pipe it into .agents/skills/adhere/SKILL.md or hand it to an agent.",
|
|
72
|
-
),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const audit = Command.make("adhere", { preset, threshold }, () =>
|
|
69
|
+
/** `adhere lint`: the audit. */
|
|
70
|
+
export const lintCommand = Command.make("lint", { preset, threshold }, () =>
|
|
76
71
|
Effect.gen(function* () {
|
|
77
72
|
const result = yield* runAudit;
|
|
78
73
|
const stdio = yield* Stdio.Stdio;
|
|
@@ -90,85 +85,66 @@ const audit = Command.make("adhere", { preset, threshold }, () =>
|
|
|
90
85
|
),
|
|
91
86
|
Command.provide((input) =>
|
|
92
87
|
auditLayer({
|
|
93
|
-
presets:
|
|
88
|
+
presets: chosenPresets(input.preset),
|
|
94
89
|
threshold: Option.getOrUndefined(input.threshold),
|
|
95
90
|
}),
|
|
96
91
|
),
|
|
97
92
|
);
|
|
98
93
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
--force Overwrite existing scaffold files
|
|
105
|
-
--help Show this help
|
|
106
|
-
|
|
107
|
-
Examples:
|
|
108
|
-
adhere init
|
|
109
|
-
adhere init --force`;
|
|
110
|
-
|
|
111
|
-
const CONTRADICTIONS_HELP = `Usage: adhere contradictions
|
|
112
|
-
|
|
113
|
-
Check this project's configured adhere rules for local textual contradictions.
|
|
114
|
-
|
|
115
|
-
Examples:
|
|
116
|
-
adhere contradictions`;
|
|
117
|
-
|
|
118
|
-
export const runInitCommand = (args: ReadonlyArray<string>) =>
|
|
94
|
+
/**
|
|
95
|
+
* `adhere validate`: everything `lint` loads, without Jev. A config, rule file,
|
|
96
|
+
* or preset that does not decode refuses here the way it would refuse `lint`.
|
|
97
|
+
*/
|
|
98
|
+
export const validateCommand = Command.make("validate", { preset }, () =>
|
|
119
99
|
Effect.gen(function* () {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
yield*
|
|
128
|
-
return yield* UsageReported.make({ message: "invalid init arguments" });
|
|
100
|
+
const config = yield* AdhereConfig;
|
|
101
|
+
const path = yield* Path.Path;
|
|
102
|
+
const entries = config.scopedRules ?? globalRuleSet(config.rules, path.resolve());
|
|
103
|
+
const contradictions = findContradictions(entries);
|
|
104
|
+
const loaded = `${entries.length} ${entries.length === 1 ? "rule" : "rules"} loaded.`;
|
|
105
|
+
yield* Console.log([loaded, formatContradictions(contradictions)].join("\n"));
|
|
106
|
+
if (contradictions.length > 0) {
|
|
107
|
+
return yield* ContradictionsReported.make({ count: contradictions.length });
|
|
129
108
|
}
|
|
109
|
+
}),
|
|
110
|
+
).pipe(
|
|
111
|
+
Command.withDescription(
|
|
112
|
+
"Load the config, rule files, and presets the way lint does, without calling Jev, and check the rules for contradictions.",
|
|
113
|
+
),
|
|
114
|
+
Command.provide((input) => AdhereConfigLive({ presets: chosenPresets(input.preset) })),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/** `adhere init`: the scaffold. */
|
|
118
|
+
export const initCommand = Command.make("init", { force }, (input) =>
|
|
119
|
+
Effect.gen(function* () {
|
|
130
120
|
const path = yield* Path.Path;
|
|
131
|
-
const result = yield* initProject(path.resolve(), {
|
|
132
|
-
force: args.includes("--force"),
|
|
133
|
-
});
|
|
121
|
+
const result = yield* initProject(path.resolve(), { force: input.force });
|
|
134
122
|
const lines = [
|
|
135
|
-
result.created.length > 0
|
|
136
|
-
|
|
137
|
-
: "created: none",
|
|
138
|
-
result.skipped.length > 0
|
|
139
|
-
? `skipped: ${result.skipped.join(", ")}`
|
|
140
|
-
: "skipped: none",
|
|
123
|
+
result.created.length > 0 ? `created: ${result.created.join(", ")}` : "created: none",
|
|
124
|
+
result.skipped.length > 0 ? `skipped: ${result.skipped.join(", ")}` : "skipped: none",
|
|
141
125
|
];
|
|
142
126
|
yield* Console.log(lines.join("\n"));
|
|
143
|
-
})
|
|
127
|
+
}),
|
|
128
|
+
).pipe(Command.withDescription("Scaffold .adhere/config.ts and example Markdown rules."));
|
|
144
129
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (args.length > 0) {
|
|
152
|
-
yield* Console.error(
|
|
153
|
-
`Unknown option for adhere contradictions: ${args.join(", ")}`,
|
|
154
|
-
);
|
|
155
|
-
yield* Console.error(CONTRADICTIONS_HELP);
|
|
156
|
-
return yield* UsageReported.make({
|
|
157
|
-
message: "invalid contradictions arguments",
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
const config = yield* AdhereConfig;
|
|
161
|
-
const path = yield* Path.Path;
|
|
162
|
-
const root = path.resolve();
|
|
163
|
-
const entries = config.scopedRules ?? globalRuleSet(config.rules, root);
|
|
164
|
-
const contradictions = findContradictions(entries);
|
|
165
|
-
yield* Console.log(formatContradictions(contradictions));
|
|
166
|
-
if (contradictions.length > 0) {
|
|
167
|
-
return yield* ContradictionsReported.make({
|
|
168
|
-
count: contradictions.length,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
});
|
|
130
|
+
/** `adhere skill`: the agent skill for writing rules, as shipped in the binary. */
|
|
131
|
+
export const skillCommand = Command.make("skill", {}, () => Console.log(skill.trimEnd())).pipe(
|
|
132
|
+
Command.withDescription(
|
|
133
|
+
"Print the agent skill that gathers a repo's conventions into rules and configures adhere. Pipe it into .agents/skills/adhere/SKILL.md or hand it to an agent.",
|
|
134
|
+
),
|
|
135
|
+
);
|
|
172
136
|
|
|
173
|
-
/** The whole CLI
|
|
174
|
-
export const
|
|
137
|
+
/** The whole CLI. Bare `adhere` prints its help. */
|
|
138
|
+
export const cli = Command.make("adhere").pipe(
|
|
139
|
+
Command.withDescription("Lint a repository against reference code in .adhere/, judged by Jev."),
|
|
140
|
+
Command.withExamples([
|
|
141
|
+
{ command: "adhere init", description: "Scaffold .adhere/config.ts and two example rules" },
|
|
142
|
+
{ command: "adhere validate", description: "Check the config and rules without calling Jev" },
|
|
143
|
+
{ command: "adhere lint", description: "Audit the working directory" },
|
|
144
|
+
{
|
|
145
|
+
command: "adhere lint --preset effect",
|
|
146
|
+
description: "Audit against the built-in Effect rules",
|
|
147
|
+
},
|
|
148
|
+
]),
|
|
149
|
+
Command.withSubcommands([lintCommand, validateCommand, initCommand, skillCommand]),
|
|
150
|
+
);
|
package/src/config.ts
CHANGED
|
@@ -38,6 +38,25 @@ export const CONFIG_FILES = [
|
|
|
38
38
|
"adhere.config.ts",
|
|
39
39
|
".adhere.config.ts",
|
|
40
40
|
] as const;
|
|
41
|
+
/**
|
|
42
|
+
* Directories never read into, for source files or for nested `.adhere/`
|
|
43
|
+
* rules: dependency, generated, vendored, and tool trees. Matched as whole
|
|
44
|
+
* path segments below the working directory, so a repo that itself sits under
|
|
45
|
+
* one of these names is still read.
|
|
46
|
+
*/
|
|
47
|
+
export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
|
|
48
|
+
"node_modules",
|
|
49
|
+
"dist",
|
|
50
|
+
".agents",
|
|
51
|
+
".claude",
|
|
52
|
+
".direnv",
|
|
53
|
+
".alchemy",
|
|
54
|
+
"coverage",
|
|
55
|
+
".vite",
|
|
56
|
+
"references",
|
|
57
|
+
"vendor",
|
|
58
|
+
"e2e",
|
|
59
|
+
]);
|
|
41
60
|
export interface AdhereConfig extends Schema.Schema.Type<typeof AdhereConfig> {}
|
|
42
61
|
|
|
43
62
|
/** The shape a config file default-exports. */
|
package/src/main.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { cli } from "#cli.ts";
|
|
3
|
+
import * as adhere from "#index.ts";
|
|
4
4
|
import { version } from "#version.ts";
|
|
5
5
|
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
6
|
+
import { plugin } from "bun";
|
|
6
7
|
import { Effect } from "effect";
|
|
7
8
|
import { Command } from "effect/unstable/cli";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// A config file may import `@drkmttr/adhere`, for `defineConfig`. The compiled
|
|
11
|
+
// executable resolves no packages from disk, so it answers that import with
|
|
12
|
+
// its own copy, whether or not the audited repo has the package installed.
|
|
13
|
+
plugin({
|
|
14
|
+
name: "@drkmttr/adhere",
|
|
15
|
+
setup: (build) => {
|
|
16
|
+
build.module("@drkmttr/adhere", () => ({ exports: { ...adhere }, loader: "object" }));
|
|
17
|
+
},
|
|
18
|
+
});
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
subcommand === "init"
|
|
13
|
-
? runInitCommand(args)
|
|
14
|
-
: subcommand === "contradictions"
|
|
15
|
-
? runContradictionsCommand(args).pipe(Effect.provide(AdhereConfigLive({})))
|
|
16
|
-
: Command.run(auditCommand, { version });
|
|
17
|
-
|
|
18
|
-
program.pipe(
|
|
20
|
+
Command.run(cli, { version }).pipe(
|
|
19
21
|
// oxlint-disable-next-line effecttsgo/strict-effect-provide -- this executable entrypoint supplies Bun's process services.
|
|
20
22
|
Effect.provide(BunServices.layer),
|
|
21
23
|
BunRuntime.runMain,
|