poe-code 10.0.2 → 10.0.4

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.
@@ -0,0 +1,55 @@
1
+ export type SnapshotMigrationFileOptions = {
2
+ snapshotPath: string;
3
+ sourcePath: string;
4
+ targetSourcePath?: string;
5
+ planPath?: string;
6
+ outputPath?: string;
7
+ inspect?: boolean;
8
+ dryRun?: boolean;
9
+ cwd?: string;
10
+ };
11
+ export declare function migrateSnapshotFile(options: SnapshotMigrationFileOptions): Promise<{
12
+ inspection: {
13
+ checkpointDigest: string;
14
+ sourceHash: string;
15
+ executionSemantics: string;
16
+ calls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
17
+ asynchronous: boolean;
18
+ outcome?: {
19
+ status: "fulfilled" | "rejected";
20
+ data: import("./snapshot/replay-data.js").ReplayData;
21
+ };
22
+ })[];
23
+ unresolvedCalls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
24
+ asynchronous: boolean;
25
+ outcome?: {
26
+ status: "fulfilled" | "rejected";
27
+ data: import("./snapshot/replay-data.js").ReplayData;
28
+ };
29
+ })[];
30
+ };
31
+ outputPath?: undefined;
32
+ dryRun?: undefined;
33
+ } | {
34
+ inspection: {
35
+ checkpointDigest: string;
36
+ sourceHash: string;
37
+ executionSemantics: string;
38
+ calls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
39
+ asynchronous: boolean;
40
+ outcome?: {
41
+ status: "fulfilled" | "rejected";
42
+ data: import("./snapshot/replay-data.js").ReplayData;
43
+ };
44
+ })[];
45
+ unresolvedCalls: (Omit<import("./index.js").HostCallRecord, "outcome" | "asynchronous"> & {
46
+ asynchronous: boolean;
47
+ outcome?: {
48
+ status: "fulfilled" | "rejected";
49
+ data: import("./snapshot/replay-data.js").ReplayData;
50
+ };
51
+ })[];
52
+ };
53
+ outputPath: string | undefined;
54
+ dryRun: boolean;
55
+ }>;
@@ -0,0 +1,79 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, open, readFile, unlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { hasOwnErrorCode } from "./error-codes.js";
5
+ import { inspectSnapshotMigration, migrateSnapshot } from "./migrate.js";
6
+ import { serializeSafeJSSnapshot } from "./snapshot/dump-format.js";
7
+ export async function migrateSnapshotFile(options) {
8
+ const cwd = options.cwd ?? process.cwd();
9
+ const resolvePath = (value, label) => {
10
+ if (typeof value !== "string" || value.trim().length === 0)
11
+ throw new TypeError(`Migration requires ${label}.`);
12
+ return path.resolve(cwd, value);
13
+ };
14
+ const snapshotPath = resolvePath(options.snapshotPath, "a snapshot path");
15
+ const sourcePath = resolvePath(options.sourcePath, "--from <original.ajs>");
16
+ if (options.inspect &&
17
+ [options.targetSourcePath, options.planPath, options.outputPath].some((value) => value !== undefined))
18
+ throw new TypeError("Migration --inspect cannot be combined with --to, --plan, or --output.");
19
+ const outputPath = options.inspect
20
+ ? undefined
21
+ : resolvePath(options.outputPath, "--output <new-checkpoint.json>");
22
+ const targetSourcePath = options.inspect
23
+ ? undefined
24
+ : resolvePath(options.targetSourcePath, "--to <continuation.ajs>");
25
+ const planPath = options.inspect
26
+ ? undefined
27
+ : resolvePath(options.planPath, "--plan <migration.json>");
28
+ for (const filepath of [sourcePath, targetSourcePath]) {
29
+ if (filepath !== undefined && ![".ajs", ".safejs"].includes(path.extname(filepath)))
30
+ throw new TypeError("Migration source paths must name executable .ajs or .safejs files, not harness Markdown.");
31
+ }
32
+ const snapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
33
+ const source = await readFile(sourcePath, "utf8");
34
+ const inspection = inspectSnapshotMigration(snapshot, { source });
35
+ if (options.inspect)
36
+ return { inspection };
37
+ const plan = JSON.parse(await readFile(planPath, "utf8"));
38
+ if (plan === null ||
39
+ typeof plan !== "object" ||
40
+ !Object.hasOwn(plan, "state") ||
41
+ !Object.hasOwn(plan, "reconciliation"))
42
+ throw new TypeError("Migration plan requires explicit state and reconciliation fields.");
43
+ const targetSource = await readFile(targetSourcePath, "utf8");
44
+ const migrated = migrateSnapshot(snapshot, {
45
+ source,
46
+ targetSource,
47
+ state: plan.state,
48
+ reconciliation: plan.reconciliation
49
+ });
50
+ const contents = serializeSafeJSSnapshot(migrated);
51
+ if (!options.dryRun) {
52
+ const temporaryPath = path.join(path.dirname(outputPath), `.safejs-migration-${randomUUID()}.tmp`);
53
+ let created = false;
54
+ try {
55
+ const handle = await open(temporaryPath, "wx", 0o600);
56
+ created = true;
57
+ try {
58
+ await handle.writeFile(contents, "utf8");
59
+ await handle.sync();
60
+ }
61
+ finally {
62
+ await handle.close();
63
+ }
64
+ try {
65
+ await link(temporaryPath, outputPath);
66
+ }
67
+ catch (error) {
68
+ if (hasOwnErrorCode(error, "EEXIST"))
69
+ throw Object.assign(new Error(`Migration output already exists: ${JSON.stringify(outputPath)}. Choose a new output path; existing files are never overwritten.`, { cause: error }), { code: "EEXIST" });
70
+ throw error;
71
+ }
72
+ }
73
+ finally {
74
+ if (created)
75
+ await unlink(temporaryPath);
76
+ }
77
+ }
78
+ return { inspection, outputPath, dryRun: options.dryRun === true };
79
+ }
@@ -1,6 +1,8 @@
1
+ import { type SnapshotMigration } from "./snapshot/migration.js";
1
2
  export type SafeJSSnapshot = {
2
3
  version?: number;
3
4
  sourceHash: string;
5
+ migration?: SnapshotMigration;
4
6
  clock?: {
5
7
  next: number;
6
8
  };
@@ -3,6 +3,7 @@ import { replaceErrorStack } from "./error/shape.js";
3
3
  import { SnapshotValidationError, validateDumpEnvelope } from "./snapshot/validation.js";
4
4
  import { EXECUTION_SEMANTICS } from "./snapshot/dump-format.js";
5
5
  import { assertSnapshotInactive } from "./interp/running-state.js";
6
+ import { validateSnapshotMigration } from "./snapshot/migration.js";
6
7
  export class SnapshotMismatchError extends Error {
7
8
  actualHash;
8
9
  expectedHash;
@@ -17,6 +18,7 @@ export class SnapshotMismatchError extends Error {
17
18
  export function restore(snapshot, options) {
18
19
  assertSnapshotInactive(snapshot);
19
20
  validateDumpEnvelope(snapshot);
21
+ validateSnapshotMigration(snapshot.migration, snapshot.sourceHash);
20
22
  if (snapshot.executionSemantics !== EXECUTION_SEMANTICS &&
21
23
  (snapshot.executionSemantics !== undefined ||
22
24
  snapshot.promiseReplay !== undefined ||
@@ -28,7 +28,7 @@ import { DUMP_FORMAT_VERSION, EXECUTION_SEMANTICS } from "./snapshot/dump-format
28
28
  import { createSnapshotScheduler } from "./snapshot/scheduler.js";
29
29
  import { UnsnapshotableValueError } from "./snapshot/serialize.js";
30
30
  import { prepareReplayInputs } from "./snapshot/replay-inputs.js";
31
- import { MissingReplayCapabilityError } from "./snapshot/replay-data.js";
31
+ import { decodeReplayData, MissingReplayCapabilityError } from "./snapshot/replay-data.js";
32
32
  export class UnhandledRejectionError extends Error {
33
33
  reason;
34
34
  constructor(reason, span) {
@@ -44,6 +44,7 @@ export function run(source, options = {}) {
44
44
  const lifecycle = { hostCallbackDepth: 0 };
45
45
  const dumpController = createDumpController(lifecycle);
46
46
  const promiseTracker = createSandboxPromiseRejectionTracker();
47
+ let completedSnapshot;
47
48
  const execute = async () => {
48
49
  const promiseReplay = new PromiseReplay(options.snapshot?.promiseReplay);
49
50
  return promiseReplayContext.run(promiseReplay, async () => {
@@ -121,6 +122,17 @@ export function run(source, options = {}) {
121
122
  }),
122
123
  ...createRegexGlobals()
123
124
  };
125
+ const importMeta = deepCopyToSandbox(options.importMeta ?? {});
126
+ if (restoredSnapshot?.migration !== undefined) {
127
+ if (Object.hasOwn(importMeta, "migration"))
128
+ throw new TypeError("import.meta.migration is reserved for migrated checkpoint state.");
129
+ Object.defineProperty(importMeta, "migration", {
130
+ value: decodeReplayData(restoredSnapshot.migration.state),
131
+ enumerable: true,
132
+ configurable: true,
133
+ writable: true
134
+ });
135
+ }
124
136
  const initialInputs = prepareReplayInputs({
125
137
  bindings: callerBindings,
126
138
  imports: resolveModuleImports(module, options.modules, {
@@ -132,7 +144,7 @@ export function run(source, options = {}) {
132
144
  entryPointArgs: options.entryPointArgs === undefined
133
145
  ? undefined
134
146
  : deepCopyToSandbox([...options.entryPointArgs]),
135
- importMeta: deepCopyToSandbox(options.importMeta ?? {})
147
+ importMeta
136
148
  }, restoredSnapshot?.initialInputs, (promise, id) => {
137
149
  if (promise !== undefined)
138
150
  observeSandboxPromise(promise);
@@ -172,6 +184,7 @@ export function run(source, options = {}) {
172
184
  });
173
185
  snapshotScheduler = activeSnapshotScheduler;
174
186
  createFailureSnapshot = () => createRunSnapshot({
187
+ migration: restoredSnapshot?.migration,
175
188
  bindings: executionScope.snapshot().bindings,
176
189
  clock: options.clock,
177
190
  hostCalls: hostCalls.snapshot(),
@@ -197,6 +210,7 @@ export function run(source, options = {}) {
197
210
  }
198
211
  const interpreterSnapshot = yieldPoint.snapshot();
199
212
  snapshot = createRunSnapshot({
213
+ migration: restoredSnapshot?.migration,
200
214
  bindings: interpreterSnapshot.bindings,
201
215
  clock: options.clock,
202
216
  hostCalls: hostCalls.snapshot(),
@@ -240,6 +254,7 @@ export function run(source, options = {}) {
240
254
  }
241
255
  const interpreterSnapshot = yieldPoint.snapshot();
242
256
  snapshot = createRunSnapshot({
257
+ migration: restoredSnapshot?.migration,
243
258
  bindings: interpreterSnapshot.bindings,
244
259
  clock: options.clock,
245
260
  hostCalls: hostCalls.snapshot(),
@@ -277,6 +292,7 @@ export function run(source, options = {}) {
277
292
  replayError = error.message;
278
293
  }
279
294
  const snapshot = createRunSnapshot({
295
+ migration: restoredSnapshot?.migration,
280
296
  bindings: executionScope.snapshot().bindings,
281
297
  clock: options.clock,
282
298
  hostCalls: hostCalls.snapshot(),
@@ -289,6 +305,7 @@ export function run(source, options = {}) {
289
305
  if (replayError !== undefined)
290
306
  snapshot.replayError = replayError;
291
307
  dumpController.finalize(snapshot);
308
+ completedSnapshot = snapshot;
292
309
  return {
293
310
  ...result,
294
311
  snapshot
@@ -327,7 +344,18 @@ export function run(source, options = {}) {
327
344
  }
328
345
  });
329
346
  };
330
- const result = withRunResources(options.signal, () => withSandboxPromiseRejectionTracker(promiseTracker, execute));
347
+ const result = withRunResources(options.signal, () => withSandboxPromiseRejectionTracker(promiseTracker, execute)).catch(async (error) => {
348
+ if (completedSnapshot !== undefined) {
349
+ try {
350
+ await createSnapshotScheduler(options).write(completedSnapshot);
351
+ }
352
+ catch (snapshotError) {
353
+ console.warn("Failed to write failure snapshot.", snapshotError);
354
+ }
355
+ }
356
+ dumpController.fail(error);
357
+ throw error;
358
+ });
331
359
  return attachDumpController(result, dumpController);
332
360
  }
333
361
  async function throwIfReturnedPromiseRejected(result) {
@@ -428,6 +456,7 @@ function createRunSnapshot(input) {
428
456
  version: DUMP_FORMAT_VERSION,
429
457
  executionSemantics: EXECUTION_SEMANTICS,
430
458
  sourceHash: input.sourceHash,
459
+ ...(input.migration === undefined ? {} : { migration: structuredClone(input.migration) }),
431
460
  bindings: input.bindings,
432
461
  clock: input.clock?.snapshot(),
433
462
  ...(input.hostCalls === undefined || input.hostCalls.length === 0
@@ -0,0 +1,34 @@
1
+ import { type HostCallReplay } from "../interp/host-call.js";
2
+ import { type ReplayData } from "./replay-data.js";
3
+ export type SnapshotMigrationResolution = {
4
+ callId: string;
5
+ disposition: "not-performed";
6
+ } | {
7
+ callId: string;
8
+ disposition: "fulfilled";
9
+ value: unknown;
10
+ } | {
11
+ callId: string;
12
+ disposition: "rejected";
13
+ reason: unknown;
14
+ };
15
+ export type SnapshotMigrationReconciliation = {
16
+ checkpointDigest: string;
17
+ quiescent: boolean;
18
+ calls: SnapshotMigrationResolution[];
19
+ };
20
+ export type SnapshotMigration = {
21
+ version: 1;
22
+ state: ReplayData;
23
+ history: Array<{
24
+ sourceHash: string;
25
+ targetSourceHash: string;
26
+ executionSemantics: string;
27
+ replay: HostCallReplay;
28
+ reconciliation: SnapshotMigrationReconciliation;
29
+ }>;
30
+ };
31
+ export declare function validateMigrationSemantics(value: unknown): asserts value is string;
32
+ export declare function validateMigrationJournal(sourceHash: string, replay: unknown, hostCalls?: unknown): HostCallReplay;
33
+ export declare function validateMigrationReconciliation(value: unknown, checkpointDigest: string, calls: HostCallReplay["calls"]): asserts value is SnapshotMigrationReconciliation;
34
+ export declare function validateSnapshotMigration(value: unknown, sourceHash: string): SnapshotMigration | undefined;
@@ -0,0 +1,92 @@
1
+ import { HostCallJournal } from "../interp/host-call.js";
2
+ import { decodeReplayData } from "./replay-data.js";
3
+ import { validateSnapshotData } from "./validation.js";
4
+ export function validateMigrationSemantics(value) {
5
+ if (!["jobs-v1", "jobs-v2", "jobs-v3", "jobs-v4", "jobs-v5"].includes(value))
6
+ throw new TypeError("Migration requires a supported execution-semantics marker (jobs-v1 through jobs-v5).");
7
+ }
8
+ export function validateMigrationJournal(sourceHash, replay, hostCalls = []) {
9
+ if (replay === undefined)
10
+ throw new TypeError("Migration requires a complete host replay journal.");
11
+ if (!Array.isArray(hostCalls))
12
+ throw new TypeError("Invalid migration hostCalls.");
13
+ const journal = new HostCallJournal(sourceHash, hostCalls, undefined, replay);
14
+ try {
15
+ const restored = journal.snapshotReplay();
16
+ for (const call of restored.calls) {
17
+ if (["created", "running"].includes(call.lifecycle) && call.outcome !== undefined)
18
+ throw new TypeError("Unresolved migration calls cannot contain a recorded outcome.");
19
+ for (const callback of call.callbacks ?? []) {
20
+ if (!call.functions?.includes(callback.id))
21
+ throw new TypeError("Migration callback has no registered source function.");
22
+ }
23
+ }
24
+ return restored;
25
+ }
26
+ finally {
27
+ journal.dispose();
28
+ }
29
+ }
30
+ export function validateMigrationReconciliation(value, checkpointDigest, calls) {
31
+ validateSnapshotData(value);
32
+ const receipt = value;
33
+ if (receipt === null ||
34
+ typeof receipt !== "object" ||
35
+ receipt.checkpointDigest !== checkpointDigest ||
36
+ !isCheckpointDigest(receipt.checkpointDigest))
37
+ throw new TypeError("Migration reconciliation must identify this exact checkpoint digest.");
38
+ if (receipt.quiescent !== true)
39
+ throw new TypeError("Migration requires explicit host confirmation that execution, operations, and callbacks are quiescent.");
40
+ if (!Array.isArray(receipt.calls))
41
+ throw new TypeError("Migration reconciliation calls must be an array.");
42
+ const pending = new Map(calls
43
+ .filter((call) => !["settled", "consumed"].includes(call.lifecycle))
44
+ .map((call) => [call.id, call]));
45
+ for (const resolution of receipt.calls) {
46
+ if (resolution === null || typeof resolution !== "object" || !pending.has(resolution.callId))
47
+ throw new TypeError("Migration contains an unknown or duplicate call resolution.");
48
+ if (!["not-performed", "fulfilled", "rejected"].includes(resolution.disposition) ||
49
+ (resolution.disposition === "fulfilled" && !Object.hasOwn(resolution, "value")) ||
50
+ (resolution.disposition === "rejected" && !Object.hasOwn(resolution, "reason")))
51
+ throw new TypeError("Migration call resolution requires a reconciled outcome or confirmed non-performance.");
52
+ pending.delete(resolution.callId);
53
+ }
54
+ if (pending.size > 0)
55
+ throw new TypeError(`Migration requires external reconciliation for: ${[...pending.keys()].join(", ")}`);
56
+ }
57
+ export function validateSnapshotMigration(value, sourceHash) {
58
+ if (value === undefined)
59
+ return undefined;
60
+ validateSnapshotData(value);
61
+ const migration = value;
62
+ if (migration === null ||
63
+ typeof migration !== "object" ||
64
+ migration.version !== 1 ||
65
+ !Array.isArray(migration.history) ||
66
+ migration.history.length === 0)
67
+ throw new TypeError("Invalid snapshot migration history.");
68
+ decodeReplayData(migration.state);
69
+ let previousTarget;
70
+ for (const entry of migration.history) {
71
+ if (entry === null ||
72
+ typeof entry !== "object" ||
73
+ typeof entry.sourceHash !== "string" ||
74
+ entry.sourceHash.length === 0 ||
75
+ typeof entry.targetSourceHash !== "string" ||
76
+ entry.targetSourceHash.length === 0 ||
77
+ (previousTarget !== undefined && previousTarget !== entry.sourceHash))
78
+ throw new TypeError("Invalid snapshot migration ancestry.");
79
+ validateMigrationSemantics(entry.executionSemantics);
80
+ const replay = validateMigrationJournal(entry.sourceHash, entry.replay);
81
+ validateMigrationReconciliation(entry.reconciliation, entry.reconciliation?.checkpointDigest, replay.calls);
82
+ previousTarget = entry.targetSourceHash;
83
+ }
84
+ if (previousTarget !== sourceHash)
85
+ throw new TypeError("Migration ancestry does not match the target source.");
86
+ return migration;
87
+ }
88
+ function isCheckpointDigest(value) {
89
+ return (typeof value === "string" &&
90
+ value.length === 64 &&
91
+ [...value].every((character) => "0123456789abcdef".includes(character)));
92
+ }