narraeon 0.2.0 → 0.3.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.
- package/dist/node/protocol/v1.js +6 -0
- package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
- package/dist/node/runtime/V1Runtime.js +7 -6
- package/dist/node/runtime/model/FileNativeModelAdapters.js +40 -13
- package/dist/node/runtime/model/ModelHost.js +7 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +96 -7
- package/dist/node/runtime/play/PlayCallChain.js +309 -53
- package/dist/node/runtime/play/PlayFollowupRequests.js +42 -22
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +5 -5
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +447 -6
- package/dist/node/runtime/world/FileNativeWorldStore.js +423 -850
- package/dist/node/server/createServer.js +10 -2
- package/dist/web/assets/index-D5VqDvjY.js +1192 -0
- package/dist/web/assets/index-Xa0s35CJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DHV_VWl3.js +0 -1192
- package/dist/web/assets/index-DjLCkP99.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
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
|
-
|
|
77
|
-
|
|
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(
|
|
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",
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
502
|
-
|
|
503
|
-
|
|
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(
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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:
|
|
750
|
-
operationId: clonedHead.operationId,
|
|
685
|
+
commitDigest: clonedHead.commitDigest,
|
|
751
686
|
});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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 (
|
|
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,
|
|
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,
|
|
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
|
|
858
|
-
const commit =
|
|
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
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
878
|
-
|
|
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:
|
|
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
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
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
|
|
975
|
-
if (
|
|
976
|
-
!
|
|
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
|
-
|
|
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
|
|
990
|
-
const authority = await
|
|
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 [
|
|
998
|
-
|
|
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(
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
999
|
+
...(input.timelineRevision === undefined
|
|
1000
|
+
? {}
|
|
1001
|
+
: { timelineParentHead: input.timelineRevision.restoresHead }),
|
|
1048
1002
|
mode: input.mode,
|
|
1049
|
-
historyAppend: input.historyAppend
|
|
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
|
-
|
|
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.
|
|
1062
|
-
? {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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:
|
|
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
|
|
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
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
|
1136
|
-
return authority
|
|
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(
|
|
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(
|
|
1177
|
+
function genesisOpeningMessage(exactText) {
|
|
1244
1178
|
return {
|
|
1245
|
-
messageId:
|
|
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
|
|
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
|
-
|
|
1440
|
-
if (authority.sequence < commit.sequence)
|
|
1354
|
+
if (authority.operationId !== operationId)
|
|
1441
1355
|
throw corruptOperationOutcome();
|
|
1442
|
-
await
|
|
1443
|
-
|
|
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,
|
|
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,
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
commit
|
|
1467
|
-
commit
|
|
1468
|
-
|
|
1469
|
-
!isDeepStrictEqual(commit
|
|
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,
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
|
1497
|
-
!
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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 !==
|
|
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
|
|
1875
|
-
if (
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
1939
|
-
await
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
|
1995
|
-
|
|
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(
|
|
1551
|
+
function materializedHistoryRecord(files) {
|
|
2015
1552
|
const result = {};
|
|
2016
1553
|
for (const file of files) {
|
|
2017
|
-
const
|
|
2018
|
-
if (
|
|
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
|
}
|