@drkmttr/adhere 0.14.0 → 0.17.5
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 +294 -146
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/presets/effect/config/tests-provide-values-directly/RULE.md +12 -1
- package/presets/effect/data/brand-meaningful-primitives/RULE.md +14 -2
- package/presets/effect/data/variants-are-tagged-unions/RULE.md +17 -4
- package/presets/effect/services/dependencies-through-layers/RULE.md +16 -3
- package/presets/effect/services/no-mutable-state/RULE.md +7 -1
- package/skills/adhere/SKILL.md +180 -31
- package/skills/adhere-setup/SKILL.md +142 -16
- package/src/cli.ts +56 -23
- package/src/config.ts +49 -16
- package/src/index.ts +14 -0
- package/src/markdown.ts +9 -5
- package/src/providers/cloudflare.ts +84 -0
- package/src/providers/jev.ts +66 -0
- package/src/providers/openai.ts +89 -0
- package/src/providers/post.ts +53 -0
- package/src/providers/provider.ts +131 -0
- package/src/providers/providers.test.ts +276 -0
- package/src/providers/systemOne.ts +95 -0
- package/src/services/AdhereConfig.ts +2 -4
- package/src/services/Credentials.ts +101 -51
- package/src/services/Jev.http.ts +165 -170
- package/src/services/Jev.provider.test.ts +207 -0
- package/src/services/Jev.reads.test.ts +439 -0
- package/src/services/Jev.ts +205 -113
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1040 -0
- package/src/services/SourceReads.ts +437 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +440 -265
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
- package/presets/effect/services/operations-have-no-requirements/RULE.md +0 -50
- package/presets/effect/services/test-layers-are-in-memory/RULE.md +0 -29
|
@@ -37,10 +37,23 @@ wherever this skill says `adhere`, run it the manager's way, as in
|
|
|
37
37
|
|
|
38
38
|
With pnpm, at a workspace root, add `--ignore-workspace-root-check`.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
package
|
|
40
|
+
Default lint enables `workspacePackages`, `symbols`, and `references`, and
|
|
41
|
+
starts native TypeScript only during execution, after confirmation, when
|
|
42
|
+
pending judgments or cached findings needing location require context. It discovers each
|
|
43
|
+
file's configured project, including package projects under solution roots,
|
|
44
|
+
and falls back to inferred context for unconfigured or excluded files. No root
|
|
45
|
+
`tsconfig.json` or literal root file-list membership is required; do not block
|
|
46
|
+
setup on either. `.adhere/tsconfig.json` only types the config and rules.
|
|
47
|
+
Planning, `--limit 0`, version, help, skills, and validate do not start the
|
|
48
|
+
compiler.
|
|
49
|
+
|
|
50
|
+
In a repo without a `package.json`, inspect any `adhere` on PATH with
|
|
51
|
+
`adhere --version`, then **Ask** whether to use the npm/Bun package launcher
|
|
52
|
+
for the full default or a raw executable with deliberately reduced context.
|
|
53
|
+
The raw release (`adhere-Windows-x86_64.exe` on Windows) needs no Node or
|
|
54
|
+
package manager, but its lint requires explicit config `reads: []` or
|
|
55
|
+
`reads: ["workspacePackages"]`. Do not silently disable readers to make it
|
|
56
|
+
work. Only fetch it after the user chooses that alternative:
|
|
44
57
|
|
|
45
58
|
```sh
|
|
46
59
|
curl -fsSL --create-dirs -o ~/.local/bin/adhere \
|
|
@@ -48,9 +61,9 @@ curl -fsSL --create-dirs -o ~/.local/bin/adhere \
|
|
|
48
61
|
chmod +x ~/.local/bin/adhere
|
|
49
62
|
```
|
|
50
63
|
|
|
51
|
-
Validate and lint need a TypeSafe AI API key: `TYPESAFE_API_KEY`,
|
|
52
|
-
by `adhere login`, which the user runs; never ask for the key in
|
|
53
|
-
it.
|
|
64
|
+
Validate and lint's paid requests need a TypeSafe AI API key: `TYPESAFE_API_KEY`,
|
|
65
|
+
or one saved by `adhere login`, which the user runs; never ask for the key in
|
|
66
|
+
chat or print it. Compiler-free planning with `--limit 0` needs no key.
|
|
54
67
|
|
|
55
68
|
## 2. Find candidates
|
|
56
69
|
|
|
@@ -124,7 +137,12 @@ the wording of any. Revise drafts the user wants changed and show them again.
|
|
|
124
137
|
- Without a config, run `adhere init`. It also writes two example rules under
|
|
125
138
|
`.adhere/rules/style/`; delete them unless the user chose them.
|
|
126
139
|
- Copy each chosen draft to `.adhere/rules/<topic>/<slug>/RULE.md`.
|
|
127
|
-
- Add chosen presets, or topics, to `presets` in `.adhere/config.ts`.
|
|
140
|
+
- Add chosen presets, or topics, to `presets` in `.adhere/config.ts`. Presets
|
|
141
|
+
use the audit-wide readers; they do not own or override `reads`.
|
|
142
|
+
- Leave `reads` omitted for the full default. Do not add `reads: []` to the
|
|
143
|
+
normal setup recommendation or as a workaround for missing compiler
|
|
144
|
+
resources. Only set an explicit replacement array if the user chooses to
|
|
145
|
+
disable or limit readers.
|
|
128
146
|
- Install chosen shared rules: `adhere install org/repo/<id>` for each, or
|
|
129
147
|
`adhere install org/repo` for all of them. They land in `.adhere/rules/org/repo/`.
|
|
130
148
|
- Add `exclude` globs for generated or vendored code the default skips miss.
|
|
@@ -146,15 +164,115 @@ which to keep, and apply it. Run it again until it reports no contradictions.
|
|
|
146
164
|
adhere lint --limit 0
|
|
147
165
|
```
|
|
148
166
|
|
|
149
|
-
This shows
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
This shows a compiler-free plan and judges nothing: overall files/rules/cache
|
|
168
|
+
check metrics, and files deferred for a later run. Planning reads/counts/hashes
|
|
169
|
+
own code and checks cached answers; `--limit 0` skips budgeting, native reads,
|
|
170
|
+
and HTTP execution. A positive limit counts files needing requests, not rule
|
|
171
|
+
checks. Only selected files' pending rules and cached-location work are budgeted;
|
|
172
|
+
deferred/waiting counts are files, which can later be found too large. Planning never calls
|
|
173
|
+
`SourceReads` or resolves native symbols/references. Explain the output/prompt
|
|
174
|
+
caveat: estimates omit native context and hook-added data, so actual tokens,
|
|
175
|
+
cost, and request splits may be higher. Source reload/hash work is bounded to
|
|
176
|
+
8 files; plans retain compact metadata, not all source text. Files are reloaded
|
|
177
|
+
for execution; if normalized code or ignore
|
|
178
|
+
directives changed since planning, rerun rather than using the old plan.
|
|
179
|
+
|
|
180
|
+
Tell the user that compiler type/docs strings and outgoing declaration snippets add
|
|
181
|
+
source/documentation sharing and token cost by default, including complete
|
|
182
|
+
scopes from files outside the audit filter. Whole declaration bodies can use more
|
|
183
|
+
tokens than fixed-line excerpts; budget overflow skips the file instead of
|
|
184
|
+
capping bodies to seven lines or silently truncating them. Compiler-provided
|
|
185
|
+
documentation and JSDoc tags can remain even when ordinary code comments are
|
|
186
|
+
stripped; omitting `symbols` disables type/docs context and omitting
|
|
187
|
+
`references` disables snippets. The global workspace catalog is not sent: only
|
|
188
|
+
this file's matched literal imports appear under canonical package keys.
|
|
189
|
+
Offer an explicit reader subset or code-only opt-out if they want less context;
|
|
190
|
+
never apply one silently. Neither reader selection nor dependency/shared-context
|
|
191
|
+
changes invalidate cached judgments. If they want fresh answers under changed
|
|
192
|
+
context, **Ask** before clearing `.adhere/cache/`; do not clear it automatically.
|
|
193
|
+
|
|
194
|
+
On a large repo, offer a subtree such as `--filter 'src/**'`, or a file cap
|
|
195
|
+
such as `--limit 100`: the first 100 files needing requests in path order, with
|
|
196
|
+
all pending rules per selected file. Judging plus cached-unlocated work in one
|
|
197
|
+
file uses one slot; a cached-locate-only file also counts. Fully cached and
|
|
198
|
+
already-located files report without using slots. After the cap, both judge and
|
|
199
|
+
locate work wait. Known cheap-budget skips refill from later files, but native
|
|
200
|
+
or HTTP-size skips after selection consume a slot without runtime refill.
|
|
201
|
+
|
|
202
|
+
Explain that a file cap is not an HTTP-request or token/cost cap: each file can
|
|
203
|
+
produce judge, locate, split-batch, and private hook requests. **Ask** before
|
|
204
|
+
sending, then run the command below with only the user's chosen filter/file cap:
|
|
153
205
|
|
|
154
206
|
```sh
|
|
155
207
|
adhere lint --yes
|
|
156
208
|
```
|
|
157
209
|
|
|
210
|
+
`SourceReads.prepare(paths)` registers eligible-path metadata only, with no
|
|
211
|
+
file I/O, compiler/SDK import, spawn, snapshot, or semantic query, even for a large list.
|
|
212
|
+
Only after confirmation do pending judgments or cached findings still needing
|
|
213
|
+
location trigger actual per-file native reads just before judge/locate. Native
|
|
214
|
+
transactions are serial through one permit; HTTP execution remains concurrent
|
|
215
|
+
at 8. Returned context is reused locally and full budgets are enforced before
|
|
216
|
+
HTTP, with oversized files skipped and no silent truncation. Shared state plus
|
|
217
|
+
one question must fit 32k tokens and state plus all questions must fit 64k per
|
|
218
|
+
request, with 1k reserved for overhead. The SDK may still parse a full selected
|
|
219
|
+
project, and additional monorepo programs may increase startup time and memory.
|
|
220
|
+
|
|
221
|
+
`symbols` remains `Record<string, string>`: compiler-rendered distinct type
|
|
222
|
+
blocks for same-name binding/narrowing contexts, with compiler documentation
|
|
223
|
+
and JSDoc tags once per canonical symbol per file. It does not promise literal
|
|
224
|
+
IDE labels or selected-call overload presentation. Symbol/type queries are
|
|
225
|
+
batched, and independent SDK requests are chunked to at most 8 within the single
|
|
226
|
+
active native document/snapshot; HTTP work remains concurrent at 8. `references` maps used names to canonical outgoing declarations
|
|
227
|
+
by pathname, selecting the smallest complete AST scope without extra surrounding
|
|
228
|
+
lines. Functions include full bodies; arrow/object initializer variables include
|
|
229
|
+
whole variable statements with `export const` and semicolons. Methods, getters,
|
|
230
|
+
and setters include their whole member, not the entire class; classes,
|
|
231
|
+
interfaces, and type aliases include the whole declaration. Repeated/overlapping
|
|
232
|
+
scopes merge and disjoint scopes join with `…`. This is syntactic scope selection,
|
|
233
|
+
not recursive helper-call expansion: helpers called by an imported function are
|
|
234
|
+
not automatically expanded. Scopes preserve indentation and the comment/directive
|
|
235
|
+
policy, including associated JSDoc when `includeComments` is true; compiler
|
|
236
|
+
docs/tags in `symbols` remain available regardless. Current-file, declaration-file, `node_modules`, and
|
|
237
|
+
outside-root exclusions are unchanged. There are no incoming callers, counts, structured locations,
|
|
238
|
+
unresolved lists, full-file collection, or completeness markers.
|
|
239
|
+
|
|
240
|
+
The first actual read lazily starts one native TypeScript LSP process with its
|
|
241
|
+
public SDK API pipe; both are pinned to exactly `7.1.0-dev.20261009.1` and must
|
|
242
|
+
match. This prerelease contains the tuple-reference serialization fix from
|
|
243
|
+
[microsoft/TypeScript#64080](https://github.com/microsoft/TypeScript/issues/64080). `NativeLsp` owns lifecycle only and makes
|
|
244
|
+
no `textDocument/hover` calls. Each read uses the passed as-written text after
|
|
245
|
+
normalized-code/ignore active-plan validation, opens only that document at
|
|
246
|
+
version 1, uses `textDocument/documentSymbol` as its readiness barrier, and calls
|
|
247
|
+
the public, parameterless `API.getCurrentLanguageServerSnapshot()`.
|
|
248
|
+
Success closes the document, disposes that snapshot under a guarded phase, and
|
|
249
|
+
clears AST caches; native failure closes the session under the watchdog.
|
|
250
|
+
|
|
251
|
+
Keep dependencies/configs stable: only current-read text is pinned, not every
|
|
252
|
+
registered file or an immutable global audit snapshot. Warm compiler project
|
|
253
|
+
graphs can still be large and slow to load; neither total memory nor payload
|
|
254
|
+
size is constant, and contexts may still exceed model budgets.
|
|
255
|
+
|
|
256
|
+
SDK/write phases and LSP requests have 30-second deadlines and reject on compiler
|
|
257
|
+
exit. Queued readers can be interrupted; the active owner is uninterruptible
|
|
258
|
+
for resource safety. A stalled phase is bounded and cleanup adds a 2-second
|
|
259
|
+
`SIGKILL` watchdog. These are phase/cleanup bounds, not a whole-audit timeout.
|
|
260
|
+
|
|
261
|
+
If the user explicitly chose a cache refresh, run from the audited working
|
|
262
|
+
directory, using the repo's package launcher as above:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
rm -rf .adhere/cache
|
|
266
|
+
adhere lint --limit 0
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Show the fresh counts/cache plan; `--limit 0` skips budgeting. For selected
|
|
270
|
+
execution work, explain that its estimate omits native context and may understate
|
|
271
|
+
cost/splits, then **Ask** before paid execution. Deleting the cache removes judgments and linter tallies; it can cause
|
|
272
|
+
new paid requests. Use this explicitly if the user wants to compare fresh
|
|
273
|
+
compiler type/docs and declaration payloads with older cached answers. A change in `B.ts` does not
|
|
274
|
+
otherwise invalidate cached judgments for an unchanged importing `A.ts`.
|
|
275
|
+
|
|
158
276
|
Walk the user through the report:
|
|
159
277
|
|
|
160
278
|
1. How to read a finding: the rule id, the description, its confidence and
|
|
@@ -165,7 +283,8 @@ Walk the user through the report:
|
|
|
165
283
|
3. For each noisy rule, propose one change from the adhere skill's "Tuning a
|
|
166
284
|
rule": narrow the wording, scope it, raise its threshold, make it a
|
|
167
285
|
warning, or turn it off. **Ask**, apply what the user accepts, and run
|
|
168
|
-
lint again.
|
|
286
|
+
lint again. With unchanged own code and model, only the edited rules are
|
|
287
|
+
judged again; dependency/context changes alone are not tracked.
|
|
169
288
|
|
|
170
289
|
CI fails on any error finding, so lint must exit 0 before step 8. **Ask** how
|
|
171
290
|
to get there: fix the real findings now with the adhere-fix skill
|
|
@@ -212,9 +331,16 @@ jobs:
|
|
|
212
331
|
|
|
213
332
|
Pin the versions the repo already uses: bun's `bun-version`, Node's
|
|
214
333
|
`node-version` from `.nvmrc` or `engines`, and pnpm's from `packageManager`,
|
|
215
|
-
which `pnpm/action-setup` reads.
|
|
216
|
-
|
|
217
|
-
|
|
334
|
+
which `pnpm/action-setup` reads. Keep optional dependencies enabled so adhere's
|
|
335
|
+
platform package and its compiler dependency are installed. Existing project
|
|
336
|
+
configs and dependencies supply configured context when available; files with
|
|
337
|
+
no configured project or excluded from one use inferred context. Do not require
|
|
338
|
+
or generate a root `tsconfig.json` merely to make default lint run. In a repo
|
|
339
|
+
without a `package.json`, use the package launcher for the full default.
|
|
340
|
+
Only skip setup/install and fetch the raw executable if the user explicitly
|
|
341
|
+
chose and committed `reads: []` or `reads: ["workspacePackages"]` locally.
|
|
342
|
+
That is a reduced-context alternative, not an equivalent default installation.
|
|
343
|
+
Fetch the executable at the version the user ran:
|
|
218
344
|
|
|
219
345
|
```yaml
|
|
220
346
|
- run: |
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { AdhereConfig, AdhereConfigLive } from "#services/AdhereConfig.ts";
|
|
2
2
|
import { AuditCacheLive } from "#services/AuditCache.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Credentials,
|
|
5
|
+
CredentialsLive,
|
|
6
|
+
CredentialsUnavailable,
|
|
7
|
+
KEYS,
|
|
8
|
+
} from "#services/Credentials.ts";
|
|
4
9
|
import { JevLive } from "#services/Jev.http.ts";
|
|
10
|
+
import { SourceReadsLive } from "#services/SourceReads.ts";
|
|
5
11
|
import { SourceWalkerLive } from "#services/SourceWalker.ts";
|
|
6
12
|
import { Status } from "#services/Status.ts";
|
|
7
13
|
import {
|
|
@@ -26,6 +32,7 @@ import { type Install, initProject } from "#init.ts";
|
|
|
26
32
|
import { installRules, listRules, parseSource } from "#install.ts";
|
|
27
33
|
import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
|
|
28
34
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
35
|
+
import type { SavedKey } from "#providers/provider.ts";
|
|
29
36
|
import { globalRuleSet } from "#rules.ts";
|
|
30
37
|
import { formatStrays, straysAmong } from "#wording.ts";
|
|
31
38
|
import {
|
|
@@ -44,7 +51,6 @@ import {
|
|
|
44
51
|
Stream,
|
|
45
52
|
} from "effect";
|
|
46
53
|
import { Argument, Command, Flag, Prompt } from "effect/cli";
|
|
47
|
-
import { FetchHttpClient } from "effect/http";
|
|
48
54
|
// A Bun text import (https://bun.sh/docs/bundler/loaders#text): the file's
|
|
49
55
|
// contents become a string at bundle time, so the compiled binary carries the
|
|
50
56
|
// skill without an --asset flag. `skills/` sits beside `src/`, outside the `#`
|
|
@@ -110,12 +116,12 @@ const sufficiencyThreshold = Flag.Finite("sufficiency-threshold").pipe(
|
|
|
110
116
|
|
|
111
117
|
const limit = Flag.Int("limit").pipe(
|
|
112
118
|
Flag.filter(
|
|
113
|
-
(
|
|
114
|
-
(
|
|
119
|
+
(files) => files >= 0,
|
|
120
|
+
(files) => `--limit is a number of files, 0 or more, not ${files}.`,
|
|
115
121
|
),
|
|
116
122
|
Flag.optional,
|
|
117
123
|
Flag.withDescription(
|
|
118
|
-
"
|
|
124
|
+
"Process at most this many files needing requests, in path order, including every pending rule per selected file. Fully cached and located files do not count. A file can send multiple requests. --limit 0 shows the plan without resolving native reads or sending requests.",
|
|
119
125
|
),
|
|
120
126
|
);
|
|
121
127
|
|
|
@@ -221,8 +227,9 @@ const force = Flag.Boolean("force").pipe(
|
|
|
221
227
|
export const auditLayer = (flags: Flags, filter: ReadonlyArray<string> = []) =>
|
|
222
228
|
Layer.mergeAll(
|
|
223
229
|
SourceWalkerLive(filter),
|
|
230
|
+
SourceReadsLive(),
|
|
224
231
|
AuditCacheLive,
|
|
225
|
-
JevLive.pipe(Layer.provide(
|
|
232
|
+
JevLive.pipe(Layer.provide(CredentialsLive)),
|
|
226
233
|
).pipe(Layer.provideMerge(AdhereConfigLive(flags)), Layer.provideMerge(statusLayer));
|
|
227
234
|
|
|
228
235
|
/** `adhere lint`: the audit. */
|
|
@@ -235,10 +242,19 @@ export const lintCommand = Command.make(
|
|
|
235
242
|
const plan = yield* planAudit({ limit: Option.getOrUndefined(input.limit) });
|
|
236
243
|
const limits = { filter: input.filter, rpm: Option.getOrUndefined(input.rpm) };
|
|
237
244
|
yield* toStderr(`${describePlan(plan, limits).join("\n")}\n`);
|
|
245
|
+
if (plan.planOnly === true) {
|
|
246
|
+
// Planning still knows the live content keys, so a complete walk can prune without sending.
|
|
247
|
+
if (input.filter.length === 0) yield* pruneCache(plan);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
238
250
|
// A prompt draws on stdout, so it needs a terminal at both ends: none when
|
|
239
251
|
// the report is redirected, or in CI.
|
|
240
252
|
const interactive = (yield* stdio.stdinIsTerminal) && (yield* stdio.stdoutIsTerminal);
|
|
241
|
-
if (
|
|
253
|
+
if (
|
|
254
|
+
(plan.requests > 0 || (plan.cachedLocateRequests ?? 0) > 0) &&
|
|
255
|
+
interactive &&
|
|
256
|
+
!input.yes
|
|
257
|
+
) {
|
|
242
258
|
const send = yield* Prompt.run(
|
|
243
259
|
Prompt.Confirm({ message: sendQuestion(plan), initial: true }),
|
|
244
260
|
);
|
|
@@ -263,6 +279,7 @@ export const lintCommand = Command.make(
|
|
|
263
279
|
root: path.resolve(),
|
|
264
280
|
threshold: plan.threshold,
|
|
265
281
|
sufficiencyThreshold: plan.sufficiencyThreshold,
|
|
282
|
+
...(plan.provider === undefined ? {} : { provider: plan.provider }),
|
|
266
283
|
}).join("\n"),
|
|
267
284
|
);
|
|
268
285
|
// Only errors fail the run, unless --deny-warnings: a warning is for a nit or a noisy rule.
|
|
@@ -290,10 +307,10 @@ export const lintCommand = Command.make(
|
|
|
290
307
|
|
|
291
308
|
/** Jev, with the key `lint` uses, and the config it reads the model and threshold from. */
|
|
292
309
|
export const validateLayer = (flags: Flags) =>
|
|
293
|
-
Layer.merge(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
310
|
+
Layer.merge(JevLive.pipe(Layer.provide(CredentialsLive)), AuditCacheLive).pipe(
|
|
311
|
+
Layer.provideMerge(AdhereConfigLive(flags)),
|
|
312
|
+
Layer.provideMerge(statusLayer),
|
|
313
|
+
);
|
|
297
314
|
|
|
298
315
|
/**
|
|
299
316
|
* `adhere validate`: everything `lint` loads, then how each rule's wording
|
|
@@ -439,10 +456,10 @@ export const installCommand = Command.make(
|
|
|
439
456
|
* The key typed at a masked prompt, or piped in: `adhere login < key.txt`.
|
|
440
457
|
* Whitespace around it, like the newline a pipe ends with, is dropped.
|
|
441
458
|
*/
|
|
442
|
-
const readApiKey = Effect.fn("login.readApiKey")(function* () {
|
|
459
|
+
const readApiKey = Effect.fn("login.readApiKey")(function* (label: string) {
|
|
443
460
|
const stdio = yield* Stdio.Stdio;
|
|
444
461
|
const typed: string = (yield* stdio.stdinIsTerminal)
|
|
445
|
-
? Redacted.value(yield* Prompt.run(Prompt.Password({ message:
|
|
462
|
+
? Redacted.value(yield* Prompt.run(Prompt.Password({ message: label })))
|
|
446
463
|
: yield* stdio.stdin.pipe(
|
|
447
464
|
Stream.decodeText(),
|
|
448
465
|
Stream.mkString,
|
|
@@ -461,33 +478,49 @@ const readApiKey = Effect.fn("login.readApiKey")(function* () {
|
|
|
461
478
|
return Redacted.make(key);
|
|
462
479
|
});
|
|
463
480
|
|
|
464
|
-
/**
|
|
465
|
-
|
|
481
|
+
/** `--openai`: the OpenAI key, for the `openai` provider, in place of TypeSafe AI's. */
|
|
482
|
+
const openaiKey = Flag.Boolean("openai").pipe(
|
|
483
|
+
Flag.withDefault(false),
|
|
484
|
+
Flag.withDescription(
|
|
485
|
+
"The OpenAI API key, for a config whose provider is openai(), in place of the TypeSafe AI key Jev asks with.",
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
/** Which saved key a command means, and how it names it: TypeSafe AI's by default. */
|
|
490
|
+
const keyOf = (openai: boolean) => {
|
|
491
|
+
const name: SavedKey = openai ? "openai" : "typesafe";
|
|
492
|
+
return { name, ...KEYS[name] };
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/** `adhere login`: save an API key, so a run needs no TYPESAFE_API_KEY, or with `--openai`, no OPENAI_API_KEY. */
|
|
496
|
+
export const loginCommand = Command.make("login", { openai: openaiKey }, ({ openai }) =>
|
|
466
497
|
Effect.gen(function* () {
|
|
498
|
+
const { name, label, said } = keyOf(openai);
|
|
467
499
|
const credentials = yield* Credentials;
|
|
468
|
-
yield* credentials.save(yield* readApiKey());
|
|
469
|
-
yield* Console.log(`Saved the
|
|
500
|
+
yield* credentials.save(name, yield* readApiKey(label));
|
|
501
|
+
yield* Console.log(`Saved the ${said} to ${yield* credentials.file}.`);
|
|
470
502
|
}),
|
|
471
503
|
).pipe(
|
|
472
504
|
Command.withDescription(
|
|
473
|
-
"Save a TypeSafe AI API key in ~/.config/adhere ($XDG_CONFIG_HOME/adhere when set), readable only by you, so lint runs without TYPESAFE_API_KEY. Prompts for the key, or reads it from stdin when piped.",
|
|
505
|
+
"Save a TypeSafe AI API key in ~/.config/adhere ($XDG_CONFIG_HOME/adhere when set), readable only by you, so lint runs without TYPESAFE_API_KEY. With --openai, save an OpenAI API key beside it instead, for the openai provider. Prompts for the key, or reads it from stdin when piped.",
|
|
474
506
|
),
|
|
475
507
|
Command.provide(CredentialsLive),
|
|
476
508
|
);
|
|
477
509
|
|
|
478
|
-
/** `adhere logout`: delete
|
|
479
|
-
export const logoutCommand = Command.make("logout", {}, () =>
|
|
510
|
+
/** `adhere logout`: delete a saved API key. */
|
|
511
|
+
export const logoutCommand = Command.make("logout", { openai: openaiKey }, ({ openai }) =>
|
|
480
512
|
Effect.gen(function* () {
|
|
513
|
+
const { name, said } = keyOf(openai);
|
|
481
514
|
const credentials = yield* Credentials;
|
|
482
515
|
const file = yield* credentials.file;
|
|
483
|
-
const removed = yield* credentials.remove;
|
|
516
|
+
const removed = yield* credentials.remove(name);
|
|
484
517
|
yield* Console.log(
|
|
485
|
-
removed ? `Deleted the
|
|
518
|
+
removed ? `Deleted the ${said} saved in ${file}.` : `No ${said} is saved in ${file}.`,
|
|
486
519
|
);
|
|
487
520
|
}),
|
|
488
521
|
).pipe(
|
|
489
522
|
Command.withDescription(
|
|
490
|
-
"Delete the API key adhere login saved. TYPESAFE_API_KEY, when set, still applies.",
|
|
523
|
+
"Delete the API key adhere login saved, or with --openai, the OpenAI one. TYPESAFE_API_KEY or OPENAI_API_KEY, when set, still applies.",
|
|
491
524
|
),
|
|
492
525
|
Command.provide(CredentialsLive),
|
|
493
526
|
);
|
package/src/config.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
2
|
+
import { type Provider, SAVED_KEYS } from "#providers/provider.ts";
|
|
2
3
|
import type { RuleSet } from "#rules.ts";
|
|
3
|
-
import { Effect, Schema, SchemaTransformation } from "effect";
|
|
4
|
+
import { Effect, Predicate, Schema, SchemaTransformation } from "effect";
|
|
4
5
|
|
|
5
6
|
export type RuleId = string;
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
+
/** Shared context prepared for a file's requests; it does not affect cache validity. */
|
|
9
|
+
export type ReadState = Readonly<Record<string, unknown>>;
|
|
10
|
+
|
|
11
|
+
/** The file's numbered sections and the context enabled by its config. */
|
|
8
12
|
export interface JevState {
|
|
9
13
|
readonly code: Readonly<Record<string, string>>;
|
|
14
|
+
readonly [key: string]: unknown;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
/** The file a request is about, as a rule's hook gets it: its absolute path, and its text as written. */
|
|
@@ -15,6 +20,21 @@ export interface JudgedFile {
|
|
|
15
20
|
readonly contents: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A config's provider, as `Provider` in `providers/provider.ts` describes it:
|
|
25
|
+
* a model, and how to ask it. Checked rather than copied, so a class keeps
|
|
26
|
+
* what its `ask` reads off `this`.
|
|
27
|
+
*/
|
|
28
|
+
const ProviderSchema = Schema.declare(
|
|
29
|
+
(value: unknown): value is Provider =>
|
|
30
|
+
Predicate.hasProperty(value, "model") &&
|
|
31
|
+
Predicate.isString(value.model) &&
|
|
32
|
+
Predicate.hasProperty(value, "ask") &&
|
|
33
|
+
Predicate.isFunction(value.ask) &&
|
|
34
|
+
(!Predicate.hasProperty(value, "savedKey") || SAVED_KEYS.some((key) => key === value.savedKey)),
|
|
35
|
+
{ expected: "a provider: a model, and a function to ask it" },
|
|
36
|
+
);
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* Bun's API, typed when the project has Bun's types. Read off `globalThis`,
|
|
20
40
|
* so this package's source type-checks in a project without them.
|
|
@@ -33,13 +53,15 @@ export type AppendState = (
|
|
|
33
53
|
) => Readonly<Record<string, unknown>> | Promise<Readonly<Record<string, unknown>>>;
|
|
34
54
|
|
|
35
55
|
/**
|
|
36
|
-
* What
|
|
37
|
-
* the state carries it under, which
|
|
38
|
-
* backticks. `workspacePackages`
|
|
39
|
-
*
|
|
40
|
-
*
|
|
56
|
+
* What the config can have Jev read beside each file's code, shared by all
|
|
57
|
+
* its rules. Each name is the key the state carries it under, which a rule's
|
|
58
|
+
* description can name in backticks. `workspacePackages` maps only workspace
|
|
59
|
+
* packages imported by this file to their directories. `symbols` maps names
|
|
60
|
+
* to native compiler types, with documentation and JSDoc tags shared across variants.
|
|
61
|
+
* `references` maps used names to complete AST declaration scopes by
|
|
62
|
+
* repo-relative path, rather than fixed line windows or full related files.
|
|
41
63
|
*/
|
|
42
|
-
export const Reads = Schema.Array(Schema.Literals(["workspacePackages"]));
|
|
64
|
+
export const Reads = Schema.Array(Schema.Literals(["workspacePackages", "symbols", "references"]));
|
|
43
65
|
|
|
44
66
|
/**
|
|
45
67
|
* A rule's examples, named in RFC 2119's words, which Jev reads as written. A
|
|
@@ -83,12 +105,6 @@ const RuleFields = Schema.Struct({
|
|
|
83
105
|
* that breaks the rule is as it describes, and a yes to any drops the finding.
|
|
84
106
|
*/
|
|
85
107
|
excludeIf: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
86
|
-
/**
|
|
87
|
-
* What Jev reads for the rule beside the code, by name (see `Reads`). The
|
|
88
|
-
* rule's questions go in requests of their own, so only they read it, and
|
|
89
|
-
* a change to what is read judges the rule again.
|
|
90
|
-
*/
|
|
91
|
-
reads: Schema.optionalKey(Reads),
|
|
92
108
|
/**
|
|
93
109
|
* A hook on the state Jev reads for the rule, in a rule written in
|
|
94
110
|
* TypeScript (see `AppendState`). The rule's questions go in requests of
|
|
@@ -106,6 +122,12 @@ export const Rule = Schema.Struct({
|
|
|
106
122
|
...RuleFields.fields,
|
|
107
123
|
reference: Schema.optionalKey(Schema.String),
|
|
108
124
|
avoid: Schema.optionalKey(Schema.String),
|
|
125
|
+
// Guard the removed key instead of silently dropping it as an excess property.
|
|
126
|
+
reads: Schema.optionalKey(
|
|
127
|
+
Schema.declare((_value: unknown): _value is never => false, {
|
|
128
|
+
expected: "no rule-level reads; move reads to the top level of defineConfig({...})",
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
109
131
|
})
|
|
110
132
|
.pipe(
|
|
111
133
|
Schema.decodeTo(
|
|
@@ -179,6 +201,8 @@ export const AdhereConfig = Schema.Struct({
|
|
|
179
201
|
* The model id sent to typesafe. Defaults to "jev-latest"
|
|
180
202
|
*/
|
|
181
203
|
model: Schema.optionalKey(Schema.String),
|
|
204
|
+
/** Where the questions go in place of TypeSafe's Jev, with its own model (see `Provider`). */
|
|
205
|
+
provider: Schema.optionalKey(ProviderSchema),
|
|
182
206
|
/**
|
|
183
207
|
* The minimum confidence threshold for a positive match. Defaults to 0.8
|
|
184
208
|
*/
|
|
@@ -196,6 +220,8 @@ export const AdhereConfig = Schema.Struct({
|
|
|
196
220
|
presets: Schema.Array(Schema.Literals(presetNames)).pipe(
|
|
197
221
|
Schema.withDecodingDefaultKey(Effect.succeed([])),
|
|
198
222
|
),
|
|
223
|
+
/** Shared context for every audited file and all its rules. Unset: all built-in reads; []: code only. */
|
|
224
|
+
reads: Schema.optionalKey(Reads),
|
|
199
225
|
/**
|
|
200
226
|
* A record of rules, or a directory path relative to the config file,
|
|
201
227
|
* which holds its rules as `.adhere/rules/` does. Unset: the rules in
|
|
@@ -303,10 +329,13 @@ export type Loaded<T extends { readonly rules?: unknown }> = Omit<T, "rules"> &
|
|
|
303
329
|
export const DEFAULT_MODEL = "jev-latest";
|
|
304
330
|
export const DEFAULT_THRESHOLD = 0.8;
|
|
305
331
|
export const DEFAULT_SUFFICIENCY_THRESHOLD = 0.6;
|
|
332
|
+
export const DEFAULT_READS: typeof Reads.Type = ["workspacePackages", "symbols", "references"];
|
|
306
333
|
|
|
307
334
|
/** A config with its presets folded in and every default applied. */
|
|
308
335
|
export interface ResolvedConfig {
|
|
309
336
|
readonly model: string;
|
|
337
|
+
/** Where the questions go in place of Jev; `model` is its model. */
|
|
338
|
+
readonly provider?: Provider;
|
|
310
339
|
readonly threshold: number;
|
|
311
340
|
/** Below this, a finding's sufficiency adds a warning to it. */
|
|
312
341
|
readonly sufficiencyThreshold: number;
|
|
@@ -318,9 +347,11 @@ export interface ResolvedConfig {
|
|
|
318
347
|
readonly exclude?: ReadonlyArray<string>;
|
|
319
348
|
/** Whether Jev reads every comment; unset, only the `@adhere` notes. */
|
|
320
349
|
readonly includeComments?: boolean;
|
|
350
|
+
/** Built-in context shared by every rule, with the default applied. */
|
|
351
|
+
readonly reads: typeof Reads.Type;
|
|
321
352
|
/**
|
|
322
353
|
* The workspace's packages, each name with its directory from the working
|
|
323
|
-
* directory, read from its manifests on every run
|
|
354
|
+
* directory, read from its manifests on every run when the config's reads
|
|
324
355
|
* names them.
|
|
325
356
|
*/
|
|
326
357
|
readonly workspacePackages?: Readonly<Record<string, string>>;
|
|
@@ -370,7 +401,8 @@ export const resolveConfig = (
|
|
|
370
401
|
const last = <K extends "model" | "threshold">(key: K) =>
|
|
371
402
|
applied.map((preset) => preset[key]).findLast((value) => value !== undefined);
|
|
372
403
|
return {
|
|
373
|
-
model: config.model ?? last("model") ?? DEFAULT_MODEL,
|
|
404
|
+
model: config.provider?.model ?? config.model ?? last("model") ?? DEFAULT_MODEL,
|
|
405
|
+
...(config.provider === undefined ? {} : { provider: config.provider }),
|
|
374
406
|
threshold: flags.threshold ?? config.threshold ?? last("threshold") ?? DEFAULT_THRESHOLD,
|
|
375
407
|
sufficiencyThreshold:
|
|
376
408
|
flags.sufficiencyThreshold ?? config.sufficiencyThreshold ?? DEFAULT_SUFFICIENCY_THRESHOLD,
|
|
@@ -378,6 +410,7 @@ export const resolveConfig = (
|
|
|
378
410
|
...(flags.rpm === undefined ? {} : { rpm: flags.rpm }),
|
|
379
411
|
...(config.exclude === undefined ? {} : { exclude: config.exclude }),
|
|
380
412
|
...(config.includeComments === undefined ? {} : { includeComments: config.includeComments }),
|
|
413
|
+
reads: config.reads ?? DEFAULT_READS,
|
|
381
414
|
};
|
|
382
415
|
};
|
|
383
416
|
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,20 @@ export {
|
|
|
12
12
|
type RuleSource,
|
|
13
13
|
} from "#config.ts";
|
|
14
14
|
export { findContradictions, formatContradictions, type Contradiction } from "#contradictions.ts";
|
|
15
|
+
export { cloudflare, type CloudflareOptions } from "#providers/cloudflare.ts";
|
|
16
|
+
export { jev, type JevOptions } from "#providers/jev.ts";
|
|
17
|
+
export { openai, type OpenAIOptions } from "#providers/openai.ts";
|
|
18
|
+
export {
|
|
19
|
+
type Answered,
|
|
20
|
+
type AskOptions,
|
|
21
|
+
ContextOverflow,
|
|
22
|
+
type Provider,
|
|
23
|
+
type Question,
|
|
24
|
+
RequestBlocked,
|
|
25
|
+
RequestFailed,
|
|
26
|
+
type SavedKey,
|
|
27
|
+
type State,
|
|
28
|
+
} from "#providers/provider.ts";
|
|
15
29
|
export { initProject, type InitOptions, type InitResult, type Install } from "#init.ts";
|
|
16
30
|
export { parseRuleMarkdown } from "#markdown.ts";
|
|
17
31
|
export { type PresetName, presetNames } from "#presets.ts";
|
package/src/markdown.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigUnavailable, type Example,
|
|
1
|
+
import { ConfigUnavailable, type Example, type Rule } from "#config.ts";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -34,8 +34,7 @@ import { Effect, Schema } from "effect";
|
|
|
34
34
|
* `warning` reports the rule's findings as warnings, which do not fail a run.
|
|
35
35
|
* `appliesTo` and `excludeIf`, the rule's scope, are JSON arrays of strings on
|
|
36
36
|
* one line, as in `excludeIf: ["a type that mirrors a third-party format"]`.
|
|
37
|
-
|
|
38
|
-
* as in `reads: ["workspacePackages"]`.
|
|
37
|
+
|
|
39
38
|
*/
|
|
40
39
|
const Matchers = Schema.fromJsonString(Schema.Array(Schema.String));
|
|
41
40
|
|
|
@@ -46,7 +45,6 @@ const FrontMatter = Schema.Struct({
|
|
|
46
45
|
level: Schema.optionalKey(Schema.Literals(["error", "warning"])),
|
|
47
46
|
appliesTo: Schema.optionalKey(Matchers),
|
|
48
47
|
excludeIf: Schema.optionalKey(Matchers),
|
|
49
|
-
reads: Schema.optionalKey(Schema.fromJsonString(Reads)),
|
|
50
48
|
});
|
|
51
49
|
|
|
52
50
|
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
@@ -209,7 +207,13 @@ export const parseRuleMarkdown = (
|
|
|
209
207
|
message: `${file}: a rule starts with front matter between --- lines`,
|
|
210
208
|
});
|
|
211
209
|
}
|
|
212
|
-
const
|
|
210
|
+
const fields = parseFrontMatter(match[1]);
|
|
211
|
+
if (Object.hasOwn(fields, "reads")) {
|
|
212
|
+
return yield* ConfigUnavailable.make({
|
|
213
|
+
message: `${file}: move reads from the rule's front matter to the top level of defineConfig({...}) in your config file.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const front = yield* Schema.decodeUnknownEffect(FrontMatter)(fields).pipe(
|
|
213
217
|
Effect.mapError((problem) =>
|
|
214
218
|
ConfigUnavailable.make({ message: `${file}: ${problem.message}` }),
|
|
215
219
|
),
|