assertledger 1.0.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 (203) hide show
  1. package/CONTRIBUTING.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.fr.md +236 -0
  4. package/README.md +224 -0
  5. package/SECURITY.md +51 -0
  6. package/benchmarks/agentic-profile/README.md +15 -0
  7. package/benchmarks/agentic-profile/public/README.md +5 -0
  8. package/benchmarks/self-hosted-core/README.md +113 -0
  9. package/benchmarks/self-hosted-core/adapter.mjs +293 -0
  10. package/benchmarks/self-hosted-core/builder.ts +193 -0
  11. package/benchmarks/self-hosted-core/campaign.ts +233 -0
  12. package/benchmarks/self-hosted-core/existing-tests-builder.ts +217 -0
  13. package/benchmarks/self-hosted-core/existing-tests.ts +146 -0
  14. package/benchmarks/self-hosted-core/liveness.test.mjs +8 -0
  15. package/conformance/v1/bundle.json +104 -0
  16. package/conformance/v1/expected/canonical-order-a.json +4 -0
  17. package/conformance/v1/expected/canonical-order-b.json +4 -0
  18. package/conformance/v1/expected/create-benchmark-v1-measured.json +575 -0
  19. package/conformance/v1/expected/create-profile-v1-qualified.json +280 -0
  20. package/conformance/v1/expected/decide-collection-failure-non-kill.json +192 -0
  21. package/conformance/v1/expected/decide-compile-failure-non-kill.json +192 -0
  22. package/conformance/v1/expected/decide-infra-error-non-kill.json +192 -0
  23. package/conformance/v1/expected/decide-no-test-discovered-non-kill.json +192 -0
  24. package/conformance/v1/expected/decide-process-crash-non-kill.json +192 -0
  25. package/conformance/v1/expected/decide-timeout-non-kill.json +192 -0
  26. package/conformance/v1/expected/decide-verified.json +192 -0
  27. package/conformance/v1/expected/replay-benchmark-v1-resealed-summary-forgery.json +12 -0
  28. package/conformance/v1/expected/replay-evidence-raw-tamper.json +6 -0
  29. package/conformance/v1/expected/replay-evidence-resealed-semantic-forgery.json +6 -0
  30. package/conformance/v1/inputs/canonical-order-a.json +8 -0
  31. package/conformance/v1/inputs/canonical-order-b.json +8 -0
  32. package/conformance/v1/inputs/create-benchmark-v1-measured.json +459 -0
  33. package/conformance/v1/inputs/create-profile-v1-qualified.json +228 -0
  34. package/conformance/v1/inputs/decide-collection-failure-non-kill.json +143 -0
  35. package/conformance/v1/inputs/decide-compile-failure-non-kill.json +143 -0
  36. package/conformance/v1/inputs/decide-infra-error-non-kill.json +143 -0
  37. package/conformance/v1/inputs/decide-no-test-discovered-non-kill.json +143 -0
  38. package/conformance/v1/inputs/decide-process-crash-non-kill.json +143 -0
  39. package/conformance/v1/inputs/decide-timeout-non-kill.json +143 -0
  40. package/conformance/v1/inputs/decide-verified.json +143 -0
  41. package/conformance/v1/inputs/replay-benchmark-v1-resealed-summary-forgery.json +575 -0
  42. package/conformance/v1/inputs/replay-evidence-raw-tamper.json +201 -0
  43. package/conformance/v1/inputs/replay-evidence-resealed-semantic-forgery.json +192 -0
  44. package/conformance/v1/schemas/expected-digests.json +175 -0
  45. package/dist/cli.d.ts +9 -0
  46. package/dist/cli.d.ts.map +1 -0
  47. package/dist/cli.js +951 -0
  48. package/dist/cli.js.map +1 -0
  49. package/dist/contracts/diagnostics.d.ts +18 -0
  50. package/dist/contracts/diagnostics.d.ts.map +1 -0
  51. package/dist/contracts/diagnostics.js +13 -0
  52. package/dist/contracts/diagnostics.js.map +1 -0
  53. package/dist/contracts/index.d.ts +3908 -0
  54. package/dist/contracts/index.d.ts.map +1 -0
  55. package/dist/contracts/index.js +2569 -0
  56. package/dist/contracts/index.js.map +1 -0
  57. package/dist/contracts/runtime-doctor.d.ts +107 -0
  58. package/dist/contracts/runtime-doctor.d.ts.map +1 -0
  59. package/dist/contracts/runtime-doctor.js +91 -0
  60. package/dist/contracts/runtime-doctor.js.map +1 -0
  61. package/dist/core/index.d.ts +200 -0
  62. package/dist/core/index.d.ts.map +1 -0
  63. package/dist/core/index.js +2587 -0
  64. package/dist/core/index.js.map +1 -0
  65. package/dist/diagnostics.d.ts +7 -0
  66. package/dist/diagnostics.d.ts.map +1 -0
  67. package/dist/diagnostics.js +252 -0
  68. package/dist/diagnostics.js.map +1 -0
  69. package/dist/engine/adapters/node-test-profile.d.ts +14 -0
  70. package/dist/engine/adapters/node-test-profile.d.ts.map +1 -0
  71. package/dist/engine/adapters/node-test-profile.js +14 -0
  72. package/dist/engine/adapters/node-test-profile.js.map +1 -0
  73. package/dist/engine/adapters/node-test-runtime.d.ts +39 -0
  74. package/dist/engine/adapters/node-test-runtime.d.ts.map +1 -0
  75. package/dist/engine/adapters/node-test-runtime.js +173 -0
  76. package/dist/engine/adapters/node-test-runtime.js.map +1 -0
  77. package/dist/engine/adapters/runtime-facts.d.ts +26 -0
  78. package/dist/engine/adapters/runtime-facts.d.ts.map +1 -0
  79. package/dist/engine/adapters/runtime-facts.js +73 -0
  80. package/dist/engine/adapters/runtime-facts.js.map +1 -0
  81. package/dist/engine/connection.d.ts +22 -0
  82. package/dist/engine/connection.d.ts.map +1 -0
  83. package/dist/engine/connection.js +343 -0
  84. package/dist/engine/connection.js.map +1 -0
  85. package/dist/engine/git-regression.d.ts +25 -0
  86. package/dist/engine/git-regression.d.ts.map +1 -0
  87. package/dist/engine/git-regression.js +803 -0
  88. package/dist/engine/git-regression.js.map +1 -0
  89. package/dist/engine/index.d.ts +55 -0
  90. package/dist/engine/index.d.ts.map +1 -0
  91. package/dist/engine/index.js +2782 -0
  92. package/dist/engine/index.js.map +1 -0
  93. package/dist/engine/node-test-reporter.d.ts +2 -0
  94. package/dist/engine/node-test-reporter.d.ts.map +1 -0
  95. package/dist/engine/node-test-reporter.js +70 -0
  96. package/dist/engine/node-test-reporter.js.map +1 -0
  97. package/dist/engine/runtime-doctor.d.ts +16 -0
  98. package/dist/engine/runtime-doctor.d.ts.map +1 -0
  99. package/dist/engine/runtime-doctor.js +100 -0
  100. package/dist/engine/runtime-doctor.js.map +1 -0
  101. package/dist/evaluation/agentic-corpus.d.ts +161 -0
  102. package/dist/evaluation/agentic-corpus.d.ts.map +1 -0
  103. package/dist/evaluation/agentic-corpus.js +710 -0
  104. package/dist/evaluation/agentic-corpus.js.map +1 -0
  105. package/dist/index.d.ts +8 -0
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.js +8 -0
  108. package/dist/index.js.map +1 -0
  109. package/dist/mcp/index.d.ts +13 -0
  110. package/dist/mcp/index.d.ts.map +1 -0
  111. package/dist/mcp/index.js +391 -0
  112. package/dist/mcp/index.js.map +1 -0
  113. package/dist/mcp/stdio.d.ts +3 -0
  114. package/dist/mcp/stdio.d.ts.map +1 -0
  115. package/dist/mcp/stdio.js +13 -0
  116. package/dist/mcp/stdio.js.map +1 -0
  117. package/dist/sdk/index.d.ts +52 -0
  118. package/dist/sdk/index.d.ts.map +1 -0
  119. package/dist/sdk/index.js +224 -0
  120. package/dist/sdk/index.js.map +1 -0
  121. package/dist/version.d.ts +2 -0
  122. package/dist/version.d.ts.map +1 -0
  123. package/dist/version.js +10 -0
  124. package/dist/version.js.map +1 -0
  125. package/docs/adapter-protocol.md +196 -0
  126. package/docs/agentic-benchmark.md +118 -0
  127. package/docs/agentic-corpus-experiment-h3.md +89 -0
  128. package/docs/agentic-corpus-plan.md +105 -0
  129. package/docs/agentic-corpus-provenance.md +59 -0
  130. package/docs/agentic-test-profile-pilot.md +57 -0
  131. package/docs/agentic-test-profile-v2.md +116 -0
  132. package/docs/agentic-test-profile.md +274 -0
  133. package/docs/architecture.md +157 -0
  134. package/docs/ci.md +37 -0
  135. package/docs/client-connections.md +61 -0
  136. package/docs/conformance-v1.md +72 -0
  137. package/docs/decisions/0001-typescript-runtime.md +24 -0
  138. package/docs/developer-experience.md +55 -0
  139. package/docs/diagnostics.md +35 -0
  140. package/docs/distribution.md +40 -0
  141. package/docs/git-regression.md +39 -0
  142. package/docs/migration-repository-validation-order.md +35 -0
  143. package/docs/migration-testforge-to-assertledger.md +64 -0
  144. package/docs/project-intent.md +173 -0
  145. package/docs/proof-model.md +116 -0
  146. package/docs/reference.md +336 -0
  147. package/docs/release-1.0.md +63 -0
  148. package/docs/repository-audit.md +52 -0
  149. package/docs/repository-init.md +60 -0
  150. package/docs/research-basis.md +27 -0
  151. package/docs/roadmap.md +74 -0
  152. package/docs/runtime-doctor.md +65 -0
  153. package/docs/testexplora-calibration.md +71 -0
  154. package/examples/agentic-benchmark/benchmark-request.mjs +19 -0
  155. package/examples/agentic-benchmark/structured-phase-adapter-fixture.mjs +35 -0
  156. package/examples/agentic-profile/profile-benchmark.mjs +34 -0
  157. package/examples/agentic-profile/profile-manifest.mjs +28 -0
  158. package/examples/git-history/README.md +44 -0
  159. package/examples/git-history/create-demo.mjs +128 -0
  160. package/examples/git-history/escape-string-regexp/LICENSE +9 -0
  161. package/examples/git-history/escape-string-regexp/before.cjs.txt +11 -0
  162. package/examples/git-history/escape-string-regexp/fixed.cjs.txt +13 -0
  163. package/examples/git-history/escape-string-regexp/provenance.json +28 -0
  164. package/examples/node-test/repository/package.json +5 -0
  165. package/examples/node-test/repository/src/is-even.js +3 -0
  166. package/examples/node-test/repository/tests/base.test.js +6 -0
  167. package/examples/node-test/request.json +93 -0
  168. package/integrations/skill/SKILL.md +51 -0
  169. package/package.json +88 -0
  170. package/schemas/agentic-benchmark-acquisition-replay-result.v1.json +70 -0
  171. package/schemas/agentic-benchmark-acquisition-request.v1.json +564 -0
  172. package/schemas/agentic-benchmark-acquisition-result.v1.json +1409 -0
  173. package/schemas/agentic-benchmark-artifact.v1.json +1251 -0
  174. package/schemas/agentic-benchmark-replay-result.v1.json +84 -0
  175. package/schemas/agentic-benchmark-request.v1.json +1034 -0
  176. package/schemas/agentic-corpus-allocation-commitment-replay-result.v1.json +58 -0
  177. package/schemas/agentic-corpus-allocation-commitment.v1.json +141 -0
  178. package/schemas/agentic-corpus-allocation-replay-result.v1.json +34 -0
  179. package/schemas/agentic-corpus-allocation-request.v1.json +65 -0
  180. package/schemas/agentic-corpus-allocation-reveal.v1.json +66 -0
  181. package/schemas/agentic-corpus-allocation.v1.json +167 -0
  182. package/schemas/agentic-corpus-experiment-artifact.v1.json +329 -0
  183. package/schemas/agentic-corpus-experiment-plan-replay-result.v1.json +50 -0
  184. package/schemas/agentic-corpus-experiment-plan.v1.json +424 -0
  185. package/schemas/agentic-corpus-experiment-replay-request.v1.json +336 -0
  186. package/schemas/agentic-corpus-experiment-replay-result.v1.json +106 -0
  187. package/schemas/agentic-corpus-experiment-request.v1.json +204 -0
  188. package/schemas/agentic-corpus-provenance.v1.json +143 -0
  189. package/schemas/agentic-corpus-trust-policy.v1.json +133 -0
  190. package/schemas/agentic-profile-replay-result.v1.json +56 -0
  191. package/schemas/agentic-profile-replay-result.v2.json +63 -0
  192. package/schemas/agentic-profile-report.v1.json +961 -0
  193. package/schemas/agentic-profile-report.v2.json +1674 -0
  194. package/schemas/agentic-profile-request.v1.json +671 -0
  195. package/schemas/agentic-profile-request.v2.json +1338 -0
  196. package/schemas/evidence-manifest.v1.json +636 -0
  197. package/schemas/replay-result.v1.json +49 -0
  198. package/schemas/repository-analysis.v1.json +119 -0
  199. package/schemas/repository-audit.v1.json +811 -0
  200. package/schemas/repository-init-config.v1.json +183 -0
  201. package/schemas/repository-init-lock.v1.json +162 -0
  202. package/schemas/repository-init-result.v1.json +212 -0
  203. package/schemas/verification-request.v1.json +389 -0
@@ -0,0 +1,803 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { lstat, mkdir, mkdtemp, realpath, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { parseEvidenceManifest } from "../contracts/index.js";
7
+ import { assertSafeRelativePath, canonicalize, portablePathKey, sealManifestArtifact, } from "../core/index.js";
8
+ import { renderDiagnostics } from "../diagnostics.js";
9
+ import { verifyCampaign } from "./index.js";
10
+ const GIT_PROCESS_LIMIT = 32;
11
+ const GIT_PROCESS_TIMEOUT_MS = 5_000;
12
+ const GIT_AGGREGATE_TIMEOUT_MS = 30_000;
13
+ const GIT_PROCESS_TERMINATION_GRACE_MS = 250;
14
+ const IDENTITY_OUTPUT_LIMIT = 65_536;
15
+ const TREE_OUTPUT_LIMIT = 4_194_304;
16
+ const MAXIMUM_REPOSITORY_FILES = 10_000;
17
+ const MAXIMUM_REPOSITORY_BYTES = 104_857_600;
18
+ const MAXIMUM_BLOB_BYTES = 8_388_608;
19
+ const MAXIMUM_OVERLAY_BYTES = 1_048_576;
20
+ const MAXIMUM_CANDIDATE_BYTES = 1_048_576;
21
+ const MAXIMUM_BATCH_BYTES = 107_003_904;
22
+ const CLEANUP_OPTIONS = {
23
+ recursive: true,
24
+ force: true,
25
+ maxRetries: 5,
26
+ retryDelay: 100,
27
+ };
28
+ const PACKAGE_IDENTITY_FILES = new Set([
29
+ "package.json",
30
+ "package-lock.json",
31
+ "npm-shrinkwrap.json",
32
+ "pnpm-lock.yaml",
33
+ "yarn.lock",
34
+ "bun.lock",
35
+ "bun.lockb",
36
+ ]);
37
+ class GitBudget {
38
+ started = Date.now();
39
+ processes = 0;
40
+ claim() {
41
+ this.processes += 1;
42
+ if (this.processes > GIT_PROCESS_LIMIT)
43
+ throw new Error("GIT_PROCESS_BUDGET_EXCEEDED");
44
+ const remaining = GIT_AGGREGATE_TIMEOUT_MS - (Date.now() - this.started);
45
+ if (remaining <= 0)
46
+ throw new Error("GIT_AGGREGATE_DEADLINE_EXCEEDED");
47
+ return Math.min(GIT_PROCESS_TIMEOUT_MS, remaining);
48
+ }
49
+ }
50
+ function gitEnvironment() {
51
+ return {
52
+ PATH: process.env.PATH,
53
+ SystemRoot: process.env.SystemRoot,
54
+ WINDIR: process.env.WINDIR,
55
+ TEMP: process.env.TEMP,
56
+ TMP: process.env.TMP,
57
+ GIT_CONFIG_NOSYSTEM: "1",
58
+ GIT_ATTR_NOSYSTEM: "1",
59
+ GIT_NO_REPLACE_OBJECTS: "1",
60
+ GIT_NO_LAZY_FETCH: "1",
61
+ GIT_TERMINAL_PROMPT: "0",
62
+ GCM_INTERACTIVE: "Never",
63
+ };
64
+ }
65
+ async function runGitBytes(repository, args, budget, maximumOutputBytes, stdin) {
66
+ const timeoutMs = budget.claim();
67
+ return runBoundedProcessBytes("git", ["-C", repository, ...args], repository, gitEnvironment(), timeoutMs, maximumOutputBytes, stdin);
68
+ }
69
+ async function runBoundedProcessBytes(executable, args, cwd, environment, timeoutMs, maximumOutputBytes, stdin) {
70
+ return new Promise((resolve, reject) => {
71
+ const child = spawn(executable, args, {
72
+ cwd,
73
+ env: environment,
74
+ shell: false,
75
+ windowsHide: true,
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ });
78
+ const stdout = [];
79
+ const stderr = [];
80
+ let stdoutBytes = 0;
81
+ let stderrBytes = 0;
82
+ let exceeded = false;
83
+ let timedOut = false;
84
+ let settled = false;
85
+ let graceTimer;
86
+ const settle = (action) => {
87
+ if (settled)
88
+ return;
89
+ settled = true;
90
+ clearTimeout(timer);
91
+ if (graceTimer !== undefined)
92
+ clearTimeout(graceTimer);
93
+ action();
94
+ };
95
+ const timer = setTimeout(() => {
96
+ timedOut = true;
97
+ child.kill("SIGKILL");
98
+ graceTimer = setTimeout(() => {
99
+ child.stdin.destroy();
100
+ child.stdout.destroy();
101
+ child.stderr.destroy();
102
+ settle(() => reject(new Error("GIT_PROCESS_TIMEOUT")));
103
+ }, GIT_PROCESS_TERMINATION_GRACE_MS);
104
+ }, timeoutMs);
105
+ child.stdout.on("data", (chunk) => {
106
+ stdoutBytes += chunk.length;
107
+ if (stdoutBytes > maximumOutputBytes) {
108
+ exceeded = true;
109
+ child.kill("SIGKILL");
110
+ }
111
+ else
112
+ stdout.push(Buffer.from(chunk));
113
+ });
114
+ child.stderr.on("data", (chunk) => {
115
+ stderrBytes += chunk.length;
116
+ if (stderrBytes > IDENTITY_OUTPUT_LIMIT) {
117
+ exceeded = true;
118
+ child.kill("SIGKILL");
119
+ }
120
+ else
121
+ stderr.push(Buffer.from(chunk));
122
+ });
123
+ child.once("error", (error) => {
124
+ settle(() => reject(new Error(`GIT_PROCESS_START_FAILED:${error.message}`)));
125
+ });
126
+ child.once("close", (code, signal) => {
127
+ settle(() => {
128
+ if (timedOut)
129
+ reject(new Error("GIT_PROCESS_TIMEOUT"));
130
+ else if (exceeded)
131
+ reject(new Error("GIT_OUTPUT_BUDGET_EXCEEDED"));
132
+ else if (code !== 0)
133
+ reject(new Error(`GIT_COMMAND_FAILED:${String(code)}:${signal ?? "none"}:${Buffer.concat(stderr).toString("utf8").trim()}`));
134
+ else
135
+ resolve(Buffer.concat(stdout));
136
+ });
137
+ });
138
+ child.stdin.once("error", (error) => {
139
+ child.kill("SIGKILL");
140
+ child.stdout.destroy();
141
+ child.stderr.destroy();
142
+ settle(() => reject(new Error(`GIT_STDIN_WRITE_FAILED:${error.code ?? "UNKNOWN"}`)));
143
+ });
144
+ if (stdin === undefined)
145
+ child.stdin.end();
146
+ else
147
+ child.stdin.end(stdin);
148
+ });
149
+ }
150
+ export async function runBoundedProcessForTesting(options) {
151
+ return runBoundedProcessBytes(options.executable, options.args, options.cwd, {
152
+ PATH: process.env.PATH,
153
+ SystemRoot: process.env.SystemRoot,
154
+ WINDIR: process.env.WINDIR,
155
+ TEMP: process.env.TEMP,
156
+ TMP: process.env.TMP,
157
+ }, options.timeoutMs, IDENTITY_OUTPUT_LIMIT, options.stdin);
158
+ }
159
+ async function runGitText(repository, args, budget, maximumOutputBytes = IDENTITY_OUTPUT_LIMIT, stdin) {
160
+ const bytes = await runGitBytes(repository, args, budget, maximumOutputBytes, stdin);
161
+ try {
162
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes).trim();
163
+ }
164
+ catch {
165
+ throw new Error("GIT_OUTPUT_INVALID_UTF8");
166
+ }
167
+ }
168
+ function oneLine(value, code) {
169
+ if (value.length === 0 || value.includes("\n") || value.includes("\r"))
170
+ throw new Error(code);
171
+ return value;
172
+ }
173
+ function decodeGitText(bytes, code) {
174
+ let decoded;
175
+ try {
176
+ decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
177
+ }
178
+ catch {
179
+ throw new Error(code);
180
+ }
181
+ if (!Buffer.from(decoded, "utf8").equals(Buffer.from(bytes)))
182
+ throw new Error(code);
183
+ return decoded;
184
+ }
185
+ export function decodeGitTextForTesting(bytes) {
186
+ return decodeGitText(bytes, "GIT_TEXT_INVALID_UTF8");
187
+ }
188
+ async function resolveRevision(repository, ref, role, objectFormat, budget) {
189
+ let commit;
190
+ try {
191
+ commit = oneLine(await runGitText(repository, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], budget), "GIT_REVISION_INVALID");
192
+ }
193
+ catch {
194
+ throw new Error("GIT_REVISION_INVALID");
195
+ }
196
+ const expectedLength = objectFormat === "sha256" ? 64 : 40;
197
+ if (!new RegExp(`^[0-9a-f]{${expectedLength}}$`).test(commit))
198
+ throw new Error("GIT_REVISION_INVALID");
199
+ const tree = oneLine(await runGitText(repository, ["rev-parse", "--verify", "--end-of-options", `${commit}^{tree}`], budget), "GIT_TREE_INVALID");
200
+ const raw = await runGitBytes(repository, ["ls-tree", "-rz", "--full-tree", commit], budget, TREE_OUTPUT_LIMIT);
201
+ const entries = parseTree(raw, expectedLength);
202
+ return { role, commit, tree, entries };
203
+ }
204
+ function parseTree(raw, oidLength) {
205
+ const entries = new Map();
206
+ const portable = new Map();
207
+ const fileKeys = new Set();
208
+ const directoryKeys = new Set();
209
+ let start = 0;
210
+ while (start < raw.length) {
211
+ const end = raw.indexOf(0, start);
212
+ if (end < 0)
213
+ throw new Error("GIT_TREE_RECORD_TRUNCATED");
214
+ const record = raw.subarray(start, end);
215
+ start = end + 1;
216
+ const tab = record.indexOf(9);
217
+ if (tab < 0)
218
+ throw new Error("GIT_TREE_RECORD_INVALID");
219
+ const header = record.subarray(0, tab).toString("ascii");
220
+ const match = new RegExp(`^(\\d{6}) ([a-z]+) ([0-9a-f]{${oidLength}})$`).exec(header);
221
+ if (!match)
222
+ throw new Error("GIT_TREE_RECORD_INVALID");
223
+ const entryPath = decodeGitText(record.subarray(tab + 1), "GIT_PATH_INVALID_UTF8");
224
+ const safePath = assertSafeRelativePath(entryPath, ["."]);
225
+ if (safePath !== entryPath)
226
+ throw new Error("GIT_PATH_NOT_CANONICAL");
227
+ const segments = safePath.split("/");
228
+ for (let length = 1; length <= segments.length; length += 1) {
229
+ const spelling = segments.slice(0, length).join("/");
230
+ const key = portablePathKey(spelling);
231
+ const prior = portable.get(key);
232
+ if (prior !== undefined && prior !== spelling)
233
+ throw new Error("PORTABLE_PATH_COLLISION");
234
+ const isFile = length === segments.length;
235
+ if ((isFile && (directoryKeys.has(key) || fileKeys.has(key))) ||
236
+ (!isFile && fileKeys.has(key)))
237
+ throw new Error("GIT_PATH_TOPOLOGY_COLLISION");
238
+ portable.set(key, spelling);
239
+ if (isFile)
240
+ fileKeys.add(key);
241
+ else
242
+ directoryKeys.add(key);
243
+ }
244
+ if (match[1] !== "100644" || match[2] !== "blob") {
245
+ throw new Error("GIT_UNSUPPORTED_ENTRY_MODE");
246
+ }
247
+ entries.set(safePath, {
248
+ mode: match[1],
249
+ type: match[2],
250
+ oid: match[3],
251
+ path: safePath,
252
+ });
253
+ }
254
+ if (entries.size > MAXIMUM_REPOSITORY_FILES)
255
+ throw new Error("GIT_REPOSITORY_FILE_BUDGET_EXCEEDED");
256
+ return entries;
257
+ }
258
+ export function parseGitTreeForTesting(raw) {
259
+ return parseTree(raw, 40);
260
+ }
261
+ function batchOutputLimit(sizes, oidLength) {
262
+ let total = 0;
263
+ for (const size of sizes) {
264
+ const next = size + oidLength + 1 + 4 + 1 + String(MAXIMUM_BLOB_BYTES).length + 2;
265
+ if (!Number.isSafeInteger(next) || next < 0 || total > Number.MAX_SAFE_INTEGER - next)
266
+ throw new Error("GIT_BLOB_BATCH_OUTPUT_BUDGET_INVALID");
267
+ total += next;
268
+ }
269
+ return total;
270
+ }
271
+ export function batchOutputLimitForTesting(sizes, oidLength) {
272
+ return batchOutputLimit(sizes, oidLength);
273
+ }
274
+ async function loadBlobs(repository, revisions, budget) {
275
+ const oids = [
276
+ ...new Set(revisions.flatMap((revision) => [...revision.entries.values()].map((entry) => entry.oid))),
277
+ ];
278
+ const input = Buffer.from(`${oids.join("\n")}\n`, "ascii");
279
+ const metadataText = await runGitText(repository, ["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], budget, TREE_OUTPUT_LIMIT, input);
280
+ const metadata = metadataText.length === 0 ? [] : metadataText.split("\n");
281
+ if (metadata.length !== oids.length)
282
+ throw new Error("GIT_BLOB_METADATA_INVALID");
283
+ const expectedSizes = new Map();
284
+ const orderedSizes = [];
285
+ let expectedPayloadBytes = 0;
286
+ for (const [index, line] of metadata.entries()) {
287
+ const match = /^([0-9a-f]+) blob (\d+)$/.exec(line);
288
+ const expectedOid = oids[index];
289
+ if (!match || expectedOid === undefined || match[1] !== expectedOid)
290
+ throw new Error("GIT_BLOB_METADATA_INVALID");
291
+ const size = Number(match[2]);
292
+ if (!Number.isSafeInteger(size) || size < 0)
293
+ throw new Error("GIT_BLOB_METADATA_INVALID");
294
+ if (size > MAXIMUM_BLOB_BYTES)
295
+ throw new Error("GIT_BLOB_BUDGET_EXCEEDED_BEFORE_CONTENT");
296
+ expectedPayloadBytes += size;
297
+ if (expectedPayloadBytes > MAXIMUM_BATCH_BYTES)
298
+ throw new Error("GIT_BLOB_BATCH_BUDGET_EXCEEDED_BEFORE_CONTENT");
299
+ expectedSizes.set(expectedOid, size);
300
+ orderedSizes.push(size);
301
+ }
302
+ const maximumBatchOutputBytes = batchOutputLimit(orderedSizes, oids[0]?.length ?? 40);
303
+ const raw = await runGitBytes(repository, ["cat-file", "--batch"], budget, maximumBatchOutputBytes, input);
304
+ const blobs = new Map();
305
+ let offset = 0;
306
+ for (const expectedOid of oids) {
307
+ const newline = raw.indexOf(10, offset);
308
+ if (newline < 0)
309
+ throw new Error("GIT_BLOB_BATCH_TRUNCATED");
310
+ const header = raw.subarray(offset, newline).toString("ascii");
311
+ const match = /^([0-9a-f]+) blob (\d+)$/.exec(header);
312
+ if (!match || match[1] !== expectedOid)
313
+ throw new Error("GIT_BLOB_BATCH_INVALID");
314
+ const size = Number(match[2]);
315
+ if (!Number.isSafeInteger(size) || size !== expectedSizes.get(expectedOid))
316
+ throw new Error("GIT_BLOB_BATCH_INVALID");
317
+ const begin = newline + 1;
318
+ const end = begin + size;
319
+ if (end >= raw.length || raw[end] !== 10)
320
+ throw new Error("GIT_BLOB_BATCH_TRUNCATED");
321
+ blobs.set(expectedOid, Buffer.from(raw.subarray(begin, end)));
322
+ offset = end + 1;
323
+ }
324
+ if (offset !== raw.length)
325
+ throw new Error("GIT_BLOB_BATCH_INVALID");
326
+ for (const revision of revisions) {
327
+ let total = 0;
328
+ for (const entry of revision.entries.values()) {
329
+ const bytes = blobs.get(entry.oid);
330
+ if (bytes === undefined)
331
+ throw new Error("GIT_BLOB_MISSING");
332
+ entry.bytes = bytes;
333
+ total += bytes.length;
334
+ }
335
+ if (total > MAXIMUM_REPOSITORY_BYTES)
336
+ throw new Error("GIT_REPOSITORY_BYTES_BUDGET_EXCEEDED");
337
+ }
338
+ }
339
+ function requireUtf8(entry, maximumBytes, code) {
340
+ const bytes = entry.bytes;
341
+ if (bytes === undefined || bytes.length > maximumBytes)
342
+ throw new Error(code);
343
+ return decodeGitText(bytes, `${code}_INVALID_UTF8`);
344
+ }
345
+ function sameBytes(left, right) {
346
+ return left !== undefined && right !== undefined && left.oid === right.oid;
347
+ }
348
+ function assertPackageBoundary(revisions) {
349
+ const identityPaths = new Set(revisions.flatMap((revision) => [...revision.entries.keys()].filter((entryPath) => PACKAGE_IDENTITY_FILES.has(path.posix.basename(entryPath)))));
350
+ for (const file of identityPaths) {
351
+ const identities = revisions.map((revision) => revision.entries.get(file)?.oid ?? null);
352
+ if (!identities.every((identity) => identity === identities[0]))
353
+ throw new Error("GIT_PACKAGE_IDENTITY_DRIFT_UNSUPPORTED");
354
+ }
355
+ for (const packagePath of [...identityPaths].filter((entryPath) => path.posix.basename(entryPath) === "package.json")) {
356
+ const packageEntry = revisions[0]?.entries.get(packagePath);
357
+ if (packageEntry === undefined)
358
+ continue;
359
+ let packageJson;
360
+ try {
361
+ packageJson = JSON.parse(requireUtf8(packageEntry, MAXIMUM_BLOB_BYTES, "GIT_PACKAGE_JSON_INVALID"));
362
+ }
363
+ catch (error) {
364
+ if (error instanceof SyntaxError)
365
+ throw new Error("GIT_PACKAGE_JSON_INVALID");
366
+ throw error;
367
+ }
368
+ if (typeof packageJson !== "object" || packageJson === null || Array.isArray(packageJson))
369
+ throw new Error("GIT_PACKAGE_JSON_INVALID");
370
+ const value = packageJson;
371
+ for (const key of ["dependencies", "optionalDependencies", "peerDependencies"]) {
372
+ const declarations = value[key];
373
+ if (typeof declarations === "object" &&
374
+ declarations !== null &&
375
+ Object.keys(declarations).length > 0)
376
+ throw new Error("GIT_RUNTIME_DEPENDENCIES_UNSUPPORTED");
377
+ }
378
+ }
379
+ }
380
+ function validateTopology(revisions, selectedTest, baseTests) {
381
+ const projection = revisions.map((revision) => [...revision.entries.keys()].filter((entryPath) => entryPath !== selectedTest).sort());
382
+ const canonical = JSON.stringify(projection[0]);
383
+ if (!projection.every((paths) => JSON.stringify(paths) === canonical))
384
+ throw new Error("GIT_UNSUPPORTED_TOPOLOGY_DRIFT");
385
+ for (const baseTest of baseTests) {
386
+ const entries = revisions.map((revision) => revision.entries.get(baseTest));
387
+ if (entries.some((entry) => entry === undefined) ||
388
+ !entries.every((entry) => sameBytes(entry, entries[0])))
389
+ throw new Error("GIT_BASE_TEST_DRIFT_UNSUPPORTED");
390
+ }
391
+ }
392
+ function validateOptions(options) {
393
+ if (options.allowUnsafeExecution !== true)
394
+ throw new Error("GIT_REGRESSION_UNSAFE_EXECUTION_NOT_ALLOWED");
395
+ const test = assertSafeRelativePath(options.test, ["."]);
396
+ if (!/\.(?:cjs|mjs|js)$/.test(test))
397
+ throw new Error("GIT_REGRESSION_TEST_TYPE_UNSUPPORTED");
398
+ const baseTests = options.baseTests.map((baseTest) => assertSafeRelativePath(baseTest, ["."]));
399
+ if (baseTests.length === 0 || new Set(baseTests.map(portablePathKey)).size !== baseTests.length)
400
+ throw new Error("GIT_REGRESSION_BASE_TESTS_INVALID");
401
+ if (baseTests.some((baseTest) => portablePathKey(baseTest) === portablePathKey(test)))
402
+ throw new Error("GIT_REGRESSION_BASE_TESTS_INVALID");
403
+ const out = assertSafeRelativePath(options.out, ["."]);
404
+ if (out.split("/").some((segment) => portablePathKey(segment) === ".git"))
405
+ throw new Error("GIT_REGRESSION_OUTPUT_METADATA_FORBIDDEN");
406
+ if (options.neutralReason.trim().length === 0 ||
407
+ options.neutralReason.length > 256 ||
408
+ [...options.neutralReason].some((character) => {
409
+ const codePoint = character.codePointAt(0) ?? 0;
410
+ return codePoint < 32 || (codePoint >= 127 && codePoint <= 159);
411
+ }))
412
+ throw new Error("GIT_REGRESSION_NEUTRAL_REASON_INVALID");
413
+ return { test, baseTests, out };
414
+ }
415
+ function provenance(revision, objectFormat, reason) {
416
+ const value = canonicalize({
417
+ format: "assertledger-git-regression/1",
418
+ role: revision.role,
419
+ commit: revision.commit,
420
+ tree: revision.tree,
421
+ objectFormat,
422
+ projection: "SELECTED_TEST_REMOVED_FOR_CONTROLS",
423
+ reason,
424
+ });
425
+ if (value.length > 1_024)
426
+ throw new Error("GIT_REGRESSION_PROVENANCE_TOO_LARGE");
427
+ return value;
428
+ }
429
+ function changedFiles(base, revision, omitted) {
430
+ const files = [];
431
+ let bytes = 0;
432
+ for (const [entryPath, entry] of revision.entries) {
433
+ if (omitted.has(portablePathKey(entryPath)))
434
+ continue;
435
+ if (entry.oid === base.entries.get(entryPath)?.oid)
436
+ continue;
437
+ const content = requireUtf8(entry, MAXIMUM_OVERLAY_BYTES, "GIT_WORLD_OVERLAY_UNSUPPORTED");
438
+ bytes += Buffer.byteLength(content);
439
+ if (bytes > MAXIMUM_OVERLAY_BYTES)
440
+ throw new Error("GIT_WORLD_OVERLAY_BUDGET_EXCEEDED");
441
+ files.push({ path: entryPath, content });
442
+ }
443
+ return files;
444
+ }
445
+ function assertBaseControlsDiscoveredTests(manifest) {
446
+ for (const worldId of ["fixed", "known-bug", "neutral"]) {
447
+ const observations = manifest.observations.filter((observation) => observation.candidateId === null && observation.worldId === worldId);
448
+ if (observations.length !== 2 ||
449
+ observations.some((observation) => observation.outcome !== "PASS" ||
450
+ observation.testsDiscovered < 1 ||
451
+ (observation.attempt !== 1 && observation.attempt !== 2)))
452
+ throw new Error("GIT_BASE_TESTS_NOT_DISCOVERED");
453
+ }
454
+ }
455
+ async function materializeBase(root, revision, selectedTest) {
456
+ let total = 0;
457
+ for (const entry of revision.entries.values()) {
458
+ if (entry.path === selectedTest)
459
+ continue;
460
+ const bytes = entry.bytes;
461
+ if (bytes === undefined)
462
+ throw new Error("GIT_BLOB_MISSING");
463
+ total += bytes.length;
464
+ if (total > MAXIMUM_REPOSITORY_BYTES)
465
+ throw new Error("GIT_REPOSITORY_BYTES_BUDGET_EXCEEDED");
466
+ const target = path.join(root, ...entry.path.split("/"));
467
+ await mkdir(path.dirname(target), { recursive: true });
468
+ await writeFile(target, bytes, { flag: "wx" });
469
+ }
470
+ }
471
+ async function reserveOutputTarget(repository, output, sourcePaths, metadataRoots) {
472
+ const target = path.resolve(repository, ...output.split("/"));
473
+ const relative = path.relative(repository, target);
474
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
475
+ throw new Error("GIT_REGRESSION_OUTPUT_OUTSIDE_REPOSITORY");
476
+ const isWithin = (candidate, root) => {
477
+ const relation = path.relative(root, candidate);
478
+ return (relation === "" ||
479
+ (!relation.startsWith(`..${path.sep}`) && relation !== ".." && !path.isAbsolute(relation)));
480
+ };
481
+ if (metadataRoots.some((root) => isWithin(target, root)))
482
+ throw new Error("GIT_REGRESSION_OUTPUT_METADATA_FORBIDDEN");
483
+ const outputKey = portablePathKey(output);
484
+ if (sourcePaths.some((source) => {
485
+ const key = portablePathKey(source);
486
+ return (key === outputKey || key.startsWith(`${outputKey}/`) || outputKey.startsWith(`${key}/`));
487
+ }))
488
+ throw new Error("GIT_REGRESSION_OUTPUT_OVERLAPS_SOURCE");
489
+ try {
490
+ await lstat(target);
491
+ throw new Error("GIT_REGRESSION_OUTPUT_EXISTS");
492
+ }
493
+ catch (error) {
494
+ if (error.code !== "ENOENT")
495
+ throw error;
496
+ }
497
+ let current = repository;
498
+ for (const segment of output.split("/").slice(0, -1)) {
499
+ current = path.join(current, segment);
500
+ try {
501
+ const information = await lstat(current);
502
+ if (information.isSymbolicLink() || !information.isDirectory())
503
+ throw new Error("GIT_REGRESSION_OUTPUT_PARENT_UNSAFE");
504
+ const resolved = await realpath(current);
505
+ if (metadataRoots.some((root) => isWithin(resolved, root)))
506
+ throw new Error("GIT_REGRESSION_OUTPUT_METADATA_FORBIDDEN");
507
+ const rel = path.relative(repository, resolved);
508
+ if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
509
+ throw new Error("GIT_REGRESSION_OUTPUT_PARENT_UNSAFE");
510
+ }
511
+ catch (error) {
512
+ if (error.code !== "ENOENT")
513
+ throw error;
514
+ break;
515
+ }
516
+ }
517
+ await mkdir(path.dirname(target), { recursive: true });
518
+ const resolvedParent = await realpath(path.dirname(target));
519
+ const parentRelative = path.relative(repository, resolvedParent);
520
+ if (parentRelative === ".." ||
521
+ parentRelative.startsWith(`..${path.sep}`) ||
522
+ path.isAbsolute(parentRelative))
523
+ throw new Error("GIT_REGRESSION_OUTPUT_PARENT_UNSAFE");
524
+ try {
525
+ await mkdir(target, { recursive: false });
526
+ }
527
+ catch (error) {
528
+ if (error.code === "EEXIST")
529
+ throw new Error("GIT_REGRESSION_OUTPUT_EXISTS");
530
+ throw error;
531
+ }
532
+ return target;
533
+ }
534
+ function markdown(value) {
535
+ return value
536
+ .replace(/[\\`*_{}[\]()<>#+.!|~-]/g, "\\$&")
537
+ .replaceAll("\r", " ")
538
+ .replaceAll("\n", " ");
539
+ }
540
+ function resolvedWorldCommit(manifest, worldId) {
541
+ const provenance = manifest.evidenceContext.worlds.find((world) => world.id === worldId)?.provenance;
542
+ if (provenance === undefined)
543
+ throw new Error("GIT_REGRESSION_PROVENANCE_INVALID");
544
+ try {
545
+ const value = JSON.parse(provenance);
546
+ if (typeof value !== "object" ||
547
+ value === null ||
548
+ !("commit" in value) ||
549
+ typeof value.commit !== "string")
550
+ throw new Error("GIT_REGRESSION_PROVENANCE_INVALID");
551
+ return value.commit;
552
+ }
553
+ catch (error) {
554
+ if (error instanceof Error && error.message === "GIT_REGRESSION_PROVENANCE_INVALID")
555
+ throw error;
556
+ throw new Error("GIT_REGRESSION_PROVENANCE_INVALID");
557
+ }
558
+ }
559
+ function inlineCode(value) {
560
+ const longestBackticks = Math.max(0, ...[...value.matchAll(/`+/g)].map((match) => match[0].length));
561
+ const fence = "`".repeat(longestBackticks + 1);
562
+ return `${fence} ${value} ${fence}`;
563
+ }
564
+ function shellArgument(value) {
565
+ const escaped = value.replaceAll("'", process.platform === "win32" ? "''" : "'\"'\"'");
566
+ return `'${escaped}'`;
567
+ }
568
+ export function renderGitRegressionSummary(manifest, options) {
569
+ const targetCommit = resolvedWorldCommit(manifest, "known-bug");
570
+ const referenceCommit = resolvedWorldCommit(manifest, "fixed");
571
+ const manifestPath = path.resolve(options.repository, options.out, "manifest.json");
572
+ const replayCommand = `assertledger replay ${shellArgument(manifestPath)} --json`;
573
+ const detection = manifest.observations
574
+ .filter((observation) => observation.candidateId === "git-regression-candidate" &&
575
+ observation.worldId === "known-bug")
576
+ .map((observation) => `attempt ${observation.attempt}: ${observation.outcome}, attributed=${String(observation.attributed)}`)
577
+ .join("; ");
578
+ return [
579
+ "# AssertLedger Git regression qualification",
580
+ "",
581
+ `- Verdict: **${manifest.decision.status}**`,
582
+ `- Candidate: ${inlineCode(options.test)}`,
583
+ `- Known bug commit: \`${targetCommit}\``,
584
+ `- Fixed commit: \`${referenceCommit}\``,
585
+ `- Known-bug observations: ${detection || "none"}`,
586
+ `- Reason codes: ${manifest.decision.reasonCodes.join(", ") || "none"}`,
587
+ ...manifest.candidates.map((candidate) => `- Candidate reasons (${candidate.id}): ${candidate.reasonCodes.join(", ") || "none"}`),
588
+ `- Neutral reason: ${markdown(options.neutralReason)}`,
589
+ "- Execution: **UNSANDBOXED trusted-local** (explicit operator opt-in)",
590
+ "",
591
+ renderDiagnostics([
592
+ ...manifest.decision.reasonCodes,
593
+ ...manifest.candidates.flatMap((candidate) => candidate.reasonCodes),
594
+ ]),
595
+ "",
596
+ "## Limitations:",
597
+ "",
598
+ ...manifest.limitations.map((limitation) => `- ${markdown(limitation)}`),
599
+ "",
600
+ "The saved executed request points to a removed disposable snapshot and is inspection evidence, not a standalone rerun input.",
601
+ `Replay (${process.platform === "win32" ? "PowerShell" : "POSIX shell"}): ${inlineCode(replayCommand)}`,
602
+ "A REJECTED verdict only describes this declared fault model; it does not prove that the test has no value elsewhere.",
603
+ "",
604
+ ].join("\n");
605
+ }
606
+ async function writeEvidence(target, manifest, request, summaryText) {
607
+ const pendingManifest = path.join(target, `.manifest-${randomUUID()}.tmp`);
608
+ const ownedFiles = [];
609
+ let completed = false;
610
+ try {
611
+ const executedRequest = path.join(target, "executed-request.json");
612
+ await writeFile(executedRequest, `${canonicalize(request)}\n`, {
613
+ flag: "wx",
614
+ });
615
+ ownedFiles.push(executedRequest);
616
+ const summaryPath = path.join(target, "summary.md");
617
+ await writeFile(summaryPath, summaryText, { flag: "wx" });
618
+ ownedFiles.push(summaryPath);
619
+ await writeFile(pendingManifest, `${canonicalize(manifest)}\n`, { flag: "wx" });
620
+ ownedFiles.push(pendingManifest);
621
+ await rename(pendingManifest, path.join(target, "manifest.json"));
622
+ completed = true;
623
+ }
624
+ finally {
625
+ if (!completed) {
626
+ for (const ownedFile of ownedFiles.reverse())
627
+ await rm(ownedFile, { force: true });
628
+ }
629
+ }
630
+ }
631
+ async function cleanupReservedOutput(target) {
632
+ try {
633
+ await rmdir(target);
634
+ }
635
+ catch (error) {
636
+ const code = error.code;
637
+ if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST")
638
+ throw error;
639
+ }
640
+ }
641
+ export async function qualifyGitRegression(options) {
642
+ const validated = validateOptions(options);
643
+ const repository = await realpath(path.resolve(options.repository));
644
+ if (!(await stat(repository)).isDirectory())
645
+ throw new Error("GIT_REPOSITORY_INVALID");
646
+ const budget = new GitBudget();
647
+ const topLevel = await realpath(oneLine(await runGitText(repository, ["rev-parse", "--show-toplevel"], budget), "GIT_REPOSITORY_INVALID"));
648
+ if (topLevel !== repository)
649
+ throw new Error("GIT_REPOSITORY_ROOT_REQUIRED");
650
+ const gitDirectory = await realpath(await runGitText(repository, ["rev-parse", "--absolute-git-dir"], budget));
651
+ const commonDirectoryValue = await runGitText(repository, ["rev-parse", "--git-common-dir"], budget);
652
+ const gitCommonDirectory = await realpath(path.isAbsolute(commonDirectoryValue)
653
+ ? commonDirectoryValue
654
+ : path.resolve(repository, commonDirectoryValue));
655
+ const objectFormat = oneLine(await runGitText(repository, ["rev-parse", "--show-object-format"], budget), "GIT_OBJECT_FORMAT_INVALID");
656
+ if (objectFormat !== "sha1" && objectFormat !== "sha256")
657
+ throw new Error("GIT_OBJECT_FORMAT_INVALID");
658
+ const afterRef = options.after ?? "HEAD";
659
+ const reference = await resolveRevision(repository, afterRef, "reference", objectFormat, budget);
660
+ const target = await resolveRevision(repository, options.before, "target", objectFormat, budget);
661
+ const neutral = await resolveRevision(repository, options.neutral, "neutral", objectFormat, budget);
662
+ if (target.tree === reference.tree)
663
+ throw new Error("GIT_BEFORE_AFTER_IDENTICAL");
664
+ try {
665
+ await runGitText(repository, ["merge-base", "--is-ancestor", target.commit, reference.commit], budget);
666
+ }
667
+ catch {
668
+ throw new Error("GIT_BEFORE_NOT_ANCESTOR_OF_AFTER");
669
+ }
670
+ await loadBlobs(repository, [reference, target, neutral], budget);
671
+ validateTopology([reference, target, neutral], validated.test, validated.baseTests);
672
+ assertPackageBoundary([reference, target, neutral]);
673
+ const candidateEntry = reference.entries.get(validated.test);
674
+ if (candidateEntry === undefined)
675
+ throw new Error("GIT_REGRESSION_TEST_MISSING_AT_AFTER");
676
+ const candidateContent = requireUtf8(candidateEntry, MAXIMUM_CANDIDATE_BYTES, "GIT_REGRESSION_CANDIDATE_BUDGET_EXCEEDED");
677
+ const outputTarget = await reserveOutputTarget(repository, validated.out, [...reference.entries.keys()], [gitDirectory, gitCommonDirectory]);
678
+ const omitted = new Set([validated.test, ...validated.baseTests].map(portablePathKey));
679
+ let temporaryRoot;
680
+ let outputCompleted = false;
681
+ let operationError;
682
+ let cleanupError;
683
+ let result;
684
+ try {
685
+ temporaryRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "assertledger-git-regression-")));
686
+ const snapshot = path.join(temporaryRoot, "repository");
687
+ await mkdir(snapshot, { recursive: false });
688
+ await materializeBase(snapshot, reference, validated.test);
689
+ const request = {
690
+ schemaVersion: "1.0.0",
691
+ repository: { root: snapshot, exclude: ["node_modules", ".git", ".testforge"] },
692
+ adapter: {
693
+ kind: "node-test",
694
+ executable: process.execPath,
695
+ baseTestFiles: validated.baseTests,
696
+ },
697
+ isolation: {
698
+ kind: "trusted-local",
699
+ acknowledgedUnsafeExecution: true,
700
+ environmentAllowlist: ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"],
701
+ },
702
+ candidateRoots: [validated.test],
703
+ budgets: {
704
+ maximumCandidates: 1,
705
+ maximumWorlds: 3,
706
+ maximumExecutions: 12,
707
+ maximumRepositoryFiles: MAXIMUM_REPOSITORY_FILES,
708
+ maximumRepositoryBytes: MAXIMUM_REPOSITORY_BYTES,
709
+ maximumWorldOverlayBytes: MAXIMUM_OVERLAY_BYTES,
710
+ maximumCandidateBytes: MAXIMUM_CANDIDATE_BYTES,
711
+ maximumTotalCandidateBytes: MAXIMUM_CANDIDATE_BYTES,
712
+ timeoutMsPerExecution: 5_000,
713
+ maximumOutputBytes: 65_536,
714
+ },
715
+ policy: {
716
+ policyVersion: "1.0.0",
717
+ requiredAttempts: 2,
718
+ minimumTargetWeightPermille: 1_000,
719
+ maximumSelectedCandidates: 1,
720
+ acceptedTargetOutcomes: ["ASSERTION_FAILURE"],
721
+ },
722
+ worlds: [
723
+ {
724
+ id: "fixed",
725
+ kind: "REFERENCE",
726
+ required: true,
727
+ weight: 0,
728
+ provenance: provenance(reference, objectFormat, "fixed revision with selected test removed for controls"),
729
+ files: [],
730
+ },
731
+ {
732
+ id: "known-bug",
733
+ kind: "TARGET",
734
+ required: true,
735
+ weight: 1,
736
+ provenance: provenance(target, objectFormat, "known bug revision projected onto the fixed snapshot"),
737
+ files: changedFiles(reference, target, omitted),
738
+ },
739
+ {
740
+ id: "neutral",
741
+ kind: "NEUTRAL",
742
+ required: true,
743
+ weight: 0,
744
+ provenance: provenance(neutral, objectFormat, options.neutralReason),
745
+ files: changedFiles(reference, neutral, omitted),
746
+ },
747
+ ],
748
+ candidates: [
749
+ {
750
+ id: "git-regression-candidate",
751
+ files: [{ path: validated.test, content: candidateContent }],
752
+ },
753
+ ],
754
+ };
755
+ const sourceManifest = parseEvidenceManifest(await verifyCampaign(request));
756
+ assertBaseControlsDiscoveredTests(sourceManifest);
757
+ const limitations = [
758
+ ...sourceManifest.limitations,
759
+ "Git regression qualification covers committed revisions only; dirty checkout content is ignored.",
760
+ "The first Git slice supports node:test JavaScript with built-in and relative imports and no runtime dependencies.",
761
+ neutral.tree === reference.tree
762
+ ? "The neutral control repeats the fixed tree and provides no independent robustness evidence."
763
+ : "The operator supplied the neutral revision and reason; AssertLedger does not infer its independence.",
764
+ "Manifest integrity binds recorded Git provenance but replay does not independently authenticate Git objects or execution.",
765
+ ];
766
+ const manifest = parseEvidenceManifest(sealManifestArtifact({ ...sourceManifest, limitations }));
767
+ await rm(temporaryRoot, CLEANUP_OPTIONS);
768
+ temporaryRoot = undefined;
769
+ await writeEvidence(outputTarget, manifest, request, renderGitRegressionSummary(manifest, options));
770
+ outputCompleted = true;
771
+ result = manifest;
772
+ }
773
+ catch (error) {
774
+ operationError = error;
775
+ }
776
+ finally {
777
+ try {
778
+ if (temporaryRoot !== undefined)
779
+ await rm(temporaryRoot, CLEANUP_OPTIONS);
780
+ }
781
+ catch (error) {
782
+ cleanupError = error;
783
+ }
784
+ finally {
785
+ if (!outputCompleted) {
786
+ try {
787
+ await cleanupReservedOutput(outputTarget);
788
+ }
789
+ catch (error) {
790
+ cleanupError ??= error;
791
+ }
792
+ }
793
+ }
794
+ }
795
+ if (operationError !== undefined)
796
+ throw operationError;
797
+ if (cleanupError !== undefined)
798
+ throw cleanupError;
799
+ if (result === undefined)
800
+ throw new Error("GIT_REGRESSION_RESULT_MISSING");
801
+ return result;
802
+ }
803
+ //# sourceMappingURL=git-regression.js.map