@avi2dg/checks 0.51.0 → 0.52.1

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 (44) hide show
  1. package/.bun-version +1 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +24 -249
  4. package/commitlint.config.ts +0 -1
  5. package/dist/presets/knip.d.ts +1 -1
  6. package/dist/presets/knip.js +1 -1
  7. package/dist/templates/how-to.md +1 -1
  8. package/dist/templates/readme.md +1 -1
  9. package/dist/templates/tutorial.md +1 -1
  10. package/docs/bins.md +67 -0
  11. package/docs/gates/checks-advisories.md +2 -2
  12. package/docs/gates/checks-browser.md +5 -1
  13. package/docs/gates/checks-ci-wiring.md +18 -9
  14. package/docs/gates/checks-commit-identity.md +3 -3
  15. package/docs/gates/checks-copy.md +4 -0
  16. package/docs/gates/checks-docs.md +24 -3
  17. package/docs/gates/checks-exports.md +1 -1
  18. package/docs/gates/checks-frontend-syntax.md +5 -1
  19. package/docs/gates/checks-lint.md +2 -2
  20. package/docs/gates/checks-mutation-baseline.md +9 -5
  21. package/docs/gates/checks-mutation-compare.md +3 -3
  22. package/docs/gates/checks-mutation.md +9 -5
  23. package/docs/gates/checks-night-run.md +1 -1
  24. package/docs/gates/checks-secrets.md +2 -2
  25. package/docs/gates/checks-unused.md +1 -1
  26. package/docs/how-to/set-up-a-repository.md +142 -0
  27. package/docs/how-to/turn-on-the-css-families.md +35 -0
  28. package/docs/how-to/upgrade-the-kit.md +42 -0
  29. package/docs/shipped-files.md +36 -0
  30. package/package.json +11 -1
  31. package/src/delivery/bun-pin.ts +19 -5
  32. package/src/delivery/ci-wiring.ts +11 -77
  33. package/src/delivery/commit-identity.ts +6 -5
  34. package/src/delivery/runner-policy.ts +385 -0
  35. package/src/delivery/workflow-references.ts +1 -1
  36. package/src/docs/doc-templates.ts +1 -1
  37. package/src/docs/doc-versions.ts +137 -0
  38. package/src/docs/docs.ts +7 -0
  39. package/src/quality/comment-matchers.ts +1 -1
  40. package/src/testing/mutation-baseline-full.ts +23 -47
  41. package/src/testing/mutation-baseline-saved.ts +1 -1
  42. package/src/testing/mutation-baseline.ts +10 -8
  43. package/src/testing/mutation-scope-graph.ts +10 -19
  44. package/src/testing/mutation-scope.ts +8 -41
@@ -0,0 +1,137 @@
1
+ import { Effect, Option, Schema } from "effect";
2
+ import { git } from "../core/git.ts";
3
+ import { PACKAGE_MANIFEST } from "./doc-references.ts";
4
+ import { scanMarkdown, type MarkdownLine } from "./prose-matchers.ts";
5
+
6
+ type Pin = { readonly name: string | undefined; readonly version: string; readonly holder: string };
7
+
8
+ const BUN_VERSION = ".bun-version";
9
+
10
+ const Group = Schema.optionalKey(Schema.Record(Schema.String, Schema.String));
11
+ const Manifest = Schema.Struct({
12
+ version: Schema.optionalKey(Schema.String),
13
+ packageManager: Schema.optionalKey(Schema.String),
14
+ engines: Group,
15
+ dependencies: Group,
16
+ devDependencies: Group,
17
+ peerDependencies: Group,
18
+ optionalDependencies: Group,
19
+ });
20
+ const decodeManifest = Schema.decodeUnknownOption(Schema.fromJsonString(Manifest));
21
+ const NO_MANIFEST: typeof Manifest.Type = {};
22
+
23
+ const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/;
24
+ const ONE_VERSION = new RegExp(String.raw`^[\^~=v]?(${VERSION.source})$`);
25
+ const VERSIONS = new RegExp(VERSION.source, "g");
26
+
27
+ function namedPins(group: Readonly<Record<string, string>> | undefined): readonly Pin[] {
28
+ return Object.entries(group ?? {}).flatMap(([name, range]) => {
29
+ const version = ONE_VERSION.exec(range.trim())?.[1];
30
+ return version === undefined ? [] : [{ name, version, holder: PACKAGE_MANIFEST }];
31
+ });
32
+ }
33
+
34
+ function barePins(value: string | undefined, holder: string): readonly Pin[] {
35
+ return [...(value ?? "").matchAll(VERSIONS)].map(([version]) => ({ name: undefined, version, holder }));
36
+ }
37
+
38
+ export function pinsOf(manifest: string, bunVersion: string): readonly Pin[] {
39
+ const { version, packageManager, engines = {}, dependencies, devDependencies, peerDependencies, optionalDependencies } = Option.getOrElse(
40
+ decodeManifest(manifest),
41
+ () => NO_MANIFEST,
42
+ );
43
+ const pins = [
44
+ ...[dependencies, devDependencies, peerDependencies, optionalDependencies].flatMap(namedPins),
45
+ ...[version, packageManager, ...Object.values(engines)].flatMap((value) => barePins(value, PACKAGE_MANIFEST)),
46
+ ...barePins(bunVersion, BUN_VERSION),
47
+ ];
48
+ return pins.filter((pin, index) => pins.findIndex((other) => other.name === pin.name && other.version === pin.version) === index);
49
+ }
50
+
51
+ function escaped(text: string): string {
52
+ return text.replaceAll(/[.*+?^${}()|[\]\\/]/g, String.raw`\$&`);
53
+ }
54
+
55
+ // A longer version that only opens with the pin, as 1.2.30 or 1.2.3-beta, is another version.
56
+ const VERSION_END = String.raw`(?![\w-]|\.\d)`;
57
+
58
+ function patternOf({ name, version }: Pin): RegExp {
59
+ const written =
60
+ name === undefined ? String.raw`(?<![\w.+-])v?${escaped(version)}` : String.raw`(?<![\w@/.-])${escaped(name)}@[\^~]?${escaped(version)}`;
61
+ return new RegExp(written + VERSION_END, "g");
62
+ }
63
+
64
+ const GENERATED_OPEN = /^\s*<!-- generated ([\w-]+):/;
65
+ const GENERATED_CLOSE = /^\s*<!-- end generated ([\w-]+) -->/;
66
+
67
+ function handWritten(lines: readonly MarkdownLine[]): readonly MarkdownLine[] {
68
+ const exempt = new Set<number>();
69
+ let at = 0;
70
+ while (at < lines.length) {
71
+ const opening = lines[at]?.kind === "code" ? null : GENERATED_OPEN.exec(lines[at]?.raw ?? "");
72
+ if (opening === null) {
73
+ at += 1;
74
+ continue;
75
+ }
76
+ const closeAt = lines.findIndex(
77
+ (line, index) => index > at && line.kind !== "code" && GENERATED_CLOSE.exec(line.raw)?.[1] === opening[1],
78
+ );
79
+ if (closeAt < 0) {
80
+ at += 1;
81
+ continue;
82
+ }
83
+ for (let index = at; index <= closeAt; index += 1) exempt.add(index);
84
+ at = closeAt + 1;
85
+ }
86
+ return lines.filter((_, index) => !exempt.has(index));
87
+ }
88
+
89
+ type Copy = { readonly at: number; readonly end: number; readonly pin: Pin; readonly written: string };
90
+
91
+ function namePresent(raw: string, name: string): boolean {
92
+ return new RegExp(String.raw`(?<![\w@/.-])${escaped(name)}(?![\w/.-])`, "i").test(raw);
93
+ }
94
+
95
+ function barePatternFor(version: string): RegExp {
96
+ return new RegExp(String.raw`(?<![\w.+-])v?${escaped(version)}` + VERSION_END, "g");
97
+ }
98
+
99
+ // A bare pin inside a named copy, as 1.2.3 in zod@1.2.3, is the same copy.
100
+ function copiesIn(raw: string, pins: readonly Pin[]): readonly Copy[] {
101
+ const found: Copy[] = [];
102
+ for (const pin of pins) {
103
+ for (const match of raw.matchAll(patternOf(pin))) {
104
+ const at = match.index;
105
+ found.push({ at, end: at + match[0].length, pin, written: match[0] });
106
+ }
107
+ if (pin.name !== undefined && namePresent(raw, pin.name)) {
108
+ for (const match of raw.matchAll(barePatternFor(pin.version))) {
109
+ const at = match.index;
110
+ found.push({ at, end: at + match[0].length, pin, written: match[0] });
111
+ }
112
+ }
113
+ }
114
+ const installed = found.filter((copy) => copy.written.includes("@"));
115
+ return found
116
+ .filter((copy) => copy.written.includes("@") || !installed.some(({ at, end }) => copy.at >= at && copy.end <= end))
117
+ .toSorted((a, b) => a.at - b.at);
118
+ }
119
+
120
+ export function versionFindings(text: string, pins: readonly Pin[], within?: ReadonlySet<number>): readonly { readonly line: number; readonly message: string }[] {
121
+ if (pins.length === 0) return [];
122
+ return handWritten(scanMarkdown(text))
123
+ .filter(({ line }) => within === undefined || within.has(line))
124
+ .flatMap(({ line, raw }) =>
125
+ copiesIn(raw, pins).map(({ written, pin }) => ({
126
+ line,
127
+ message: `carries \`${written}\`, a version ${pin.holder} pins. Name ${pin.holder} instead, or let a build write the line between generated markers`,
128
+ })),
129
+ );
130
+ }
131
+
132
+ const readAt = (root: string, head: string, path: string) =>
133
+ git(["show", `${head}:${path}`], root).pipe(Effect.catchTag("GitFailure", () => Effect.succeed("")));
134
+
135
+ export const pinsAt = Effect.fn("pinsAt")(function* (root: string, head: string) {
136
+ return pinsOf(yield* readAt(root, head, PACKAGE_MANIFEST), yield* readAt(root, head, BUN_VERSION));
137
+ });
package/src/docs/docs.ts CHANGED
@@ -4,6 +4,7 @@ import { ceilingFinding, entryFindings, isAgentFile, maintainingFinding } from "
4
4
  import { rootsOf, snapshotOf, unresolvedIn, type Judging, type Unresolved } from "./doc-references.ts";
5
5
  import { ADR_DIRECTORY, judge, placementOf, placementProblem, recordsOf, speaksToConsumers, type Placement, type Records } from "./doc-rules.ts";
6
6
  import { vanishedNames } from "./doc-names.ts";
7
+ import { pinsAt, versionFindings } from "./doc-versions.ts";
7
8
  import { readTexts, snapshotAt, stillMissing } from "./doc-snapshot.ts";
8
9
  import { changedLines, changedPaths, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
9
10
  import { runMain } from "../core/main.ts";
@@ -107,6 +108,10 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
107
108
  const prose = edited.flatMap(({ path, reader }) =>
108
109
  proseFindings(text(path), reader, changed.get(path)).map(({ line, message }) => ({ path, line, message })),
109
110
  );
111
+ const pins = yield* pinsAt(root, head);
112
+ const versions = edited.flatMap(({ path }) =>
113
+ versionFindings(text(path), pins, changed.get(path)).map(({ line, message }) => ({ path, line, message })),
114
+ );
110
115
  const sentenceAdvisory = edited
111
116
  .flatMap(({ path }) => longSentences(text(path), changed.get(path)).map(({ line, message }) => ({ path, line, message })))
112
117
  .toSorted(inPathOrder);
@@ -132,6 +137,7 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
132
137
  findings: [
133
138
  ...templated.filter((finding) => touched.has(finding.path)),
134
139
  ...prose,
140
+ ...versions,
135
141
  ...references.failing,
136
142
  ...vanished,
137
143
  ...shapes,
@@ -153,6 +159,7 @@ export function report({ held, edited, named, agents, findings, advisory, senten
153
159
  ? [
154
160
  `${NAME}: ${held.length} doc file(s) the range touches hold to their templates`,
155
161
  `${NAME}: ${edited.lines} line(s) the range adds or edits in ${edited.docs} living doc(s) or agent file(s) hold to the prose rules`,
162
+ `${NAME}: no line the range adds or edits copies a version package.json or .bun-version pins`,
156
163
  `${NAME}: the range breaks no path, link or command the ${named} living doc(s) or agent file(s) name`,
157
164
  `${NAME}: the ${agents} agent file(s) hold to the ceiling, and every entry names a tracked path, a link or a command`,
158
165
  ]
@@ -274,7 +274,7 @@ const CHECKS: readonly Check[] = [
274
274
  `carries the machine-read directive \`${match}\`. Fix what the tool is reporting, or stop running the tool on this file`,
275
275
  },
276
276
  {
277
- find: /\bADR[\s-]?\d+|\bdocs\/adr\b|\bRFC[\s-]?\d+|(?:^|[\s(["'`])#\d+\b|[\w.-]+\/[\w.-]+#\d+|\/(?:issues|pull)\/\d+/i,
277
+ find: /\bADR[\s-]?\d+|\bdocs\/adr\b|(?:^|[\s(["'`])#\d+\b|[\w.-]+\/[\w.-]+#\d+|\/(?:issues|pull)\/\d+/i,
278
278
  refusal: (match) =>
279
279
  `points at a record or a ticket (${JSON.stringify(match.trim())}). Drop the pointer: a record is reached by searching docs/adr, and the story of the change goes in the commit message`,
280
280
  },
@@ -1,7 +1,7 @@
1
- import { Console, Effect, FileSystem, Schema } from "effect";
1
+ import { Clock, Config, Console, Effect, FileSystem, Option, Schema } from "effect";
2
2
  import { ChildProcessSpawner } from "effect/process";
3
3
  import { collect } from "../core/git.ts";
4
- import { isRecord, withoutStale } from "./mutation-baseline-saved.ts";
4
+ import { withoutStale } from "./mutation-baseline-saved.ts";
5
5
  import { STRYKER_CONFIGS, stalenessSince } from "./mutation-scope.ts";
6
6
 
7
7
  export class BaselineFailure extends Schema.TaggedError<BaselineFailure>()("BaselineFailure", {
@@ -17,49 +17,18 @@ export const gh = Effect.fn("gh")(function* (args: readonly string[]) {
17
17
 
18
18
  const FULL_CONFIGS = [...STRYKER_CONFIGS, "stryker.preset.js", "bun.lock", ".bun-version"];
19
19
 
20
- const parsedJson = (text: string) =>
21
- Effect.try({
22
- try: (): unknown => JSON.parse(text),
23
- catch: () => new BaselineFailure({ message: "package.json is not JSON" }),
24
- });
20
+ const REFRESH_DAYS = 28;
21
+ const DAY_MS = 86_400_000;
25
22
 
26
- const withoutVersion = (text: string) =>
27
- parsedJson(text).pipe(
28
- Effect.map((parsed) => {
29
- if (!isRecord(parsed)) return undefined;
30
- return JSON.stringify(Object.fromEntries(Object.entries(parsed).filter(([key]) => key !== "version")));
31
- }),
32
- Effect.catchTag("BaselineFailure", () => Effect.succeed(undefined)),
33
- );
23
+ const refreshWindow = (time: number): number => Math.floor(time / (REFRESH_DAYS * DAY_MS));
34
24
 
35
- const patchedPaths = (text: string | undefined): Effect.Effect<readonly string[], BaselineFailure> => {
36
- if (text === undefined) return Effect.succeed([]);
37
- return parsedJson(text).pipe(
38
- Effect.map((parsed) => {
39
- if (!isRecord(parsed)) return [];
40
- const patched = parsed["patchedDependencies"];
41
- if (!isRecord(patched)) return [];
42
- return Object.values(patched).flatMap((value) => (typeof value === "string" ? [value.replace(/^\.\//, "")] : []));
43
- }),
44
- Effect.catchTag("BaselineFailure", (): Effect.Effect<readonly string[]> => Effect.succeed([])),
45
- );
46
- };
25
+ // Each full run reuses what the run before it restored, so a cap on the restored run's age alone caps nothing.
26
+ // Windows fixed from the epoch cap the whole chain: every state in a window was built from runs in that window.
27
+ export function inRefreshWindow(createdAt: string, now: number): boolean {
28
+ return refreshWindow(Date.parse(createdAt)) === refreshWindow(now);
29
+ }
47
30
 
48
- const shownAt = (rev: string, path: string) =>
49
- collect("git", ["show", `${rev}:${path}`]).pipe(
50
- Effect.map(({ stdout, exitCode }) => (exitCode === ChildProcessSpawner.ExitCode(0) ? stdout : undefined)),
51
- Effect.mapError((cause) => new BaselineFailure({ message: cause.message })),
52
- Effect.catchTag("BaselineFailure", () => Effect.succeed(undefined)),
53
- );
54
-
55
- const packageInputs = Effect.fn("packageInputs")(function* (base: string, head: string, paths: ReadonlySet<string>) {
56
- const inputs = new Set<string>();
57
- const [before, after] = yield* Effect.all([shownAt(base, "package.json"), shownAt(head, "package.json")]);
58
- if (paths.has("package.json") && (before === undefined || after === undefined || (yield* withoutVersion(before)) !== (yield* withoutVersion(after))))
59
- inputs.add("package.json");
60
- for (const text of [before, after]) for (const path of yield* patchedPaths(text)) if (paths.has(path)) inputs.add(path);
61
- return inputs;
62
- });
31
+ export type FullRun = { readonly databaseId: number; readonly createdAt: string };
63
32
 
64
33
  const decodeHeadSha = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ headSha: Schema.String })));
65
34
 
@@ -67,16 +36,23 @@ const viewedHead = Effect.fn("viewedHead")(function* (run: number) {
67
36
  return (yield* gh(["run", "view", String(run), "--json", "headSha"]).pipe(Effect.flatMap(decodeHeadSha))).headSha;
68
37
  });
69
38
 
70
- export const pruneFullState = Effect.fn("pruneFullState")(function* (run: number, state: string) {
39
+ export const pruneFullState = Effect.fn("pruneFullState")(function* (run: FullRun, state: string) {
71
40
  const fs = yield* FileSystem.FileSystem;
72
41
  if (!(yield* fs.exists(state))) return;
73
- const base = yield* viewedHead(run);
42
+ const override = yield* Config.Int("CHECKS_MUTATION_BASELINE_NOW_MS").pipe(Config.option);
43
+ let now = yield* Clock.currentTimeMillis;
44
+ if (Option.isSome(override)) now = override.value;
45
+ if (!inRefreshWindow(run.createdAt, now)) {
46
+ yield* fs.remove(state);
47
+ yield* Console.log(`mutation-baseline: dropped the restored full state, run ${run.databaseId} started before the current ${REFRESH_DAYS}-day refresh window`);
48
+ return;
49
+ }
50
+ const base = yield* viewedHead(run.databaseId);
74
51
  const { paths, preload, stale, config } = yield* stalenessSince(base, "HEAD");
75
- const inputs = yield* packageInputs(base, "HEAD", paths);
76
- const changed = [...paths].filter((path) => FULL_CONFIGS.includes(path) || inputs.has(path) || config.has(path)).toSorted();
52
+ const changed = [...paths].filter((path) => FULL_CONFIGS.includes(path) || config.has(path)).toSorted();
77
53
  if (changed.length > 0 || preload !== undefined) {
78
54
  yield* fs.remove(state);
79
- yield* Console.log(`mutation-baseline: dropped the restored full state, ${preload ?? changed.join(" ")} changed since run ${run}`);
55
+ yield* Console.log(`mutation-baseline: dropped the restored full state, ${preload ?? changed.join(" ")} changed since run ${run.databaseId}`);
80
56
  return;
81
57
  }
82
58
  const text = yield* fs.readFileString(state);
@@ -16,7 +16,7 @@ const SavedFiles = Schema.Struct({ [INCREMENTAL]: Schema.Unknown, [REPORT]: Sche
16
16
  export type SavedFiles = typeof SavedFiles.Type;
17
17
  const decodeSavedFiles = Schema.decodeUnknownEffect(Schema.fromJsonString(SavedFiles));
18
18
 
19
- export const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null && !Array.isArray(value);
19
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null && !Array.isArray(value);
20
20
 
21
21
  const withoutReason = (mutant: unknown): unknown => (isRecord(mutant) ? Object.fromEntries(Object.entries(mutant).filter(([key]) => key !== "statusReason")) : mutant);
22
22
 
@@ -46,14 +46,16 @@ const listed = Effect.fn("listed")(function* (event: string | undefined, limit:
46
46
  );
47
47
  });
48
48
 
49
+ const byNewest = (runs: readonly Run[]): readonly Run[] => [...runs].sort((one, other) => other.createdAt.localeCompare(one.createdAt));
50
+
49
51
  export function newestFirst(runs: readonly Run[]): readonly number[] {
50
- return [...runs].sort((one, other) => other.createdAt.localeCompare(one.createdAt)).map((run) => run.databaseId);
52
+ return byNewest(runs).map((run) => run.databaseId);
51
53
  }
52
54
 
53
55
  // All non-pull-request runs newest first: a newer full run holds no baseline, and the newest baseline must survive it.
54
56
  const latestCandidates = Effect.gen(function* () {
55
57
  const newest = (yield* listed(undefined, LATEST_LIMIT)).find((run) => run.event !== "pull_request");
56
- return newest === undefined ? [] : [newest.databaseId];
58
+ return newest === undefined ? [] : [newest];
57
59
  });
58
60
 
59
61
  const latestRuns = listed(undefined, LATEST_LIMIT).pipe(Effect.map((runs) => newestFirst(runs.filter((run) => run.event !== "pull_request"))));
@@ -61,7 +63,7 @@ const latestRuns = listed(undefined, LATEST_LIMIT).pipe(Effect.map((runs) => new
61
63
  // Each full event is listed on its own, because one shared window lets push runs crowd out every full run.
62
64
  const fullCandidates = Effect.gen(function* () {
63
65
  const perEvent = yield* Effect.forEach(FULL_EVENTS, (event) => listed(event, FULL_LIMIT));
64
- return newestFirst(perEvent.flat());
66
+ return byNewest(perEvent.flat());
65
67
  });
66
68
 
67
69
  // A successful run uploaded the files, so the newest unexpired artifact is the one the restore takes.
@@ -108,7 +110,7 @@ const keptLatest = Effect.fn("keptLatest")(function* (runs: readonly number[], s
108
110
  const pruneBaselines = Effect.fn("pruneBaselines")(function* (dryRun: boolean) {
109
111
  const running = yield* Config.Int("GITHUB_RUN_ID").pipe(Config.option);
110
112
  const latest = yield* keptLatest(yield* latestRuns, yield* savedShelf(yield* runnerCache));
111
- const full = yield* keptArtifact(yield* fullCandidates, FULL_ARTIFACT);
113
+ const full = yield* keptArtifact((yield* fullCandidates).map((run) => run.databaseId), FULL_ARTIFACT);
112
114
  const artifacts = yield* everyArtifact();
113
115
  const keeping = [...Option.toArray(latest.kept), ...Option.toArray(full), ...uploadedBy(artifacts, running)];
114
116
  const victims = superseded(artifacts, new Set(keeping.map((keep) => keep.id)));
@@ -341,10 +343,10 @@ function parsed([first, ...rest]: readonly string[]): Command | undefined {
341
343
  return restoreRequest(first, report, extra);
342
344
  }
343
345
 
344
- const restoredRun = Effect.fn("restoredRun")(function* (run: number, name: string, root: Option.Option<string>, shelf: Option.Option<string>, request: Request) {
345
- if (Option.isSome(shelf) && (yield* restoredFromSaved(shelf.value, run, request))) return true;
346
- const artifact = yield* artifactOf(run, name).pipe(Effect.catch(warned(Option.none<Artifact>())));
347
- if (Option.isNone(artifact) || !(yield* restoredFrom(run, artifact.value, root, request))) return false;
346
+ const restoredRun = Effect.fn("restoredRun")(function* (run: Run, name: string, root: Option.Option<string>, shelf: Option.Option<string>, request: Request) {
347
+ if (Option.isSome(shelf) && (yield* restoredFromSaved(shelf.value, run.databaseId, request))) return true;
348
+ const artifact = yield* artifactOf(run.databaseId, name).pipe(Effect.catch(warned(Option.none<Artifact>())));
349
+ if (Option.isNone(artifact) || !(yield* restoredFrom(run.databaseId, artifact.value, root, request))) return false;
348
350
  const incremental = request.full ? request.optional[0] : undefined;
349
351
  if (incremental !== undefined) yield* pruneFullState(run, incremental.to);
350
352
  return true;
@@ -18,20 +18,11 @@ export function resolved(directory: string, spec: string): string {
18
18
  }
19
19
 
20
20
  // Bun takes the file a specifier names, then one with an extension added, and only then the directory's index.
21
- export function importedFile(path: string, files: ReadonlySet<string>, mains: ReadonlyMap<string, string> = new Map()): readonly string[] {
22
- const probe = (target: string): readonly string[] => {
23
- const bare = target.replace(/\.m?js$/, "");
24
- return [target, ...CANDIDATE_EXTENSIONS.map((extension) => bare + extension)].filter((file) => files.has(file));
25
- };
26
- const index = (target: string): readonly string[] =>
27
- CANDIDATE_EXTENSIONS.map((extension) => `${target}/index${extension}`).filter((file) => files.has(file));
28
- const direct = probe(path);
21
+ export function importedFile(path: string, files: ReadonlySet<string>): readonly string[] {
22
+ const bare = path.replace(/\.m?js$/, "");
23
+ const direct = [path, ...CANDIDATE_EXTENSIONS.map((extension) => bare + extension)].filter((file) => files.has(file));
29
24
  if (direct.length > 0) return direct;
30
- const main = mains.get(path);
31
- if (main === undefined) return index(path);
32
- const manifest = path === "" ? "package.json" : `${path}/package.json`;
33
- if (main === manifest) return [manifest, ...index(path)];
34
- return [manifest, ...probe(main), ...index(main), ...index(path)];
25
+ return CANDIDATE_EXTENSIONS.map((extension) => `${path}/index${extension}`).filter((file) => files.has(file));
35
26
  }
36
27
 
37
28
  function fixtureNeedles(changed: readonly string[]): readonly Needle[] {
@@ -54,13 +45,13 @@ function mentions(line: string, { text, kind }: Needle): boolean {
54
45
 
55
46
  type Edge = { readonly importer: string; readonly imported: string };
56
47
 
57
- export function importsOf(file: string, text: string, files: ReadonlySet<string>, mains: ReadonlyMap<string, string> = new Map()): readonly string[] {
48
+ export function importsOf(file: string, text: string, files: ReadonlySet<string>): readonly string[] {
58
49
  const directory = file.slice(0, Math.max(file.lastIndexOf("/"), 0));
59
- return [...text.matchAll(IMPORT)].flatMap(([, throughImport = "", throughRequire = ""]) => importedFile(resolved(directory, throughImport || throughRequire), files, mains));
50
+ return [...text.matchAll(IMPORT)].flatMap(([, throughImport = "", throughRequire = ""]) => importedFile(resolved(directory, throughImport || throughRequire), files));
60
51
  }
61
52
 
62
- const edgesOf = (file: string, text: string, files: ReadonlySet<string>, mains: ReadonlyMap<string, string> = new Map()): readonly Edge[] =>
63
- importsOf(file, text, files, mains).map((imported) => ({ importer: file, imported }));
53
+ const edgesOf = (file: string, text: string, files: ReadonlySet<string>): readonly Edge[] =>
54
+ importsOf(file, text, files).map((imported) => ({ importer: file, imported }));
64
55
 
65
56
  function fixtureNamedIn(text: string, needles: readonly Needle[]): string | undefined {
66
57
  if (needles.length === 0) return undefined;
@@ -85,7 +76,7 @@ function spread(reached: Map<string, string>, edges: readonly Edge[]): void {
85
76
  }
86
77
 
87
78
  // Maps each file that reads a changed path, through an import or the fixture's path, to that changed path.
88
- export function readersOf(sources: ReadonlyMap<string, string>, changed: readonly string[], mains: ReadonlyMap<string, string> = new Map()): Map<string, string> {
79
+ export function readersOf(sources: ReadonlyMap<string, string>, changed: readonly string[]): Map<string, string> {
89
80
  const reached = new Map(changed.map((path) => [path, path]));
90
81
  const needles = fixtureNeedles(changed);
91
82
  for (const [file, text] of sources) {
@@ -93,6 +84,6 @@ export function readersOf(sources: ReadonlyMap<string, string>, changed: readonl
93
84
  if (fixture !== undefined) reached.set(file, fixture);
94
85
  }
95
86
  const files = new Set([...sources.keys(), ...changed]);
96
- spread(reached, [...sources].flatMap(([file, text]) => edgesOf(file, text, files, mains)));
87
+ spread(reached, [...sources].flatMap(([file, text]) => edgesOf(file, text, files)));
97
88
  return new Map([...reached].filter(([file, via]) => file !== via));
98
89
  }
@@ -3,7 +3,6 @@ import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
3
  import { escape, minimatch } from "minimatch";
4
4
  import { changedPaths, git, pathsAt, rangeEnds, type Change } from "../core/git.ts";
5
5
  import { runMain, Usage } from "../core/main.ts";
6
- import { isRecord } from "./mutation-baseline-saved.ts";
7
6
  import { importedFile, importsOf, readersOf, resolved, TEST_TREE } from "./mutation-scope-graph.ts";
8
7
 
9
8
  const NAME = "mutation-scope";
@@ -180,13 +179,13 @@ const preloadsAt = Effect.fn("preloadsAt")(function* (rev: string, paths: Readon
180
179
  return (typeof preload === "string" ? [preload] : preload).map((entry) => resolved("", entry));
181
180
  });
182
181
 
183
- const transitivelyImported = Effect.fn("transitivelyImported")(function* (rev: string, roots: readonly string[], paths: ReadonlySet<string>, mains: ReadonlyMap<string, string> = new Map()) {
182
+ const transitivelyImported = Effect.fn("transitivelyImported")(function* (rev: string, roots: readonly string[], paths: ReadonlySet<string>) {
184
183
  const loaded = new Set<string>();
185
184
  for (let next = [...roots]; next.length > 0; ) {
186
185
  for (const file of next) loaded.add(file);
187
186
  const read = (file: string) =>
188
187
  git(["show", `${rev}:${file}`]).pipe(
189
- Effect.map((text) => importsOf(file, text, paths, mains)),
188
+ Effect.map((text) => importsOf(file, text, paths)),
190
189
  Effect.catchTag("GitFailure", (): Effect.Effect<readonly string[]> => Effect.succeed([])),
191
190
  );
192
191
  const imported = (yield* Effect.forEach(next, read, { concurrency: 16 })).flat();
@@ -195,39 +194,8 @@ const transitivelyImported = Effect.fn("transitivelyImported")(function* (rev: s
195
194
  return loaded;
196
195
  });
197
196
 
198
- const loadedByPreloads = Effect.fn("loadedByPreloads")(function* (rev: string, preloads: readonly string[], paths: ReadonlySet<string>, mains: ReadonlyMap<string, string> = new Map()) {
199
- return yield* transitivelyImported(rev, preloads.flatMap((preload) => importedFile(preload, paths, mains)), paths, mains);
200
- });
201
-
202
- const mainOf = (text: string) =>
203
- Effect.try({
204
- try: (): unknown => JSON.parse(text),
205
- catch: () => new ScopeError({ message: "package.json does not parse as JSON holding a main entry" }),
206
- }).pipe(
207
- Effect.map((parsed) => {
208
- if (!isRecord(parsed)) return undefined;
209
- const main = parsed["main"];
210
- return typeof main === "string" && main.trim() !== "" ? main : undefined;
211
- }),
212
- Effect.catchTag("ScopeError", () => Effect.succeed(undefined)),
213
- );
214
-
215
- const readMains = Effect.fn("readMains")(function* (rev: string, paths: ReadonlySet<string>) {
216
- const packages = [...paths].filter((path) => path === "package.json" || path.endsWith("/package.json"));
217
- const entries = yield* Effect.forEach(
218
- packages,
219
- (file) => git(["show", `${rev}:${file}`]).pipe(Effect.map((text): readonly [string, string] => [file, text]), Effect.catchTag("GitFailure", () => Effect.succeed(undefined))),
220
- { concurrency: 16 },
221
- );
222
- const mains = new Map<string, string>();
223
- for (const entry of entries) {
224
- if (entry === undefined) continue;
225
- const [file, text] = entry;
226
- const main = yield* mainOf(text);
227
- const directory = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
228
- mains.set(directory, main === undefined ? file : resolved(directory, main));
229
- }
230
- return mains;
197
+ const loadedByPreloads = Effect.fn("loadedByPreloads")(function* (rev: string, preloads: readonly string[], paths: ReadonlySet<string>) {
198
+ return yield* transitivelyImported(rev, preloads.flatMap((preload) => importedFile(preload, paths)), paths);
231
199
  });
232
200
 
233
201
  const readFiles = Effect.fn("readFiles")(function* (files: readonly string[]) {
@@ -279,17 +247,16 @@ export const stalenessSince = Effect.fn("stalenessSince")(function* (base: strin
279
247
  const atBase = new Set(yield* pathsAt(base, []));
280
248
  const atHead = new Set(yield* pathsAt(head, []));
281
249
  const preloads = { atBase: yield* preloadsAt(base, atBase), atHead: yield* preloadsAt(head, atHead) };
282
- const mains = new Map([...(yield* readMains(base, atBase)), ...(yield* readMains(head, atHead))]);
283
- const loaded = new Set([...(yield* loadedByPreloads(head, preloads.atHead, atHead, mains)), ...(yield* loadedByPreloads(base, preloads.atBase, atBase, mains))]);
250
+ const loaded = new Set([...(yield* loadedByPreloads(head, preloads.atHead, atHead)), ...(yield* loadedByPreloads(base, preloads.atBase, atBase))]);
284
251
  const preload = preloadChange(picked.paths, preloads, loaded);
285
252
  const active = { atBase: STRYKER_CONFIGS.find((name) => atBase.has(name)), atHead: STRYKER_CONFIGS.find((name) => atHead.has(name)) };
286
253
  const config = new Set([
287
- ...(yield* transitivelyImported(head, active.atHead === undefined ? [] : [active.atHead], atHead, mains)),
288
- ...(yield* transitivelyImported(base, active.atBase === undefined ? [] : [active.atBase], atBase, mains)),
254
+ ...(yield* transitivelyImported(head, active.atHead === undefined ? [] : [active.atHead], atHead)),
255
+ ...(yield* transitivelyImported(base, active.atBase === undefined ? [] : [active.atBase], atBase)),
289
256
  ]);
290
257
  if (preload !== undefined) return { paths: picked.paths, preload, stale: [], config };
291
258
  if (picked.paths.size === 0) return { paths: picked.paths, preload: undefined, stale: [], config };
292
- const readers = [...readersOf(yield* readImportGraph(atHead), [...picked.paths], mains)]
259
+ const readers = [...readersOf(yield* readImportGraph(atHead), [...picked.paths])]
293
260
  .filter(([reader]) => isUnitTest(reader))
294
261
  .map(([reader]) => reader);
295
262
  const stale = [...new Set([...[...picked.paths].filter(isUnitTest), ...readers])].toSorted();