narraeon 0.1.1 → 0.2.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.
@@ -6,9 +6,13 @@ import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, }
6
6
  import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
7
7
  import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
8
8
  import { WorldDocumentStore } from "./WorldDocumentStore.js";
9
+ import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
10
+ import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
11
+ import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
9
12
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
10
13
  const publicationFile = "publication.json";
11
14
  const localMetadataFile = "local.json";
15
+ const playAuthorityHeadFile = "play-authority-head.json";
12
16
  export class FileNativeWorldCreationError extends Error {
13
17
  code;
14
18
  constructor(code, message, options) {
@@ -29,6 +33,11 @@ export class FileNativeWorldStore {
29
33
  #promptCompiler;
30
34
  #activeControlUsers = new Map();
31
35
  operations;
36
+ playTimeline;
37
+ playAdvances;
38
+ #storageMigrator;
39
+ #storageMigrations = new Map();
40
+ #currentStorage = new Set();
32
41
  constructor(dataRoot, options = {}) {
33
42
  const root = resolve(dataRoot);
34
43
  this.#worldsRoot = join(root, "worlds-file-native");
@@ -36,6 +45,64 @@ export class FileNativeWorldStore {
36
45
  this.#promptCompiler =
37
46
  options.promptCompiler ?? new FileNativePromptCompiler();
38
47
  this.operations = new FileNativeWorldOperationCoordinator(root);
48
+ this.playTimeline = new FileNativePlayTimelineStore(root);
49
+ this.playAdvances = new FileNativePlayAdvanceStore(root);
50
+ this.#storageMigrator = new FileNativeWorldStorageMigrator(root, this.playTimeline);
51
+ }
52
+ /**
53
+ * Upgrade a formally released cumulative layout before current Authority or
54
+ * play-timeline behavior crosses its seam. Concurrent callers share one
55
+ * in-process promise; the durable world lock serializes processes.
56
+ */
57
+ async ensureCurrentStorage(worldId) {
58
+ assertIdentity(worldId, "World ID");
59
+ if (this.#currentStorage.has(worldId))
60
+ return;
61
+ const existing = this.#storageMigrations.get(worldId);
62
+ if (existing !== undefined)
63
+ return existing;
64
+ const migration = this.#ensureCurrentStorageSerialized(worldId);
65
+ this.#storageMigrations.set(worldId, migration);
66
+ try {
67
+ await migration;
68
+ }
69
+ finally {
70
+ if (this.#storageMigrations.get(worldId) === migration)
71
+ this.#storageMigrations.delete(worldId);
72
+ }
73
+ }
74
+ async #ensureCurrentStorageSerialized(worldId) {
75
+ try {
76
+ try {
77
+ await readAuthorityHead(join(this.#worldsRoot, worldId));
78
+ this.#currentStorage.add(worldId);
79
+ return;
80
+ }
81
+ catch (error) {
82
+ if (!isNodeError(error) || error.code !== "ENOENT")
83
+ throw error;
84
+ }
85
+ await this.operations.withWorldAuthorityLock(worldId, async () => {
86
+ await this.#storageMigrator.migrate(worldId);
87
+ });
88
+ this.#currentStorage.add(worldId);
89
+ }
90
+ catch (error) {
91
+ if (error instanceof FileNativeWorldCreationError)
92
+ throw error;
93
+ if (isNodeError(error) && error.code === "ENOENT") {
94
+ const root = await stat(join(this.#worldsRoot, worldId)).catch((statError) => {
95
+ if (isNodeError(statError) && statError.code === "ENOENT")
96
+ return null;
97
+ throw statError;
98
+ });
99
+ if (root === null)
100
+ throw new FileNativeWorldNotFoundError({ cause: error });
101
+ }
102
+ if (error instanceof WorldOperationBusyError)
103
+ throw new FileNativeWorldCreationError("operation_conflict", "World storage is being upgraded by another durable operation", { cause: error });
104
+ throw new FileNativeWorldCreationError("world_corrupt", "Released world storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
105
+ }
39
106
  }
40
107
  async listWorlds() {
41
108
  await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
@@ -90,6 +157,7 @@ export class FileNativeWorldStore {
90
157
  }
91
158
  return this.operations.withExclusiveWorldStateMutation(worldId, async () => {
92
159
  await rm(root, { recursive: true, force: true });
160
+ this.#currentStorage.delete(worldId);
93
161
  return { deleted: true };
94
162
  });
95
163
  }
@@ -169,8 +237,17 @@ export class FileNativeWorldStore {
169
237
  additionalMaterials: initialMaterials,
170
238
  };
171
239
  await writeJson(join(stagingRoot, "runtime", "genesis.json"), genesis);
240
+ await writeJson(join(stagingRoot, "runtime", "play-genesis-timeline.json"), {
241
+ schemaVersion: 1,
242
+ worldId,
243
+ history: genesis.history,
244
+ });
245
+ await writeJson(join(stagingRoot, "runtime", playAuthorityHeadFile), genesisAuthorityHead());
172
246
  await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
247
+ schemaVersion: 2,
173
248
  head: "genesis",
249
+ sequence: 0,
250
+ commitDigest: null,
174
251
  });
175
252
  await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
176
253
  const publication = {
@@ -215,6 +292,7 @@ export class FileNativeWorldStore {
215
292
  }
216
293
  async readSurface(worldId, surface) {
217
294
  assertIdentity(worldId, "World ID");
295
+ await this.ensureCurrentStorage(worldId);
218
296
  const root = join(this.#worldsRoot, worldId);
219
297
  await readPublicationAt(root);
220
298
  if (surface === "runtime") {
@@ -236,28 +314,37 @@ export class FileNativeWorldStore {
236
314
  * must never pair a mutable state tree with a different response head.
237
315
  */
238
316
  async renderPlayerViewsAtHead(worldId, head) {
239
- const endpoint = await this.recoverEndpoint(worldId, head);
240
- const control = await this.readSurface(worldId, "control");
317
+ const root = join(this.#worldsRoot, worldId);
318
+ const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
319
+ const [state, control] = await Promise.all([
320
+ materialized?.head === head
321
+ ? readTree(join(root, "state"))
322
+ : this.recoverEndpoint(worldId, head).then(({ state }) => state),
323
+ this.readSurface(worldId, "control"),
324
+ ]);
241
325
  const source = control.find(({ path }) => path === "player-views.yaml");
242
326
  return new PlayerViewRenderer().render({
243
- snapshot: openWorldDocumentSnapshot(endpoint.state),
327
+ snapshot: openWorldDocumentSnapshot(state),
244
328
  control: source?.contents ?? "",
245
329
  });
246
330
  }
247
331
  async currentHead(worldId) {
248
332
  assertIdentity(worldId, "World ID");
333
+ await this.ensureCurrentStorage(worldId);
249
334
  const root = join(this.#worldsRoot, worldId);
250
335
  await readPublicationAt(root);
251
- return (await readAcceptedAuthority(root))?.head ?? "genesis";
336
+ return (await readAuthorityHead(root)).head;
252
337
  }
253
338
  async currentHeadOperationId(worldId) {
254
339
  assertIdentity(worldId, "World ID");
340
+ await this.ensureCurrentStorage(worldId);
255
341
  const root = join(this.#worldsRoot, worldId);
256
342
  await readPublicationAt(root);
257
- return ((await readAcceptedAuthority(root))?.commits.at(-1)?.operationId ?? null);
343
+ return (await readAuthorityHead(root)).operationId;
258
344
  }
259
345
  async saveControlDraft(worldId, files) {
260
346
  assertIdentity(worldId, "World ID");
347
+ await this.ensureCurrentStorage(worldId);
261
348
  const root = join(this.#worldsRoot, worldId);
262
349
  await readPublicationAt(root);
263
350
  for (const file of files)
@@ -350,6 +437,7 @@ export class FileNativeWorldStore {
350
437
  }
351
438
  async #readControlDraft(worldId) {
352
439
  assertIdentity(worldId, "World ID");
440
+ await this.ensureCurrentStorage(worldId);
353
441
  const root = join(this.#worldsRoot, worldId);
354
442
  await readPublicationAt(root);
355
443
  const [state, draft] = await Promise.all([
@@ -363,6 +451,7 @@ export class FileNativeWorldStore {
363
451
  /** Bind the current Authority endpoint for a model-directed play call chain. */
364
452
  async bindPlayCallChain(worldId) {
365
453
  assertIdentity(worldId, "World ID");
454
+ await this.ensureCurrentStorage(worldId);
366
455
  const root = join(this.#worldsRoot, worldId);
367
456
  try {
368
457
  await readPublicationAt(root);
@@ -384,23 +473,16 @@ export class FileNativeWorldStore {
384
473
  }
385
474
  try {
386
475
  return await this.operations.withWorldAuthorityLock(worldId, async () => {
387
- const publishedAuthority = await readAcceptedAuthority(root);
388
- if (publishedAuthority !== null) {
389
- const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
390
- let materializedIndex = 0;
391
- if (materialized !== null && materialized.head !== "genesis") {
392
- const found = publishedAuthority.commits.findIndex(({ head }) => head === materialized.head);
393
- if (found < 0)
394
- throw new FileNativeWorldCreationError("world_corrupt", "World materialized head does not belong to the authority commit chain");
395
- materializedIndex = found + 1;
396
- }
397
- for (let index = materializedIndex; index < publishedAuthority.commits.length; index += 1) {
398
- const commit = publishedAuthority.commits[index];
399
- await materializePlayCommit(root, index + 1, {
476
+ const authority = await readAuthorityHead(root);
477
+ const checkpoint = await readMaterializedCheckpoint(root);
478
+ if (!sameMaterializedEndpoint(checkpoint, authority)) {
479
+ const missing = await readAuthorityTail(root, authority, checkpoint);
480
+ for (const commit of missing) {
481
+ await materializePlayCommit(root, commit.sequence, {
400
482
  mode: commit.mode,
401
483
  historyAppend: commit.historyAppend,
402
484
  nextMaterials: commit.nextAdditionalMaterials,
403
- stateChanges: commit.stateChanges ?? [],
485
+ stateChanges: commit.stateChanges,
404
486
  ...(commit.timelineRevision === undefined
405
487
  ? {}
406
488
  : { timelineRevision: commit.timelineRevision }),
@@ -410,35 +492,37 @@ export class FileNativeWorldStore {
410
492
  worldId,
411
493
  parentHead: commit.parentHead,
412
494
  head: commit.head,
495
+ commitDigest: playCommitDigest(commit),
413
496
  historyAppend: commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })),
414
497
  nextAdditionalMaterials: commit.nextAdditionalMaterials,
415
498
  mode: commit.mode,
416
499
  });
417
500
  }
418
501
  await publishJson(join(root, "runtime", "materialized-head.json"), {
419
- head: publishedAuthority.head,
502
+ schemaVersion: 2,
503
+ head: authority.head,
504
+ sequence: authority.sequence,
505
+ commitDigest: authority.commitDigest,
420
506
  });
421
507
  }
422
- const [state, control, authority, materials, recovered] = await Promise.all([
508
+ const [state, control, history, materials] = await Promise.all([
423
509
  readTree(join(root, "state")),
424
510
  readTree(join(root, "control")),
425
- readAcceptedAuthority(root),
511
+ readTree(join(root, "history")),
426
512
  readOptionalJson(join(root, "runtime", "additional-materials.json")),
427
- this.recoverEndpoint(worldId),
428
513
  ]);
514
+ if ((materials?.head ?? "genesis") !== authority.head)
515
+ throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
429
516
  return {
430
517
  worldId,
431
- parentHead: authority?.head ?? "genesis",
518
+ parentHead: authority.head,
432
519
  files: [...state, ...control].reduce((files, { path, contents }, index) => {
433
520
  files[`${index < state.length ? "state" : "control"}/${path}`] =
434
521
  contents;
435
522
  return files;
436
523
  }, {}),
437
524
  additionalMaterials: materials?.items ?? [],
438
- history: Object.fromEntries(recovered.history.map(({ messageId, exactText }) => [
439
- messageId,
440
- exactText,
441
- ])),
525
+ history: materializedHistoryRecord(worldId, history),
442
526
  };
443
527
  });
444
528
  }
@@ -479,6 +563,7 @@ export class FileNativeWorldStore {
479
563
  }
480
564
  async recoverEndpoint(worldId, head) {
481
565
  assertIdentity(worldId, "World ID");
566
+ await this.ensureCurrentStorage(worldId);
482
567
  const root = join(this.#worldsRoot, worldId);
483
568
  await readPublicationAt(root);
484
569
  const genesis = await readJson(join(root, "runtime", "genesis.json"));
@@ -540,6 +625,7 @@ export class FileNativeWorldStore {
540
625
  }
541
626
  async repairMaterialization(worldId) {
542
627
  assertIdentity(worldId, "World ID");
628
+ await this.ensureCurrentStorage(worldId);
543
629
  try {
544
630
  const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
545
631
  await this.bindPlayCallChain(worldId);
@@ -572,6 +658,7 @@ export class FileNativeWorldStore {
572
658
  const finalRoot = join(this.#worldsRoot, worldId);
573
659
  const previous = await readOptionalJson(operationPath);
574
660
  if (previous !== null) {
661
+ await this.ensureCurrentStorage(previous.worldId);
575
662
  if (!(await derivationPublicationMatches(join(this.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
576
663
  throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
577
664
  return {
@@ -581,6 +668,7 @@ export class FileNativeWorldStore {
581
668
  }
582
669
  const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
583
670
  if (alreadyPublished !== null) {
671
+ await this.ensureCurrentStorage(alreadyPublished.worldId);
584
672
  if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
585
673
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
586
674
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
@@ -590,6 +678,7 @@ export class FileNativeWorldStore {
590
678
  world: await this.#currentSummary(alreadyPublished),
591
679
  };
592
680
  }
681
+ await this.ensureCurrentStorage(input.sourceWorldId);
593
682
  const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
594
683
  const sourcePublication = await readPublicationAt(sourceRoot);
595
684
  const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
@@ -628,12 +717,18 @@ export class FileNativeWorldStore {
628
717
  additionalMaterials: cloned.genesisMaterials,
629
718
  };
630
719
  await writeJson(join(staging, "runtime", "genesis.json"), genesis);
720
+ await writeJson(join(staging, "runtime", "play-genesis-timeline.json"), {
721
+ schemaVersion: 1,
722
+ worldId,
723
+ history: genesis.history,
724
+ });
631
725
  await writeJson(join(staging, "runtime", "additional-materials.json"), {
632
726
  head: "genesis",
633
727
  items: cloned.genesisMaterials,
634
728
  });
635
729
  for (const [index, commit] of cloned.commits.entries()) {
636
- await publishImmutableJson(join(staging, "runtime", "play-commits", `${createHash("sha256").update(JSON.stringify(commit)).digest("hex")}.json`), commit);
730
+ const digest = playCommitDigest(commit);
731
+ await publishImmutableJson(join(staging, "runtime", "play-commits", `${digest}.json`), commit);
637
732
  await materializePlayCommit(staging, index + 1, {
638
733
  mode: commit.mode,
639
734
  historyAppend: commit.historyAppend,
@@ -644,14 +739,21 @@ export class FileNativeWorldStore {
644
739
  : { timelineRevision: commit.timelineRevision }),
645
740
  });
646
741
  }
647
- if (cloned.commits.length > 0)
648
- await writeJson(join(staging, "runtime", "play-authority.json"), {
649
- schemaVersion: 1,
650
- head: input.sourceHead,
651
- commits: cloned.commits,
742
+ const clonedHead = cloned.commits.at(-1);
743
+ await writeJson(join(staging, "runtime", playAuthorityHeadFile), clonedHead === undefined
744
+ ? genesisAuthorityHead()
745
+ : {
746
+ schemaVersion: 2,
747
+ head: clonedHead.head,
748
+ sequence: clonedHead.sequence,
749
+ commitDigest: playCommitDigest(clonedHead),
750
+ operationId: clonedHead.operationId,
652
751
  });
653
752
  await writeJson(join(staging, "runtime", "materialized-head.json"), {
753
+ schemaVersion: 2,
654
754
  head: input.sourceHead,
755
+ sequence: clonedHead?.sequence ?? 0,
756
+ commitDigest: clonedHead === undefined ? null : playCommitDigest(clonedHead),
655
757
  });
656
758
  const materializedState = await readTree(join(staging, "state"));
657
759
  if (!isDeepStrictEqual(materializedState, selected.state))
@@ -698,7 +800,11 @@ export class FileNativeWorldStore {
698
800
  assertIdentity(operationId, "operation ID");
699
801
  let directRecord;
700
802
  try {
701
- const value = await readOptionalJson(this.#operationOutcomePath(operationId));
803
+ let value = await readOptionalJson(this.#operationOutcomePath(operationId));
804
+ if (isReleasedCommittedOperationOutcome(value)) {
805
+ await this.ensureCurrentStorage(value.worldId);
806
+ value = await readOptionalJson(this.#operationOutcomePath(operationId));
807
+ }
702
808
  if (value === null)
703
809
  directRecord = null;
704
810
  else {
@@ -711,46 +817,18 @@ export class FileNativeWorldStore {
711
817
  throw error;
712
818
  throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
713
819
  }
820
+ if (directRecord?.outcome === "acceptance_prepared")
821
+ return resolvePreparedOperationOutcome(this.#worldsRoot, operationId, directRecord);
714
822
  const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
715
823
  if (direct?.outcome === "committed" ||
716
824
  direct?.outcome === "committed_materialization_pending") {
717
- if (direct.outcome === "committed") {
718
- await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, direct.worldId), operationId, direct);
719
- const materialized = await readOptionalJson(join(this.#worldsRoot, direct.worldId, "runtime", "materialized-head.json"));
720
- if (materialized?.head !== direct.head)
721
- return { ...direct, outcome: "committed_materialization_pending" };
722
- }
723
- else {
724
- await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, direct.worldId), operationId, direct);
725
- await assertPendingMaterializationCompatible(join(this.#worldsRoot, direct.worldId), operationId);
726
- }
727
- return direct;
728
- }
729
- const worlds = await readDirectoryNames(this.#worldsRoot);
730
- for (const worldId of worlds) {
731
- const authority = await readAcceptedAuthority(join(this.#worldsRoot, worldId));
732
- const commitIndex = authority?.commits.findIndex((candidate) => candidate.operationId === operationId) ?? -1;
733
- const commit = authority?.commits[commitIndex];
734
- if (commit !== undefined) {
735
- const materialized = await readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "materialized-head.json"));
736
- const materializedIndex = materialized?.head === "genesis"
737
- ? -1
738
- : (authority?.commits.findIndex(({ head }) => head === materialized?.head) ?? -1);
739
- return {
740
- outcome: materializedIndex >= commitIndex
741
- ? "committed"
742
- : "committed_materialization_pending",
743
- worldId,
744
- parentHead: commit.parentHead,
745
- head: commit.head,
746
- historyAppend: commit.historyAppend.map(({ role, exactText }) => ({
747
- role,
748
- exactText,
749
- })),
750
- nextAdditionalMaterials: commit.nextAdditionalMaterials,
751
- mode: commit.mode,
752
- };
753
- }
825
+ const worldRoot = join(this.#worldsRoot, direct.worldId);
826
+ const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, direct);
827
+ const materialized = await readMaterializedCheckpoint(worldRoot);
828
+ if (materialized !== null && materialized.sequence >= commit.sequence)
829
+ return { ...direct, outcome: "committed" };
830
+ await assertPendingMaterializationCompatible(worldRoot, operationId, direct.commitDigest);
831
+ return { ...direct, outcome: "committed_materialization_pending" };
754
832
  }
755
833
  return direct?.outcome === "in_progress"
756
834
  ? { outcome: "in_progress" }
@@ -773,6 +851,7 @@ export class FileNativeWorldStore {
773
851
  throw new TypeError("Edited player message cannot be empty");
774
852
  if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
775
853
  throw new TypeError("Timeline-revision request fingerprint is invalid");
854
+ await this.ensureCurrentStorage(input.worldId);
776
855
  const existing = await this.getOperationOutcome(input.operationId);
777
856
  if (isCommittedOutcome(existing)) {
778
857
  const authority = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
@@ -821,6 +900,7 @@ export class FileNativeWorldStore {
821
900
  });
822
901
  }
823
902
  async commitCorrection(input) {
903
+ await this.ensureCurrentStorage(input.worldId);
824
904
  try {
825
905
  const commit = async () => {
826
906
  let operationReserved = input.stateOperationClaim !== undefined;
@@ -856,6 +936,7 @@ export class FileNativeWorldStore {
856
936
  * separate commits; there is no synthetic call-chain-wide settlement boundary.
857
937
  */
858
938
  async commitPlayStep(input) {
939
+ await this.ensureCurrentStorage(input.worldId);
859
940
  if (input.historyAppend.length === 0 && input.stateChanges.length === 0)
860
941
  throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
861
942
  return this.#commitHistoryChange({
@@ -863,24 +944,11 @@ export class FileNativeWorldStore {
863
944
  mode: "play",
864
945
  });
865
946
  }
866
- async readPlayCallChain(worldId) {
867
- assertIdentity(worldId, "World ID");
868
- return readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"));
869
- }
870
- async writePlayCallChain(worldId, value) {
871
- assertIdentity(worldId, "World ID");
872
- await publishJson(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"), value);
873
- }
874
- async removePlayCallChain(worldId) {
875
- assertIdentity(worldId, "World ID");
876
- await rm(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"), { force: true });
877
- }
878
947
  async #commitHistoryChange(input) {
879
948
  const existing = await this.getOperationOutcome(input.operationId);
880
949
  if (isCommittedOutcome(existing)) {
881
950
  assertSameOperationOutcome(existing, input);
882
- const accepted = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
883
- const commit = accepted?.commits.find(({ operationId }) => operationId === input.operationId);
951
+ const commit = await readPlayCommitByDigest(join(this.#worldsRoot, existing.worldId), existing.commitDigest);
884
952
  if (commit?.mode !== input.mode ||
885
953
  JSON.stringify(commit?.stateChanges) !==
886
954
  JSON.stringify(input.stateChanges) ||
@@ -888,8 +956,14 @@ export class FileNativeWorldStore {
888
956
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
889
957
  return existing;
890
958
  }
959
+ const prepared = existing.outcome === "in_progress"
960
+ ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
961
+ : null;
962
+ if (prepared !== null)
963
+ await assertPreparedOperationMatches(this.#worldsRoot, input.operationId, prepared, input);
891
964
  if (existing.outcome !== "not_started" &&
892
- !(input.operationReserved === true && existing.outcome === "in_progress"))
965
+ !(existing.outcome === "in_progress" &&
966
+ (input.operationReserved === true || prepared !== null)))
893
967
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
894
968
  const root = join(this.#worldsRoot, input.worldId);
895
969
  try {
@@ -897,20 +971,23 @@ export class FileNativeWorldStore {
897
971
  const concurrent = await this.#readOperationOutcome(input.operationId);
898
972
  if (isCommittedOutcome(concurrent)) {
899
973
  assertSameOperationOutcome(concurrent, input);
900
- const accepted = await readAcceptedAuthority(join(this.#worldsRoot, concurrent.worldId));
901
- const knownCommit = accepted?.commits.find(({ operationId }) => operationId === input.operationId);
974
+ const knownCommit = await readPlayCommitByDigest(join(this.#worldsRoot, concurrent.worldId), concurrent.commitDigest);
902
975
  if (knownCommit?.mode !== input.mode ||
903
976
  !isDeepStrictEqual(knownCommit.stateChanges, input.stateChanges) ||
904
977
  !isDeepStrictEqual(knownCommit.timelineRevision, input.timelineRevision))
905
978
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
906
979
  return concurrent;
907
980
  }
908
- const authorityPath = join(root, "runtime", "play-authority.json");
909
- const authority = (await readAcceptedAuthority(root)) ?? {
910
- schemaVersion: 1,
911
- head: "genesis",
912
- commits: [],
913
- };
981
+ const concurrentPrepared = concurrent.outcome === "in_progress"
982
+ ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
983
+ : null;
984
+ if (concurrentPrepared !== null)
985
+ await assertPreparedOperationMatches(this.#worldsRoot, input.operationId, concurrentPrepared, input);
986
+ else if (concurrent.outcome === "in_progress" &&
987
+ input.operationReserved !== true)
988
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
989
+ const authorityPath = join(root, "runtime", playAuthorityHeadFile);
990
+ const authority = await readAuthorityHead(root);
914
991
  if (authority.head !== input.parentHead) {
915
992
  throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
916
993
  }
@@ -959,12 +1036,14 @@ export class FileNativeWorldStore {
959
1036
  ], { requireOpening: false });
960
1037
  if (inspection.status !== "usable")
961
1038
  throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
962
- const sequence = authority.commits.length + 1;
1039
+ const sequence = authority.sequence + 1;
963
1040
  const head = `commit:${sequence}`;
964
1041
  const commit = {
965
- schemaVersion: 1,
1042
+ schemaVersion: 2,
1043
+ sequence,
966
1044
  operationId: input.operationId,
967
1045
  parentHead: input.parentHead,
1046
+ parentCommitDigest: authority.commitDigest,
968
1047
  head,
969
1048
  mode: input.mode,
970
1049
  historyAppend: input.historyAppend.map((message, index) => ({
@@ -985,23 +1064,32 @@ export class FileNativeWorldStore {
985
1064
  }
986
1065
  : {}),
987
1066
  };
1067
+ const digest = playCommitDigest(commit);
988
1068
  crashAtFileNativeAuthorityEdge("before_commit_acceptance");
989
- await publishImmutableJson(join(root, "runtime", "play-commits", `${createHash("sha256").update(JSON.stringify(commit)).digest("hex")}.json`), commit);
990
- await publishJson(authorityPath, {
991
- schemaVersion: 1,
992
- head,
993
- commits: [...authority.commits, commit],
994
- });
995
- crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1069
+ await publishImmutableJson(join(root, "runtime", "play-commits", `${digest}.json`), commit);
996
1070
  const pending = {
997
1071
  outcome: "committed_materialization_pending",
998
1072
  worldId: input.worldId,
999
1073
  parentHead: input.parentHead,
1000
1074
  head,
1075
+ commitDigest: digest,
1001
1076
  historyAppend: structuredClone(input.historyAppend),
1002
1077
  nextAdditionalMaterials: structuredClone(input.nextMaterials),
1003
1078
  mode: input.mode,
1004
1079
  };
1080
+ await publishJson(this.#operationOutcomePath(input.operationId), {
1081
+ ...pending,
1082
+ outcome: "acceptance_prepared",
1083
+ });
1084
+ crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
1085
+ await publishJson(authorityPath, {
1086
+ schemaVersion: 2,
1087
+ head,
1088
+ sequence,
1089
+ commitDigest: digest,
1090
+ operationId: input.operationId,
1091
+ });
1092
+ crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1005
1093
  await publishJson(this.#operationOutcomePath(input.operationId), pending);
1006
1094
  try {
1007
1095
  await materializePlayCommit(root, sequence, {
@@ -1015,7 +1103,10 @@ export class FileNativeWorldStore {
1015
1103
  });
1016
1104
  crashAtFileNativeAuthorityEdge("after_materialization");
1017
1105
  await publishJson(join(root, "runtime", "materialized-head.json"), {
1106
+ schemaVersion: 2,
1018
1107
  head,
1108
+ sequence,
1109
+ commitDigest: digest,
1019
1110
  });
1020
1111
  const outcome = { ...pending, outcome: "committed" };
1021
1112
  await publishJson(this.#operationOutcomePath(input.operationId), outcome);
@@ -1038,6 +1129,7 @@ export class FileNativeWorldStore {
1038
1129
  }
1039
1130
  async readAuthorityHistory(worldId) {
1040
1131
  assertIdentity(worldId, "World ID");
1132
+ await this.ensureCurrentStorage(worldId);
1041
1133
  const root = join(this.#worldsRoot, worldId);
1042
1134
  await readPublicationAt(root);
1043
1135
  const authority = await readAcceptedAuthority(root);
@@ -1222,19 +1314,6 @@ async function readTree(root, prefix = "") {
1222
1314
  }
1223
1315
  return files;
1224
1316
  }
1225
- async function readDirectoryNames(root) {
1226
- try {
1227
- return (await readdir(root, { withFileTypes: true }))
1228
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
1229
- .map((entry) => entry.name)
1230
- .sort();
1231
- }
1232
- catch (error) {
1233
- if (isNodeError(error) && error.code === "ENOENT")
1234
- return [];
1235
- throw error;
1236
- }
1237
- }
1238
1317
  function immutableFiles(files) {
1239
1318
  return files.map(({ path, contents }) => ({
1240
1319
  path,
@@ -1286,13 +1365,15 @@ function assertOperationOutcomeIntegrity(value) {
1286
1365
  throw corruptOperationOutcome();
1287
1366
  return;
1288
1367
  }
1289
- if (value.outcome === "committed" ||
1368
+ if (value.outcome === "acceptance_prepared" ||
1369
+ value.outcome === "committed" ||
1290
1370
  value.outcome === "committed_materialization_pending") {
1291
1371
  if (!hasExactKeys(value, [
1292
1372
  "outcome",
1293
1373
  "worldId",
1294
1374
  "parentHead",
1295
1375
  "head",
1376
+ "commitDigest",
1296
1377
  "historyAppend",
1297
1378
  "nextAdditionalMaterials",
1298
1379
  "mode",
@@ -1303,6 +1384,8 @@ function assertOperationOutcomeIntegrity(value) {
1303
1384
  !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1304
1385
  typeof value.head !== "string" ||
1305
1386
  !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1387
+ typeof value.commitDigest !== "string" ||
1388
+ !/^[a-f0-9]{64}$/u.test(value.commitDigest) ||
1306
1389
  !Array.isArray(value.historyAppend) ||
1307
1390
  !value.historyAppend.every(isPlayHistoryAppendInput) ||
1308
1391
  !Array.isArray(value.nextAdditionalMaterials) ||
@@ -1315,28 +1398,85 @@ function assertOperationOutcomeIntegrity(value) {
1315
1398
  }
1316
1399
  throw corruptOperationOutcome();
1317
1400
  }
1401
+ function isReleasedCommittedOperationOutcome(value) {
1402
+ return (isRecord(value) &&
1403
+ (value.outcome === "committed" ||
1404
+ value.outcome === "committed_materialization_pending") &&
1405
+ !Object.hasOwn(value, "commitDigest") &&
1406
+ typeof value.worldId === "string");
1407
+ }
1318
1408
  function projectOperationOutcome(value) {
1319
1409
  return structuredClone(value);
1320
1410
  }
1411
+ async function readAcceptancePreparedOutcome(path) {
1412
+ const value = await readOptionalJson(path);
1413
+ if (value === null)
1414
+ return null;
1415
+ assertOperationOutcomeIntegrity(value);
1416
+ if (value.outcome !== "acceptance_prepared")
1417
+ return null;
1418
+ return structuredClone(value);
1419
+ }
1420
+ async function assertPreparedOperationMatches(worldsRoot, operationId, prepared, input) {
1421
+ const comparable = {
1422
+ ...structuredClone(prepared),
1423
+ outcome: "committed_materialization_pending",
1424
+ };
1425
+ assertSameOperationOutcome(comparable, input);
1426
+ const commit = await readPlayCommitByDigest(join(worldsRoot, prepared.worldId), prepared.commitDigest);
1427
+ if (commit.operationId !== operationId ||
1428
+ commit.mode !== input.mode ||
1429
+ !isDeepStrictEqual(commit.stateChanges, input.stateChanges) ||
1430
+ !isDeepStrictEqual(commit.timelineRevision, input.timelineRevision))
1431
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different prepared commit payload");
1432
+ }
1433
+ async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
1434
+ const prepared = value;
1435
+ const worldRoot = join(worldsRoot, prepared.worldId);
1436
+ const authority = await readAuthorityHead(worldRoot);
1437
+ if (authority.head === prepared.parentHead)
1438
+ return { outcome: "in_progress" };
1439
+ const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, prepared);
1440
+ if (authority.sequence < commit.sequence)
1441
+ throw corruptOperationOutcome();
1442
+ await readAuthorityTail(worldRoot, authority, {
1443
+ schemaVersion: 2,
1444
+ head: commit.head,
1445
+ sequence: commit.sequence,
1446
+ commitDigest: prepared.commitDigest,
1447
+ }).catch(() => {
1448
+ throw corruptOperationOutcome();
1449
+ });
1450
+ const materialized = await readMaterializedCheckpoint(worldRoot);
1451
+ const outcome = materialized !== null && materialized.sequence >= commit.sequence
1452
+ ? "committed"
1453
+ : "committed_materialization_pending";
1454
+ const resolved = { ...structuredClone(prepared), outcome };
1455
+ if (outcome === "committed_materialization_pending")
1456
+ await assertPendingMaterializationCompatible(worldRoot, operationId, prepared.commitDigest);
1457
+ return resolved;
1458
+ }
1321
1459
  function corruptOperationOutcome() {
1322
1460
  return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1323
1461
  }
1324
1462
  async function assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, outcome) {
1325
- const authority = await readAcceptedAuthority(worldRoot);
1326
- const commit = authority?.commits.find((candidate) => candidate.operationId === operationId);
1327
- if (commit?.parentHead !== outcome.parentHead ||
1463
+ const commit = await readPlayCommitByDigest(worldRoot, outcome.commitDigest);
1464
+ if (commit.operationId !== operationId ||
1465
+ commit?.parentHead !== outcome.parentHead ||
1328
1466
  commit?.head !== outcome.head ||
1329
1467
  commit?.mode !== outcome.mode ||
1330
1468
  !isDeepStrictEqual(commit?.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
1331
1469
  !isDeepStrictEqual(commit?.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
1332
1470
  throw corruptOperationOutcome();
1471
+ return commit;
1333
1472
  }
1334
- async function assertPendingMaterializationCompatible(worldRoot, operationId) {
1335
- const authority = await readAcceptedAuthority(worldRoot);
1336
- const sequence = authority?.commits.findIndex((candidate) => candidate.operationId === operationId) ?? -1;
1337
- const commit = sequence < 0 ? undefined : authority?.commits[sequence];
1338
- if (commit === undefined)
1473
+ async function assertPendingMaterializationCompatible(worldRoot, operationId, commitDigest) {
1474
+ const commit = commitDigest === undefined
1475
+ ? (await readAcceptedAuthority(worldRoot))?.commits.find((candidate) => candidate.operationId === operationId)
1476
+ : await readPlayCommitByDigest(worldRoot, commitDigest);
1477
+ if (commit?.operationId !== operationId)
1339
1478
  throw corruptOperationOutcome();
1479
+ const sequence = commit.sequence - 1;
1340
1480
  for (const change of commit.stateChanges) {
1341
1481
  const path = join(worldRoot, "state", change.relativePath);
1342
1482
  const existing = await readOptionalText(path);
@@ -1430,55 +1570,36 @@ async function publishImmutableJson(path, value) {
1430
1570
  }
1431
1571
  async function readAcceptedAuthority(root) {
1432
1572
  try {
1433
- const published = await readOptionalJson(join(root, "runtime", "play-authority.json"));
1434
- if (published === null) {
1435
- const commitRoot = join(root, "runtime", "play-commits");
1436
- const names = await readDirectoryFileNames(commitRoot);
1437
- if (names.length === 0)
1438
- return null;
1439
- const remaining = await Promise.all(names.map(async (name) => {
1440
- const commit = await readJson(join(commitRoot, name));
1441
- assertFileNativePlayCommit(commit);
1442
- const canonicalName = `${createHash("sha256")
1443
- .update(JSON.stringify(commit))
1444
- .digest("hex")}.json`;
1445
- if (name !== canonicalName)
1446
- throw new FileNativeWorldCreationError("world_corrupt", "Immutable commit does not match the durable path identity");
1447
- return commit;
1448
- }));
1449
- const commits = [];
1450
- let parent = "genesis";
1451
- while (remaining.length > 0) {
1452
- const matches = remaining.filter((commit) => commit.parentHead === parent);
1453
- if (matches.length !== 1)
1454
- throw new FileNativeWorldCreationError("world_corrupt", "Immutable commits cannot rebuild one unique authority chain");
1455
- const next = matches[0];
1456
- commits.push(next);
1457
- remaining.splice(remaining.indexOf(next), 1);
1458
- parent = next.head;
1459
- }
1460
- assertFileNativePlayAuthority({
1461
- schemaVersion: 1,
1462
- head: parent,
1463
- commits,
1464
- });
1465
- return { schemaVersion: 1, head: parent, commits };
1466
- }
1467
- assertFileNativePlayAuthority(published);
1468
- const commits = [];
1469
- for (const recorded of published.commits) {
1470
- const digest = createHash("sha256")
1471
- .update(JSON.stringify(recorded))
1472
- .digest("hex");
1573
+ const published = await readAuthorityHead(root);
1574
+ if (published.sequence === 0)
1575
+ return null;
1576
+ const reverse = [];
1577
+ let digest = published.commitDigest;
1578
+ let expectedHead = published.head;
1579
+ let expectedSequence = published.sequence;
1580
+ while (digest !== null) {
1473
1581
  const immutable = await readJson(join(root, "runtime", "play-commits", `${digest}.json`)).catch((error) => {
1474
- throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint is missing an immutable commit: ${recorded.head}`, { cause: error });
1582
+ throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint is missing an immutable commit: ${expectedHead}`, { cause: error });
1475
1583
  });
1476
1584
  assertFileNativePlayCommit(immutable);
1477
- if (JSON.stringify(immutable) !== JSON.stringify(recorded))
1478
- throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint does not match its immutable commit: ${recorded.head}`);
1479
- commits.push(immutable);
1585
+ if (playCommitDigest(immutable) !== digest ||
1586
+ immutable.head !== expectedHead ||
1587
+ immutable.sequence !== expectedSequence)
1588
+ throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint does not match its immutable commit: ${expectedHead}`);
1589
+ reverse.push(immutable);
1590
+ digest = immutable.parentCommitDigest;
1591
+ expectedHead = immutable.parentHead;
1592
+ expectedSequence -= 1;
1480
1593
  }
1481
- return { ...published, commits };
1594
+ if (expectedSequence !== 0 || expectedHead !== "genesis")
1595
+ throw new FileNativeWorldCreationError("world_corrupt", "Accepted Authority chain does not terminate at genesis");
1596
+ const authority = {
1597
+ schemaVersion: 2,
1598
+ head: published.head,
1599
+ commits: reverse.reverse(),
1600
+ };
1601
+ assertFileNativePlayAuthority(authority);
1602
+ return authority;
1482
1603
  }
1483
1604
  catch (error) {
1484
1605
  if (error instanceof FileNativeWorldCreationError)
@@ -1486,10 +1607,118 @@ async function readAcceptedAuthority(root) {
1486
1607
  throw new FileNativeWorldCreationError("world_corrupt", "Play Authority durable data is corrupt", { cause: error });
1487
1608
  }
1488
1609
  }
1610
+ async function readMaterializedCheckpoint(root) {
1611
+ const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1612
+ if (value === null)
1613
+ return null;
1614
+ if (!isRecord(value) ||
1615
+ !hasExactKeys(value, [
1616
+ "schemaVersion",
1617
+ "head",
1618
+ "sequence",
1619
+ "commitDigest",
1620
+ ]) ||
1621
+ value.schemaVersion !== 2 ||
1622
+ typeof value.head !== "string" ||
1623
+ !Number.isSafeInteger(value.sequence) ||
1624
+ Number(value.sequence) < 0 ||
1625
+ (value.commitDigest !== null &&
1626
+ (typeof value.commitDigest !== "string" ||
1627
+ !/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
1628
+ (Number(value.sequence) === 0
1629
+ ? value.head !== "genesis" || value.commitDigest !== null
1630
+ : value.head !== `commit:${String(value.sequence)}` ||
1631
+ value.commitDigest === null))
1632
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized world checkpoint has an invalid shape");
1633
+ return value;
1634
+ }
1635
+ function sameMaterializedEndpoint(checkpoint, authority) {
1636
+ return (checkpoint !== null &&
1637
+ checkpoint.head === authority.head &&
1638
+ checkpoint.sequence === authority.sequence &&
1639
+ checkpoint.commitDigest === authority.commitDigest);
1640
+ }
1641
+ async function readAuthorityTail(root, authority, checkpoint) {
1642
+ const targetDigest = checkpoint?.commitDigest ?? null;
1643
+ const targetHead = checkpoint?.head ?? "genesis";
1644
+ const targetSequence = checkpoint?.sequence ?? 0;
1645
+ if (targetSequence > authority.sequence)
1646
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized checkpoint is ahead of Authority");
1647
+ const reverse = [];
1648
+ let digest = authority.commitDigest;
1649
+ let expectedHead = authority.head;
1650
+ let expectedSequence = authority.sequence;
1651
+ while (digest !== targetDigest) {
1652
+ if (digest === null)
1653
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized checkpoint is not an ancestor of Authority");
1654
+ const commit = await readPlayCommitByDigest(root, digest);
1655
+ if (commit.head !== expectedHead || commit.sequence !== expectedSequence)
1656
+ throw new FileNativeWorldCreationError("world_corrupt", "Authority tail does not match its immutable commit chain");
1657
+ reverse.push(commit);
1658
+ digest = commit.parentCommitDigest;
1659
+ expectedHead = commit.parentHead;
1660
+ expectedSequence -= 1;
1661
+ }
1662
+ if (expectedHead !== targetHead || expectedSequence !== targetSequence)
1663
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized checkpoint identity does not match Authority ancestry");
1664
+ return reverse.reverse();
1665
+ }
1666
+ function genesisAuthorityHead() {
1667
+ return {
1668
+ schemaVersion: 2,
1669
+ head: "genesis",
1670
+ sequence: 0,
1671
+ commitDigest: null,
1672
+ operationId: null,
1673
+ };
1674
+ }
1675
+ async function readAuthorityHead(root) {
1676
+ const value = await readJson(join(root, "runtime", playAuthorityHeadFile));
1677
+ assertFileNativePlayAuthorityHead(value);
1678
+ return value;
1679
+ }
1680
+ function assertFileNativePlayAuthorityHead(value) {
1681
+ if (!isRecord(value) ||
1682
+ !hasExactKeys(value, [
1683
+ "schemaVersion",
1684
+ "head",
1685
+ "sequence",
1686
+ "commitDigest",
1687
+ "operationId",
1688
+ ]) ||
1689
+ value.schemaVersion !== 2 ||
1690
+ typeof value.head !== "string" ||
1691
+ !Number.isSafeInteger(value.sequence) ||
1692
+ Number(value.sequence) < 0 ||
1693
+ (value.commitDigest !== null &&
1694
+ (typeof value.commitDigest !== "string" ||
1695
+ !/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
1696
+ (value.operationId !== null && typeof value.operationId !== "string") ||
1697
+ (Number(value.sequence) === 0
1698
+ ? value.head !== "genesis" ||
1699
+ value.commitDigest !== null ||
1700
+ value.operationId !== null
1701
+ : value.head !== `commit:${String(value.sequence)}` ||
1702
+ value.commitDigest === null ||
1703
+ value.operationId === null))
1704
+ throw new Error("Play Authority head has an invalid shape");
1705
+ }
1706
+ function playCommitDigest(commit) {
1707
+ return createHash("sha256").update(JSON.stringify(commit)).digest("hex");
1708
+ }
1709
+ async function readPlayCommitByDigest(root, digest) {
1710
+ if (!/^[a-f0-9]{64}$/u.test(digest))
1711
+ throw corruptOperationOutcome();
1712
+ const value = await readJson(join(root, "runtime", "play-commits", `${digest}.json`));
1713
+ assertFileNativePlayCommit(value);
1714
+ if (playCommitDigest(value) !== digest)
1715
+ throw new FileNativeWorldCreationError("world_corrupt", "Immutable play commit does not match its durable identity");
1716
+ return value;
1717
+ }
1489
1718
  function assertFileNativePlayAuthority(value) {
1490
1719
  if (!isRecord(value) ||
1491
1720
  !hasExactKeys(value, ["schemaVersion", "head", "commits"]) ||
1492
- value.schemaVersion !== 1 ||
1721
+ value.schemaVersion !== 2 ||
1493
1722
  typeof value.head !== "string" ||
1494
1723
  !Array.isArray(value.commits))
1495
1724
  throw new Error("Play Authority has an invalid shape");
@@ -1497,6 +1726,7 @@ function assertFileNativePlayAuthority(value) {
1497
1726
  const heads = new Set();
1498
1727
  const commitsByHead = new Map();
1499
1728
  let parent = "genesis";
1729
+ let parentDigest = null;
1500
1730
  for (const [index, commit] of value.commits.entries()) {
1501
1731
  assertFileNativePlayCommit(commit);
1502
1732
  const revision = commit.timelineRevision;
@@ -1507,6 +1737,8 @@ function assertFileNativePlayAuthority(value) {
1507
1737
  ? replaced.timelineRevision.restoresHead
1508
1738
  : replaced?.parentHead;
1509
1739
  if (commit.parentHead !== parent ||
1740
+ commit.parentCommitDigest !== parentDigest ||
1741
+ commit.sequence !== index + 1 ||
1510
1742
  commit.head !== `commit:${index + 1}` ||
1511
1743
  operationIds.has(commit.operationId) ||
1512
1744
  heads.has(commit.head) ||
@@ -1521,6 +1753,7 @@ function assertFileNativePlayAuthority(value) {
1521
1753
  heads.add(commit.head);
1522
1754
  commitsByHead.set(commit.head, commit);
1523
1755
  parent = commit.head;
1756
+ parentDigest = playCommitDigest(commit);
1524
1757
  }
1525
1758
  if (value.head !== parent)
1526
1759
  throw new Error("Play Authority head is invalid");
@@ -1532,8 +1765,10 @@ function assertFileNativePlayCommit(value) {
1532
1765
  const timelineRevision = value.mode === "timeline_revision";
1533
1766
  if (!hasExactKeys(value, [
1534
1767
  "schemaVersion",
1768
+ "sequence",
1535
1769
  "operationId",
1536
1770
  "parentHead",
1771
+ "parentCommitDigest",
1537
1772
  "head",
1538
1773
  "mode",
1539
1774
  "historyAppend",
@@ -1542,13 +1777,20 @@ function assertFileNativePlayCommit(value) {
1542
1777
  ...(correction ? ["correctionTargets", "corrects"] : []),
1543
1778
  ...(timelineRevision ? ["timelineRevision"] : []),
1544
1779
  ]) ||
1545
- value.schemaVersion !== 1 ||
1780
+ value.schemaVersion !== 2 ||
1781
+ !Number.isSafeInteger(value.sequence) ||
1782
+ Number(value.sequence) < 1 ||
1546
1783
  typeof value.operationId !== "string" ||
1547
1784
  value.operationId.trim() === "" ||
1548
1785
  typeof value.parentHead !== "string" ||
1549
1786
  !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1787
+ (value.parentCommitDigest !== null &&
1788
+ (typeof value.parentCommitDigest !== "string" ||
1789
+ !/^[a-f0-9]{64}$/u.test(value.parentCommitDigest))) ||
1790
+ (value.parentHead === "genesis") !== (value.parentCommitDigest === null) ||
1550
1791
  typeof value.head !== "string" ||
1551
1792
  !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1793
+ value.head !== `commit:${String(value.sequence)}` ||
1552
1794
  (value.mode !== "play" &&
1553
1795
  value.mode !== "correction" &&
1554
1796
  value.mode !== "timeline_revision") ||
@@ -1666,19 +1908,6 @@ function isFileNativeStateChange(value) {
1666
1908
  }
1667
1909
  return true;
1668
1910
  }
1669
- async function readDirectoryFileNames(root) {
1670
- try {
1671
- return (await readdir(root, { withFileTypes: true }))
1672
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
1673
- .map((entry) => entry.name)
1674
- .sort();
1675
- }
1676
- catch (error) {
1677
- if (isNodeError(error) && error.code === "ENOENT")
1678
- return [];
1679
- throw error;
1680
- }
1681
- }
1682
1911
  async function materializePlayCommit(root, sequence, input) {
1683
1912
  if (input.mode === "timeline_revision") {
1684
1913
  if (input.timelineRevision === undefined)
@@ -1782,6 +2011,26 @@ function historySurfaceFiles(messages) {
1782
2011
  };
1783
2012
  });
1784
2013
  }
2014
+ function materializedHistoryRecord(worldId, files) {
2015
+ const result = {};
2016
+ for (const file of files) {
2017
+ const match = /^(\d{8})-(\d+)-(player|narrator)-[a-f0-9]{12}\.md$/u.exec(file.path);
2018
+ if (match === null)
2019
+ throw new FileNativeWorldCreationError("world_corrupt", `Materialized history path is invalid: ${file.path}`);
2020
+ const sequence = Number.parseInt(match[1], 10);
2021
+ const index = Number.parseInt(match[2], 10);
2022
+ const role = match[3];
2023
+ const messageId = sequence === 0
2024
+ ? index === 1 && role === "narrator"
2025
+ ? `${worldId}.message.genesis.narrator`
2026
+ : `${worldId}.message.genesis.${index}.${role}`
2027
+ : `${worldId}.message.${sequence}.${index}.${role}`;
2028
+ if (messageId in result)
2029
+ throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
2030
+ result[messageId] = file.contents;
2031
+ }
2032
+ return result;
2033
+ }
1785
2034
  async function writeIdempotentText(path, contents) {
1786
2035
  try {
1787
2036
  const existing = await readFile(path, "utf8");
@@ -1825,11 +2074,13 @@ function cloneAuthorityPrefix(input) {
1825
2074
  ...structuredClone(message),
1826
2075
  messageId: messageIds.get(message.messageId),
1827
2076
  }));
1828
- return {
1829
- genesisHistory: rewriteHistory(input.sourceGenesis.history),
1830
- genesisMaterials: rewriteMaterials(input.sourceGenesis.additionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
1831
- commits: sourceCommits.map((commit) => ({
2077
+ const clonedCommits = [];
2078
+ let parentCommitDigest = null;
2079
+ for (const commit of sourceCommits) {
2080
+ const cloned = {
1832
2081
  ...structuredClone(commit),
2082
+ schemaVersion: 2,
2083
+ parentCommitDigest,
1833
2084
  operationId: `clone-${operationDigest(`${input.targetWorldId}\0${commit.head}`).slice(0, 56)}`,
1834
2085
  historyAppend: rewriteHistory(commit.historyAppend),
1835
2086
  nextAdditionalMaterials: rewriteMaterials(commit.nextAdditionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
@@ -1841,7 +2092,14 @@ function cloneAuthorityPrefix(input) {
1841
2092
  replacementHistory: rewriteHistory(commit.timelineRevision.replacementHistory),
1842
2093
  },
1843
2094
  }),
1844
- })),
2095
+ };
2096
+ clonedCommits.push(cloned);
2097
+ parentCommitDigest = playCommitDigest(cloned);
2098
+ }
2099
+ return {
2100
+ genesisHistory: rewriteHistory(input.sourceGenesis.history),
2101
+ genesisMaterials: rewriteMaterials(input.sourceGenesis.additionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
2102
+ commits: clonedCommits,
1845
2103
  };
1846
2104
  }
1847
2105
  function rewriteMaterials(materials, messageIds, sourceWorldId, targetWorldId) {