@drkmttr/adhere 0.7.3 → 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/README.md +145 -32
- package/package.json +12 -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 +66 -12
- package/src/services/Jev.ts +66 -6
- package/src/services/SourceWalker.ts +11 -4
- package/src/wording.ts +90 -0
- package/src/workflows/audit.ts +169 -26
- package/src/workflows/format.ts +35 -18
- 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/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
|
|
@@ -129,6 +139,22 @@ more, and start a pattern with `!` to leave out what it matches. Together:
|
|
|
129
139
|
adhere lint --filter 'packages/api/**' --filter '!**/generated/**' --limit 200 --rpm 30
|
|
130
140
|
```
|
|
131
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
|
+
|
|
132
158
|
### What gets read
|
|
133
159
|
|
|
134
160
|
`adhere lint` reads the `.ts` files under the working directory, except `.d.ts`,
|
|
@@ -150,16 +176,45 @@ kept, and no second one is added, even with `--force`.
|
|
|
150
176
|
|
|
151
177
|
`adhere validate` loads the config, rule files, and presets the way `lint`
|
|
152
178
|
does, so a file that fails to decode refuses here too, and it prints how many
|
|
153
|
-
rules it loaded. `--preset` adds a built-in rule set, as for `lint`.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
163
218
|
|
|
164
219
|
### Login
|
|
165
220
|
|
|
@@ -309,18 +364,47 @@ violations with the fewest false positives. In general:
|
|
|
309
364
|
|
|
310
365
|
On the Effect preset, rules written this way cut the findings Jev got wrong at
|
|
311
366
|
the default threshold from 6 to 1, and caught as many violations. For more
|
|
312
|
-
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.
|
|
313
369
|
|
|
314
370
|
### Presets
|
|
315
371
|
|
|
316
372
|
A preset has the shape of a config without `presets`: `rules`, and optionally
|
|
317
|
-
`model` and `threshold`. `effect` is the one preset:
|
|
373
|
+
`model` and `threshold`. `effect` is the one preset: 19 Markdown rules in
|
|
318
374
|
[`presets/effect/`](./presets/effect/), lifted from the
|
|
319
375
|
[effect-solutions](https://github.com/kitlangton/effect-solutions) docs and the
|
|
320
376
|
[effect/platform](https://effect.website/docs/platform/introduction/) docs.
|
|
321
377
|
Name it in the config, or on the command line with `--preset effect`, in which
|
|
322
378
|
case the config file is optional.
|
|
323
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
|
+
|
|
324
408
|
### Precedence
|
|
325
409
|
|
|
326
410
|
Highest first: `--threshold` on the command line, the config file, presets in
|
|
@@ -340,7 +424,9 @@ beats all of the above for that rule.
|
|
|
340
424
|
at the rule's scope, so a file with no code the rule is about is a no. Since
|
|
341
425
|
no rule sits in the shared state, a rule's probability depends only on the
|
|
342
426
|
file and that rule, not on which other rules share the request. Every
|
|
343
|
-
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.
|
|
344
430
|
2. A second request only when at least one rule's probability is above its
|
|
345
431
|
threshold: one `choice` question per flagged rule over the file's non-blank
|
|
346
432
|
lines, which yields the line to report. A file with more than 255 non-blank
|
|
@@ -356,15 +442,42 @@ beats all of the above for that rule.
|
|
|
356
442
|
|
|
357
443
|
## Cache
|
|
358
444
|
|
|
359
|
-
Judgments are cached in `.adhere/cache
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
```
|
|
368
481
|
|
|
369
482
|
The API key is read only when a request is about to be sent. A run where
|
|
370
483
|
every file is cached needs no key and no network.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
"license": "MIT",
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
"bin": {
|
|
12
12
|
"adhere": "bin/adhere.js"
|
|
13
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"presets",
|
|
18
|
+
"skills"
|
|
19
|
+
],
|
|
14
20
|
"type": "module",
|
|
15
21
|
"imports": {
|
|
16
22
|
"#*": "./src/*"
|
|
@@ -18,12 +24,6 @@
|
|
|
18
24
|
"exports": {
|
|
19
25
|
".": "./src/index.ts"
|
|
20
26
|
},
|
|
21
|
-
"files": [
|
|
22
|
-
"bin",
|
|
23
|
-
"src",
|
|
24
|
-
"presets",
|
|
25
|
-
"skills"
|
|
26
|
-
],
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
@@ -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.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"
|
|
76
76
|
}
|
|
77
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
|
{
|