@drkmttr/adhere 0.10.0 → 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 (51) hide show
  1. package/README.md +177 -41
  2. package/package.json +7 -7
  3. package/presets/effect/basics/gen-for-sequencing.md +1 -1
  4. package/presets/effect/data/brand-meaningful-primitives.md +1 -1
  5. package/presets/effect/data/variants-are-tagged-unions.md +1 -1
  6. package/presets/effect/services/dependencies-through-layers.md +1 -1
  7. package/presets/react/effects/clean-up-what-they-start.md +23 -0
  8. package/presets/react/effects/event-logic-in-handlers.md +29 -0
  9. package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
  10. package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
  11. package/presets/react/effects/notify-parents-in-handlers.md +29 -0
  12. package/presets/react/server/no-private-data-to-client.md +23 -0
  13. package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
  14. package/presets/react/state/no-contradictory-state.md +17 -0
  15. package/presets/react/state/no-props-copied-into-state.md +23 -0
  16. package/presets/react/state/store-ids-not-copies.md +20 -0
  17. package/presets/security/constant-time-secret-compare.md +22 -0
  18. package/presets/security/no-secrets-in-output.md +19 -0
  19. package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
  20. package/presets/security/parameterized-queries.md +16 -0
  21. package/presets/typescript/async/concurrent-independent-work.md +23 -0
  22. package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
  23. package/presets/typescript/design/no-argument-mutation.md +20 -0
  24. package/presets/typescript/errors/no-swallowed-errors.md +29 -0
  25. package/presets/typescript/errors/release-on-every-path.md +29 -0
  26. package/presets/typescript/testing/assert-outcomes.md +22 -0
  27. package/presets/typescript/testing/independent-tests.md +41 -0
  28. package/presets/typescript/types/assertions-only-when-proven.md +20 -0
  29. package/presets/typescript/types/derive-dont-restate.md +24 -0
  30. package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
  31. package/presets/typescript/types/validate-external-data.md +21 -0
  32. package/skills/adhere/SKILL.md +4 -3
  33. package/skills/adhere-fix/SKILL.md +10 -4
  34. package/src/cli.ts +9 -1
  35. package/src/config.ts +29 -1
  36. package/src/excerpt.ts +72 -62
  37. package/src/highlight.ts +2 -0
  38. package/src/markdown.ts +6 -0
  39. package/src/presets.ts +6 -0
  40. package/src/rules.ts +1 -1
  41. package/src/services/AuditCache.ts +31 -5
  42. package/src/services/Jev.http.ts +76 -11
  43. package/src/services/Jev.ts +191 -73
  44. package/src/services/SourceWalker.ts +23 -7
  45. package/src/suppress.ts +13 -7
  46. package/src/version.ts +25 -1
  47. package/src/workflows/audit.ts +96 -26
  48. package/src/workflows/format.ts +51 -11
  49. package/presets/alchemy/data/parameterized-sql.md +0 -17
  50. package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -19
  51. package/presets/effect/basics/external-calls-are-resilient.md +0 -21
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: A component must read a prop, or compute from it, during render, never copy it into state, where the copy stops following the prop when the parent passes a new value.
3
+ appliesTo: ["a React component or hook"]
4
+ excludeIf: ["a prop meant only as the starting value and named for that, such as initialColor or defaultValue"]
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```tsx
10
+ function Message({ messageColor }: { messageColor: string }) {
11
+ const color = messageColor;
12
+ return <p style={{ color }}>Hello</p>;
13
+ }
14
+ ```
15
+
16
+ ## Never
17
+
18
+ ```tsx
19
+ function Message({ messageColor }: { messageColor: string }) {
20
+ const [color, setColor] = useState(messageColor);
21
+ return <p style={{ color }}>Hello</p>;
22
+ }
23
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: State that says which item of a list is selected, or otherwise points into data held elsewhere, should hold the item's id and look the item up during render, and should not hold a copy of the item, which goes stale when the list changes.
3
+ appliesTo: ["a React component or hook"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```tsx
10
+ const [items, setItems] = useState(initialItems);
11
+ const [selectedId, setSelectedId] = useState(initialItems[0].id);
12
+ const selectedItem = items.find((item) => item.id === selectedId);
13
+ ```
14
+
15
+ ## Should not
16
+
17
+ ```tsx
18
+ const [items, setItems] = useState(initialItems);
19
+ const [selectedItem, setSelectedItem] = useState(initialItems[0]);
20
+ ```
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Code that checks a secret it received, such as a token, an API key, or an HMAC signature, must compare it in constant time, as `crypto.timingSafeEqual` or a library's verify function does, never with `===`, `!==`, or a string method.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ const expected = createHmac("sha256", WEBHOOK_SECRET).update(body).digest();
9
+ const received = Buffer.from(request.headers.get("x-signature") ?? "", "hex");
10
+ if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
11
+ return new Response("Invalid signature", { status: 401 });
12
+ }
13
+ ```
14
+
15
+ ## Never
16
+
17
+ ```ts
18
+ const expected = createHmac("sha256", WEBHOOK_SECRET).update(body).digest("hex");
19
+ if (request.headers.get("x-signature") !== expected) {
20
+ return new Response("Invalid signature", { status: 401 });
21
+ }
22
+ ```
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: A secret, such as a password, an API key, a token, a private key, a session id, or a connection string with credentials, must never be written to a log, put in an error message, or returned in an HTTP response; code must refer to a secret only by its name, its presence, or a non-secret id.
3
+ appliesTo: ["code that writes a log, builds an error message, or builds an HTTP response"]
4
+ excludeIf: ["a masked secret that shows at most a few of its characters"]
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```ts
10
+ logger.info("calling billing", { keyId: apiKey.id });
11
+ throw new Error(`Could not connect to the database at ${dbHost}`);
12
+ ```
13
+
14
+ ## Never
15
+
16
+ ```ts
17
+ logger.info(`calling billing with key ${apiKey.secret}`);
18
+ throw new Error(`Could not connect to ${process.env.DATABASE_URL}`);
19
+ ```
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Input from outside the program, such as a request's parameters or body, must reach a shell command only as a separate argument, and a file path only after the resolved path is checked to be inside the directory meant for it; it must never be interpolated into a command string, passed to `eval` or `new Function`, or joined into a path unchecked.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ const file = resolve(UPLOADS, request.params.name);
9
+ if (!file.startsWith(UPLOADS + sep)) return new Response("Not found", { status: 404 });
10
+ const { stdout } = await execFile("ffprobe", ["-show_format", file]);
11
+ ```
12
+
13
+ ## Never
14
+
15
+ ```ts
16
+ const file = join(UPLOADS, request.params.name);
17
+ const { stdout } = await exec(`ffprobe -show_format ${file}`);
18
+ ```
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: A SQL query that includes a value that comes from outside the code, such as user input or a function's argument, must pass it as a bound parameter, or through a query builder or tagged template that binds it, never concatenate or interpolate the value into the query string.
3
+ excludeIf: ["an identifier, such as a table or column name, the code chose from a fixed list"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ const { rows } = await db.query("SELECT * FROM users WHERE email = $1", [email]);
10
+ ```
11
+
12
+ ## Never
13
+
14
+ ```ts
15
+ const { rows } = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
16
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Awaited calls that do not depend on each other's results should run concurrently, with `Promise.all` or `Promise.allSettled`, and should not be awaited one after another or one per loop iteration.
3
+ excludeIf: ["calls that must run in order, such as writes whose order matters", "calls spaced out on purpose, such as to respect a rate limit"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```ts
10
+ const [user, orders, preferences] = await Promise.all([
11
+ getUser(userId),
12
+ getOrders(userId),
13
+ getPreferences(userId),
14
+ ]);
15
+ ```
16
+
17
+ ## Should not
18
+
19
+ ```ts
20
+ const user = await getUser(userId);
21
+ const orders = await getOrders(userId);
22
+ const preferences = await getPreferences(userId);
23
+ ```
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: An HTTP request, such as a `fetch` or a call through an HTTP client, should carry a timeout or an AbortSignal that ends it, and should not wait without limit.
3
+ excludeIf: ["a call through a client configured with a default timeout", "a call whose signal the caller passes in", "a request from browser code to the app's own API"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```ts
10
+ const response = await fetch(`${BILLING_URL}/invoices/${id}`, {
11
+ signal: AbortSignal.timeout(5_000),
12
+ });
13
+ ```
14
+
15
+ ## Should not
16
+
17
+ ```ts
18
+ const response = await fetch(`${BILLING_URL}/invoices/${id}`);
19
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: A function must return a changed copy of an object or array it received as an argument, never mutate the argument, such as by sorting it in place, pushing to it, deleting a key, or assigning to a property, where its caller does not expect the change.
3
+ excludeIf: ["a function whose name says it changes its argument, such as sortInPlace", "an accumulator the function builds, such as a reduce callback's"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ const byNewest = (posts: ReadonlyArray<Post>) =>
10
+ posts.toSorted((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
11
+ ```
12
+
13
+ ## Never
14
+
15
+ ```ts
16
+ const byNewest = (posts: Post[]) => {
17
+ posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
18
+ return posts;
19
+ };
20
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: A caught error must be handled, rethrown, or returned as a failure the caller can tell apart from success, never discarded or turned into a value that reads as a successful result, such as `null`, an empty array, or a default. Logging an error and carrying on as if nothing failed discards it. Recovering from one specific, expected error, such as a missing file that has a default, handles it.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ async function loadSettings(path: string): Promise<Settings> {
9
+ try {
10
+ return parseSettings(await readFile(path, "utf8"));
11
+ } catch (error) {
12
+ if (isNotFound(error)) return DEFAULT_SETTINGS;
13
+ throw new Error(`Could not load settings from ${path}`, { cause: error });
14
+ }
15
+ }
16
+ ```
17
+
18
+ ## Never
19
+
20
+ ```ts
21
+ async function loadInvoices(customerId: string): Promise<Invoice[]> {
22
+ try {
23
+ return await api.invoices.list({ customerId });
24
+ } catch (error) {
25
+ console.error(error);
26
+ return [];
27
+ }
28
+ }
29
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: A resource the code acquires and must give back, such as a file handle, a lock, a database connection or transaction, a temporary file, or a child process, must be released on every path, including when an error is thrown between acquiring and releasing it, with `finally` or `using`; it must never be released only after the work succeeds. A resource whose owner releases it, such as a framework or a pool's query method, is out of scope.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ const client = await pool.connect();
9
+ try {
10
+ await client.query("BEGIN");
11
+ await client.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, from]);
12
+ await client.query("COMMIT");
13
+ } catch (error) {
14
+ await client.query("ROLLBACK");
15
+ throw error;
16
+ } finally {
17
+ client.release();
18
+ }
19
+ ```
20
+
21
+ ## Never
22
+
23
+ ```ts
24
+ const client = await pool.connect();
25
+ await client.query("BEGIN");
26
+ await client.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, from]);
27
+ await client.query("COMMIT");
28
+ client.release();
29
+ ```
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: A test must assert what the code under test returned or did, such as its result, what it rendered, or the state it left, never only that it ran without throwing or returned something defined.
3
+ tests: only
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ it("applies the discount to each line", () => {
10
+ const total = checkout([{ price: 100, quantity: 2 }], { discount: 0.1 });
11
+ expect(total).toBe(180);
12
+ });
13
+ ```
14
+
15
+ ## Never
16
+
17
+ ```ts
18
+ it("applies the discount to each line", () => {
19
+ const total = checkout([{ price: 100, quantity: 2 }], { discount: 0.1 });
20
+ expect(total).toBeDefined();
21
+ });
22
+ ```
@@ -0,0 +1,41 @@
1
+ ---
2
+ description: A test must set up the state it needs itself, or in a `beforeEach`, and must never rely on state another test left behind, such as a module-level variable, a record another test inserted, or a mock another test configured.
3
+ excludeIf: ["state a suite sets up once for all its tests, in beforeAll or its body, that no test changes"]
4
+ tests: only
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```ts
10
+ let cart: Cart;
11
+ beforeEach(() => {
12
+ cart = new Cart();
13
+ });
14
+
15
+ it("adds an item", () => {
16
+ cart.add(book);
17
+ expect(cart.items).toEqual([book]);
18
+ });
19
+
20
+ it("totals its items", () => {
21
+ cart.add(book);
22
+ cart.add(pen);
23
+ expect(cart.total()).toBe(book.price + pen.price);
24
+ });
25
+ ```
26
+
27
+ ## Never
28
+
29
+ ```ts
30
+ const cart = new Cart();
31
+
32
+ it("adds an item", () => {
33
+ cart.add(book);
34
+ expect(cart.items).toEqual([book]);
35
+ });
36
+
37
+ it("totals its items", () => {
38
+ cart.add(pen);
39
+ expect(cart.total()).toBe(book.price + pen.price);
40
+ });
41
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: A type assertion with `as`, or a non-null assertion with `!`, should claim only what the code around it has already established, and should not be used to make a type error go away. `as const` claims nothing and is out of scope.
3
+ excludeIf: ["data read from outside the program, such as a parsed body or a fetch response"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```ts
10
+ const input = document.querySelector("#email");
11
+ if (!(input instanceof HTMLInputElement)) throw new Error("#email is not an input");
12
+ input.value = email;
13
+ ```
14
+
15
+ ## Should not
16
+
17
+ ```ts
18
+ const input = document.querySelector("#email") as HTMLInputElement;
19
+ input.value = email;
20
+ ```
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: A type that describes the same data as another type, a schema, a function, or a constant should be derived from it, with a utility type such as `Pick`, with `typeof` or `keyof`, or as a schema's inferred type, and should not restate its fields or values by hand.
3
+ level: warning
4
+ ---
5
+
6
+ ## Should
7
+
8
+ ```ts
9
+ const ROLES = ["admin", "member", "guest"] as const;
10
+ type Role = (typeof ROLES)[number];
11
+
12
+ const User = z.object({ id: z.string(), email: z.string(), role: z.enum(ROLES) });
13
+ type User = z.infer<typeof User>;
14
+ ```
15
+
16
+ ## Should not
17
+
18
+ ```ts
19
+ const ROLES = ["admin", "member", "guest"];
20
+ type Role = "admin" | "member" | "guest";
21
+
22
+ const User = z.object({ id: z.string(), email: z.string(), role: z.enum(ROLES) });
23
+ type User = { id: string; email: string; role: Role };
24
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: A type whose fields are valid only in some combinations must be a union with one member per state, each carrying only the fields that state has, never one object type of optional fields and flags whose valid combinations only the code that reads them knows. A union of Schema.TaggedClass members is such a union.
3
+ appliesTo: ["a declared type, interface, or schema"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ type Request<T> =
10
+ | { status: "loading" }
11
+ | { status: "ok"; data: T }
12
+ | { status: "error"; error: Error };
13
+ ```
14
+
15
+ ## Never
16
+
17
+ ```ts
18
+ interface Request<T> {
19
+ loading: boolean;
20
+ data?: T;
21
+ error?: Error;
22
+ }
23
+ ```
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Data from outside the program, such as a parsed request body, a fetch response, a file's contents, a message, or an environment variable, must be checked at runtime before code gives it a type, never cast with `as` or assigned from `any` to a declared type. A schema's parse or decode, such as Zod's or Effect Schema's, or a type guard that checks each field it claims, is a runtime check.
3
+ appliesTo: ["code that reads data from outside the program"]
4
+ excludeIf: ["a value read through a typed client of the program's own database"]
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```ts
10
+ const User = z.object({ id: z.string(), email: z.string().email() });
11
+
12
+ const response = await fetch(`/api/users/${id}`);
13
+ const user = User.parse(await response.json());
14
+ ```
15
+
16
+ ## Never
17
+
18
+ ```ts
19
+ const response = await fetch(`/api/users/${id}`);
20
+ const user = (await response.json()) as User;
21
+ ```
@@ -94,9 +94,10 @@ Rules for the rule:
94
94
  - Jev reads code without its comments, except comments that say `@adhere`. A
95
95
  rule about comments, such as doc comments on exports, needs
96
96
  `includeComments: true` in the config.
97
- - Rules skip tests: `.test.ts` and `.spec.ts` files, and files under `test/`,
98
- `tests/`, `__tests__/`, or `fixtures/`. A rule about tests says `tests: only`
99
- in its front matter, and one that holds in tests as well `tests: include`.
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`.
100
101
  - A nit, or a rule that tends to flag code wrongly, says `level: warning` in
101
102
  its front matter: its findings show in amber and do not fail CI.
102
103
  - `threshold` is optional. Start without it; set it in step 5 if needed.
@@ -7,8 +7,8 @@ description: Verify and fix the findings `adhere lint` reports. Decide whether e
7
7
 
8
8
  adhere is a linter for rules a normal linter cannot check. Jev, TypeSafe AI's
9
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, at the line Jev
11
- points to. A finding is a judgment, not a proof. On one rule adhere's authors
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
12
  checked against the live API, only 1 of its 20 findings was a real violation.
13
13
  Verify every finding before you change code for it: fix the real ones, and
14
14
  report the false ones with the evidence.
@@ -34,7 +34,13 @@ Each finding shows:
34
34
  - the rule id, a preset's with its preset first, as in
35
35
  `alchemy/providers/idempotent-delete`, Jev's probability, and the rule's
36
36
  description;
37
- - the line Jev points at, underlined, with the code around it;
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);
38
44
  - `hint:`, the code the rule wants, or `never:` for a rule that only shows code
39
45
  to avoid.
40
46
 
@@ -57,7 +63,7 @@ the code under `## Must` and `## Never`, and anything it puts out of scope.
57
63
 
58
64
  Read the code as a reviewer would, not only the excerpt:
59
65
 
60
- - **The whole unit.** Read the function, handler, or class the line is in,
66
+ - **The whole unit.** Read the function, handler, or class the section holds,
61
67
  and what calls it.
62
68
  - **What it calls.** Follow helpers into other files. A helper can already do
63
69
  what the rule asks, such as a delete that looks unguarded calling a client
package/src/cli.ts CHANGED
@@ -99,6 +99,13 @@ const threshold = Flag.float("threshold").pipe(
99
99
  ),
100
100
  );
101
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
+
102
109
  const limit = Flag.integer("limit").pipe(
103
110
  Flag.filter(
104
111
  (checks) => checks >= 0,
@@ -219,7 +226,7 @@ export const auditLayer = (flags: Flags, filter: ReadonlyArray<string> = []) =>
219
226
  /** `adhere lint`: the audit. */
220
227
  export const lintCommand = Command.make(
221
228
  "lint",
222
- { preset, threshold, yes, limit, rpm, filter, denyWarnings },
229
+ { preset, threshold, sufficiencyThreshold, yes, limit, rpm, filter, denyWarnings },
223
230
  (input) =>
224
231
  Effect.gen(function* () {
225
232
  const stdio = yield* Stdio.Stdio;
@@ -264,6 +271,7 @@ export const lintCommand = Command.make(
264
271
  {
265
272
  presets: input.preset,
266
273
  threshold: Option.getOrUndefined(input.threshold),
274
+ sufficiencyThreshold: Option.getOrUndefined(input.sufficiencyThreshold),
267
275
  rpm: Option.getOrUndefined(input.rpm),
268
276
  },
269
277
  input.filter,
package/src/config.ts CHANGED
@@ -28,6 +28,17 @@ const RuleFields = Schema.Struct({
28
28
  * findings, which does not.
29
29
  */
30
30
  level: Schema.optionalKey(Schema.Literals(["error", "warning"])),
31
+ /**
32
+ * Where the rule applies, each a description of code that breaks it: Jev is
33
+ * asked of each whether any code that breaks the rule is as it describes,
34
+ * and a finding stands only when it says yes to every one.
35
+ */
36
+ appliesTo: Schema.optionalKey(Schema.Array(Schema.String)),
37
+ /**
38
+ * Where the rule does not apply: Jev is asked of each whether all the code
39
+ * that breaks the rule is as it describes, and a yes to any drops the finding.
40
+ */
41
+ excludeIf: Schema.optionalKey(Schema.Array(Schema.String)),
31
42
  });
32
43
 
33
44
  /** Before 0.7, a rule's examples were `reference` and `avoid`: read as `must` and `never`. */
@@ -112,6 +123,12 @@ export const AdhereConfig = Schema.Struct({
112
123
  * The minimum confidence threshold for a positive match. Defaults to 0.8
113
124
  */
114
125
  threshold: Schema.optionalKey(Schema.Finite),
126
+ /**
127
+ * Below this, Jev's probability that a file shows enough to decide a
128
+ * finding's rule adds a warning to the finding: check what the code relies
129
+ * on outside the file. Defaults to 0.6
130
+ */
131
+ sufficiencyThreshold: Schema.optionalKey(Schema.Finite),
115
132
  /**
116
133
  * The presets to use for rule matching. Defaults to an empty array. Current presets
117
134
  * include "effect" and "alchemy". Subcategories can be expanded by appending `/<subcategory>`.
@@ -175,13 +192,17 @@ export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
175
192
  /**
176
193
  * Directories whose files are tests, matched as whole path segments below the
177
194
  * working directory, as skipped directories are. Only a rule that says `tests`
178
- * judges them, or a `.test.ts` or `.spec.ts` file anywhere.
195
+ * judges them, or a `.test` or `.spec` file, such as `a.test.tsx`, anywhere.
179
196
  */
180
197
  export const TEST_DIRECTORIES: ReadonlySet<string> = new Set([
181
198
  "test",
182
199
  "tests",
183
200
  "__tests__",
201
+ "testing",
184
202
  "fixtures",
203
+ "fixture",
204
+ "__fixtures__",
205
+ "__mocks__",
185
206
  ]);
186
207
 
187
208
  /** The shape a config file default-exports: the schema's input, where `presets` is optional. */
@@ -212,11 +233,14 @@ export type Loaded<T extends { readonly rules?: unknown }> = Omit<T, "rules"> &
212
233
 
213
234
  export const DEFAULT_MODEL = "jev-latest";
214
235
  export const DEFAULT_THRESHOLD = 0.8;
236
+ export const DEFAULT_SUFFICIENCY_THRESHOLD = 0.6;
215
237
 
216
238
  /** A config with its presets folded in and every default applied. */
217
239
  export interface ResolvedConfig {
218
240
  readonly model: string;
219
241
  readonly threshold: number;
242
+ /** Below this, a finding's sufficiency adds a warning to it. */
243
+ readonly sufficiencyThreshold: number;
220
244
  readonly rules: Rules;
221
245
  readonly scopedRules?: RuleSet;
222
246
  /** Requests to Jev a minute, at most. Unset, requests go out as fast as they are made. */
@@ -232,6 +256,8 @@ export interface Flags {
232
256
  readonly presets?: ReadonlyArray<PresetName>;
233
257
  /** Replaces the config's global threshold. Per-rule thresholds still win. */
234
258
  readonly threshold?: number | undefined;
259
+ /** Replaces the config's sufficiency threshold. */
260
+ readonly sufficiencyThreshold?: number | undefined;
235
261
  /** Throttles requests to Jev to at most this many a minute. */
236
262
  readonly rpm?: number | undefined;
237
263
  }
@@ -271,6 +297,8 @@ export const resolveConfig = (
271
297
  return {
272
298
  model: config.model ?? last("model") ?? DEFAULT_MODEL,
273
299
  threshold: flags.threshold ?? config.threshold ?? last("threshold") ?? DEFAULT_THRESHOLD,
300
+ sufficiencyThreshold:
301
+ flags.sufficiencyThreshold ?? config.sufficiencyThreshold ?? DEFAULT_SUFFICIENCY_THRESHOLD,
274
302
  rules: Object.assign({}, ...applied.map((preset) => preset.rules), config.rules),
275
303
  ...(flags.rpm === undefined ? {} : { rpm: flags.rpm }),
276
304
  ...(config.exclude === undefined ? {} : { exclude: config.exclude }),