@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451

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 (165) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +70 -22
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +192 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +29 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -0,0 +1,661 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { attemptDatabaseCapabilityEnvironment, attemptDatabaseCapabilitySecrets, } from "../runtime/attempt-database-broker.js";
7
+ import { assertHostDatabaseCapabilityBoundary } from "../runtime/host-database-capability-boundary.js";
8
+ import { nativeTestPostgresFallbackEnvironment, parseNativeTestPostgresFallback, } from "../runtime/native-test-postgres-fallback.js";
9
+ import { hostRuntimeLegacyTestPostgresEnvironment, hostRuntimeTestPostgresAdminEnvironment, } from "../runtime/host-runtime-contract.js";
10
+ import { redactText } from "../shared/redaction/redaction.js";
11
+ import { findUnreferencedNewExports } from "./unreferenced-exports.js";
12
+ import { ForgeFailureError, sanitizeFailureError, } from "./session.types.js";
13
+ const execFileAsync = promisify(execFile);
14
+ const contractRelativePath = ".forge/validation/ci-parity.v1.json";
15
+ const requiredCheckIds = new Set([
16
+ "file-size-inventory",
17
+ "owned-suite-registration",
18
+ "dotnet-build-and-unit",
19
+ "openapi-and-generated-types",
20
+ "client",
21
+ "bridge",
22
+ "playwright-e2e",
23
+ "github-provider-policy",
24
+ ]);
25
+ const currentApprovedContractDigest = "378c767c7977c850140ffb19d69db41da62d0187c37a33828d6cda8803a61c83";
26
+ // Comment repair ran the whole ApiComponent and Orchestration tiers under this contract.
27
+ // Story branches cut before the affected-tests contract still carry it.
28
+ const wholeTierCommentRepairContractDigest = "024de3648c907db9c0eab13def9a4f91e81148433e0a2ba7f44776446421054a";
29
+ // Contracts that declare the comment-repair-dotnet-regression check.
30
+ const commentRepairRegressionContractDigests = new Set([
31
+ currentApprovedContractDigest,
32
+ wholeTierCommentRepairContractDigest,
33
+ ]);
34
+ const compatibleApprovedContractDigests = new Set([
35
+ wholeTierCommentRepairContractDigest,
36
+ // Preserve dispatches pinned before comment repair gained all .NET regression tiers.
37
+ "c0014ce6abc0e0b2e66dd2bcf6cb14c47d28506444c73f87d1b76b3d1c06027b",
38
+ // The contract immediately before Contract and Infrastructure tiers joined pre-publication parity.
39
+ "06d1ca5ed48614298ae53d044d725109fe3a316a88381d361ef704681e208bbd",
40
+ // The Docker-prerequisite contract immediately before native PostgreSQL QA isolation.
41
+ "02bc1eb622fce173b00d14e15930c841f99678500858da588ec37fe21a7bec5c",
42
+ // The contract used by Stories prepared before prerequisite readiness was made explicit.
43
+ "0e8cfb2b1c1ab52bea1475dabed95aa5a2977bdd3c8500a6895632316dba0de5",
44
+ ]);
45
+ const approvedContractDigests = new Set([
46
+ currentApprovedContractDigest,
47
+ ...compatibleApprovedContractDigests,
48
+ ]);
49
+ const ansiColorSequence = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu");
50
+ export class PrePublicationValidationError extends ForgeFailureError {
51
+ checkId;
52
+ exitCode;
53
+ command;
54
+ args;
55
+ detail;
56
+ code = "prepublication_validation_failed";
57
+ constructor(checkId, exitCode, command, args, detail) {
58
+ super("prepublication_validation_failed", `prepublication_validation_failed: ${checkId} exited ${exitCode}${detail ? `: ${detail}` : ""}`, "bridge");
59
+ this.checkId = checkId;
60
+ this.exitCode = exitCode;
61
+ this.command = command;
62
+ this.args = args;
63
+ this.detail = detail;
64
+ this.name = "PrePublicationValidationError";
65
+ }
66
+ }
67
+ export class PrePublicationInfrastructureError extends ForgeFailureError {
68
+ checkId;
69
+ exitCode;
70
+ command;
71
+ args;
72
+ detail;
73
+ code = "prepublication_infrastructure_failed";
74
+ constructor(checkId, exitCode, command, args, detail) {
75
+ super("prepublication_infrastructure_failed", `prepublication_infrastructure_failed: ${checkId} exited ${exitCode}${detail ? `: ${detail}` : ""}`, "bridge");
76
+ this.checkId = checkId;
77
+ this.exitCode = exitCode;
78
+ this.command = command;
79
+ this.args = args;
80
+ this.detail = detail;
81
+ this.name = "PrePublicationInfrastructureError";
82
+ }
83
+ }
84
+ class ProcessPrePublicationCommandRunner {
85
+ async run(command, args, cwd, signal, environment) {
86
+ try {
87
+ const windowsBatch = process.platform === "win32" && (command === "npm" || command === "pnpm");
88
+ const executable = windowsBatch ? (process.env.ComSpec ?? "cmd.exe") : command;
89
+ const commandArgs = windowsBatch
90
+ ? ["/d", "/s", "/c", [`${command}.cmd`, ...args].map(quoteWindowsCommandArgument).join(" ")]
91
+ : [...args];
92
+ const result = await execFileAsync(executable, commandArgs, {
93
+ cwd,
94
+ encoding: "utf8",
95
+ maxBuffer: 4 * 1024 * 1024,
96
+ signal,
97
+ env: prePublicationChildEnvironment(environment),
98
+ });
99
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
100
+ }
101
+ catch (error) {
102
+ const failure = error;
103
+ if (failure.name === "AbortError")
104
+ throw error;
105
+ if (failure.code === "ENOENT") {
106
+ return { exitCode: 127, stderr: `executable_not_found: ${command}` };
107
+ }
108
+ return {
109
+ exitCode: typeof failure.code === "number" ? failure.code : 1,
110
+ stdout: failure.stdout,
111
+ stderr: failure.stderr ?? failure.message,
112
+ };
113
+ }
114
+ }
115
+ }
116
+ function quoteWindowsCommandArgument(value) {
117
+ return /^[A-Za-z0-9_./:@=-]+$/u.test(value) ? value : `"${value.replaceAll('"', '""')}"`;
118
+ }
119
+ function prePublicationChildEnvironment(environment) {
120
+ const child = {};
121
+ const privilegedNames = new Set([
122
+ hostRuntimeTestPostgresAdminEnvironment.toLowerCase(),
123
+ hostRuntimeLegacyTestPostgresEnvironment.toLowerCase(),
124
+ ]);
125
+ for (const [name, value] of Object.entries(process.env)) {
126
+ if (typeof value === "string" && !privilegedNames.has(name.toLowerCase())) {
127
+ child[name] = value;
128
+ }
129
+ }
130
+ return { ...child, ...environment };
131
+ }
132
+ export class PrePublicationValidator {
133
+ commands;
134
+ dockerEngineProbe;
135
+ nativeTestPostgresFallback;
136
+ constructor(commands = new ProcessPrePublicationCommandRunner(), dockerEngineProbe, nativeTestPostgresFallback = () => null) {
137
+ this.commands = commands;
138
+ this.dockerEngineProbe = dockerEngineProbe;
139
+ this.nativeTestPostgresFallback = nativeTestPostgresFallback;
140
+ }
141
+ async validate(worktreePath, profile, capabilities, emit, signal, execution) {
142
+ const checksExports = profile === "implementation" || profile === "comment_repair";
143
+ const contract = await loadValidationContract(worktreePath);
144
+ if (contract === null) {
145
+ await emit?.(validationEvent("prepublication_validation_legacy_contract", "legacy-provider-validation", `This checkout predates ${contractRelativePath}; Bridge preserved legacy provider validation without source mutation.`));
146
+ if (checksExports) {
147
+ const legacy = await prepareValidationExecution(execution);
148
+ try {
149
+ await this.assertNoTestOnlyExports(worktreePath, emit, signal, legacy.execution);
150
+ }
151
+ finally {
152
+ await legacy.cleanup();
153
+ }
154
+ }
155
+ return [];
156
+ }
157
+ const scoped = await prepareValidationExecution(execution);
158
+ try {
159
+ if (checksExports)
160
+ await this.assertNoTestOnlyExports(worktreePath, emit, signal, scoped.execution);
161
+ const contractDigest = validationContractDigest(contract);
162
+ await this.assertContractMatchesDispatchBaseline(worktreePath, contractDigest, signal, scoped.execution);
163
+ if (contractDigest !== currentApprovedContractDigest) {
164
+ await emit?.(validationEvent("prepublication_validation_compatible_contract", "validation-contract", `This checkout is pinned to approved compatible CI parity contract ${contractDigest}; the host's current contract is ${currentApprovedContractDigest}.`));
165
+ }
166
+ const changedPaths = await this.changedPaths(worktreePath, signal, scoped.execution);
167
+ const selected = applicableChecks(contract, profile, changedPaths);
168
+ const completed = [];
169
+ for (const check of selected) {
170
+ if (check.providerOnly) {
171
+ await emit?.(validationEvent("prepublication_validation_provider_only", check.id, `Preserved provider-only validation '${check.id}' for the provider check run.`));
172
+ continue;
173
+ }
174
+ if (check.requiredCapability && !capabilities.has(check.requiredCapability)) {
175
+ throw new ForgeFailureError("prepublication_capability_unavailable", `prepublication_capability_unavailable: ${check.id} requires ${check.requiredCapability}; publication is holding until a capable runner is available.`, "bridge");
176
+ }
177
+ const postgresAdminConnection = await this.ensurePrerequisite(check, scoped.execution?.environment, scoped.postgresAdminConnection, emit) ?? scoped.postgresAdminConnection;
178
+ await emit?.(validationEvent("prepublication_validation_started", check.id, `Running pre-publication CI parity check '${check.id}'.`));
179
+ for (const step of check.steps) {
180
+ let result = await this.runStep(check.id, step, worktreePath, signal, scoped.execution, postgresAdminConnection);
181
+ await this.throwIfPrerequisiteWasLost(check, result, scoped.execution?.environment, postgresAdminConnection, emit);
182
+ if (isTimeoutOnlyClientTestFailure(check.id, step, result)) {
183
+ await emit?.(validationEvent("prepublication_validation_timeout_confirmation_started", check.id, "The full client test step reported only Vitest timeouts; confirming once with the same command and attempt environment."));
184
+ result = await this.runStep(check.id, step, worktreePath, signal, scoped.execution, postgresAdminConnection);
185
+ await this.throwIfPrerequisiteWasLost(check, result, scoped.execution?.environment, postgresAdminConnection, emit);
186
+ await emit?.(validationEvent(result.exitCode === 0
187
+ ? "prepublication_validation_timeout_confirmation_passed"
188
+ : "prepublication_validation_timeout_confirmation_failed", check.id, result.exitCode === 0
189
+ ? "The complete client test step passed on its single timeout confirmation run."
190
+ : `The complete client test step failed on its single timeout confirmation run (exit ${result.exitCode}).`));
191
+ }
192
+ if (isTransientBridgeCleanupFailure(check.id, step, result)) {
193
+ await emit?.(validationEvent("prepublication_validation_cleanup_confirmation_started", check.id, "The Bridge suite hit the known Windows temporary-worktree cleanup race; confirming once with the same command and attempt environment."));
194
+ result = await this.runStep(check.id, step, worktreePath, signal, scoped.execution, postgresAdminConnection);
195
+ await this.throwIfPrerequisiteWasLost(check, result, scoped.execution?.environment, postgresAdminConnection, emit);
196
+ await emit?.(validationEvent(result.exitCode === 0
197
+ ? "prepublication_validation_cleanup_confirmation_passed"
198
+ : "prepublication_validation_cleanup_confirmation_failed", check.id, result.exitCode === 0
199
+ ? "The complete Bridge test step passed on its single cleanup confirmation run."
200
+ : `The complete Bridge test step failed on its single cleanup confirmation run (exit ${result.exitCode}).`));
201
+ }
202
+ if (isTransientShellInfrastructureFailure(result)) {
203
+ await emit?.(validationEvent("prepublication_infrastructure_confirmation_started", check.id, "The validation shell reported a known host-runtime failure; confirming once with the same command and attempt environment."));
204
+ result = await this.runStep(check.id, step, worktreePath, signal, scoped.execution, postgresAdminConnection);
205
+ await this.throwIfPrerequisiteWasLost(check, result, scoped.execution?.environment, postgresAdminConnection, emit);
206
+ const infrastructureFailed = isTransientShellInfrastructureFailure(result);
207
+ await emit?.(validationEvent(infrastructureFailed
208
+ ? "prepublication_infrastructure_confirmation_failed"
209
+ : "prepublication_infrastructure_confirmation_passed", check.id, infrastructureFailed
210
+ ? `The validation shell failed again on its single infrastructure confirmation run (exit ${result.exitCode}).`
211
+ : result.exitCode === 0
212
+ ? "The validation step recovered on its single infrastructure confirmation run."
213
+ : "The host-runtime failure did not recur; continuing with normal validation failure handling."));
214
+ if (infrastructureFailed) {
215
+ const detail = diagnosticDetail(result);
216
+ await emit?.(validationEvent("prepublication_infrastructure_failed", check.id, `Pre-publication infrastructure for '${check.id}' remained unavailable${detail ? `: ${detail}` : "."}`));
217
+ throw new PrePublicationInfrastructureError(check.id, result.exitCode, result.command, result.args, detail);
218
+ }
219
+ }
220
+ if (result.exitCode !== 0) {
221
+ const detail = diagnosticDetail(result);
222
+ await emit?.(validationEvent("prepublication_validation_failed", check.id, `Pre-publication CI parity check '${check.id}' exited ${result.exitCode}${detail ? `: ${detail}` : "."}`));
223
+ throw new PrePublicationValidationError(check.id, result.exitCode, result.command, result.args, detail);
224
+ }
225
+ }
226
+ completed.push(check.id);
227
+ await emit?.(validationEvent("prepublication_validation_passed", check.id, `Pre-publication CI parity check '${check.id}' passed.`));
228
+ }
229
+ return completed;
230
+ }
231
+ finally {
232
+ await scoped.cleanup();
233
+ }
234
+ }
235
+ /** Returns the native fallback connection when this check must use it instead of Docker. */
236
+ async ensurePrerequisite(check, environment, postgresAdminConnection, emit) {
237
+ if (check.requiredPrerequisite === "isolated_postgres"
238
+ && isolatedPostgresProvided(check.id, environment, postgresAdminConnection)) {
239
+ await emit?.(validationEvent("prepublication_prerequisite_ready", check.id, check.id === "playwright-e2e"
240
+ ? `Pre-publication check '${check.id}' is using its attempt-owned PostgreSQL database. No Docker daemon is required.`
241
+ : `Pre-publication check '${check.id}' is using the configured loopback PostgreSQL test server with per-run owned databases. No Docker daemon is required.`));
242
+ return undefined;
243
+ }
244
+ if (check.requiredPrerequisite === undefined || !this.dockerEngineProbe)
245
+ return undefined;
246
+ const current = this.dockerEngineProbe.current();
247
+ await emit?.(validationEvent(current?.available === false
248
+ ? "prepublication_prerequisite_recovery_waiting"
249
+ : "prepublication_prerequisite_verification_started", check.id, current?.available === false
250
+ ? `Waiting for Docker recovery before pre-publication check '${check.id}'.`
251
+ : `Verifying Docker immediately before pre-publication check '${check.id}'.`));
252
+ const snapshot = await this.dockerEngineProbe.ensureAvailableForRequiredWork();
253
+ if (!snapshot.available) {
254
+ const detail = `Docker prerequisite unavailable (${snapshot.unavailableCode ?? "docker_engine_unavailable"}): ${snapshot.unavailableReason ?? "the daemon did not pass a fresh readiness probe"}`;
255
+ const fallback = check.requiredPrerequisite === "isolated_postgres" && supportsNativeTestPostgresFallback(check)
256
+ ? this.nativeTestPostgresFallback()
257
+ : null;
258
+ if (fallback) {
259
+ await emit?.(validationEvent("prepublication_prerequisite_native_fallback", check.id, `${detail}. Pre-publication check '${check.id}' is using the dedicated native PostgreSQL test cluster at ${fallback.endpoint} with per-run owned databases.`));
260
+ return fallback.connectionString;
261
+ }
262
+ await emit?.(validationEvent("prepublication_prerequisite_unavailable", check.id, `Pre-publication check '${check.id}' is waiting for Docker infrastructure recovery: ${detail}`));
263
+ throw new PrePublicationInfrastructureError(check.id, 75, "docker", ["info"], detail);
264
+ }
265
+ await emit?.(validationEvent("prepublication_prerequisite_ready", check.id, `Docker passed a fresh daemon probe for pre-publication check '${check.id}'.`));
266
+ return undefined;
267
+ }
268
+ async throwIfPrerequisiteWasLost(check, result, environment, postgresAdminConnection, emit) {
269
+ if (result.exitCode === 0
270
+ || check.requiredPrerequisite === undefined
271
+ || (check.requiredPrerequisite === "isolated_postgres"
272
+ && isolatedPostgresProvided(check.id, environment, postgresAdminConnection))
273
+ || !this.dockerEngineProbe)
274
+ return;
275
+ const snapshot = await this.dockerEngineProbe.refresh(true);
276
+ if (snapshot.available)
277
+ return;
278
+ const detail = `Docker became unavailable while '${check.id}' was running (${snapshot.unavailableCode ?? "docker_engine_unavailable"}): ${snapshot.unavailableReason ?? "the daemon did not pass the failure-time probe"}`;
279
+ await emit?.(validationEvent("prepublication_prerequisite_lost", check.id, `${detail}. The failed command is classified as retryable infrastructure and will not consume an agent repair turn.`));
280
+ throw new PrePublicationInfrastructureError(check.id, result.exitCode, result.command, result.args, detail);
281
+ }
282
+ // Runs for every target repository, with or without a CI parity contract: it only reads the worktree.
283
+ async assertNoTestOnlyExports(worktreePath, emit, signal, execution) {
284
+ const found = await findUnreferencedNewExports(args => this.runCommand("git", args, worktreePath, signal, execution), worktreePath, execution?.baselineRevision);
285
+ const unused = found.filter(row => !row.testOnly);
286
+ if (unused.length > 0) {
287
+ await emit?.(validationEvent("prepublication_unreferenced_exports", "unreferenced-exports", `New exports with no caller yet: ${unused.map(row => `${row.name} (${row.path})`).join(", ")}. Wire them to a real caller or keep them unexported.`));
288
+ }
289
+ const testOnly = found.filter(row => row.testOnly);
290
+ if (testOnly.length === 0)
291
+ return;
292
+ const detail = `only tests use ${testOnly.map(row => `${row.name} (${row.path})`).join(", ")}. Wire each to a real caller in this Story, or move the export and its test to the Story that first uses it. Do not delete assertions to pass.`;
293
+ await emit?.(validationEvent("prepublication_validation_failed", "unreferenced-exports", `New code is tested but never called: ${detail}`));
294
+ throw new PrePublicationValidationError("unreferenced-exports", 1, "git", ["grep"], detail);
295
+ }
296
+ async changedPaths(worktreePath, signal, execution) {
297
+ const baselineRevision = execution?.baselineRevision?.trim();
298
+ const requests = [
299
+ ["diff", "--name-only", "HEAD", "--"],
300
+ ...(baselineRevision ? [["diff", "--name-only", `${baselineRevision}..HEAD`, "--"]] : []),
301
+ ["status", "--porcelain=v1", "--untracked-files=all"],
302
+ ];
303
+ const paths = new Set();
304
+ for (const args of requests) {
305
+ const result = await this.runCommand("git", args, worktreePath, signal, execution);
306
+ if (result.exitCode !== 0) {
307
+ throw new ForgeFailureError("prepublication_validation_failed", `prepublication_validation_failed: unable to inventory changed paths: ${result.stderr ?? result.stdout ?? "git failed"}`, "bridge");
308
+ }
309
+ for (const line of `${result.stdout ?? ""}`.split(/\r?\n/u).filter(Boolean)) {
310
+ const raw = args[0] === "status" ? line.slice(3).split(" -> ").at(-1) ?? "" : line;
311
+ const normalized = raw.trim().replaceAll("\\", "/");
312
+ if (normalized)
313
+ paths.add(normalized);
314
+ }
315
+ }
316
+ return [...paths];
317
+ }
318
+ async assertContractMatchesDispatchBaseline(worktreePath, contractDigest, signal, execution) {
319
+ const baselineRevision = execution?.baselineRevision?.trim();
320
+ if (!baselineRevision)
321
+ return;
322
+ const result = await this.runCommand("git", ["show", `${baselineRevision}:${contractRelativePath}`], worktreePath, signal, execution);
323
+ if (result.exitCode !== 0) {
324
+ throw new ForgeFailureError("prepublication_contract_invalid", `prepublication_contract_invalid: the dispatch baseline contract could not be read from ${baselineRevision}: ${result.stderr ?? result.stdout ?? "git show failed"}`, "bridge");
325
+ }
326
+ let baselineContract;
327
+ try {
328
+ baselineContract = JSON.parse(result.stdout ?? "");
329
+ }
330
+ catch (error) {
331
+ throw new ForgeFailureError("prepublication_contract_invalid", `prepublication_contract_invalid: the dispatch baseline contract at ${baselineRevision} is not valid JSON.`, "bridge", { cause: error });
332
+ }
333
+ const baselineDigest = validationContractDigest(baselineContract);
334
+ if (!approvedContractDigests.has(baselineDigest)) {
335
+ throw new ForgeFailureError("prepublication_contract_invalid", `prepublication_contract_invalid: the dispatch baseline uses unapproved CI parity contract ${baselineDigest}.`, "bridge");
336
+ }
337
+ if (baselineDigest !== contractDigest) {
338
+ throw new ForgeFailureError("prepublication_contract_invalid", `prepublication_contract_invalid: the CI parity contract changed after dispatch (baseline ${baselineDigest}, worktree ${contractDigest}).`, "bridge");
339
+ }
340
+ }
341
+ async runStep(checkId, step, cwd, signal, execution, postgresAdminConnection) {
342
+ const invocation = managedAttemptInvocation(checkId, step, execution?.environment);
343
+ const stepExecution = validationExecutionForStep(checkId, step, execution, postgresAdminConnection);
344
+ const before = await readValidationFiles(cwd, step.assertUnchangedPaths ?? []);
345
+ const result = await this.runCommand(invocation.command, invocation.args, cwd, signal, stepExecution);
346
+ if (result.exitCode === 127 && step.fallbackCommand) {
347
+ const fallback = await this.runCommand(step.fallbackCommand, step.fallbackArgs ?? step.args, cwd, signal, stepExecution);
348
+ return {
349
+ ...await assertValidationFilesUnchanged(cwd, before, fallback),
350
+ command: step.fallbackCommand,
351
+ args: step.fallbackArgs ?? step.args,
352
+ };
353
+ }
354
+ return {
355
+ ...await assertValidationFilesUnchanged(cwd, before, result),
356
+ command: invocation.command,
357
+ args: invocation.args,
358
+ };
359
+ }
360
+ async runCommand(command, args, cwd, signal, execution) {
361
+ const exactSecrets = validationExactSecrets(execution?.environment);
362
+ try {
363
+ if (execution?.processRunner) {
364
+ const result = await execution.processRunner.run({
365
+ command,
366
+ args,
367
+ cwd,
368
+ env: execution.environment,
369
+ captureOutput: true,
370
+ failureRedactionValues: exactSecrets,
371
+ }, undefined, signal);
372
+ return {
373
+ exitCode: result.exitCode,
374
+ stdout: result.stdout === undefined ? undefined : redactText(result.stdout, exactSecrets),
375
+ stderr: result.stderr === undefined ? undefined : redactText(result.stderr, exactSecrets),
376
+ };
377
+ }
378
+ const result = await this.commands.run(command, args, cwd, signal, execution?.environment);
379
+ return {
380
+ ...result,
381
+ stdout: result.stdout === undefined ? undefined : redactText(result.stdout, exactSecrets),
382
+ stderr: result.stderr === undefined ? undefined : redactText(result.stderr, exactSecrets),
383
+ };
384
+ }
385
+ catch (error) {
386
+ throw sanitizeFailureError(error, exactSecrets);
387
+ }
388
+ }
389
+ }
390
+ function managedAttemptInvocation(checkId, step, environment) {
391
+ const playwrightConfig = environment?.FORGE_PLAYWRIGHT_CONFIG?.trim();
392
+ const attemptOwned = environment?.FORGE_E2E_OWNERSHIP_MODE === "attempt"
393
+ || Boolean(environment?.FORGE_DISPATCH_ATTEMPT_ID?.trim());
394
+ if (checkId !== "playwright-e2e" || !attemptOwned) {
395
+ return step;
396
+ }
397
+ if (!playwrightConfig) {
398
+ throw new ForgeFailureError("prepublication_capability_unavailable", "prepublication_capability_unavailable: playwright-e2e requires the Bridge-generated FORGE_PLAYWRIGHT_CONFIG for a managed attempt.", "bridge");
399
+ }
400
+ return {
401
+ command: "pnpm",
402
+ args: ["--dir", "tests/e2e", "exec", "playwright", "test", "--config", playwrightConfig],
403
+ };
404
+ }
405
+ const nativeFallbackCheckIds = new Set([
406
+ "owned-suite-registration",
407
+ "dotnet-build-and-unit",
408
+ "comment-repair-dotnet-regression",
409
+ ]);
410
+ const nativeFallbackDotnetTierScripts = [
411
+ "scripts/run-dotnet-test-tier.ps1",
412
+ "scripts/run-dotnet-test-tier-set.ps1",
413
+ "scripts/run-affected-dotnet-tests.mjs",
414
+ ];
415
+ function supportsNativeTestPostgresFallback(check) {
416
+ return nativeFallbackCheckIds.has(check.id);
417
+ }
418
+ function validationExecutionForStep(checkId, step, execution, postgresAdminConnection) {
419
+ // The connection only exists when Docker is unavailable and the operator
420
+ // provisioned a dedicated native test cluster. Only the runner-authored
421
+ // .NET test tiers and the suite-identity check receive it; build, client,
422
+ // Bridge, and E2E steps never do.
423
+ if (!postgresAdminConnection || !nativeFallbackCheckIds.has(checkId))
424
+ return execution;
425
+ const runsDotnetTier = step.args.some(argument => nativeFallbackDotnetTierScripts
426
+ .some(script => argument.replaceAll("\\", "/").endsWith(script)));
427
+ if (checkId !== "owned-suite-registration" && !runsDotnetTier)
428
+ return execution;
429
+ return {
430
+ ...execution,
431
+ environment: {
432
+ ...execution?.environment,
433
+ ...nativeTestPostgresFallbackEnvironment(parseNativeTestPostgresFallback(postgresAdminConnection)),
434
+ },
435
+ };
436
+ }
437
+ function environmentValue(environment, name) {
438
+ return Object.prototype.hasOwnProperty.call(environment ?? {}, name)
439
+ ? environment?.[name]?.trim() ?? ""
440
+ : process.env[name]?.trim() ?? "";
441
+ }
442
+ function isolatedPostgresProvided(checkId, environment, postgresAdminConnection) {
443
+ if (checkId === "playwright-e2e") {
444
+ const attemptId = environmentValue(environment, "FORGE_DISPATCH_ATTEMPT_ID");
445
+ return (environmentValue(environment, "FORGE_E2E_OWNERSHIP_MODE") === "attempt"
446
+ || /^[0-9a-f-]{36}$/iu.test(attemptId))
447
+ && /^forge_attempt_[0-9a-f]{32}$/u.test(environmentValue(environment, "FORGE_ATTEMPT_DATABASE_SCHEMA"))
448
+ && path.isAbsolute(environmentValue(environment, "FORGE_ATTEMPT_DATABASE_READINESS_PATH"))
449
+ // The attempt owns a database only once the Bridge's broker has issued it one.
450
+ && environmentValue(environment, attemptDatabaseCapabilityEnvironment).length > 0;
451
+ }
452
+ return Boolean(postgresAdminConnection);
453
+ }
454
+ function validationExactSecrets(environment) {
455
+ return [
456
+ environment?.[hostRuntimeTestPostgresAdminEnvironment]?.trim() ?? "",
457
+ environment?.[hostRuntimeLegacyTestPostgresEnvironment]?.trim() ?? "",
458
+ ...attemptDatabaseCapabilitySecrets(environment?.[attemptDatabaseCapabilityEnvironment]),
459
+ ].filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
460
+ }
461
+ function diagnosticDetail(result) {
462
+ const diagnostic = [result.stderr, result.stdout]
463
+ .map(value => value?.trim() ?? "")
464
+ .filter(Boolean)
465
+ .join("\n");
466
+ if (diagnostic.length <= 6000)
467
+ return diagnostic;
468
+ const lines = diagnostic.split(/\r?\n/u);
469
+ const signal = /(?:^|\s)(?:fail(?:ed|ure)?|error|assertionerror|expected|received|×|❯|not ok)(?:\s|:|$)/iu;
470
+ const signalIndexes = new Set();
471
+ for (const [index, line] of lines.entries()) {
472
+ if (!signal.test(line))
473
+ continue;
474
+ for (let contextIndex = index; contextIndex <= Math.min(lines.length - 1, index + 1); contextIndex += 1) {
475
+ signalIndexes.add(contextIndex);
476
+ }
477
+ }
478
+ const head = diagnostic.slice(0, 1200);
479
+ const focused = [...signalIndexes]
480
+ .sort((left, right) => left - right)
481
+ .map(index => lines[index].slice(0, 1200))
482
+ .join("\n")
483
+ .slice(0, 3000);
484
+ const tail = diagnostic.slice(-1600);
485
+ return [
486
+ head,
487
+ focused ? `--- failure-focused excerpt ---\n${focused}` : "",
488
+ `--- diagnostic tail ---\n${tail}`,
489
+ ].filter(Boolean).join("\n").slice(0, 6000);
490
+ }
491
+ function isTimeoutOnlyClientTestFailure(checkId, step, result) {
492
+ if (checkId !== "client"
493
+ || step.command !== "pnpm"
494
+ || step.args.join("\0") !== ["--dir", "client", "test"].join("\0")
495
+ || result.exitCode !== 1) {
496
+ return false;
497
+ }
498
+ // Only the owned Vitest suite is eligible. A compiler, build, assertion,
499
+ // missing-test, or ambiguous output failure remains terminal.
500
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n")
501
+ .replace(ansiColorSequence, "");
502
+ if (!output.includes("client_suite_test_failed: Vitest exited 1"))
503
+ return false;
504
+ const lines = output.split(/\r?\n/u);
505
+ const failedTasks = lines.map(line => line.trim())
506
+ .filter(line => /^Failed:\s+@forge\/\S+#test$/u.test(line));
507
+ if (failedTasks.length !== 1 || !/^Failed:\s+@forge\/web#test$/u.test(failedTasks[0])) {
508
+ return false;
509
+ }
510
+ const webLines = lines.filter(line => /^@forge\/web:test:/u.test(line))
511
+ .map(line => line.replace(/^@forge\/web:test:\s*/u, "").trim());
512
+ const failedTests = webLines.map(line => line.match(/\bFailed Tests\s+(\d+)\b/u)).find(Boolean);
513
+ const summary = webLines.find(line => /^Tests\s+\d+ failed\s*\|\s*\d+ passed/u.test(line));
514
+ const expectedCount = failedTests ? Number(failedTests[1]) : 0;
515
+ const summaryCount = summary ? Number(summary.match(/^Tests\s+(\d+) failed/u)?.[1]) : 0;
516
+ const failures = webLines.flatMap((line, index) => /^FAIL\s+\S/u.test(line) ? [index] : []);
517
+ if (expectedCount === 0 || expectedCount !== summaryCount || failures.length !== expectedCount) {
518
+ return false;
519
+ }
520
+ return failures.every((start, index) => {
521
+ const end = index + 1 < failures.length ? failures[index + 1] : webLines.length;
522
+ const firstError = webLines.slice(start + 1, end)
523
+ .find(line => /^(?:Error|AssertionError):/u.test(line));
524
+ return /^Error: Test timed out in \d+ms\.$/u.test(firstError ?? "");
525
+ });
526
+ }
527
+ function isTransientBridgeCleanupFailure(checkId, step, result) {
528
+ if (checkId !== "bridge"
529
+ || step.command !== "npm"
530
+ || step.args.join("\0") !== ["--prefix", "tools/forge-bridge", "test"].join("\0")
531
+ || result.exitCode !== 1) {
532
+ return false;
533
+ }
534
+ // Retry only the exact Windows cleanup race observed after the cancellation
535
+ // readiness test. Assertions, compile failures, and unrelated EBUSY errors
536
+ // remain terminal and cannot consume this confirmation run.
537
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n")
538
+ .replace(ansiColorSequence, "");
539
+ return /\bEBUSY:\s+resource busy or locked,\s+rmdir\s+['"][^'"\r\n]*forge-prepared-probe-cancel-[^'"\r\n]*\\worktree['"]/iu.test(output)
540
+ && output.includes("waits for probe cleanup on cancellation and never publishes its evidence");
541
+ }
542
+ function isTransientShellInfrastructureFailure(result) {
543
+ if (result.exitCode === 0)
544
+ return false;
545
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n")
546
+ .replace(ansiColorSequence, "");
547
+ return /(?:Bash\/Service\/E_UNEXPECTED|Wsl\/Service\/E_UNEXPECTED|child_copy: cygheap read copy failed|couldn't create signal pipe)/iu.test(output);
548
+ }
549
+ async function prepareValidationExecution(execution) {
550
+ assertHostDatabaseCapabilityBoundary();
551
+ // Validation executes mutable worktree scripts, SDK targets, and tests. It
552
+ // never inherits a host PostgreSQL administrative capability; the only
553
+ // non-container database is the dedicated native test cluster, selected per
554
+ // check in ensurePrerequisite when Docker is unavailable.
555
+ const postgresAdminConnection = undefined;
556
+ const scopedExecution = execution === undefined
557
+ ? undefined
558
+ : { ...execution, environment: withoutPrivilegedPostgresEnvironment(execution.environment) };
559
+ if (process.platform !== "win32" || !execution) {
560
+ return { execution: scopedExecution, postgresAdminConnection, cleanup: async () => undefined };
561
+ }
562
+ const systemDrive = process.env.SystemDrive?.trim() || "C:";
563
+ const configuredRoot = scopedExecution?.environment?.FORGE_VALIDATION_TEMP_ROOT?.trim()
564
+ || process.env.FORGE_VALIDATION_TEMP_ROOT?.trim();
565
+ const root = configuredRoot || path.win32.join(systemDrive, "ForgeValidationTmp");
566
+ const temporaryRoot = path.win32.join(root, `${process.pid}-${randomUUID().slice(0, 8)}`);
567
+ await fs.mkdir(temporaryRoot, { recursive: true });
568
+ return {
569
+ execution: {
570
+ ...scopedExecution,
571
+ environment: {
572
+ ...scopedExecution?.environment,
573
+ TEMP: temporaryRoot,
574
+ TMP: temporaryRoot,
575
+ TMPDIR: temporaryRoot,
576
+ },
577
+ },
578
+ postgresAdminConnection,
579
+ cleanup: async () => {
580
+ await fs.rm(temporaryRoot, { recursive: true, force: true, maxRetries: 15, retryDelay: 100 });
581
+ },
582
+ };
583
+ }
584
+ function withoutPrivilegedPostgresEnvironment(environment) {
585
+ if (!environment)
586
+ return undefined;
587
+ const privilegedNames = new Set([
588
+ hostRuntimeTestPostgresAdminEnvironment.toLowerCase(),
589
+ hostRuntimeLegacyTestPostgresEnvironment.toLowerCase(),
590
+ ]);
591
+ return Object.fromEntries(Object.entries(environment)
592
+ .filter(([name]) => !privilegedNames.has(name.toLowerCase())));
593
+ }
594
+ async function readValidationFiles(cwd, relativePaths) {
595
+ const values = new Map();
596
+ for (const relativePath of relativePaths) {
597
+ values.set(relativePath, await fs.readFile(path.join(cwd, relativePath), "utf8"));
598
+ }
599
+ return values;
600
+ }
601
+ async function assertValidationFilesUnchanged(cwd, before, result) {
602
+ if (result.exitCode !== 0)
603
+ return result;
604
+ for (const [relativePath, original] of before) {
605
+ const current = await fs.readFile(path.join(cwd, relativePath), "utf8");
606
+ if (current !== original) {
607
+ return {
608
+ exitCode: 1,
609
+ stderr: `generated_drift: ${relativePath} changed during validation; regenerate and review it before publication.`,
610
+ };
611
+ }
612
+ }
613
+ return result;
614
+ }
615
+ export function applicableChecks(contract, profile, changedPaths) {
616
+ return contract.checks.filter(check => check.profiles.includes(profile)
617
+ && changedPaths.some(changedPath => check.changedPathPatterns.some(pattern => new RegExp(pattern, "u").test(changedPath))));
618
+ }
619
+ export async function loadValidationContract(worktreePath) {
620
+ let source;
621
+ try {
622
+ source = await fs.readFile(path.join(worktreePath, contractRelativePath), "utf8");
623
+ }
624
+ catch (error) {
625
+ if (error.code === "ENOENT")
626
+ return null;
627
+ throw error;
628
+ }
629
+ const contract = JSON.parse(source);
630
+ const ids = new Set(contract.checks?.map(check => check.id));
631
+ const contractDigest = validationContractDigest(contract);
632
+ const expectedCheckIds = commentRepairRegressionContractDigests.has(contractDigest)
633
+ ? new Set([...requiredCheckIds, "comment-repair-dotnet-regression"])
634
+ : requiredCheckIds;
635
+ if (contract.schemaVersion !== 1
636
+ || contract.contractId !== "forge-ci-parity-v1"
637
+ || !approvedContractDigests.has(contractDigest)
638
+ || expectedCheckIds.size !== ids.size
639
+ || [...expectedCheckIds].some(id => !ids.has(id))
640
+ || contract.checks.some(check => !check.id || !Array.isArray(check.steps) || !Array.isArray(check.changedPathPatterns))) {
641
+ throw new ForgeFailureError("prepublication_contract_invalid", `prepublication_contract_invalid: the Forge CI parity contract does not match an approved definition (digest ${contractDigest}, current ${currentApprovedContractDigest}).`, "bridge");
642
+ }
643
+ return contract;
644
+ }
645
+ function validationContractDigest(contract) {
646
+ return createHash("sha256")
647
+ .update(JSON.stringify(canonicalizeJson(contract)))
648
+ .digest("hex");
649
+ }
650
+ function canonicalizeJson(value) {
651
+ if (Array.isArray(value))
652
+ return value.map(canonicalizeJson);
653
+ if (value !== null && typeof value === "object") {
654
+ const record = value;
655
+ return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonicalizeJson(record[key])]));
656
+ }
657
+ return value;
658
+ }
659
+ function validationEvent(eventType, checkId, content) {
660
+ return { role: "runner", kind: "checkpoint", content, eventType, data: JSON.stringify({ checkId, contractId: "forge-ci-parity-v1" }) };
661
+ }