@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.
Files changed (113) hide show
  1. package/README.md +406 -90
  2. package/package.json +7 -7
  3. package/presets/alchemy/apis/match-remote-errors-by-tag.md +6 -2
  4. package/presets/alchemy/apis/schemas-at-trust-boundaries.md +6 -2
  5. package/presets/alchemy/data/commit-migrations-then-deploy.md +6 -2
  6. package/presets/alchemy/data/hyperdrive-direct-origin.md +6 -2
  7. package/presets/alchemy/data/migrations-use-direct-url.md +6 -2
  8. package/presets/alchemy/data/never-disable-tls-verification.md +6 -2
  9. package/presets/alchemy/data/private-endpoint-at-runtime.md +6 -2
  10. package/presets/alchemy/data/single-migration-owner.md +6 -2
  11. package/presets/alchemy/durable/idempotent-event-handlers.md +6 -2
  12. package/presets/alchemy/durable/io-inside-steps.md +6 -2
  13. package/presets/alchemy/durable/no-self-triggering-bucket-writes.md +6 -2
  14. package/presets/alchemy/durable/stable-step-names.md +6 -2
  15. package/presets/alchemy/durable/webhook-state-is-truth.md +6 -2
  16. package/presets/alchemy/providers/diff-guards-unresolved-props.md +6 -2
  17. package/presets/alchemy/providers/idempotent-delete.md +8 -3
  18. package/presets/alchemy/providers/lazy-layer-construction.md +6 -2
  19. package/presets/alchemy/providers/reconcile-from-observed-state.md +6 -2
  20. package/presets/alchemy/resources/anchor-paths-with-import-meta.md +6 -2
  21. package/presets/alchemy/resources/idempotent-actions-and-seeds.md +6 -2
  22. package/presets/alchemy/resources/previews-reference-shared-resources.md +6 -2
  23. package/presets/alchemy/resources/protect-irreplaceable-data.md +6 -2
  24. package/presets/alchemy/resources/remote-state-for-ci.md +6 -2
  25. package/presets/alchemy/resources/stable-logical-ids.md +6 -2
  26. package/presets/alchemy/resources/stage-unique-physical-names.md +6 -2
  27. package/presets/alchemy/runtime/bind-tag-not-implementation.md +6 -2
  28. package/presets/alchemy/runtime/deterministic-config-defaults.md +6 -2
  29. package/presets/alchemy/runtime/disposables-in-request-scope.md +6 -2
  30. package/presets/alchemy/runtime/lambda-no-post-response-work.md +6 -2
  31. package/presets/alchemy/runtime/no-per-request-work-at-construction.md +6 -2
  32. package/presets/alchemy/runtime/resolve-config-and-bindings-at-construction.md +6 -2
  33. package/presets/alchemy/secrets/let-alchemy-mint-infra-secrets.md +6 -2
  34. package/presets/alchemy/secrets/none-in-browser-env.md +6 -2
  35. package/presets/alchemy/secrets/redacted-until-use.md +6 -2
  36. package/presets/alchemy/secrets/unwrap-before-compare-or-send.md +6 -2
  37. package/presets/alchemy/security/authorize-public-endpoints.md +6 -2
  38. package/presets/alchemy/security/github-secret-not-variable.md +6 -2
  39. package/presets/alchemy/security/identity-from-verified-session.md +6 -2
  40. package/presets/alchemy/security/keep-auth-protections-on.md +6 -2
  41. package/presets/alchemy/security/least-privilege-bindings.md +6 -2
  42. package/presets/alchemy/security/least-privilege-ci-credentials.md +6 -2
  43. package/presets/alchemy/security/lookup-failure-is-not-anonymous.md +6 -2
  44. package/presets/effect/basics/gen-for-sequencing.md +7 -3
  45. package/presets/effect/basics/instrument-with-pipe.md +6 -2
  46. package/presets/effect/config/business-logic-depends-on-config-service.md +6 -2
  47. package/presets/effect/config/secrets-are-redacted.md +6 -2
  48. package/presets/effect/config/tests-provide-values-directly.md +7 -2
  49. package/presets/effect/config/validate-with-schema.md +6 -2
  50. package/presets/effect/data/brand-meaningful-primitives.md +7 -3
  51. package/presets/effect/data/variants-are-tagged-unions.md +7 -3
  52. package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
  53. package/presets/effect/errors/defects-for-bugs.md +6 -2
  54. package/presets/effect/errors/wrap-external-errors.md +6 -2
  55. package/presets/effect/services/dependencies-through-layers.md +7 -3
  56. package/presets/effect/services/fresh-layer-per-test.md +7 -2
  57. package/presets/effect/services/memoize-parameterized-layers.md +6 -2
  58. package/presets/effect/services/no-mutable-state.md +6 -2
  59. package/presets/effect/services/operations-have-no-requirements.md +6 -2
  60. package/presets/effect/services/test-layers-are-in-memory.md +7 -2
  61. package/presets/effect/testing/test-clock-for-time.md +7 -2
  62. package/presets/react/effects/clean-up-what-they-start.md +23 -0
  63. package/presets/react/effects/event-logic-in-handlers.md +29 -0
  64. package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
  65. package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
  66. package/presets/react/effects/notify-parents-in-handlers.md +29 -0
  67. package/presets/react/server/no-private-data-to-client.md +23 -0
  68. package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
  69. package/presets/react/state/no-contradictory-state.md +17 -0
  70. package/presets/react/state/no-props-copied-into-state.md +23 -0
  71. package/presets/react/state/store-ids-not-copies.md +20 -0
  72. package/presets/security/constant-time-secret-compare.md +22 -0
  73. package/presets/security/no-secrets-in-output.md +19 -0
  74. package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
  75. package/presets/security/parameterized-queries.md +16 -0
  76. package/presets/typescript/async/concurrent-independent-work.md +23 -0
  77. package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
  78. package/presets/typescript/design/no-argument-mutation.md +20 -0
  79. package/presets/typescript/errors/no-swallowed-errors.md +29 -0
  80. package/presets/typescript/errors/release-on-every-path.md +29 -0
  81. package/presets/typescript/testing/assert-outcomes.md +22 -0
  82. package/presets/typescript/testing/independent-tests.md +41 -0
  83. package/presets/typescript/types/assertions-only-when-proven.md +20 -0
  84. package/presets/typescript/types/derive-dont-restate.md +24 -0
  85. package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
  86. package/presets/typescript/types/validate-external-data.md +21 -0
  87. package/skills/adhere/SKILL.md +41 -12
  88. package/skills/adhere-fix/SKILL.md +180 -0
  89. package/src/cli.ts +59 -14
  90. package/src/comments.ts +36 -0
  91. package/src/config.ts +126 -14
  92. package/src/contradictions.ts +10 -2
  93. package/src/excerpt.ts +78 -58
  94. package/src/highlight.ts +2 -0
  95. package/src/index.ts +1 -1
  96. package/src/init.ts +119 -12
  97. package/src/main.ts +0 -1
  98. package/src/markdown.ts +83 -14
  99. package/src/models/Audit.ts +2 -0
  100. package/src/presets.ts +12 -3
  101. package/src/rules.ts +36 -3
  102. package/src/services/AdhereConfig.ts +36 -13
  103. package/src/services/AuditCache.ts +31 -5
  104. package/src/services/Jev.http.ts +76 -11
  105. package/src/services/Jev.ts +191 -73
  106. package/src/services/SourceWalker.ts +51 -13
  107. package/src/suppress.ts +119 -0
  108. package/src/version.ts +25 -1
  109. package/src/workflows/audit.ts +192 -40
  110. package/src/workflows/format.ts +71 -24
  111. package/presets/alchemy/data/parameterized-sql.md +0 -13
  112. package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -15
  113. package/presets/effect/basics/external-calls-are-resilient.md +0 -17
@@ -58,51 +58,77 @@ threshold: 0.8
58
58
 
59
59
  Optional prose for readers on GitHub. adhere ignores it.
60
60
 
61
- ```ts must
61
+ ## Must
62
+
63
+ ```ts
62
64
  // Correct code lifted from this repo, trimmed to the pattern.
63
65
  ```
64
66
 
65
- ```ts never
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 fences:
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 in a fence tagged `never`, which Jev reads as what
79
- a violation looks like. Add one whenever violations have a recognizable
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 tags its fences `should` and `should not`. A rule is one
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. Use a type-only import so
95
- the file also loads under the native binary:
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 type { Config } from "@drkmttr/adhere";
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
- } satisfies Config;
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 { Overrides } from "#config.ts";
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 = (overrides: Overrides, filter: ReadonlyArray<string> = []) =>
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(overrides)), Layer.provideMerge(statusLayer));
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
- if (result.findings.length > 0) {
246
- yield* FindingsReported.make({ count: result.findings.length });
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 = (overrides: Overrides) =>
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(overrides)), Layer.provideMerge(statusLayer));
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(Command.withDescription("Scaffold .adhere/config.ts and example Markdown rules."));
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
- /** `adhere skill`: the agent skill for writing rules, as shipped in the binary. */
380
- export const skillCommand = Command.make("skill", {}, () => Console.log(skill.trimEnd())).pipe(
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 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.",
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
 
@@ -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
+ };