@tiangong-lca/cli 0.0.27 → 0.0.29

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 (36) hide show
  1. package/README.md +85 -2
  2. package/dist/src/cli.js +544 -3
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
  5. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js +175 -0
  6. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js.map +1 -0
  7. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js +511 -0
  8. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js.map +1 -0
  9. package/dist/src/lib/dataset-maintenance-flow-identity-command.js +26 -0
  10. package/dist/src/lib/dataset-maintenance-flow-identity-command.js.map +1 -0
  11. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js +784 -0
  12. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js.map +1 -0
  13. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js +1317 -0
  14. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js.map +1 -0
  15. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js +342 -0
  16. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js.map +1 -0
  17. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js +900 -0
  18. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js.map +1 -0
  19. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js +688 -0
  20. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js.map +1 -0
  21. package/dist/src/lib/dataset-maintenance-flow-identity-run.js +1369 -0
  22. package/dist/src/lib/dataset-maintenance-flow-identity-run.js.map +1 -0
  23. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js +144 -0
  24. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js.map +1 -0
  25. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js +377 -0
  26. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js.map +1 -0
  27. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js +178 -0
  28. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js.map +1 -0
  29. package/dist/src/lib/dataset-maintenance-protected-verify.js +76 -1
  30. package/dist/src/lib/dataset-maintenance-protected-verify.js.map +1 -1
  31. package/dist/src/lib/dataset-maintenance-remote.js +156 -10
  32. package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
  33. package/dist/src/lib/http.js.map +1 -1
  34. package/dist/src/lib/lca-release.js +683 -0
  35. package/dist/src/lib/lca-release.js.map +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,1369 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ensurePrivateArtifactDirectory, readProtectedJsonArtifact, writePrivateImmutableJson, } from './dataset-maintenance-protected-artifacts.js';
4
+ import { buildFlowIdentityFinalizeRequest, buildFlowIdentityProcessRequest, buildFlowIdentityScopeLookupRequest, flowIdentityScopeHasCurrentDerivativeClosure, flowIdentityScopeIsReadyToFinalize, parseFlowIdentityFinalizeProof, parseFlowIdentityProcessProof, parseFlowIdentityScopePreflightProof, parseFlowIdentityScopeLookupProof, parseFlowIdentityScopeStatus, prepareFlowIdentityExecution, splitFlowIdentityPermitResponse, } from './dataset-maintenance-flow-identity-execution-contract.js';
5
+ import { assertFreshRecoveryBaseline, parseFlowIdentityRecoveryProof, prepareFlowIdentityRecoveryExecution, } from './dataset-maintenance-flow-identity-recovery.js';
6
+ import { isJsonObject, sha256Json, stableJsonText, } from './dataset-maintenance-contract.js';
7
+ import { flowIdentityRestrictedSha256 } from './dataset-maintenance-flow-identity-wire.js';
8
+ import { finalizeMaintenanceFlowIdentityScope, isMaintenanceRpcDomainFailure, lookupMaintenanceFlowIdentityScope, preflightMaintenanceFlowIdentityScope, recoverMaintenanceFlowIdentityScope, readMaintenanceFlowIdentityScope, resolveMaintenanceRemoteContext, rewriteMaintenanceFlowIdentityProcess, } from './dataset-maintenance-remote.js';
9
+ import { CliError } from './errors.js';
10
+ import { withStateFileLock } from './state-lock.js';
11
+ import { claimFlowIdentityApproval, readFlowIdentityApprovalClaim, } from './dataset-maintenance-flow-identity-approval-claim.js';
12
+ const DEFAULT_POLL_MS = 10_000;
13
+ const PARTIAL_EXECUTION_RECOVERY = 'partial_execution_requires_status_read_and_operator_approved_scope_recovery';
14
+ const OPERATOR_REVIEW_REQUIRED = 'operator_review_required';
15
+ function fail(message, code, details) {
16
+ throw new CliError(message, {
17
+ code,
18
+ exitCode: 1,
19
+ ...(details === undefined ? {} : { details }),
20
+ });
21
+ }
22
+ function errorDetails(error) {
23
+ return {
24
+ name: error instanceof Error ? error.name : 'Error',
25
+ message: error instanceof Error ? error.message : String(error),
26
+ code: error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
27
+ ? error.code
28
+ : null,
29
+ };
30
+ }
31
+ function assertDomainFailureHasNoPermit(value, label) {
32
+ if (Object.hasOwn(value, 'execution_permit')) {
33
+ fail(`${label} domain rejection unexpectedly contained a bearer permit.`, 'DATASET_FLOW_IDENTITY_DOMAIN_REJECTION_PERMIT_INVALID');
34
+ }
35
+ }
36
+ function readCanonicalJson(filePath, label) {
37
+ const artifact = readProtectedJsonArtifact({ filePath, label });
38
+ if (artifact.text !== `${stableJsonText(artifact.value)}\n`) {
39
+ fail(`${label} must be canonical JSON with exactly one trailing newline.`, 'DATASET_FLOW_IDENTITY_ARTIFACT_NONCANONICAL');
40
+ }
41
+ return artifact.value;
42
+ }
43
+ function processStem(ordinal, id, version) {
44
+ return `${String(ordinal).padStart(6, '0')}-${id}-${version}`;
45
+ }
46
+ function loadProcessTemplates(planPath, plan) {
47
+ const planDir = path.dirname(planPath);
48
+ return plan.processes.map((process) => {
49
+ const filePath = path.join(planDir, plan.artifacts.process_request_dir, `${processStem(process.ordinal, process.id, process.version)}.json`);
50
+ const value = readCanonicalJson(filePath, `Flow identity process template ${process.ordinal}`);
51
+ if (!isJsonObject(value) ||
52
+ !isJsonObject(value.process) ||
53
+ !Array.isArray(value.rewrites) ||
54
+ !isJsonObject(value.collision_ledger) ||
55
+ sha256Json(value.process) !== sha256Json(process) ||
56
+ sha256Json(value.rewrites) !== process.rewrite_set_sha256 ||
57
+ sha256Json(value.collision_ledger) !== process.collision_ledger_sha256 ||
58
+ value.rewrites.length !== process.rewrite_count) {
59
+ fail(`Process template ${process.ordinal} does not bind the sealed manifest.`, 'DATASET_FLOW_IDENTITY_PROCESS_TEMPLATE_INVALID');
60
+ }
61
+ return value;
62
+ });
63
+ }
64
+ function readRecoveryInputScope(options) {
65
+ const candidates = [
66
+ path.join(path.resolve(options.runDir), 'scope-preflight-proof.json'),
67
+ path.join(path.resolve(options.runDir), 'scope-lookup-proof.json'),
68
+ path.join(path.dirname(path.resolve(options.recoveryFreezePath)), 'flow-identity-recovery-scope-proof.json'),
69
+ ];
70
+ const found = candidates.find((candidate) => existsSync(candidate));
71
+ if (!found) {
72
+ fail('Recovery run requires an exact preflight or read-only lookup scope proof.', 'DATASET_FLOW_IDENTITY_SCOPE_PROOF_REQUIRED');
73
+ }
74
+ return readCanonicalJson(found, 'Flow identity recovery scope proof');
75
+ }
76
+ function prepareRun(options) {
77
+ if (options.commit === options.statusOnly) {
78
+ fail('Choose exactly one of commit or statusOnly.', 'DATASET_FLOW_IDENTITY_RUN_MODE_INVALID');
79
+ }
80
+ const planPath = path.resolve(options.planPath);
81
+ const prepared = prepareFlowIdentityExecution({
82
+ plan: readCanonicalJson(planPath, 'Flow identity plan'),
83
+ freeze: readCanonicalJson(options.freezePath, 'Flow identity freeze'),
84
+ approval: readCanonicalJson(options.approvalPath, 'Flow identity approval'),
85
+ });
86
+ const recoveryPathCount = [
87
+ options.recoveryFreezePath,
88
+ options.recoveryApprovalPath,
89
+ options.recoveryRunDir,
90
+ ].filter(Boolean).length;
91
+ if (recoveryPathCount !== 0 && recoveryPathCount !== 3) {
92
+ fail('Recovery run requires recoveryFreezePath, recoveryApprovalPath, and recoveryRunDir together.', 'DATASET_FLOW_IDENTITY_RECOVERY_ARGUMENTS_INVALID');
93
+ }
94
+ const recovery = recoveryPathCount === 3
95
+ ? prepareFlowIdentityRecoveryExecution({
96
+ plan: readCanonicalJson(planPath, 'Flow identity recovery plan'),
97
+ originalFreeze: readCanonicalJson(options.freezePath, 'Flow identity recovery original freeze'),
98
+ originalApproval: readCanonicalJson(options.approvalPath, 'Flow identity recovery original approval'),
99
+ scope: readRecoveryInputScope({
100
+ runDir: options.recoveryRunDir,
101
+ recoveryFreezePath: options.recoveryFreezePath,
102
+ plan: prepared.plan,
103
+ identity: prepared.identity,
104
+ }),
105
+ recoveryFreeze: readCanonicalJson(options.recoveryFreezePath, 'Flow identity recovery freeze'),
106
+ recoveryApproval: readCanonicalJson(options.recoveryApprovalPath, 'Flow identity recovery approval'),
107
+ })
108
+ : null;
109
+ if (options.commit) {
110
+ const expectedApprovalIdentity = recovery?.recoveryApproval.recovery_approval_identity_sha256 ??
111
+ prepared.identity.identity_sha256;
112
+ if (options.approveExecution !== expectedApprovalIdentity ||
113
+ options.confirm !== prepared.plan.account.email) {
114
+ fail('Commit requires the exact execution identity hash and frozen account email.', 'DATASET_FLOW_IDENTITY_EXECUTION_APPROVAL_REQUIRED');
115
+ }
116
+ }
117
+ let outDir = path.resolve(options.outDir);
118
+ let approvalClaim = null;
119
+ if (options.statusOnly && recovery === null) {
120
+ const claim = readFlowIdentityApprovalClaim({
121
+ approvalIdentitySha256: prepared.approval.execution_approval_identity_sha256,
122
+ env: options.env,
123
+ });
124
+ if (claim) {
125
+ approvalClaim = claim;
126
+ if (claim.approval_kind !== 'initial' ||
127
+ claim.execution_identity_sha256 !== prepared.identity.identity_sha256 ||
128
+ claim.request_id !== prepared.identity.request_id ||
129
+ claim.project_ref !== prepared.plan.project_ref ||
130
+ claim.actor_user_id !== prepared.plan.account.user_id ||
131
+ claim.actor_email !== prepared.plan.account.email ||
132
+ claim.plan_sha256 !== prepared.plan.plan_sha256 ||
133
+ claim.freeze_sha256 !== prepared.freeze.freeze_sha256) {
134
+ fail('Local approval claim does not bind this immutable execution.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID');
135
+ }
136
+ outDir = path.resolve(claim.canonical_out_dir);
137
+ if (!existsSync(outDir)) {
138
+ fail('Local approval claim points to a missing canonical run directory.', 'DATASET_FLOW_IDENTITY_APPROVAL_CLAIM_INVALID');
139
+ }
140
+ }
141
+ }
142
+ outDir = ensurePrivateArtifactDirectory(outDir);
143
+ return {
144
+ ...prepared,
145
+ planPath,
146
+ outDir,
147
+ templates: loadProcessTemplates(planPath, prepared.plan),
148
+ recovery,
149
+ approvalClaim,
150
+ };
151
+ }
152
+ function assertContext(prepared, context) {
153
+ if (context.project_ref !== prepared.plan.project_ref ||
154
+ context.account.user_id !== prepared.plan.account.user_id ||
155
+ context.account.email.trim().toLowerCase() !== prepared.plan.account.email) {
156
+ fail('Authenticated production RLS context does not match the sealed plan.', 'DATASET_FLOW_IDENTITY_CONTEXT_MISMATCH', {
157
+ expected_project_ref: prepared.plan.project_ref,
158
+ observed_project_ref: context.project_ref,
159
+ expected_user_id: prepared.plan.account.user_id,
160
+ observed_user_id: context.account.user_id,
161
+ });
162
+ }
163
+ }
164
+ function numberedArtifact(directory, prefix) {
165
+ let ordinal = 1;
166
+ while (existsSync(path.join(directory, `${prefix}.${String(ordinal).padStart(6, '0')}.json`))) {
167
+ ordinal += 1;
168
+ }
169
+ return path.join(directory, `${prefix}.${String(ordinal).padStart(6, '0')}.json`);
170
+ }
171
+ function writeStatusSnapshot(prepared, value) {
172
+ writePrivateImmutableJson(numberedArtifact(prepared.outDir, 'scope-status'), value);
173
+ }
174
+ function readStoredScopeProof(prepared) {
175
+ const filePath = path.join(prepared.outDir, 'scope-preflight-proof.json');
176
+ if (existsSync(filePath)) {
177
+ return parseFlowIdentityScopePreflightProof(readCanonicalJson(filePath, 'Flow identity scope preflight proof'), prepared.plan);
178
+ }
179
+ const lookupPath = path.join(prepared.outDir, 'scope-lookup-proof.json');
180
+ if (!existsSync(lookupPath))
181
+ return null;
182
+ return parseFlowIdentityScopeLookupProof(readCanonicalJson(lookupPath, 'Flow identity scope lookup proof'), prepared.plan, prepared.identity);
183
+ }
184
+ function readStoredDomainRejection(options) {
185
+ if (!existsSync(options.filePath))
186
+ return null;
187
+ const value = readCanonicalJson(options.filePath, options.label);
188
+ if (!isJsonObject(value) ||
189
+ value.status !== 'rejected' ||
190
+ value.automatic_retry !== false ||
191
+ value.approval_reusable !== false ||
192
+ value[options.requestHashField] !== options.expectedRequestSha256 ||
193
+ !isMaintenanceRpcDomainFailure(value.response)) {
194
+ fail(`${options.label} does not contain an exact database domain rejection.`, 'DATASET_FLOW_IDENTITY_DOMAIN_REJECTION_INVALID');
195
+ }
196
+ return value.response;
197
+ }
198
+ function finalizeArtifactPath(prepared, kind, requestSha256) {
199
+ return path.join(prepared.outDir, `finalize-${kind}-${requestSha256}.json`);
200
+ }
201
+ function readStoredFinalizeAttempt(options) {
202
+ const filePath = finalizeArtifactPath(options.prepared, 'attempt', options.requestSha256);
203
+ if (!existsSync(filePath))
204
+ return null;
205
+ const value = readCanonicalJson(filePath, 'Flow identity finalize attempt');
206
+ if (!isJsonObject(value) ||
207
+ value.schema_version !== 'dataset-flow-identity-finalize-attempt.v3' ||
208
+ value.scope_id !== options.scope.scope_id ||
209
+ value.request_sha256 !== options.requestSha256 ||
210
+ value.max_posts !== 1 ||
211
+ value.automatic_retry !== false ||
212
+ !isJsonObject(value.request) ||
213
+ flowIdentityRestrictedSha256(value.request) !== options.requestSha256) {
214
+ fail('Flow identity finalize attempt does not bind the exact request.', 'DATASET_FLOW_IDENTITY_FINALIZE_ATTEMPT_INVALID');
215
+ }
216
+ return value.request;
217
+ }
218
+ function storedFinalizeAttemptHashes(prepared) {
219
+ return readdirSync(prepared.outDir)
220
+ .map((name) => /^finalize-attempt-([a-f0-9]{64})\.json$/u.exec(name)?.[1] ?? null)
221
+ .filter((value) => value !== null)
222
+ .sort();
223
+ }
224
+ function readStoredFinalizeProof(options) {
225
+ const filePath = finalizeArtifactPath(options.prepared, 'proof', options.requestSha256);
226
+ if (!existsSync(filePath))
227
+ return null;
228
+ return parseFlowIdentityFinalizeProof({
229
+ value: readCanonicalJson(filePath, 'Flow identity finalize proof'),
230
+ plan: options.prepared.plan,
231
+ scopeId: options.scope.scope_id,
232
+ scopeProofSha256: options.scope.scope_proof_sha256,
233
+ request: options.request,
234
+ });
235
+ }
236
+ async function obtainScope(options) {
237
+ if (options.prepared.recovery) {
238
+ if (options.command.statusOnly) {
239
+ fail('Recovery admission is write-capable and requires an explicit commit invocation.', 'DATASET_FLOW_IDENTITY_RECOVERY_COMMIT_REQUIRED');
240
+ }
241
+ const recovery = options.prepared.recovery;
242
+ const baselineRaw = await options.dependencies.read({
243
+ context: options.context,
244
+ scopeId: recovery.scope.scope_id,
245
+ });
246
+ if (isMaintenanceRpcDomainFailure(baselineRaw)) {
247
+ assertDomainFailureHasNoPermit(baselineRaw, 'Recovery baseline read');
248
+ return { scope: baselineRaw, executionPermit: null };
249
+ }
250
+ const baseline = parseFlowIdentityScopeStatus(baselineRaw, options.prepared.plan, recovery.scope.scope_id, recovery.scope.scope_proof_sha256);
251
+ assertFreshRecoveryBaseline(baseline, recovery.recoveryFreeze);
252
+ writeStatusSnapshot(options.prepared, baseline);
253
+ const requestSha256 = flowIdentityRestrictedSha256(recovery.recoveryRequest);
254
+ if (!options.dependencies.recover) {
255
+ fail('Recovery RPC dependency is unavailable.', 'DATASET_FLOW_IDENTITY_RECOVERY_RPC_UNAVAILABLE');
256
+ }
257
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-recovery-attempt.json'), {
258
+ schema_version: 'dataset-flow-identity-recovery-attempt.v1',
259
+ generated_at_utc: options.dependencies.now().toISOString(),
260
+ scope_id: recovery.scope.scope_id,
261
+ request_sha256: requestSha256,
262
+ recovery_approval_identity_sha256: recovery.recoveryApproval.recovery_approval_identity_sha256,
263
+ maximum_wrapper_invocations: 1,
264
+ maximum_cli_apply_spawns: 1,
265
+ approval_reusable: false,
266
+ automatic_retry: false,
267
+ });
268
+ let raw;
269
+ try {
270
+ raw = await options.dependencies.recover({
271
+ context: options.context,
272
+ scopeId: recovery.scope.scope_id,
273
+ request: recovery.recoveryRequest,
274
+ });
275
+ }
276
+ catch (error) {
277
+ const details = errorDetails(error);
278
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-recovery-transport-error.json'), {
279
+ schema_version: 'dataset-flow-identity-recovery-transport-error.v1',
280
+ observed_at_utc: options.dependencies.now().toISOString(),
281
+ scope_id: recovery.scope.scope_id,
282
+ request_sha256: requestSha256,
283
+ approval_reusable: false,
284
+ automatic_retry: false,
285
+ recovery: 'read_scope_only',
286
+ error: details,
287
+ });
288
+ let observed = null;
289
+ try {
290
+ const observedRaw = await options.dependencies.read({
291
+ context: options.context,
292
+ scopeId: recovery.scope.scope_id,
293
+ });
294
+ if (!isMaintenanceRpcDomainFailure(observedRaw)) {
295
+ observed = parseFlowIdentityScopeStatus(observedRaw, options.prepared.plan, recovery.scope.scope_id, recovery.scope.scope_proof_sha256);
296
+ writeStatusSnapshot(options.prepared, observed);
297
+ }
298
+ else {
299
+ assertDomainFailureHasNoPermit(observedRaw, 'Recovery ambiguity status read');
300
+ }
301
+ }
302
+ catch {
303
+ // The immutable transport error is sufficient; never retry recovery admission.
304
+ }
305
+ return {
306
+ scope: recovery.scope,
307
+ executionPermit: null,
308
+ recoveryAmbiguity: { observed, error: details },
309
+ };
310
+ }
311
+ if (isMaintenanceRpcDomainFailure(raw)) {
312
+ assertDomainFailureHasNoPermit(raw, 'Scope recovery');
313
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-recovery-domain-rejection.json'), {
314
+ schema_version: 'dataset-flow-identity-recovery-domain-rejection.v1',
315
+ status: 'rejected',
316
+ scope_id: recovery.scope.scope_id,
317
+ request_sha256: requestSha256,
318
+ approval_reusable: false,
319
+ automatic_retry: false,
320
+ response: raw,
321
+ });
322
+ return { scope: raw, executionPermit: null };
323
+ }
324
+ const envelope = splitFlowIdentityPermitResponse({
325
+ value: raw,
326
+ expectedGeneration: 0,
327
+ permitRequired: raw.replay !== true,
328
+ permitForbidden: raw.replay === true,
329
+ label: 'Flow identity scope recovery',
330
+ });
331
+ const recoveryProof = parseFlowIdentityRecoveryProof({
332
+ value: envelope.proof,
333
+ freeze: recovery.recoveryFreeze,
334
+ approval: recovery.recoveryApproval,
335
+ request: recovery.recoveryRequest,
336
+ expectedInvocationId: envelope.executionPermit?.invocation_id,
337
+ });
338
+ writePrivateImmutableJson(path.join(options.prepared.outDir, recovery.scope.schema_version === 'dataset-flow-identity-scope-lookup-result.v1'
339
+ ? 'scope-lookup-proof.json'
340
+ : 'scope-preflight-proof.json'), recovery.scope);
341
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-recovery-proof.json'), recoveryProof);
342
+ return { scope: recovery.scope, executionPermit: envelope.executionPermit };
343
+ }
344
+ const stored = readStoredScopeProof(options.prepared);
345
+ if (stored)
346
+ return { scope: stored, executionPermit: null };
347
+ const storedRejection = readStoredDomainRejection({
348
+ filePath: path.join(options.prepared.outDir, 'scope-preflight-domain-rejection.json'),
349
+ label: 'Flow identity scope preflight domain rejection',
350
+ requestHashField: 'request_sha256',
351
+ expectedRequestSha256: flowIdentityRestrictedSha256(options.prepared.preflightRequest),
352
+ });
353
+ if (storedRejection)
354
+ return { scope: storedRejection, executionPermit: null };
355
+ if (options.command.statusOnly) {
356
+ if (!options.prepared.approvalClaim || !options.dependencies.lookup) {
357
+ fail('Status-only recovery requires an immutable local scope proof or a consumed local approval claim with the read-only lookup capability.', 'DATASET_FLOW_IDENTITY_SCOPE_PROOF_REQUIRED');
358
+ }
359
+ const rawLookup = await options.dependencies.lookup({
360
+ context: options.context,
361
+ request: buildFlowIdentityScopeLookupRequest({ identity: options.prepared.identity }),
362
+ });
363
+ if (isMaintenanceRpcDomainFailure(rawLookup)) {
364
+ assertDomainFailureHasNoPermit(rawLookup, 'Scope lookup');
365
+ return { scope: rawLookup, executionPermit: null };
366
+ }
367
+ const lookupProof = parseFlowIdentityScopeLookupProof(rawLookup, options.prepared.plan, options.prepared.identity);
368
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-lookup-proof.json'), lookupProof);
369
+ return { scope: lookupProof, executionPermit: null };
370
+ }
371
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-preflight-attempt.json'), {
372
+ schema_version: 'dataset-flow-identity-preflight-attempt.v2',
373
+ generated_at_utc: options.dependencies.now().toISOString(),
374
+ request_id: options.prepared.identity.request_id,
375
+ plan_sha256: options.prepared.plan.plan_sha256,
376
+ request_sha256: flowIdentityRestrictedSha256(options.prepared.preflightRequest),
377
+ exact_replay_allowed: false,
378
+ automatic_retry: false,
379
+ });
380
+ const raw = await options.dependencies.preflight({
381
+ context: options.context,
382
+ request: options.prepared.preflightRequest,
383
+ });
384
+ if (isMaintenanceRpcDomainFailure(raw)) {
385
+ assertDomainFailureHasNoPermit(raw, 'Scope preflight');
386
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-preflight-domain-rejection.json'), {
387
+ schema_version: 'dataset-flow-identity-preflight-domain-rejection.v1',
388
+ status: 'rejected',
389
+ request_sha256: flowIdentityRestrictedSha256(options.prepared.preflightRequest),
390
+ automatic_retry: false,
391
+ approval_reusable: false,
392
+ recovery: 'new_capture_freeze_and_exact_approval_required',
393
+ response: raw,
394
+ });
395
+ return { scope: raw, executionPermit: null };
396
+ }
397
+ const envelope = splitFlowIdentityPermitResponse({
398
+ value: raw,
399
+ expectedGeneration: 0,
400
+ permitRequired: raw.replay !== true,
401
+ permitForbidden: raw.replay === true,
402
+ label: 'Flow identity scope preflight',
403
+ });
404
+ const proof = parseFlowIdentityScopePreflightProof(envelope.proof, options.prepared.plan);
405
+ writePrivateImmutableJson(path.join(options.prepared.outDir, 'scope-preflight-proof.json'), proof);
406
+ return { scope: proof, executionPermit: envelope.executionPermit };
407
+ }
408
+ async function readScope(options) {
409
+ const raw = await options.dependencies.read({
410
+ context: options.context,
411
+ scopeId: options.scope.scope_id,
412
+ });
413
+ const status = parseFlowIdentityScopeStatus(raw, options.prepared.plan, options.scope.scope_id, options.scope.scope_proof_sha256);
414
+ writeStatusSnapshot(options.prepared, status);
415
+ return status;
416
+ }
417
+ function validateCompletedRequests(prepared, status) {
418
+ for (const ledger of status.processes.filter((entry) => entry.status === 'completed')) {
419
+ const template = prepared.templates[ledger.ordinal - 1];
420
+ if (!template) {
421
+ fail('Database progress contains a foreign completed ordinal.', 'DATASET_FLOW_IDENTITY_PROGRESS_INVALID');
422
+ }
423
+ const request = buildFlowIdentityProcessRequest({
424
+ scopeProofSha256: status.scope_proof_sha256,
425
+ ordinal: ledger.ordinal,
426
+ processIntentProofSha256: ledger.process_intent_proof_sha256,
427
+ });
428
+ if (ledger.process_request_sha256 !== request.process_request_sha256) {
429
+ fail(`Completed process ${ledger.ordinal} used a foreign request hash.`, 'DATASET_FLOW_IDENTITY_PROGRESS_INVALID');
430
+ }
431
+ }
432
+ }
433
+ function requirePendingProcess(prepared, status, ordinal) {
434
+ const ledger = status.processes[ordinal - 1];
435
+ const template = prepared.templates[ordinal - 1];
436
+ if (!ledger || !template || ledger.status !== 'pending') {
437
+ fail('Durable scope did not expose exactly one pending next ordinal.', 'DATASET_FLOW_IDENTITY_NEXT_ORDINAL_INVALID');
438
+ }
439
+ return { ledger, template };
440
+ }
441
+ function report(options) {
442
+ const value = {
443
+ schema_version: 'dataset-flow-identity-run-report.v2',
444
+ generated_at_utc: options.now.toISOString(),
445
+ mode: options.mode,
446
+ status: options.status,
447
+ operation_id: options.prepared.plan.operation_id,
448
+ plan_sha256: options.prepared.plan.plan_sha256,
449
+ request_id: options.prepared.identity.request_id,
450
+ identity_sha256: options.prepared.identity.identity_sha256,
451
+ scope_id: options.scope?.scope_id ?? null,
452
+ database_status: options.database?.status ?? null,
453
+ process_count: options.prepared.plan.processes.length,
454
+ completed_process_count: options.database?.completed_process_count ?? 0,
455
+ next_ordinal: options.database?.next_ordinal ?? null,
456
+ automatic_retry: false,
457
+ issues: options.issues ?? [],
458
+ };
459
+ writePrivateImmutableJson(numberedArtifact(options.prepared.outDir, 'run-report'), value);
460
+ return value;
461
+ }
462
+ async function reportProcessDomainRejection(options) {
463
+ let recovered = null;
464
+ let readError = null;
465
+ try {
466
+ recovered = await readScope({
467
+ prepared: options.prepared,
468
+ context: options.context,
469
+ scope: options.scope,
470
+ dependencies: options.dependencies,
471
+ });
472
+ validateCompletedRequests(options.prepared, recovered);
473
+ }
474
+ catch (error) {
475
+ readError = error;
476
+ }
477
+ const recovery = recovered && recovered.completed_process_count > 0
478
+ ? PARTIAL_EXECUTION_RECOVERY
479
+ : OPERATOR_REVIEW_REQUIRED;
480
+ if (!existsSync(options.rejectionPath)) {
481
+ writePrivateImmutableJson(options.rejectionPath, {
482
+ schema_version: 'dataset-flow-identity-process-domain-rejection.v2',
483
+ status: 'rejected',
484
+ ordinal: options.ordinal,
485
+ process_request_sha256: options.requestSha256,
486
+ automatic_retry: false,
487
+ approval_reusable: false,
488
+ recovery,
489
+ response: options.response,
490
+ });
491
+ }
492
+ writePrivateImmutableJson(numberedArtifact(options.prepared.outDir, `process-domain-rejection-recovery-${String(options.ordinal).padStart(6, '0')}`), {
493
+ schema_version: 'dataset-flow-identity-process-domain-rejection-recovery.v1',
494
+ observed_at_utc: options.dependencies.now().toISOString(),
495
+ scope_id: options.scope.scope_id,
496
+ ordinal: options.ordinal,
497
+ process_request_sha256: options.requestSha256,
498
+ status_read_succeeded: recovered !== null,
499
+ completed_process_count: recovered?.completed_process_count ?? null,
500
+ next_ordinal: recovered?.next_ordinal ?? null,
501
+ strict_continuation_required: recovered?.strict_continuation_required ?? null,
502
+ recovery,
503
+ automatic_retry: false,
504
+ ...(readError === null ? {} : { error: errorDetails(readError) }),
505
+ });
506
+ const recoveryIssue = recovery === PARTIAL_EXECUTION_RECOVERY
507
+ ? {
508
+ code: 'DATASET_FLOW_IDENTITY_PARTIAL_EXECUTION_REQUIRES_STATUS_READ_AND_OPERATOR_APPROVED_SCOPE_RECOVERY',
509
+ message: 'Primary writes already exist. Recovery requires fresh status reads and an operator-approved scope recovery; do not start a fresh capture.',
510
+ details: { recovery },
511
+ }
512
+ : {
513
+ code: 'DATASET_FLOW_IDENTITY_OPERATOR_REVIEW_REQUIRED',
514
+ message: 'No completed primary write is proven by the bounded status read; operator review is required before any next action.',
515
+ details: { recovery },
516
+ };
517
+ return report({
518
+ prepared: options.prepared,
519
+ mode: options.mode,
520
+ status: readError === null ? 'blocked' : 'indeterminate',
521
+ scope: options.scope,
522
+ database: recovered,
523
+ issues: [
524
+ {
525
+ code: options.response.code,
526
+ message: typeof options.response.message === 'string'
527
+ ? options.response.message
528
+ : `The database deterministically rejected process ${options.ordinal}.`,
529
+ details: { response: options.response, recovery },
530
+ },
531
+ ...(readError === null
532
+ ? [recoveryIssue]
533
+ : [
534
+ {
535
+ code: 'DATASET_FLOW_IDENTITY_PROCESS_DOMAIN_REJECTION_STATUS_READ_FAILED',
536
+ message: 'The bounded recovery status read failed; execution is indeterminate and requires operator review.',
537
+ details: { recovery, error: errorDetails(readError) },
538
+ },
539
+ ]),
540
+ ],
541
+ now: options.dependencies.now(),
542
+ });
543
+ }
544
+ function hasFlowIdentityFinalizeDecision(status) {
545
+ return (status.status === 'completed' ||
546
+ status.status === 'live_drift' ||
547
+ status.status === 'failed' ||
548
+ status.compensation_required === true ||
549
+ flowIdentityScopeIsReadyToFinalize(status));
550
+ }
551
+ function completedRunStatus(status) {
552
+ return flowIdentityScopeHasCurrentDerivativeClosure(status) ? 'passed' : 'blocked';
553
+ }
554
+ async function waitForFinalizeDecision(options) {
555
+ const waitSeconds = options.command.waitSeconds ?? 0;
556
+ const pollMs = options.command.pollMs ?? DEFAULT_POLL_MS;
557
+ if (!Number.isInteger(waitSeconds) || waitSeconds < 0 || waitSeconds > 86_400) {
558
+ fail('waitSeconds must be an integer from 0 to 86400.', 'DATASET_FLOW_IDENTITY_WAIT_INVALID');
559
+ }
560
+ if (!Number.isInteger(pollMs) || pollMs < 100 || pollMs > 60_000) {
561
+ fail('pollMs must be an integer from 100 to 60000.', 'DATASET_FLOW_IDENTITY_POLL_INVALID');
562
+ }
563
+ const deadline = Date.now() + waitSeconds * 1_000;
564
+ let current = options.initial;
565
+ while (!hasFlowIdentityFinalizeDecision(current) && Date.now() < deadline) {
566
+ await options.dependencies.sleep(Math.min(pollMs, Math.max(deadline - Date.now(), 0)));
567
+ current = await readScope(options);
568
+ validateCompletedRequests(options.prepared, current);
569
+ }
570
+ return current;
571
+ }
572
+ async function executeRun(command, dependencies, preparedInput) {
573
+ const prepared = preparedInput ?? prepareRun(command);
574
+ const mode = command.commit ? 'commit' : 'status_only';
575
+ const context = await dependencies.resolveContext({
576
+ env: command.env,
577
+ fetchImpl: command.fetchImpl,
578
+ timeoutMs: command.timeoutMs,
579
+ now: command.now,
580
+ });
581
+ assertContext(prepared, context);
582
+ if (command.commit && dependencies.claimApproval) {
583
+ const recovery = prepared.recovery;
584
+ dependencies.claimApproval({
585
+ env: command.env,
586
+ claim: {
587
+ schema_version: 'dataset-flow-identity-local-approval-claim.v1',
588
+ claimed_at_utc: dependencies.now().toISOString(),
589
+ approval_kind: recovery ? 'recovery' : 'initial',
590
+ approval_identity_sha256: recovery
591
+ ? recovery.recoveryApproval.recovery_approval_identity_sha256
592
+ : prepared.approval.execution_approval_identity_sha256,
593
+ execution_identity_sha256: prepared.identity.identity_sha256,
594
+ request_id: recovery
595
+ ? String(recovery.recoveryRequest.request_id)
596
+ : prepared.identity.request_id,
597
+ environment: 'production',
598
+ project_ref: prepared.plan.project_ref,
599
+ actor_user_id: prepared.plan.account.user_id,
600
+ actor_email: prepared.plan.account.email,
601
+ target_visibility: 'owner_draft',
602
+ user_state_claim: 'authenticated_actor_state_100_plus_own_state_0',
603
+ plan_sha256: prepared.plan.plan_sha256,
604
+ freeze_sha256: recovery
605
+ ? recovery.recoveryFreeze.recovery_freeze_sha256
606
+ : prepared.freeze.freeze_sha256,
607
+ canonical_out_dir: prepared.outDir,
608
+ maximum_cli_apply_spawns: 1,
609
+ approval_reusable: false,
610
+ },
611
+ });
612
+ }
613
+ const obtained = await obtainScope({ command, prepared, context, dependencies });
614
+ const scope = obtained.scope;
615
+ const executionPermit = obtained.executionPermit;
616
+ if (isMaintenanceRpcDomainFailure(scope)) {
617
+ return report({
618
+ prepared,
619
+ mode,
620
+ status: 'blocked',
621
+ scope: null,
622
+ database: null,
623
+ issues: [
624
+ {
625
+ code: scope.code,
626
+ message: typeof scope.message === 'string'
627
+ ? scope.message
628
+ : 'The database deterministically rejected scope preflight.',
629
+ details: scope,
630
+ },
631
+ ],
632
+ now: dependencies.now(),
633
+ });
634
+ }
635
+ if (obtained.recoveryAmbiguity) {
636
+ return report({
637
+ prepared,
638
+ mode,
639
+ status: 'indeterminate',
640
+ scope,
641
+ database: obtained.recoveryAmbiguity.observed,
642
+ issues: [
643
+ {
644
+ code: 'DATASET_FLOW_IDENTITY_RECOVERY_RESPONSE_AMBIGUOUS',
645
+ message: 'Recovery admission response was ambiguous; scope was read once and recovery was not retried. A fresh exact recovery freeze and approval are required.',
646
+ details: obtained.recoveryAmbiguity.error,
647
+ },
648
+ ],
649
+ now: dependencies.now(),
650
+ });
651
+ }
652
+ let status = await readScope({ prepared, context, scope, dependencies });
653
+ validateCompletedRequests(prepared, status);
654
+ if (status.status === 'failed' || status.status === 'live_drift') {
655
+ return report({
656
+ prepared,
657
+ mode,
658
+ status: status.status === 'live_drift' ? 'blocked' : 'failed',
659
+ scope,
660
+ database: status,
661
+ issues: status.status === 'live_drift'
662
+ ? [
663
+ {
664
+ code: status.code ?? 'FLOW_IDENTITY_PRIMARY_OR_GUARD_DRIFT',
665
+ message: 'The database dynamically reproved primary or guard drift; no process submission or compensation is allowed.',
666
+ },
667
+ ]
668
+ : [],
669
+ now: dependencies.now(),
670
+ });
671
+ }
672
+ if (!status.live_guard_current || !status.protected_closure_current) {
673
+ return report({
674
+ prepared,
675
+ mode,
676
+ status: 'blocked',
677
+ scope,
678
+ database: status,
679
+ issues: [
680
+ {
681
+ code: 'DATASET_FLOW_IDENTITY_PROTECTED_CLOSURE_DRIFT',
682
+ message: 'The exactly sealed pending/blocker/orphan closure changed; no process submission is allowed.',
683
+ },
684
+ ],
685
+ now: dependencies.now(),
686
+ });
687
+ }
688
+ if (status.status === 'completed') {
689
+ return report({
690
+ prepared,
691
+ mode,
692
+ status: completedRunStatus(status),
693
+ scope,
694
+ database: status,
695
+ now: dependencies.now(),
696
+ });
697
+ }
698
+ if (command.statusOnly) {
699
+ status = await waitForFinalizeDecision({
700
+ command,
701
+ prepared,
702
+ context,
703
+ scope,
704
+ initial: status,
705
+ dependencies,
706
+ });
707
+ return report({
708
+ prepared,
709
+ mode,
710
+ status: status.status === 'completed'
711
+ ? completedRunStatus(status)
712
+ : status.status === 'live_drift'
713
+ ? 'blocked'
714
+ : status.status === 'failed'
715
+ ? 'failed'
716
+ : 'pending',
717
+ scope,
718
+ database: status,
719
+ issues: status.compensation_required
720
+ ? [
721
+ {
722
+ code: 'FLOW_IDENTITY_DERIVATIVE_COMPENSATION_REQUIRED',
723
+ message: 'A derivative-only compensation needs a separate plan, freeze, and exact approval; the process mutation will not be replayed.',
724
+ details: { compensation_targets: status.compensation_targets },
725
+ },
726
+ ]
727
+ : status.status !== 'completed' && flowIdentityScopeIsReadyToFinalize(status)
728
+ ? [
729
+ {
730
+ code: 'FLOW_IDENTITY_FINALIZE_READY',
731
+ message: 'The derivative closure is ready. Status-only mode remains read-only; an explicit approved commit invocation is required to finalize.',
732
+ },
733
+ ]
734
+ : [],
735
+ now: dependencies.now(),
736
+ });
737
+ }
738
+ if (executionPermit === null) {
739
+ return report({
740
+ prepared,
741
+ mode,
742
+ status: 'blocked',
743
+ scope,
744
+ database: status,
745
+ issues: [
746
+ {
747
+ code: 'DATASET_FLOW_IDENTITY_FRESH_RECOVERY_APPROVAL_REQUIRED',
748
+ message: 'This wrapper has no live database permit. Scope read remains available, but any further process or finalize write requires a fresh exact recovery freeze and human approval.',
749
+ details: {
750
+ approval_reusable: false,
751
+ maximum_wrapper_invocations: 1,
752
+ completed_process_count: status.completed_process_count,
753
+ next_ordinal: status.next_ordinal,
754
+ whole_scope_proof_sha256: status.whole_scope_proof_sha256,
755
+ },
756
+ },
757
+ ],
758
+ now: dependencies.now(),
759
+ });
760
+ }
761
+ let livePermit = executionPermit;
762
+ let nextOrdinal = status.next_ordinal;
763
+ let submittedProcesses = 0;
764
+ while (!status.primary_complete &&
765
+ nextOrdinal !== null &&
766
+ nextOrdinal <= prepared.plan.processes.length) {
767
+ if (!status.primary_current ||
768
+ !status.live_guard_current ||
769
+ !status.protected_closure_current) {
770
+ return report({
771
+ prepared,
772
+ mode,
773
+ status: 'blocked',
774
+ scope,
775
+ database: status,
776
+ issues: [
777
+ {
778
+ code: 'DATASET_FLOW_IDENTITY_PROTECTED_CLOSURE_DRIFT',
779
+ message: 'The exactly sealed pending/blocker/orphan closure changed; no next process will be submitted.',
780
+ },
781
+ ],
782
+ now: dependencies.now(),
783
+ });
784
+ }
785
+ const ordinal = nextOrdinal;
786
+ const { ledger, template } = requirePendingProcess(prepared, status, ordinal);
787
+ const request = buildFlowIdentityProcessRequest({
788
+ scopeProofSha256: scope.scope_proof_sha256,
789
+ ordinal,
790
+ processIntentProofSha256: ledger.process_intent_proof_sha256,
791
+ });
792
+ const markerPath = path.join(prepared.outDir, `process-attempt-${String(ordinal).padStart(6, '0')}.json`);
793
+ const domainRejectionPath = path.join(prepared.outDir, `process-domain-rejection-${String(ordinal).padStart(6, '0')}.json`);
794
+ if (existsSync(markerPath)) {
795
+ const storedRejection = readStoredDomainRejection({
796
+ filePath: domainRejectionPath,
797
+ label: `Flow identity process ${ordinal} domain rejection`,
798
+ requestHashField: 'process_request_sha256',
799
+ expectedRequestSha256: String(request.process_request_sha256),
800
+ });
801
+ if (storedRejection) {
802
+ return reportProcessDomainRejection({
803
+ prepared,
804
+ mode,
805
+ context,
806
+ scope,
807
+ dependencies,
808
+ ordinal,
809
+ requestSha256: String(request.process_request_sha256),
810
+ response: storedRejection,
811
+ rejectionPath: domainRejectionPath,
812
+ });
813
+ }
814
+ return report({
815
+ prepared,
816
+ mode,
817
+ status: 'blocked',
818
+ scope,
819
+ database: status,
820
+ issues: [
821
+ {
822
+ code: 'DATASET_FLOW_IDENTITY_PROCESS_ATTEMPT_AMBIGUOUS',
823
+ message: `Process ${ordinal} has a local attempt marker but no durable completed proof; no automatic retry is allowed.`,
824
+ },
825
+ ],
826
+ now: dependencies.now(),
827
+ });
828
+ }
829
+ writePrivateImmutableJson(markerPath, {
830
+ schema_version: 'dataset-flow-identity-process-attempt.v2',
831
+ generated_at_utc: dependencies.now().toISOString(),
832
+ scope_id: scope.scope_id,
833
+ scope_proof_sha256: scope.scope_proof_sha256,
834
+ ordinal,
835
+ process_request_sha256: request.process_request_sha256,
836
+ max_posts: 1,
837
+ automatic_retry: false,
838
+ });
839
+ try {
840
+ const raw = await dependencies.rewrite({
841
+ context,
842
+ scopeId: scope.scope_id,
843
+ request,
844
+ authorization: livePermit,
845
+ });
846
+ if (isMaintenanceRpcDomainFailure(raw)) {
847
+ assertDomainFailureHasNoPermit(raw, `Process ${ordinal}`);
848
+ return reportProcessDomainRejection({
849
+ prepared,
850
+ mode,
851
+ context,
852
+ scope,
853
+ dependencies,
854
+ ordinal,
855
+ requestSha256: String(request.process_request_sha256),
856
+ response: raw,
857
+ rejectionPath: domainRejectionPath,
858
+ });
859
+ }
860
+ const envelope = splitFlowIdentityPermitResponse({
861
+ value: raw,
862
+ expectedGeneration: livePermit.generation + 1,
863
+ expectedInvocationId: livePermit.invocation_id,
864
+ permitRequired: true,
865
+ label: `Flow identity process ${ordinal}`,
866
+ });
867
+ const proof = parseFlowIdentityProcessProof({
868
+ value: envelope.proof,
869
+ scopeId: scope.scope_id,
870
+ process: template.process,
871
+ requestSha256: String(request.process_request_sha256),
872
+ receiptId: prepared.plan.receipt_id,
873
+ receiptProofSha256: prepared.plan.receipt_proof_sha256,
874
+ mappingGuardSetSha256: prepared.plan.mapping_guard_set_sha256,
875
+ processIntentSetSha256: prepared.plan.process_intent_set_sha256,
876
+ processIntentProofSha256: ledger.process_intent_proof_sha256,
877
+ processCount: prepared.plan.processes.length,
878
+ expectedInvocationId: livePermit.invocation_id,
879
+ expectedPermitGenerationBefore: livePermit.generation,
880
+ });
881
+ writePrivateImmutableJson(path.join(prepared.outDir, `process-proof-${String(ordinal).padStart(6, '0')}.json`), proof);
882
+ // permitRequired=true above already rejects null before the proof parser.
883
+ livePermit = envelope.executionPermit;
884
+ submittedProcesses += 1;
885
+ nextOrdinal = proof.next_ordinal;
886
+ }
887
+ catch (error) {
888
+ writePrivateImmutableJson(path.join(prepared.outDir, `process-transport-error-${String(ordinal).padStart(6, '0')}.json`), {
889
+ schema_version: 'dataset-flow-identity-process-transport-error.v1',
890
+ observed_at_utc: dependencies.now().toISOString(),
891
+ ordinal,
892
+ process_request_sha256: request.process_request_sha256,
893
+ automatic_retry: false,
894
+ recovery: 'read_scope_only',
895
+ error: errorDetails(error),
896
+ });
897
+ let recovered = null;
898
+ try {
899
+ recovered = await readScope({ prepared, context, scope, dependencies });
900
+ validateCompletedRequests(prepared, recovered);
901
+ }
902
+ catch (readError) {
903
+ writePrivateImmutableJson(path.join(prepared.outDir, `process-recovery-read-error-${String(ordinal).padStart(6, '0')}.json`), { observed_at_utc: dependencies.now().toISOString(), error: errorDetails(readError) });
904
+ }
905
+ return report({
906
+ prepared,
907
+ mode,
908
+ status: 'indeterminate',
909
+ scope,
910
+ database: recovered,
911
+ issues: [
912
+ {
913
+ code: 'DATASET_FLOW_IDENTITY_PROCESS_RESPONSE_AMBIGUOUS',
914
+ message: 'Process response was ambiguous; durable scope was read once and no process submission was retried.',
915
+ details: errorDetails(error),
916
+ },
917
+ ],
918
+ now: dependencies.now(),
919
+ });
920
+ }
921
+ }
922
+ if (submittedProcesses > 0) {
923
+ status = await readScope({ prepared, context, scope, dependencies });
924
+ validateCompletedRequests(prepared, status);
925
+ }
926
+ if (!status.primary_current || !status.live_guard_current || !status.protected_closure_current) {
927
+ return report({
928
+ prepared,
929
+ mode,
930
+ status: 'blocked',
931
+ scope,
932
+ database: status,
933
+ issues: [
934
+ {
935
+ code: 'DATASET_FLOW_IDENTITY_PROTECTED_CLOSURE_DRIFT',
936
+ message: 'The exactly sealed pending/blocker/orphan closure changed after the last process; finalize is forbidden.',
937
+ },
938
+ ],
939
+ now: dependencies.now(),
940
+ });
941
+ }
942
+ status = await waitForFinalizeDecision({
943
+ command,
944
+ prepared,
945
+ context,
946
+ scope,
947
+ initial: status,
948
+ dependencies,
949
+ });
950
+ if (status.status === 'completed') {
951
+ return report({
952
+ prepared,
953
+ mode,
954
+ status: completedRunStatus(status),
955
+ scope,
956
+ database: status,
957
+ now: dependencies.now(),
958
+ });
959
+ }
960
+ if (status.status === 'live_drift' || status.status === 'failed') {
961
+ return report({
962
+ prepared,
963
+ mode,
964
+ status: status.status === 'live_drift' ? 'blocked' : 'failed',
965
+ scope,
966
+ database: status,
967
+ issues: status.status === 'live_drift'
968
+ ? [
969
+ {
970
+ code: status.code ?? 'FLOW_IDENTITY_PRIMARY_OR_GUARD_DRIFT',
971
+ message: 'The database dynamically reproved primary or guard drift while derivatives were settling; finalize was not submitted.',
972
+ },
973
+ ]
974
+ : [],
975
+ now: dependencies.now(),
976
+ });
977
+ }
978
+ if (!status.primary_current || !status.live_guard_current || !status.protected_closure_current) {
979
+ return report({
980
+ prepared,
981
+ mode,
982
+ status: 'blocked',
983
+ scope,
984
+ database: status,
985
+ issues: [
986
+ {
987
+ code: 'DATASET_FLOW_IDENTITY_PROTECTED_CLOSURE_DRIFT',
988
+ message: 'The exactly sealed closure changed while derivatives were settling; finalize was not submitted.',
989
+ },
990
+ ],
991
+ now: dependencies.now(),
992
+ });
993
+ }
994
+ if (status.compensation_required) {
995
+ return report({
996
+ prepared,
997
+ mode,
998
+ status: 'pending',
999
+ scope,
1000
+ database: status,
1001
+ issues: [
1002
+ {
1003
+ code: 'FLOW_IDENTITY_DERIVATIVE_COMPENSATION_REQUIRED',
1004
+ message: 'A derivative-only compensation needs a separate plan, freeze, and exact approval; finalize was not submitted and the process mutation will not be replayed.',
1005
+ details: { compensation_targets: status.compensation_targets },
1006
+ },
1007
+ ],
1008
+ now: dependencies.now(),
1009
+ });
1010
+ }
1011
+ if (!flowIdentityScopeIsReadyToFinalize(status)) {
1012
+ return report({
1013
+ prepared,
1014
+ mode,
1015
+ status: 'pending',
1016
+ scope,
1017
+ database: status,
1018
+ issues: [
1019
+ {
1020
+ code: 'FLOW_IDENTITY_DERIVATIVES_PENDING',
1021
+ message: 'Derivative closure is still pending at the wait deadline; finalize was not submitted. This in-memory permit is discarded when the wrapper exits, so continuation requires a fresh exact recovery freeze and human approval.',
1022
+ },
1023
+ ],
1024
+ now: dependencies.now(),
1025
+ });
1026
+ }
1027
+ const finalizeRequest = buildFlowIdentityFinalizeRequest({
1028
+ scopeProofSha256: scope.scope_proof_sha256,
1029
+ plan: prepared.plan,
1030
+ status,
1031
+ });
1032
+ const finalizeRequestSha256 = flowIdentityRestrictedSha256(finalizeRequest);
1033
+ const finalizeAttemptPath = finalizeArtifactPath(prepared, 'attempt', finalizeRequestSha256);
1034
+ const finalizeRejectionPath = finalizeArtifactPath(prepared, 'domain-rejection', finalizeRequestSha256);
1035
+ const finalizeProofPath = finalizeArtifactPath(prepared, 'proof', finalizeRequestSha256);
1036
+ const finalizeTransportPath = finalizeArtifactPath(prepared, 'transport-error', finalizeRequestSha256);
1037
+ const priorAttemptHashes = storedFinalizeAttemptHashes(prepared);
1038
+ const exactStoredRequest = readStoredFinalizeAttempt({
1039
+ prepared,
1040
+ scope,
1041
+ requestSha256: finalizeRequestSha256,
1042
+ });
1043
+ if (exactStoredRequest) {
1044
+ const storedRejection = readStoredDomainRejection({
1045
+ filePath: finalizeRejectionPath,
1046
+ label: 'Flow identity finalize domain rejection',
1047
+ requestHashField: 'request_sha256',
1048
+ expectedRequestSha256: finalizeRequestSha256,
1049
+ });
1050
+ const storedProof = readStoredFinalizeProof({
1051
+ prepared,
1052
+ scope,
1053
+ request: exactStoredRequest,
1054
+ requestSha256: finalizeRequestSha256,
1055
+ });
1056
+ if (storedRejection && storedProof) {
1057
+ fail('Finalize request has contradictory rejection and proof artifacts.', 'DATASET_FLOW_IDENTITY_FINALIZE_ARTIFACT_CONFLICT');
1058
+ }
1059
+ if (storedRejection) {
1060
+ return report({
1061
+ prepared,
1062
+ mode,
1063
+ status: 'blocked',
1064
+ scope,
1065
+ database: status,
1066
+ issues: [
1067
+ {
1068
+ code: storedRejection.code,
1069
+ message: typeof storedRejection.message === 'string'
1070
+ ? storedRejection.message
1071
+ : 'The database deterministically rejected finalize.',
1072
+ details: {
1073
+ response: storedRejection,
1074
+ request_sha256: finalizeRequestSha256,
1075
+ recovery: OPERATOR_REVIEW_REQUIRED,
1076
+ },
1077
+ },
1078
+ ],
1079
+ now: dependencies.now(),
1080
+ });
1081
+ }
1082
+ if (storedProof) {
1083
+ return report({
1084
+ prepared,
1085
+ mode,
1086
+ status: storedProof.status === 'failed'
1087
+ ? 'failed'
1088
+ : storedProof.status === 'live_drift'
1089
+ ? 'blocked'
1090
+ : 'pending',
1091
+ scope,
1092
+ database: status,
1093
+ issues: [
1094
+ {
1095
+ code: 'DATASET_FLOW_IDENTITY_FINALIZE_REQUEST_ALREADY_PROVEN',
1096
+ message: 'This exact finalize request already has a durable proof and will not be posted again. A new explicit finalize requires a prior derivatives_pending proof and a later status that creates a new request hash.',
1097
+ details: {
1098
+ request_sha256: finalizeRequestSha256,
1099
+ proof_status: storedProof.status,
1100
+ },
1101
+ },
1102
+ ],
1103
+ now: dependencies.now(),
1104
+ });
1105
+ }
1106
+ return report({
1107
+ prepared,
1108
+ mode,
1109
+ status: 'indeterminate',
1110
+ scope,
1111
+ database: status,
1112
+ issues: [
1113
+ {
1114
+ code: 'DATASET_FLOW_IDENTITY_FINALIZE_ATTEMPT_AMBIGUOUS',
1115
+ message: 'This exact finalize request has an attempt but no durable proof or domain rejection; the scope was read first and finalize will not be posted again.',
1116
+ details: { request_sha256: finalizeRequestSha256 },
1117
+ },
1118
+ ],
1119
+ now: dependencies.now(),
1120
+ });
1121
+ }
1122
+ if (existsSync(finalizeRejectionPath) || existsSync(finalizeProofPath)) {
1123
+ fail('Finalize terminal artifact exists without its exact attempt.', 'DATASET_FLOW_IDENTITY_FINALIZE_ARTIFACT_INVALID');
1124
+ }
1125
+ for (const priorRequestSha256 of priorAttemptHashes) {
1126
+ const priorRequest = readStoredFinalizeAttempt({
1127
+ prepared,
1128
+ scope,
1129
+ requestSha256: priorRequestSha256,
1130
+ });
1131
+ if (!priorRequest)
1132
+ continue;
1133
+ const priorRejection = readStoredDomainRejection({
1134
+ filePath: finalizeArtifactPath(prepared, 'domain-rejection', priorRequestSha256),
1135
+ label: 'Prior flow identity finalize domain rejection',
1136
+ requestHashField: 'request_sha256',
1137
+ expectedRequestSha256: priorRequestSha256,
1138
+ });
1139
+ const priorProof = readStoredFinalizeProof({
1140
+ prepared,
1141
+ scope,
1142
+ request: priorRequest,
1143
+ requestSha256: priorRequestSha256,
1144
+ });
1145
+ if (priorRejection || !priorProof) {
1146
+ return report({
1147
+ prepared,
1148
+ mode,
1149
+ status: priorRejection ? 'blocked' : 'indeterminate',
1150
+ scope,
1151
+ database: status,
1152
+ issues: [
1153
+ {
1154
+ code: priorRejection?.code ?? 'DATASET_FLOW_IDENTITY_FINALIZE_ATTEMPT_AMBIGUOUS',
1155
+ message: 'A prior finalize attempt is not backed by a valid derivatives_pending proof; no new finalize request is allowed in this output directory.',
1156
+ details: {
1157
+ prior_request_sha256: priorRequestSha256,
1158
+ current_request_sha256: finalizeRequestSha256,
1159
+ },
1160
+ },
1161
+ ],
1162
+ now: dependencies.now(),
1163
+ });
1164
+ }
1165
+ if (priorProof.status !== 'derivatives_pending') {
1166
+ return report({
1167
+ prepared,
1168
+ mode,
1169
+ status: priorProof.status === 'failed' ? 'failed' : 'blocked',
1170
+ scope,
1171
+ database: status,
1172
+ issues: [
1173
+ {
1174
+ code: 'DATASET_FLOW_IDENTITY_FINALIZE_NEW_REQUEST_NOT_AUTHORIZED',
1175
+ message: 'Only a valid derivatives_pending proof can authorize a later explicit finalize with a new status-derived request hash.',
1176
+ details: {
1177
+ prior_request_sha256: priorRequestSha256,
1178
+ prior_status: priorProof.status,
1179
+ current_request_sha256: finalizeRequestSha256,
1180
+ },
1181
+ },
1182
+ ],
1183
+ now: dependencies.now(),
1184
+ });
1185
+ }
1186
+ }
1187
+ writePrivateImmutableJson(finalizeAttemptPath, {
1188
+ schema_version: 'dataset-flow-identity-finalize-attempt.v3',
1189
+ generated_at_utc: dependencies.now().toISOString(),
1190
+ scope_id: scope.scope_id,
1191
+ request_sha256: finalizeRequestSha256,
1192
+ request: finalizeRequest,
1193
+ max_posts: 1,
1194
+ automatic_retry: false,
1195
+ });
1196
+ let finalizeProof;
1197
+ try {
1198
+ const raw = await dependencies.finalize({
1199
+ context,
1200
+ scopeId: scope.scope_id,
1201
+ request: finalizeRequest,
1202
+ authorization: livePermit,
1203
+ });
1204
+ if (isJsonObject(raw) &&
1205
+ raw.schema_version === 'dataset-flow-identity-scope-finalize-result.v2') {
1206
+ const permitUsed = livePermit;
1207
+ const envelope = raw.ok === true
1208
+ ? splitFlowIdentityPermitResponse({
1209
+ value: raw,
1210
+ expectedGeneration: permitUsed.generation + 1,
1211
+ expectedInvocationId: permitUsed.invocation_id,
1212
+ permitRequired: raw.status === 'derivatives_pending',
1213
+ permitForbidden: raw.status !== 'derivatives_pending',
1214
+ label: 'Flow identity finalize',
1215
+ })
1216
+ : (() => {
1217
+ if (Object.hasOwn(raw, 'execution_permit')) {
1218
+ fail('Rejected flow identity finalize response contained a bearer permit.', 'DATASET_FLOW_IDENTITY_DOMAIN_REJECTION_PERMIT_INVALID');
1219
+ }
1220
+ return { proof: raw, executionPermit: null };
1221
+ })();
1222
+ finalizeProof = parseFlowIdentityFinalizeProof({
1223
+ value: envelope.proof,
1224
+ plan: prepared.plan,
1225
+ scopeId: scope.scope_id,
1226
+ scopeProofSha256: scope.scope_proof_sha256,
1227
+ request: finalizeRequest,
1228
+ expectedInvocationId: permitUsed.invocation_id,
1229
+ expectedPermitGenerationBefore: permitUsed.generation,
1230
+ });
1231
+ if (envelope.executionPermit !== null)
1232
+ livePermit = envelope.executionPermit;
1233
+ writePrivateImmutableJson(finalizeProofPath, finalizeProof);
1234
+ }
1235
+ else if (isMaintenanceRpcDomainFailure(raw)) {
1236
+ assertDomainFailureHasNoPermit(raw, 'Finalize');
1237
+ writePrivateImmutableJson(finalizeRejectionPath, {
1238
+ schema_version: 'dataset-flow-identity-finalize-domain-rejection.v1',
1239
+ status: 'rejected',
1240
+ scope_id: scope.scope_id,
1241
+ request_sha256: finalizeRequestSha256,
1242
+ automatic_retry: false,
1243
+ approval_reusable: false,
1244
+ recovery: OPERATOR_REVIEW_REQUIRED,
1245
+ response: raw,
1246
+ });
1247
+ return report({
1248
+ prepared,
1249
+ mode,
1250
+ status: 'blocked',
1251
+ scope,
1252
+ database: status,
1253
+ issues: [
1254
+ {
1255
+ code: raw.code,
1256
+ message: typeof raw.message === 'string'
1257
+ ? raw.message
1258
+ : 'The database deterministically rejected finalize.',
1259
+ details: {
1260
+ response: raw,
1261
+ request_sha256: finalizeRequestSha256,
1262
+ recovery: OPERATOR_REVIEW_REQUIRED,
1263
+ },
1264
+ },
1265
+ ],
1266
+ now: dependencies.now(),
1267
+ });
1268
+ }
1269
+ else {
1270
+ fail('Finalize returned neither a guarded proof nor a deterministic domain rejection.', 'DATASET_FLOW_IDENTITY_FINALIZE_RESPONSE_INVALID');
1271
+ }
1272
+ }
1273
+ catch (error) {
1274
+ writePrivateImmutableJson(finalizeTransportPath, {
1275
+ schema_version: 'dataset-flow-identity-finalize-transport-error.v1',
1276
+ observed_at_utc: dependencies.now().toISOString(),
1277
+ scope_id: scope.scope_id,
1278
+ request_sha256: finalizeRequestSha256,
1279
+ automatic_retry: false,
1280
+ recovery: 'read_scope_only',
1281
+ error: errorDetails(error),
1282
+ });
1283
+ let recovered = null;
1284
+ try {
1285
+ recovered = await readScope({ prepared, context, scope, dependencies });
1286
+ validateCompletedRequests(prepared, recovered);
1287
+ }
1288
+ catch {
1289
+ // The immutable transport error is sufficient; never replace it with a blind finalize retry.
1290
+ }
1291
+ return report({
1292
+ prepared,
1293
+ mode,
1294
+ status: 'indeterminate',
1295
+ scope,
1296
+ database: recovered,
1297
+ issues: [
1298
+ {
1299
+ code: 'DATASET_FLOW_IDENTITY_FINALIZE_RESPONSE_AMBIGUOUS',
1300
+ message: 'Finalize response was ambiguous; scope was read once and finalize was not retried in this invocation.',
1301
+ details: errorDetails(error),
1302
+ },
1303
+ ],
1304
+ now: dependencies.now(),
1305
+ });
1306
+ }
1307
+ status = await readScope({ prepared, context, scope, dependencies });
1308
+ validateCompletedRequests(prepared, status);
1309
+ return report({
1310
+ prepared,
1311
+ mode,
1312
+ status: finalizeProof.status === 'completed' &&
1313
+ status.status === 'completed' &&
1314
+ flowIdentityScopeHasCurrentDerivativeClosure(status)
1315
+ ? 'passed'
1316
+ : status.status === 'live_drift' || finalizeProof.status === 'live_drift'
1317
+ ? 'blocked'
1318
+ : status.status === 'failed' || finalizeProof.status === 'failed'
1319
+ ? 'failed'
1320
+ : 'pending',
1321
+ scope,
1322
+ database: status,
1323
+ issues: finalizeProof.compensation_required
1324
+ ? [
1325
+ {
1326
+ code: 'FLOW_IDENTITY_DERIVATIVE_COMPENSATION_REQUIRED',
1327
+ message: 'A derivative-only compensation needs a separate plan, freeze, and exact approval; the process mutation will not be replayed.',
1328
+ details: { compensation_targets: finalizeProof.compensation_targets },
1329
+ },
1330
+ ]
1331
+ : [],
1332
+ now: dependencies.now(),
1333
+ });
1334
+ }
1335
+ export async function runFlowIdentity(options) {
1336
+ // Status-only may be invoked with a different caller path. Resolve the
1337
+ // immutable approval claim before locking so every reader/writer serializes
1338
+ // on the one canonical run directory.
1339
+ const prepared = prepareRun(options);
1340
+ const outDir = prepared.outDir;
1341
+ return withStateFileLock(path.join(outDir, 'flow-identity-run-state'), { reason: 'dataset flow identity serial runner', timeoutMs: 0 }, () => executeRun(options, {
1342
+ resolveContext: resolveMaintenanceRemoteContext,
1343
+ preflight: preflightMaintenanceFlowIdentityScope,
1344
+ lookup: lookupMaintenanceFlowIdentityScope,
1345
+ recover: recoverMaintenanceFlowIdentityScope,
1346
+ rewrite: rewriteMaintenanceFlowIdentityProcess,
1347
+ read: readMaintenanceFlowIdentityScope,
1348
+ finalize: finalizeMaintenanceFlowIdentityScope,
1349
+ sleep: options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
1350
+ now: () => options.now ?? new Date(),
1351
+ claimApproval: claimFlowIdentityApproval,
1352
+ }, prepared));
1353
+ }
1354
+ export const __testInternals = {
1355
+ assertContext,
1356
+ completedRunStatus,
1357
+ errorDetails,
1358
+ executeRun,
1359
+ loadProcessTemplates,
1360
+ numberedArtifact,
1361
+ prepareRun,
1362
+ processStem,
1363
+ readCanonicalJson,
1364
+ requirePendingProcess,
1365
+ report,
1366
+ validateCompletedRequests,
1367
+ waitForFinalizeDecision,
1368
+ };
1369
+ //# sourceMappingURL=dataset-maintenance-flow-identity-run.js.map