@drkmttr/adhere 0.9.5 → 0.11.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 +406 -90
- package/package.json +7 -7
- package/presets/alchemy/apis/match-remote-errors-by-tag.md +6 -2
- package/presets/alchemy/apis/schemas-at-trust-boundaries.md +6 -2
- package/presets/alchemy/data/commit-migrations-then-deploy.md +6 -2
- package/presets/alchemy/data/hyperdrive-direct-origin.md +6 -2
- package/presets/alchemy/data/migrations-use-direct-url.md +6 -2
- package/presets/alchemy/data/never-disable-tls-verification.md +6 -2
- package/presets/alchemy/data/private-endpoint-at-runtime.md +6 -2
- package/presets/alchemy/data/single-migration-owner.md +6 -2
- package/presets/alchemy/durable/idempotent-event-handlers.md +6 -2
- package/presets/alchemy/durable/io-inside-steps.md +6 -2
- package/presets/alchemy/durable/no-self-triggering-bucket-writes.md +6 -2
- package/presets/alchemy/durable/stable-step-names.md +6 -2
- package/presets/alchemy/durable/webhook-state-is-truth.md +6 -2
- package/presets/alchemy/providers/diff-guards-unresolved-props.md +6 -2
- package/presets/alchemy/providers/idempotent-delete.md +8 -3
- package/presets/alchemy/providers/lazy-layer-construction.md +6 -2
- package/presets/alchemy/providers/reconcile-from-observed-state.md +6 -2
- package/presets/alchemy/resources/anchor-paths-with-import-meta.md +6 -2
- package/presets/alchemy/resources/idempotent-actions-and-seeds.md +6 -2
- package/presets/alchemy/resources/previews-reference-shared-resources.md +6 -2
- package/presets/alchemy/resources/protect-irreplaceable-data.md +6 -2
- package/presets/alchemy/resources/remote-state-for-ci.md +6 -2
- package/presets/alchemy/resources/stable-logical-ids.md +6 -2
- package/presets/alchemy/resources/stage-unique-physical-names.md +6 -2
- package/presets/alchemy/runtime/bind-tag-not-implementation.md +6 -2
- package/presets/alchemy/runtime/deterministic-config-defaults.md +6 -2
- package/presets/alchemy/runtime/disposables-in-request-scope.md +6 -2
- package/presets/alchemy/runtime/lambda-no-post-response-work.md +6 -2
- package/presets/alchemy/runtime/no-per-request-work-at-construction.md +6 -2
- package/presets/alchemy/runtime/resolve-config-and-bindings-at-construction.md +6 -2
- package/presets/alchemy/secrets/let-alchemy-mint-infra-secrets.md +6 -2
- package/presets/alchemy/secrets/none-in-browser-env.md +6 -2
- package/presets/alchemy/secrets/redacted-until-use.md +6 -2
- package/presets/alchemy/secrets/unwrap-before-compare-or-send.md +6 -2
- package/presets/alchemy/security/authorize-public-endpoints.md +6 -2
- package/presets/alchemy/security/github-secret-not-variable.md +6 -2
- package/presets/alchemy/security/identity-from-verified-session.md +6 -2
- package/presets/alchemy/security/keep-auth-protections-on.md +6 -2
- package/presets/alchemy/security/least-privilege-bindings.md +6 -2
- package/presets/alchemy/security/least-privilege-ci-credentials.md +6 -2
- package/presets/alchemy/security/lookup-failure-is-not-anonymous.md +6 -2
- package/presets/effect/basics/gen-for-sequencing.md +7 -3
- package/presets/effect/basics/instrument-with-pipe.md +6 -2
- package/presets/effect/config/business-logic-depends-on-config-service.md +6 -2
- package/presets/effect/config/secrets-are-redacted.md +6 -2
- package/presets/effect/config/tests-provide-values-directly.md +7 -2
- package/presets/effect/config/validate-with-schema.md +6 -2
- package/presets/effect/data/brand-meaningful-primitives.md +7 -3
- package/presets/effect/data/variants-are-tagged-unions.md +7 -3
- package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
- package/presets/effect/errors/defects-for-bugs.md +6 -2
- package/presets/effect/errors/wrap-external-errors.md +6 -2
- package/presets/effect/services/dependencies-through-layers.md +7 -3
- package/presets/effect/services/fresh-layer-per-test.md +7 -2
- package/presets/effect/services/memoize-parameterized-layers.md +6 -2
- package/presets/effect/services/no-mutable-state.md +6 -2
- package/presets/effect/services/operations-have-no-requirements.md +6 -2
- package/presets/effect/services/test-layers-are-in-memory.md +7 -2
- package/presets/effect/testing/test-clock-for-time.md +7 -2
- package/presets/react/effects/clean-up-what-they-start.md +23 -0
- package/presets/react/effects/event-logic-in-handlers.md +29 -0
- package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
- package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
- package/presets/react/effects/notify-parents-in-handlers.md +29 -0
- package/presets/react/server/no-private-data-to-client.md +23 -0
- package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
- package/presets/react/state/no-contradictory-state.md +17 -0
- package/presets/react/state/no-props-copied-into-state.md +23 -0
- package/presets/react/state/store-ids-not-copies.md +20 -0
- package/presets/security/constant-time-secret-compare.md +22 -0
- package/presets/security/no-secrets-in-output.md +19 -0
- package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
- package/presets/security/parameterized-queries.md +16 -0
- package/presets/typescript/async/concurrent-independent-work.md +23 -0
- package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
- package/presets/typescript/design/no-argument-mutation.md +20 -0
- package/presets/typescript/errors/no-swallowed-errors.md +29 -0
- package/presets/typescript/errors/release-on-every-path.md +29 -0
- package/presets/typescript/testing/assert-outcomes.md +22 -0
- package/presets/typescript/testing/independent-tests.md +41 -0
- package/presets/typescript/types/assertions-only-when-proven.md +20 -0
- package/presets/typescript/types/derive-dont-restate.md +24 -0
- package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
- package/presets/typescript/types/validate-external-data.md +21 -0
- package/skills/adhere/SKILL.md +41 -12
- package/skills/adhere-fix/SKILL.md +180 -0
- package/src/cli.ts +59 -14
- package/src/comments.ts +36 -0
- package/src/config.ts +126 -14
- package/src/contradictions.ts +10 -2
- package/src/excerpt.ts +78 -58
- package/src/highlight.ts +2 -0
- package/src/index.ts +1 -1
- package/src/init.ts +119 -12
- package/src/main.ts +0 -1
- package/src/markdown.ts +83 -14
- package/src/models/Audit.ts +2 -0
- package/src/presets.ts +12 -3
- package/src/rules.ts +36 -3
- package/src/services/AdhereConfig.ts +36 -13
- package/src/services/AuditCache.ts +31 -5
- package/src/services/Jev.http.ts +76 -11
- package/src/services/Jev.ts +191 -73
- package/src/services/SourceWalker.ts +51 -13
- package/src/suppress.ts +119 -0
- package/src/version.ts +25 -1
- package/src/workflows/audit.ts +192 -40
- package/src/workflows/format.ts +71 -24
- package/presets/alchemy/data/parameterized-sql.md +0 -13
- package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -15
- package/presets/effect/basics/external-calls-are-resilient.md +0 -17
package/skills/adhere/SKILL.md
CHANGED
|
@@ -58,51 +58,77 @@ threshold: 0.8
|
|
|
58
58
|
|
|
59
59
|
Optional prose for readers on GitHub. adhere ignores it.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
## Must
|
|
62
|
+
|
|
63
|
+
```ts
|
|
62
64
|
// Correct code lifted from this repo, trimmed to the pattern.
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
## Never
|
|
68
|
+
|
|
69
|
+
```ts
|
|
66
70
|
// Optional: the incorrect form this rule catches, as it appeared in the repo.
|
|
67
71
|
```
|
|
68
72
|
````
|
|
69
73
|
|
|
70
74
|
Rules for the rule:
|
|
71
75
|
|
|
72
|
-
- `description` is one sentence, specific, in the same words as the
|
|
76
|
+
- `description` is one sentence, specific, in the same words as the headings:
|
|
73
77
|
what code must be, and what it must never be. Jev reads the description
|
|
74
78
|
with the code, and "must" in both is one demand; vague words ("properly",
|
|
75
79
|
"correctly") give it nothing to judge.
|
|
80
|
+
- A heading names the code under it only when it is the word alone: `## Never`,
|
|
81
|
+
not `## Never do this`. The next heading at its level or higher ends it.
|
|
76
82
|
- The `must` block is real code from the repo, not invented, and only correct
|
|
77
83
|
code: Jev compares files to it, so incorrect code there teaches the wrong
|
|
78
|
-
thing. Incorrect code goes
|
|
79
|
-
|
|
84
|
+
thing. Incorrect code goes under `## Never`, which Jev reads as what a
|
|
85
|
+
violation looks like. Add one whenever violations have a recognizable
|
|
80
86
|
shape, ideally one found in the repo's history: over the effect preset, a
|
|
81
87
|
`never` block raised how well Jev told violations from compliant code under
|
|
82
88
|
every wording tried. A rule with no single correct form can be only a
|
|
83
89
|
`never` block.
|
|
84
90
|
- A convention that is a guideline rather than a requirement says "should" in
|
|
85
|
-
its description and
|
|
86
|
-
or the other; it cannot mix the two.
|
|
91
|
+
its description and puts its code under `## Should` and `## Should not`. A
|
|
92
|
+
rule is one or the other; it cannot mix the two.
|
|
87
93
|
- One pattern per file. Two patterns in one `must` block blur the probability.
|
|
94
|
+
- Jev reads code without its comments, except comments that say `@adhere`. A
|
|
95
|
+
rule about comments, such as doc comments on exports, needs
|
|
96
|
+
`includeComments: true` in the config.
|
|
97
|
+
- Rules skip tests: `.test` and `.spec` files, such as `a.test.ts` and
|
|
98
|
+
`Button.spec.tsx`, and files under `test/`, `tests/`, `__tests__/`, or
|
|
99
|
+
`fixtures/`. A rule about tests says `tests: only` in its front matter, and
|
|
100
|
+
one that holds in tests as well `tests: include`.
|
|
101
|
+
- A nit, or a rule that tends to flag code wrongly, says `level: warning` in
|
|
102
|
+
its front matter: its findings show in amber and do not fail CI.
|
|
88
103
|
- `threshold` is optional. Start without it; set it in step 5 if needed.
|
|
89
104
|
|
|
90
105
|
## 4. Configure
|
|
91
106
|
|
|
92
107
|
Put the config at `.adhere/config.ts`. `adhere.config.ts` and
|
|
93
108
|
`.adhere.config.ts` also load; keep one. A config is optional when `.adhere/`
|
|
94
|
-
exists or a preset is passed on the command line.
|
|
95
|
-
|
|
109
|
+
exists or a preset is passed on the command line. `adhere init` scaffolds it
|
|
110
|
+
with `defineConfig`, adds `@drkmttr/adhere` as a dev dependency, and writes
|
|
111
|
+
`.adhere/tsconfig.json` so the editor resolves its types; the native binary
|
|
112
|
+
supplies the import either way:
|
|
96
113
|
|
|
97
114
|
```ts
|
|
98
|
-
import
|
|
115
|
+
import { defineConfig } from "@drkmttr/adhere";
|
|
99
116
|
|
|
100
|
-
export default {
|
|
117
|
+
export default defineConfig({
|
|
101
118
|
presets: ["effect"],
|
|
102
119
|
threshold: 0.75,
|
|
103
|
-
|
|
120
|
+
exclude: ["**/generated/**"],
|
|
121
|
+
});
|
|
104
122
|
```
|
|
105
123
|
|
|
124
|
+
To quiet a preset rule that does not suit the repo, override it rather than
|
|
125
|
+
copy it: `overrides: { "alchemy/providers/idempotent-delete": "warning" }`, or
|
|
126
|
+
`"off"`, or `{ threshold: 0.9 }`. The id is the one the report shows.
|
|
127
|
+
|
|
128
|
+
`exclude` lists globs, from the working directory, of files no rule judges.
|
|
129
|
+
Exclude generated and vendored code the skipped directories do not already
|
|
130
|
+
cover, since findings there are noise no one will fix.
|
|
131
|
+
|
|
106
132
|
Commit `.adhere/cache/` with the code, so the team and CI reuse its judgments
|
|
107
133
|
instead of paying for them again. Uncached files need a TypeSafe AI API
|
|
108
134
|
key: one saved by `adhere login`, or `TYPESAFE_API_KEY`, which takes
|
|
@@ -127,6 +153,9 @@ Then, per rule, read the findings and decide:
|
|
|
127
153
|
rule fires, remove the file. A rule that cannot fire is not a rule.
|
|
128
154
|
- Hits at 0.9 and above: read them first. They are usually real.
|
|
129
155
|
|
|
156
|
+
To verify and fix the findings themselves, one at a time, use the adhere-fix
|
|
157
|
+
skill, which `adhere skill fix` prints.
|
|
158
|
+
|
|
130
159
|
Threshold changes never re-judge; only edited rules do (per rule) and edited
|
|
131
160
|
files do (per file). Iterate on wording freely, it costs one request per file
|
|
132
161
|
per changed rule.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adhere-fix
|
|
3
|
+
description: Verify and fix the findings `adhere lint` reports. Decide whether each finding is a real violation of its rule, fix the real ones, and report the false ones with the evidence, suppressing a finding with an `adhere-ignore` comment only when the user allows it. Use when adhere, or CI running it, reports errors or warnings, or when asked to fix adhere findings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify and fix adhere findings
|
|
7
|
+
|
|
8
|
+
adhere is a linter for rules a normal linter cannot check. Jev, TypeSafe AI's
|
|
9
|
+
model, reads each file against each rule and answers with the probability that
|
|
10
|
+
the file breaks it; adhere reports each rule above a threshold, in the section
|
|
11
|
+
of the file Jev points to. A finding is a judgment, not a proof. On one rule adhere's authors
|
|
12
|
+
checked against the live API, only 1 of its 20 findings was a real violation.
|
|
13
|
+
Verify every finding before you change code for it: fix the real ones, and
|
|
14
|
+
report the false ones with the evidence.
|
|
15
|
+
|
|
16
|
+
## 1. Get the findings
|
|
17
|
+
|
|
18
|
+
Invoking this skill is the user's permission to run `adhere lint` as many
|
|
19
|
+
times as the work needs, and to pay for every request it sends to Jev. Never
|
|
20
|
+
ask before a run, never wait for a go-ahead, and never stop to report its cost:
|
|
21
|
+
the skill runs unattended, as in CI, where no one is there to answer. Pass
|
|
22
|
+
`--yes`, so adhere does not ask either.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
adhere lint --yes # judges what is not cached, then reports
|
|
26
|
+
adhere lint --yes --filter 'src/**' # only the files a glob matches
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Judgments are cached in `.adhere/cache/`, so a rerun judges only files and
|
|
30
|
+
rules that changed.
|
|
31
|
+
|
|
32
|
+
Each finding shows:
|
|
33
|
+
|
|
34
|
+
- the rule id, a preset's with its preset first, as in
|
|
35
|
+
`alchemy/providers/idempotent-delete`, Jev's probability, and the rule's
|
|
36
|
+
description;
|
|
37
|
+
- the section of the file Jev points at, a run of whole statements of about 30
|
|
38
|
+
lines, with the lines it names underlined, or marked with a bar when they
|
|
39
|
+
are several: usually where the violation starts, which may run on past them;
|
|
40
|
+
- sometimes `warning: this file may not show enough to check this rule`: the
|
|
41
|
+
rule may turn on another file, a library, or a service. Most of these are
|
|
42
|
+
false; read what the code relies on before fixing it. Its `help:` suggests
|
|
43
|
+
an `@adhere` note, which follows the same rule as a suppression (see below);
|
|
44
|
+
- `hint:`, the code the rule wants, or `never:` for a rule that only shows code
|
|
45
|
+
to avoid.
|
|
46
|
+
|
|
47
|
+
`×` marks an error, which fails the run; `⚠` a warning, which does not: a nit,
|
|
48
|
+
or a rule that tends to flag code wrongly. Verify warnings the same way; they
|
|
49
|
+
are wrong more often.
|
|
50
|
+
|
|
51
|
+
## 2. Read the rule
|
|
52
|
+
|
|
53
|
+
The report gives the rule's description and one example. Read the whole rule:
|
|
54
|
+
the code under `## Must` and `## Never`, and anything it puts out of scope.
|
|
55
|
+
|
|
56
|
+
- A project rule is `.adhere/<id>.md`, or under the directory the config's
|
|
57
|
+
`rules` names.
|
|
58
|
+
- A preset rule is `presets/<preset>/<id>.md` in the adhere package, in
|
|
59
|
+
`node_modules/@drkmttr/adhere/presets/`, or at
|
|
60
|
+
https://github.com/darkmatter/adhere/tree/main/presets.
|
|
61
|
+
|
|
62
|
+
## 3. Verify each finding
|
|
63
|
+
|
|
64
|
+
Read the code as a reviewer would, not only the excerpt:
|
|
65
|
+
|
|
66
|
+
- **The whole unit.** Read the function, handler, or class the section holds,
|
|
67
|
+
and what calls it.
|
|
68
|
+
- **What it calls.** Follow helpers into other files. A helper can already do
|
|
69
|
+
what the rule asks, such as a delete that looks unguarded calling a client
|
|
70
|
+
that treats "already gone" as success.
|
|
71
|
+
- **The rule's scope.** Code the rule is not about, such as a runtime binding
|
|
72
|
+
under a rule about providers' delete handlers, is a false finding.
|
|
73
|
+
- **Comments are claims.** A comment saying the code is fine is not evidence.
|
|
74
|
+
In adhere's study of one rule, 8 of 35 comments saying a delete succeeds on a
|
|
75
|
+
missing resource were wrong. Check the claim before you rely on it. Jev does
|
|
76
|
+
not see comments, except those that say `@adhere`, so it judged the code
|
|
77
|
+
without them.
|
|
78
|
+
- **Behavior outside the file.** When the rule turns on how an API or a library
|
|
79
|
+
behaves, find out: its source, its tests, its documentation. Typings help but
|
|
80
|
+
can be wrong. A live check can settle it, such as a call with an identifier
|
|
81
|
+
no resource has, but only when the user asked for live checks, and never
|
|
82
|
+
against a real resource. Do not ask for one: without it, the finding is
|
|
83
|
+
unsure.
|
|
84
|
+
|
|
85
|
+
Then decide:
|
|
86
|
+
|
|
87
|
+
- **real**: the code breaks the rule as it is written, in its scope;
|
|
88
|
+
- **false**: it does not, and you can say how you know;
|
|
89
|
+
- **unsure**: you could not tell. Do not guess; leave it for the user.
|
|
90
|
+
|
|
91
|
+
## 4. Fix a real finding
|
|
92
|
+
|
|
93
|
+
- Change the code the way the rule's `must` code shows, in the style of the
|
|
94
|
+
code around it, and only as much as the fix needs.
|
|
95
|
+
- Run the repo's tests and type check.
|
|
96
|
+
- Confirm with `adhere lint --yes --filter '<file>'`, which judges only the
|
|
97
|
+
changed file again. If the finding stays, reread the rule: the fix may not
|
|
98
|
+
be what it asks for.
|
|
99
|
+
- Try at most three fixes for one finding. If it stays after the third, undo
|
|
100
|
+
your changes for it, so no half-done fix is left, and leave it for the user.
|
|
101
|
+
|
|
102
|
+
## 5. Suppress a finding, only when allowed
|
|
103
|
+
|
|
104
|
+
By default, never add an `adhere-ignore` comment or an `@adhere` note, and
|
|
105
|
+
never change the ones already there. Leave the finding, and say why in the
|
|
106
|
+
report. Suppressing is allowed only when the user allows it: their request
|
|
107
|
+
says so, or `ADHERE_ALLOW_SUPPRESSIONS=1` is set in your environment, which
|
|
108
|
+
`printenv ADHERE_ALLOW_SUPPRESSIONS` shows.
|
|
109
|
+
|
|
110
|
+
When it is allowed, suppress:
|
|
111
|
+
|
|
112
|
+
- a false finding;
|
|
113
|
+
- a real finding that three fixes did not clear, or whose fix would break
|
|
114
|
+
something, such as behavior, a public API, or loading the code on a
|
|
115
|
+
platform. The reason names what the fix would break.
|
|
116
|
+
|
|
117
|
+
Above the statement the finding is in, add a comment naming the rule as the
|
|
118
|
+
report does, then `--` and the evidence:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// adhere-ignore alchemy/providers/idempotent-delete -- DeleteActivity succeeds on a missing activity; probed 2026-09-25
|
|
122
|
+
delete: Effect.fn(function* ({ output }) {
|
|
123
|
+
yield* sfn.deleteActivity({ activityArn: output.activityArn });
|
|
124
|
+
}),
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- The comment covers the whole statement that starts on the next line of code,
|
|
128
|
+
wherever in it Jev points. At the end of a line of code, it covers the
|
|
129
|
+
statement that starts on that line.
|
|
130
|
+
- `// adhere-ignore-file <rule> -- <reason>` covers a whole file. Use it only
|
|
131
|
+
when the rule is wrong for all of the file, such as generated code.
|
|
132
|
+
- Name several rules with commas: `// adhere-ignore <rule>, <rule> -- <reason>`.
|
|
133
|
+
- The reason says what you checked and what you found, so whoever reads it can
|
|
134
|
+
check it again. "False positive" is not a reason.
|
|
135
|
+
- Never suppress a finding you are unsure of, or a real one you have not
|
|
136
|
+
tried to fix.
|
|
137
|
+
- Jev never reads these comments: adhere removes them before it sends a file.
|
|
138
|
+
|
|
139
|
+
When suppressing is allowed and the finding was false because of a fact the
|
|
140
|
+
file does not show, such as how an API behaves, also write the fact where Jev
|
|
141
|
+
will see it the next time it judges this code: in a comment that says
|
|
142
|
+
`@adhere`, on the code the fact is about, with how you know.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
/**
|
|
146
|
+
* Deletes the activity. @adhere DeleteActivity succeeds on a missing
|
|
147
|
+
* activity, so no not-found error needs catching; probed 2026-09-25.
|
|
148
|
+
*/
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A note informs Jev and suppresses nothing: Jev still judges the code, so a
|
|
152
|
+
real violation added later is still found. Write one only for a fact you
|
|
153
|
+
checked. A wrong note hides real violations the way wrong comments did.
|
|
154
|
+
|
|
155
|
+
## 6. When a rule is wrong more often than right
|
|
156
|
+
|
|
157
|
+
If most of one rule's findings are false, suppressing them one by one hides
|
|
158
|
+
the problem. Stop suppressing that rule's findings, carry on with the other
|
|
159
|
+
rules, and in the report give the user the counts and suggest one of:
|
|
160
|
+
|
|
161
|
+
- in the config, `overrides: { "<rule id>": "warning" }`, or `"off"`, or
|
|
162
|
+
`{ threshold: 0.9 }`;
|
|
163
|
+
- for a project rule, rewording it to say what code it is about and what is
|
|
164
|
+
out of scope, following the rule writing tips:
|
|
165
|
+
https://github.com/darkmatter/adhere#rule-writing-tips.
|
|
166
|
+
|
|
167
|
+
## 7. Report
|
|
168
|
+
|
|
169
|
+
Tell the user, for each finding: the rule, the file and line, the verdict, and
|
|
170
|
+
what you did: fixed it, suppressed it with the reason, or left it for them.
|
|
171
|
+
Give the evidence for each false one. Name the rules that were mostly wrong,
|
|
172
|
+
and every finding you were unsure of.
|
|
173
|
+
|
|
174
|
+
If you left findings because suppressing was not allowed, end the report with
|
|
175
|
+
this message, filled in:
|
|
176
|
+
|
|
177
|
+
> Left N findings that suppressing would settle: `<rule>` at `<file>:<line>`,
|
|
178
|
+
> … To let the agent suppress findings it shows are false, or that three fixes
|
|
179
|
+
> did not clear, with the evidence in each comment, set
|
|
180
|
+
> `ADHERE_ALLOW_SUPPRESSIONS=1` in the agent's environment and run it again.
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Status } from "#services/Status.ts";
|
|
|
7
7
|
import {
|
|
8
8
|
type AuditPlan,
|
|
9
9
|
executeAudit,
|
|
10
|
+
failing,
|
|
10
11
|
type FileDone,
|
|
11
12
|
planAudit,
|
|
12
13
|
pruneCache,
|
|
@@ -19,9 +20,9 @@ import {
|
|
|
19
20
|
progressLine,
|
|
20
21
|
sendQuestion,
|
|
21
22
|
} from "#workflows/format.ts";
|
|
22
|
-
import type {
|
|
23
|
+
import type { Flags } from "#config.ts";
|
|
23
24
|
import { findContradictions, formatContradictions } from "#contradictions.ts";
|
|
24
|
-
import { initProject } from "#init.ts";
|
|
25
|
+
import { type Install, initProject } from "#init.ts";
|
|
25
26
|
import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
|
|
26
27
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
27
28
|
import { globalRuleSet } from "#rules.ts";
|
|
@@ -41,13 +42,14 @@ import {
|
|
|
41
42
|
Stdio,
|
|
42
43
|
Stream,
|
|
43
44
|
} from "effect";
|
|
44
|
-
import { Command, Flag, Prompt } from "effect/unstable/cli";
|
|
45
|
+
import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
45
46
|
import { FetchHttpClient } from "effect/unstable/http";
|
|
46
47
|
// A Bun text import (https://bun.sh/docs/bundler/loaders#text): the file's
|
|
47
48
|
// contents become a string at bundle time, so the compiled binary carries the
|
|
48
49
|
// skill without an --asset flag. `skills/` sits beside `src/`, outside the `#`
|
|
49
50
|
// import map, hence the relative path.
|
|
50
51
|
import skill from "../skills/adhere/SKILL.md" with { type: "text" };
|
|
52
|
+
import fixSkill from "../skills/adhere-fix/SKILL.md" with { type: "text" };
|
|
51
53
|
|
|
52
54
|
class FindingsReported extends Schema.TaggedError<FindingsReported>()("FindingsReported", {
|
|
53
55
|
count: Schema.Finite,
|
|
@@ -97,6 +99,13 @@ const threshold = Flag.float("threshold").pipe(
|
|
|
97
99
|
),
|
|
98
100
|
);
|
|
99
101
|
|
|
102
|
+
const sufficiencyThreshold = Flag.float("sufficiency-threshold").pipe(
|
|
103
|
+
Flag.optional,
|
|
104
|
+
Flag.withDescription(
|
|
105
|
+
"Warn on a finding when Jev's probability that the file shows enough to decide it is below this value, 0 to 1. Replaces the config's sufficiencyThreshold; the default is 0.6.",
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
100
109
|
const limit = Flag.integer("limit").pipe(
|
|
101
110
|
Flag.filter(
|
|
102
111
|
(checks) => checks >= 0,
|
|
@@ -124,6 +133,11 @@ const filter = Flag.string("filter").pipe(
|
|
|
124
133
|
),
|
|
125
134
|
);
|
|
126
135
|
|
|
136
|
+
const denyWarnings = Flag.boolean("deny-warnings").pipe(
|
|
137
|
+
Flag.withDefault(false),
|
|
138
|
+
Flag.withDescription("Fail the run on warnings as well as errors."),
|
|
139
|
+
);
|
|
140
|
+
|
|
127
141
|
const yes = Flag.boolean("yes").pipe(
|
|
128
142
|
Flag.withAlias("y"),
|
|
129
143
|
Flag.withDefault(false),
|
|
@@ -202,17 +216,17 @@ const force = Flag.boolean("force").pipe(
|
|
|
202
216
|
Flag.withDescription("Overwrite existing scaffold files."),
|
|
203
217
|
);
|
|
204
218
|
|
|
205
|
-
export const auditLayer = (
|
|
219
|
+
export const auditLayer = (flags: Flags, filter: ReadonlyArray<string> = []) =>
|
|
206
220
|
Layer.mergeAll(
|
|
207
221
|
SourceWalkerLive(filter),
|
|
208
222
|
AuditCacheLive,
|
|
209
223
|
JevLive.pipe(Layer.provide([FetchHttpClient.layer, CredentialsLive])),
|
|
210
|
-
).pipe(Layer.provideMerge(AdhereConfigLive(
|
|
224
|
+
).pipe(Layer.provideMerge(AdhereConfigLive(flags)), Layer.provideMerge(statusLayer));
|
|
211
225
|
|
|
212
226
|
/** `adhere lint`: the audit. */
|
|
213
227
|
export const lintCommand = Command.make(
|
|
214
228
|
"lint",
|
|
215
|
-
{ preset, threshold, yes, limit, rpm, filter },
|
|
229
|
+
{ preset, threshold, sufficiencyThreshold, yes, limit, rpm, filter, denyWarnings },
|
|
216
230
|
(input) =>
|
|
217
231
|
Effect.gen(function* () {
|
|
218
232
|
const stdio = yield* Stdio.Stdio;
|
|
@@ -242,8 +256,10 @@ export const lintCommand = Command.make(
|
|
|
242
256
|
const path = yield* Path.Path;
|
|
243
257
|
// One write: separate Console.log calls have interleaved out of order here.
|
|
244
258
|
yield* Console.log(render(result, { color, root: path.resolve() }).join("\n"));
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
// Only errors fail the run, unless --deny-warnings: a warning is for a nit or a noisy rule.
|
|
260
|
+
const failed = failing(result.findings, input.denyWarnings);
|
|
261
|
+
if (failed.length > 0) {
|
|
262
|
+
yield* FindingsReported.make({ count: failed.length });
|
|
247
263
|
}
|
|
248
264
|
}),
|
|
249
265
|
).pipe(
|
|
@@ -255,6 +271,7 @@ export const lintCommand = Command.make(
|
|
|
255
271
|
{
|
|
256
272
|
presets: input.preset,
|
|
257
273
|
threshold: Option.getOrUndefined(input.threshold),
|
|
274
|
+
sufficiencyThreshold: Option.getOrUndefined(input.sufficiencyThreshold),
|
|
258
275
|
rpm: Option.getOrUndefined(input.rpm),
|
|
259
276
|
},
|
|
260
277
|
input.filter,
|
|
@@ -263,11 +280,11 @@ export const lintCommand = Command.make(
|
|
|
263
280
|
);
|
|
264
281
|
|
|
265
282
|
/** Jev, with the key `lint` uses, and the config it reads the model and threshold from. */
|
|
266
|
-
export const validateLayer = (
|
|
283
|
+
export const validateLayer = (flags: Flags) =>
|
|
267
284
|
Layer.merge(
|
|
268
285
|
JevLive.pipe(Layer.provide([FetchHttpClient.layer, CredentialsLive])),
|
|
269
286
|
AuditCacheLive,
|
|
270
|
-
).pipe(Layer.provideMerge(AdhereConfigLive(
|
|
287
|
+
).pipe(Layer.provideMerge(AdhereConfigLive(flags)), Layer.provideMerge(statusLayer));
|
|
271
288
|
|
|
272
289
|
/**
|
|
273
290
|
* `adhere validate`: everything `lint` loads, then how each rule's wording
|
|
@@ -306,6 +323,17 @@ export const validateCommand = Command.make("validate", { preset }, () =>
|
|
|
306
323
|
Command.provide((input) => validateLayer({ presets: input.preset })),
|
|
307
324
|
);
|
|
308
325
|
|
|
326
|
+
const installLine = (install: Install): string => {
|
|
327
|
+
switch (install.status) {
|
|
328
|
+
case "installed":
|
|
329
|
+
return `installed: @drkmttr/adhere with ${install.packageManager}`;
|
|
330
|
+
case "listed":
|
|
331
|
+
return "installed: none, @drkmttr/adhere is already in package.json";
|
|
332
|
+
case "no-package-json":
|
|
333
|
+
return "installed: none, there is no package.json";
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
309
337
|
/** `adhere init`: the scaffold. */
|
|
310
338
|
export const initCommand = Command.make("init", { force }, (input) =>
|
|
311
339
|
Effect.gen(function* () {
|
|
@@ -314,10 +342,15 @@ export const initCommand = Command.make("init", { force }, (input) =>
|
|
|
314
342
|
const lines = [
|
|
315
343
|
result.created.length > 0 ? `created: ${result.created.join(", ")}` : "created: none",
|
|
316
344
|
result.skipped.length > 0 ? `skipped: ${result.skipped.join(", ")}` : "skipped: none",
|
|
345
|
+
installLine(result.install),
|
|
317
346
|
];
|
|
318
347
|
yield* Console.log(lines.join("\n"));
|
|
319
348
|
}),
|
|
320
|
-
).pipe(
|
|
349
|
+
).pipe(
|
|
350
|
+
Command.withDescription(
|
|
351
|
+
"Scaffold .adhere/config.ts and example Markdown rules, and add @drkmttr/adhere to devDependencies with the project's package manager.",
|
|
352
|
+
),
|
|
353
|
+
);
|
|
321
354
|
|
|
322
355
|
/**
|
|
323
356
|
* The key typed at a masked prompt, or piped in: `adhere login < key.txt`.
|
|
@@ -376,10 +409,22 @@ export const logoutCommand = Command.make("logout", {}, () =>
|
|
|
376
409
|
Command.provide(CredentialsLive),
|
|
377
410
|
);
|
|
378
411
|
|
|
379
|
-
/**
|
|
380
|
-
|
|
412
|
+
/** The agent skills the binary carries, by the name `adhere skill` takes. */
|
|
413
|
+
const skills = { rules: skill, fix: fixSkill } as const;
|
|
414
|
+
|
|
415
|
+
const skillName = Argument.choice("skill", ["rules", "fix"] as const).pipe(
|
|
416
|
+
Argument.withDefault("rules" as const),
|
|
417
|
+
Argument.withDescription(
|
|
418
|
+
"rules, the default, to gather a repo's conventions into rules and configure adhere; fix, to verify and fix the findings lint reports.",
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
/** `adhere skill [rules|fix]`: an agent skill, as shipped in the binary. */
|
|
423
|
+
export const skillCommand = Command.make("skill", { name: skillName }, ({ name }) =>
|
|
424
|
+
Console.log(skills[name].trimEnd()),
|
|
425
|
+
).pipe(
|
|
381
426
|
Command.withDescription(
|
|
382
|
-
"Print
|
|
427
|
+
"Print an agent skill: by default the one that gathers a repo's conventions into rules and configures adhere, or with fix, the one that verifies and fixes lint's findings. Pipe it into .agents/skills/<name>/SKILL.md or hand it to an agent.",
|
|
383
428
|
),
|
|
384
429
|
);
|
|
385
430
|
|
package/src/comments.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { scan } from "#highlight.ts";
|
|
2
|
+
|
|
3
|
+
/** `@adhere` anywhere in a comment makes it a note for Jev. */
|
|
4
|
+
const NOTE = /@adhere\b/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether a comment is a note for Jev: one that says `@adhere` anywhere in
|
|
8
|
+
* it, which stays when comments are taken out.
|
|
9
|
+
*/
|
|
10
|
+
export const isNote = (comment: string): boolean => NOTE.test(comment);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A file's lines with its comments taken out, but for those `keep` keeps:
|
|
14
|
+
* each comment's text removed and its line breaks kept, so every line keeps
|
|
15
|
+
* its number, and a line with only the spaces before a comment left empty. A
|
|
16
|
+
* string, template literal, or regular expression that holds `//` or `/*`
|
|
17
|
+
* stays whole, as the highlighter reads it.
|
|
18
|
+
*/
|
|
19
|
+
export const withoutComments = (
|
|
20
|
+
lines: ReadonlyArray<string>,
|
|
21
|
+
keep: (comment: string) => boolean = () => false,
|
|
22
|
+
): ReadonlyArray<string> => {
|
|
23
|
+
const code = lines.join("\n");
|
|
24
|
+
// No comment can start without one of these, so a file without them is sent as it is.
|
|
25
|
+
if (!code.includes("//") && !code.includes("/*")) return lines;
|
|
26
|
+
const tokens = scan(code);
|
|
27
|
+
// What the highlighter could not read, if anything, stays as it was.
|
|
28
|
+
const read = tokens.reduce((length, token) => length + token.text.length, 0);
|
|
29
|
+
const kept =
|
|
30
|
+
tokens
|
|
31
|
+
.map(({ text, kind }) =>
|
|
32
|
+
kind === "comment" && !keep(text) ? text.replace(/[^\n]/g, "") : text,
|
|
33
|
+
)
|
|
34
|
+
.join("") + code.slice(read);
|
|
35
|
+
return kept.split("\n").map((line, index) => (line === lines[index] ? line : line.trimEnd()));
|
|
36
|
+
};
|