narraeon 0.2.0 → 0.3.1

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.
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
2
+ import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, } from "../content/FileNativeContentTree.js";
@@ -9,10 +9,11 @@ import { WorldDocumentStore } from "./WorldDocumentStore.js";
9
9
  import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
10
10
  import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
11
11
  import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
12
+ import { FileNativeAuthorityV3, FileNativeAuthorityV3Error, } from "./FileNativeAuthorityV3.js";
12
13
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
14
+ import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, } from "./FileNativeHistoryProjection.js";
13
15
  const publicationFile = "publication.json";
14
16
  const localMetadataFile = "local.json";
15
- const playAuthorityHeadFile = "play-authority-head.json";
16
17
  export class FileNativeWorldCreationError extends Error {
17
18
  code;
18
19
  constructor(code, message, options) {
@@ -73,15 +74,13 @@ export class FileNativeWorldStore {
73
74
  }
74
75
  async #ensureCurrentStorageSerialized(worldId) {
75
76
  try {
76
- try {
77
- await readAuthorityHead(join(this.#worldsRoot, worldId));
77
+ const root = join(this.#worldsRoot, worldId);
78
+ const authority = new FileNativeAuthorityV3(root);
79
+ if (await authority.exists()) {
80
+ await authority.readHead();
78
81
  this.#currentStorage.add(worldId);
79
82
  return;
80
83
  }
81
- catch (error) {
82
- if (!isNodeError(error) || error.code !== "ENOENT")
83
- throw error;
84
- }
85
84
  await this.operations.withWorldAuthorityLock(worldId, async () => {
86
85
  await this.#storageMigrator.migrate(worldId);
87
86
  });
@@ -216,7 +215,7 @@ export class FileNativeWorldStore {
216
215
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
217
216
  await rm(stagingRoot, { recursive: true, force: true });
218
217
  try {
219
- const openingMessage = genesisOpeningMessage(worldId, openingText(packageFiles));
218
+ const openingMessage = genesisOpeningMessage(openingText(packageFiles));
220
219
  const initialMaterials = [
221
220
  { kind: "history_message", message: openingMessage.messageId },
222
221
  ];
@@ -224,7 +223,7 @@ export class FileNativeWorldStore {
224
223
  await writeSurface(stagingRoot, "control", control);
225
224
  await mkdir(join(stagingRoot, "history"), { recursive: true });
226
225
  await mkdir(join(stagingRoot, "runtime"), { recursive: true });
227
- await writeIdempotentText(join(stagingRoot, "history", `00000000-${historyFileName(1, openingMessage)}`), openingMessage.exactText);
226
+ await writeIdempotentText(join(stagingRoot, "history", historySurfaceFiles([openingMessage])[0].path), openingMessage.exactText);
228
227
  const genesis = {
229
228
  schemaVersion: 1,
230
229
  type: "file_native_genesis",
@@ -242,9 +241,14 @@ export class FileNativeWorldStore {
242
241
  worldId,
243
242
  history: genesis.history,
244
243
  });
245
- await writeJson(join(stagingRoot, "runtime", playAuthorityHeadFile), genesisAuthorityHead());
244
+ await new FileNativeAuthorityV3(stagingRoot).initialize({
245
+ operationId: input.operationId,
246
+ state,
247
+ history: [openingMessage],
248
+ additionalMaterials: initialMaterials,
249
+ });
246
250
  await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
247
- schemaVersion: 2,
251
+ schemaVersion: 3,
248
252
  head: "genesis",
249
253
  sequence: 0,
250
254
  commitDigest: null,
@@ -333,14 +337,14 @@ export class FileNativeWorldStore {
333
337
  await this.ensureCurrentStorage(worldId);
334
338
  const root = join(this.#worldsRoot, worldId);
335
339
  await readPublicationAt(root);
336
- return (await readAuthorityHead(root)).head;
340
+ return (await new FileNativeAuthorityV3(root).readHead()).head;
337
341
  }
338
342
  async currentHeadOperationId(worldId) {
339
343
  assertIdentity(worldId, "World ID");
340
344
  await this.ensureCurrentStorage(worldId);
341
345
  const root = join(this.#worldsRoot, worldId);
342
346
  await readPublicationAt(root);
343
- return (await readAuthorityHead(root)).operationId;
347
+ return new FileNativeAuthorityV3(root).currentOperationId();
344
348
  }
345
349
  async saveControlDraft(worldId, files) {
346
350
  assertIdentity(worldId, "World ID");
@@ -473,37 +477,28 @@ export class FileNativeWorldStore {
473
477
  }
474
478
  try {
475
479
  return await this.operations.withWorldAuthorityLock(worldId, async () => {
476
- const authority = await readAuthorityHead(root);
480
+ const authorityStore = new FileNativeAuthorityV3(root);
481
+ const authority = await authorityStore.readHead();
477
482
  const checkpoint = await readMaterializedCheckpoint(root);
478
483
  if (!sameMaterializedEndpoint(checkpoint, authority)) {
479
- const missing = await readAuthorityTail(root, authority, checkpoint);
480
- for (const commit of missing) {
481
- await materializePlayCommit(root, commit.sequence, {
482
- mode: commit.mode,
483
- historyAppend: commit.historyAppend,
484
- nextMaterials: commit.nextAdditionalMaterials,
485
- stateChanges: commit.stateChanges,
486
- ...(commit.timelineRevision === undefined
487
- ? {}
488
- : { timelineRevision: commit.timelineRevision }),
489
- });
490
- await publishJson(this.#operationOutcomePath(commit.operationId), {
491
- outcome: "committed",
492
- worldId,
493
- parentHead: commit.parentHead,
494
- head: commit.head,
495
- commitDigest: playCommitDigest(commit),
496
- historyAppend: commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })),
497
- nextAdditionalMaterials: commit.nextAdditionalMaterials,
498
- mode: commit.mode,
499
- });
484
+ const recovered = await authorityStore.recover();
485
+ let operation = null;
486
+ if (authority.operationId !== null && authority.sequence > 0) {
487
+ const candidate = await this.getOperationOutcome(authority.operationId);
488
+ if (isCommittedOutcome(candidate)) {
489
+ if (candidate.worldId !== worldId ||
490
+ candidate.head !== authority.head ||
491
+ candidate.commitDigest !== authority.commitDigest)
492
+ throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head is bound to a different operation receipt");
493
+ operation = candidate;
494
+ }
495
+ else if (candidate.outcome !== "not_started") {
496
+ throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head has no recoverable acceptance receipt");
497
+ }
500
498
  }
501
- await publishJson(join(root, "runtime", "materialized-head.json"), {
502
- schemaVersion: 2,
503
- head: authority.head,
504
- sequence: authority.sequence,
505
- commitDigest: authority.commitDigest,
506
- });
499
+ await materializeRecoveredEndpoint(root, authority, recovered);
500
+ if (operation !== null)
501
+ await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
507
502
  }
508
503
  const [state, control, history, materials] = await Promise.all([
509
504
  readTree(join(root, "state")),
@@ -522,7 +517,7 @@ export class FileNativeWorldStore {
522
517
  return files;
523
518
  }, {}),
524
519
  additionalMaterials: materials?.items ?? [],
525
- history: materializedHistoryRecord(worldId, history),
520
+ history: materializedHistoryRecord(history),
526
521
  };
527
522
  });
528
523
  }
@@ -566,62 +561,21 @@ export class FileNativeWorldStore {
566
561
  await this.ensureCurrentStorage(worldId);
567
562
  const root = join(this.#worldsRoot, worldId);
568
563
  await readPublicationAt(root);
569
- const genesis = await readJson(join(root, "runtime", "genesis.json"));
570
- for (const file of [...genesis.state, ...genesis.control])
571
- if (sha256(file.canonicalBytes) !== file.sha256)
572
- throw new FileNativeWorldCreationError("world_corrupt", `Genesis file hash does not match: ${file.path}`);
573
- const authority = await readAcceptedAuthority(root);
574
- const target = head ?? authority?.head ?? "genesis";
575
- const commits = authority?.commits ?? [];
576
- const end = target === "genesis"
577
- ? 0
578
- : commits.findIndex((commit) => commit.head === target) + 1;
579
- if (target !== "genesis" && end === 0)
580
- throw new FileNativeWorldCreationError("world_corrupt", `Cannot rebuild endpoint from immutable authority chain: ${target}`);
581
- const state = new Map(genesis.state.map((file) => [file.path, file.canonicalBytes]));
582
- const history = structuredClone(genesis.history);
583
- let additionalMaterials = genesis.additionalMaterials;
584
- for (const commit of commits.slice(0, end)) {
585
- if (commit.mode === "timeline_revision") {
586
- const revision = commit.timelineRevision;
587
- if (revision === undefined)
588
- throw new FileNativeWorldCreationError("world_corrupt", "Timeline-revision commit is missing its recovery snapshot");
589
- const restored = await this.recoverEndpoint(worldId, revision.restoresHead);
590
- if (!isDeepStrictEqual(revision.replacementState, immutableFiles(restored.state)) ||
591
- !isDeepStrictEqual(revision.replacementHistory, restored.history) ||
592
- !isDeepStrictEqual(commit.nextAdditionalMaterials, restored.additionalMaterials))
593
- throw new FileNativeWorldCreationError("world_corrupt", "Timeline-revision recovery snapshot does not match its logical parent endpoint");
594
- state.clear();
595
- for (const file of revision.replacementState) {
596
- if (sha256(file.canonicalBytes) !== file.sha256)
597
- throw new FileNativeWorldCreationError("world_corrupt", `Timeline-revision state hash does not match: ${file.path}`);
598
- state.set(file.path, file.canonicalBytes);
599
- }
600
- history.splice(0, history.length, ...structuredClone(revision.replacementHistory));
601
- }
602
- else {
603
- for (const change of commit.stateChanges) {
604
- const existing = state.get(change.relativePath);
605
- const existingHash = existing === undefined ? null : sha256(existing);
606
- if (existingHash !== change.expectedPreviousHash)
607
- throw new FileNativeWorldCreationError("world_corrupt", `Immutable commit-chain previous hash does not match: ${change.relativePath}`);
608
- if (sha256(change.canonicalNextBytes) !== change.nextHash)
609
- throw new FileNativeWorldCreationError("world_corrupt", `Immutable commit-chain next hash does not match: ${change.relativePath}`);
610
- state.set(change.relativePath, change.canonicalNextBytes);
611
- }
612
- }
613
- history.push(...structuredClone(commit.historyAppend));
614
- additionalMaterials = structuredClone(commit.nextAdditionalMaterials);
564
+ try {
565
+ const recovered = await new FileNativeAuthorityV3(root).recover(head);
566
+ return {
567
+ worldId,
568
+ head: recovered.head,
569
+ state: recovered.state,
570
+ history: recovered.history,
571
+ additionalMaterials: recovered.additionalMaterials,
572
+ };
573
+ }
574
+ catch (error) {
575
+ if (error instanceof FileNativeWorldCreationError)
576
+ throw error;
577
+ throw new FileNativeWorldCreationError("world_corrupt", `Cannot recover immutable Authority endpoint${head === undefined ? "" : `: ${head}`}`, error instanceof Error ? { cause: error } : undefined);
615
578
  }
616
- return {
617
- worldId,
618
- head: target,
619
- state: [...state]
620
- .map(([path, contents]) => ({ path, contents }))
621
- .sort((a, b) => a.path.localeCompare(b.path)),
622
- history,
623
- additionalMaterials,
624
- };
625
579
  }
626
580
  async repairMaterialization(worldId) {
627
581
  assertIdentity(worldId, "World ID");
@@ -630,8 +584,7 @@ export class FileNativeWorldStore {
630
584
  const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
631
585
  await this.bindPlayCallChain(worldId);
632
586
  const root = join(this.#worldsRoot, worldId);
633
- const authority = await readAcceptedAuthority(root);
634
- return authority?.commits.at(-1)?.operationId ?? null;
587
+ return (await new FileNativeAuthorityV3(root).readHead()).operationId;
635
588
  });
636
589
  return operationId === null
637
590
  ? { outcome: "not_started" }
@@ -679,111 +632,111 @@ export class FileNativeWorldStore {
679
632
  };
680
633
  }
681
634
  await this.ensureCurrentStorage(input.sourceWorldId);
682
- const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
683
- const sourcePublication = await readPublicationAt(sourceRoot);
684
- const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
685
- const [sourceGenesis, selected, sourceAuthority, control] = await Promise.all([
686
- this.recoverEndpoint(input.sourceWorldId, "genesis"),
687
- this.recoverEndpoint(input.sourceWorldId, input.sourceHead),
688
- readAcceptedAuthority(sourceRoot),
689
- readTree(join(sourceRoot, "control")),
690
- ]);
691
- const cloned = cloneAuthorityPrefix({
692
- sourceWorldId: input.sourceWorldId,
693
- sourceHead: input.sourceHead,
694
- targetWorldId: worldId,
695
- sourceGenesis,
696
- sourceAuthority,
697
- });
698
- const staging = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
699
- await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
700
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
701
- try {
702
- await writeSurface(staging, "state", sourceGenesis.state);
703
- await writeSurface(staging, "control", control);
704
- await mkdir(join(staging, "history"), { recursive: true });
705
- await mkdir(join(staging, "runtime"), { recursive: true });
706
- for (const [index, message] of cloned.genesisHistory.entries())
707
- await writeIdempotentText(join(staging, "history", `00000000-${historyFileName(index + 1, message)}`), message.exactText);
708
- const genesis = {
709
- schemaVersion: 1,
710
- type: "file_native_genesis",
711
- worldId,
712
- operationId: input.operationId,
713
- parentEndpoint: "genesis",
714
- state: immutableFiles(sourceGenesis.state),
715
- control: immutableFiles(control),
716
- history: cloned.genesisHistory,
717
- additionalMaterials: cloned.genesisMaterials,
718
- };
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
- });
725
- await writeJson(join(staging, "runtime", "additional-materials.json"), {
726
- head: "genesis",
727
- items: cloned.genesisMaterials,
728
- });
729
- for (const [index, commit] of cloned.commits.entries()) {
730
- const digest = playCommitDigest(commit);
731
- await publishImmutableJson(join(staging, "runtime", "play-commits", `${digest}.json`), commit);
732
- await materializePlayCommit(staging, index + 1, {
733
- mode: commit.mode,
734
- historyAppend: commit.historyAppend,
735
- nextMaterials: commit.nextAdditionalMaterials,
736
- stateChanges: commit.stateChanges,
737
- ...(commit.timelineRevision === undefined
738
- ? {}
739
- : { timelineRevision: commit.timelineRevision }),
635
+ const retainSourceSnapshot = async () => {
636
+ const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
637
+ const sourcePublication = await readPublicationAt(sourceRoot);
638
+ const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
639
+ const sourceAuthority = new FileNativeAuthorityV3(sourceRoot);
640
+ const [sourceGenesis, control] = await Promise.all([
641
+ readGenesisForFork(sourceRoot, input.sourceWorldId),
642
+ readTree(join(sourceRoot, "control")),
643
+ ]);
644
+ const staging = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
645
+ await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
646
+ await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
647
+ try {
648
+ await mkdir(join(staging, "runtime"), { recursive: true });
649
+ const clonedHead = await sourceAuthority.clonePrefixTo({
650
+ targetWorldRoot: staging,
651
+ selectedHead: input.sourceHead,
652
+ operationId: input.operationId,
740
653
  });
741
- }
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,
654
+ const selected = await new FileNativeAuthorityV3(staging).recoverHeadResult();
655
+ await writeSurface(staging, "state", selected.state);
656
+ await writeSurface(staging, "control", control);
657
+ await mkdir(join(staging, "history"), { recursive: true });
658
+ for (const file of historySurfaceFiles(selected.history))
659
+ await writeIdempotentText(join(staging, "history", file.path), file.contents);
660
+ const genesis = {
661
+ schemaVersion: 1,
662
+ type: "file_native_genesis",
663
+ worldId,
664
+ operationId: input.operationId,
665
+ parentEndpoint: "genesis",
666
+ state: structuredClone(sourceGenesis.state),
667
+ control: immutableFiles(control),
668
+ history: structuredClone(sourceGenesis.history),
669
+ additionalMaterials: structuredClone(sourceGenesis.additionalMaterials),
670
+ };
671
+ await writeJson(join(staging, "runtime", "genesis.json"), genesis);
672
+ await writeJson(join(staging, "runtime", "play-genesis-timeline.json"), {
673
+ schemaVersion: 1,
674
+ worldId,
675
+ history: genesis.history,
676
+ });
677
+ await writeJson(join(staging, "runtime", "additional-materials.json"), {
678
+ head: input.sourceHead,
679
+ items: selected.additionalMaterials,
680
+ });
681
+ await writeJson(join(staging, "runtime", "materialized-head.json"), {
682
+ schemaVersion: 3,
683
+ head: input.sourceHead,
748
684
  sequence: clonedHead.sequence,
749
- commitDigest: playCommitDigest(clonedHead),
750
- operationId: clonedHead.operationId,
685
+ commitDigest: clonedHead.commitDigest,
751
686
  });
752
- await writeJson(join(staging, "runtime", "materialized-head.json"), {
753
- schemaVersion: 2,
754
- head: input.sourceHead,
755
- sequence: clonedHead?.sequence ?? 0,
756
- commitDigest: clonedHead === undefined ? null : playCommitDigest(clonedHead),
757
- });
758
- const materializedState = await readTree(join(staging, "state"));
759
- if (!isDeepStrictEqual(materializedState, selected.state))
760
- throw new FileNativeWorldCreationError("world_corrupt", "Fork Authority prefix cannot rebuild the source endpoint state");
761
- const publication = {
762
- schemaVersion: 1,
763
- worldId,
764
- title: derivedWorldName(sourceSummary.title),
765
- parentEndpoint: "genesis",
766
- operationId: input.operationId,
767
- sourceFingerprint: requestFingerprint,
768
- };
769
- await writeJson(join(staging, publicationFile), publication);
770
- await durableTree(staging);
771
- crashAtFileNativeAuthorityEdge("derivation_before_publish");
772
- await rename(staging, finalRoot);
773
- await syncDirectory(this.#worldsRoot);
774
- crashAtFileNativeAuthorityEdge("derivation_after_publish");
775
- await publishJson(operationPath, publication);
776
- return { outcome: "derived", world: toSummary(publication) };
687
+ await input.stageTarget?.({
688
+ targetWorldRoot: staging,
689
+ targetWorldId: worldId,
690
+ binding: {
691
+ worldId,
692
+ parentHead: input.sourceHead,
693
+ files: [...selected.state, ...control].reduce((files, { path, contents }, index) => {
694
+ files[`${index < selected.state.length ? "state" : "control"}/${path}`] = contents;
695
+ return files;
696
+ }, {}),
697
+ additionalMaterials: structuredClone(selected.additionalMaterials),
698
+ history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
699
+ messageId,
700
+ exactText,
701
+ ])),
702
+ },
703
+ });
704
+ crashAtFileNativeAuthorityEdge("derivation_after_target_staging");
705
+ const publication = {
706
+ schemaVersion: 1,
707
+ worldId,
708
+ title: derivedWorldName(sourceSummary.title),
709
+ parentEndpoint: "genesis",
710
+ operationId: input.operationId,
711
+ sourceFingerprint: requestFingerprint,
712
+ };
713
+ await writeJson(join(staging, publicationFile), publication);
714
+ await durableTree(staging);
715
+ crashAtFileNativeAuthorityEdge("derivation_before_publish");
716
+ await rename(staging, finalRoot);
717
+ await syncDirectory(this.#worldsRoot);
718
+ crashAtFileNativeAuthorityEdge("derivation_after_publish");
719
+ await publishJson(operationPath, publication);
720
+ return { outcome: "derived", world: toSummary(publication) };
721
+ }
722
+ catch (error) {
723
+ await rm(staging, { recursive: true, force: true }).catch(() => undefined);
724
+ const published = await readOptionalJson(join(finalRoot, publicationFile));
725
+ if (published !== null) {
726
+ if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
727
+ throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
728
+ await publishJson(operationPath, published);
729
+ return { outcome: "derived", world: toSummary(published) };
730
+ }
731
+ throw error;
732
+ }
733
+ };
734
+ try {
735
+ return await this.operations.withExclusiveWorldStateMutation(input.sourceWorldId, async () => this.operations.withWorldAuthorityLock(input.sourceWorldId, retainSourceSnapshot));
777
736
  }
778
737
  catch (error) {
779
- await rm(staging, { recursive: true, force: true }).catch(() => undefined);
780
- const published = await readOptionalJson(join(finalRoot, publicationFile));
781
- if (published !== null) {
782
- if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
783
- throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
784
- await publishJson(operationPath, published);
785
- return { outcome: "derived", world: toSummary(published) };
786
- }
738
+ if (error instanceof WorldOperationBusyError)
739
+ throw new FileNativeWorldCreationError("operation_conflict", "The source world is changing and cannot provide one stable fork snapshot", { cause: error });
787
740
  throw error;
788
741
  }
789
742
  }
@@ -801,7 +754,11 @@ export class FileNativeWorldStore {
801
754
  let directRecord;
802
755
  try {
803
756
  let value = await readOptionalJson(this.#operationOutcomePath(operationId));
804
- if (isReleasedCommittedOperationOutcome(value)) {
757
+ if (isRecord(value) &&
758
+ (value.outcome === "acceptance_prepared" ||
759
+ value.outcome === "committed" ||
760
+ value.outcome === "committed_materialization_pending") &&
761
+ typeof value.worldId === "string") {
805
762
  await this.ensureCurrentStorage(value.worldId);
806
763
  value = await readOptionalJson(this.#operationOutcomePath(operationId));
807
764
  }
@@ -823,11 +780,11 @@ export class FileNativeWorldStore {
823
780
  if (direct?.outcome === "committed" ||
824
781
  direct?.outcome === "committed_materialization_pending") {
825
782
  const worldRoot = join(this.#worldsRoot, direct.worldId);
826
- const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, direct);
783
+ const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, direct);
827
784
  const materialized = await readMaterializedCheckpoint(worldRoot);
828
785
  if (materialized !== null && materialized.sequence >= commit.sequence)
829
786
  return { ...direct, outcome: "committed" };
830
- await assertPendingMaterializationCompatible(worldRoot, operationId, direct.commitDigest);
787
+ await assertPendingMaterializationCompatible(worldRoot, direct);
831
788
  return { ...direct, outcome: "committed_materialization_pending" };
832
789
  }
833
790
  return direct?.outcome === "in_progress"
@@ -854,9 +811,10 @@ export class FileNativeWorldStore {
854
811
  await this.ensureCurrentStorage(input.worldId);
855
812
  const existing = await this.getOperationOutcome(input.operationId);
856
813
  if (isCommittedOutcome(existing)) {
857
- const authority = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
858
- const commit = authority?.commits.find(({ operationId }) => operationId === input.operationId);
814
+ const fact = await new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId)).authorityFactAt(existing.head);
815
+ const commit = fact.commit;
859
816
  if (existing.worldId !== input.worldId ||
817
+ fact.digest !== existing.commitDigest ||
860
818
  commit?.mode !== "timeline_revision" ||
861
819
  commit.timelineRevision?.restoresHead !== input.restoresHead ||
862
820
  commit.timelineRevision.replacesHead !== input.replacesHead ||
@@ -870,15 +828,13 @@ export class FileNativeWorldStore {
870
828
  return existing;
871
829
  }
872
830
  const root = join(this.#worldsRoot, input.worldId);
873
- const [authority, replacement] = await Promise.all([
874
- readAcceptedAuthority(root),
875
- this.recoverEndpoint(input.worldId, input.restoresHead),
831
+ const authorityStore = new FileNativeAuthorityV3(root);
832
+ const [replaced, replacementMaterials] = await Promise.all([
833
+ authorityStore.commitAt(input.replacesHead),
834
+ authorityStore.recoverMaterials(input.restoresHead),
876
835
  ]);
877
- const replaced = authority?.commits.find(({ head }) => head === input.replacesHead);
878
- const logicalParent = replaced?.mode === "timeline_revision"
879
- ? replaced.timelineRevision?.restoresHead
880
- : replaced?.parentHead;
881
- if (replaced === undefined ||
836
+ const logicalParent = replaced?.timelineParent.head;
837
+ if (replaced === null ||
882
838
  !replaced.historyAppend.some(({ role }) => role === "player") ||
883
839
  logicalParent !== input.restoresHead)
884
840
  throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
@@ -887,15 +843,13 @@ export class FileNativeWorldStore {
887
843
  worldId: input.worldId,
888
844
  parentHead: input.expectedCurrentHead,
889
845
  historyAppend: [{ role: "player", exactText: input.replacementText }],
890
- nextMaterials: structuredClone(replacement.additionalMaterials),
846
+ nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
891
847
  stateChanges: [],
892
848
  mode: "timeline_revision",
893
849
  timelineRevision: {
894
850
  restoresHead: input.restoresHead,
895
851
  replacesHead: input.replacesHead,
896
852
  requestFingerprint: input.requestFingerprint,
897
- replacementState: immutableFiles(replacement.state),
898
- replacementHistory: structuredClone(replacement.history),
899
853
  },
900
854
  });
901
855
  }
@@ -913,7 +867,7 @@ export class FileNativeWorldStore {
913
867
  operationId: input.operationId,
914
868
  worldId: input.worldId,
915
869
  parentHead: input.parentHead,
916
- nextMaterials: input.nextMaterials,
870
+ nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
917
871
  stateChanges: input.stateChanges,
918
872
  mode: "correction",
919
873
  historyAppend: [],
@@ -941,6 +895,7 @@ export class FileNativeWorldStore {
941
895
  throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
942
896
  return this.#commitHistoryChange({
943
897
  ...input,
898
+ nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
944
899
  mode: "play",
945
900
  });
946
901
  }
@@ -948,19 +903,23 @@ export class FileNativeWorldStore {
948
903
  const existing = await this.getOperationOutcome(input.operationId);
949
904
  if (isCommittedOutcome(existing)) {
950
905
  assertSameOperationOutcome(existing, input);
951
- const commit = await readPlayCommitByDigest(join(this.#worldsRoot, existing.worldId), existing.commitDigest);
952
- if (commit?.mode !== input.mode ||
953
- JSON.stringify(commit?.stateChanges) !==
954
- JSON.stringify(input.stateChanges) ||
955
- !isDeepStrictEqual(commit?.timelineRevision, input.timelineRevision))
906
+ const authority = new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId));
907
+ const fact = await authority.authorityFactAt(existing.head);
908
+ if (fact.digest !== existing.commitDigest ||
909
+ !authorityCommitMatchesInput(fact.commit, input))
956
910
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
957
911
  return existing;
958
912
  }
959
913
  const prepared = existing.outcome === "in_progress"
960
914
  ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
961
915
  : null;
962
- if (prepared !== null)
963
- await assertPreparedOperationMatches(this.#worldsRoot, input.operationId, prepared, input);
916
+ if (prepared !== null) {
917
+ const comparable = {
918
+ ...structuredClone(prepared),
919
+ outcome: "committed_materialization_pending",
920
+ };
921
+ assertSameOperationOutcome(comparable, input);
922
+ }
964
923
  if (existing.outcome !== "not_started" &&
965
924
  !(existing.outcome === "in_progress" &&
966
925
  (input.operationReserved === true || prepared !== null)))
@@ -971,10 +930,9 @@ export class FileNativeWorldStore {
971
930
  const concurrent = await this.#readOperationOutcome(input.operationId);
972
931
  if (isCommittedOutcome(concurrent)) {
973
932
  assertSameOperationOutcome(concurrent, input);
974
- const knownCommit = await readPlayCommitByDigest(join(this.#worldsRoot, concurrent.worldId), concurrent.commitDigest);
975
- if (knownCommit?.mode !== input.mode ||
976
- !isDeepStrictEqual(knownCommit.stateChanges, input.stateChanges) ||
977
- !isDeepStrictEqual(knownCommit.timelineRevision, input.timelineRevision))
933
+ const known = await new FileNativeAuthorityV3(join(this.#worldsRoot, concurrent.worldId)).authorityFactAt(concurrent.head);
934
+ if (known.digest !== concurrent.commitDigest ||
935
+ !authorityCommitMatchesInput(known.commit, input))
978
936
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
979
937
  return concurrent;
980
938
  }
@@ -982,34 +940,30 @@ export class FileNativeWorldStore {
982
940
  ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
983
941
  : null;
984
942
  if (concurrentPrepared !== null)
985
- await assertPreparedOperationMatches(this.#worldsRoot, input.operationId, concurrentPrepared, input);
943
+ assertSameOperationOutcome({
944
+ ...structuredClone(concurrentPrepared),
945
+ outcome: "committed_materialization_pending",
946
+ }, input);
986
947
  else if (concurrent.outcome === "in_progress" &&
987
948
  input.operationReserved !== true)
988
949
  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);
950
+ const authorityStore = new FileNativeAuthorityV3(root);
951
+ const authority = await authorityStore.readHead();
991
952
  if (authority.head !== input.parentHead) {
992
953
  throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
993
954
  }
994
955
  const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
995
956
  if ((materialized?.head ?? "genesis") !== authority.head)
996
957
  throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
997
- const [currentState, currentControl] = await Promise.all([
998
- readTree(join(root, "state")),
958
+ const [basisState, currentControl] = await Promise.all([
959
+ authorityStore.recoverState(input.timelineRevision?.restoresHead ?? authority.head),
999
960
  readTree(join(root, "control")),
1000
961
  ]);
1001
- const candidateState = new Map(currentState.map((file) => [file.path, file.contents]));
962
+ const candidateState = new Map(basisState.map((file) => [file.path, file.contents]));
1002
963
  if (input.mode === "timeline_revision") {
1003
964
  if (input.timelineRevision === undefined ||
1004
965
  input.stateChanges.length !== 0)
1005
966
  throw new FileNativeWorldCreationError("operation_conflict", "Timeline-revision commit payload is invalid");
1006
- candidateState.clear();
1007
- for (const file of input.timelineRevision.replacementState) {
1008
- assertRelativePath(file.path);
1009
- if (sha256(file.canonicalBytes) !== file.sha256)
1010
- throw new FileNativeWorldCreationError("operation_conflict", `Timeline-revision state hash conflicts: ${file.path}`);
1011
- candidateState.set(file.path, file.canonicalBytes);
1012
- }
1013
967
  }
1014
968
  else {
1015
969
  if (input.timelineRevision !== undefined)
@@ -1036,43 +990,41 @@ export class FileNativeWorldStore {
1036
990
  ], { requireOpening: false });
1037
991
  if (inspection.status !== "usable")
1038
992
  throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
1039
- const sequence = authority.sequence + 1;
1040
- const head = `commit:${sequence}`;
1041
- const commit = {
1042
- schemaVersion: 2,
1043
- sequence,
993
+ if (concurrentPrepared === null)
994
+ await authorityStore.discardUnacceptedNextEpoch();
995
+ crashAtFileNativeAuthorityEdge("before_commit_acceptance");
996
+ const preparedAppend = await authorityStore.prepareAppend({
1044
997
  operationId: input.operationId,
1045
998
  parentHead: input.parentHead,
1046
- parentCommitDigest: authority.commitDigest,
1047
- head,
999
+ ...(input.timelineRevision === undefined
1000
+ ? {}
1001
+ : { timelineParentHead: input.timelineRevision.restoresHead }),
1048
1002
  mode: input.mode,
1049
- historyAppend: input.historyAppend.map((message, index) => ({
1050
- messageId: `${input.worldId}.message.${sequence}.${index + 1}.${message.role}`,
1051
- ...structuredClone(message),
1052
- })),
1003
+ historyAppend: input.historyAppend,
1053
1004
  stateChanges: structuredClone(input.stateChanges),
1054
- nextAdditionalMaterials: input.nextMaterials,
1005
+ nextMaterials: input.nextMaterials,
1055
1006
  ...(input.mode === "correction"
1056
1007
  ? {
1057
1008
  correctionTargets: input.stateChanges.map(({ documentId }) => documentId),
1058
1009
  corrects: input.parentHead,
1059
1010
  }
1060
1011
  : {}),
1061
- ...(input.mode === "timeline_revision"
1062
- ? {
1063
- timelineRevision: structuredClone(input.timelineRevision),
1064
- }
1065
- : {}),
1066
- };
1067
- const digest = playCommitDigest(commit);
1068
- crashAtFileNativeAuthorityEdge("before_commit_acceptance");
1069
- await publishImmutableJson(join(root, "runtime", "play-commits", `${digest}.json`), commit);
1012
+ ...(input.timelineRevision === undefined
1013
+ ? {}
1014
+ : { timelineRevision: input.timelineRevision }),
1015
+ });
1016
+ crashAtFileNativeAuthorityEdge("after_authority_objects");
1017
+ const acceptedPreparation = concurrentPrepared ?? prepared;
1018
+ if (acceptedPreparation !== null &&
1019
+ acceptedPreparation.commitDigest !== preparedAppend.commitDigest)
1020
+ throw new FileNativeWorldCreationError("operation_conflict", "The prepared Authority fact does not match the reserved operation");
1021
+ const { commit } = preparedAppend;
1070
1022
  const pending = {
1071
1023
  outcome: "committed_materialization_pending",
1072
1024
  worldId: input.worldId,
1073
1025
  parentHead: input.parentHead,
1074
- head,
1075
- commitDigest: digest,
1026
+ head: commit.head,
1027
+ commitDigest: preparedAppend.commitDigest,
1076
1028
  historyAppend: structuredClone(input.historyAppend),
1077
1029
  nextAdditionalMaterials: structuredClone(input.nextMaterials),
1078
1030
  mode: input.mode,
@@ -1082,32 +1034,14 @@ export class FileNativeWorldStore {
1082
1034
  outcome: "acceptance_prepared",
1083
1035
  });
1084
1036
  crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
1085
- await publishJson(authorityPath, {
1086
- schemaVersion: 2,
1087
- head,
1088
- sequence,
1089
- commitDigest: digest,
1090
- operationId: input.operationId,
1091
- });
1037
+ await authorityStore.publishPrepared(preparedAppend);
1092
1038
  crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1093
1039
  await publishJson(this.#operationOutcomePath(input.operationId), pending);
1094
1040
  try {
1095
- await materializePlayCommit(root, sequence, {
1096
- mode: commit.mode,
1097
- historyAppend: commit.historyAppend,
1098
- nextMaterials: input.nextMaterials,
1099
- stateChanges: input.stateChanges,
1100
- ...(commit.timelineRevision === undefined
1101
- ? {}
1102
- : { timelineRevision: commit.timelineRevision }),
1103
- });
1041
+ const recovered = await authorityStore.recover(commit.head);
1042
+ await assertPendingMaterializationCompatible(root, pending);
1043
+ await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered);
1104
1044
  crashAtFileNativeAuthorityEdge("after_materialization");
1105
- await publishJson(join(root, "runtime", "materialized-head.json"), {
1106
- schemaVersion: 2,
1107
- head,
1108
- sequence,
1109
- commitDigest: digest,
1110
- });
1111
1045
  const outcome = { ...pending, outcome: "committed" };
1112
1046
  await publishJson(this.#operationOutcomePath(input.operationId), outcome);
1113
1047
  return outcome;
@@ -1124,6 +1058,8 @@ export class FileNativeWorldStore {
1124
1058
  catch (error) {
1125
1059
  if (error instanceof WorldOperationBusyError)
1126
1060
  throw new FileNativeWorldCreationError("operation_conflict", "World Authority commit is being serialized by another process");
1061
+ if (error instanceof FileNativeAuthorityV3Error)
1062
+ throw new FileNativeWorldCreationError(error.code === "corrupt" ? "world_corrupt" : "operation_conflict", error.message, { cause: error });
1127
1063
  throw error;
1128
1064
  }
1129
1065
  }
@@ -1132,10 +1068,8 @@ export class FileNativeWorldStore {
1132
1068
  await this.ensureCurrentStorage(worldId);
1133
1069
  const root = join(this.#worldsRoot, worldId);
1134
1070
  await readPublicationAt(root);
1135
- const authority = await readAcceptedAuthority(root);
1136
- return authority === null
1137
- ? { head: "genesis", commits: [] }
1138
- : { head: authority.head, commits: structuredClone(authority.commits) };
1071
+ const authority = await new FileNativeAuthorityV3(root).readHistory();
1072
+ return structuredClone(authority);
1139
1073
  }
1140
1074
  async readAuthorityEndpoint(worldId, endpoint) {
1141
1075
  const recovered = await this.recoverEndpoint(worldId, endpoint);
@@ -1207,7 +1141,7 @@ export class FileNativeWorldStore {
1207
1141
  return join(this.#operationsRoot, `derive-${operationDigest(operationId)}.json`);
1208
1142
  }
1209
1143
  #preview(input, worldId) {
1210
- const openingMessage = genesisOpeningMessage(worldId, openingText(input.packageFiles));
1144
+ const openingMessage = genesisOpeningMessage(openingText(input.packageFiles));
1211
1145
  return this.#promptCompiler.preview({
1212
1146
  endpoint: { id: `${worldId}:genesis`, commit: "genesis" },
1213
1147
  hostBinding: input.prompt.hostBinding,
@@ -1240,9 +1174,9 @@ function openingText(files) {
1240
1174
  throw new FileNativeWorldCreationError("content_package_needs_repair", "Content package is missing a usable opening.md");
1241
1175
  return opening.contents;
1242
1176
  }
1243
- function genesisOpeningMessage(worldId, exactText) {
1177
+ function genesisOpeningMessage(exactText) {
1244
1178
  return {
1245
- messageId: `${worldId}.message.genesis.narrator`,
1179
+ messageId: "message.genesis.narrator",
1246
1180
  role: "narrator",
1247
1181
  exactText,
1248
1182
  };
@@ -1398,13 +1332,6 @@ function assertOperationOutcomeIntegrity(value) {
1398
1332
  }
1399
1333
  throw corruptOperationOutcome();
1400
1334
  }
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
- }
1408
1335
  function projectOperationOutcome(value) {
1409
1336
  return structuredClone(value);
1410
1337
  }
@@ -1417,94 +1344,86 @@ async function readAcceptancePreparedOutcome(path) {
1417
1344
  return null;
1418
1345
  return structuredClone(value);
1419
1346
  }
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
1347
  async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
1434
1348
  const prepared = value;
1435
1349
  const worldRoot = join(worldsRoot, prepared.worldId);
1436
- const authority = await readAuthorityHead(worldRoot);
1350
+ const authorityStore = new FileNativeAuthorityV3(worldRoot);
1351
+ const authority = await authorityStore.readHead();
1437
1352
  if (authority.head === prepared.parentHead)
1438
1353
  return { outcome: "in_progress" };
1439
- const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, prepared);
1440
- if (authority.sequence < commit.sequence)
1354
+ if (authority.operationId !== operationId)
1441
1355
  throw corruptOperationOutcome();
1442
- await readAuthorityTail(worldRoot, authority, {
1443
- schemaVersion: 2,
1444
- head: commit.head,
1445
- sequence: commit.sequence,
1446
- commitDigest: prepared.commitDigest,
1447
- }).catch(() => {
1356
+ const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, prepared);
1357
+ if (authority.sequence < commit.sequence)
1448
1358
  throw corruptOperationOutcome();
1449
- });
1450
1359
  const materialized = await readMaterializedCheckpoint(worldRoot);
1451
1360
  const outcome = materialized !== null && materialized.sequence >= commit.sequence
1452
1361
  ? "committed"
1453
1362
  : "committed_materialization_pending";
1454
1363
  const resolved = { ...structuredClone(prepared), outcome };
1455
1364
  if (outcome === "committed_materialization_pending")
1456
- await assertPendingMaterializationCompatible(worldRoot, operationId, prepared.commitDigest);
1365
+ await assertPendingMaterializationCompatible(worldRoot, resolved);
1457
1366
  return resolved;
1458
1367
  }
1459
1368
  function corruptOperationOutcome() {
1460
1369
  return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1461
1370
  }
1462
- async function assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, outcome) {
1463
- const commit = await readPlayCommitByDigest(worldRoot, outcome.commitDigest);
1464
- if (commit.operationId !== operationId ||
1465
- commit?.parentHead !== outcome.parentHead ||
1466
- commit?.head !== outcome.head ||
1467
- commit?.mode !== outcome.mode ||
1468
- !isDeepStrictEqual(commit?.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
1469
- !isDeepStrictEqual(commit?.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
1371
+ async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1372
+ const fact = await new FileNativeAuthorityV3(worldRoot).authorityFactAt(outcome.head);
1373
+ const { commit } = fact;
1374
+ if (fact.digest !== outcome.commitDigest ||
1375
+ commit.auditParent.head !== outcome.parentHead ||
1376
+ commit.head !== outcome.head ||
1377
+ commit.mode !== outcome.mode ||
1378
+ !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
1379
+ !isDeepStrictEqual(commit.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
1470
1380
  throw corruptOperationOutcome();
1471
1381
  return commit;
1472
1382
  }
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)
1478
- throw corruptOperationOutcome();
1479
- const sequence = commit.sequence - 1;
1480
- for (const change of commit.stateChanges) {
1481
- const path = join(worldRoot, "state", change.relativePath);
1482
- const existing = await readOptionalText(path);
1483
- const existingHash = existing === null
1484
- ? null
1485
- : `sha256:${createHash("sha256").update(existing).digest("hex")}`;
1486
- if (existingHash !== change.expectedPreviousHash &&
1487
- existingHash !== change.nextHash)
1488
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World-state materialization conflict: ${change.relativePath}`);
1489
- }
1490
- for (const [index, message] of commit.historyAppend.entries()) {
1491
- const existing = await readOptionalText(join(worldRoot, "history", `${String(sequence + 1).padStart(8, "0")}-${historyFileName(index + 1, message)}`));
1492
- if (existing !== null && existing !== message.exactText)
1493
- throw new FileNativeWorldCreationError("inconsistent_materialization", "History-message materialization content conflict");
1494
- }
1383
+ async function assertPendingMaterializationCompatible(worldRoot, outcome) {
1384
+ const authority = new FileNativeAuthorityV3(worldRoot);
1385
+ await assertCommittedAuthorityOutcomeMatches(worldRoot, outcome);
1386
+ const [parent, target, state, history] = await Promise.all([
1387
+ authority.recover(outcome.parentHead),
1388
+ authority.recover(outcome.head),
1389
+ readTree(join(worldRoot, "state")),
1390
+ readTree(join(worldRoot, "history")),
1391
+ ]);
1392
+ assertProjectionBetweenEndpoints("state", state, parent.state, target.state);
1393
+ assertHistoryProjectionBetweenEndpoints(history, historySurfaceFiles(parent.history), historySurfaceFiles(target.history));
1495
1394
  const existingMaterials = await readOptionalJson(join(worldRoot, "runtime", "additional-materials.json"));
1496
- if (existingMaterials?.head === commit.head &&
1497
- !isDeepStrictEqual(existingMaterials.items, commit.nextAdditionalMaterials))
1395
+ if (existingMaterials !== null &&
1396
+ !((existingMaterials.head === parent.head &&
1397
+ isDeepStrictEqual(existingMaterials.items, parent.additionalMaterials)) ||
1398
+ (existingMaterials.head === target.head &&
1399
+ isDeepStrictEqual(existingMaterials.items, target.additionalMaterials))))
1498
1400
  throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1499
1401
  }
1500
- async function readOptionalText(path) {
1501
- try {
1502
- return await readFile(path, "utf8");
1503
- }
1504
- catch (error) {
1505
- if (isNodeError(error) && error.code === "ENOENT")
1506
- return null;
1507
- throw error;
1402
+ function assertProjectionBetweenEndpoints(surface, current, parent, target) {
1403
+ const parentFiles = new Map(parent.map((file) => [file.path, file.contents]));
1404
+ const targetFiles = new Map(target.map((file) => [file.path, file.contents]));
1405
+ for (const file of current) {
1406
+ if (file.contents !== parentFiles.get(file.path) &&
1407
+ file.contents !== targetFiles.get(file.path))
1408
+ throw new FileNativeWorldCreationError("inconsistent_materialization", `World ${surface} projection conflicts with both accepted endpoints: ${file.path}`);
1409
+ }
1410
+ }
1411
+ function assertHistoryProjectionBetweenEndpoints(current, parent, target) {
1412
+ const byMessageId = (files) => new Map(files.map((file) => [
1413
+ fileNativeHistoryMessageIdFromProjectionPath(file.path),
1414
+ file.contents,
1415
+ ]));
1416
+ const parentFiles = byMessageId(parent);
1417
+ const targetFiles = byMessageId(target);
1418
+ const seen = new Set();
1419
+ for (const file of current) {
1420
+ const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1421
+ if (messageId === null ||
1422
+ seen.has(messageId) ||
1423
+ (file.contents !== parentFiles.get(messageId) &&
1424
+ file.contents !== targetFiles.get(messageId)))
1425
+ throw new FileNativeWorldCreationError("inconsistent_materialization", `World history projection conflicts with both accepted endpoints: ${file.path}`);
1426
+ seen.add(messageId);
1508
1427
  }
1509
1428
  }
1510
1429
  function isAuthorityMaterialSelection(value) {
@@ -1540,73 +1459,6 @@ async function publishJson(path, value) {
1540
1459
  await rename(temporary, path);
1541
1460
  await syncDirectory(dirname(path));
1542
1461
  }
1543
- async function publishImmutableJson(path, value) {
1544
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
1545
- const contents = `${JSON.stringify(value, null, 2)}\n`;
1546
- const temporary = `${path}.${randomUUID()}.tmp`;
1547
- try {
1548
- await writeFile(temporary, contents, {
1549
- encoding: "utf8",
1550
- flag: "wx",
1551
- mode: 0o600,
1552
- });
1553
- await syncFile(temporary);
1554
- try {
1555
- await link(temporary, path);
1556
- await syncDirectory(dirname(path));
1557
- return true;
1558
- }
1559
- catch (error) {
1560
- if (!isNodeError(error) || error.code !== "EEXIST")
1561
- throw error;
1562
- if ((await readFile(path, "utf8")) !== contents)
1563
- throw new FileNativeWorldCreationError("operation_conflict", "Immutable play-commit file conflicts");
1564
- return false;
1565
- }
1566
- }
1567
- finally {
1568
- await rm(temporary, { force: true });
1569
- }
1570
- }
1571
- async function readAcceptedAuthority(root) {
1572
- try {
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) {
1581
- const immutable = await readJson(join(root, "runtime", "play-commits", `${digest}.json`)).catch((error) => {
1582
- throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint is missing an immutable commit: ${expectedHead}`, { cause: error });
1583
- });
1584
- assertFileNativePlayCommit(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;
1593
- }
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;
1603
- }
1604
- catch (error) {
1605
- if (error instanceof FileNativeWorldCreationError)
1606
- throw error;
1607
- throw new FileNativeWorldCreationError("world_corrupt", "Play Authority durable data is corrupt", { cause: error });
1608
- }
1609
- }
1610
1462
  async function readMaterializedCheckpoint(root) {
1611
1463
  const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1612
1464
  if (value === null)
@@ -1618,7 +1470,7 @@ async function readMaterializedCheckpoint(root) {
1618
1470
  "sequence",
1619
1471
  "commitDigest",
1620
1472
  ]) ||
1621
- value.schemaVersion !== 2 ||
1473
+ value.schemaVersion !== 3 ||
1622
1474
  typeof value.head !== "string" ||
1623
1475
  !Number.isSafeInteger(value.sequence) ||
1624
1476
  Number(value.sequence) < 0 ||
@@ -1638,327 +1490,28 @@ function sameMaterializedEndpoint(checkpoint, authority) {
1638
1490
  checkpoint.sequence === authority.sequence &&
1639
1491
  checkpoint.commitDigest === authority.commitDigest);
1640
1492
  }
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
- }
1718
- function assertFileNativePlayAuthority(value) {
1719
- if (!isRecord(value) ||
1720
- !hasExactKeys(value, ["schemaVersion", "head", "commits"]) ||
1721
- value.schemaVersion !== 2 ||
1722
- typeof value.head !== "string" ||
1723
- !Array.isArray(value.commits))
1724
- throw new Error("Play Authority has an invalid shape");
1725
- const operationIds = new Set();
1726
- const heads = new Set();
1727
- const commitsByHead = new Map();
1728
- let parent = "genesis";
1729
- let parentDigest = null;
1730
- for (const [index, commit] of value.commits.entries()) {
1731
- assertFileNativePlayCommit(commit);
1732
- const revision = commit.timelineRevision;
1733
- const replaced = revision === undefined
1734
- ? undefined
1735
- : commitsByHead.get(revision.replacesHead);
1736
- const replacedLogicalParent = replaced?.mode === "timeline_revision"
1737
- ? replaced.timelineRevision.restoresHead
1738
- : replaced?.parentHead;
1739
- if (commit.parentHead !== parent ||
1740
- commit.parentCommitDigest !== parentDigest ||
1741
- commit.sequence !== index + 1 ||
1742
- commit.head !== `commit:${index + 1}` ||
1743
- operationIds.has(commit.operationId) ||
1744
- heads.has(commit.head) ||
1745
- (commit.mode === "timeline_revision" &&
1746
- ((revision.restoresHead !== "genesis" &&
1747
- !heads.has(revision.restoresHead)) ||
1748
- replaced === undefined ||
1749
- !replaced.historyAppend.some(({ role }) => role === "player") ||
1750
- replacedLogicalParent !== revision.restoresHead)))
1751
- throw new Error("Play Authority commit chain is invalid");
1752
- operationIds.add(commit.operationId);
1753
- heads.add(commit.head);
1754
- commitsByHead.set(commit.head, commit);
1755
- parent = commit.head;
1756
- parentDigest = playCommitDigest(commit);
1757
- }
1758
- if (value.head !== parent)
1759
- throw new Error("Play Authority head is invalid");
1760
- }
1761
- function assertFileNativePlayCommit(value) {
1762
- if (!isRecord(value))
1763
- throw new Error("Play commit has an invalid shape");
1764
- const correction = value.mode === "correction";
1765
- const timelineRevision = value.mode === "timeline_revision";
1766
- if (!hasExactKeys(value, [
1767
- "schemaVersion",
1768
- "sequence",
1769
- "operationId",
1770
- "parentHead",
1771
- "parentCommitDigest",
1772
- "head",
1773
- "mode",
1774
- "historyAppend",
1775
- "stateChanges",
1776
- "nextAdditionalMaterials",
1777
- ...(correction ? ["correctionTargets", "corrects"] : []),
1778
- ...(timelineRevision ? ["timelineRevision"] : []),
1779
- ]) ||
1780
- value.schemaVersion !== 2 ||
1781
- !Number.isSafeInteger(value.sequence) ||
1782
- Number(value.sequence) < 1 ||
1783
- typeof value.operationId !== "string" ||
1784
- value.operationId.trim() === "" ||
1785
- typeof value.parentHead !== "string" ||
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) ||
1791
- typeof value.head !== "string" ||
1792
- !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1793
- value.head !== `commit:${String(value.sequence)}` ||
1794
- (value.mode !== "play" &&
1795
- value.mode !== "correction" &&
1796
- value.mode !== "timeline_revision") ||
1797
- !Array.isArray(value.historyAppend) ||
1798
- (correction
1799
- ? value.historyAppend.length !== 0
1800
- : timelineRevision
1801
- ? value.historyAppend.length !== 1 ||
1802
- !isRecord(value.historyAppend[0]) ||
1803
- value.historyAppend[0].role !== "player"
1804
- : value.historyAppend.length === 0 &&
1805
- (!Array.isArray(value.stateChanges) ||
1806
- value.stateChanges.length === 0)) ||
1807
- !value.historyAppend.every(isFileNativeHistoryMessage) ||
1808
- !Array.isArray(value.stateChanges) ||
1809
- !value.stateChanges.every(isFileNativeStateChange) ||
1810
- !Array.isArray(value.nextAdditionalMaterials) ||
1811
- !value.nextAdditionalMaterials.every(isAuthorityMaterialSelection))
1812
- throw new Error("Play commit has an invalid shape");
1813
- if (correction &&
1814
- (!Array.isArray(value.correctionTargets) ||
1815
- !value.correctionTargets.every((target) => typeof target === "string" && target.trim() !== "") ||
1816
- typeof value.corrects !== "string" ||
1817
- value.corrects !== value.parentHead))
1818
- throw new Error("Correction commit has an invalid shape");
1819
- if (timelineRevision &&
1820
- (value.stateChanges.length !== 0 ||
1821
- !isFileNativeTimelineRevision(value.timelineRevision)))
1822
- throw new Error("Timeline-revision commit has an invalid shape");
1823
- }
1824
- function isFileNativeTimelineRevision(value) {
1825
- if (!isRecord(value) ||
1826
- !hasExactKeys(value, [
1827
- "restoresHead",
1828
- "replacesHead",
1829
- "requestFingerprint",
1830
- "replacementState",
1831
- "replacementHistory",
1832
- ]) ||
1833
- typeof value.restoresHead !== "string" ||
1834
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.restoresHead) ||
1835
- typeof value.replacesHead !== "string" ||
1836
- !/^commit:[1-9][0-9]*$/u.test(value.replacesHead) ||
1837
- typeof value.requestFingerprint !== "string" ||
1838
- !/^sha256:[a-f0-9]{64}$/u.test(value.requestFingerprint) ||
1839
- !Array.isArray(value.replacementState) ||
1840
- !Array.isArray(value.replacementHistory) ||
1841
- !value.replacementHistory.every(isFileNativeHistoryMessage))
1842
- return false;
1843
- for (const file of value.replacementState) {
1844
- if (!isRecord(file) ||
1845
- !hasExactKeys(file, ["path", "sha256", "canonicalBytes"]) ||
1846
- typeof file.path !== "string" ||
1847
- typeof file.sha256 !== "string" ||
1848
- typeof file.canonicalBytes !== "string" ||
1849
- sha256(file.canonicalBytes) !== file.sha256)
1850
- return false;
1851
- try {
1852
- assertRelativePath(file.path);
1853
- }
1854
- catch {
1855
- return false;
1856
- }
1857
- }
1858
- return true;
1859
- }
1860
- function isFileNativeHistoryMessage(value) {
1861
- return (isRecord(value) &&
1862
- hasExactKeys(value, ["messageId", "role", "exactText"]) &&
1863
- typeof value.messageId === "string" &&
1864
- value.messageId.trim() !== "" &&
1865
- (value.role === "player" || value.role === "narrator") &&
1866
- typeof value.exactText === "string");
1867
- }
1868
1493
  function isPlayHistoryAppendInput(value) {
1869
1494
  return (isRecord(value) &&
1870
1495
  hasExactKeys(value, ["role", "exactText"]) &&
1871
1496
  (value.role === "player" || value.role === "narrator") &&
1872
1497
  typeof value.exactText === "string");
1873
1498
  }
1874
- function isFileNativeStateChange(value) {
1875
- if (!isRecord(value) ||
1876
- !hasExactKeys(value, [
1877
- "kind",
1878
- "documentId",
1879
- "stableShortRef",
1880
- "relativePath",
1881
- "codec",
1882
- "expectedPreviousHash",
1883
- "nextHash",
1884
- "canonicalNextBytes",
1885
- ]) ||
1886
- (value.kind !== "create" && value.kind !== "replace") ||
1887
- typeof value.documentId !== "string" ||
1888
- value.documentId.trim() === "" ||
1889
- typeof value.stableShortRef !== "string" ||
1890
- value.stableShortRef.trim() === "" ||
1891
- typeof value.relativePath !== "string" ||
1892
- (value.codec !== "yaml" && value.codec !== "markdown") ||
1893
- (value.expectedPreviousHash !== null &&
1894
- (typeof value.expectedPreviousHash !== "string" ||
1895
- !/^sha256:[a-f0-9]{64}$/u.test(value.expectedPreviousHash))) ||
1896
- typeof value.nextHash !== "string" ||
1897
- !/^sha256:[a-f0-9]{64}$/u.test(value.nextHash) ||
1898
- typeof value.canonicalNextBytes !== "string" ||
1899
- `sha256:${createHash("sha256")
1900
- .update(value.canonicalNextBytes)
1901
- .digest("hex")}` !== value.nextHash)
1902
- return false;
1903
- try {
1904
- assertRelativePath(value.relativePath);
1905
- }
1906
- catch {
1907
- return false;
1908
- }
1909
- return true;
1910
- }
1911
- async function materializePlayCommit(root, sequence, input) {
1912
- if (input.mode === "timeline_revision") {
1913
- if (input.timelineRevision === undefined)
1914
- throw new FileNativeWorldCreationError("world_corrupt", "Timeline-revision commit is missing its materialization snapshot");
1915
- await replaceMaterializedTree(join(root, "state"), input.timelineRevision.replacementState.map(({ path, canonicalBytes }) => ({ path, contents: canonicalBytes })));
1916
- }
1917
- else {
1918
- for (const change of input.stateChanges) {
1919
- assertRelativePath(change.relativePath);
1920
- const path = join(root, "state", change.relativePath);
1921
- let existing = null;
1922
- try {
1923
- existing = await readFile(path, "utf8");
1924
- }
1925
- catch (error) {
1926
- if (!isNodeError(error) || error.code !== "ENOENT")
1927
- throw error;
1928
- }
1929
- const existingHash = existing === null ? null : sha256(existing);
1930
- if (existingHash === change.nextHash)
1931
- continue;
1932
- if (existingHash !== change.expectedPreviousHash)
1933
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World-state materialization conflict: ${change.relativePath}`);
1934
- await publishText(path, change.canonicalNextBytes);
1935
- }
1936
- }
1499
+ async function materializeRecoveredEndpoint(root, authority, endpoint) {
1500
+ if (authority.head !== endpoint.head)
1501
+ throw new FileNativeWorldCreationError("world_corrupt", "Recovered endpoint does not match the Authority head to materialize");
1502
+ await replaceMaterializedTree(join(root, "state"), endpoint.state);
1937
1503
  crashAtFileNativeAuthorityEdge("after_state_materialization");
1938
- const historyRoot = join(root, "history");
1939
- await mkdir(historyRoot, { recursive: true });
1940
- if (input.mode === "timeline_revision") {
1941
- await replaceMaterializedTree(historyRoot, historySurfaceFiles([
1942
- ...input.timelineRevision.replacementHistory,
1943
- ...input.historyAppend,
1944
- ]));
1945
- }
1946
- else {
1947
- for (const [index, message] of input.historyAppend.entries())
1948
- await writeIdempotentText(join(historyRoot, `${String(sequence).padStart(8, "0")}-${historyFileName(index + 1, message)}`), message.exactText);
1949
- }
1950
- const materialsPath = join(root, "runtime", "additional-materials.json");
1951
- const nextMaterials = {
1952
- head: `commit:${sequence}`,
1953
- items: input.nextMaterials,
1954
- };
1955
- const existingMaterials = await readOptionalJson(materialsPath);
1956
- if (existingMaterials?.head === nextMaterials.head &&
1957
- JSON.stringify(existingMaterials.items) !==
1958
- JSON.stringify(nextMaterials.items))
1959
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1960
- if (JSON.stringify(existingMaterials) !== JSON.stringify(nextMaterials))
1961
- await publishJson(materialsPath, nextMaterials);
1504
+ await replaceMaterializedTree(join(root, "history"), historySurfaceFiles(endpoint.history));
1505
+ await publishJson(join(root, "runtime", "additional-materials.json"), {
1506
+ head: authority.head,
1507
+ items: endpoint.additionalMaterials,
1508
+ });
1509
+ await publishJson(join(root, "runtime", "materialized-head.json"), {
1510
+ schemaVersion: 3,
1511
+ head: authority.head,
1512
+ sequence: authority.sequence,
1513
+ commitDigest: authority.commitDigest,
1514
+ });
1962
1515
  }
1963
1516
  async function replaceMaterializedTree(root, files) {
1964
1517
  const next = new Map();
@@ -1991,40 +1544,16 @@ async function publishText(path, contents) {
1991
1544
  await syncDirectory(dirname(path));
1992
1545
  }
1993
1546
  function historySurfaceFiles(messages) {
1994
- return messages.map((message) => {
1995
- const genesis = /\.message\.genesis(?:\.([1-9][0-9]*))?\.(player|narrator)$/u.exec(message.messageId);
1996
- const committed = /\.message\.([1-9][0-9]*)\.([1-9][0-9]*)\.(player|narrator)$/u.exec(message.messageId);
1997
- const sequence = genesis !== null ? 0 : Number.parseInt(committed?.[1] ?? "", 10);
1998
- const index = genesis !== null
1999
- ? Number.parseInt(genesis[1] ?? "1", 10)
2000
- : Number.parseInt(committed?.[2] ?? "", 10);
2001
- const role = genesis?.[2] ?? committed?.[3];
2002
- if (!Number.isSafeInteger(sequence) ||
2003
- sequence < 0 ||
2004
- !Number.isSafeInteger(index) ||
2005
- index < 1 ||
2006
- role !== message.role)
2007
- throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${message.messageId}`);
2008
- return {
2009
- path: `${String(sequence).padStart(8, "0")}-${historyFileName(index, message)}`,
2010
- contents: message.exactText,
2011
- };
1547
+ return projectFileNativeHistorySurface(messages, (messageId) => {
1548
+ throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${messageId}`);
2012
1549
  });
2013
1550
  }
2014
- function materializedHistoryRecord(worldId, files) {
1551
+ function materializedHistoryRecord(files) {
2015
1552
  const result = {};
2016
1553
  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)
1554
+ const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1555
+ if (messageId === null)
2019
1556
  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
1557
  if (messageId in result)
2029
1558
  throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
2030
1559
  result[messageId] = file.contents;
@@ -2047,81 +1576,6 @@ async function writeIdempotentText(path, contents) {
2047
1576
  await syncDirectory(dirname(path));
2048
1577
  }
2049
1578
  }
2050
- function historyFileName(index, message) {
2051
- return `${String(index).padStart(2, "0")}-${message.role}-${operationDigest(message.messageId).slice(0, 12)}.md`;
2052
- }
2053
- function cloneAuthorityPrefix(input) {
2054
- const end = input.sourceHead === "genesis"
2055
- ? 0
2056
- : (input.sourceAuthority?.commits.findIndex(({ head }) => head === input.sourceHead) ?? -1) + 1;
2057
- if (input.sourceHead !== "genesis" && end === 0)
2058
- throw new FileNativeWorldCreationError("world_corrupt", `Cannot copy a nonexistent source Authority endpoint: ${input.sourceHead}`);
2059
- const sourceCommits = input.sourceAuthority?.commits.slice(0, end) ?? [];
2060
- const messageIds = new Map();
2061
- const bindMessageId = (source, target) => {
2062
- if (messageIds.has(source))
2063
- throw new FileNativeWorldCreationError("world_corrupt", `Source Authority contains a duplicate history-message identity: ${source}`);
2064
- messageIds.set(source, target);
2065
- };
2066
- for (const [index, message] of input.sourceGenesis.history.entries())
2067
- bindMessageId(message.messageId, index === 0 && message.role === "narrator"
2068
- ? `${input.targetWorldId}.message.genesis.narrator`
2069
- : `${input.targetWorldId}.message.genesis.${index + 1}.${message.role}`);
2070
- for (const [commitIndex, commit] of sourceCommits.entries())
2071
- for (const [messageIndex, message] of commit.historyAppend.entries())
2072
- bindMessageId(message.messageId, `${input.targetWorldId}.message.${commitIndex + 1}.${messageIndex + 1}.${message.role}`);
2073
- const rewriteHistory = (history) => history.map((message) => ({
2074
- ...structuredClone(message),
2075
- messageId: messageIds.get(message.messageId),
2076
- }));
2077
- const clonedCommits = [];
2078
- let parentCommitDigest = null;
2079
- for (const commit of sourceCommits) {
2080
- const cloned = {
2081
- ...structuredClone(commit),
2082
- schemaVersion: 2,
2083
- parentCommitDigest,
2084
- operationId: `clone-${operationDigest(`${input.targetWorldId}\0${commit.head}`).slice(0, 56)}`,
2085
- historyAppend: rewriteHistory(commit.historyAppend),
2086
- nextAdditionalMaterials: rewriteMaterials(commit.nextAdditionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
2087
- ...(commit.timelineRevision === undefined
2088
- ? {}
2089
- : {
2090
- timelineRevision: {
2091
- ...structuredClone(commit.timelineRevision),
2092
- replacementHistory: rewriteHistory(commit.timelineRevision.replacementHistory),
2093
- },
2094
- }),
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,
2103
- };
2104
- }
2105
- function rewriteMaterials(materials, messageIds, sourceWorldId, targetWorldId) {
2106
- return materials.map((material) => {
2107
- if (material.kind === "history_message") {
2108
- const message = messageIds.get(material.message);
2109
- if (message === undefined)
2110
- throw new FileNativeWorldCreationError("world_corrupt", `Fork material references a history message that cannot be remapped: ${material.message}`);
2111
- return { ...material, message };
2112
- }
2113
- if (material.kind === "history_commit") {
2114
- const sourcePrefix = `${sourceWorldId}.`;
2115
- return {
2116
- ...material,
2117
- commit: material.commit.startsWith(sourcePrefix)
2118
- ? `${targetWorldId}.${material.commit.slice(sourcePrefix.length)}`
2119
- : material.commit,
2120
- };
2121
- }
2122
- return structuredClone(material);
2123
- });
2124
- }
2125
1579
  function isCommittedOutcome(outcome) {
2126
1580
  return (outcome.outcome === "committed" ||
2127
1581
  outcome.outcome === "committed_materialization_pending");
@@ -2134,6 +1588,63 @@ function assertSameOperationOutcome(outcome, input) {
2134
1588
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different commit payload");
2135
1589
  }
2136
1590
  }
1591
+ function worldNeutralMaterials(worldId, materials) {
1592
+ const prefix = `${worldId}.`;
1593
+ return materials.map((material) => {
1594
+ if (material.kind === "history_message") {
1595
+ const message = material.message.startsWith(prefix)
1596
+ ? material.message.slice(prefix.length)
1597
+ : material.message;
1598
+ if (!/^message\.(?:genesis(?:\.[1-9][0-9]*)?\.(?:player|narrator)|[1-9][0-9]*\.[1-9][0-9]*\.(?:player|narrator))$/u.test(message))
1599
+ throw new FileNativeWorldCreationError("operation_conflict", "History material does not name a local immutable message");
1600
+ return { ...material, message };
1601
+ }
1602
+ if (material.kind === "history_commit") {
1603
+ const commit = material.commit.startsWith(prefix)
1604
+ ? material.commit.slice(prefix.length)
1605
+ : material.commit;
1606
+ if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(commit))
1607
+ throw new FileNativeWorldCreationError("operation_conflict", "History material does not name a local immutable Authority endpoint");
1608
+ return { ...material, commit };
1609
+ }
1610
+ return structuredClone(material);
1611
+ });
1612
+ }
1613
+ function authorityCommitMatchesInput(commit, input) {
1614
+ const stateChangesMatch = commit.stateChanges.length === input.stateChanges.length &&
1615
+ commit.stateChanges.every((stored, index) => {
1616
+ const candidate = input.stateChanges[index];
1617
+ if (candidate === undefined)
1618
+ return false;
1619
+ const { nextBlob, ...metadata } = stored;
1620
+ const expected = {
1621
+ kind: candidate.kind,
1622
+ documentId: candidate.documentId,
1623
+ stableShortRef: candidate.stableShortRef,
1624
+ relativePath: candidate.relativePath,
1625
+ codec: candidate.codec,
1626
+ expectedPreviousHash: candidate.expectedPreviousHash,
1627
+ nextHash: candidate.nextHash,
1628
+ };
1629
+ return (nextBlob.epoch === commit.sequence &&
1630
+ `sha256:${nextBlob.digest}` === candidate.nextHash &&
1631
+ isDeepStrictEqual(metadata, expected));
1632
+ });
1633
+ const expectedCorrection = input.mode === "correction"
1634
+ ? input.stateChanges.map(({ documentId }) => documentId)
1635
+ : undefined;
1636
+ return (commit.mode === input.mode &&
1637
+ commit.auditParent.head === input.parentHead &&
1638
+ commit.timelineParent.head ===
1639
+ (input.timelineRevision?.restoresHead ?? input.parentHead) &&
1640
+ isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })), input.historyAppend) &&
1641
+ stateChangesMatch &&
1642
+ isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
1643
+ isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
1644
+ isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
1645
+ commit.corrects ===
1646
+ (input.mode === "correction" ? input.parentHead : undefined));
1647
+ }
2137
1648
  function openWorldDocumentSnapshot(files) {
2138
1649
  return WorldDocumentStore.open({
2139
1650
  layout: "world_state",
@@ -2181,6 +1692,68 @@ async function durableTree(root) {
2181
1692
  }
2182
1693
  await syncDirectory(root);
2183
1694
  }
1695
+ async function readGenesisForFork(worldRoot, worldId) {
1696
+ const value = await readJson(join(worldRoot, "runtime", "genesis.json"));
1697
+ if (!isRecord(value) ||
1698
+ value.schemaVersion !== 1 ||
1699
+ value.type !== "file_native_genesis" ||
1700
+ value.worldId !== worldId ||
1701
+ !Array.isArray(value.state) ||
1702
+ !Array.isArray(value.history) ||
1703
+ !Array.isArray(value.additionalMaterials))
1704
+ throw new FileNativeWorldCreationError("world_corrupt", "World genesis projection has an invalid shape");
1705
+ const state = value.state.map((file) => {
1706
+ if (!isRecord(file) ||
1707
+ !hasExactKeys(file, ["path", "sha256", "canonicalBytes"]) ||
1708
+ typeof file.path !== "string" ||
1709
+ typeof file.sha256 !== "string" ||
1710
+ typeof file.canonicalBytes !== "string" ||
1711
+ sha256(file.canonicalBytes) !== file.sha256)
1712
+ throw new FileNativeWorldCreationError("world_corrupt", "World genesis state projection is corrupt");
1713
+ try {
1714
+ assertRelativePath(file.path);
1715
+ }
1716
+ catch (error) {
1717
+ throw new FileNativeWorldCreationError("world_corrupt", "World genesis state path is invalid", error instanceof Error ? { cause: error } : undefined);
1718
+ }
1719
+ return {
1720
+ path: file.path,
1721
+ sha256: file.sha256,
1722
+ canonicalBytes: file.canonicalBytes,
1723
+ };
1724
+ });
1725
+ const prefix = `${worldId}.`;
1726
+ const history = value.history.map((message, index) => {
1727
+ if (!isRecord(message) ||
1728
+ !hasExactKeys(message, ["messageId", "role", "exactText"]) ||
1729
+ typeof message.messageId !== "string" ||
1730
+ (message.role !== "player" && message.role !== "narrator") ||
1731
+ typeof message.exactText !== "string")
1732
+ throw new FileNativeWorldCreationError("world_corrupt", "World genesis history projection is corrupt");
1733
+ const messageId = message.messageId.startsWith(prefix)
1734
+ ? message.messageId.slice(prefix.length)
1735
+ : message.messageId;
1736
+ const role = message.role === "player" ? "player" : "narrator";
1737
+ const expected = index === 0 && role === "narrator"
1738
+ ? "message.genesis.narrator"
1739
+ : `message.genesis.${index + 1}.${role}`;
1740
+ if (messageId !== expected)
1741
+ throw new FileNativeWorldCreationError("world_corrupt", "World genesis history identity is invalid");
1742
+ return {
1743
+ messageId,
1744
+ role,
1745
+ exactText: message.exactText,
1746
+ };
1747
+ });
1748
+ let additionalMaterials;
1749
+ try {
1750
+ additionalMaterials = worldNeutralMaterials(worldId, value.additionalMaterials);
1751
+ }
1752
+ catch (error) {
1753
+ throw new FileNativeWorldCreationError("world_corrupt", "World genesis material projection is invalid", error instanceof Error ? { cause: error } : undefined);
1754
+ }
1755
+ return { state, history, additionalMaterials };
1756
+ }
2184
1757
  async function readPublicationAt(root) {
2185
1758
  return readJson(join(root, publicationFile));
2186
1759
  }