poe-code 10.0.3 → 11.0.0

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.
Files changed (32) hide show
  1. package/dist/cli/commands/harness.js +27 -1
  2. package/dist/cli/commands/harness.js.map +1 -1
  3. package/dist/index.js +1125 -888
  4. package/dist/index.js.map +4 -4
  5. package/dist/metafile.json +1 -1
  6. package/package.json +1 -1
  7. package/packages/agent-harness/dist/loader/run.js +4 -1
  8. package/packages/safejs/dist/chunks/{chunk-MRH5YDMR.js → chunk-ARFGEQTM.js} +213 -51
  9. package/packages/safejs/dist/chunks/chunk-ARFGEQTM.js.map +7 -0
  10. package/packages/safejs/dist/chunks/{chunk-A2XBEN3H.js → chunk-DKXGPEOM.js} +182 -133
  11. package/packages/safejs/dist/chunks/{chunk-A2XBEN3H.js.map → chunk-DKXGPEOM.js.map} +3 -3
  12. package/packages/safejs/dist/cli.js +51 -2
  13. package/packages/safejs/dist/cli.js.map +2 -2
  14. package/packages/safejs/dist/core.js +1 -1
  15. package/packages/safejs/dist/index.d.ts +3 -0
  16. package/packages/safejs/dist/index.js +8 -2
  17. package/packages/safejs/dist/index.js.map +1 -1
  18. package/packages/safejs/dist/interp/interpreter.js +73 -127
  19. package/packages/safejs/dist/interp/scope.js +3 -3
  20. package/packages/safejs/dist/migrate.d.ts +33 -0
  21. package/packages/safejs/dist/migrate.js +70 -0
  22. package/packages/safejs/dist/migration-file.d.ts +55 -0
  23. package/packages/safejs/dist/migration-file.js +79 -0
  24. package/packages/safejs/dist/restore.d.ts +2 -0
  25. package/packages/safejs/dist/restore.js +2 -0
  26. package/packages/safejs/dist/run.js +18 -2
  27. package/packages/safejs/dist/snapshot/dump-format.d.ts +1 -1
  28. package/packages/safejs/dist/snapshot/dump-format.js +1 -1
  29. package/packages/safejs/dist/snapshot/migration.d.ts +34 -0
  30. package/packages/safejs/dist/snapshot/migration.js +92 -0
  31. package/packages/safejs/dist/snapshot/validation.js +0 -4
  32. package/packages/safejs/dist/chunks/chunk-MRH5YDMR.js.map +0 -7
@@ -422,32 +422,30 @@ async function evaluateAssignmentExpression(node, context) {
422
422
  error: createError("UNSUPPORTED_NODE", node, `Unsupported assignment target '${node.left.type}'.`)
423
423
  };
424
424
  }
425
- const binding = context.scope.lookup(node.left.name);
426
- if (!binding.found) {
425
+ const binding = node.operator === "=" ? undefined : context.scope.lookup(node.left.name);
426
+ if (binding?.found === false) {
427
427
  throw new ReferenceError(`Cannot assign to undeclared binding '${node.left.name}'.`);
428
428
  }
429
- if (binding.kind === "const") {
430
- throw new TypeError(`Cannot assign to const '${node.left.name}'`);
431
- }
432
- if (node.operator === "&&=" && !isTruthy(binding.value)) {
429
+ const current = binding?.value;
430
+ if (node.operator === "&&=" && !isTruthy(current)) {
433
431
  return {
434
432
  kind: "normal",
435
433
  hasValue: true,
436
- value: binding.value
434
+ value: current
437
435
  };
438
436
  }
439
- if (node.operator === "||=" && isTruthy(binding.value)) {
437
+ if (node.operator === "||=" && isTruthy(current)) {
440
438
  return {
441
439
  kind: "normal",
442
440
  hasValue: true,
443
- value: binding.value
441
+ value: current
444
442
  };
445
443
  }
446
- if (node.operator === "??=" && binding.value !== null && binding.value !== undefined) {
444
+ if (node.operator === "??=" && current !== null && current !== undefined) {
447
445
  return {
448
446
  kind: "normal",
449
447
  hasValue: true,
450
- value: binding.value
448
+ value: current
451
449
  };
452
450
  }
453
451
  const right = await evaluateNode(node.right, context);
@@ -459,7 +457,7 @@ async function evaluateAssignmentExpression(node, context) {
459
457
  node.operator === "||=" ||
460
458
  node.operator === "??="
461
459
  ? right.value
462
- : applyCompoundAssignmentOperator(node.operator, binding.value, right.value, context);
460
+ : await applyCompoundAssignmentOperator(node.operator, current, right.value, context);
463
461
  context.scope.assign(node.left.name, value);
464
462
  return {
465
463
  kind: "normal",
@@ -481,55 +479,38 @@ async function evaluateMemberAssignmentExpression(node, context) {
481
479
  if (member.kind === "nullish") {
482
480
  throw new TypeError("Cannot assign properties of null or undefined.");
483
481
  }
484
- if (isSandboxRegex(member.object)) {
485
- if (node.operator !== "=") {
486
- throw new TypeError("RegExp properties only support direct assignment.");
487
- }
488
- const right = await evaluateNode(node.right, context);
489
- if (right.kind !== "normal")
490
- return right;
491
- setRegexMember(member.object, member.property, right.value);
492
- return { kind: "normal", hasValue: true, value: right.value };
493
- }
494
- if (!isIndexableSandboxValue(member.object)) {
495
- throw new TypeError("Assignment expressions require a sandbox object property.");
496
- }
497
- if (node.operator === "&&=" &&
498
- !isTruthy(getMemberValue(member.object, member.property, context))) {
482
+ const current = node.operator === "=" ? undefined : getPropertyValue(member.object, member.property, context);
483
+ if (node.operator === "&&=" && !isTruthy(current)) {
499
484
  return {
500
485
  kind: "normal",
501
486
  hasValue: true,
502
- value: getMemberValue(member.object, member.property, context)
487
+ value: current
503
488
  };
504
489
  }
505
- if (node.operator === "||=" &&
506
- isTruthy(getMemberValue(member.object, member.property, context))) {
490
+ if (node.operator === "||=" && isTruthy(current)) {
507
491
  return {
508
492
  kind: "normal",
509
493
  hasValue: true,
510
- value: getMemberValue(member.object, member.property, context)
494
+ value: current
511
495
  };
512
496
  }
513
- if (node.operator === "??=" &&
514
- getMemberValue(member.object, member.property, context) !== null &&
515
- getMemberValue(member.object, member.property, context) !== undefined) {
497
+ if (node.operator === "??=" && current !== null && current !== undefined) {
516
498
  return {
517
499
  kind: "normal",
518
500
  hasValue: true,
519
- value: getMemberValue(member.object, member.property, context)
501
+ value: current
520
502
  };
521
503
  }
522
504
  const right = await evaluateNode(node.right, context);
523
505
  if (right.kind !== "normal") {
524
506
  return right;
525
507
  }
526
- const current = getMemberValue(member.object, member.property, context);
527
508
  const value = node.operator === "=" ||
528
509
  node.operator === "&&=" ||
529
510
  node.operator === "||=" ||
530
511
  node.operator === "??="
531
512
  ? right.value
532
- : applyCompoundAssignmentOperator(node.operator, current, right.value, context);
513
+ : await applyCompoundAssignmentOperator(node.operator, current, right.value, context);
533
514
  setSandboxProperty(member.object, member.property, value);
534
515
  return {
535
516
  kind: "normal",
@@ -1493,10 +1474,7 @@ async function evaluateIdentifierUpdateExpression(node, context) {
1493
1474
  error: createError("UNBOUND_IDENTIFIER", node.argument, `Identifier '${node.argument.name}' is not defined.`)
1494
1475
  };
1495
1476
  }
1496
- if (binding.kind === "const") {
1497
- throw new TypeError(`Cannot assign to const '${node.argument.name}'`);
1498
- }
1499
- const current = Number(binding.value);
1477
+ const current = toNumber(await toNumericPrimitive(binding.value, context));
1500
1478
  const next = node.operator === "++" ? current + 1 : current - 1;
1501
1479
  context.scope.assign(node.argument.name, next);
1502
1480
  return {
@@ -1519,10 +1497,7 @@ async function evaluateMemberUpdateExpression(node, context) {
1519
1497
  if (member.kind === "nullish") {
1520
1498
  throw new TypeError("Cannot update properties of null or undefined.");
1521
1499
  }
1522
- if (!isIndexableSandboxValue(member.object)) {
1523
- throw new TypeError("Update expressions require a sandbox object property.");
1524
- }
1525
- const current = Number(getMemberValue(member.object, member.property, context));
1500
+ const current = toNumber(await toNumericPrimitive(getPropertyValue(member.object, member.property, context), context));
1526
1501
  const next = node.operator === "++" ? current + 1 : current - 1;
1527
1502
  setSandboxProperty(member.object, member.property, next);
1528
1503
  return {
@@ -1533,94 +1508,45 @@ async function evaluateMemberUpdateExpression(node, context) {
1533
1508
  }
1534
1509
  async function evaluateMemberExpression(node, context) {
1535
1510
  const member = await evaluateMemberAccess(node, context);
1536
- if (member.kind === "error") {
1511
+ if (member.kind === "error")
1537
1512
  return member;
1538
- }
1539
- if (member.kind === "completion") {
1513
+ if (member.kind === "completion")
1540
1514
  return member.result;
1541
- }
1542
1515
  if (member.kind === "nullish") {
1543
- if (node.optional) {
1544
- return {
1545
- kind: "normal",
1546
- hasValue: true,
1547
- value: undefined
1548
- };
1549
- }
1516
+ if (node.optional)
1517
+ return { kind: "normal", hasValue: true, value: undefined };
1550
1518
  throw new TypeError("Cannot read properties of null or undefined.");
1551
1519
  }
1552
- if (typeof member.object === "string") {
1553
- return {
1554
- kind: "normal",
1555
- hasValue: true,
1556
- value: getStringMember(member.object, member.property, context.budget)
1557
- };
1558
- }
1559
- if (typeof member.object === "number") {
1560
- return {
1561
- kind: "normal",
1562
- hasValue: true,
1563
- value: getNumberMember(member.object, member.property, context.budget)
1564
- };
1565
- }
1566
- if (Array.isArray(member.object)) {
1567
- return {
1568
- kind: "normal",
1569
- hasValue: true,
1570
- value: getArrayMemberValue(member.object, member.property, context)
1571
- };
1572
- }
1573
- if (isSandboxMap(member.object)) {
1574
- return {
1575
- kind: "normal",
1576
- hasValue: true,
1577
- value: getMapMember(member.object, member.property, createMapMethodOptions(context))
1578
- };
1579
- }
1580
- if (isSandboxSet(member.object)) {
1581
- return {
1582
- kind: "normal",
1583
- hasValue: true,
1584
- value: getSetMember(member.object, member.property, createSetMethodOptions(context))
1585
- };
1586
- }
1587
- if (isSandboxGenerator(member.object)) {
1588
- return {
1589
- kind: "normal",
1590
- hasValue: true,
1591
- value: getGeneratorMember(member.object, member.property, context.budget)
1592
- };
1593
- }
1594
- if (isSandboxClosure(member.object)) {
1595
- return {
1596
- kind: "normal",
1597
- hasValue: true,
1598
- value: getClosureMemberValue(member.object, member.property, context)
1599
- };
1600
- }
1601
- if (isSandboxPromise(member.object)) {
1602
- return {
1603
- kind: "normal",
1604
- hasValue: true,
1605
- value: getPromiseMember(member.property, context.budget)
1606
- };
1607
- }
1608
- if (isSandboxRegex(member.object)) {
1609
- return {
1610
- kind: "normal",
1611
- hasValue: true,
1612
- value: getRegexMember(member.object, member.property)
1613
- };
1614
- }
1615
- if (!isIndexableSandboxValue(member.object)) {
1616
- throw new TypeError("Attempted to read a property from a non-object value.");
1617
- }
1618
1520
  return {
1619
1521
  kind: "normal",
1620
1522
  hasValue: true,
1621
- value: getMemberValue(member.object, member.property, context)
1523
+ value: getPropertyValue(member.object, member.property, context)
1622
1524
  };
1623
1525
  }
1526
+ function getPropertyValue(target, property, context) {
1527
+ if (typeof target === "string")
1528
+ return getStringMember(target, property, context.budget);
1529
+ if (typeof target === "number")
1530
+ return getNumberMember(target, property, context.budget);
1531
+ if (typeof target === "boolean")
1532
+ return undefined;
1533
+ if (isSandboxMap(target))
1534
+ return getMapMember(target, property, createMapMethodOptions(context));
1535
+ if (isSandboxSet(target))
1536
+ return getSetMember(target, property, createSetMethodOptions(context));
1537
+ if (isSandboxGenerator(target))
1538
+ return getGeneratorMember(target, property, context.budget);
1539
+ if (isSandboxClosure(target))
1540
+ return getClosureMemberValue(target, property, context);
1541
+ if (isSandboxPromise(target))
1542
+ return getPromiseMember(property, context.budget);
1543
+ if (isSandboxRegex(target))
1544
+ return getRegexMember(target, property);
1545
+ if (!isIndexableSandboxValue(target)) {
1546
+ throw new TypeError("Attempted to read a property from a non-object value.");
1547
+ }
1548
+ return getMemberValue(target, property, context);
1549
+ }
1624
1550
  async function evaluateCallExpression(node, context) {
1625
1551
  if (node.callee.type === "MemberExpression") {
1626
1552
  context.budget.visitNode();
@@ -2033,7 +1959,9 @@ function applyBinaryOperator(node, left, right, context) {
2033
1959
  throw createError("UNSUPPORTED_NODE", node, "Binary operator 'in' is not supported.");
2034
1960
  }
2035
1961
  }
2036
- function applyCompoundAssignmentOperator(operator, left, right, context) {
1962
+ async function applyCompoundAssignmentOperator(operator, left, right, context) {
1963
+ left = await toNumericPrimitive(left, context);
1964
+ right = await toNumericPrimitive(right, context);
2037
1965
  switch (operator) {
2038
1966
  case "+=":
2039
1967
  return applyAdditionOperator(left, right, context);
@@ -2160,6 +2088,9 @@ async function toNumericPrimitive(value, context) {
2160
2088
  if (isIndexableSandboxValue(value)) {
2161
2089
  for (const methodName of ["valueOf", "toString"]) {
2162
2090
  const method = getMemberValue(value, methodName, context);
2091
+ if (methodName === "toString" && method === undefined && !Object.hasOwn(value, methodName)) {
2092
+ return toString(value);
2093
+ }
2163
2094
  if (!isSandboxClosure(method)) {
2164
2095
  continue;
2165
2096
  }
@@ -2168,6 +2099,7 @@ async function toNumericPrimitive(value, context) {
2168
2099
  return result;
2169
2100
  }
2170
2101
  }
2102
+ throw new TypeError("Cannot convert object to primitive value.");
2171
2103
  }
2172
2104
  return toString(value);
2173
2105
  }
@@ -2231,17 +2163,31 @@ function getArrayMemberValue(target, property, context) {
2231
2163
  return getArrayMember(target, property, createArrayMethodOptions(context));
2232
2164
  }
2233
2165
  function setSandboxProperty(target, property, value) {
2166
+ if (isSandboxRegex(target)) {
2167
+ setRegexMember(target, property, value);
2168
+ return;
2169
+ }
2170
+ if (!isIndexableSandboxValue(target)) {
2171
+ throw new TypeError("Assignment expressions require a sandbox object property.");
2172
+ }
2173
+ const key = String(property);
2234
2174
  if (Array.isArray(target)) {
2235
2175
  assertCollectionMutable(target);
2236
- const key = String(property);
2237
2176
  if (key === "length" || isArrayIndexKey(key)) {
2238
2177
  target[key] = value;
2239
2178
  return;
2240
2179
  }
2180
+ }
2181
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
2182
+ if (descriptor !== undefined) {
2183
+ if (descriptor.writable !== true) {
2184
+ throw new TypeError(`Cannot assign to read only property '${key}'.`);
2185
+ }
2186
+ Object.defineProperty(target, key, { value });
2187
+ }
2188
+ else {
2241
2189
  defineSandboxProperty(target, key, value);
2242
- return;
2243
2190
  }
2244
- defineSandboxProperty(target, String(property), value);
2245
2191
  }
2246
2192
  function deleteSandboxProperty(target, property) {
2247
2193
  if (Array.isArray(target)) {
@@ -126,15 +126,15 @@ export class Scope {
126
126
  throw new ReferenceError(`Cannot assign to undeclared binding '${name}'.`);
127
127
  }
128
128
  const binding = scope.#bindings.get(name);
129
- if (binding?.kind === "const") {
130
- throw new TypeError(`Cannot assign to const binding '${name}'.`);
131
- }
132
129
  if (binding === undefined) {
133
130
  throw new ReferenceError(`Cannot assign to undeclared binding '${name}'.`);
134
131
  }
135
132
  if (binding.value === uninitialized) {
136
133
  throw new ReferenceError(`Cannot access '${name}' before initialization.`);
137
134
  }
135
+ if (binding.kind === "const") {
136
+ throw new TypeError(`Cannot assign to const binding '${name}'.`);
137
+ }
138
138
  scope.#bindings.set(name, {
139
139
  kind: binding.kind,
140
140
  value
@@ -0,0 +1,33 @@
1
+ import type { SafeJSSnapshot } from "./restore.js";
2
+ import { type SnapshotMigration, type SnapshotMigrationReconciliation } from "./snapshot/migration.js";
3
+ export type { SnapshotMigration, SnapshotMigrationReconciliation, SnapshotMigrationResolution } from "./snapshot/migration.js";
4
+ export type SnapshotMigrationOptions = {
5
+ source: string;
6
+ targetSource: string;
7
+ state: unknown;
8
+ reconciliation: SnapshotMigrationReconciliation;
9
+ };
10
+ export declare function inspectSnapshotMigration(snapshot: SafeJSSnapshot, options: {
11
+ source: string;
12
+ }): {
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
+ export declare function migrateSnapshot(snapshot: SafeJSSnapshot, options: SnapshotMigrationOptions): SafeJSSnapshot & {
32
+ migration: SnapshotMigration;
33
+ };
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import { assertSnapshotInactive } from "./interp/running-state.js";
3
+ import { deepCopyToSandbox } from "./interp/values.js";
4
+ import { hashSource } from "./parse/hash.js";
5
+ import { parseModule } from "./parse/parser.js";
6
+ import { DUMP_FORMAT_VERSION, EXECUTION_SEMANTICS } from "./snapshot/dump-format.js";
7
+ import { validateMigrationJournal, validateMigrationReconciliation, validateMigrationSemantics, validateSnapshotMigration } from "./snapshot/migration.js";
8
+ import { encodeReplayData } from "./snapshot/replay-data.js";
9
+ import { validateDumpEnvelope, validateSnapshotData } from "./snapshot/validation.js";
10
+ export function inspectSnapshotMigration(snapshot, options) {
11
+ assertSnapshotInactive(snapshot);
12
+ validateSnapshotData(snapshot);
13
+ validateDumpEnvelope(snapshot);
14
+ validateMigrationSemantics(snapshot.executionSemantics);
15
+ const sourceHash = hashSource(options.source);
16
+ if (snapshot.sourceHash !== sourceHash)
17
+ throw new TypeError(`Migration requires the original executable source (checkpoint hash ${snapshot.sourceHash}, supplied ${sourceHash}). Preserve the checkpoint; do not replace its hash.`);
18
+ validateSnapshotMigration(snapshot.migration, sourceHash);
19
+ const replay = validateMigrationJournal(sourceHash, snapshot.replay, snapshot.hostCalls);
20
+ return {
21
+ checkpointDigest: createHash("sha256").update(canonicalData(snapshot)).digest("hex"),
22
+ sourceHash,
23
+ executionSemantics: snapshot.executionSemantics,
24
+ calls: replay.calls,
25
+ unresolvedCalls: replay.calls.filter((call) => !["settled", "consumed"].includes(call.lifecycle))
26
+ };
27
+ }
28
+ export function migrateSnapshot(snapshot, options) {
29
+ if (!Object.hasOwn(options, "state"))
30
+ throw new TypeError("Migration requires explicit application state.");
31
+ const inspection = inspectSnapshotMigration(snapshot, { source: options.source });
32
+ parseModule(options.targetSource);
33
+ const targetSourceHash = hashSource(options.targetSource);
34
+ validateMigrationReconciliation(options.reconciliation, inspection.checkpointDigest, inspection.calls);
35
+ const state = encodeReplayData(deepCopyToSandbox(options.state));
36
+ const previous = snapshot.migration;
37
+ const migration = {
38
+ version: 1,
39
+ state,
40
+ history: [
41
+ ...structuredClone(previous?.history ?? []),
42
+ {
43
+ sourceHash: inspection.sourceHash,
44
+ targetSourceHash,
45
+ executionSemantics: inspection.executionSemantics,
46
+ replay: { version: 1, calls: inspection.calls },
47
+ reconciliation: structuredClone(options.reconciliation)
48
+ }
49
+ ]
50
+ };
51
+ validateSnapshotMigration(migration, targetSourceHash);
52
+ return {
53
+ version: DUMP_FORMAT_VERSION,
54
+ executionSemantics: EXECUTION_SEMANTICS,
55
+ sourceHash: targetSourceHash,
56
+ bindings: {},
57
+ replay: { version: 1, calls: [] },
58
+ migration
59
+ };
60
+ }
61
+ function canonicalData(value) {
62
+ if (Array.isArray(value))
63
+ return `[${value.map(canonicalData).join(",")}]`;
64
+ if (value !== null && typeof value === "object")
65
+ return `{${Object.keys(value)
66
+ .sort()
67
+ .map((key) => `${JSON.stringify(key)}:${canonicalData(value[key])}`)
68
+ .join(",")}}`;
69
+ return JSON.stringify(value);
70
+ }
@@ -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 ||