@drkmttr/adhere 0.7.1 → 0.8.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/LICENSE +21 -0
- package/README.md +156 -32
- package/package.json +13 -12
- package/presets/effect/basics/external-calls-are-resilient.md +1 -1
- package/presets/effect/testing/test-clock-for-time.md +1 -1
- package/skills/adhere/SKILL.md +3 -2
- package/src/cli.ts +39 -15
- package/src/excerpt.ts +255 -0
- package/src/highlight.ts +82 -20
- package/src/init.ts +10 -4
- package/src/main.ts +35 -4
- package/src/mechanical.ts +89 -0
- package/src/presets.ts +1 -2
- package/src/rules.ts +2 -0
- package/src/services/AdhereConfig.ts +13 -17
- package/src/services/AuditCache.ts +272 -23
- package/src/services/Jev.http.ts +92 -18
- package/src/services/Jev.ts +77 -7
- package/src/services/SourceWalker.ts +11 -4
- package/src/wording.ts +90 -0
- package/src/workflows/audit.ts +210 -30
- package/src/workflows/format.ts +77 -21
- package/presets/effect/basics/fn-for-named-effects.md +0 -19
- package/presets/effect/data/decode-json-with-schema.md +0 -18
- package/presets/effect/errors/domain-errors-are-tagged.md +0 -19
- package/presets/effect/platform/os-access-through-platform-services.md +0 -26
- package/presets/effect/services/methods-have-no-requirements.md +0 -20
- package/presets/effect/services/provide-at-entry.md +0 -23
- package/presets/effect/testing/test-random.md +0 -22
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darkmatter Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@ model) whether the file breaks each rule, gets a calibrated probability per
|
|
|
8
8
|
rule, and reports the ones above a threshold with the line Jev points at. The
|
|
9
9
|
report uses the same frame as `vp lint`.
|
|
10
10
|
|
|
11
|
-
Deterministic rules (substring matches, type checks) belong in a normal linter
|
|
11
|
+
Deterministic rules (substring matches, type checks) belong in a normal linter;
|
|
12
|
+
`adhere validate` points out a rule that looks like one.
|
|
12
13
|
|
|
13
|
-
A preset for effect is included which can be run without setup:
|
|
14
|
+
A preset for effect is included which can be run without setup:
|
|
14
15
|
|
|
15
16
|
```sh
|
|
16
17
|
TYPESAFE_API_KEY=xxx npx @drkmttr/adhere lint --preset effect
|
|
@@ -36,20 +37,25 @@ with a TypeSafe AI API key: the one `adhere login` saved, or
|
|
|
36
37
|
|
|
37
38
|
```sh
|
|
38
39
|
adhere init # .adhere/config.ts and two example rules
|
|
39
|
-
echo ".adhere/cache/" >> .gitignore
|
|
40
40
|
adhere login # save your TypeSafe AI API key, once
|
|
41
|
-
adhere validate # Checks your rules
|
|
41
|
+
adhere validate # Checks your rules' wording, and contradictions using Jev
|
|
42
42
|
adhere lint # audit the working directory
|
|
43
|
+
git add .adhere # commit the rules, and the judgments they cost
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
Without a config or rules, `adhere lint --preset effect` audits against the
|
|
46
47
|
built-in Effect rules. A finding looks like this:
|
|
47
48
|
|
|
48
49
|
```text
|
|
49
|
-
× data/brand-ports (0.93): A port
|
|
50
|
-
╭─[src/server.ts:
|
|
51
|
-
4 │ const
|
|
52
|
-
|
|
50
|
+
× data/brand-ports (0.93): A port must be a branded, range-checked integer, never a bare number.
|
|
51
|
+
╭─[src/server.ts:6:3]
|
|
52
|
+
4 │ export const serve = Effect.gen(function* () {
|
|
53
|
+
5 │ const host = process.env.HOST ?? "localhost";
|
|
54
|
+
6 │ const port: number = Number(process.env.PORT ?? 3000);
|
|
55
|
+
· ──────────────────────────────────────────────────────
|
|
56
|
+
7 │ yield* listen({ host, port });
|
|
57
|
+
8 │ yield* Effect.log(`Listening on ${host}:${port}`);
|
|
58
|
+
9 │ });
|
|
53
59
|
╰────
|
|
54
60
|
hint: const Port = Schema.Int.pipe(
|
|
55
61
|
Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
|
|
@@ -60,7 +66,10 @@ Found 1 error.
|
|
|
60
66
|
42 files, 3 judged, 39 cached.
|
|
61
67
|
```
|
|
62
68
|
|
|
63
|
-
The header is the rule id, Jev's probability, and the rule's description.
|
|
69
|
+
The header is the rule id, Jev's probability, and the rule's description.
|
|
70
|
+
Under it is the line Jev points at, underlined, with the code around it: the
|
|
71
|
+
largest statement around the line that is 30 lines or fewer, usually the whole
|
|
72
|
+
function, and up to 3 lines of whole statements on either side. The
|
|
64
73
|
hint is the rule's code that must be written; a rule with only code that must
|
|
65
74
|
never be written shows that code, labeled `never:`, instead. On a terminal, the report is in color and the code
|
|
66
75
|
in it is highlighted. The exit code is 0 when nothing is reported and 1 when
|
|
@@ -78,7 +87,8 @@ adhere lint --yes # send the requests without asking first
|
|
|
78
87
|
adhere lint --limit 500 # judge at most 500 checks; the rest wait for the next run
|
|
79
88
|
adhere lint --rpm 30 # send at most 30 requests a minute
|
|
80
89
|
adhere lint --filter 'src/**' # read only the files a glob matches
|
|
81
|
-
adhere
|
|
90
|
+
adhere lint --log-level debug # log each request to Jev on stderr
|
|
91
|
+
adhere validate # check the rules' wording; ask Jev whether any contradict
|
|
82
92
|
adhere init [--force] # scaffold .adhere/config.ts and two example rules
|
|
83
93
|
adhere login # save a TypeSafe AI API key for later runs
|
|
84
94
|
adhere logout # delete the saved key
|
|
@@ -107,6 +117,13 @@ the report goes to stdout. A request Jev refuses stops the run with the file,
|
|
|
107
117
|
the HTTP status, and Jev's reason. Files finished before it are cached, so
|
|
108
118
|
running again continues from there.
|
|
109
119
|
|
|
120
|
+
The firewall in front of Jev's API can refuse a request whose code reads to
|
|
121
|
+
it as an attack, with a 403 and an HTML page rather than Jev's JSON. It refuses
|
|
122
|
+
the same request every run, so that file is skipped, the run goes on, and the
|
|
123
|
+
report lists each such file with Cloudflare's Ray ID, which TypeSafe AI can
|
|
124
|
+
look the block up by. When only the question that finds the line is refused,
|
|
125
|
+
the file's judgments are kept, and a rerun sends that question alone.
|
|
126
|
+
|
|
110
127
|
### Limiting a run
|
|
111
128
|
|
|
112
129
|
Three flags bound what a run does. `--limit <checks>` judges at most that many
|
|
@@ -122,6 +139,22 @@ more, and start a pattern with `!` to leave out what it matches. Together:
|
|
|
122
139
|
adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 200 --rpm 30
|
|
123
140
|
```
|
|
124
141
|
|
|
142
|
+
### Logging a run
|
|
143
|
+
|
|
144
|
+
`--log-level debug` logs a run's work on stderr: the config it loaded, how many
|
|
145
|
+
paths it listed and how many of them it reads, and each request to Jev, with
|
|
146
|
+
the file it is for, about how many tokens it carries, the HTTP status, how long
|
|
147
|
+
it took, and Cloudflare's Ray ID. A failed attempt is logged even when a retry
|
|
148
|
+
hides it. `--log-level trace` adds each file as it is read, planned, and
|
|
149
|
+
answered from the cache, and the first 4000 characters of any error Jev's API
|
|
150
|
+
answers with. The counter stays off at these levels, since its redraws would
|
|
151
|
+
garble the lines, and stdout still carries only the report, so the log can go
|
|
152
|
+
to a file of its own:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
adhere lint --log-level debug 2> adhere.log
|
|
156
|
+
```
|
|
157
|
+
|
|
125
158
|
### What gets read
|
|
126
159
|
|
|
127
160
|
`adhere lint` reads the `.ts` files under the working directory, except `.d.ts`,
|
|
@@ -143,16 +176,45 @@ kept, and no second one is added, even with `--force`.
|
|
|
143
176
|
|
|
144
177
|
`adhere validate` loads the config, rule files, and presets the way `lint`
|
|
145
178
|
does, so a file that fails to decode refuses here too, and it prints how many
|
|
146
|
-
rules it loaded. `--preset` adds a built-in rule set, as for `lint`.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
rules it loaded. `--preset` adds a built-in rule set, as for `lint`.
|
|
180
|
+
|
|
181
|
+
Next it checks each rule's wording against the
|
|
182
|
+
[rule writing tips](#rule-writing-tips), which sends nothing, and prints each
|
|
183
|
+
rule worded otherwise with its file and how:
|
|
184
|
+
|
|
185
|
+
- its description does not say a word its code is under, such as "never"
|
|
186
|
+
beside a `never` block;
|
|
187
|
+
- it has code to write but no example of code that breaks it;
|
|
188
|
+
- its description says the other kind of rule's words: "should" in a
|
|
189
|
+
requirement, or "must" or "never" in a guideline.
|
|
190
|
+
|
|
191
|
+
A rule with only code that must never be written needs no `must` example, as
|
|
192
|
+
[Rules as Markdown files](#rules-as-markdown-files) explains. The tips are
|
|
193
|
+
advice, so the wording does not change the exit code.
|
|
194
|
+
|
|
195
|
+
Then it reports the linter check, which also sends nothing. While `lint`
|
|
196
|
+
judges a file against one of the project's own rules, it asks Jev a second
|
|
197
|
+
question beside it: should the rule have been checked by a regular linter, that
|
|
198
|
+
is, could a linter or type checker have decided exactly whether this file
|
|
199
|
+
follows it? It asks on 10 files per rule, spread over the files it judges, and
|
|
200
|
+
keeps the answers in the cache until the rule's text changes. `validate` prints
|
|
201
|
+
each rule flagged on 7 or more of its 10 files as one a regular linter should
|
|
202
|
+
probably check, and each flagged on 3 to 6 as a rule that reads differently
|
|
203
|
+
from file to file, whose description should say more precisely what it
|
|
204
|
+
applies to. A rule with fewer than 10 answers is counted as waiting; `lint`
|
|
205
|
+
collects them as it judges files, so a repo whose files are all cached collects
|
|
206
|
+
them as its files change. Preset rules are left out: they are not the
|
|
207
|
+
project's to change. The linter check is advice too.
|
|
208
|
+
|
|
209
|
+
It then asks Jev whether any two rules that apply to the same files
|
|
210
|
+
contradict, so that no code can follow both: one request names, per rule, the
|
|
211
|
+
rule it conflicts with, if any, and each named pair then gets a probability
|
|
212
|
+
from a request of its own, holding only those two rules, since other rules
|
|
213
|
+
beside them dilute the judgment. A pair above the threshold is printed with
|
|
214
|
+
both rule files, and the exit code is 1. Two rules with the same id are not
|
|
215
|
+
compared: a nested rule that shares an id shadows the other on purpose. When
|
|
216
|
+
no two rules share files nothing is sent; otherwise `validate` needs the API
|
|
217
|
+
key, as `lint` does.
|
|
156
218
|
|
|
157
219
|
### Login
|
|
158
220
|
|
|
@@ -302,18 +364,47 @@ violations with the fewest false positives. In general:
|
|
|
302
364
|
|
|
303
365
|
On the Effect preset, rules written this way cut the findings Jev got wrong at
|
|
304
366
|
the default threshold from 6 to 1, and caught as many violations. For more
|
|
305
|
-
about the evaluations, see [the eval](eval/README.md).
|
|
367
|
+
about the evaluations, see [the eval](eval/README.md). `adhere validate` lists
|
|
368
|
+
each rule worded otherwise.
|
|
306
369
|
|
|
307
370
|
### Presets
|
|
308
371
|
|
|
309
372
|
A preset has the shape of a config without `presets`: `rules`, and optionally
|
|
310
|
-
`model` and `threshold`. `effect` is the one preset:
|
|
373
|
+
`model` and `threshold`. `effect` is the one preset: 19 Markdown rules in
|
|
311
374
|
[`presets/effect/`](./presets/effect/), lifted from the
|
|
312
375
|
[effect-solutions](https://github.com/kitlangton/effect-solutions) docs and the
|
|
313
376
|
[effect/platform](https://effect.website/docs/platform/introduction/) docs.
|
|
314
377
|
Name it in the config, or on the command line with `--preset effect`, in which
|
|
315
378
|
case the config file is optional.
|
|
316
379
|
|
|
380
|
+
The preset leaves out conventions a linter checks exactly. Effect's language
|
|
381
|
+
service, [`@effect/tsgo`](https://github.com/Effect-TS/tsgo) on TypeScript 7,
|
|
382
|
+
checks these seven, which the preset checked through 0.7. Most are off by
|
|
383
|
+
default; `npx @effect/tsgo setup` installs it, and these lines in its plugin
|
|
384
|
+
options in `tsconfig.json` turn them on:
|
|
385
|
+
|
|
386
|
+
```jsonc
|
|
387
|
+
"diagnosticSeverity": {
|
|
388
|
+
"strictEffectProvide": "warning", // layers are provided once, at the entry point
|
|
389
|
+
"leakingRequirements": "warning", // service methods have no requirements
|
|
390
|
+
"effectFnOpportunity": "warning", // a named function returning an Effect uses Effect.fn
|
|
391
|
+
"preferSchemaOverJson": "warning", // JSON is decoded with Schema, not JSON.parse
|
|
392
|
+
"nodeBuiltinImport": "warning", // platform services, not node: builtins
|
|
393
|
+
"globalFetch": "warning",
|
|
394
|
+
"globalFetchInEffect": "warning",
|
|
395
|
+
"processEnv": "warning",
|
|
396
|
+
"processEnvInEffect": "warning",
|
|
397
|
+
"globalRandom": "warning", // the Random service, not Math.random
|
|
398
|
+
"globalRandomInEffect": "warning",
|
|
399
|
+
"globalErrorInEffectFailure": "warning", // tagged errors, not Error
|
|
400
|
+
"extendsNativeError": "warning"
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`npx @effect/tsgo diagnostics --project tsconfig.json` runs them in CI. Bun
|
|
405
|
+
globals, which the old platform rule also ruled out, need a lint rule of their
|
|
406
|
+
own, such as `no-restricted-globals`.
|
|
407
|
+
|
|
317
408
|
### Precedence
|
|
318
409
|
|
|
319
410
|
Highest first: `--threshold` on the command line, the config file, presets in
|
|
@@ -333,7 +424,9 @@ beats all of the above for that rule.
|
|
|
333
424
|
at the rule's scope, so a file with no code the rule is about is a no. Since
|
|
334
425
|
no rule sits in the shared state, a rule's probability depends only on the
|
|
335
426
|
file and that rule, not on which other rules share the request. Every
|
|
336
|
-
question shares the state cost of the request.
|
|
427
|
+
question shares the state cost of the request. On up to 10 files per
|
|
428
|
+
project rule, the rule's question has a second one beside it, the linter
|
|
429
|
+
check (see [Validate](#validate)), with the same fields.
|
|
337
430
|
2. A second request only when at least one rule's probability is above its
|
|
338
431
|
threshold: one `choice` question per flagged rule over the file's non-blank
|
|
339
432
|
lines, which yields the line to report. A file with more than 255 non-blank
|
|
@@ -349,15 +442,42 @@ beats all of the above for that rule.
|
|
|
349
442
|
|
|
350
443
|
## Cache
|
|
351
444
|
|
|
352
|
-
Judgments are cached in `.adhere/cache
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
445
|
+
Judgments are cached in `.adhere/cache/`. Commit it. Every judgment is a paid
|
|
446
|
+
request, and Jev's answers vary a little from run to run, so a committed cache
|
|
447
|
+
gives everyone and CI the same findings without paying for them again, and a
|
|
448
|
+
pull request changes the judgments of only the files it changes.
|
|
449
|
+
|
|
450
|
+
`files/` holds Jev's answers by the content they are about: each cache file is
|
|
451
|
+
named for the hash of a source file's content, so a moved or copied file keeps
|
|
452
|
+
its judgments, and identical files share them. Each answer sits under a
|
|
453
|
+
fingerprint of the model and the question asked for the rule, which carries
|
|
454
|
+
the rule's text. A changed file re-judges every rule for that file. An edited
|
|
455
|
+
rule re-judges only that rule, and an adhere that asks its questions
|
|
456
|
+
differently re-judges every rule once. A lowered threshold locates cached
|
|
457
|
+
judgments that are newly above it without judging again. `tallies/` keeps the
|
|
458
|
+
linter check's answers, one tally per rule, under a key that changes with the
|
|
459
|
+
rule's text and the model.
|
|
460
|
+
|
|
461
|
+
A cache file is written once and never changed: it is also named for the hash
|
|
462
|
+
of its own text. Two branches that judge the same code add files rather than
|
|
463
|
+
edit them, so git merges the cache without a conflict. After a run that reads
|
|
464
|
+
every file, adhere prunes the cache: it deletes answers about content no file
|
|
465
|
+
has and answers to rule texts no rule asks, and folds what is left into one
|
|
466
|
+
file per content. A run narrowed by `--filter` does not prune. To keep the
|
|
467
|
+
cache out of diffs, mark it as generated in `.gitattributes`:
|
|
468
|
+
|
|
469
|
+
```text
|
|
470
|
+
.adhere/cache/** linguist-generated -diff
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Anyone who can commit can also write a cache file saying that code passes.
|
|
474
|
+
Where lint gates a merge, lint a pull request against the cache as merged,
|
|
475
|
+
not as the pull request has it; only the files it changes are judged again:
|
|
476
|
+
|
|
477
|
+
```sh
|
|
478
|
+
rm -rf .adhere/cache && git checkout origin/main -- .adhere/cache
|
|
479
|
+
adhere lint --yes
|
|
480
|
+
```
|
|
361
481
|
|
|
362
482
|
The API key is read only when a request is about to be sent. A run where
|
|
363
483
|
every file is cached needs no key and no network.
|
|
@@ -431,3 +551,7 @@ runs. The publish workflow can be run manually for an already-created release
|
|
|
431
551
|
tag if a publish needs to be retried; it skips every package whose version is
|
|
432
552
|
already on npm. The main package's `files` list keeps it to `bin/`, `src/`,
|
|
433
553
|
`presets/`, and `skills/`.
|
|
554
|
+
|
|
555
|
+
## License
|
|
556
|
+
|
|
557
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/darkmatter/adhere.git"
|
|
@@ -10,6 +11,12 @@
|
|
|
10
11
|
"bin": {
|
|
11
12
|
"adhere": "bin/adhere.js"
|
|
12
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"presets",
|
|
18
|
+
"skills"
|
|
19
|
+
],
|
|
13
20
|
"type": "module",
|
|
14
21
|
"imports": {
|
|
15
22
|
"#*": "./src/*"
|
|
@@ -17,12 +24,6 @@
|
|
|
17
24
|
"exports": {
|
|
18
25
|
".": "./src/index.ts"
|
|
19
26
|
},
|
|
20
|
-
"files": [
|
|
21
|
-
"bin",
|
|
22
|
-
"src",
|
|
23
|
-
"presets",
|
|
24
|
-
"skills"
|
|
25
|
-
],
|
|
26
27
|
"publishConfig": {
|
|
27
28
|
"access": "public"
|
|
28
29
|
},
|
|
@@ -67,10 +68,10 @@
|
|
|
67
68
|
}
|
|
68
69
|
},
|
|
69
70
|
"optionalDependencies": {
|
|
70
|
-
"@drkmttr/adhere-darwin-arm64": "0.
|
|
71
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
72
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
73
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
74
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.8.0",
|
|
72
|
+
"@drkmttr/adhere-darwin-x64": "0.8.0",
|
|
73
|
+
"@drkmttr/adhere-linux-arm64": "0.8.0",
|
|
74
|
+
"@drkmttr/adhere-linux-x64": "0.8.0",
|
|
75
|
+
"@drkmttr/adhere-win32-x64": "0.8.0"
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A call over the network, such as an HTTP request, a database query, or a third-party API, must
|
|
2
|
+
description: A call over the network, such as an HTTP request, a database query, or a third-party API, must be made with a timeout and a retry schedule, never bare. Calls through the platform FileSystem and Path services are local and are not in scope.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
```ts must
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A test that depends on time must use TestClock
|
|
2
|
+
description: A test that depends on time must use TestClock, never a real sleep or it.live, unless the test needs real time.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
```ts must
|
package/skills/adhere/SKILL.md
CHANGED
|
@@ -103,14 +103,15 @@ export default {
|
|
|
103
103
|
} satisfies Config;
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Commit `.adhere/cache/` with the code, so the team and CI reuse its judgments
|
|
107
|
+
instead of paying for them again. Uncached files need a TypeSafe AI API
|
|
107
108
|
key: one saved by `adhere login`, or `TYPESAFE_API_KEY`, which takes
|
|
108
109
|
precedence; in this org it comes from SOPS via `just shell`.
|
|
109
110
|
|
|
110
111
|
## 5. Run and calibrate
|
|
111
112
|
|
|
112
113
|
```sh
|
|
113
|
-
adhere validate # decodes the config and rules,
|
|
114
|
+
adhere validate # decodes the config and rules; reports their wording, rules a linter could check, and contradictions
|
|
114
115
|
adhere lint # or: adhere lint --preset effect
|
|
115
116
|
adhere lint --threshold 0.9 # re-reads the cache, sends nothing
|
|
116
117
|
```
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
executeAudit,
|
|
9
9
|
type FileDone,
|
|
10
10
|
planAudit,
|
|
11
|
+
pruneCache,
|
|
11
12
|
render,
|
|
12
13
|
} from "#workflows/audit.ts";
|
|
13
14
|
import {
|
|
@@ -20,16 +21,20 @@ import {
|
|
|
20
21
|
import type { Overrides } from "#config.ts";
|
|
21
22
|
import { findContradictions, formatContradictions } from "#contradictions.ts";
|
|
22
23
|
import { initProject } from "#init.ts";
|
|
24
|
+
import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
|
|
23
25
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
24
26
|
import { globalRuleSet } from "#rules.ts";
|
|
27
|
+
import { formatStrays, straysAmong } from "#wording.ts";
|
|
25
28
|
import {
|
|
26
29
|
Console,
|
|
27
30
|
Effect,
|
|
28
31
|
Exit,
|
|
29
32
|
Layer,
|
|
33
|
+
LogLevel,
|
|
30
34
|
Option,
|
|
31
35
|
Path,
|
|
32
36
|
Redacted,
|
|
37
|
+
References,
|
|
33
38
|
Runtime,
|
|
34
39
|
Schema,
|
|
35
40
|
Stdio,
|
|
@@ -118,8 +123,9 @@ const toStderr = (text: string) =>
|
|
|
118
123
|
* anything else it stays quiet, so a log gets the plan and the report rather
|
|
119
124
|
* than a line per file. A failed run keeps its last count on screen.
|
|
120
125
|
*/
|
|
121
|
-
const counterFor = (plan: AuditPlan) => {
|
|
122
|
-
|
|
126
|
+
const counterFor = (plan: AuditPlan, logging: LogLevel.LogLevel) => {
|
|
127
|
+
// Log lines at debug or below would land between redraws and garble the counter.
|
|
128
|
+
const live = process.stderr.isTTY === true && LogLevel.isGreaterThan(logging, "Debug");
|
|
123
129
|
let progress: Progress = { files: 0, requests: 0, findings: 0 };
|
|
124
130
|
const draw = () => (live ? toStderr(`\r\u001b[2K${progressLine(progress, plan)}`) : Effect.void);
|
|
125
131
|
return {
|
|
@@ -170,11 +176,13 @@ export const lintCommand = Command.make(
|
|
|
170
176
|
return yield* Cancelled.make({});
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
|
-
const counter = counterFor(plan);
|
|
179
|
+
const counter = counterFor(plan, yield* References.MinimumLogLevel);
|
|
174
180
|
yield* counter.start;
|
|
175
181
|
const result = yield* executeAudit(plan, counter.update).pipe(
|
|
176
182
|
Effect.onExit((exit) => counter.finish(Exit.isFailure(exit))),
|
|
177
183
|
);
|
|
184
|
+
// Only a run that read every file knows what the cache still needs.
|
|
185
|
+
if (input.filter.length === 0) yield* pruneCache(plan);
|
|
178
186
|
const color = yield* stdio.stdoutIsTerminal;
|
|
179
187
|
const path = yield* Path.Path;
|
|
180
188
|
// One write: separate Console.log calls have interleaved out of order here.
|
|
@@ -201,31 +209,44 @@ export const lintCommand = Command.make(
|
|
|
201
209
|
|
|
202
210
|
/** Jev, with the key `lint` uses, and the config it reads the model and threshold from. */
|
|
203
211
|
export const validateLayer = (overrides: Overrides) =>
|
|
204
|
-
|
|
205
|
-
Layer.provide([FetchHttpClient.layer, CredentialsLive]),
|
|
206
|
-
|
|
207
|
-
);
|
|
212
|
+
Layer.merge(
|
|
213
|
+
JevLive.pipe(Layer.provide([FetchHttpClient.layer, CredentialsLive])),
|
|
214
|
+
AuditCacheLive,
|
|
215
|
+
).pipe(Layer.provideMerge(AdhereConfigLive(overrides)));
|
|
208
216
|
|
|
209
217
|
/**
|
|
210
|
-
* `adhere validate`: everything `lint` loads, then
|
|
211
|
-
*
|
|
212
|
-
*
|
|
218
|
+
* `adhere validate`: everything `lint` loads, then how each rule's wording
|
|
219
|
+
* differs from the README's rule writing tips, then the linter check's tallies
|
|
220
|
+
* from lint, then Jev on whether any two rules that apply to the same files
|
|
221
|
+
* contradict. Only a contradiction fails the run: the tips and the linter
|
|
222
|
+
* check are advice. A config, rule file, or preset that does not decode
|
|
223
|
+
* refuses here the way it would refuse `lint`.
|
|
213
224
|
*/
|
|
214
225
|
export const validateCommand = Command.make("validate", { preset }, () =>
|
|
215
226
|
Effect.gen(function* () {
|
|
216
227
|
const config = yield* AdhereConfig;
|
|
217
228
|
const path = yield* Path.Path;
|
|
218
|
-
const
|
|
219
|
-
const
|
|
229
|
+
const root = path.resolve();
|
|
230
|
+
const entries = config.scopedRules ?? globalRuleSet(config.rules, root);
|
|
220
231
|
const loaded = `${entries.length} ${entries.length === 1 ? "rule" : "rules"} loaded.`;
|
|
221
|
-
yield*
|
|
232
|
+
const tallied = yield* tallyProjectRules(entries, config.model);
|
|
233
|
+
// The wording and the tallies need no request, so they show before Jev is asked anything.
|
|
234
|
+
yield* Console.log(
|
|
235
|
+
[
|
|
236
|
+
loaded,
|
|
237
|
+
...formatStrays(straysAmong(entries), root),
|
|
238
|
+
...formatLinterCheck(tallied, root),
|
|
239
|
+
].join("\n"),
|
|
240
|
+
);
|
|
241
|
+
const contradictions = yield* findContradictions(entries, config.threshold);
|
|
242
|
+
yield* Console.log(formatContradictions(contradictions));
|
|
222
243
|
if (contradictions.length > 0) {
|
|
223
244
|
return yield* ContradictionsReported.make({ count: contradictions.length });
|
|
224
245
|
}
|
|
225
246
|
}),
|
|
226
247
|
).pipe(
|
|
227
248
|
Command.withDescription(
|
|
228
|
-
"Load the config, rule files, and presets the way lint does, then ask Jev whether any two rules that apply to the same files contradict each other.",
|
|
249
|
+
"Load the config, rule files, and presets the way lint does, check each rule's wording against the README's rule writing tips, report the rules lint's linter check flags, then ask Jev whether any two rules that apply to the same files contradict each other.",
|
|
229
250
|
),
|
|
230
251
|
Command.provide((input) => validateLayer({ presets: chosenPresets(input.preset) })),
|
|
231
252
|
);
|
|
@@ -312,7 +333,10 @@ export const cli = Command.make("adhere").pipe(
|
|
|
312
333
|
Command.withDescription("Lint a repository against the rules in .adhere/, judged by Jev."),
|
|
313
334
|
Command.withExamples([
|
|
314
335
|
{ command: "adhere init", description: "Scaffold .adhere/config.ts and two example rules" },
|
|
315
|
-
{
|
|
336
|
+
{
|
|
337
|
+
command: "adhere validate",
|
|
338
|
+
description: "Check the rules' wording, and ask Jev whether any contradict",
|
|
339
|
+
},
|
|
316
340
|
{ command: "adhere login", description: "Save your TypeSafe AI API key for later runs" },
|
|
317
341
|
{ command: "adhere lint", description: "Audit the working directory" },
|
|
318
342
|
{
|