deepline 0.3.90 → 0.3.91

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 (87) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +101 -6
  2. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
  3. package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
  4. package/dist/bundling-sources/sdk/src/release.ts +7 -12
  5. package/dist/bundling-sources/sdk/src/types.ts +6 -1
  6. package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
  7. package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
  8. package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
  9. package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
  10. package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
  11. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
  12. package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
  13. package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
  14. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
  15. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +36 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
  35. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
  45. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
  46. package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
  47. package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
  48. package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
  49. package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
  50. package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
  51. package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
  52. package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
  53. package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
  55. package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
  56. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
  57. package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
  58. package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
  59. package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
  60. package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
  61. package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
  62. package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
  63. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
  64. package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
  65. package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
  66. package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
  67. package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
  68. package/dist/cli/index.js +369 -162
  69. package/dist/cli/index.mjs +369 -162
  70. package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
  71. package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
  72. package/dist/index.d.mts +45 -8
  73. package/dist/index.d.ts +45 -8
  74. package/dist/index.js +61 -14
  75. package/dist/index.mjs +61 -14
  76. package/dist/install-integrity.json +54 -2
  77. package/dist/monitor-fleet-contract.d.mts +3 -10
  78. package/dist/monitor-fleet-contract.d.ts +3 -10
  79. package/dist/monitor-fleet-contract.js +2 -0
  80. package/dist/monitor-fleet-contract.mjs +1 -1
  81. package/dist/plays/bundle-play-file.d.mts +2 -2
  82. package/dist/plays/bundle-play-file.d.ts +2 -2
  83. package/dist/release.d.mts +3 -3
  84. package/dist/release.d.ts +3 -3
  85. package/dist/release.js +7 -12
  86. package/dist/release.mjs +7 -12
  87. package/package.json +2 -2
@@ -0,0 +1,280 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+
3
+ const ROLLBACK_EVENT_TYPES = new Set([
4
+ 'deployment.rollback',
5
+ 'deployment-rollback',
6
+ ]);
7
+ const TRACKED_EVENT_TYPES = new Set([
8
+ 'deployment.succeeded',
9
+ 'deployment.promoted',
10
+ ...ROLLBACK_EVENT_TYPES,
11
+ ]);
12
+
13
+ export type AlignmentStatus =
14
+ | 'ignored'
15
+ | 'tracked'
16
+ | 'rollback_tracked'
17
+ | 'rollback_needs_manual_mapping';
18
+
19
+ export interface VercelWebhookEvent {
20
+ type?: string;
21
+ id?: string;
22
+ createdAt?: number;
23
+ payload?: Record<string, unknown>;
24
+ }
25
+
26
+ export interface VercelDeploymentSummary {
27
+ id: string;
28
+ url?: string;
29
+ meta?: Record<string, unknown>;
30
+ }
31
+
32
+ export interface VercelRollbackNotice {
33
+ status: AlignmentStatus;
34
+ eventType: string;
35
+ deliveryId: string | null;
36
+ vercelProject: string | null;
37
+ fromDeploymentId: string | null;
38
+ toDeploymentId: string | null;
39
+ toDeploymentUrl: string | null;
40
+ gitSha: string | null;
41
+ workerBuildId: string | null;
42
+ }
43
+
44
+ export interface VercelDeploymentEventSummary {
45
+ status: 'ignored' | 'tracked';
46
+ eventType: string;
47
+ deliveryId: string | null;
48
+ vercelProject: string | null;
49
+ deploymentId: string | null;
50
+ deploymentUrl: string | null;
51
+ target: string | null;
52
+ gitSha: string | null;
53
+ workerBuildId: string | null;
54
+ }
55
+
56
+ function readRecord(value: unknown): Record<string, unknown> | null {
57
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
58
+ return value as Record<string, unknown>;
59
+ }
60
+
61
+ function readString(value: unknown): string | null {
62
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
63
+ }
64
+
65
+ function readNestedString(
66
+ record: Record<string, unknown> | null,
67
+ path: string[],
68
+ ): string | null {
69
+ let current: unknown = record;
70
+ for (const key of path) {
71
+ const next = readRecord(current);
72
+ if (!next) return null;
73
+ current = next[key];
74
+ }
75
+ return readString(current);
76
+ }
77
+
78
+ function normalizeGitSha(value: string | null): string | null {
79
+ if (!value) return null;
80
+ const trimmed = value.trim();
81
+ return /^[a-f0-9]{7,40}$/i.test(trimmed) ? trimmed : null;
82
+ }
83
+
84
+ export function verifyVercelSignature(input: {
85
+ rawBody: string;
86
+ signature: string | null;
87
+ secret: string;
88
+ }): boolean {
89
+ const signature = input.signature?.trim();
90
+ if (!signature) return false;
91
+ const expected = createHmac('sha1', input.secret)
92
+ .update(input.rawBody)
93
+ .digest('hex');
94
+ const expectedBuffer = Buffer.from(expected, 'hex');
95
+ const actualBuffer = Buffer.from(signature, 'hex');
96
+ if (expectedBuffer.byteLength !== actualBuffer.byteLength) return false;
97
+ return timingSafeEqual(expectedBuffer, actualBuffer);
98
+ }
99
+
100
+ export function extractVercelDeploymentEvent(event: VercelWebhookEvent): {
101
+ eventType: string;
102
+ deliveryId: string | null;
103
+ deploymentId: string | null;
104
+ fromDeploymentId: string | null;
105
+ toDeploymentId: string | null;
106
+ vercelProject: string | null;
107
+ deploymentUrl: string | null;
108
+ target: string | null;
109
+ } {
110
+ const payload = readRecord(event.payload);
111
+ const eventType = readString(event.type) ?? 'unknown';
112
+ const deployment = readRecord(payload?.deployment);
113
+ const previousDeployment = readRecord(payload?.previousDeployment);
114
+ const targetDeployment = readRecord(payload?.targetDeployment);
115
+ const rollback = readRecord(payload?.rollback);
116
+
117
+ return {
118
+ eventType,
119
+ deliveryId: readString(event.id),
120
+ deploymentId:
121
+ readNestedString(deployment, ['id']) ?? readString(payload?.deploymentId),
122
+ fromDeploymentId:
123
+ readString(payload?.fromDeploymentId) ??
124
+ readNestedString(rollback, ['fromDeploymentId']) ??
125
+ readNestedString(previousDeployment, ['id']) ??
126
+ readNestedString(deployment, ['id']),
127
+ toDeploymentId:
128
+ readString(payload?.toDeploymentId) ??
129
+ readString(payload?.targetDeploymentId) ??
130
+ readNestedString(rollback, ['toDeploymentId']) ??
131
+ readNestedString(targetDeployment, ['id']),
132
+ vercelProject:
133
+ readNestedString(payload, ['project', 'name']) ??
134
+ readString(payload?.projectName) ??
135
+ readString(payload?.projectId) ??
136
+ readNestedString(deployment, ['name']),
137
+ deploymentUrl:
138
+ readNestedString(deployment, ['url']) ??
139
+ readNestedString(payload, ['links', 'deployment']),
140
+ target: readString(payload?.target),
141
+ };
142
+ }
143
+
144
+ export const extractVercelRollbackIds = extractVercelDeploymentEvent;
145
+
146
+ export function extractGitShaFromVercelDeployment(
147
+ deployment: VercelDeploymentSummary | null,
148
+ ): string | null {
149
+ const meta = deployment?.meta ?? {};
150
+ return normalizeGitSha(
151
+ readString(meta.githubCommitSha) ??
152
+ readString(meta.githubCommitSHA) ??
153
+ readString(meta.gitCommitSha) ??
154
+ readString(meta.commitSha) ??
155
+ readString(meta.VERCEL_GIT_COMMIT_SHA),
156
+ );
157
+ }
158
+
159
+ export function extractDeploymentSummaryFromVercelEvent(
160
+ event: VercelWebhookEvent,
161
+ ): VercelDeploymentSummary | null {
162
+ const payload = readRecord(event.payload);
163
+ const ids = extractVercelDeploymentEvent(event);
164
+ const deployment =
165
+ readRecord(payload?.targetDeployment) ?? readRecord(payload?.deployment);
166
+ if (!deployment) return null;
167
+
168
+ const id =
169
+ readString(deployment.id) ?? ids.toDeploymentId ?? ids.deploymentId;
170
+ if (!id) return null;
171
+
172
+ return {
173
+ id,
174
+ url: readString(deployment.url) ?? ids.deploymentUrl ?? undefined,
175
+ meta: readRecord(deployment.meta) ?? undefined,
176
+ };
177
+ }
178
+
179
+ export function buildVercelRollbackNotice(input: {
180
+ event: VercelWebhookEvent;
181
+ targetDeployment: VercelDeploymentSummary | null;
182
+ }): VercelRollbackNotice {
183
+ const ids = extractVercelDeploymentEvent(input.event);
184
+ if (!ROLLBACK_EVENT_TYPES.has(ids.eventType)) {
185
+ return {
186
+ status: 'ignored',
187
+ eventType: ids.eventType,
188
+ deliveryId: ids.deliveryId,
189
+ vercelProject: ids.vercelProject,
190
+ fromDeploymentId: ids.fromDeploymentId,
191
+ toDeploymentId: ids.toDeploymentId,
192
+ toDeploymentUrl: input.targetDeployment?.url ?? null,
193
+ gitSha: null,
194
+ workerBuildId: null,
195
+ };
196
+ }
197
+
198
+ const gitSha = extractGitShaFromVercelDeployment(input.targetDeployment);
199
+ if (!gitSha) {
200
+ return {
201
+ status: 'rollback_needs_manual_mapping',
202
+ eventType: ids.eventType,
203
+ deliveryId: ids.deliveryId,
204
+ vercelProject: ids.vercelProject,
205
+ fromDeploymentId: ids.fromDeploymentId,
206
+ toDeploymentId: ids.toDeploymentId,
207
+ toDeploymentUrl: input.targetDeployment?.url ?? null,
208
+ gitSha: null,
209
+ workerBuildId: null,
210
+ };
211
+ }
212
+
213
+ const workerBuildId = `git-${gitSha}`;
214
+
215
+ return {
216
+ status: 'rollback_tracked',
217
+ eventType: ids.eventType,
218
+ deliveryId: ids.deliveryId,
219
+ vercelProject: ids.vercelProject,
220
+ fromDeploymentId: ids.fromDeploymentId,
221
+ toDeploymentId: ids.toDeploymentId,
222
+ toDeploymentUrl: input.targetDeployment?.url ?? null,
223
+ gitSha,
224
+ workerBuildId,
225
+ };
226
+ }
227
+
228
+ export function buildVercelDeploymentEventSummary(input: {
229
+ event: VercelWebhookEvent;
230
+ deployment: VercelDeploymentSummary | null;
231
+ }): VercelDeploymentEventSummary {
232
+ const ids = extractVercelDeploymentEvent(input.event);
233
+ const gitSha = extractGitShaFromVercelDeployment(input.deployment);
234
+ return {
235
+ status: TRACKED_EVENT_TYPES.has(ids.eventType) ? 'tracked' : 'ignored',
236
+ eventType: ids.eventType,
237
+ deliveryId: ids.deliveryId,
238
+ vercelProject: ids.vercelProject,
239
+ deploymentId: ids.deploymentId,
240
+ deploymentUrl: input.deployment?.url ?? ids.deploymentUrl,
241
+ target: ids.target,
242
+ gitSha,
243
+ workerBuildId: gitSha ? `git-${gitSha}` : null,
244
+ };
245
+ }
246
+
247
+ export function buildVercelRollbackSlackText(
248
+ plan: VercelRollbackNotice,
249
+ ): string {
250
+ const lines = [
251
+ plan.status === 'rollback_tracked'
252
+ ? ':warning: Vercel rollback detected; runtime rollback is manual.'
253
+ : ':warning: Vercel rollback detected; runtime build mapping needs manual lookup.',
254
+ plan.vercelProject ? `Project: ${plan.vercelProject}` : null,
255
+ plan.fromDeploymentId ? `From: ${plan.fromDeploymentId}` : null,
256
+ plan.toDeploymentId ? `To: ${plan.toDeploymentId}` : null,
257
+ plan.workerBuildId ? `Worker build: ${plan.workerBuildId}` : null,
258
+ ];
259
+ return lines.filter(Boolean).join('\n');
260
+ }
261
+
262
+ export function buildVercelDeploymentSlackText(
263
+ summary: VercelDeploymentEventSummary,
264
+ ): string {
265
+ const title =
266
+ summary.eventType === 'deployment.succeeded'
267
+ ? ':white_check_mark: Vercel deployment succeeded.'
268
+ : ':large_green_circle: Vercel deployment promoted.';
269
+ const lines = [
270
+ title,
271
+ summary.vercelProject ? `Project: ${summary.vercelProject}` : null,
272
+ summary.deploymentId ? `Deployment: ${summary.deploymentId}` : null,
273
+ summary.deploymentUrl ? `URL: ${summary.deploymentUrl}` : null,
274
+ summary.target ? `Target: ${summary.target}` : null,
275
+ summary.workerBuildId
276
+ ? `Aligned worker build: ${summary.workerBuildId}`
277
+ : null,
278
+ ];
279
+ return lines.filter(Boolean).join('\n');
280
+ }
@@ -0,0 +1,159 @@
1
+ export type VercelDeployment = {
2
+ id?: string;
3
+ /** Vercel's deployment-list endpoint names this same identifier `uid`. */
4
+ uid?: string;
5
+ url?: string;
6
+ gitSource?: { sha?: string };
7
+ meta?: { githubCommitSha?: string };
8
+ readyState?: string;
9
+ target?: string;
10
+ };
11
+
12
+ export type VercelGateDecision =
13
+ | { kind: 'ready' }
14
+ | { kind: 'superseded'; activeSha: string }
15
+ | { kind: 'assert-mismatch'; activeSha?: string }
16
+ | { kind: 'wait' };
17
+
18
+ export type VercelStagedProductionDecision =
19
+ | { kind: 'ready'; deployment: VercelDeployment }
20
+ | { kind: 'auto-assigned' }
21
+ | { kind: 'vercel-failed'; description?: string; targetUrl?: string }
22
+ | { kind: 'wait' };
23
+
24
+ export type VercelStagedProductionPolicyDecision =
25
+ | { kind: 'ready' }
26
+ | { kind: 'auto-assign-enabled' };
27
+
28
+ /**
29
+ * The release controller only owns the Vercel alias while Vercel is configured
30
+ * to leave Production builds staged. Treat an absent or legacy project setting
31
+ * as unsafe rather than hoping the candidate gate catches an alias move later.
32
+ */
33
+ export function decideVercelStagedProductionPolicy(input: {
34
+ autoAssignCustomDomains?: boolean;
35
+ }): VercelStagedProductionPolicyDecision {
36
+ return input.autoAssignCustomDomains === false
37
+ ? { kind: 'ready' }
38
+ : { kind: 'auto-assign-enabled' };
39
+ }
40
+
41
+ function deploymentSha(deployment: VercelDeployment): string | undefined {
42
+ return deployment.meta?.githubCommitSha ?? deployment.gitSource?.sha;
43
+ }
44
+
45
+ export function deploymentId(deployment: VercelDeployment): string | undefined {
46
+ return deployment.id ?? deployment.uid;
47
+ }
48
+
49
+ /**
50
+ * Git creates the production build; the release controller only promotes the
51
+ * already-staged deployment after its runtime proof has passed. Keep that one
52
+ * provider mutation shared by normal release and panic recovery.
53
+ */
54
+ export async function promoteVercelProductionDeployment(input: {
55
+ deploymentId: string;
56
+ fetchImpl?: typeof fetch;
57
+ projectId: string;
58
+ teamId: string;
59
+ token: string;
60
+ }): Promise<Response> {
61
+ return await (input.fetchImpl ?? fetch)(
62
+ `https://api.vercel.com/v10/projects/${encodeURIComponent(input.projectId)}/promote/${encodeURIComponent(input.deploymentId)}?teamId=${encodeURIComponent(input.teamId)}`,
63
+ {
64
+ method: 'POST',
65
+ headers: {
66
+ authorization: `Bearer ${input.token}`,
67
+ 'content-type': 'application/json',
68
+ },
69
+ body: '{}',
70
+ },
71
+ );
72
+ }
73
+
74
+ /**
75
+ * A staged production deployment is the exact production build we will
76
+ * promote later, but it must not yet own the public production alias. This is
77
+ * deliberately separate from decideVercelProductionGate: accepting an active
78
+ * alias here would make the release controller silently lose traffic control.
79
+ */
80
+ export function decideVercelStagedProductionGate(input: {
81
+ expectedSha: string;
82
+ activeDeployment: VercelDeployment;
83
+ deployments: VercelDeployment[];
84
+ vercelStatus?: {
85
+ state?: string;
86
+ description?: string;
87
+ target_url?: string;
88
+ };
89
+ }): VercelStagedProductionDecision {
90
+ if (
91
+ input.vercelStatus?.state === 'failure' ||
92
+ input.vercelStatus?.state === 'error'
93
+ ) {
94
+ return {
95
+ kind: 'vercel-failed',
96
+ description: input.vercelStatus.description,
97
+ targetUrl: input.vercelStatus.target_url,
98
+ };
99
+ }
100
+ const activeServesExpectedSha =
101
+ deploymentSha(input.activeDeployment) === input.expectedSha;
102
+ if (activeServesExpectedSha) {
103
+ return { kind: 'auto-assigned' };
104
+ }
105
+
106
+ const candidates = input.deployments.filter(
107
+ (deployment) =>
108
+ deploymentSha(deployment) === input.expectedSha &&
109
+ deployment.target === 'production' &&
110
+ deploymentId(deployment) !== deploymentId(input.activeDeployment),
111
+ );
112
+ const staged = candidates.find(
113
+ (deployment) =>
114
+ deployment.readyState === 'READY' &&
115
+ Boolean(deploymentId(deployment)) &&
116
+ Boolean(deployment.url),
117
+ );
118
+ if (staged) return { kind: 'ready', deployment: staged };
119
+
120
+ if (candidates.some((deployment) => deployment.readyState === 'ERROR')) {
121
+ return {
122
+ kind: 'vercel-failed',
123
+ description: input.vercelStatus?.description,
124
+ targetUrl: input.vercelStatus?.target_url,
125
+ };
126
+ }
127
+ return { kind: 'wait' };
128
+ }
129
+
130
+ /**
131
+ * Decide whether a shared-plane mutation may proceed from the deployment that
132
+ * is actually behind the production alias. Kept pure so the race cases are
133
+ * tested without making Vercel or GitHub API calls.
134
+ */
135
+ export function decideVercelProductionGate(input: {
136
+ expectedSha: string;
137
+ activeDeployment: VercelDeployment;
138
+ activeRevisionIsNewer: boolean;
139
+ mode: 'wait' | 'assert';
140
+ }): VercelGateDecision {
141
+ const activeSha =
142
+ input.activeDeployment.meta?.githubCommitSha ??
143
+ input.activeDeployment.gitSource?.sha;
144
+
145
+ if (
146
+ activeSha === input.expectedSha &&
147
+ input.activeDeployment.readyState === 'READY' &&
148
+ input.activeDeployment.target === 'production'
149
+ ) {
150
+ return { kind: 'ready' };
151
+ }
152
+ if (input.activeRevisionIsNewer && activeSha) {
153
+ return { kind: 'superseded', activeSha };
154
+ }
155
+ if (input.mode === 'assert') {
156
+ return { kind: 'assert-mismatch', activeSha };
157
+ }
158
+ return { kind: 'wait' };
159
+ }
@@ -0,0 +1,114 @@
1
+ import type {
2
+ ReaderFenceApplyOutcome,
3
+ RuntimeGatewayReaderFence,
4
+ RuntimeGatewayReaderFenceCandidate,
5
+ } from '../integration-operation-reader-fence/contracts';
6
+ import type { RuntimeGatewayReaderFenceStore } from '../integration-operation-reader-fence/ports';
7
+
8
+ /**
9
+ * Recording adapter for contract tests and local assembly. It implements the
10
+ * same atomic compare-and-advance law required of the Runtime Postgres mirror.
11
+ */
12
+ export function createMemoryRuntimeGatewayReaderFenceStore(
13
+ initial: readonly RuntimeGatewayReaderFence[] = [],
14
+ ): RuntimeGatewayReaderFenceStore {
15
+ const fences = new Map(
16
+ initial.map((fence) => [fence.schema, copyFence(fence)]),
17
+ );
18
+
19
+ return {
20
+ async read({ schema }) {
21
+ const fence = fences.get(schema);
22
+ return fence ? copyFence(fence) : null;
23
+ },
24
+
25
+ async apply(candidate): Promise<ReaderFenceApplyOutcome> {
26
+ const next = copyFence(candidate);
27
+ const current = fences.get(next.schema);
28
+ if (!current) {
29
+ fences.set(next.schema, next);
30
+ return { kind: 'advanced', fence: copyFence(next) };
31
+ }
32
+ if (next.epoch < current.epoch) {
33
+ return {
34
+ kind: 'rejected',
35
+ reason: 'stale_epoch',
36
+ current: copyFence(current),
37
+ };
38
+ }
39
+ if (next.epoch === current.epoch) {
40
+ if (sameFence(next, current)) {
41
+ return { kind: 'already_applied', fence: copyFence(current) };
42
+ }
43
+ return {
44
+ kind: 'rejected',
45
+ reason: 'epoch_conflict',
46
+ current: copyFence(current),
47
+ };
48
+ }
49
+ if (current.mode === 'canonical_v1' && next.mode !== 'canonical_v1') {
50
+ return {
51
+ kind: 'rejected',
52
+ reason: 'unsafe_mode_downgrade',
53
+ current: copyFence(current),
54
+ };
55
+ }
56
+ fences.set(next.schema, next);
57
+ return { kind: 'advanced', fence: copyFence(next) };
58
+ },
59
+ };
60
+ }
61
+
62
+ function sameFence(
63
+ left: RuntimeGatewayReaderFence,
64
+ right: RuntimeGatewayReaderFence,
65
+ ): boolean {
66
+ if (
67
+ left.schema !== right.schema ||
68
+ left.epoch !== right.epoch ||
69
+ left.mode !== right.mode
70
+ ) {
71
+ return false;
72
+ }
73
+ if (!left.canonicalReaderProof || !right.canonicalReaderProof) {
74
+ return left.canonicalReaderProof === right.canonicalReaderProof;
75
+ }
76
+ return (
77
+ left.canonicalReaderProof.capability ===
78
+ right.canonicalReaderProof.capability &&
79
+ left.canonicalReaderProof.sourceSha ===
80
+ right.canonicalReaderProof.sourceSha &&
81
+ sameMachineSet(
82
+ left.canonicalReaderProof.machineIds.receipt,
83
+ right.canonicalReaderProof.machineIds.receipt,
84
+ ) &&
85
+ sameMachineSet(
86
+ left.canonicalReaderProof.machineIds.execution,
87
+ right.canonicalReaderProof.machineIds.execution,
88
+ )
89
+ );
90
+ }
91
+
92
+ function sameMachineSet(
93
+ left: readonly string[],
94
+ right: readonly string[],
95
+ ): boolean {
96
+ return left.length === right.length && left.every((id) => right.includes(id));
97
+ }
98
+
99
+ function copyFence(
100
+ fence: RuntimeGatewayReaderFenceCandidate,
101
+ ): RuntimeGatewayReaderFence {
102
+ return {
103
+ ...fence,
104
+ canonicalReaderProof: fence.canonicalReaderProof
105
+ ? {
106
+ ...fence.canonicalReaderProof,
107
+ machineIds: {
108
+ receipt: [...fence.canonicalReaderProof.machineIds.receipt],
109
+ execution: [...fence.canonicalReaderProof.machineIds.execution],
110
+ },
111
+ }
112
+ : null,
113
+ };
114
+ }
@@ -20,6 +20,7 @@ import {
20
20
  normalizeDatasetBornFrom,
21
21
  type PlayDatasetBornFrom,
22
22
  } from './cell-provenance';
23
+ import { isLedgerTerminalResultRef } from './output-size-limits';
23
24
 
24
25
  export type PlayRunLedgerStatus =
25
26
  | 'queued'
@@ -1019,6 +1020,32 @@ function conflictingTerminalSnapshot(
1019
1020
  );
1020
1021
  }
1021
1022
 
1023
+ /**
1024
+ * A legacy out-of-line terminal reference did not prove that its Postgres
1025
+ * value was written in full. A later ref bearing that proof is an enrichment
1026
+ * of the same terminal result, not a competing outcome or payload.
1027
+ */
1028
+ function canPromoteTerminalResultReference(
1029
+ current: unknown,
1030
+ next: unknown,
1031
+ ): boolean {
1032
+ return (
1033
+ isLedgerTerminalResultRef(current) &&
1034
+ current.content !== 'full' &&
1035
+ isLedgerTerminalResultRef(next) &&
1036
+ next.content === 'full'
1037
+ );
1038
+ }
1039
+
1040
+ function mergeTerminalResult(input: {
1041
+ current: unknown;
1042
+ next: unknown;
1043
+ }): unknown {
1044
+ return canPromoteTerminalResultReference(input.current, input.next)
1045
+ ? input.next
1046
+ : (input.next ?? input.current);
1047
+ }
1048
+
1022
1049
  function retryablePlatformDeployFailureSnapshot(
1023
1050
  base: PlayRunLedgerSnapshot,
1024
1051
  eventError?: string | null,
@@ -1249,6 +1276,7 @@ export function reducePlayRunLedgerEvent(
1249
1276
  conflictingTerminalSnapshot(base, event.type, occurredAt, null, {
1250
1277
  allowStaleSameStatusPayloadMerge:
1251
1278
  (base.result === undefined && event.result !== undefined) ||
1279
+ canPromoteTerminalResultReference(base.result, event.result) ||
1252
1280
  (base.resultSummary === undefined &&
1253
1281
  event.resultSummary !== undefined),
1254
1282
  }) ??
@@ -1261,7 +1289,10 @@ export function reducePlayRunLedgerEvent(
1261
1289
  activeStepId: null,
1262
1290
  activeActivityId: null,
1263
1291
  activeArtifactTableNamespace: null,
1264
- result: event.result ?? base.result,
1292
+ result: mergeTerminalResult({
1293
+ current: base.result,
1294
+ next: event.result,
1295
+ }),
1265
1296
  resultSummary: event.resultSummary ?? base.resultSummary,
1266
1297
  terminalSource: nextTerminalSource(base, event.source),
1267
1298
  })
@@ -1272,7 +1303,8 @@ export function reducePlayRunLedgerEvent(
1272
1303
  conflictingTerminalSnapshot(base, event.type, occurredAt, event.error, {
1273
1304
  allowStaleSameStatusPayloadMerge:
1274
1305
  (base.error == null && event.error != null) ||
1275
- (base.result === undefined && event.result !== undefined),
1306
+ (base.result === undefined && event.result !== undefined) ||
1307
+ canPromoteTerminalResultReference(base.result, event.result),
1276
1308
  }) ??
1277
1309
  withTiming({
1278
1310
  ...settleRunningStepsOnTerminal(base, 'failed', occurredAt),
@@ -1286,7 +1318,10 @@ export function reducePlayRunLedgerEvent(
1286
1318
  activeStepId: null,
1287
1319
  activeActivityId: null,
1288
1320
  activeArtifactTableNamespace: null,
1289
- result: event.result ?? base.result,
1321
+ result: mergeTerminalResult({
1322
+ current: base.result,
1323
+ next: event.result,
1324
+ }),
1290
1325
  terminalSource: nextTerminalSource(base, event.source),
1291
1326
  })
1292
1327
  );
@@ -1295,7 +1330,8 @@ export function reducePlayRunLedgerEvent(
1295
1330
  conflictingTerminalSnapshot(base, event.type, occurredAt, event.error, {
1296
1331
  allowStaleSameStatusPayloadMerge:
1297
1332
  (base.error == null && event.error != null) ||
1298
- (base.result === undefined && event.result !== undefined),
1333
+ (base.result === undefined && event.result !== undefined) ||
1334
+ canPromoteTerminalResultReference(base.result, event.result),
1299
1335
  }) ??
1300
1336
  withTiming({
1301
1337
  ...settleRunningStepsOnTerminal(base, 'failed', occurredAt),
@@ -1309,7 +1345,10 @@ export function reducePlayRunLedgerEvent(
1309
1345
  activeStepId: null,
1310
1346
  activeActivityId: null,
1311
1347
  activeArtifactTableNamespace: null,
1312
- result: event.result ?? base.result,
1348
+ result: mergeTerminalResult({
1349
+ current: base.result,
1350
+ next: event.result,
1351
+ }),
1313
1352
  terminalSource: nextTerminalSource(base, event.source),
1314
1353
  })
1315
1354
  );
@@ -0,0 +1,26 @@
1
+ import { createSingleFlight } from '../single-flight';
2
+
3
+ /**
4
+ * Cache authorized child metadata for one Node runner execution. The caller
5
+ * owns this resolver beside the root PlayContext, so all map rows share it and
6
+ * the cache becomes unreachable as soon as the run settles.
7
+ */
8
+ export function createRunScopedChildPlayResolver<T>(input: {
9
+ resolvePlay: (playRef: string) => Promise<T | null>;
10
+ }): (playRef: string) => Promise<T | null> {
11
+ const resolved = new Map<string, T>();
12
+ const flights = createSingleFlight<string, T | null>();
13
+
14
+ return async (playRef) => {
15
+ const cached = resolved.get(playRef);
16
+ if (cached) return cached;
17
+
18
+ return flights.run(playRef, async () => {
19
+ const cachedAfterAdmission = resolved.get(playRef);
20
+ if (cachedAfterAdmission) return cachedAfterAdmission;
21
+ const play = await input.resolvePlay(playRef);
22
+ if (play) resolved.set(playRef, play);
23
+ return play;
24
+ });
25
+ };
26
+ }