@drkmttr/adhere 0.18.0 → 0.21.2
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 +1099 -1066
- package/package.json +15 -15
- package/presets/README.md +132 -0
- package/skills/adhere/SKILL.md +51 -33
- package/skills/adhere-setup/SKILL.md +77 -53
- package/src/cli.ts +133 -14
- package/src/config.ts +157 -138
- package/src/create/agents.ts +149 -0
- package/src/create/ask.ts +147 -0
- package/src/create/look.ts +140 -0
- package/src/create/wizard.ts +432 -0
- package/src/index.ts +2 -0
- package/src/init.ts +24 -4
- package/src/install.ts +30 -24
- package/src/instructions.ts +76 -0
- package/src/markdown.ts +3 -9
- package/src/rules.ts +42 -45
- package/src/services/AdhereConfig.ts +31 -18
- package/src/services/Credentials.ts +14 -8
- package/src/services/Jev.reads.test.ts +26 -2
- package/src/services/Jev.ts +36 -14
- package/src/wording.ts +1 -1
- package/src/workflows/audit.ts +84 -16
- package/src/workflows/format.ts +97 -0
- package/src/workspaces.test.ts +37 -1
- package/src/workspaces.ts +121 -12
package/src/cli.ts
CHANGED
|
@@ -24,12 +24,18 @@ import {
|
|
|
24
24
|
type Progress,
|
|
25
25
|
progressAfter,
|
|
26
26
|
progressLine,
|
|
27
|
+
renderJson,
|
|
28
|
+
renderMarkdown,
|
|
29
|
+
renderRuleReports,
|
|
27
30
|
sendQuestion,
|
|
28
31
|
} from "#workflows/format.ts";
|
|
29
32
|
import type { Flags } from "#config.ts";
|
|
30
33
|
import { findContradictions, formatContradictions } from "#contradictions.ts";
|
|
34
|
+
import { createRule } from "#create/wizard.ts";
|
|
35
|
+
import { AGENTS } from "#create/agents.ts";
|
|
31
36
|
import { type Install, initProject } from "#init.ts";
|
|
32
37
|
import { installRules, listRules, parseSource } from "#install.ts";
|
|
38
|
+
import { renderInstructions } from "#instructions.ts";
|
|
33
39
|
import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
|
|
34
40
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
35
41
|
import type { SavedKey } from "#providers/provider.ts";
|
|
@@ -39,6 +45,7 @@ import {
|
|
|
39
45
|
Console,
|
|
40
46
|
Effect,
|
|
41
47
|
Exit,
|
|
48
|
+
FileSystem,
|
|
42
49
|
Layer,
|
|
43
50
|
LogLevel,
|
|
44
51
|
Option,
|
|
@@ -49,8 +56,9 @@ import {
|
|
|
49
56
|
Schema,
|
|
50
57
|
Stdio,
|
|
51
58
|
Stream,
|
|
59
|
+
Terminal,
|
|
52
60
|
} from "effect";
|
|
53
|
-
import { Argument, Command, Flag, Prompt } from "effect/cli";
|
|
61
|
+
import { Argument, CliError, Command, Flag, Prompt } from "effect/cli";
|
|
54
62
|
// A Bun text import (https://bun.sh/docs/bundler/loaders#text): the file's
|
|
55
63
|
// contents become a string at bundle time, so the compiled binary carries the
|
|
56
64
|
// skill without an --asset flag. `skills/` sits beside `src/`, outside the `#`
|
|
@@ -59,6 +67,10 @@ import skill from "../skills/adhere/SKILL.md" with { type: "text" };
|
|
|
59
67
|
import fixSkill from "../skills/adhere-fix/SKILL.md" with { type: "text" };
|
|
60
68
|
import setupSkill from "../skills/adhere-setup/SKILL.md" with { type: "text" };
|
|
61
69
|
|
|
70
|
+
// @adhere This file owns CLI flags, progress output, and command wiring. Audit execution,
|
|
71
|
+
// scaffolding, rule installation, credential storage, and rule checks are implemented in
|
|
72
|
+
// their own modules imported above; the commands here connect them to the CLI.
|
|
73
|
+
|
|
62
74
|
class FindingsReported extends Schema.TaggedError<FindingsReported>()("FindingsReported", {
|
|
63
75
|
count: Schema.Finite,
|
|
64
76
|
}) {
|
|
@@ -114,6 +126,13 @@ const sufficiencyThreshold = Flag.Finite("sufficiency-threshold").pipe(
|
|
|
114
126
|
),
|
|
115
127
|
);
|
|
116
128
|
|
|
129
|
+
const typeAware = Flag.Boolean("type-aware").pipe(
|
|
130
|
+
Flag.optional,
|
|
131
|
+
Flag.withDescription(
|
|
132
|
+
"Include compiler-resolved symbols and declaration excerpts. Off by default.",
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
|
|
117
136
|
const limit = Flag.Int("limit").pipe(
|
|
118
137
|
Flag.filter(
|
|
119
138
|
(files) => files >= 0,
|
|
@@ -235,9 +254,43 @@ export const auditLayer = (flags: Flags, filter: ReadonlyArray<string> = []) =>
|
|
|
235
254
|
/** `adhere lint`: the audit. */
|
|
236
255
|
export const lintCommand = Command.make(
|
|
237
256
|
"lint",
|
|
238
|
-
{
|
|
257
|
+
{
|
|
258
|
+
preset,
|
|
259
|
+
threshold,
|
|
260
|
+
sufficiencyThreshold,
|
|
261
|
+
typeAware,
|
|
262
|
+
yes,
|
|
263
|
+
limit,
|
|
264
|
+
rpm,
|
|
265
|
+
filter,
|
|
266
|
+
denyWarnings,
|
|
267
|
+
json: Flag.Boolean("json").pipe(
|
|
268
|
+
Flag.withDefault(false),
|
|
269
|
+
Flag.withDescription(
|
|
270
|
+
"Write one JSON file per violated rule to .adhere/reports/. Use --stdout for one combined report instead.",
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
md: Flag.Boolean("md").pipe(
|
|
274
|
+
Flag.withDefault(false),
|
|
275
|
+
Flag.withDescription(
|
|
276
|
+
"Write one Markdown file per violated rule to .adhere/reports/. Use --stdout for one combined report instead.",
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
stdout: Flag.Boolean("stdout").pipe(
|
|
280
|
+
Flag.withDefault(false),
|
|
281
|
+
Flag.withDescription(
|
|
282
|
+
"Print all findings together instead of writing files. Select either --json or --md for a formatted report.",
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
},
|
|
239
286
|
(input) =>
|
|
240
287
|
Effect.gen(function* () {
|
|
288
|
+
if (input.stdout && input.json && input.md) {
|
|
289
|
+
return yield* CliError.UserError.make({
|
|
290
|
+
cause:
|
|
291
|
+
"Choose either --json or --md with --stdout; both formats can be written to files together.",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
241
294
|
const stdio = yield* Stdio.Stdio;
|
|
242
295
|
const plan = yield* planAudit({ limit: Option.getOrUndefined(input.limit) });
|
|
243
296
|
const limits = { filter: input.filter, rpm: Option.getOrUndefined(input.rpm) };
|
|
@@ -247,8 +300,7 @@ export const lintCommand = Command.make(
|
|
|
247
300
|
if (input.filter.length === 0) yield* pruneCache(plan);
|
|
248
301
|
return;
|
|
249
302
|
}
|
|
250
|
-
//
|
|
251
|
-
// the report is redirected, or in CI.
|
|
303
|
+
// Keep prompting limited to interactive runs, including when JSON is requested.
|
|
252
304
|
const interactive = (yield* stdio.stdinIsTerminal) && (yield* stdio.stdoutIsTerminal);
|
|
253
305
|
if (
|
|
254
306
|
(plan.requests > 0 || (plan.cachedLocateRequests ?? 0) > 0) &&
|
|
@@ -257,6 +309,11 @@ export const lintCommand = Command.make(
|
|
|
257
309
|
) {
|
|
258
310
|
const send = yield* Prompt.run(
|
|
259
311
|
Prompt.Confirm({ message: sendQuestion(plan), initial: true }),
|
|
312
|
+
).pipe(
|
|
313
|
+
Effect.updateService(Terminal.Terminal, (terminal) => ({
|
|
314
|
+
...terminal,
|
|
315
|
+
display: toStderr,
|
|
316
|
+
})),
|
|
260
317
|
);
|
|
261
318
|
if (!send) {
|
|
262
319
|
yield* toStderr("Nothing sent.\n");
|
|
@@ -272,16 +329,33 @@ export const lintCommand = Command.make(
|
|
|
272
329
|
if (input.filter.length === 0) yield* pruneCache(plan);
|
|
273
330
|
const color = yield* stdio.stdoutIsTerminal;
|
|
274
331
|
const path = yield* Path.Path;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
332
|
+
const options = {
|
|
333
|
+
color,
|
|
334
|
+
root: path.resolve(),
|
|
335
|
+
threshold: plan.threshold,
|
|
336
|
+
sufficiencyThreshold: plan.sufficiencyThreshold,
|
|
337
|
+
...(plan.provider === undefined ? {} : { provider: plan.provider }),
|
|
338
|
+
};
|
|
339
|
+
if (!input.stdout && (input.json || input.md)) {
|
|
340
|
+
const fs = yield* FileSystem.FileSystem;
|
|
341
|
+
const directory = path.join(options.root, ".adhere", "reports");
|
|
342
|
+
const reports = renderRuleReports(result, input, options);
|
|
343
|
+
yield* fs.remove(directory, { recursive: true, force: true });
|
|
344
|
+
yield* fs.makeDirectory(directory, { recursive: true });
|
|
345
|
+
for (const [name, report] of reports) {
|
|
346
|
+
yield* fs.writeFileString(path.join(directory, name), report);
|
|
347
|
+
}
|
|
348
|
+
yield* toStderr(`Wrote ${reports.size} report files to .adhere/reports/.\n`);
|
|
349
|
+
} else {
|
|
350
|
+
// One write: separate Console.log calls have interleaved out of order here.
|
|
351
|
+
yield* Console.log(
|
|
352
|
+
input.json
|
|
353
|
+
? renderJson(result, options)
|
|
354
|
+
: input.md
|
|
355
|
+
? renderMarkdown(result, options)
|
|
356
|
+
: render(result, options).join("\n"),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
285
359
|
// Only errors fail the run, unless --deny-warnings: a warning is for a nit or a noisy rule.
|
|
286
360
|
const failed = failing(result.findings, input.denyWarnings);
|
|
287
361
|
if (failed.length > 0) {
|
|
@@ -298,6 +372,7 @@ export const lintCommand = Command.make(
|
|
|
298
372
|
presets: input.preset,
|
|
299
373
|
threshold: Option.getOrUndefined(input.threshold),
|
|
300
374
|
sufficiencyThreshold: Option.getOrUndefined(input.sufficiencyThreshold),
|
|
375
|
+
typeAware: Option.getOrUndefined(input.typeAware),
|
|
301
376
|
rpm: Option.getOrUndefined(input.rpm),
|
|
302
377
|
},
|
|
303
378
|
input.filter,
|
|
@@ -544,6 +619,48 @@ export const skillCommand = Command.make("skill", { name: skillName }, ({ name }
|
|
|
544
619
|
),
|
|
545
620
|
);
|
|
546
621
|
|
|
622
|
+
const agent = Flag.Literals("agent", AGENTS).pipe(
|
|
623
|
+
Flag.optional,
|
|
624
|
+
Flag.withDescription(
|
|
625
|
+
"The coding agent to ask this time: claude, codex, or omp, in place of the one setup saved.",
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
/** `adhere rules create [idea]`: a wizard that writes one rule, with a coding agent's help. */
|
|
630
|
+
const rulesCreateCommand = Command.make(
|
|
631
|
+
"create",
|
|
632
|
+
{
|
|
633
|
+
idea: Argument.String("idea").pipe(
|
|
634
|
+
Argument.optional,
|
|
635
|
+
Argument.withDescription("What the rule should enforce. Asked for when left out."),
|
|
636
|
+
),
|
|
637
|
+
agent,
|
|
638
|
+
setup: Flag.Boolean("setup").pipe(
|
|
639
|
+
Flag.withDefault(false),
|
|
640
|
+
Flag.withDescription(
|
|
641
|
+
"Choose again which agent to ask and which GitHub repo, as org/repo, holds the organization's shared rules. The first run asks without it.",
|
|
642
|
+
),
|
|
643
|
+
),
|
|
644
|
+
},
|
|
645
|
+
(input) => createRule(input.idea, input.agent, input.setup).pipe(Effect.provide(statusLayer)),
|
|
646
|
+
).pipe(
|
|
647
|
+
Command.withDescription(
|
|
648
|
+
"Write a rule with a coding agent's help: it suggests whether the rule belongs to this project or the organization's shared rules repo, names it, chooses a RULE.md or, when Jev needs state the file cannot show, a RULE.ts, and says which config context (typeAware, reads) it needs. It finds code in the repo for you to judge, with the lines around it, and drafts the rule from your answers; the draft is checked as adhere validate checks wording. A project rule is written to .adhere/rules/; an organization rule is pushed to the shared repo on a branch for a pull request.",
|
|
649
|
+
),
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
export const rulesCommand = Command.make("rules").pipe(
|
|
653
|
+
Command.withDescription("Work on this repo's rules."),
|
|
654
|
+
Command.withSubcommands([rulesCreateCommand]),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
export const postinstallCommand = Command.make("postinstall", {}, () =>
|
|
658
|
+
Effect.gen(function* () {
|
|
659
|
+
const path = yield* Path.Path;
|
|
660
|
+
yield* Console.log(yield* renderInstructions(path.resolve()));
|
|
661
|
+
}),
|
|
662
|
+
).pipe(Command.withDescription("Render instruction files into a managed block in AGENTS.md."));
|
|
663
|
+
|
|
547
664
|
/** The whole CLI. Bare `adhere` prints its help. */
|
|
548
665
|
export const cli = Command.make("adhere").pipe(
|
|
549
666
|
Command.withDescription("Lint a repository against the rules in .adhere/rules/, judged by Jev."),
|
|
@@ -568,10 +685,12 @@ export const cli = Command.make("adhere").pipe(
|
|
|
568
685
|
lintCommand,
|
|
569
686
|
validateCommand,
|
|
570
687
|
initCommand,
|
|
688
|
+
postinstallCommand,
|
|
571
689
|
listCommand,
|
|
572
690
|
installCommand,
|
|
573
691
|
loginCommand,
|
|
574
692
|
logoutCommand,
|
|
575
693
|
skillCommand,
|
|
694
|
+
rulesCommand,
|
|
576
695
|
]),
|
|
577
696
|
);
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
2
2
|
import { type Provider, SAVED_KEYS } from "#providers/provider.ts";
|
|
3
3
|
import type { RuleSet } from "#rules.ts";
|
|
4
|
-
import { Effect, Predicate, Schema
|
|
4
|
+
import { Effect, Predicate, Schema } from "effect";
|
|
5
5
|
|
|
6
6
|
export type RuleId = string;
|
|
7
7
|
|
|
@@ -32,7 +32,10 @@ const ProviderSchema = Schema.declare(
|
|
|
32
32
|
Predicate.hasProperty(value, "ask") &&
|
|
33
33
|
Predicate.isFunction(value.ask) &&
|
|
34
34
|
(!Predicate.hasProperty(value, "savedKey") || SAVED_KEYS.some((key) => key === value.savedKey)),
|
|
35
|
-
{
|
|
35
|
+
{
|
|
36
|
+
expected: "a provider: a model, and a function to ask it",
|
|
37
|
+
toCode: () => ({ runtime: "Provider", Type: "Provider" }),
|
|
38
|
+
},
|
|
36
39
|
);
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -56,12 +59,16 @@ export type AppendState = (
|
|
|
56
59
|
* What the config can have Jev read beside each file's code, shared by all
|
|
57
60
|
* its rules. Each name is the key the state carries it under, which a rule's
|
|
58
61
|
* description can name in backticks. `workspacePackages` maps only workspace
|
|
59
|
-
* packages imported by this file to their directories. `
|
|
62
|
+
* packages imported by this file to their directories. `dependents` lists what
|
|
63
|
+
* depends on this file: workspace packages whose package.json names its package,
|
|
64
|
+
* and files that import it by a relative path. `symbols` maps names
|
|
60
65
|
* to native compiler types, with documentation and JSDoc tags shared across variants.
|
|
61
66
|
* `references` maps used names to complete AST declaration scopes by
|
|
62
67
|
* repo-relative path, rather than fixed line windows or full related files.
|
|
63
68
|
*/
|
|
64
|
-
export const Reads = Schema.Array(
|
|
69
|
+
export const Reads = Schema.Array(
|
|
70
|
+
Schema.Literals(["workspacePackages", "dependents", "symbols", "references"]),
|
|
71
|
+
);
|
|
65
72
|
|
|
66
73
|
/**
|
|
67
74
|
* A rule's examples, named in RFC 2119's words, which Jev reads as written. A
|
|
@@ -69,91 +76,84 @@ export const Reads = Schema.Array(Schema.Literals(["workspacePackages", "symbols
|
|
|
69
76
|
* be. A guideline, which asks for less, shows code that `should` be written
|
|
70
77
|
* and code that `shouldNot` be.
|
|
71
78
|
*/
|
|
72
|
-
const
|
|
73
|
-
description: Schema.String
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*/
|
|
79
|
+
export const Rule = Schema.Struct({
|
|
80
|
+
description: Schema.String.annotate({
|
|
81
|
+
description: "What the rule says, in one sentence. The report shows it with each finding.",
|
|
82
|
+
}),
|
|
83
|
+
details: Schema.optionalKey(Schema.String).annotate({
|
|
84
|
+
description:
|
|
85
|
+
"What the description leaves out, such as why the rule holds or where it does not: in a `RULE.md`, the prose around its code. Jev reads it after the description, as part of the rule; the report shows the description alone.",
|
|
86
|
+
}),
|
|
87
|
+
must: Schema.optionalKey(Schema.String).annotate({
|
|
88
|
+
description:
|
|
89
|
+
"Code the rule requires. A rule has `must` and `never`, or, as a guideline, `should` and `shouldNot`.",
|
|
90
|
+
}),
|
|
91
|
+
never: Schema.optionalKey(Schema.String).annotate({
|
|
92
|
+
description: "Code the rule forbids.",
|
|
93
|
+
}),
|
|
94
|
+
should: Schema.optionalKey(Schema.String).annotate({
|
|
95
|
+
description: "A guideline's code to write.",
|
|
96
|
+
}),
|
|
97
|
+
shouldNot: Schema.optionalKey(Schema.String).annotate({
|
|
98
|
+
description: "A guideline's code not to write.",
|
|
99
|
+
}),
|
|
100
|
+
threshold: Schema.optionalKey(Schema.Finite).annotate({
|
|
101
|
+
description: "The rule's own confidence threshold, 0 to 1, in place of the config's.",
|
|
102
|
+
}),
|
|
103
|
+
tests: Schema.optionalKey(Schema.Literals(["only", "include"])).annotate({
|
|
104
|
+
description:
|
|
105
|
+
"Test files, which a rule skips by default: `only` for a rule about tests, which judges nothing else, and `include` for one that holds in tests too.",
|
|
106
|
+
}),
|
|
107
|
+
level: Schema.optionalKey(Schema.Literals(["error", "warning"])).annotate({
|
|
108
|
+
description:
|
|
109
|
+
"How a finding under the rule is reported: an `error`, the default, which fails the run, or a `warning`, for a nit or a rule that errs toward false findings, which does not.",
|
|
110
|
+
}),
|
|
111
|
+
appliesTo: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
|
|
112
|
+
description:
|
|
113
|
+
"Where the rule applies, each a description of code that breaks it: Jev is asked of each whether any code that breaks the rule is as it describes, and a finding stands only when it says yes to every one.",
|
|
114
|
+
}),
|
|
115
|
+
excludeIf: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
|
|
116
|
+
description:
|
|
117
|
+
"Where the rule does not apply: Jev is asked of each whether all the code that breaks the rule is as it describes, and a yes to any drops the finding.",
|
|
118
|
+
}),
|
|
113
119
|
appendState: Schema.optionalKey(
|
|
114
120
|
Schema.declare((value: unknown): value is AppendState => typeof value === "function", {
|
|
115
121
|
expected: "a function",
|
|
122
|
+
toCode: () => ({ runtime: "AppendState", Type: "AppendState" }),
|
|
116
123
|
}),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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({...})",
|
|
124
|
+
).annotate({
|
|
125
|
+
description:
|
|
126
|
+
"A hook on the state Jev reads for the rule, in a rule written in TypeScript (see `AppendState`). The rule's questions go in requests of their own, so only they read what it adds.",
|
|
127
|
+
}),
|
|
128
|
+
options: Schema.optionalKey(
|
|
129
|
+
Schema.declare((value: unknown): value is Schema.Decoder<unknown> => Schema.isSchema(value), {
|
|
130
|
+
expected: "an Effect Schema",
|
|
131
|
+
toCode: () => ({ runtime: "Schema", Type: "Schema.Decoder<unknown>" }),
|
|
129
132
|
}),
|
|
130
|
-
)
|
|
133
|
+
).annotate({
|
|
134
|
+
description:
|
|
135
|
+
"The settings a rule written in TypeScript takes, as an Effect Schema, set under the rule's id in a config's `overrides`, as `options`. A key the schema does not make optional must be set. Jev reads the decoded settings as `options`, in requests of the rule's own.",
|
|
136
|
+
}),
|
|
131
137
|
})
|
|
132
|
-
.
|
|
133
|
-
Schema.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
138
|
+
.check(
|
|
139
|
+
Schema.makeFilter(
|
|
140
|
+
(rule) => {
|
|
141
|
+
const requirement = rule.must !== undefined || rule.never !== undefined;
|
|
142
|
+
const guideline = rule.should !== undefined || rule.shouldNot !== undefined;
|
|
143
|
+
return requirement && guideline
|
|
144
|
+
? "a rule is a requirement (must, never) or a guideline (should, shouldNot), not both"
|
|
145
|
+
: requirement || guideline
|
|
146
|
+
? undefined
|
|
147
|
+
: "a rule needs an example: must, never, should, or shouldNot";
|
|
148
|
+
},
|
|
149
|
+
{ toCode: () => ({ runtime: "Schema.makeFilter(isRequirementOrGuideline)" }) },
|
|
143
150
|
),
|
|
144
151
|
)
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
? "a rule is a requirement (must, never) or a guideline (should, shouldNot), not both"
|
|
151
|
-
: requirement || guideline
|
|
152
|
-
? undefined
|
|
153
|
-
: "a rule needs an example: must, never, should, or shouldNot";
|
|
154
|
-
}),
|
|
155
|
-
);
|
|
156
|
-
export interface Rule extends Schema.Schema.Type<typeof Rule> {}
|
|
152
|
+
.annotate({ identifier: "Rule" });
|
|
153
|
+
export interface Rule extends Schema.Schema.Type<typeof Rule> {
|
|
154
|
+
/** The settings `options` decoded from the config's override, set as the config is loaded. */
|
|
155
|
+
readonly optionValues?: unknown;
|
|
156
|
+
}
|
|
157
157
|
|
|
158
158
|
/** How a finding is reported: an error fails the run, a warning does not. */
|
|
159
159
|
export type Level = NonNullable<Rule["level"]>;
|
|
@@ -186,70 +186,76 @@ export type Rules = Readonly<Record<RuleId, Rule>>;
|
|
|
186
186
|
/** Rules inline, or a directory of Markdown rule files (see `markdown.ts`). */
|
|
187
187
|
export type RuleSource = Rules | string | URL;
|
|
188
188
|
|
|
189
|
-
/** A rule's setting in a config's `overrides`: `off`, a level, or a level and
|
|
189
|
+
/** A rule's setting in a config's `overrides`: `off`, a level, or a level, a threshold, and its options. */
|
|
190
190
|
const Override = Schema.Union([
|
|
191
191
|
Schema.Literals(["off", "error", "warning"]),
|
|
192
192
|
Schema.Struct({
|
|
193
193
|
level: Schema.optionalKey(Schema.Literals(["off", "error", "warning"])),
|
|
194
194
|
threshold: Schema.optionalKey(Schema.Finite),
|
|
195
|
+
options: Schema.optionalKey(Schema.Unknown),
|
|
195
196
|
}),
|
|
196
197
|
]);
|
|
197
198
|
export type Override = typeof Override.Type;
|
|
198
199
|
|
|
200
|
+
/** A config file's keys. Each `description` is what the docs' Config file reference shows. */
|
|
199
201
|
export const AdhereConfig = Schema.Struct({
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
202
|
+
model: Schema.optionalKey(Schema.String).annotate({
|
|
203
|
+
description: `The model id sent to TypeSafe AI. Defaults to "jev-latest".`,
|
|
204
|
+
}),
|
|
205
|
+
provider: Schema.optionalKey(ProviderSchema).annotate({
|
|
206
|
+
description:
|
|
207
|
+
"Where the questions go in place of TypeSafe's Jev, with its own model (see `Provider`).",
|
|
208
|
+
}),
|
|
209
|
+
threshold: Schema.optionalKey(Schema.Finite).annotate({
|
|
210
|
+
description: "The minimum confidence threshold for a positive match. Defaults to 0.8.",
|
|
211
|
+
}),
|
|
212
|
+
sufficiencyThreshold: Schema.optionalKey(Schema.Finite).annotate({
|
|
213
|
+
description:
|
|
214
|
+
"Below this, Jev's probability that a file shows enough to decide a finding's rule adds a warning to the finding: check what the code relies on outside the file. Defaults to 0.6.",
|
|
215
|
+
}),
|
|
216
|
+
presets: Schema.Array(Schema.Literals(presetNames))
|
|
217
|
+
.pipe(Schema.withDecodingDefaultKey(Effect.succeed([])))
|
|
218
|
+
.annotate({
|
|
219
|
+
description:
|
|
220
|
+
"The built-in rule sets to use, such as `effect` or `alchemy`. Append `/<topic>` for one topic, as in `effect/basics`. Defaults to none.",
|
|
221
|
+
}),
|
|
222
|
+
reads: Schema.optionalKey(Reads).annotate({
|
|
223
|
+
description:
|
|
224
|
+
"Workspace context shared by every rule: `workspacePackages` (the default) and `dependents`; `[]` disables it. Native read names do not enable type awareness.",
|
|
225
|
+
}),
|
|
226
|
+
typeAware: Schema.optionalKey(Schema.Boolean).annotate({
|
|
227
|
+
description:
|
|
228
|
+
"Include both compiler-resolved symbols and declaration excerpts. Defaults to false.",
|
|
229
|
+
}),
|
|
230
|
+
maxSymbolLength: Schema.optionalKey(Schema.Natural).annotate({
|
|
231
|
+
description: "Maximum characters of each symbol's complete resolved content. Defaults to 256.",
|
|
232
|
+
}),
|
|
233
|
+
maxSymbolDocumentationLength: Schema.optionalKey(Schema.Natural).annotate({
|
|
234
|
+
description:
|
|
235
|
+
"Maximum characters of documentation and JSDoc tags per symbol. Defaults to 256; 0 omits them.",
|
|
236
|
+
}),
|
|
237
|
+
rules: Schema.optionalKey(
|
|
238
|
+
Schema.Union([Schema.Record(Schema.String, Rule), Schema.String]),
|
|
239
|
+
).annotate({
|
|
240
|
+
description:
|
|
241
|
+
"A record of rules (see Rule keys), or a directory path relative to the config file, which holds its rules as `.adhere/rules/` does. Unset: the rules in `.adhere/rules/`, and in any nested `.adhere/rules/`.",
|
|
242
|
+
}),
|
|
243
|
+
instructions: Schema.optionalKey(Schema.NonEmptyString).annotate({
|
|
244
|
+
description:
|
|
245
|
+
"Instructions directory relative to the config file. Unset: `.adhere/instructions`, then `.agents/instructions`.",
|
|
246
|
+
}),
|
|
247
|
+
exclude: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
|
|
248
|
+
description:
|
|
249
|
+
"Globs, relative to the working directory, of files no rule judges: what `--filter '!<glob>'` leaves out of one run, left out of every run.",
|
|
250
|
+
}),
|
|
251
|
+
overrides: Schema.optionalKey(Schema.Record(Schema.String, Override)).annotate({
|
|
252
|
+
description:
|
|
253
|
+
"Settings for rules by the id a report names them with, a preset's with its preset first, as in `alchemy/providers/idempotent-delete`: `off`, a level, or `{ level, threshold, options }`. A preset's rule changes without its text being copied into `rules`.",
|
|
254
|
+
}),
|
|
255
|
+
includeComments: Schema.optionalKey(Schema.Boolean).annotate({
|
|
256
|
+
description:
|
|
257
|
+
"Whether Jev reads every comment, for rules about comments. By default comments are taken out, since a comment changes nothing a file does, but for the notes that say `@adhere`.",
|
|
258
|
+
}),
|
|
253
259
|
});
|
|
254
260
|
|
|
255
261
|
/** Where a repo keeps what adhere reads and writes: its config, its rules, and the cache. */
|
|
@@ -333,7 +339,7 @@ export type Loaded<T extends { readonly rules?: unknown }> = Omit<T, "rules"> &
|
|
|
333
339
|
export const DEFAULT_MODEL = "jev-latest";
|
|
334
340
|
export const DEFAULT_THRESHOLD = 0.8;
|
|
335
341
|
export const DEFAULT_SUFFICIENCY_THRESHOLD = 0.6;
|
|
336
|
-
export const DEFAULT_READS: typeof Reads.Type = ["workspacePackages"
|
|
342
|
+
export const DEFAULT_READS: typeof Reads.Type = ["workspacePackages"];
|
|
337
343
|
export const DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH = 256;
|
|
338
344
|
export const DEFAULT_MAX_SYMBOL_LENGTH = 256;
|
|
339
345
|
|
|
@@ -355,6 +361,8 @@ export interface ResolvedConfig {
|
|
|
355
361
|
readonly includeComments?: boolean;
|
|
356
362
|
/** Built-in context shared by every rule, with the default applied. */
|
|
357
363
|
readonly reads: typeof Reads.Type;
|
|
364
|
+
/** Whether both native symbol and declaration context are enabled. */
|
|
365
|
+
readonly typeAware: boolean;
|
|
358
366
|
/** Maximum characters of each symbol's complete resolved content. */
|
|
359
367
|
readonly maxSymbolLength: number;
|
|
360
368
|
/** Maximum characters of documentation and JSDoc tags before the complete symbol limit applies. */
|
|
@@ -362,9 +370,11 @@ export interface ResolvedConfig {
|
|
|
362
370
|
/**
|
|
363
371
|
* The workspace's packages, each name with its directory from the working
|
|
364
372
|
* directory, read from its manifests on every run when the config's reads
|
|
365
|
-
* names
|
|
373
|
+
* names `workspacePackages` or `dependents`.
|
|
366
374
|
*/
|
|
367
375
|
readonly workspacePackages?: Readonly<Record<string, string>>;
|
|
376
|
+
/** Per workspace package, the workspace packages that depend on it, when reads names `dependents`. */
|
|
377
|
+
readonly workspaceDependents?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
368
378
|
}
|
|
369
379
|
|
|
370
380
|
/** Command-line values that apply on top of the config file. */
|
|
@@ -376,6 +386,8 @@ export interface Flags {
|
|
|
376
386
|
readonly sufficiencyThreshold?: number | undefined;
|
|
377
387
|
/** Throttles requests to Jev to at most this many a minute. */
|
|
378
388
|
readonly rpm?: number | undefined;
|
|
389
|
+
/** Replaces the config's native-context opt-in. */
|
|
390
|
+
readonly typeAware?: boolean | undefined;
|
|
379
391
|
}
|
|
380
392
|
|
|
381
393
|
/**
|
|
@@ -410,6 +422,7 @@ export const resolveConfig = (
|
|
|
410
422
|
});
|
|
411
423
|
const last = <K extends "model" | "threshold">(key: K) =>
|
|
412
424
|
applied.map((preset) => preset[key]).findLast((value) => value !== undefined);
|
|
425
|
+
const typeAware = flags.typeAware ?? config.typeAware ?? false;
|
|
413
426
|
return {
|
|
414
427
|
model: config.provider?.model ?? config.model ?? last("model") ?? DEFAULT_MODEL,
|
|
415
428
|
...(config.provider === undefined ? {} : { provider: config.provider }),
|
|
@@ -420,7 +433,13 @@ export const resolveConfig = (
|
|
|
420
433
|
...(flags.rpm === undefined ? {} : { rpm: flags.rpm }),
|
|
421
434
|
...(config.exclude === undefined ? {} : { exclude: config.exclude }),
|
|
422
435
|
...(config.includeComments === undefined ? {} : { includeComments: config.includeComments }),
|
|
423
|
-
|
|
436
|
+
typeAware,
|
|
437
|
+
reads: [
|
|
438
|
+
...(config.reads ?? DEFAULT_READS).filter(
|
|
439
|
+
(read) => read === "workspacePackages" || read === "dependents",
|
|
440
|
+
),
|
|
441
|
+
...(typeAware ? (["symbols", "references"] as const) : []),
|
|
442
|
+
],
|
|
424
443
|
maxSymbolLength: config.maxSymbolLength ?? DEFAULT_MAX_SYMBOL_LENGTH,
|
|
425
444
|
maxSymbolDocumentationLength:
|
|
426
445
|
config.maxSymbolDocumentationLength ?? DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|