@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-flow",
3
- "version": "0.3.79",
3
+ "version": "0.3.81",
4
4
  "description": "Filesystem-first Desk registry for projects, seats, tasks, threads, and handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 === "atdd-flow/pi-runtime-launch-receipt/v1" && receipt.seat === seat
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
- /** True when the seat records a manual binding_mode or its launch receipt is a manual rebinding receipt, rather than a launcher receipt. */
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
- if (record.runtime?.binding_mode !== undefined) return true;
272
- try { return yaml.parse<PriorReceipt>(await readFile(path, "utf8")).schema === MANUAL_REBINDING_SCHEMA; } catch { return false; }
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
- /** An existing stale active request must retain exactly the retired placement of the recorded Pi. */
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 { source, target } = yaml.parse<HandoverReplacementRequest>(bytes);
290
- return source.pi_session === receipt.pi_session && source.pi_session_path === receipt.pi_session_path
291
- && [source, target].every((placement) => placement.herdr_session === receipt.herdr_session && placement.pane === receipt.pane);
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
- try {
394
- const receipt = yaml.parse<PriorReceipt>(await readFile(path, "utf8"));
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. */