@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
package/src/excerpt.ts CHANGED
@@ -1,20 +1,12 @@
1
1
  import { scan } from "#highlight.ts";
2
2
 
3
- /**
4
- * The lines a finding shows: the statement its line is in, such as the
5
- * function around it, and a few lines on either side of the line.
6
- */
3
+ /** The lines a finding shows: the section of the file it is in. */
7
4
  export interface Excerpt {
8
5
  /** The number of the first line, counting from 1. */
9
6
  readonly start: number;
10
7
  readonly lines: ReadonlyArray<string>;
11
8
  }
12
9
 
13
- /** How far an excerpt reaches on either side of the finding's line, in whole statements. */
14
- const AROUND = 3;
15
- /** The most lines a statement can have and still be shown whole. */
16
- const WHOLE = 30;
17
-
18
10
  /** Lines of the code, first to last, counting from 1. */
19
11
  export interface Range {
20
12
  readonly first: number;
@@ -48,19 +40,13 @@ const ROLES: Readonly<Record<string, Piece["role"]>> = {
48
40
  ",": "separator",
49
41
  };
50
42
 
51
- /**
52
- * The code as pieces, without whitespace and comments, and the comments and
53
- * literals that span lines: a line that starts inside one does not read as
54
- * code on its own.
55
- */
43
+ /** The code as pieces, without whitespace and comments. */
56
44
  const piecesOf = (code: string) => {
57
45
  const pieces: Array<Piece> = [];
58
- const spans: Array<Range> = [];
59
46
  let line = 1;
60
47
  let spaced = true;
61
48
  for (const { text, kind } of scan(code)) {
62
49
  const end = line + text.split("\n").length - 1;
63
- if (kind !== undefined && end > line) spans.push({ first: line, last: end });
64
50
  if (kind === "comment" || text.trim().length === 0) {
65
51
  spaced = true;
66
52
  } else if (kind !== undefined || /^[\w$]/.test(text)) {
@@ -75,7 +61,7 @@ const piecesOf = (code: string) => {
75
61
  }
76
62
  line = end;
77
63
  }
78
- return { pieces, spans };
64
+ return { pieces };
79
65
  };
80
66
 
81
67
  /**
@@ -209,6 +195,75 @@ const statementsAround = (
209
195
  ];
210
196
  };
211
197
 
198
+ /** The most lines a section holds, unless one statement with no group to split is longer. */
199
+ const SECTION = 30;
200
+
201
+ /**
202
+ * The file in sections, each ending where a statement does: its top-level
203
+ * statements, such as its imports, constants, and functions, packed in
204
+ * order into sections of at most `SECTION` lines, a decorator with what it
205
+ * decorates. A statement longer than that splits into the statements of its
206
+ * largest group, such as a class's methods or a component's body, the lines
207
+ * around that group going with the first and last of them. Every line is in
208
+ * one section: the comments and blank lines above a statement go with it,
209
+ * and the last section runs to the end.
210
+ */
211
+ export const sectionsOf = (lines: ReadonlyArray<string>): ReadonlyArray<Range> => {
212
+ const { pieces } = piecesOf(lines.join("\n"));
213
+ const closes = closesOf(pieces);
214
+ const lineOf = (index: number) => pieces[index]?.line ?? 0;
215
+ /** Statements as ranges of at most `SECTION` lines where they can be split. */
216
+ const units = (statements: ReadonlyArray<Statement>): ReadonlyArray<Range> =>
217
+ statements.flatMap((statement): ReadonlyArray<Range> => {
218
+ if (statement.last - statement.first < SECTION) return [statement];
219
+ const largest = statement.groups.reduce<readonly [number, number] | undefined>(
220
+ (best, group) =>
221
+ best === undefined ||
222
+ lineOf(group[1]) - lineOf(group[0]) > lineOf(best[1]) - lineOf(best[0])
223
+ ? group
224
+ : best,
225
+ undefined,
226
+ );
227
+ const inner =
228
+ largest === undefined
229
+ ? []
230
+ : units(statementsIn(pieces, closes, largest[0] + 1, largest[1]));
231
+ const [head, tail] = [inner[0], inner.at(-1)];
232
+ if (head === undefined || tail === undefined || inner.length < 2) return [statement];
233
+ return [
234
+ { first: statement.first, last: head.last },
235
+ ...inner.slice(1, -1),
236
+ { first: tail.first, last: statement.last },
237
+ ];
238
+ });
239
+ const sections: Array<Range> = [];
240
+ /** A decorator, such as `@Command({…})`, waiting for the declaration it decorates. */
241
+ let decorator: Range | undefined;
242
+ for (const found of units(statementsIn(pieces, closes, 0, pieces.length))) {
243
+ if ((lines[found.first - 1] ?? "").trimStart().startsWith("@")) {
244
+ decorator = { first: decorator?.first ?? found.first, last: found.last };
245
+ continue;
246
+ }
247
+ const unit = { first: decorator?.first ?? found.first, last: found.last };
248
+ decorator = undefined;
249
+ const previous = sections.at(-1);
250
+ if (previous !== undefined && unit.last - previous.first < SECTION) {
251
+ sections[sections.length - 1] = { first: previous.first, last: unit.last };
252
+ } else {
253
+ sections.push({ first: unit.first, last: unit.last });
254
+ }
255
+ }
256
+ if (decorator !== undefined) sections.push(decorator);
257
+ // A file of comments alone, with no statement, is one section.
258
+ if (sections.length === 0) return lines.length === 0 ? [] : [{ first: 1, last: lines.length }];
259
+ // Each section starts where the one before it ends, and the last one ends with the file.
260
+ return sections.flatMap((section, index): ReadonlyArray<Range> => {
261
+ const first = (sections[index - 1]?.last ?? 0) + 1;
262
+ const last = index === sections.length - 1 ? lines.length : section.last;
263
+ return last < first ? [] : [{ first, last }];
264
+ });
265
+ };
266
+
212
267
  /**
213
268
  * The statement that starts on `line`, such as a property, a function, or a
214
269
  * call that opens there: the outermost statement around `line` whose first
@@ -218,48 +273,3 @@ export const statementFrom = (lines: ReadonlyArray<string>, line: number): Range
218
273
  const { pieces } = piecesOf(lines.join("\n"));
219
274
  return statementsAround(pieces, closesOf(pieces), line).find((range) => range.first === line);
220
275
  };
221
-
222
- /**
223
- * The excerpt for a finding on `line`: the outermost statement around it
224
- * with at most `WHOLE` lines, such as the function it is in, and up to
225
- * `AROUND` lines on either side of `line`. A comment or template literal
226
- * that spans lines counts as a statement inside the one it is in.
227
- *
228
- * Around `line`, the excerpt shows only whole statements, never the end of
229
- * the one above or the start of the one below, so it cannot start partway
230
- * through a comment, whose lines do not read as code on their own. What
231
- * `line` itself is in can show in part: a long function's first line, above
232
- * the statement of its body. It ends at code, since a comment at the end
233
- * would be about code the excerpt leaves out.
234
- */
235
- export const excerptOf = (lines: ReadonlyArray<string>, line: number): Excerpt => {
236
- const { pieces, spans } = piecesOf(lines.join("\n"));
237
- const closes = closesOf(pieces);
238
- /** What a line is in, outermost first. */
239
- const around = (at: number): ReadonlyArray<Range> => [
240
- ...statementsAround(pieces, closes, at),
241
- ...spans.filter((span) => holds(span, at)),
242
- ];
243
- const scope = around(line).find((range) => range.last - range.first < WHOLE);
244
- const top = Math.max(1, Math.min(scope?.first ?? line, line - AROUND));
245
- const bottom = Math.min(lines.length, Math.max(scope?.last ?? line, line + AROUND));
246
- /** What a line is in that `line` is not, which an edge shows whole or not at all. */
247
- const beside = (at: number) => around(at).filter((range) => !holds(range, line));
248
- let first = Math.max(
249
- top,
250
- ...beside(top)
251
- .filter((range) => range.first < top)
252
- .map((range) => range.last + 1),
253
- );
254
- let last = Math.min(
255
- bottom,
256
- ...beside(bottom)
257
- .filter((range) => range.last > bottom)
258
- .map((range) => range.first - 1),
259
- );
260
- const isBlank = (at: number) => (lines[at - 1] ?? "").trim().length === 0;
261
- const hasCode = (at: number) => pieces.some((piece) => piece.line <= at && at <= piece.end);
262
- while (first < line && isBlank(first)) first += 1;
263
- while (last > line && !hasCode(last)) last -= 1;
264
- return { start: first, lines: lines.slice(first - 1, last) };
265
- };
package/src/highlight.ts CHANGED
@@ -30,6 +30,8 @@ const CLASSES = ["comment", "string", "constant"] as const;
30
30
  /** One group per token class, tried in order at each position. A template literal is read apart. */
31
31
  const TOKEN = new RegExp(
32
32
  [
33
+ // A URL outside a string is JSX text, as in `<a>https://example.com</a>`, and its `//` no comment.
34
+ /(?<url>[a-z][\w+.-]*:\/\/[^\s<>{}"'`]*)/,
33
35
  /(?<comment>\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$))/,
34
36
  /(?<string>"(?:[^"\\\n]|\\.)*"?|'(?:[^'\\\n]|\\.)*'?)/,
35
37
  /(?<constant>0[box][\da-f_]+n?|\d[\d_]*(?:\.\d[\d_]*)?(?:e[+-]?\d+)?n?)/,
package/src/markdown.ts CHANGED
@@ -32,12 +32,18 @@ import { Effect, Schema } from "effect";
32
32
  * so is `tests`: `only` for a rule about test files, which rules otherwise
33
33
  * skip, and `include` for a rule that judges them too. So is `level`:
34
34
  * `warning` reports the rule's findings as warnings, which do not fail a run.
35
+ * `appliesTo` and `excludeIf`, the rule's scope, are JSON arrays of strings on
36
+ * one line, as in `excludeIf: ["a type that mirrors a third-party format"]`.
35
37
  */
38
+ const Matchers = Schema.fromJsonString(Schema.Array(Schema.String));
39
+
36
40
  const FrontMatter = Schema.Struct({
37
41
  description: Schema.String,
38
42
  threshold: Schema.optionalKey(Schema.FiniteFromString),
39
43
  tests: Schema.optionalKey(Schema.Literals(["only", "include"])),
40
44
  level: Schema.optionalKey(Schema.Literals(["error", "warning"])),
45
+ appliesTo: Schema.optionalKey(Matchers),
46
+ excludeIf: Schema.optionalKey(Matchers),
41
47
  });
42
48
 
43
49
  const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
package/src/presets.ts CHANGED
@@ -23,6 +23,9 @@ const presetsRoot = isStandaloneExecutable
23
23
  const topics = {
24
24
  effect: ["basics", "config", "data", "errors", "services", "testing"],
25
25
  alchemy: ["apis", "data", "durable", "providers", "resources", "runtime", "secrets", "security"],
26
+ typescript: ["async", "design", "errors", "testing", "types"],
27
+ react: ["effects", "server", "state"],
28
+ security: [],
26
29
  } as const;
27
30
 
28
31
  type Topics = typeof topics;
@@ -57,6 +60,9 @@ export const presetOf = (name: PresetName): BuiltInPreset => {
57
60
  export const presets: Readonly<Record<WholePreset, BuiltInPreset>> = {
58
61
  effect: presetOf("effect"),
59
62
  alchemy: presetOf("alchemy"),
63
+ typescript: presetOf("typescript"),
64
+ react: presetOf("react"),
65
+ security: presetOf("security"),
60
66
  };
61
67
 
62
68
  /** The directory each whole preset reads, for checking `topics` against it. */
package/src/rules.ts CHANGED
@@ -27,7 +27,7 @@ export interface RuleEntry {
27
27
 
28
28
  /**
29
29
  * A rule's id as output shows it: a preset's rule with the preset's name
30
- * first, as in effect/basics/external-calls-are-resilient, so a report that
30
+ * first, as in effect/basics/gen-for-sequencing, so a report that
31
31
  * mixes presets with the project's own rules says where each came from. The
32
32
  * project's own rules show their `.adhere/` ids as they are.
33
33
  */
@@ -1,11 +1,30 @@
1
1
  import { CACHE_DIRECTORY } from "#config.ts";
2
2
  import { Context, Crypto, Effect, FileSystem, Layer, Option, Path, Record, Schema } from "effect";
3
3
 
4
+ /** Jev's probability for each of a rule's `appliesTo` and `excludeIf` matchers, when it has them. */
5
+ const MatcherFields = {
6
+ appliesTo: Schema.optionalKey(Schema.Array(Schema.Finite)),
7
+ excludeIf: Schema.optionalKey(Schema.Array(Schema.Finite)),
8
+ };
9
+
10
+ /** Lines of a file, first to last. */
11
+ const Lines = Schema.Struct({ first: Schema.Finite, last: Schema.Finite });
12
+ /** The lines of the section a finding is in, once located. */
13
+ const Section = Schema.optionalKey(Lines);
14
+ /** The lines in its section the code that breaks the rule starts and ends on, when Jev named them. */
15
+ const Located = Schema.optionalKey(Lines);
16
+
17
+ /** Jev's probability, asked with the section, that the file shows enough to decide the rule. */
18
+ const Sufficiency = Schema.optionalKey(Schema.Finite);
19
+
4
20
  /** A rule's judgment as a run holds it: Jev's answer, with the fingerprint of the rule's text. */
5
21
  export const Judgment = Schema.Struct({
6
22
  fingerprint: Schema.String,
7
23
  probability: Schema.Finite,
8
- line: Schema.optionalKey(Schema.Finite),
24
+ section: Section,
25
+ lines: Located,
26
+ sufficiency: Sufficiency,
27
+ ...MatcherFields,
9
28
  });
10
29
  export interface Judgment extends Schema.Schema.Type<typeof Judgment> {}
11
30
 
@@ -21,10 +40,17 @@ export const CacheEntry = Schema.Struct({
21
40
  });
22
41
  export interface CacheEntry extends Schema.Schema.Type<typeof CacheEntry> {}
23
42
 
24
- /** Jev's answer about some code for one rule text: the probability, and the line once located. */
43
+ /**
44
+ * Jev's answer about some code for one rule text: the probability, the
45
+ * section, lines, and sufficiency once located, and its matchers' scores. An answer from before
46
+ * sections held a line instead, under a fingerprint no rule has now.
47
+ */
25
48
  export const Answer = Schema.Struct({
26
49
  probability: Schema.Finite,
27
- line: Schema.optionalKey(Schema.Finite),
50
+ section: Section,
51
+ lines: Located,
52
+ sufficiency: Sufficiency,
53
+ ...MatcherFields,
28
54
  });
29
55
  export interface Answer extends Schema.Schema.Type<typeof Answer> {}
30
56
 
@@ -98,13 +124,13 @@ const sorted = <A>(record: Readonly<Record<string, A>>): Record<string, A> =>
98
124
 
99
125
  const textOf = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n`;
100
126
 
101
- /** One answer per rule text: one that has its line over one that has not, otherwise the first. */
127
+ /** One answer per rule text: one that has its section over one that has not, otherwise the first. */
102
128
  const unionOf = (entries: ReadonlyArray<Answers>): Answers => {
103
129
  const union: Record<string, Answer> = {};
104
130
  for (const answers of entries) {
105
131
  for (const [fingerprint, answer] of Object.entries(answers)) {
106
132
  const held = union[fingerprint];
107
- if (held === undefined || (held.line === undefined && answer.line !== undefined)) {
133
+ if (held === undefined || (held.section === undefined && answer.section !== undefined)) {
108
134
  union[fingerprint] = answer;
109
135
  }
110
136
  }
@@ -1,10 +1,11 @@
1
1
  import type { RuleId } from "#config.ts";
2
+ import { sectionsOf } from "#excerpt.ts";
2
3
  import { AdhereConfig } from "#services/AdhereConfig.ts";
3
4
  import { Credentials } from "#services/Credentials.ts";
4
5
  import {
5
- blockBody,
6
6
  type Body,
7
7
  type ComparedRule,
8
+ edgeKey,
8
9
  conflictBody,
9
10
  contradictBody,
10
11
  Jev,
@@ -16,15 +17,27 @@ import {
16
17
  type Lines,
17
18
  linterKey,
18
19
  locateBody,
20
+ matcherKey,
21
+ type MatcherScores,
19
22
  namedPairs,
20
- needsBlocks,
21
23
  type Pair,
22
24
  pairProbability,
23
25
  requestsOf,
24
26
  type Rules,
27
+ sufficiencyKey,
25
28
  tokensOf,
26
29
  } from "#services/Jev.ts";
27
- import { type Cause, Duration, Effect, Layer, Option, Record, Schedule, Schema } from "effect";
30
+ import {
31
+ type Cause,
32
+ Duration,
33
+ Effect,
34
+ Layer,
35
+ Option,
36
+ Record,
37
+ Result,
38
+ Schedule,
39
+ Schema,
40
+ } from "effect";
28
41
  import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http";
29
42
  import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
30
43
  import { RateLimiter } from "effect/unstable/persistence";
@@ -37,6 +50,16 @@ const NoulAnswers = Schema.Struct({
37
50
  const ChoiceAnswers = Schema.Struct({
38
51
  answers: Schema.Record(Schema.String, Schema.Struct({ choice: Schema.String })),
39
52
  });
53
+ /** A locate request's answers: a choice per rule, and a noul per sufficiency question. */
54
+ const LocateAnswers = Schema.Struct({
55
+ answers: Schema.Record(
56
+ Schema.String,
57
+ Schema.Struct({
58
+ choice: Schema.optionalKey(Schema.String),
59
+ noul: Schema.optionalKey(Schema.Finite),
60
+ }),
61
+ ),
62
+ });
40
63
 
41
64
  const refused = (message: string) => JevUnavailable.make({ message });
42
65
 
@@ -219,22 +242,64 @@ export const JevLive = Layer.effect(Jev)(
219
242
  return answer === undefined ? [] : [[id, answer.noul] as const];
220
243
  }),
221
244
  );
245
+ // A matcher Jev left unanswered counts as a no.
246
+ const scored = (
247
+ kind: keyof MatcherScores,
248
+ id: RuleId,
249
+ matchers: ReadonlyArray<string> = [],
250
+ ) => matchers.map((_, index) => answers[matcherKey(kind, id, index)]?.noul ?? 0);
251
+ const matchers: Record<RuleId, MatcherScores> = Record.fromEntries(
252
+ Object.entries(rules).flatMap(([id, rule]) =>
253
+ rule.appliesTo === undefined && rule.excludeIf === undefined
254
+ ? []
255
+ : [
256
+ [
257
+ id,
258
+ {
259
+ appliesTo: scored("appliesTo", id, rule.appliesTo),
260
+ excludeIf: scored("excludeIf", id, rule.excludeIf),
261
+ },
262
+ ] as const,
263
+ ],
264
+ ),
265
+ );
222
266
  return {
223
267
  probabilities: answered(Object.keys(rules), (id) => id),
224
268
  linter: answered(sampled, linterKey),
269
+ ...(Record.isEmptyRecord(matchers) ? {} : { matchers }),
225
270
  } satisfies Judged;
226
271
  });
227
272
 
228
- const chosen = (answers: Readonly<Record<string, { choice: string }>>) =>
229
- Record.map(answers, (answer) => Number(answer.choice));
230
-
231
273
  const locate = Effect.fn("Jev.locate")(function* (lines: Lines, rules: Rules) {
232
- const blocks = needsBlocks(lines)
233
- ? chosen(yield* answersTo("blocks", blockBody(config.model, lines, rules), ChoiceAnswers))
234
- : undefined;
235
- return chosen(
236
- yield* answersTo("locate", locateBody(config.model, lines, rules, blocks), ChoiceAnswers),
274
+ const sections = sectionsOf(lines);
275
+ const answers = yield* answersTo(
276
+ "locate",
277
+ locateBody(config.model, lines, rules),
278
+ LocateAnswers,
237
279
  );
280
+ // A file of one section has every finding in it. An answer that names no section,
281
+ // or none at all, which Jev should not give, leaves the rule unlocated; lines outside
282
+ // the section leave it without lines.
283
+ return Record.filterMap(rules, (_, id) => {
284
+ const chosen = sections.length === 1 ? 1 : Number(answers[id]?.choice);
285
+ const section = sections[chosen - 1];
286
+ const sufficiency = answers[sufficiencyKey(id)]?.noul;
287
+ if (section === undefined || sufficiency === undefined) return Result.failVoid;
288
+ const [start, end] = (["start", "end"] as const).map((edge) =>
289
+ Number(answers[edgeKey(edge, id, chosen)]?.choice),
290
+ );
291
+ const within = (line: number | undefined): line is number =>
292
+ line !== undefined && section.first <= line && line <= section.last;
293
+ return Result.succeed(
294
+ within(start) && within(end)
295
+ ? {
296
+ section,
297
+ sufficiency,
298
+ lines: { first: Math.min(start, end), last: Math.max(start, end) },
299
+ }
300
+ : { section, sufficiency },
301
+ );
302
+ });
238
303
  });
239
304
 
240
305
  const conflicts = Effect.fn("Jev.conflicts")(function* (