poe-code 10.0.3 → 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) {
@@ -122,6 +122,17 @@ export function run(source, options = {}) {
122
122
  }),
123
123
  ...createRegexGlobals()
124
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
+ }
125
136
  const initialInputs = prepareReplayInputs({
126
137
  bindings: callerBindings,
127
138
  imports: resolveModuleImports(module, options.modules, {
@@ -133,7 +144,7 @@ export function run(source, options = {}) {
133
144
  entryPointArgs: options.entryPointArgs === undefined
134
145
  ? undefined
135
146
  : deepCopyToSandbox([...options.entryPointArgs]),
136
- importMeta: deepCopyToSandbox(options.importMeta ?? {})
147
+ importMeta
137
148
  }, restoredSnapshot?.initialInputs, (promise, id) => {
138
149
  if (promise !== undefined)
139
150
  observeSandboxPromise(promise);
@@ -173,6 +184,7 @@ export function run(source, options = {}) {
173
184
  });
174
185
  snapshotScheduler = activeSnapshotScheduler;
175
186
  createFailureSnapshot = () => createRunSnapshot({
187
+ migration: restoredSnapshot?.migration,
176
188
  bindings: executionScope.snapshot().bindings,
177
189
  clock: options.clock,
178
190
  hostCalls: hostCalls.snapshot(),
@@ -198,6 +210,7 @@ export function run(source, options = {}) {
198
210
  }
199
211
  const interpreterSnapshot = yieldPoint.snapshot();
200
212
  snapshot = createRunSnapshot({
213
+ migration: restoredSnapshot?.migration,
201
214
  bindings: interpreterSnapshot.bindings,
202
215
  clock: options.clock,
203
216
  hostCalls: hostCalls.snapshot(),
@@ -241,6 +254,7 @@ export function run(source, options = {}) {
241
254
  }
242
255
  const interpreterSnapshot = yieldPoint.snapshot();
243
256
  snapshot = createRunSnapshot({
257
+ migration: restoredSnapshot?.migration,
244
258
  bindings: interpreterSnapshot.bindings,
245
259
  clock: options.clock,
246
260
  hostCalls: hostCalls.snapshot(),
@@ -278,6 +292,7 @@ export function run(source, options = {}) {
278
292
  replayError = error.message;
279
293
  }
280
294
  const snapshot = createRunSnapshot({
295
+ migration: restoredSnapshot?.migration,
281
296
  bindings: executionScope.snapshot().bindings,
282
297
  clock: options.clock,
283
298
  hostCalls: hostCalls.snapshot(),
@@ -441,6 +456,7 @@ function createRunSnapshot(input) {
441
456
  version: DUMP_FORMAT_VERSION,
442
457
  executionSemantics: EXECUTION_SEMANTICS,
443
458
  sourceHash: input.sourceHash,
459
+ ...(input.migration === undefined ? {} : { migration: structuredClone(input.migration) }),
444
460
  bindings: input.bindings,
445
461
  clock: input.clock?.snapshot(),
446
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
+ }