repo-contract 0.1.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 (145) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +967 -0
  4. package/dist/.dts/config/define-repo-contract.d.ts +36 -0
  5. package/dist/.dts/config/define-repo-contract.d.ts.map +1 -0
  6. package/dist/.dts/config/tokenize-command.d.ts +33 -0
  7. package/dist/.dts/config/tokenize-command.d.ts.map +1 -0
  8. package/dist/.dts/config/validate-config.d.ts +32 -0
  9. package/dist/.dts/config/validate-config.d.ts.map +1 -0
  10. package/dist/.dts/errors.d.ts +155 -0
  11. package/dist/.dts/errors.d.ts.map +1 -0
  12. package/dist/.dts/evidence/build-evidence.d.ts +26 -0
  13. package/dist/.dts/evidence/build-evidence.d.ts.map +1 -0
  14. package/dist/.dts/execution/abort-signals.d.ts +29 -0
  15. package/dist/.dts/execution/abort-signals.d.ts.map +1 -0
  16. package/dist/.dts/execution/concurrency-pool.d.ts +14 -0
  17. package/dist/.dts/execution/concurrency-pool.d.ts.map +1 -0
  18. package/dist/.dts/execution/dependency-scheduler.d.ts +30 -0
  19. package/dist/.dts/execution/dependency-scheduler.d.ts.map +1 -0
  20. package/dist/.dts/execution/process-tree.d.ts +48 -0
  21. package/dist/.dts/execution/process-tree.d.ts.map +1 -0
  22. package/dist/.dts/execution/run-checks.d.ts +29 -0
  23. package/dist/.dts/execution/run-checks.d.ts.map +1 -0
  24. package/dist/.dts/execution/spawn-check.d.ts +30 -0
  25. package/dist/.dts/execution/spawn-check.d.ts.map +1 -0
  26. package/dist/.dts/index.d.ts +13 -0
  27. package/dist/.dts/index.d.ts.map +1 -0
  28. package/dist/.dts/parsing/parse-json.d.ts +8 -0
  29. package/dist/.dts/parsing/parse-json.d.ts.map +1 -0
  30. package/dist/.dts/parsing/parse-output.d.ts +10 -0
  31. package/dist/.dts/parsing/parse-output.d.ts.map +1 -0
  32. package/dist/.dts/parsing/parse-text.d.ts +8 -0
  33. package/dist/.dts/parsing/parse-text.d.ts.map +1 -0
  34. package/dist/.dts/parsing/parse-yaml.d.ts +21 -0
  35. package/dist/.dts/parsing/parse-yaml.d.ts.map +1 -0
  36. package/dist/.dts/policy/run-policies.d.ts +38 -0
  37. package/dist/.dts/policy/run-policies.d.ts.map +1 -0
  38. package/dist/.dts/presets/arethetypeswrong.d.ts +39 -0
  39. package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -0
  40. package/dist/.dts/presets/broken-links.d.ts +16 -0
  41. package/dist/.dts/presets/broken-links.d.ts.map +1 -0
  42. package/dist/.dts/presets/commitlint.d.ts +22 -0
  43. package/dist/.dts/presets/commitlint.d.ts.map +1 -0
  44. package/dist/.dts/presets/dead-code.d.ts +23 -0
  45. package/dist/.dts/presets/dead-code.d.ts.map +1 -0
  46. package/dist/.dts/presets/duplication.d.ts +14 -0
  47. package/dist/.dts/presets/duplication.d.ts.map +1 -0
  48. package/dist/.dts/presets/e2e.d.ts +4 -0
  49. package/dist/.dts/presets/e2e.d.ts.map +1 -0
  50. package/dist/.dts/presets/format.d.ts +4 -0
  51. package/dist/.dts/presets/format.d.ts.map +1 -0
  52. package/dist/.dts/presets/index.d.ts +31 -0
  53. package/dist/.dts/presets/index.d.ts.map +1 -0
  54. package/dist/.dts/presets/license.d.ts +4 -0
  55. package/dist/.dts/presets/license.d.ts.map +1 -0
  56. package/dist/.dts/presets/lint.d.ts +20 -0
  57. package/dist/.dts/presets/lint.d.ts.map +1 -0
  58. package/dist/.dts/presets/markdownlint.d.ts +23 -0
  59. package/dist/.dts/presets/markdownlint.d.ts.map +1 -0
  60. package/dist/.dts/presets/publint.d.ts +13 -0
  61. package/dist/.dts/presets/publint.d.ts.map +1 -0
  62. package/dist/.dts/presets/security-deps.d.ts +4 -0
  63. package/dist/.dts/presets/security-deps.d.ts.map +1 -0
  64. package/dist/.dts/presets/security-secrets.d.ts +4 -0
  65. package/dist/.dts/presets/security-secrets.d.ts.map +1 -0
  66. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +13 -0
  67. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +1 -0
  68. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +23 -0
  69. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +1 -0
  70. package/dist/.dts/presets/shared/missing-dependency.d.ts +19 -0
  71. package/dist/.dts/presets/shared/missing-dependency.d.ts.map +1 -0
  72. package/dist/.dts/presets/shared/read-json-report.d.ts +33 -0
  73. package/dist/.dts/presets/shared/read-json-report.d.ts.map +1 -0
  74. package/dist/.dts/presets/shared/terminal-status.d.ts +25 -0
  75. package/dist/.dts/presets/shared/terminal-status.d.ts.map +1 -0
  76. package/dist/.dts/presets/shared/vitest-json-policy.d.ts +20 -0
  77. package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +1 -0
  78. package/dist/.dts/presets/stylelint.d.ts +17 -0
  79. package/dist/.dts/presets/stylelint.d.ts.map +1 -0
  80. package/dist/.dts/presets/test.d.ts +4 -0
  81. package/dist/.dts/presets/test.d.ts.map +1 -0
  82. package/dist/.dts/presets/typecheck.d.ts +4 -0
  83. package/dist/.dts/presets/typecheck.d.ts.map +1 -0
  84. package/dist/.dts/run-repo-contract.d.ts +38 -0
  85. package/dist/.dts/run-repo-contract.d.ts.map +1 -0
  86. package/dist/.dts/types.d.ts +324 -0
  87. package/dist/.dts/types.d.ts.map +1 -0
  88. package/dist/index.cjs +46 -0
  89. package/dist/index.cjs.map +1 -0
  90. package/dist/index.d.cts +1 -0
  91. package/dist/index.d.ts +1 -0
  92. package/dist/index.js +11 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/presets.cjs +30 -0
  95. package/dist/presets.cjs.map +1 -0
  96. package/dist/presets.d.cts +1 -0
  97. package/dist/presets.d.ts +1 -0
  98. package/dist/presets.js +13 -0
  99. package/dist/presets.js.map +1 -0
  100. package/package.json +192 -0
  101. package/presets/package.json +5 -0
  102. package/schemas/evidence.schema.json +253 -0
  103. package/schemas/verdict.schema.json +66 -0
  104. package/src/config/define-repo-contract.ts +38 -0
  105. package/src/config/tokenize-command.ts +214 -0
  106. package/src/config/validate-config.ts +368 -0
  107. package/src/errors.ts +229 -0
  108. package/src/evidence/build-evidence.ts +91 -0
  109. package/src/execution/abort-signals.ts +56 -0
  110. package/src/execution/concurrency-pool.ts +64 -0
  111. package/src/execution/dependency-scheduler.ts +216 -0
  112. package/src/execution/process-tree.ts +107 -0
  113. package/src/execution/run-checks.ts +348 -0
  114. package/src/execution/spawn-check.ts +494 -0
  115. package/src/index.ts +44 -0
  116. package/src/parsing/parse-json.ts +18 -0
  117. package/src/parsing/parse-output.ts +26 -0
  118. package/src/parsing/parse-text.ts +10 -0
  119. package/src/parsing/parse-yaml.ts +40 -0
  120. package/src/policy/run-policies.ts +261 -0
  121. package/src/presets/arethetypeswrong.ts +116 -0
  122. package/src/presets/broken-links.ts +95 -0
  123. package/src/presets/commitlint.ts +77 -0
  124. package/src/presets/dead-code.ts +223 -0
  125. package/src/presets/duplication.ts +137 -0
  126. package/src/presets/e2e.ts +144 -0
  127. package/src/presets/format.ts +25 -0
  128. package/src/presets/index.ts +30 -0
  129. package/src/presets/license.ts +90 -0
  130. package/src/presets/lint.ts +116 -0
  131. package/src/presets/markdownlint.ts +105 -0
  132. package/src/presets/publint.ts +38 -0
  133. package/src/presets/security-deps.ts +142 -0
  134. package/src/presets/security-secrets.ts +93 -0
  135. package/src/presets/shared/error-warning-pass-policy.ts +39 -0
  136. package/src/presets/shared/exit-code-fail-rationale.ts +34 -0
  137. package/src/presets/shared/missing-dependency.ts +31 -0
  138. package/src/presets/shared/read-json-report.ts +46 -0
  139. package/src/presets/shared/terminal-status.ts +70 -0
  140. package/src/presets/shared/vitest-json-policy.ts +95 -0
  141. package/src/presets/stylelint.ts +101 -0
  142. package/src/presets/test.ts +19 -0
  143. package/src/presets/typecheck.ts +25 -0
  144. package/src/run-repo-contract.ts +80 -0
  145. package/src/types.ts +340 -0
@@ -0,0 +1,348 @@
1
+ import { UnknownCheckIdError } from "../errors.js"
2
+ import type {
3
+ CheckDefinition,
4
+ CheckEvidence,
5
+ CheckSchema,
6
+ RunRepoContractOptions,
7
+ } from "../types.js"
8
+ import { composeSignals } from "./abort-signals.js"
9
+ import { runWithConcurrency } from "./concurrency-pool.js"
10
+ import { runWithConcurrencyGraph } from "./dependency-scheduler.js"
11
+ import type { ActiveCheckHandle } from "./spawn-check.js"
12
+ import { SIGKILL_GRACE_PERIOD_MS, spawnCheck } from "./spawn-check.js"
13
+
14
+ /**
15
+ * One check's id, its original definition, and its raw execution evidence,
16
+ * threaded together as a triple rather than three separately-keyed maps --
17
+ * every later stage (parsing, policy evaluation) consumes this directly
18
+ * instead of re-looking a check up by id, which under `noUncheckedIndexedAccess`
19
+ * would otherwise force handling an "undefined" case that can't actually
20
+ * happen (every checkId here comes from the same `Object.entries(checks)`
21
+ * that produced it).
22
+ */
23
+ export type CheckExecutionEntry = readonly [string, CheckDefinition, CheckEvidence]
24
+
25
+ const TERMINATION_SIGNALS: readonly NodeJS.Signals[] = ["SIGINT", "SIGTERM"]
26
+
27
+ // Extra headroom over SIGKILL_GRACE_PERIOD_MS itself: killWithEscalation arms its own SIGKILL
28
+ // follow-up timer with that exact delay, in the same synchronous pass as the self-terminate timer
29
+ // below -- waiting only that same delay would race Node's timer ordering rather than reliably
30
+ // outlasting it. This margin ensures every active check's scheduled SIGKILL follow-up has already
31
+ // had the chance to run and observe the process exit before this process re-signals itself.
32
+ // Exported so a unit test can pin its exact value against SIGKILL_GRACE_PERIOD_MS without needing
33
+ // to trigger the real, only-testable-in-a-child-process SIGINT handler this constant feeds (see
34
+ // installTerminationHandlers below).
35
+ export const SELF_TERMINATE_DELAY_MS = SIGKILL_GRACE_PERIOD_MS + 250
36
+
37
+ /**
38
+ * While any check is in flight, installs a handler for SIGINT/SIGTERM that
39
+ * kills every currently-active check's process tree before the host process
40
+ * itself terminates -- otherwise a Ctrl+C during `runRepoContract()` would
41
+ * leave every spawned check (and their own descendants) running as orphans.
42
+ * After cleanup, removes its own handler and re-sends the signal to this
43
+ * process so default termination behavior (or any other listener the host
44
+ * application registered) still applies -- this never itself decides to
45
+ * keep the process alive or call `process.exit()`, it only ensures cleanup
46
+ * happens first.
47
+ *
48
+ * Also aborts `hostAbortController` before killing anything: `activeHandles` only ever contains
49
+ * checks that have *already* spawned, so killing every current member does nothing to stop the
50
+ * concurrency pool/scheduler from immediately launching the *next* queued check the instant a
51
+ * killed one's promise settles -- by then this handler has already returned and `uninstall()` has
52
+ * already removed these very listeners, so nothing would be left to kill that newly-spawned
53
+ * process. Aborting `hostAbortController` first means `runChecks`' composed `runSignal` is already
54
+ * `aborted` by the time any such check is considered, so `spawnCheck` takes its documented
55
+ * already-aborted path (see spawn-check.ts's own doc comment) and resolves without spawning at
56
+ * all, instead of racing this cleanup.
57
+ * @param activeHandles - the currently-running checks' kill handles, killed with the received signal on termination
58
+ * @param hostAbortController - aborted before any active check is killed, so a check still queued behind the concurrency limit never spawns instead of racing this cleanup
59
+ * @returns a function that removes the installed signal handlers without killing anything, for cleanup once no check is in flight
60
+ */
61
+ function installTerminationHandlers(
62
+ activeHandles: Set<ActiveCheckHandle>,
63
+ hostAbortController: AbortController,
64
+ ): () => void {
65
+ const handlers = new Map<NodeJS.Signals, () => void>()
66
+
67
+ for (const signal of TERMINATION_SIGNALS) {
68
+ /* v8 ignore start -- this body only runs when the host *process* actually
69
+ * receives a real SIGINT/SIGTERM. test/unit/execution/run-checks.test.ts's
70
+ * "SIGINT while checks are in flight..." test exercises it for real, but
71
+ * necessarily in a separate child process (sending SIGINT to this test
72
+ * worker's own process would kill the test runner) -- v8 coverage is
73
+ * per-process, so that real exercise is invisible here. Same reasoning
74
+ * applies to Stryker's mutation testing, which only observes this
75
+ * process's own test run. */
76
+ // Stryker disable BlockStatement,CallExpression,ConditionalExpression,EqualityOperator,BooleanLiteral -- this handler body only executes when the host process receives a real SIGINT/SIGTERM; the test exercising it necessarily runs in a separate child process (v8 coverage and Stryker's own instrumentation are both per-process), so mutating this body -- including the hadActiveChecks/!hadActiveChecks branch below -- always reports an uncoverable-looking survivor rather than a real gap.
77
+ const handler = (): void => {
78
+ // Must run before killing anything currently active (see this
79
+ // function's own doc comment): synchronous, so every check the
80
+ // scheduler considers launching from this point forward -- including
81
+ // one whose turn comes only after a check killed below actually exits
82
+ // -- already observes `aborted === true`.
83
+ hostAbortController.abort()
84
+ // Captured before killing anything: `activeHandles` only loses entries
85
+ // once each check's own process actually exits (asynchronously, via
86
+ // spawn-check.ts's cleanup), so it still reads non-empty immediately
87
+ // after calling kill() on every one of them below -- this check must
88
+ // run first to know whether there's anything worth waiting on.
89
+ const hadActiveChecks = activeHandles.size > 0
90
+ for (const handle of activeHandles) handle.kill(signal)
91
+ uninstall()
92
+ if (!hadActiveChecks) {
93
+ process.kill(process.pid, signal)
94
+ return
95
+ }
96
+ // Previously this re-signaled (and thereby terminated, via Node's
97
+ // default disposition once uninstall() removed this process's own
98
+ // listener) the host process immediately after arming the kills above
99
+ // -- starving every check's own SIGKILL-escalation timer
100
+ // (spawn-check.ts's killWithEscalation) of the time it needs to fire,
101
+ // orphaning any check whose command traps or ignores the initial
102
+ // signal. Waiting SELF_TERMINATE_DELAY_MS first gives every one of
103
+ // those timers a real chance to run before this process exits. This
104
+ // delay is sized against `SIGKILL_GRACE_PERIOD_MS` alone -- it is
105
+ // deliberately not tied to whether `runChecks()`'s own promise (or any
106
+ // work a caller does with its result, e.g. persisting evidence) has
107
+ // resolved by then: a real Ctrl+C is a request to stop now, and this
108
+ // process is going to re-terminate itself via the signal re-sent below
109
+ // regardless of what the caller is doing, the same as if this handler
110
+ // did not exist at all -- the only thing this delay buys is letting
111
+ // already-spawned child processes actually die first.
112
+ setTimeout(() => {
113
+ process.kill(process.pid, signal)
114
+ }, SELF_TERMINATE_DELAY_MS)
115
+ }
116
+ // Stryker restore all
117
+ /* v8 ignore stop */
118
+ handlers.set(signal, handler)
119
+ process.once(signal, handler)
120
+ }
121
+
122
+ /**
123
+ *
124
+ */
125
+ function uninstall(): void {
126
+ for (const [signal, handler] of handlers) process.removeListener(signal, handler)
127
+ }
128
+
129
+ return uninstall
130
+ }
131
+
132
+ /**
133
+ * Resolves `requestedChecks` plus every check transitively required to satisfy their own
134
+ * `dependsOn` -- never `isolated`, whose implied positional edges (see `dependencyIndexesFor`
135
+ * below) are scheduling-only and must not pull unrelated checks into a partial run just because
136
+ * one happens to sit after an isolated barrier (see `CheckDefinitionConfig.isolated`'s own doc
137
+ * comment). No cycle guard is needed: `validateRepoContractConfig` already guarantees, before
138
+ * `runChecks` is ever reached, that every `dependsOn` id names a check declared *earlier* than the
139
+ * check declaring it -- a cycle is structurally impossible once every edge points backward.
140
+ * @param checks - the full set of configured checks, keyed by id, in declaration order
141
+ * @param requestedChecks - the check ids explicitly requested for this run
142
+ * @returns the requested checks plus every transitive `dependsOn` dependency, in declaration order
143
+ */
144
+ function resolveCheckDependencies(
145
+ checks: Record<string, CheckDefinition>,
146
+ requestedChecks: readonly string[],
147
+ ): [string, CheckDefinition][] {
148
+ const required = new Set<string>()
149
+
150
+ const visit = (checkId: string): void => {
151
+ // Real, tested logic -- "throws when a requested subset's dependsOn forms a cycle" in
152
+ // run-checks.test.ts proves this guard fires correctly for the one caller shape that can still
153
+ // reach a cycle here (runChecks invoked directly, bypassing validateRepoContractConfig's own
154
+ // backward-reference check). In a plain, uninstrumented process, disabling it and reproducing
155
+ // the resulting unbounded recursion throws a stack-overflow RangeError in single-digit
156
+ // milliseconds (confirmed empirically, not assumed). Mutation-tested here it is not: disabling
157
+ // this condition turns a would-be 2-node cycle into unbounded mutual recursion between `visit`
158
+ // calls, and Stryker's own per-statement coverage instrumentation adds enough overhead per
159
+ // recursive frame that reaching V8's actual stack limit -- still the only thing that stops it --
160
+ // takes long enough to exceed stryker.config.mjs's timeoutMS. A "Timeout" verdict under
161
+ // instrumentation here reflects that mismatch between wall-clock budget and V8's stack limit,
162
+ // not a gap in test coverage -- a known limitation of mutation testing for recursion/loop-guard
163
+ // code generally, not specific to this function.
164
+ // Stryker disable next-line ConditionalExpression -- disabling this in an uninstrumented process empirically produces unbounded mutual recursion that hits V8's stack limit in single-digit milliseconds, but under Stryker's own per-statement instrumentation overhead reaching that same stack limit takes long enough to exceed stryker.config.mjs's timeoutMS, surfacing as a "Timeout" verdict rather than a real test gap.
165
+ if (required.has(checkId)) return
166
+
167
+ const check = checks[checkId]
168
+ if (!check) {
169
+ throw new UnknownCheckIdError(checkId)
170
+ }
171
+
172
+ required.add(checkId)
173
+ for (const dependency of check.dependsOn ?? []) {
174
+ visit(dependency)
175
+ }
176
+ }
177
+
178
+ for (const checkId of requestedChecks) visit(checkId)
179
+
180
+ return Object.entries(checks).filter(([checkId]) => required.has(checkId))
181
+ }
182
+
183
+ /**
184
+ * Executes every resolved check with at most `concurrency` running at once,
185
+ * returning each check's raw evidence keyed by check id. For a full run
186
+ * (`options.checks` omitted), every configured check id is resolved and
187
+ * therefore appears in the result exactly once, regardless of whether it
188
+ * ever actually spawned (see spawnCheck's documentation for the pre-aborted
189
+ * case). When `options.checks` restricts execution to a subset, only the
190
+ * requested ids and their transitive `dependsOn` are resolved (see
191
+ * `resolveCheckDependencies` above) -- every other configured check id is
192
+ * simply absent from the result, not present with some placeholder value.
193
+ * @param checks - the full set of configured checks, keyed by id
194
+ * @param concurrency - the maximum number of checks to run in parallel at once
195
+ * @param options - run options; `options.checks` restricts execution to those ids (plus their dependencies), `options.signal` cancels the whole run
196
+ * @returns each executed check's id, definition, and raw evidence, one entry per resolved check regardless of whether it actually spawned
197
+ */
198
+ export async function runChecks(
199
+ checks: CheckSchema,
200
+ concurrency: number,
201
+ options?: RunRepoContractOptions,
202
+ ): Promise<readonly CheckExecutionEntry[]> {
203
+ const entries = options?.checks
204
+ ? resolveCheckDependencies(checks, options.checks)
205
+ : Object.entries(checks)
206
+
207
+ const activeHandles = new Set<ActiveCheckHandle>()
208
+ const hostAbortController = new AbortController()
209
+ const uninstall = installTerminationHandlers(activeHandles, hostAbortController)
210
+ // Composed rather than passing `options?.signal` straight through: a host-process
211
+ // SIGINT/SIGTERM must abort every not-yet-spawned check exactly like an
212
+ // explicit `options.signal` cancellation already does, or a check queued behind the
213
+ // concurrency limit would spawn unsupervised after installTerminationHandlers' own
214
+ // listeners are already removed (see that function's doc comment). The `if` branch below
215
+ // remains fully covered without further exemption: removing `options.signal` from that array
216
+ // breaks "a global AbortSignal fired mid-run..." (run-checks.test.ts), which supplies
217
+ // `options.signal` and asserts on its effect directly, in-process.
218
+ const { signal: runSignal, dispose: disposeRunSignal } = composeSignals(
219
+ options?.signal !== undefined
220
+ ? [options.signal, hostAbortController.signal]
221
+ : // The `else` branch's inclusion of `hostAbortController.signal` is only observably
222
+ // meaningful once `hostAbortController.abort()` is actually called, which happens
223
+ // exclusively inside installTerminationHandlers' real-OS-signal handler above -- and per
224
+ // that handler's own doc comment, that handler body only executes when the host
225
+ // *process* actually receives a real SIGINT/SIGTERM, which cannot be triggered from this
226
+ // same in-process test worker without killing it (v8/Stryker coverage is per-process, so
227
+ // the real exercise -- run-checks.test.ts's "does not spawn a check still queued behind
228
+ // the concurrency limit..." test, run in a separate child process -- is invisible here
229
+ // for the identical reason already documented on that handler).
230
+ // Stryker disable next-line ArrayDeclaration -- see comment immediately above: only provable via a real signal delivered to a separate child process, invisible to this process's own Stryker instrumentation, the same per-process-invisible situation already documented on installTerminationHandlers' handler.
231
+ [hostAbortController.signal],
232
+ )
233
+
234
+ const worker = async ([checkId, check]: readonly [
235
+ string,
236
+ CheckDefinition,
237
+ ]): Promise<CheckExecutionEntry> => {
238
+ const evidence = await spawnCheck(checkId, check, runSignal, activeHandles)
239
+ return [checkId, check, evidence]
240
+ }
241
+
242
+ try {
243
+ // A fully-disconnected graph (no check declares dependsOn or isolated --
244
+ // the default) takes the exact same runWithConcurrency path as before
245
+ // this feature existed, not a "generalized but behaviorally equivalent"
246
+ // one -- zero risk to the common case. This choice of code path is
247
+ // provably unobservable either way: dependency-scheduler.test.ts's own
248
+ // "zero-edges equivalence" suite confirms runWithConcurrencyGraph
249
+ // behaves identically to runWithConcurrency for a graph with no real
250
+ // edges (order, concurrency clamping, rejection propagation, all
251
+ // matching) -- routing every call through the graph-aware scheduler
252
+ // regardless of hasDependencies would produce the same observable
253
+ // results for every existing no-dependsOn/no-isolated test, making that
254
+ // decision a pure performance optimization, not a behavioral one (an
255
+ // isolated check alone in `entries`, with nothing else to wait on,
256
+ // resolves to zero effective edges too -- the same proven-equivalent
257
+ // case).
258
+ // Stryker disable CallExpression,ArrowFunction,OptionalChaining,LogicalOperator,EqualityOperator,UnaryOperator,ConditionalExpression,BlockStatement -- dependency-scheduler.test.ts's own "zero-edges equivalence" suite already proves runWithConcurrencyGraph behaves identically to runWithConcurrency for a graph with no real edges, so this fast path is a pure performance optimization: every existing no-dependsOn/no-isolated test would pass identically either way, making this branch provably unobservable rather than undertested.
259
+ const hasDependencies = entries.some(
260
+ ([, check]) => (check.dependsOn?.length ?? 0) > 0 || check.isolated === true,
261
+ )
262
+ if (!hasDependencies) {
263
+ return await runWithConcurrency(entries, concurrency, worker)
264
+ }
265
+ // Stryker restore all
266
+
267
+ const indexById = new Map(entries.map(([checkId], index) => [checkId, index]))
268
+ // Precomputed once per run rather than re-scanned inside
269
+ // dependencyIndexesFor's own per-check closure below: every plain
270
+ // (non-isolated) check needs this same list to find which isolated
271
+ // checks it must wait for (see that edge's own comment below).
272
+ //
273
+ // This array's own initial contents are unobservable regardless of what they are: every real
274
+ // isolated index is still pushed on below, and dependencyIndexesFor's own `earlierIsolated`
275
+ // filter (further down) compares each entry against a numeric `index` with `<` -- a stray
276
+ // non-numeric seed value fails that numeric comparison (JavaScript's `<` coerces a non-numeric
277
+ // operand to NaN, and every comparison against NaN is false) and is silently filtered out,
278
+ // never appearing in any real edge list. Confirmed empirically, not assumed: `["Stryker was
279
+ // here", 2, 5].filter((i) => i < 10)` evaluates to `[2, 5]`, dropping the bogus entry with no
280
+ // trace.
281
+ // Stryker disable next-line ArrayDeclaration -- this array's own initial contents are unobservable regardless of what they are: every real isolated index is still pushed on below, and dependencyIndexesFor's own earlierIsolated filter compares each entry against a numeric index with `<`, which coerces a non-numeric seed value to NaN and silently filters it out (every comparison against NaN is false) -- confirmed empirically that a bogus seed entry never appears in any real edge list.
282
+ const isolatedIndexes: number[] = []
283
+ for (const [entryIndex, [, entryCheck]] of entries.entries()) {
284
+ if (entryCheck.isolated === true) isolatedIndexes.push(entryIndex)
285
+ }
286
+ // Declaration order in `entries` doubles as the required scheduling order (see
287
+ // CheckDefinitionConfig.isolated and CheckDefinition.dependsOn's own doc comments,
288
+ // specs/architecture.md, and validate-config.ts's backward-reference validation, which
289
+ // guarantees every declared `dependsOn` id resolves to an index strictly less than this
290
+ // check's own before this function is ever reached): a check runs concurrently with whatever's
291
+ // declared around it, launched in declaration order and bounded by `concurrency`, EXCEPT that
292
+ // (a) an explicit `dependsOn` id must have already reached a terminal status, and (b) an
293
+ // `isolated` check is a full barrier at its own declared position -- it waits for every check
294
+ // declared earlier (nothing "currently in flight" when its turn comes can be anything other
295
+ // than an earlier-declared check, since nothing later has been reached in the walk yet), and
296
+ // every check declared *after* it waits for it in turn, so nothing overlaps it either
297
+ // direction. Two isolated checks are therefore always sequential relative to each other (the
298
+ // later one's "everything declared earlier" already includes the earlier one).
299
+ const dependencyIndexesFor = (
300
+ [, check]: readonly [string, CheckDefinition],
301
+ index: number,
302
+ ): number[] => {
303
+ const declared = (check.dependsOn ?? []).map((depId) => {
304
+ const depIndex = indexById.get(depId)
305
+ // validate-config.ts has already guaranteed, before runChecks is
306
+ // ever invoked, that every dependsOn id names a check that exists
307
+ // in this same `checks` record -- reaching this would be a bug in
308
+ // that guarantee, not a user-input problem, so it fails loudly
309
+ // rather than silently miscounting the dependency graph. Provably
310
+ // unreachable given that guarantee, same as this file's other
311
+ // upstream-validated invariants.
312
+ /* v8 ignore start */
313
+ // Stryker disable EqualityOperator,ConditionalExpression,BlockStatement,StringLiteral,CallExpression -- validate-config.ts has already guaranteed, before runChecks is ever invoked, that every dependsOn id names a check that exists in this same checks record; reaching this would be a bug in that guarantee, not a user-input problem.
314
+ if (depIndex === undefined) {
315
+ throw new Error(`internal: dependsOn references unknown check id "${depId}".`)
316
+ }
317
+ // Stryker restore all
318
+ /* v8 ignore stop */
319
+ return depIndex
320
+ })
321
+
322
+ if (check.isolated === true) {
323
+ // Every index declared earlier than this one -- see this function's own doc comment above.
324
+ const earlierIndexes = Array.from({ length: index }, (_, earlierIndex) => earlierIndex)
325
+ return [...new Set([...declared, ...earlierIndexes])]
326
+ }
327
+
328
+ // A plain check waits for every isolated check declared earlier than it (so it never starts
329
+ // while that barrier is still draining or running), but not for every plain check earlier
330
+ // than it -- those remain free to run concurrently, unchanged from the disconnected-graph
331
+ // default.
332
+ //
333
+ // `<` vs `<=` here is provably equivalent, not a coverage gap: this branch only ever runs for
334
+ // a check whose own `isolated !== true` (the `if (check.isolated === true)` branch above
335
+ // already returned otherwise), and `isolatedIndexes` contains only indexes of checks whose
336
+ // `isolated === true` -- so `index` (this check's own) can never itself be a member of
337
+ // `isolatedIndexes`, making `isolatedIndex === index` unconditionally false for every value
338
+ // this filter is ever called with. `<` and `<=` therefore select the identical subset here.
339
+ // Stryker disable next-line EqualityOperator -- provably equivalent: this branch only runs when check.isolated !== true, and isolatedIndexes contains only indexes of checks where isolated === true, so this check's own `index` can never be a member of isolatedIndexes -- isolatedIndex === index is unconditionally false, making `<` and `<=` select the identical subset for every value this filter is ever called with.
340
+ const earlierIsolated = isolatedIndexes.filter((isolatedIndex) => isolatedIndex < index)
341
+ return [...new Set([...declared, ...earlierIsolated])]
342
+ }
343
+ return await runWithConcurrencyGraph(entries, concurrency, dependencyIndexesFor, worker)
344
+ } finally {
345
+ uninstall()
346
+ disposeRunSignal()
347
+ }
348
+ }