review-council 4.4.15 → 4.4.17
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.
- package/README.md +5 -2
- package/dist/converge/correction-anchors.d.ts +1 -0
- package/dist/converge/correction-anchors.js +2 -0
- package/dist/converge/correction-anchors.js.map +1 -0
- package/dist/converge/ordinary-pending-package.js +5 -2
- package/dist/converge/ordinary-pending-package.js.map +1 -1
- package/dist/converge/pending-legacy-resume.d.ts +50 -0
- package/dist/converge/pending-legacy-resume.js +337 -3
- package/dist/converge/pending-legacy-resume.js.map +1 -1
- package/dist/converge/run-state.d.ts +2 -0
- package/dist/converge/run-state.js +28 -7
- package/dist/converge/run-state.js.map +1 -1
- package/dist/evidence/claim-recovery/authenticated-receipts.d.ts +46 -0
- package/dist/evidence/claim-recovery/authenticated-receipts.js +120 -0
- package/dist/evidence/claim-recovery/authenticated-receipts.js.map +1 -0
- package/dist/evidence/claim-recovery/carrier-inventory.d.ts +49 -0
- package/dist/evidence/claim-recovery/carrier-inventory.js +340 -0
- package/dist/evidence/claim-recovery/carrier-inventory.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/anchors.d.ts +20 -0
- package/dist/evidence/claim-recovery/validation/anchors.js +22 -0
- package/dist/evidence/claim-recovery/validation/anchors.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/carrier-projection.d.ts +2 -0
- package/dist/evidence/claim-recovery/validation/carrier-projection.js +3 -0
- package/dist/evidence/claim-recovery/validation/carrier-projection.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/carrier-types.d.ts +83 -0
- package/dist/evidence/claim-recovery/validation/carrier-types.js +2 -0
- package/dist/evidence/claim-recovery/validation/carrier-types.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/claim-split.d.ts +57 -0
- package/dist/evidence/claim-recovery/validation/claim-split.js +209 -0
- package/dist/evidence/claim-recovery/validation/claim-split.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/current-projection.d.ts +20 -0
- package/dist/evidence/claim-recovery/validation/current-projection.js +2 -0
- package/dist/evidence/claim-recovery/validation/current-projection.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/native-material.d.ts +17 -0
- package/dist/evidence/claim-recovery/validation/native-material.js +2 -0
- package/dist/evidence/claim-recovery/validation/native-material.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/native-occurrences.d.ts +62 -0
- package/dist/evidence/claim-recovery/validation/native-occurrences.js +2 -0
- package/dist/evidence/claim-recovery/validation/native-occurrences.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/native-state.d.ts +21 -0
- package/dist/evidence/claim-recovery/validation/native-state.js +162 -0
- package/dist/evidence/claim-recovery/validation/native-state.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/obligations.d.ts +15 -0
- package/dist/evidence/claim-recovery/validation/obligations.js +65 -0
- package/dist/evidence/claim-recovery/validation/obligations.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/occurrence-source.d.ts +45 -0
- package/dist/evidence/claim-recovery/validation/occurrence-source.js +208 -0
- package/dist/evidence/claim-recovery/validation/occurrence-source.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/occurrence-types.d.ts +101 -0
- package/dist/evidence/claim-recovery/validation/occurrence-types.js +2 -0
- package/dist/evidence/claim-recovery/validation/occurrence-types.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/sighting.d.ts +2 -0
- package/dist/evidence/claim-recovery/validation/sighting.js +15 -0
- package/dist/evidence/claim-recovery/validation/sighting.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/sources.d.ts +15 -0
- package/dist/evidence/claim-recovery/validation/sources.js +2 -0
- package/dist/evidence/claim-recovery/validation/sources.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/types.d.ts +126 -0
- package/dist/evidence/claim-recovery/validation/types.js +2 -0
- package/dist/evidence/claim-recovery/validation/types.js.map +1 -0
- package/dist/evidence/claim-split.d.ts +1 -0
- package/dist/evidence/claim-split.js +2 -0
- package/dist/evidence/claim-split.js.map +1 -0
- package/dist/index.js +57 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,7 +111,8 @@ Review a PR, a local diff, or uncommitted work.
|
|
|
111
111
|
| `--retry-reason <reason>` | Explicit bounded recovery decision for a failed/unknown launch; preserves spent attempts |
|
|
112
112
|
| `--retry-report <path>` | Bind an original legacy report to an inconclusive retry; new inputs may differ; requires `--retry-reason` |
|
|
113
113
|
| `--resume-pending` / `--resume-async-sha256 <hashes>` | Recover one proven dead-owner pending launch while retaining exact async artifacts and treating unknown blocking work as failed |
|
|
114
|
-
| `--ordinary-pending-package <path>` / `--preview-pending` | Authenticate a sealed ordinary pending recovery package without mutation
|
|
114
|
+
| `--ordinary-pending-package <path>` / `--preview-pending` | Authenticate a sealed ordinary pending recovery package without mutation before choosing a recovery operation |
|
|
115
|
+
| `--finalize-pending-only` / `--pending-native-sha256 <digest>` / `--pending-attempt-sha256 <digest>` | Finalize the previewed ordinary pending attempt as failed/unknown without claiming or dispatching its successor |
|
|
115
116
|
| `--max-attempts <n>` / `--max-rounds <n>` | Guarded launch only: explicitly authorized caps; omission preserves native caps |
|
|
116
117
|
| `--attest` | GitHub Actions gate workflow only: exchange the job's OIDC token for a run-bound Harness credential and record the review as attested (see below) |
|
|
117
118
|
| `--config <path>` | Path to a config file |
|
|
@@ -236,7 +237,9 @@ new claim retains the source bytes and binding; no history or budget is reset.
|
|
|
236
237
|
|
|
237
238
|
For an ordinary pending launch whose coordinator is provably dead, supply an independently reconstructed immutable package that binds the original target, head, base, guarded input, attempt, round, PID and retained async artifact descriptors. RCL recomputes its production guarded-input digest and checks every binding under the target lock before it mutates native state.
|
|
238
239
|
|
|
239
|
-
Use `--preview-pending` with `--resume-pending
|
|
240
|
+
Use `--preview-pending` with either `--resume-pending` or `--finalize-pending-only` and `--ordinary-pending-package <path>` to authenticate that package with zero state, output or provider writes. Combined `--resume-pending` repeats the checks, marks the spent attempt failed with blocking outcome unknown, archives the exact retained async artifacts, and claims exactly the next checkpointed attempt under its explicitly bounded cap.
|
|
241
|
+
|
|
242
|
+
Use `--finalize-pending-only` when recovery must stop at that failed/unknown finalization. Pass the unchanged cap plus the exact `nativeStateSha256` and `attemptStateSha256` returned by its preview as `--pending-native-sha256` and `--pending-attempt-sha256`. Apply archives the retained async artifacts idempotently and returns a source-bound receipt while leaving the attempt counter, cap, round history and next-free ordinal unchanged. Repeating the command reads back the same receipt; it never creates a successor claim, checkpoint, reviewer callback or provider call. Receipt readback validates either the exact finalized state or a monotonic successor against retained source snapshots. An exact legacy receipt without those snapshots is upgraded idempotently before a successor proceeds, without changing native state or accounting. A later guarded convergence process may claim the next attempt independently.
|
|
240
243
|
|
|
241
244
|
When native triage reports `converged-dismissal-only` but Harness still reports
|
|
242
245
|
`fixes_pending` with no actionable findings, an explicit recovery can authorize
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../evidence/claim-recovery/validation/anchors.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correction-anchors.js","sourceRoot":"","sources":["../../src/converge/correction-anchors.ts"],"names":[],"mappings":"AAAA,cAAc,kDAAkD,CAAC"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { guardedInputSha256, stableStringify } from '../report/run-header.js';
|
|
2
2
|
export function validateOrdinaryPendingPackage(value, expected) {
|
|
3
|
-
const input = value?.guardedInput
|
|
3
|
+
const input = value?.guardedInput;
|
|
4
|
+
const specPresent = !!input && Object.hasOwn(input, 'spec');
|
|
5
|
+
const keys = ['asyncRoles', 'config', 'diff', 'head', 'kind', 'pr', 'prompts', 'repo', 'roster',
|
|
6
|
+
...(specPresent ? ['spec'] : [])];
|
|
4
7
|
const digest = (candidate) => typeof candidate === 'string' && /^[a-f0-9]{64}$/.test(candidate);
|
|
5
8
|
const structuredInput = input && input.kind === 'patch' && typeof input.repo === 'string' && /^[^/\s]+\/[^/\s]+$/.test(input.repo) &&
|
|
6
9
|
Number.isSafeInteger(input.pr) && input.pr > 0 && digest(input.diff) && digest(input.config) &&
|
|
@@ -11,7 +14,7 @@ export function validateOrdinaryPendingPackage(value, expected) {
|
|
|
11
14
|
const role = item;
|
|
12
15
|
return item && typeof item === 'object' && typeof role.name === 'string' && role.name.length > 0;
|
|
13
16
|
}) &&
|
|
14
|
-
input.spec && typeof input.spec === 'object';
|
|
17
|
+
(!specPresent || (input.spec && typeof input.spec === 'object'));
|
|
15
18
|
if (!value || value.target !== expected.target || value.headSha !== expected.headSha || value.baseSha !== expected.baseSha || value.attempt !== expected.attempt || value.round !== expected.round || value.pid !== expected.pid || !/^[a-f0-9]{40}$/.test(value.baseSha) ||
|
|
16
19
|
!Number.isSafeInteger(value.attempt) || value.attempt < 1 || !Number.isSafeInteger(value.round) || value.round < 1 ||
|
|
17
20
|
!Number.isSafeInteger(value.pid) || value.pid < 1 || !input || Object.keys(input).sort().join(',') !== keys.join(',') ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ordinary-pending-package.js","sourceRoot":"","sources":["../../src/converge/ordinary-pending-package.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAK9E,MAAM,UAAU,8BAA8B,CAAC,KAA6B,EAAE,QAA+B;IAC3G,MAAM,KAAK,GAAG,KAAK,EAAE,YAAY,EAAE,IAAI,GAAG,CAAC,YAAY,EAAC,QAAQ,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,IAAI,EAAC,SAAS,EAAC,MAAM,EAAC,QAAQ,
|
|
1
|
+
{"version":3,"file":"ordinary-pending-package.js","sourceRoot":"","sources":["../../src/converge/ordinary-pending-package.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { type LegacyRetrySelection } from './legacy-launch-health.js';
|
|
2
3
|
import { type GuardedLaunchCompletion } from './launch-record.js';
|
|
4
|
+
import { writeState } from './run-state.js';
|
|
3
5
|
import { type ConvergeAttemptClaim } from './attempt-budget.js';
|
|
4
6
|
import { type NativeTargetOwnership } from './target-ownership.js';
|
|
5
7
|
import { type OriginalLaunch } from '../dispatch/original-launch.js';
|
|
@@ -43,6 +45,7 @@ export interface PendingLegacyResumeOptions {
|
|
|
43
45
|
/** Required for recovering an ordinary dead-owner launch without a legacy report. */
|
|
44
46
|
migrationPackage?: OrdinaryPendingPackage;
|
|
45
47
|
preview?: boolean;
|
|
48
|
+
previewMode?: 'combined' | 'finalize-only';
|
|
46
49
|
}
|
|
47
50
|
export interface PendingLegacyResumeResult {
|
|
48
51
|
claim: ConvergeAttemptClaim;
|
|
@@ -57,6 +60,52 @@ export interface OrdinaryPendingPreview {
|
|
|
57
60
|
cap: number;
|
|
58
61
|
nextAttempt: number;
|
|
59
62
|
}
|
|
63
|
+
export interface OrdinaryPendingFinalizeOptions {
|
|
64
|
+
gitCommonDir: string;
|
|
65
|
+
target: string;
|
|
66
|
+
headSha: string;
|
|
67
|
+
baseSha: string;
|
|
68
|
+
pendingInputSha256: string;
|
|
69
|
+
nativeStateSha256: string;
|
|
70
|
+
attemptStateSha256: string;
|
|
71
|
+
retainedAsyncSha256: readonly string[];
|
|
72
|
+
maxAttempts: number;
|
|
73
|
+
migrationPackage: OrdinaryPendingPackage;
|
|
74
|
+
ownerAlive?: (pid: number) => boolean;
|
|
75
|
+
loadRetainedAsync: () => Promise<AsyncResultReference[]>;
|
|
76
|
+
/** Fault-injection boundary for proving archive-complete/native-write recovery. */
|
|
77
|
+
writeFinalizedState?: typeof writeState;
|
|
78
|
+
}
|
|
79
|
+
declare const ordinaryPendingFinalizationReceiptSchema: z.ZodObject<{
|
|
80
|
+
version: z.ZodLiteral<1>;
|
|
81
|
+
operation: z.ZodLiteral<"ordinary-pending-finalize-only">;
|
|
82
|
+
target: z.ZodString;
|
|
83
|
+
headSha: z.ZodString;
|
|
84
|
+
baseSha: z.ZodString;
|
|
85
|
+
inputSha256: z.ZodString;
|
|
86
|
+
sourceDigest: z.ZodString;
|
|
87
|
+
migrationPackageSha256: z.ZodString;
|
|
88
|
+
finalizedAttempt: z.ZodNumber;
|
|
89
|
+
round: z.ZodNumber;
|
|
90
|
+
originalPid: z.ZodNumber;
|
|
91
|
+
attemptsUsed: z.ZodNumber;
|
|
92
|
+
cap: z.ZodNumber;
|
|
93
|
+
nextFreeAttempt: z.ZodNumber;
|
|
94
|
+
blockingOutcome: z.ZodLiteral<"unknown">;
|
|
95
|
+
retainedAsyncSha256: z.ZodArray<z.ZodString>;
|
|
96
|
+
sourceNativeStateSha256: z.ZodString;
|
|
97
|
+
sourceAttemptStateSha256: z.ZodString;
|
|
98
|
+
finalizedNativeStateSha256: z.ZodString;
|
|
99
|
+
receiptDigest: z.ZodString;
|
|
100
|
+
}, z.core.$strict>;
|
|
101
|
+
export type OrdinaryPendingFinalizationReceipt = z.infer<typeof ordinaryPendingFinalizationReceiptSchema>;
|
|
102
|
+
export interface OrdinaryPendingFinalizeResult {
|
|
103
|
+
receipt: OrdinaryPendingFinalizationReceipt;
|
|
104
|
+
reusedReceipt: boolean;
|
|
105
|
+
snapshotBindingUpgraded: boolean;
|
|
106
|
+
}
|
|
107
|
+
/** Finalize one authenticated ordinary pending launch without claiming or dispatching its successor. */
|
|
108
|
+
export declare function finalizeOrdinaryPendingLaunch(input: OrdinaryPendingFinalizeOptions): Promise<OrdinaryPendingFinalizeResult>;
|
|
60
109
|
/** Authenticate an ordinary dead-owner recovery under its target lock without mutating native state. */
|
|
61
110
|
export declare function previewOrdinaryPendingLaunch(input: PendingLegacyResumeOptions): Promise<OrdinaryPendingPreview>;
|
|
62
111
|
/** Finalize unknown A34 evidence, then claim one fresh checkpointed A35. */
|
|
@@ -67,3 +116,4 @@ export declare function resumePendingLegacyLaunch(input: PendingLegacyResumeOpti
|
|
|
67
116
|
preview?: false;
|
|
68
117
|
}): Promise<PendingLegacyResumeResult>;
|
|
69
118
|
export declare function resumePendingLegacyLaunch(input: PendingLegacyResumeOptions): Promise<PendingLegacyResumeResult | OrdinaryPendingPreview>;
|
|
119
|
+
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { z } from 'zod';
|
|
4
6
|
import { inspectPendingLegacyRetry } from './legacy-launch-health.js';
|
|
5
7
|
import { completionSchema, launchSchema } from './launch-record.js';
|
|
6
|
-
import { convergeRunStatePath, loadConvergeRunState, writeState } from './run-state.js';
|
|
8
|
+
import { convergeRunStatePath, loadConvergeRunState, preflightConvergeRunStateWrite, writeState } from './run-state.js';
|
|
7
9
|
import { claimConvergeAttempt, convergeAttemptStatePath, previewConvergeAttemptState } from './attempt-budget.js';
|
|
8
10
|
import { withRecoveryTarget } from './target-ownership.js';
|
|
9
11
|
import { bindOriginalCouncil } from '../dispatch/original-execution.js';
|
|
@@ -12,10 +14,46 @@ import { uuidv7 } from '../report/uuid.js';
|
|
|
12
14
|
import { captureSupplementalAsync } from '../report/supplemental-async.js';
|
|
13
15
|
import { stableStringify } from '../report/run-header.js';
|
|
14
16
|
import { readStable } from '../telemetry/recovery/files.js';
|
|
17
|
+
import { serializeRecoveryDocument } from '../evidence/original-run/journal.js';
|
|
15
18
|
import { prepareLockRoot } from '../evidence/original-run/lock-path.js';
|
|
16
19
|
import { retainStaleFile } from './stale-report-storage.js';
|
|
17
20
|
import { createPendingRecoverySource, createOrdinaryMigrationSource, pendingRecoverySourceSchema } from './pending-recovery-source.js';
|
|
18
21
|
import { validateOrdinaryPendingPackage } from './ordinary-pending-package.js';
|
|
22
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
23
|
+
const ordinaryPendingFinalizationReceiptBodySchema = z.object({
|
|
24
|
+
version: z.literal(1),
|
|
25
|
+
operation: z.literal('ordinary-pending-finalize-only'),
|
|
26
|
+
target: z.string().min(1),
|
|
27
|
+
headSha: z.string().regex(/^[a-f0-9]{40}$/),
|
|
28
|
+
baseSha: z.string().regex(/^[a-f0-9]{40}$/),
|
|
29
|
+
inputSha256: digestSchema,
|
|
30
|
+
sourceDigest: digestSchema,
|
|
31
|
+
migrationPackageSha256: digestSchema,
|
|
32
|
+
finalizedAttempt: z.number().int().positive().safe(),
|
|
33
|
+
round: z.number().int().positive().safe(),
|
|
34
|
+
originalPid: z.number().int().positive().safe(),
|
|
35
|
+
attemptsUsed: z.number().int().positive().safe(),
|
|
36
|
+
cap: z.number().int().positive().safe(),
|
|
37
|
+
nextFreeAttempt: z.number().int().positive().safe(),
|
|
38
|
+
blockingOutcome: z.literal('unknown'),
|
|
39
|
+
retainedAsyncSha256: z.array(digestSchema).min(1),
|
|
40
|
+
sourceNativeStateSha256: digestSchema,
|
|
41
|
+
sourceAttemptStateSha256: digestSchema,
|
|
42
|
+
finalizedNativeStateSha256: digestSchema,
|
|
43
|
+
}).strict();
|
|
44
|
+
const ordinaryPendingFinalizationReceiptSchema = ordinaryPendingFinalizationReceiptBodySchema.extend({
|
|
45
|
+
receiptDigest: digestSchema,
|
|
46
|
+
}).strict().superRefine((receipt, context) => {
|
|
47
|
+
const { receiptDigest, ...body } = receipt;
|
|
48
|
+
if (receiptDigest !== createHash('sha256').update(stableStringify(body)).digest('hex')) {
|
|
49
|
+
context.addIssue({ code: 'custom', message: 'Pending finalization receipt digest is invalid' });
|
|
50
|
+
}
|
|
51
|
+
if (receipt.nextFreeAttempt !== receipt.finalizedAttempt + 1 ||
|
|
52
|
+
receipt.attemptsUsed !== receipt.finalizedAttempt || receipt.attemptsUsed > receipt.cap ||
|
|
53
|
+
new Set(receipt.retainedAsyncSha256).size !== receipt.retainedAsyncSha256.length) {
|
|
54
|
+
context.addIssue({ code: 'custom', message: 'Pending finalization accounting is inconsistent' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
19
57
|
function fail(code) { throw new Error(`pending_legacy_resume_${code}`); }
|
|
20
58
|
function defaultOwnerAlive(pid) {
|
|
21
59
|
try {
|
|
@@ -89,7 +127,22 @@ function validateOptions(input) {
|
|
|
89
127
|
!Number.isSafeInteger(input.maxAttempts) || input.maxAttempts < 1 ||
|
|
90
128
|
!Number.isSafeInteger(input.maxPhysicalCalls) || input.maxPhysicalCalls < 1 ||
|
|
91
129
|
!Number.isSafeInteger(input.maxAttemptsPerCell) || input.maxAttemptsPerCell < 1 ||
|
|
92
|
-
!Number.isSafeInteger(input.maxDurationMs) || input.maxDurationMs < 1 || input.maxDurationMs > 2_147_483_647
|
|
130
|
+
!Number.isSafeInteger(input.maxDurationMs) || input.maxDurationMs < 1 || input.maxDurationMs > 2_147_483_647 ||
|
|
131
|
+
(input.previewMode !== undefined && !['combined', 'finalize-only'].includes(input.previewMode))) {
|
|
132
|
+
fail('invalid_input');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function validateFinalizeOptions(input) {
|
|
136
|
+
const hashes = input?.retainedAsyncSha256;
|
|
137
|
+
if (!input || typeof input.gitCommonDir !== 'string' || typeof input.target !== 'string' || !input.target.trim() ||
|
|
138
|
+
!/^[a-f0-9]{40}$/.test(input.headSha ?? '') || !/^[a-f0-9]{40}$/.test(input.baseSha ?? '') ||
|
|
139
|
+
!/^[a-f0-9]{64}$/.test(input.pendingInputSha256 ?? '') ||
|
|
140
|
+
!/^[a-f0-9]{64}$/.test(input.nativeStateSha256 ?? '') ||
|
|
141
|
+
!/^[a-f0-9]{64}$/.test(input.attemptStateSha256 ?? '') ||
|
|
142
|
+
!Array.isArray(hashes) || hashes.length === 0 || hashes.some(value => !/^[a-f0-9]{64}$/.test(value)) ||
|
|
143
|
+
new Set(hashes).size !== hashes.length || typeof input.loadRetainedAsync !== 'function' ||
|
|
144
|
+
(input.writeFinalizedState !== undefined && typeof input.writeFinalizedState !== 'function') ||
|
|
145
|
+
!input.migrationPackage || !Number.isSafeInteger(input.maxAttempts) || input.maxAttempts < 1) {
|
|
93
146
|
fail('invalid_input');
|
|
94
147
|
}
|
|
95
148
|
}
|
|
@@ -104,6 +157,277 @@ async function archiveAsync(common, source, artifacts, packet) {
|
|
|
104
157
|
blockingOutcome: 'unknown', artifacts: artifacts.map(item => ({ sha256: item.sha256 })).sort((a, b) => a.sha256.localeCompare(b.sha256)) }));
|
|
105
158
|
await retainStaleFile(join(directory, 'manifest.json'), manifest);
|
|
106
159
|
}
|
|
160
|
+
function finalizationReceiptPath(common, migrationPackageSha256) {
|
|
161
|
+
return join(common, 'rcl-converge-pending-finalizations', migrationPackageSha256, 'receipt.json');
|
|
162
|
+
}
|
|
163
|
+
function finalizationSnapshotPath(common, migrationPackageSha256, name) {
|
|
164
|
+
return join(common, 'rcl-converge-pending-finalizations', migrationPackageSha256, name);
|
|
165
|
+
}
|
|
166
|
+
async function readFinalizationReceipt(common, migrationPackageSha256) {
|
|
167
|
+
try {
|
|
168
|
+
const stored = await readStable(finalizationReceiptPath(common, migrationPackageSha256));
|
|
169
|
+
return ordinaryPendingFinalizationReceiptSchema.parse(JSON.parse(stored.text));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code === 'ENOENT')
|
|
173
|
+
return undefined;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function retainFinalizationReceipt(common, receipt) {
|
|
178
|
+
const directory = await prepareLockRoot(join(common, 'rcl-converge-pending-finalizations', receipt.migrationPackageSha256));
|
|
179
|
+
await retainStaleFile(join(directory, 'receipt.json'), Buffer.from(stableStringify(receipt)));
|
|
180
|
+
}
|
|
181
|
+
async function retainFinalizationSnapshots(common, receipt, sourceAttemptState, finalizedNativeState) {
|
|
182
|
+
if (createHash('sha256').update(sourceAttemptState).digest('hex') !== receipt.sourceAttemptStateSha256 ||
|
|
183
|
+
createHash('sha256').update(finalizedNativeState).digest('hex') !== receipt.finalizedNativeStateSha256) {
|
|
184
|
+
fail('finalization_snapshot_mismatch');
|
|
185
|
+
}
|
|
186
|
+
const directory = await prepareLockRoot(join(common, 'rcl-converge-pending-finalizations', receipt.migrationPackageSha256));
|
|
187
|
+
await retainStaleFile(join(directory, 'source-attempt-state.json'), sourceAttemptState);
|
|
188
|
+
await retainStaleFile(join(directory, 'finalized-native-state.json'), finalizedNativeState);
|
|
189
|
+
}
|
|
190
|
+
async function readFinalizationSnapshot(common, receipt, name, digest) {
|
|
191
|
+
const snapshot = await readStable(finalizationSnapshotPath(common, receipt.migrationPackageSha256, name));
|
|
192
|
+
if (snapshot.sha256 !== digest)
|
|
193
|
+
fail('finalization_snapshot_mismatch');
|
|
194
|
+
return snapshot.raw;
|
|
195
|
+
}
|
|
196
|
+
function isPrefix(before, after) {
|
|
197
|
+
return before.length <= after.length && before.every((value, index) => isDeepStrictEqual(value, after[index]));
|
|
198
|
+
}
|
|
199
|
+
async function finalizationSnapshotsPresent(common, receipt) {
|
|
200
|
+
try {
|
|
201
|
+
await Promise.all([
|
|
202
|
+
readFinalizationSnapshot(common, receipt, 'source-attempt-state.json', receipt.sourceAttemptStateSha256),
|
|
203
|
+
readFinalizationSnapshot(common, receipt, 'finalized-native-state.json', receipt.finalizedNativeStateSha256),
|
|
204
|
+
]);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code === 'ENOENT')
|
|
209
|
+
return false;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function assertFinalizedStateMatchesReceipt(common, receipt) {
|
|
214
|
+
const [native, attempts] = await Promise.all([
|
|
215
|
+
readStable(convergeRunStatePath(common, receipt.target)),
|
|
216
|
+
readStable(convergeAttemptStatePath(common, receipt.target)),
|
|
217
|
+
]);
|
|
218
|
+
if (native.sha256 === receipt.finalizedNativeStateSha256 &&
|
|
219
|
+
attempts.sha256 === receipt.sourceAttemptStateSha256) {
|
|
220
|
+
return { native: native.raw, attempts: attempts.raw, exact: true };
|
|
221
|
+
}
|
|
222
|
+
let originalAttemptsBytes, finalizedNativeBytes;
|
|
223
|
+
try {
|
|
224
|
+
[originalAttemptsBytes, finalizedNativeBytes] = await Promise.all([
|
|
225
|
+
readFinalizationSnapshot(common, receipt, 'source-attempt-state.json', receipt.sourceAttemptStateSha256),
|
|
226
|
+
readFinalizationSnapshot(common, receipt, 'finalized-native-state.json', receipt.finalizedNativeStateSha256),
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error.code === 'ENOENT')
|
|
231
|
+
fail('finalization_receipt_state_mismatch');
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
const [currentNative, currentAttempts] = await Promise.all([
|
|
235
|
+
loadConvergeRunState(common, receipt.target),
|
|
236
|
+
previewConvergeAttemptState(common, receipt.target),
|
|
237
|
+
]);
|
|
238
|
+
if (!currentNative || !currentAttempts)
|
|
239
|
+
fail('finalization_receipt_state_mismatch');
|
|
240
|
+
const originalAttempts = JSON.parse(originalAttemptsBytes.toString('utf8'));
|
|
241
|
+
const finalizedNative = JSON.parse(finalizedNativeBytes.toString('utf8'));
|
|
242
|
+
const finalizedLaunch = launchSchema.safeParse(finalizedNative.lastLaunch);
|
|
243
|
+
const currentLaunch = launchSchema.safeParse(currentNative.lastLaunch);
|
|
244
|
+
const priorStale = finalizedNative.staleReportAudit ?? [], currentStale = currentNative.staleReportAudit ?? [];
|
|
245
|
+
const priorGaps = finalizedNative.roundGapAudit?.entries ?? [], currentGaps = currentNative.roundGapAudit?.entries ?? [];
|
|
246
|
+
if (originalAttempts.target !== receipt.target || currentAttempts.target !== receipt.target ||
|
|
247
|
+
originalAttempts.attemptsUsed !== receipt.attemptsUsed || originalAttempts.cap !== receipt.cap ||
|
|
248
|
+
originalAttempts.migratedAttempts !== currentAttempts.migratedAttempts ||
|
|
249
|
+
currentAttempts.attemptsUsed <= receipt.attemptsUsed ||
|
|
250
|
+
currentAttempts.cap < currentAttempts.attemptsUsed || currentAttempts.cap > receipt.cap ||
|
|
251
|
+
!isDeepStrictEqual(currentAttempts.cycle, originalAttempts.cycle) ||
|
|
252
|
+
!isPrefix(originalAttempts.attempts, currentAttempts.attempts) ||
|
|
253
|
+
currentAttempts.attempts.at(originalAttempts.attempts.length)?.attempt !== receipt.nextFreeAttempt ||
|
|
254
|
+
finalizedNative.target !== receipt.target || currentNative.target !== receipt.target ||
|
|
255
|
+
!finalizedLaunch.success || finalizedLaunch.data.status !== 'failed' ||
|
|
256
|
+
finalizedLaunch.data.attempt !== receipt.finalizedAttempt ||
|
|
257
|
+
finalizedLaunch.data.round !== receipt.round ||
|
|
258
|
+
finalizedLaunch.data.pendingRecovery?.sourceDigest !== receipt.sourceDigest ||
|
|
259
|
+
!isDeepStrictEqual(currentNative.cycle, finalizedNative.cycle) ||
|
|
260
|
+
currentNative.roundCap < finalizedNative.roundCap ||
|
|
261
|
+
!isPrefix(finalizedNative.rounds, currentNative.rounds) ||
|
|
262
|
+
!isPrefix(priorStale, currentStale) || !isPrefix(priorGaps, currentGaps) ||
|
|
263
|
+
Object.entries(finalizedNative.findings)
|
|
264
|
+
.some(([key, value]) => !isDeepStrictEqual(value, currentNative.findings[key])) ||
|
|
265
|
+
!currentLaunch.success || currentLaunch.data.attempt <= receipt.finalizedAttempt ||
|
|
266
|
+
currentLaunch.data.attempt !== currentAttempts.attemptsUsed) {
|
|
267
|
+
fail('finalization_receipt_state_mismatch');
|
|
268
|
+
}
|
|
269
|
+
return { native: native.raw, attempts: attempts.raw, exact: false };
|
|
270
|
+
}
|
|
271
|
+
function createFinalizationReceipt(input) {
|
|
272
|
+
const body = ordinaryPendingFinalizationReceiptBodySchema.parse(input);
|
|
273
|
+
return ordinaryPendingFinalizationReceiptSchema.parse({
|
|
274
|
+
...body,
|
|
275
|
+
receiptDigest: createHash('sha256').update(stableStringify(body)).digest('hex'),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function assertReceiptMatchesInput(receipt, input, migrationPackageSha256) {
|
|
279
|
+
if (receipt.target !== input.target || receipt.headSha !== input.headSha || receipt.baseSha !== input.baseSha ||
|
|
280
|
+
receipt.inputSha256 !== input.pendingInputSha256 || receipt.migrationPackageSha256 !== migrationPackageSha256 ||
|
|
281
|
+
receipt.sourceNativeStateSha256 !== input.nativeStateSha256 ||
|
|
282
|
+
receipt.sourceAttemptStateSha256 !== input.attemptStateSha256 ||
|
|
283
|
+
receipt.cap !== input.maxAttempts ||
|
|
284
|
+
receipt.retainedAsyncSha256.join(',') !== [...input.retainedAsyncSha256].sort().join(',')) {
|
|
285
|
+
fail('finalization_receipt_mismatch');
|
|
286
|
+
}
|
|
287
|
+
return validateOrdinaryPendingPackage(input.migrationPackage, {
|
|
288
|
+
target: receipt.target, headSha: receipt.headSha, inputSha256: receipt.inputSha256,
|
|
289
|
+
baseSha: receipt.baseSha, attempt: receipt.finalizedAttempt, round: receipt.round,
|
|
290
|
+
pid: receipt.originalPid, retainedAsyncSha256: receipt.retainedAsyncSha256,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async function readArchivedAsync(common, sourceDigest, retainedAsyncSha256) {
|
|
294
|
+
const directory = join(common, 'rcl-converge-pending-recovery', sourceDigest);
|
|
295
|
+
const artifacts = await Promise.all(retainedAsyncSha256.map(async (digest) => {
|
|
296
|
+
const file = await readStable(join(directory, digest));
|
|
297
|
+
if (file.sha256 !== digest)
|
|
298
|
+
fail('async_binding_mismatch');
|
|
299
|
+
return { path: join(directory, digest), sha256: digest, bytesBase64: file.raw.toString('base64') };
|
|
300
|
+
}));
|
|
301
|
+
const expectedManifest = stableStringify({ version: 1, sourceDigest,
|
|
302
|
+
blockingOutcome: 'unknown', artifacts: [...retainedAsyncSha256].sort().map(sha256 => ({ sha256 })) });
|
|
303
|
+
if ((await readStable(join(directory, 'manifest.json'))).text !== expectedManifest) {
|
|
304
|
+
fail('async_binding_mismatch');
|
|
305
|
+
}
|
|
306
|
+
return artifacts;
|
|
307
|
+
}
|
|
308
|
+
async function loadOriginalOrArchivedAsync(options, source) {
|
|
309
|
+
try {
|
|
310
|
+
return await readArchivedAsync(options.gitCommonDir, source.digest, source.retainedAsyncSha256);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (error.code !== 'ENOENT')
|
|
314
|
+
throw error;
|
|
315
|
+
return options.loadRetainedAsync();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** Finalize one authenticated ordinary pending launch without claiming or dispatching its successor. */
|
|
319
|
+
export async function finalizeOrdinaryPendingLaunch(input) {
|
|
320
|
+
validateFinalizeOptions(input);
|
|
321
|
+
const options = { ...input, target: input.target.trim(),
|
|
322
|
+
retainedAsyncSha256: [...input.retainedAsyncSha256].sort(),
|
|
323
|
+
gitCommonDir: await realpath(resolve(input.gitCommonDir)) };
|
|
324
|
+
const migrationPackageSha256 = createHash('sha256')
|
|
325
|
+
.update(stableStringify(options.migrationPackage)).digest('hex');
|
|
326
|
+
return withRecoveryTarget(options.gitCommonDir, options.target, async (ownership) => {
|
|
327
|
+
const existingReceipt = await readFinalizationReceipt(options.gitCommonDir, migrationPackageSha256);
|
|
328
|
+
if (existingReceipt) {
|
|
329
|
+
const packet = assertReceiptMatchesInput(existingReceipt, options, migrationPackageSha256);
|
|
330
|
+
const state = await assertFinalizedStateMatchesReceipt(options.gitCommonDir, existingReceipt);
|
|
331
|
+
validateAsyncArtifacts({ retainedAsyncSha256: existingReceipt.retainedAsyncSha256 }, await readArchivedAsync(options.gitCommonDir, existingReceipt.sourceDigest, existingReceipt.retainedAsyncSha256), packet);
|
|
332
|
+
const snapshotsPresent = await finalizationSnapshotsPresent(options.gitCommonDir, existingReceipt);
|
|
333
|
+
if (!snapshotsPresent) {
|
|
334
|
+
if (!state.exact)
|
|
335
|
+
fail('finalization_receipt_state_mismatch');
|
|
336
|
+
await retainFinalizationSnapshots(options.gitCommonDir, existingReceipt, state.attempts, state.native);
|
|
337
|
+
}
|
|
338
|
+
return { receipt: existingReceipt, reusedReceipt: true,
|
|
339
|
+
snapshotBindingUpgraded: !snapshotsPresent };
|
|
340
|
+
}
|
|
341
|
+
const [state, attempts] = await Promise.all([
|
|
342
|
+
loadConvergeRunState(options.gitCommonDir, options.target),
|
|
343
|
+
previewConvergeAttemptState(options.gitCommonDir, options.target),
|
|
344
|
+
]);
|
|
345
|
+
if (!state || !attempts || !state.lastLaunch)
|
|
346
|
+
fail('missing_launch');
|
|
347
|
+
if (attempts.cap !== options.maxAttempts)
|
|
348
|
+
fail('attempt_cap_mismatch');
|
|
349
|
+
const current = launchSchema.parse(state.lastLaunch);
|
|
350
|
+
if (current.attempt !== attempts.attemptsUsed || current.headSha !== options.headSha ||
|
|
351
|
+
current.inputSha256 !== options.pendingInputSha256 || !['pending', 'failed'].includes(current.status)) {
|
|
352
|
+
fail('launch_mismatch');
|
|
353
|
+
}
|
|
354
|
+
const packet = validateOrdinaryPendingPackage(options.migrationPackage, {
|
|
355
|
+
target: options.target, headSha: current.headSha, inputSha256: current.inputSha256,
|
|
356
|
+
baseSha: options.baseSha, attempt: current.attempt, round: current.round, pid: current.pid,
|
|
357
|
+
retainedAsyncSha256: options.retainedAsyncSha256,
|
|
358
|
+
});
|
|
359
|
+
let source;
|
|
360
|
+
let sourceAttemptStateBytes;
|
|
361
|
+
if (current.status === 'pending') {
|
|
362
|
+
if ((options.ownerAlive ?? defaultOwnerAlive)(current.pid))
|
|
363
|
+
fail('owner_alive');
|
|
364
|
+
const [nativeBytes, attemptBytes] = await Promise.all([
|
|
365
|
+
readStable(convergeRunStatePath(options.gitCommonDir, options.target)),
|
|
366
|
+
readStable(convergeAttemptStatePath(options.gitCommonDir, options.target)),
|
|
367
|
+
]);
|
|
368
|
+
sourceAttemptStateBytes = attemptBytes.raw;
|
|
369
|
+
source = createOrdinaryMigrationSource({ version: 1, target: options.target,
|
|
370
|
+
headSha: current.headSha, inputSha256: current.inputSha256, pendingAttempt: current.attempt,
|
|
371
|
+
round: current.round, originalPid: current.pid, startedAt: current.startedAt,
|
|
372
|
+
blockingOutcome: 'unknown', reason: 'coordinator_exited_without_durable_blocking_receipts',
|
|
373
|
+
nativeStateSha256: nativeBytes.sha256, attemptStateSha256: attemptBytes.sha256,
|
|
374
|
+
retainedAsyncSha256: options.retainedAsyncSha256, migrationPackageSha256 });
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
source = sourceFromFinalized(options.target, current, attempts, packet);
|
|
378
|
+
}
|
|
379
|
+
if (source.nativeStateSha256 !== options.nativeStateSha256 ||
|
|
380
|
+
source.attemptStateSha256 !== options.attemptStateSha256)
|
|
381
|
+
fail('source_digest_mismatch');
|
|
382
|
+
// Once the failed/unknown native state exists, archive publication already
|
|
383
|
+
// completed. Resume from those immutable bytes so a lost acknowledgment
|
|
384
|
+
// does not depend on the original async store still being available.
|
|
385
|
+
const artifacts = current.status === 'pending'
|
|
386
|
+
? await loadOriginalOrArchivedAsync(options, source)
|
|
387
|
+
: await readArchivedAsync(options.gitCommonDir, source.digest, source.retainedAsyncSha256);
|
|
388
|
+
validateAsyncArtifacts(source, artifacts, packet);
|
|
389
|
+
let finalizedNativeStateSha256;
|
|
390
|
+
if (current.status === 'pending') {
|
|
391
|
+
state.lastLaunch = { ...current, status: 'failed', pendingRecovery: descriptor(source) };
|
|
392
|
+
state.updatedAt = new Date().toISOString();
|
|
393
|
+
finalizedNativeStateSha256 = createHash('sha256')
|
|
394
|
+
.update(serializeRecoveryDocument(state)).digest('hex');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
finalizedNativeStateSha256 = (await readStable(convergeRunStatePath(options.gitCommonDir, options.target))).sha256;
|
|
398
|
+
}
|
|
399
|
+
// Construct and validate the exact receipt before the first write. The
|
|
400
|
+
// native serializer is shared with writeState, so its post-state digest is
|
|
401
|
+
// known without publishing a partially validated operation.
|
|
402
|
+
const receipt = createFinalizationReceipt({ version: 1, operation: 'ordinary-pending-finalize-only',
|
|
403
|
+
target: options.target, headSha: current.headSha, baseSha: options.baseSha,
|
|
404
|
+
inputSha256: current.inputSha256, sourceDigest: source.digest, migrationPackageSha256,
|
|
405
|
+
finalizedAttempt: current.attempt, round: current.round, originalPid: current.pid,
|
|
406
|
+
attemptsUsed: attempts.attemptsUsed, cap: attempts.cap, nextFreeAttempt: current.attempt + 1,
|
|
407
|
+
blockingOutcome: 'unknown', retainedAsyncSha256: options.retainedAsyncSha256,
|
|
408
|
+
sourceNativeStateSha256: source.nativeStateSha256,
|
|
409
|
+
sourceAttemptStateSha256: source.attemptStateSha256,
|
|
410
|
+
finalizedNativeStateSha256 });
|
|
411
|
+
if (current.status === 'pending') {
|
|
412
|
+
await preflightConvergeRunStateWrite(options.gitCommonDir, state, ownership);
|
|
413
|
+
}
|
|
414
|
+
// Every fallible authentication and accounting check above precedes the
|
|
415
|
+
// first durable write. Archive publication is idempotent if this process
|
|
416
|
+
// stops before the native failed/unknown state or receipt is published.
|
|
417
|
+
await archiveAsync(options.gitCommonDir, source, artifacts, packet);
|
|
418
|
+
if (current.status === 'pending') {
|
|
419
|
+
await (options.writeFinalizedState ?? writeState)(options.gitCommonDir, state, ownership);
|
|
420
|
+
}
|
|
421
|
+
const finalizedNative = await readStable(convergeRunStatePath(options.gitCommonDir, options.target));
|
|
422
|
+
if (finalizedNative.sha256 !== finalizedNativeStateSha256)
|
|
423
|
+
fail('finalization_write_mismatch');
|
|
424
|
+
const finalizedAttempts = await readStable(convergeAttemptStatePath(options.gitCommonDir, options.target));
|
|
425
|
+
sourceAttemptStateBytes ??= finalizedAttempts.raw;
|
|
426
|
+
await retainFinalizationSnapshots(options.gitCommonDir, receipt, sourceAttemptStateBytes, finalizedNative.raw);
|
|
427
|
+
await retainFinalizationReceipt(options.gitCommonDir, receipt);
|
|
428
|
+
return { receipt, reusedReceipt: false, snapshotBindingUpgraded: false };
|
|
429
|
+
});
|
|
430
|
+
}
|
|
107
431
|
function validateAsyncArtifacts(source, artifacts, packet) {
|
|
108
432
|
const expected = [...source.retainedAsyncSha256].sort();
|
|
109
433
|
const actual = artifacts.map(item => item.sha256).sort();
|
|
@@ -148,8 +472,13 @@ export async function previewOrdinaryPendingLaunch(input) {
|
|
|
148
472
|
if (!record || current.status !== 'pending' || current.attempt !== attempts.attemptsUsed ||
|
|
149
473
|
current.headSha !== options.headSha || current.inputSha256 !== options.pendingInputSha256)
|
|
150
474
|
fail('launch_mismatch');
|
|
151
|
-
if (
|
|
475
|
+
if (options.previewMode === 'finalize-only'
|
|
476
|
+
? attempts.cap !== options.maxAttempts
|
|
477
|
+
: options.maxAttempts > current.attempt + 1)
|
|
478
|
+
fail('attempt_cap_mismatch');
|
|
479
|
+
if (options.previewMode !== 'finalize-only' && attempts.attemptsUsed >= attempts.cap) {
|
|
152
480
|
fail('attempt_cap_exhausted');
|
|
481
|
+
}
|
|
153
482
|
if ((options.ownerAlive ?? defaultOwnerAlive)(current.pid))
|
|
154
483
|
fail('owner_alive');
|
|
155
484
|
const packet = validateOrdinaryPendingPackage(options.migrationPackage, {
|
|
@@ -336,6 +665,11 @@ export async function resumePendingLegacyLaunch(input) {
|
|
|
336
665
|
baseSha: options.baseSha, attempt: originalAttempt, round: original.round, pid: originalPid,
|
|
337
666
|
retainedAsyncSha256: options.retainedAsyncSha256,
|
|
338
667
|
});
|
|
668
|
+
// Combined finalize-and-successor recovery is explicit one-attempt
|
|
669
|
+
// authority. Refuse a broader cap before archiving evidence or changing
|
|
670
|
+
// the pending launch to failed/unknown.
|
|
671
|
+
if (options.maxAttempts > originalAttempt + 1)
|
|
672
|
+
fail('attempt_cap_mismatch');
|
|
339
673
|
if (persistedSource) {
|
|
340
674
|
return resumeFresh(options, ownership, state, attempts, current);
|
|
341
675
|
}
|