@afokapu/atdd-flow 0.3.79 → 0.3.81
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/package.json +1 -1
- package/src/stale-main-recovery.ts +27 -12
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
13
13
|
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
15
|
import { type Runtime, type Seat, runOutput, yaml } from "./core";
|
|
15
16
|
import type { HandoverReplacementRequest, LiveSourceProof } from "./handover";
|
|
16
17
|
|
|
@@ -30,6 +31,7 @@ export type StaleMainRecovery = {
|
|
|
30
31
|
relocation?: Relocation;
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
const LAUNCH_RECEIPT_SCHEMA = "atdd-flow/pi-runtime-launch-receipt/v1";
|
|
33
35
|
export const MANUAL_REBINDING_SCHEMA = "atdd-flow/manual-pi-runtime-rebinding-receipt/v1";
|
|
34
36
|
export const MANUAL_REBINDING_MODE = "operator-authorized-manual-in-place-adoption";
|
|
35
37
|
|
|
@@ -47,6 +49,7 @@ type Recorded = { session: string; sessionPath: string; locator: { session: stri
|
|
|
47
49
|
type Source = HandoverReplacementRequest["source"];
|
|
48
50
|
type Target = HandoverReplacementRequest["target"];
|
|
49
51
|
type PriorReceipt = {
|
|
52
|
+
handover_replacement?: { request?: unknown };
|
|
50
53
|
schema?: string; seat?: string; pi_session?: string; pi_session_path?: string; herdr_session?: string; pane?: string;
|
|
51
54
|
mode?: string; authorized_by?: string; authorization?: string;
|
|
52
55
|
target?: { cwd?: string; pi_session?: string; pi_session_path?: string; herdr_session?: string; pane?: string };
|
|
@@ -84,7 +87,7 @@ export function receiptProvesCanonical(receipt: PriorReceipt, seat: string, cano
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
function launchReceiptProvesCanonical(receipt: PriorReceipt, seat: string, canonical: StaleMainRecovery["canonical_source"]) {
|
|
87
|
-
return receipt.schema ===
|
|
90
|
+
return receipt.schema === LAUNCH_RECEIPT_SCHEMA && receipt.seat === seat
|
|
88
91
|
&& receipt.pi_session === canonical.pi_session && receipt.pi_session_path === canonical.pi_session_path
|
|
89
92
|
&& receipt.herdr_session === canonical.herdr_session && receipt.pane === canonical.pane;
|
|
90
93
|
}
|
|
@@ -264,12 +267,18 @@ export async function buildStaleMainRecovery(root: string, seat: string, intent:
|
|
|
264
267
|
|
|
265
268
|
const refuse = (reason: string) => new Error(`Stale main relocation receipt recovery refused: ${reason}.`);
|
|
266
269
|
|
|
267
|
-
/**
|
|
270
|
+
/** The recorded receipt, or undefined when it is unreadable or malformed. */
|
|
271
|
+
async function readReceipt(path: string) {
|
|
272
|
+
try { return yaml.parse<PriorReceipt>(await readFile(path, "utf8")); } catch { return undefined; }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A launcher receipt is never manual, even if the seat retains a binding_mode; any other receipt is manual when it has the manual schema or the seat records a binding_mode. */
|
|
268
276
|
export async function isManualRebindingReceipt(record: Seat) {
|
|
269
277
|
const path = record.runtime?.launch_receipt;
|
|
270
278
|
if (!path) return false;
|
|
271
|
-
|
|
272
|
-
|
|
279
|
+
const schema = (await readReceipt(path))?.schema;
|
|
280
|
+
if (schema === LAUNCH_RECEIPT_SCHEMA) return false;
|
|
281
|
+
return record.runtime?.binding_mode !== undefined || schema === MANUAL_REBINDING_SCHEMA;
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
/** A backup file must hash to its recorded digest (and size, when recorded) and be the exact byte prefix of the adopted session transcript. */
|
|
@@ -281,14 +290,22 @@ export async function backupIsTranscriptPrefix(backup: { path?: string; sha256?:
|
|
|
281
290
|
} catch { return false; }
|
|
282
291
|
}
|
|
283
292
|
|
|
284
|
-
|
|
293
|
+
const atReceiptPlacement = (placement: { herdr_session: string; pane: string }, receipt: PriorReceipt) =>
|
|
294
|
+
placement.herdr_session === receipt.herdr_session && placement.pane === receipt.pane;
|
|
295
|
+
|
|
296
|
+
/** A launcher receipt's own launch request: the active request is exactly the request embedded in the receipt and targets the receipt placement. */
|
|
297
|
+
function isEmbeddedLaunchRequest(request: HandoverReplacementRequest, receipt: PriorReceipt) {
|
|
298
|
+
return receipt.schema === LAUNCH_RECEIPT_SCHEMA && isDeepStrictEqual(request, receipt.handover_replacement?.request) && atReceiptPlacement(request.target, receipt);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** An existing stale active request must be for the recorded Pi and retain the retired placement: both ends at it, or the launch request embedded in the launcher receipt. */
|
|
285
302
|
async function priorRequestIsRetiredLineage(file: string, receipt: PriorReceipt) {
|
|
286
303
|
let bytes: string;
|
|
287
304
|
try { bytes = await readFile(file, "utf8"); } catch { return true; }
|
|
288
305
|
try {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
306
|
+
const request = yaml.parse<HandoverReplacementRequest>(bytes), { source, target } = request;
|
|
307
|
+
const samePi = source.pi_session === receipt.pi_session && source.pi_session_path === receipt.pi_session_path;
|
|
308
|
+
return samePi && ((atReceiptPlacement(source, receipt) && atReceiptPlacement(target, receipt)) || isEmbeddedLaunchRequest(request, receipt));
|
|
292
309
|
} catch { return false; }
|
|
293
310
|
}
|
|
294
311
|
|
|
@@ -390,10 +407,8 @@ export async function relocationFor(record: Seat, source: Source, target: Target
|
|
|
390
407
|
}
|
|
391
408
|
|
|
392
409
|
async function readLauncherReceipt(path: string) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return receipt.schema === "atdd-flow/pi-runtime-launch-receipt/v1" ? receipt : undefined;
|
|
396
|
-
} catch { return undefined; }
|
|
410
|
+
const receipt = await readReceipt(path);
|
|
411
|
+
return receipt?.schema === LAUNCH_RECEIPT_SCHEMA ? receipt : undefined;
|
|
397
412
|
}
|
|
398
413
|
|
|
399
414
|
/** The launcher receipt names this seat's exact recorded Pi at a pane other than the seat's current locator. */
|