vouchington-tooling 0.20.0 → 0.22.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 (257) hide show
  1. package/README.md +73 -10
  2. package/dist/agent-blackboard/snapshot-partition-format.mjs +2 -72
  3. package/dist/agent-blackboard/snapshot-partition-guards.d.mts +3 -0
  4. package/dist/agent-blackboard/snapshot-partition-guards.mjs +9 -0
  5. package/dist/agent-blackboard/snapshot-partition-predicates.d.mts +3 -0
  6. package/dist/agent-blackboard/snapshot-partition-predicates.mjs +65 -0
  7. package/dist/api-contract-discovery/header-contract-parser.d.mts +3 -0
  8. package/dist/api-contract-discovery/header-contract-parser.mjs +93 -0
  9. package/dist/api-contract-discovery/header-contract-registry.d.mts +3 -0
  10. package/dist/api-contract-discovery/header-contract-registry.mjs +38 -0
  11. package/dist/api-contract-discovery/header-contract-types.d.mts +19 -0
  12. package/dist/api-contract-discovery/header-contract-types.mjs +1 -0
  13. package/dist/api-contract-discovery/index.d.mts +31 -0
  14. package/dist/api-contract-discovery/index.mjs +26 -0
  15. package/dist/api-contract-discovery/query-contract-extraction.d.mts +3 -0
  16. package/dist/api-contract-discovery/query-contract-extraction.mjs +65 -0
  17. package/dist/api-contract-discovery/query-contract-literals.d.mts +8 -0
  18. package/dist/api-contract-discovery/query-contract-literals.mjs +54 -0
  19. package/dist/api-contract-discovery/query-contract-registry.d.mts +3 -0
  20. package/dist/api-contract-discovery/query-contract-registry.mjs +68 -0
  21. package/dist/api-contract-discovery/query-contract-types.d.mts +8 -0
  22. package/dist/api-contract-discovery/query-contract-types.mjs +1 -0
  23. package/dist/api-contract-discovery/registered-route-catalog.d.mts +10 -0
  24. package/dist/api-contract-discovery/registered-route-catalog.mjs +81 -0
  25. package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +2 -0
  26. package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +74 -0
  27. package/dist/api-contract-discovery/request-contract-implicit.d.mts +17 -0
  28. package/dist/api-contract-discovery/request-contract-implicit.mjs +73 -0
  29. package/dist/api-contract-discovery/request-contract-lenient.d.mts +28 -0
  30. package/dist/api-contract-discovery/request-contract-lenient.mjs +59 -0
  31. package/dist/api-contract-discovery/request-contract-registry.d.mts +14 -0
  32. package/dist/api-contract-discovery/request-contract-registry.mjs +61 -0
  33. package/dist/api-contract-discovery/request-contract-route-analysis.d.mts +25 -0
  34. package/dist/api-contract-discovery/request-contract-route-analysis.mjs +68 -0
  35. package/dist/api-contract-discovery/request-contract-types.d.mts +12 -0
  36. package/dist/api-contract-discovery/request-contract-types.mjs +1 -0
  37. package/dist/api-contract-discovery/response-contract-ctx-analysis.d.mts +7 -0
  38. package/dist/api-contract-discovery/response-contract-ctx-analysis.mjs +28 -0
  39. package/dist/api-contract-discovery/response-contract-error-branch.d.mts +15 -0
  40. package/dist/api-contract-discovery/response-contract-error-branch.mjs +47 -0
  41. package/dist/api-contract-discovery/response-contract-implicit-sources.test-helpers.d.mts +145 -0
  42. package/dist/api-contract-discovery/response-contract-implicit-sources.test-helpers.mjs +145 -0
  43. package/dist/api-contract-discovery/response-contract-implicit.d.mts +5 -0
  44. package/dist/api-contract-discovery/response-contract-implicit.mjs +110 -0
  45. package/dist/api-contract-discovery/response-contract-lenient.d.mts +23 -0
  46. package/dist/api-contract-discovery/response-contract-lenient.mjs +52 -0
  47. package/dist/api-contract-discovery/response-contract-media.d.mts +3 -0
  48. package/dist/api-contract-discovery/response-contract-media.mjs +46 -0
  49. package/dist/api-contract-discovery/response-contract-registration.d.mts +11 -0
  50. package/dist/api-contract-discovery/response-contract-registration.mjs +61 -0
  51. package/dist/api-contract-discovery/response-contract-registry.d.mts +7 -0
  52. package/dist/api-contract-discovery/response-contract-registry.mjs +76 -0
  53. package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +13 -0
  54. package/dist/api-contract-discovery/response-contract-route-analysis.mjs +144 -0
  55. package/dist/api-contract-discovery/response-contract-status.d.mts +27 -0
  56. package/dist/api-contract-discovery/response-contract-status.mjs +80 -0
  57. package/dist/api-contract-discovery/response-contract-types.d.mts +16 -0
  58. package/dist/api-contract-discovery/response-contract-types.mjs +1 -0
  59. package/dist/api-contract-discovery/test-setup.test-helpers.d.mts +3 -0
  60. package/dist/api-contract-discovery/test-setup.test-helpers.mjs +2 -0
  61. package/dist/ast-grep-examples/companion-parity-compare.d.mts +3 -0
  62. package/dist/ast-grep-examples/companion-parity-compare.mjs +58 -0
  63. package/dist/ast-grep-examples/companion-parity.d.mts +2 -1
  64. package/dist/ast-grep-examples/companion-parity.mjs +2 -58
  65. package/dist/browser-session-runner/attempt.mjs +29 -48
  66. package/dist/browser-session-runner/termination.d.mts +11 -0
  67. package/dist/browser-session-runner/termination.mjs +34 -0
  68. package/dist/cli/parse-direct-options.d.mts +26 -0
  69. package/dist/cli/parse-direct-options.mjs +72 -0
  70. package/dist/cli/parse-forwarded-commands.d.mts +42 -0
  71. package/dist/cli/parse-forwarded-commands.mjs +29 -0
  72. package/dist/cli/parse-options.d.mts +1 -2
  73. package/dist/cli/parse-options.mjs +2 -72
  74. package/dist/cli/parse.mjs +4 -26
  75. package/dist/compiler-build/freshness.d.mts +27 -0
  76. package/dist/compiler-build/freshness.mjs +116 -0
  77. package/dist/compiler-build/index.d.mts +2 -0
  78. package/dist/compiler-build/index.mjs +2 -0
  79. package/dist/compiler-build/settlement.d.mts +10 -0
  80. package/dist/compiler-build/settlement.mjs +14 -0
  81. package/dist/config-inventory/call-arguments.d.mts +1 -0
  82. package/dist/config-inventory/call-arguments.mjs +30 -0
  83. package/dist/config-inventory/collect.d.mts +3 -0
  84. package/dist/config-inventory/collect.mjs +43 -0
  85. package/dist/config-inventory/dynamic-configs.d.mts +2 -0
  86. package/dist/config-inventory/dynamic-configs.mjs +9 -0
  87. package/dist/config-inventory/env-expression-readers.d.mts +3 -0
  88. package/dist/config-inventory/env-expression-readers.mjs +117 -0
  89. package/dist/config-inventory/env-prefix-readers.d.mts +2 -0
  90. package/dist/config-inventory/env-prefix-readers.mjs +14 -0
  91. package/dist/config-inventory/env-reference-matchers.d.mts +3 -0
  92. package/dist/config-inventory/env-reference-matchers.mjs +9 -0
  93. package/dist/config-inventory/env-var-accumulator.d.mts +6 -0
  94. package/dist/config-inventory/env-var-accumulator.mjs +82 -0
  95. package/dist/config-inventory/env-vars.d.mts +4 -0
  96. package/dist/config-inventory/env-vars.mjs +108 -0
  97. package/dist/config-inventory/format.d.mts +2 -0
  98. package/dist/config-inventory/format.mjs +69 -0
  99. package/dist/config-inventory/index.d.mts +3 -0
  100. package/dist/config-inventory/index.mjs +2 -0
  101. package/dist/config-inventory/package-gates.d.mts +2 -0
  102. package/dist/config-inventory/package-gates.mjs +54 -0
  103. package/dist/config-inventory/shared.d.mts +3 -0
  104. package/dist/config-inventory/shared.mjs +7 -0
  105. package/dist/config-inventory/types.d.mts +96 -0
  106. package/dist/config-inventory/types.mjs +1 -0
  107. package/dist/contract-schema/contract-schema-bounded-array.d.mts +15 -0
  108. package/dist/contract-schema/contract-schema-bounded-array.mjs +32 -0
  109. package/dist/contract-schema/contract-schema-type-extractor.d.mts +2 -8
  110. package/dist/contract-schema/contract-schema-type-extractor.mjs +2 -31
  111. package/dist/coverage-transport/control-v2-parse-object.d.mts +12 -0
  112. package/dist/coverage-transport/control-v2-parse-object.mjs +28 -0
  113. package/dist/coverage-transport/control-v2.mjs +2 -27
  114. package/dist/dependency-license-policy/audit-directory.d.mts +28 -0
  115. package/dist/dependency-license-policy/audit-directory.mjs +115 -0
  116. package/dist/dependency-license-policy/audit-signals.d.mts +12 -0
  117. package/dist/dependency-license-policy/audit-signals.mjs +44 -0
  118. package/dist/dependency-license-policy/audit-store.d.mts +12 -0
  119. package/dist/dependency-license-policy/audit-store.mjs +32 -0
  120. package/dist/dependency-license-policy/collect.d.mts +3 -1
  121. package/dist/dependency-license-policy/collect.mjs +16 -21
  122. package/dist/dependency-license-policy/execute-pnpm.d.mts +10 -0
  123. package/dist/dependency-license-policy/execute-pnpm.mjs +83 -0
  124. package/dist/dependency-license-policy/types.d.mts +11 -9
  125. package/dist/dependency-license-policy/workspace.d.mts +3 -1
  126. package/dist/dependency-license-policy/workspace.mjs +12 -5
  127. package/dist/gh-api-shell-quoting/argument-scan.d.mts +7 -0
  128. package/dist/gh-api-shell-quoting/argument-scan.mjs +35 -0
  129. package/dist/gh-api-shell-quoting/logical-line.d.mts +3 -0
  130. package/dist/gh-api-shell-quoting/logical-line.mjs +90 -0
  131. package/dist/gh-api-shell-quoting/scan.d.mts +2 -4
  132. package/dist/gh-api-shell-quoting/scan.mjs +1 -119
  133. package/dist/gh-cli/diff.d.mts +24 -4
  134. package/dist/gh-cli/diff.mjs +111 -4
  135. package/dist/gh-cli/index.d.mts +2 -1
  136. package/dist/gh-cli/index.mjs +1 -1
  137. package/dist/gha-workspace-policy/docker-options.d.mts +4 -0
  138. package/dist/gha-workspace-policy/docker-options.mjs +93 -0
  139. package/dist/gha-workspace-policy/docker-workspace-user.mjs +1 -168
  140. package/dist/gha-workspace-policy/shell-token-modes.d.mts +10 -0
  141. package/dist/gha-workspace-policy/shell-token-modes.mjs +31 -0
  142. package/dist/gha-workspace-policy/shell-tokens.d.mts +1 -0
  143. package/dist/gha-workspace-policy/shell-tokens.mjs +65 -0
  144. package/dist/index.d.mts +2 -1
  145. package/dist/index.mjs +1 -1
  146. package/dist/openapi-document/contract-schema-types.d.mts +1 -1
  147. package/dist/openapi-document/openapi-types.d.mts +1 -1
  148. package/dist/pnpm-install/native-family.d.mts +3 -0
  149. package/dist/pnpm-install/native-family.mjs +31 -0
  150. package/dist/pnpm-install/native-health.d.mts +1 -4
  151. package/dist/pnpm-install/native-health.mjs +2 -31
  152. package/dist/retrospective-facts/local-merge.d.mts +2 -0
  153. package/dist/retrospective-facts/local-merge.mjs +21 -0
  154. package/dist/retrospective-facts/local.mjs +1 -21
  155. package/dist/retrospective-transcript/claude-blocks.d.mts +3 -0
  156. package/dist/retrospective-transcript/claude-blocks.mjs +38 -0
  157. package/dist/retrospective-transcript/claude.mjs +4 -34
  158. package/dist/retrospective-transcript/codex-payload.d.mts +2 -0
  159. package/dist/retrospective-transcript/codex-payload.mjs +51 -0
  160. package/dist/retrospective-transcript/codex-records.d.mts +2 -0
  161. package/dist/retrospective-transcript/codex-records.mjs +95 -0
  162. package/dist/retrospective-transcript/codex.mjs +2 -144
  163. package/dist/retrospective-transcript/index.d.mts +3 -16
  164. package/dist/retrospective-transcript/index.mjs +4 -48
  165. package/dist/retrospective-transcript/resolve-transcript-file.d.mts +19 -0
  166. package/dist/retrospective-transcript/resolve-transcript-file.mjs +50 -0
  167. package/dist/session-friction/command-segments.d.mts +3 -0
  168. package/dist/session-friction/command-segments.mjs +94 -0
  169. package/dist/session-friction/log-file.d.mts +7 -0
  170. package/dist/session-friction/log-file.mjs +83 -0
  171. package/dist/session-friction/log.mjs +3 -83
  172. package/dist/session-friction/normalize.mjs +1 -94
  173. package/dist/sql-scanner/comments.d.mts +1 -0
  174. package/dist/sql-scanner/comments.mjs +101 -0
  175. package/dist/sql-scanner/index.d.mts +1 -1
  176. package/dist/sql-scanner/index.mjs +1 -100
  177. package/dist/sql-scanner/literals.mjs +1 -35
  178. package/dist/sql-scanner/quote-decode.d.mts +2 -0
  179. package/dist/sql-scanner/quote-decode.mjs +35 -0
  180. package/dist/vitest-diagnostics/index.d.mts +7 -0
  181. package/dist/vitest-diagnostics/index.mjs +4 -0
  182. package/dist/vitest-diagnostics/vitest-fork-exit-error-detail.d.mts +7 -0
  183. package/dist/vitest-diagnostics/vitest-fork-exit-error-detail.mjs +26 -0
  184. package/dist/vitest-diagnostics/vitest-fork-exit-records.d.mts +26 -0
  185. package/dist/vitest-diagnostics/vitest-fork-exit-records.mjs +161 -0
  186. package/dist/vitest-diagnostics/vitest-fork-exit-sentinel.d.mts +14 -0
  187. package/dist/vitest-diagnostics/vitest-fork-exit-sentinel.mjs +131 -0
  188. package/dist/vitest-diagnostics/vitest-fork-leak-detection.d.mts +26 -0
  189. package/dist/vitest-diagnostics/vitest-fork-leak-detection.mjs +96 -0
  190. package/dist/vitest-diagnostics/vitest-fork-leak-diagnostics.d.mts +3 -0
  191. package/dist/vitest-diagnostics/vitest-fork-leak-diagnostics.mjs +14 -0
  192. package/dist/vitest-diagnostics/vitest-fork-leak-episode.d.mts +37 -0
  193. package/dist/vitest-diagnostics/vitest-fork-leak-episode.mjs +118 -0
  194. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-errors.d.mts +2 -0
  195. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-errors.mjs +42 -0
  196. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-reporter.d.mts +12 -0
  197. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-reporter.mjs +142 -0
  198. package/dist/workflow-policy/concurrency-scope.d.mts +4 -0
  199. package/dist/workflow-policy/concurrency-scope.mjs +66 -0
  200. package/dist/workflow-policy/concurrency.d.mts +10 -0
  201. package/dist/workflow-policy/concurrency.mjs +77 -0
  202. package/dist/workflow-policy/condition-entailment.d.mts +20 -0
  203. package/dist/workflow-policy/condition-entailment.mjs +132 -0
  204. package/dist/workflow-policy/condition-split.d.mts +15 -0
  205. package/dist/workflow-policy/condition-split.mjs +71 -0
  206. package/dist/workflow-policy/graph.d.mts +2 -0
  207. package/dist/workflow-policy/graph.mjs +138 -0
  208. package/dist/workflow-policy/index.d.mts +7 -0
  209. package/dist/workflow-policy/index.mjs +6 -0
  210. package/dist/workflow-policy/permission-mismatches.d.mts +3 -0
  211. package/dist/workflow-policy/permission-mismatches.mjs +52 -0
  212. package/dist/workflow-policy/permissions.d.mts +16 -0
  213. package/dist/workflow-policy/permissions.mjs +83 -0
  214. package/dist/workflow-policy/secret-readiness.d.mts +18 -0
  215. package/dist/workflow-policy/secret-readiness.mjs +163 -0
  216. package/dist/workflow-policy/shell-comments.d.mts +7 -0
  217. package/dist/workflow-policy/shell-comments.mjs +28 -0
  218. package/dist/workflow-policy/types.d.mts +90 -0
  219. package/dist/workflow-policy/types.mjs +1 -0
  220. package/dist/workspace-gates/manifest-version-parser.mjs +1 -52
  221. package/dist/workspace-gates/manifest-version-skips.d.mts +1 -0
  222. package/dist/workspace-gates/manifest-version-skips.mjs +52 -0
  223. package/docs/workflow-and-compiler.md +74 -0
  224. package/package.json +26 -2
  225. package/skills/agent-workflow/SKILL.md +4 -4
  226. package/skills/agent-workflow/references/review-response.md +47 -44
  227. package/skills/backend-vitest-test-authoring/SKILL.md +9 -6
  228. package/skills/blackboard/SKILL.md +8 -8
  229. package/skills/dependabot/SKILL.md +68 -67
  230. package/skills/dotnet-test-authoring/SKILL.md +11 -12
  231. package/skills/git-commit-checklist/SKILL.md +3 -3
  232. package/skills/github-actions-authoring/SKILL.md +40 -38
  233. package/skills/github-actions-checklist/SKILL.md +93 -85
  234. package/skills/github-issue/SKILL.md +82 -78
  235. package/skills/nextjs-vitest-test-authoring/SKILL.md +8 -6
  236. package/skills/npm-publishing/SKILL.md +42 -38
  237. package/skills/organize-github-issues/SKILL.md +18 -22
  238. package/skills/package-json-checklist/SKILL.md +5 -5
  239. package/skills/planning/SKILL.md +3 -3
  240. package/skills/playwright-authoring/SKILL.md +10 -9
  241. package/skills/postgres-node-performance-tuning/SKILL.md +10 -6
  242. package/skills/postgres-partitioning-uuid-v7/SKILL.md +10 -7
  243. package/skills/pr-description/SKILL.md +4 -3
  244. package/skills/retrospective/SKILL.md +14 -15
  245. package/skills/retrospective-distill/SKILL.md +14 -14
  246. package/skills/review-ci-logs/SKILL.md +18 -19
  247. package/skills/review-github-issue-taxonomy/SKILL.md +21 -18
  248. package/skills/revisit-followups/SKILL.md +3 -3
  249. package/skills/stacked-prs/SKILL.md +30 -61
  250. package/skills/static-analysis-checklist/SKILL.md +6 -4
  251. package/skills/storybook-authoring/SKILL.md +9 -9
  252. package/skills/swift-test-authoring/SKILL.md +10 -9
  253. package/skills/test-authoring/SKILL.md +14 -14
  254. package/skills/test-authoring/references/core-practice.md +12 -11
  255. package/skills/test-authoring/references/dependency-boundaries.md +36 -35
  256. package/skills/test-authoring/references/tautological-tests.md +45 -44
  257. package/skills/vitest-test-authoring/SKILL.md +9 -6
@@ -0,0 +1,26 @@
1
+ export type ForkExitRecordMode = 'exit' | 'uncaught' | 'unhandled' | `signal:${NodeJS.Signals}`;
2
+ export type ForkExitSentinelRecord = {
3
+ kind: 'start';
4
+ pid: number;
5
+ } | {
6
+ kind: 'exit';
7
+ pid: number;
8
+ project: string;
9
+ module: string;
10
+ mode: ForkExitRecordMode;
11
+ code: number;
12
+ errorMessage?: string;
13
+ errorStack?: string;
14
+ };
15
+ export declare function writeForkExitRecord(directory: string, record: ForkExitSentinelRecord): void;
16
+ export declare function readForkExitRecords(dir: string): ForkExitSentinelRecord[];
17
+ export declare function clearForkExitRecords(dir: string): void;
18
+ export interface ForkExitRecordSummary {
19
+ startedPidCount: number;
20
+ exitRecords: Extract<ForkExitSentinelRecord, {
21
+ kind: 'exit';
22
+ }>[];
23
+ forksWithoutExitSentinel: number;
24
+ }
25
+ export declare function summarizeForkExitRecords(records: ForkExitSentinelRecord[]): ForkExitRecordSummary;
26
+ export declare function formatForkExitSentinelSection(summary: ForkExitRecordSummary): string[];
@@ -0,0 +1,161 @@
1
+ // Each fork writes start and exit records to its own `<pid>.jsonl` file. A start without an exit
2
+ // identifies a death before the sentinel handler ran. Synchronous writes avoid async pipe loss.
3
+ import { closeSync, constants, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, unlinkSync, writeSync, } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { sanitizeInlineErrorMessage } from './vitest-fork-exit-error-detail.mjs';
6
+ const recordFileFds = new Map();
7
+ function recordFileDescriptor(directory) {
8
+ const cached = recordFileFds.get(directory);
9
+ if (cached !== undefined)
10
+ return cached;
11
+ try {
12
+ mkdirSync(directory, { recursive: true });
13
+ if (!lstatSync(directory).isDirectory())
14
+ return null;
15
+ const fd = openSync(join(directory, `${process.pid}.jsonl`), constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW, 0o600);
16
+ recordFileFds.set(directory, fd);
17
+ return fd;
18
+ }
19
+ catch {
20
+ // Best-effort: a read-only or missing directory must not turn attribution into a crash. The
21
+ // fd-2 [vitest-fork-exit] line stays the primary signal; this file is a durable supplement.
22
+ return null;
23
+ }
24
+ }
25
+ export function writeForkExitRecord(directory, record) {
26
+ const fd = recordFileDescriptor(directory);
27
+ if (fd === null)
28
+ return;
29
+ try {
30
+ writeSync(fd, `${JSON.stringify(record)}\n`);
31
+ }
32
+ catch {
33
+ // Same best-effort rationale as recordFileDescriptor() above.
34
+ }
35
+ }
36
+ export function readForkExitRecords(dir) {
37
+ const records = [];
38
+ for (const filename of recordFileNames(dir)) {
39
+ parseRecordLines(readFileSafely(join(dir, filename)), records);
40
+ }
41
+ return records;
42
+ }
43
+ export function clearForkExitRecords(dir) {
44
+ const fd = recordFileFds.get(dir);
45
+ if (fd !== undefined) {
46
+ closeSync(fd);
47
+ recordFileFds.delete(dir);
48
+ }
49
+ for (const filename of recordFileNames(dir)) {
50
+ try {
51
+ const path = join(dir, filename);
52
+ if (lstatSync(path).isFile())
53
+ unlinkSync(path);
54
+ }
55
+ catch {
56
+ // A concurrently exiting worker may have removed its file already.
57
+ }
58
+ }
59
+ }
60
+ function recordFileNames(dir) {
61
+ try {
62
+ if (!lstatSync(dir).isDirectory())
63
+ return [];
64
+ return readdirSync(dir).filter((name) => /^\d+\.jsonl$/.test(name));
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ export function summarizeForkExitRecords(records) {
71
+ const startedPids = new Set();
72
+ const exitRecordsByPid = new Map();
73
+ for (const record of records) {
74
+ if (record.kind === 'start')
75
+ startedPids.add(record.pid);
76
+ else
77
+ exitRecordsByPid.set(record.pid, record);
78
+ }
79
+ const forksWithoutExitSentinel = [...startedPids].filter((pid) => !exitRecordsByPid.has(pid)).length;
80
+ return {
81
+ startedPidCount: startedPids.size,
82
+ exitRecords: [...exitRecordsByPid.values()],
83
+ forksWithoutExitSentinel,
84
+ };
85
+ }
86
+ const MAX_SENTINEL_EXIT_RECORDS = 20;
87
+ // Consumed by vitest-worker-exit-diagnostics-reporter.mts's onTestRunEnd — kept here rather than
88
+ // there so the record shape and its presentation stay next to each other.
89
+ export function formatForkExitSentinelSection(summary) {
90
+ const lines = [
91
+ `forks started: ${summary.startedPidCount}`,
92
+ `forks without an exit sentinel: ${summary.forksWithoutExitSentinel}`,
93
+ 'sentinel exit records:',
94
+ ];
95
+ if (summary.exitRecords.length === 0) {
96
+ lines.push(' (none recorded)');
97
+ return lines;
98
+ }
99
+ for (const record of summary.exitRecords.slice(0, MAX_SENTINEL_EXIT_RECORDS)) {
100
+ lines.push(` - pid=${record.pid} project=${sanitizeInlineErrorMessage(record.project)} module=${sanitizeInlineErrorMessage(record.module)} mode=${record.mode} code=${record.code}`);
101
+ // sanitizeInlineErrorMessage collapses the record's raw, unbounded message to one line: this
102
+ // report is written straight into a test log, so a raw multi-line Error.message could forge
103
+ // fresh physical lines that a line-anchored log predicate reads as real output.
104
+ if (record.errorMessage)
105
+ lines.push(` error: ${sanitizeInlineErrorMessage(record.errorMessage)}`);
106
+ if (record.errorStack)
107
+ lines.push(` stack: ${sanitizeInlineErrorMessage(record.errorStack.slice(0, 500))}`);
108
+ }
109
+ if (summary.exitRecords.length > MAX_SENTINEL_EXIT_RECORDS) {
110
+ lines.push(` ... ${summary.exitRecords.length - MAX_SENTINEL_EXIT_RECORDS} more`);
111
+ }
112
+ return lines;
113
+ }
114
+ function readFileSafely(path) {
115
+ try {
116
+ const stat = lstatSync(path);
117
+ if (!stat.isFile() || stat.size > 1024 * 1024)
118
+ return '';
119
+ return readFileSync(path, 'utf8');
120
+ }
121
+ catch {
122
+ return '';
123
+ }
124
+ }
125
+ function parseRecordLines(content, into) {
126
+ for (const line of content.split('\n')) {
127
+ if (!line.trim())
128
+ continue;
129
+ try {
130
+ const record = JSON.parse(line);
131
+ if (isForkExitRecord(record))
132
+ into.push(record);
133
+ }
134
+ catch {
135
+ // A torn trailing line from a fork still mid-write when this is read; skip it rather than
136
+ // fail the whole roll-up over one partial record.
137
+ }
138
+ }
139
+ }
140
+ function isForkExitRecord(value) {
141
+ if (!isRecord(value))
142
+ return false;
143
+ if (!Number.isSafeInteger(value.pid) || value.pid <= 0)
144
+ return false;
145
+ if (value.kind === 'start')
146
+ return true;
147
+ return (value.kind === 'exit' &&
148
+ typeof value.project === 'string' &&
149
+ typeof value.module === 'string' &&
150
+ typeof value.mode === 'string' &&
151
+ (value.mode === 'exit' ||
152
+ value.mode === 'uncaught' ||
153
+ value.mode === 'unhandled' ||
154
+ /^signal:SIG[A-Z0-9]+$/.test(value.mode)) &&
155
+ Number.isSafeInteger(value.code) &&
156
+ (value.errorMessage === undefined || typeof value.errorMessage === 'string') &&
157
+ (value.errorStack === undefined || typeof value.errorStack === 'string'));
158
+ }
159
+ function isRecord(value) {
160
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
161
+ }
@@ -0,0 +1,14 @@
1
+ declare global {
2
+ namespace NodeJS {
3
+ interface Process {
4
+ reallyExit(code?: number): never;
5
+ }
6
+ }
7
+ }
8
+ export type ForkExitSentinelOptions = {
9
+ recordDirectory: string;
10
+ };
11
+ export declare function setCurrentForkExitModule(modulePath: string | null): void;
12
+ export declare function setCurrentForkExitProject(projectName: string | undefined): void;
13
+ export declare function formatPercent(part: number, whole: number): string;
14
+ export declare function registerForkExitSentinel(options: ForkExitSentinelOptions): void;
@@ -0,0 +1,131 @@
1
+ // Registers, inside a Vitest fork, the last line of defense for naming how that fork died.
2
+ //
3
+ // The parent-side worker-exit error may lack the child's exit code and signal. This module writes
4
+ // one synchronous line from inside the dying fork before the parent reports its failure.
5
+ //
6
+ // Every write goes through `writeSync(2, …)` rather than `process.stderr.write` — the async stderr
7
+ // pipe may lose data on an abrupt kill. A synchronous fd-2 write survives everything short of
8
+ // SIGKILL/SIGSEGV/a hard V8 abort — and the *absence* of this line for a fork Vitest reports as
9
+ // unexpectedly exited is itself the diagnostic: it proves a handler-unreachable death.
10
+ import { writeSync } from 'node:fs';
11
+ import { constants as osConstants } from 'node:os';
12
+ import { getHeapStatistics } from 'node:v8';
13
+ import { MAX_RECORDED_ERROR_STACK_CHARS, sanitizeInlineErrorMessage, toErrorDetail, } from './vitest-fork-exit-error-detail.mjs';
14
+ import { writeForkExitRecord } from './vitest-fork-exit-records.mjs';
15
+ const SENTINEL_REGISTERED = Symbol.for('vouchington-tooling.vitest-fork-exit-sentinel.registered');
16
+ let currentModule = null;
17
+ // isolate: false lets one fork run files from multiple projects. Track the current project per test.
18
+ let currentProject = 'unknown';
19
+ // Called from the setup file's beforeEach/afterEach so a mid-test death attributes to the file
20
+ // and project that were running, and a between-tests death (e.g. pool teardown) reports
21
+ // module=none.
22
+ export function setCurrentForkExitModule(modulePath) {
23
+ currentModule = modulePath;
24
+ }
25
+ export function setCurrentForkExitProject(projectName) {
26
+ currentProject = projectName ?? 'unknown';
27
+ }
28
+ function formatBytes(bytes) {
29
+ return (bytes / 1024 / 1024).toFixed(1);
30
+ }
31
+ export function formatPercent(part, whole) {
32
+ return whole > 0 ? ((part / whole) * 100).toFixed(1) : '0.0';
33
+ }
34
+ function writeForkExitLine(directory, mode, code, error) {
35
+ const memory = process.memoryUsage();
36
+ const heapLimitBytes = getHeapStatistics().heap_size_limit;
37
+ const errorDetail = mode === 'uncaught' || mode === 'unhandled' ? toErrorDetail(error) : undefined;
38
+ const fields = [
39
+ '[vitest-fork-exit]',
40
+ `pid=${process.pid}`,
41
+ `project=${currentProject}`,
42
+ `module=${currentModule ?? 'none'}`,
43
+ `mode=${mode}`,
44
+ `code=${code}`,
45
+ `heapUsedMB=${formatBytes(memory.heapUsed)}`,
46
+ `heapLimitMB=${formatBytes(heapLimitBytes)}`,
47
+ `heapPctOfLimit=${formatPercent(memory.heapUsed, heapLimitBytes)}`,
48
+ `rssMB=${formatBytes(memory.rss)}`,
49
+ `uptimeMs=${Math.round(process.uptime() * 1000)}`,
50
+ ];
51
+ if (errorDetail)
52
+ fields.push(`errorMessage=${sanitizeInlineErrorMessage(errorDetail.message)}`);
53
+ writeSync(2, `${fields.join(' ')}\n`);
54
+ // Durable file twin (see vitest-fork-exit-records.mts) — the roll-up in
55
+ // vitest-worker-exit-diagnostics-reporter.mts reads this back from the main process, which
56
+ // cannot see the stderr line above at all (it's piped straight to the job's raw log output, not
57
+ // through Vitest's onUserConsoleLog channel). Carries the full message and stack, unlike the
58
+ // bounded single-line fd-2 field above.
59
+ writeForkExitRecord(directory, {
60
+ kind: 'exit',
61
+ pid: process.pid,
62
+ project: currentProject,
63
+ module: currentModule ?? 'none',
64
+ mode,
65
+ code,
66
+ ...(errorDetail
67
+ ? {
68
+ errorMessage: errorDetail.message,
69
+ ...(errorDetail.stack
70
+ ? { errorStack: errorDetail.stack.slice(0, MAX_RECORDED_ERROR_STACK_CHARS) }
71
+ : {}),
72
+ }
73
+ : {}),
74
+ });
75
+ }
76
+ // Idempotent: multiple setup files across multiple projects may import this module inside the
77
+ // same fork (isolate: false reuses one fork across many files), and process.on listeners must
78
+ // only be attached once.
79
+ export function registerForkExitSentinel(options) {
80
+ const directory = options.recordDirectory;
81
+ if (!directory)
82
+ throw new Error('recordDirectory is required');
83
+ if (globalThis[SENTINEL_REGISTERED]) {
84
+ if (globalThis[SENTINEL_REGISTERED] !== directory) {
85
+ throw new Error('Fork exit sentinel is already registered with a different directory');
86
+ }
87
+ return;
88
+ }
89
+ // Turns a future Node major removing this internal (see the module header comment) into an
90
+ // immediate, attributable failure instead of a silent one: without it, every handler below would
91
+ // throw when it tries to call reallyExit(), cascading into Vitest's own uncaughtException
92
+ // listener and producing an untraceable crash rather than a clear cause.
93
+ if (typeof process.reallyExit !== 'function') {
94
+ throw new Error('process.reallyExit is unavailable — the Node version may have removed this internal API. ' +
95
+ 'The fork-exit sentinel cannot terminate forks without it.');
96
+ }
97
+ // Records that this fork started, independent of how it ends — see vitest-fork-exit-records.mts
98
+ // for why this is the only way to count forks that die before writing anything else.
99
+ writeForkExitRecord(directory, { kind: 'start', pid: process.pid });
100
+ globalThis[SENTINEL_REGISTERED] = directory;
101
+ // The normal path: every fork's process eventually exits, whether the pool recycles it or the
102
+ // run ends. On a fully green run this is the only line these forks ever write.
103
+ process.on('exit', (code) => {
104
+ writeForkExitLine(directory, 'exit', code);
105
+ });
106
+ // Preserve default crash semantics (non-zero exit) — this only adds attribution, it must not
107
+ // turn a real crash into a silently-swallowed one. reallyExit(1), not exit(1): see the module
108
+ // header comment — process.exit() is unusable here, and reallyExit's silence on the 'exit'
109
+ // event is exactly right since the line for this death is already written above.
110
+ process.on('uncaughtException', (error) => {
111
+ writeForkExitLine(directory, 'uncaught', 1, error);
112
+ process.reallyExit(1);
113
+ });
114
+ process.on('unhandledRejection', (reason) => {
115
+ writeForkExitLine(directory, 'unhandled', 1, reason);
116
+ process.reallyExit(1);
117
+ });
118
+ const trapSignal = (signal) => {
119
+ process.on(signal, () => {
120
+ // 128+n mirrors POSIX/shell exit-code convention for a signal death, so the recorded
121
+ // code stays interpretable even though we're now exiting deliberately rather than by
122
+ // the raw signal.
123
+ const code = 128 + osConstants.signals[signal];
124
+ writeForkExitLine(directory, `signal:${signal}`, code);
125
+ process.reallyExit(code);
126
+ });
127
+ };
128
+ trapSignal('SIGTERM');
129
+ trapSignal('SIGINT');
130
+ trapSignal('SIGHUP');
131
+ }
@@ -0,0 +1,26 @@
1
+ export { formatForkLeakDiagnostics } from './vitest-fork-leak-diagnostics.mts';
2
+ import { type ForkLeakGrowthStep } from './vitest-fork-leak-episode.mts';
3
+ export declare const WARMUP_CHECKPOINTS = 5;
4
+ export declare const LEAK_THRESHOLD = 4;
5
+ export declare const SUSTAINED_CHECKPOINTS = 5;
6
+ export type ForkLeakVerdict = {
7
+ type: string;
8
+ baseline: number;
9
+ current: number;
10
+ streak: number;
11
+ growthCheckpoints: number;
12
+ requiredGrowthCheckpoints: number;
13
+ detectedInFile: string | null;
14
+ suspectedGrowth: ForkLeakGrowthStep | null;
15
+ };
16
+ export type ForkLeakDetectorOptions = {
17
+ trackedTypes?: ReadonlySet<string>;
18
+ warmupCheckpoints?: number;
19
+ leakThreshold?: number;
20
+ sustainedCheckpoints?: number;
21
+ };
22
+ export type ForkLeakDetector = {
23
+ record(counts: ReadonlyMap<string, number>, testFile?: string | null): ForkLeakVerdict[];
24
+ };
25
+ export declare function waitForResourceCloseCallbacks(): Promise<void>;
26
+ export declare function createForkLeakDetector(options?: ForkLeakDetectorOptions): ForkLeakDetector;
@@ -0,0 +1,96 @@
1
+ // Fork-side growth-delta leak detector for Vitest's reused forks pool.
2
+ // It spans setup-file resets with one episode per type and stable candidate plateau.
3
+ // Episodes combine new high-water marks with rolling samples for transient-high recovery.
4
+ export { formatForkLeakDiagnostics } from './vitest-fork-leak-diagnostics.mjs';
5
+ import { createForkLeakEpisode, findForkLeakEpisodeEvidence, recordForkLeakEpisode, setForkLeakCandidate, takeConfirmedForkLeakCandidate, } from './vitest-fork-leak-episode.mjs';
6
+ const realSetTimeout = globalThis.setTimeout;
7
+ const PERSISTENT_HANDLE_TYPES = new Set([
8
+ 'TCPSocketWrap',
9
+ 'TCPServerWrap',
10
+ 'PipeWrap',
11
+ 'TLSWrap',
12
+ 'UDPWrap',
13
+ 'FSEvent',
14
+ 'StatWatcher',
15
+ 'ProcessWrap',
16
+ 'MessagePortData',
17
+ ]);
18
+ export const WARMUP_CHECKPOINTS = 5;
19
+ export const LEAK_THRESHOLD = 4;
20
+ export const SUSTAINED_CHECKPOINTS = 5;
21
+ export function waitForResourceCloseCallbacks() {
22
+ return new Promise((resolve) => realSetTimeout(resolve, 0));
23
+ }
24
+ // Baselines live (no static list to go stale) and flags at most once per type, so a confirmed
25
+ // leak fails the test it's detected in without cascading into every subsequent test in the fork.
26
+ export function createForkLeakDetector(options = {}) {
27
+ const trackedTypes = options.trackedTypes ?? PERSISTENT_HANDLE_TYPES;
28
+ const warmupCheckpoints = options.warmupCheckpoints ?? WARMUP_CHECKPOINTS;
29
+ const leakThreshold = options.leakThreshold ?? LEAK_THRESHOLD;
30
+ const sustainedCheckpoints = options.sustainedCheckpoints ?? SUSTAINED_CHECKPOINTS;
31
+ const requiredGrowthCheckpoints = growthCheckpointRequirement(sustainedCheckpoints);
32
+ const baseline = new Map();
33
+ const previousCounts = new Map();
34
+ const episodes = new Map();
35
+ const reported = new Set();
36
+ let checkpoint = 0;
37
+ return {
38
+ record(counts, testFile = null) {
39
+ checkpoint += 1;
40
+ const verdicts = [];
41
+ for (const type of trackedTypes) {
42
+ const current = counts.get(type) ?? 0;
43
+ const previous = previousCounts.get(type) ?? current;
44
+ previousCounts.set(type, current);
45
+ if (checkpoint <= warmupCheckpoints) {
46
+ baseline.set(type, Math.max(baseline.get(type) ?? 0, current));
47
+ episodes.delete(type);
48
+ continue;
49
+ }
50
+ const base = baseline.get(type);
51
+ if (current <= base + leakThreshold) {
52
+ episodes.delete(type);
53
+ continue;
54
+ }
55
+ let episode = episodes.get(type);
56
+ if (!episode) {
57
+ episode = createForkLeakEpisode(base, previous, current, testFile);
58
+ episodes.set(type, episode);
59
+ }
60
+ else {
61
+ recordForkLeakEpisode(episode, previous, current, testFile, sustainedCheckpoints);
62
+ }
63
+ const confirmedEvidence = takeConfirmedForkLeakCandidate(episode, previous, current, testFile, sustainedCheckpoints);
64
+ if (confirmedEvidence) {
65
+ if (!reported.has(type)) {
66
+ reported.add(type);
67
+ verdicts.push({
68
+ type,
69
+ baseline: base,
70
+ current,
71
+ streak: confirmedEvidence.age,
72
+ growthCheckpoints: confirmedEvidence.growthCheckpoints,
73
+ requiredGrowthCheckpoints,
74
+ detectedInFile: testFile,
75
+ suspectedGrowth: confirmedEvidence.suspectedGrowth,
76
+ });
77
+ }
78
+ episodes.delete(type);
79
+ }
80
+ else {
81
+ const evidence = findForkLeakEpisodeEvidence(episode, current, sustainedCheckpoints, requiredGrowthCheckpoints);
82
+ for (const signal of evidence)
83
+ setForkLeakCandidate(episode, signal);
84
+ if (episode.plateauCheckpoints >= sustainedCheckpoints) {
85
+ baseline.set(type, episode.plateauCandidate);
86
+ episodes.delete(type);
87
+ }
88
+ }
89
+ }
90
+ return verdicts;
91
+ },
92
+ };
93
+ }
94
+ function growthCheckpointRequirement(sustainedCheckpoints) {
95
+ return Math.floor(sustainedCheckpoints / 2) + 1;
96
+ }
@@ -0,0 +1,3 @@
1
+ import type { ForkLeakVerdict } from './vitest-fork-leak-detection.mts';
2
+ /** Formats a bounded, single-line verdict for a test failure or diagnostic log. */
3
+ export declare function formatForkLeakDiagnostics(verdict: ForkLeakVerdict): string;
@@ -0,0 +1,14 @@
1
+ /** Formats a bounded, single-line verdict for a test failure or diagnostic log. */
2
+ export function formatForkLeakDiagnostics(verdict) {
3
+ const growth = verdict.suspectedGrowth;
4
+ const file = growth?.testFile ?? verdict.detectedInFile ?? 'unknown';
5
+ return [
6
+ `[vitest-fork-leak] type=${verdict.type}`,
7
+ `baseline=${verdict.baseline}`,
8
+ `current=${verdict.current}`,
9
+ `streak=${verdict.streak}`,
10
+ `growth=${verdict.growthCheckpoints}/${verdict.requiredGrowthCheckpoints}`,
11
+ `file=${file.replace(/\s+/g, ' ').slice(0, 240)}`,
12
+ `delta=${growth?.delta ?? 0}`,
13
+ ].join(' ');
14
+ }
@@ -0,0 +1,37 @@
1
+ export type ForkLeakGrowthStep = {
2
+ testFile: string | null;
3
+ previous: number;
4
+ current: number;
5
+ delta: number;
6
+ };
7
+ type ForkLeakSample = ForkLeakGrowthStep;
8
+ type ForkLeakEvidenceKind = 'primary' | 'rolling';
9
+ export type ForkLeakEpisode = {
10
+ baseline: number;
11
+ start: number;
12
+ highWater: number;
13
+ age: number;
14
+ growthCheckpoints: number;
15
+ largestGrowthStep: ForkLeakGrowthStep | null;
16
+ plateauCandidate: number;
17
+ plateauCheckpoints: number;
18
+ samples: ForkLeakSample[];
19
+ pendingCandidate: ForkLeakCandidate | null;
20
+ rejectedHighWater: Record<ForkLeakEvidenceKind, number>;
21
+ };
22
+ export type ForkLeakEpisodeEvidence = {
23
+ kind: ForkLeakEvidenceKind;
24
+ age: number;
25
+ growthCheckpoints: number;
26
+ highWater: number;
27
+ suspectedGrowth: ForkLeakGrowthStep | null;
28
+ };
29
+ type ForkLeakCandidate = ForkLeakEpisodeEvidence & {
30
+ createdAt: number;
31
+ };
32
+ export declare function createForkLeakEpisode(baseline: number, previous: number, current: number, testFile: string | null): ForkLeakEpisode;
33
+ export declare function recordForkLeakEpisode(episode: ForkLeakEpisode, previous: number, current: number, testFile: string | null, sustainedCheckpoints: number): void;
34
+ export declare function findForkLeakEpisodeEvidence(episode: ForkLeakEpisode, current: number, sustainedCheckpoints: number, requiredGrowthCheckpoints: number): ForkLeakEpisodeEvidence[];
35
+ export declare function takeConfirmedForkLeakCandidate(episode: ForkLeakEpisode, previous: number, current: number, testFile: string | null, sustainedCheckpoints: number): ForkLeakEpisodeEvidence | null;
36
+ export declare function setForkLeakCandidate(episode: ForkLeakEpisode, evidence: ForkLeakEpisodeEvidence): void;
37
+ export {};
@@ -0,0 +1,118 @@
1
+ export function createForkLeakEpisode(baseline, previous, current, testFile) {
2
+ const sample = makeSample(previous, current, testFile);
3
+ return {
4
+ baseline,
5
+ start: current,
6
+ highWater: current,
7
+ age: 1,
8
+ growthCheckpoints: sample.delta > 0 ? 1 : 0,
9
+ largestGrowthStep: sample.delta > 0 ? sample : null,
10
+ plateauCandidate: current,
11
+ plateauCheckpoints: 0,
12
+ samples: [sample],
13
+ pendingCandidate: null,
14
+ rejectedHighWater: { primary: Number.NEGATIVE_INFINITY, rolling: Number.NEGATIVE_INFINITY },
15
+ };
16
+ }
17
+ export function recordForkLeakEpisode(episode, previous, current, testFile, sustainedCheckpoints) {
18
+ episode.age += 1;
19
+ const sample = makeSample(previous, current, testFile);
20
+ episode.samples.push(sample);
21
+ if (episode.samples.length > sustainedCheckpoints)
22
+ episode.samples.shift();
23
+ if (current === episode.plateauCandidate) {
24
+ episode.plateauCheckpoints += 1;
25
+ }
26
+ else {
27
+ episode.plateauCandidate = current;
28
+ episode.plateauCheckpoints = 0;
29
+ }
30
+ if (current > episode.highWater) {
31
+ episode.highWater = current;
32
+ episode.growthCheckpoints += 1;
33
+ if (!episode.largestGrowthStep || sample.delta > episode.largestGrowthStep.delta) {
34
+ episode.largestGrowthStep = sample;
35
+ }
36
+ }
37
+ }
38
+ export function findForkLeakEpisodeEvidence(episode, current, sustainedCheckpoints, requiredGrowthCheckpoints) {
39
+ const evidence = [];
40
+ if (episode.age >= sustainedCheckpoints &&
41
+ episode.growthCheckpoints >= requiredGrowthCheckpoints &&
42
+ current > episode.start) {
43
+ evidence.push({
44
+ kind: 'primary',
45
+ age: episode.age,
46
+ growthCheckpoints: episode.growthCheckpoints,
47
+ highWater: episode.highWater,
48
+ suspectedGrowth: episode.largestGrowthStep,
49
+ });
50
+ }
51
+ const rollingEvidence = findRollingEvidence(episode.samples, current, sustainedCheckpoints, requiredGrowthCheckpoints);
52
+ if (rollingEvidence)
53
+ evidence.push(rollingEvidence);
54
+ return evidence;
55
+ }
56
+ export function takeConfirmedForkLeakCandidate(episode, previous, current, testFile, sustainedCheckpoints) {
57
+ const candidate = episode.pendingCandidate;
58
+ if (!candidate)
59
+ return null;
60
+ if (episode.age - candidate.createdAt > sustainedCheckpoints) {
61
+ episode.rejectedHighWater[candidate.kind] = Math.max(episode.rejectedHighWater[candidate.kind], candidate.highWater);
62
+ episode.pendingCandidate = null;
63
+ return null;
64
+ }
65
+ if (current <= candidate.highWater)
66
+ return null;
67
+ episode.pendingCandidate = null;
68
+ const delta = current - previous;
69
+ const confirmation = delta > 0 ? { testFile, previous, current, delta } : null;
70
+ return {
71
+ ...candidate,
72
+ suspectedGrowth: confirmation &&
73
+ (!candidate.suspectedGrowth || confirmation.delta > candidate.suspectedGrowth.delta)
74
+ ? confirmation
75
+ : candidate.suspectedGrowth,
76
+ };
77
+ }
78
+ export function setForkLeakCandidate(episode, evidence) {
79
+ if (evidence.highWater <= episode.rejectedHighWater[evidence.kind])
80
+ return;
81
+ const candidate = episode.pendingCandidate;
82
+ if (candidate?.kind === 'primary' && evidence.kind === 'rolling') {
83
+ episode.rejectedHighWater.primary = Math.max(episode.rejectedHighWater.primary, candidate.highWater);
84
+ }
85
+ if (!candidate ||
86
+ (candidate.kind === 'primary' &&
87
+ evidence.kind === 'rolling' &&
88
+ evidence.highWater < candidate.highWater) ||
89
+ (candidate.kind === evidence.kind && evidence.highWater > candidate.highWater)) {
90
+ episode.pendingCandidate = { ...evidence, createdAt: episode.age };
91
+ }
92
+ }
93
+ function findRollingEvidence(samples, current, sustainedCheckpoints, requiredGrowthCheckpoints) {
94
+ if (samples.length !== sustainedCheckpoints || current <= samples[0].current)
95
+ return null;
96
+ const positiveSamples = [];
97
+ let highWater = Number.NEGATIVE_INFINITY;
98
+ for (const sample of samples) {
99
+ if (sample.current <= highWater)
100
+ continue;
101
+ highWater = sample.current;
102
+ if (sample.delta > 0)
103
+ positiveSamples.push(sample);
104
+ }
105
+ if (positiveSamples.length < requiredGrowthCheckpoints)
106
+ return null;
107
+ const suspectedGrowth = positiveSamples.reduce((largest, sample) => (!largest || sample.delta > largest.delta ? sample : largest), null);
108
+ return {
109
+ kind: 'rolling',
110
+ age: sustainedCheckpoints,
111
+ growthCheckpoints: positiveSamples.length,
112
+ highWater,
113
+ suspectedGrowth,
114
+ };
115
+ }
116
+ function makeSample(previous, current, testFile) {
117
+ return { testFile, previous, current, delta: current - previous };
118
+ }
@@ -0,0 +1,2 @@
1
+ export declare function collectStringValues(value: unknown, seen?: Set<unknown>): string[];
2
+ export declare function serializeDiagnosticsError(error: unknown, seen?: Set<unknown>): Record<string, unknown>;