narraeon 0.1.1 → 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.
Files changed (29) hide show
  1. package/dist/node/protocol/modelConnections.js +49 -0
  2. package/dist/node/protocol/modelUsage.js +39 -0
  3. package/dist/node/protocol/v1.js +35 -0
  4. package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
  5. package/dist/node/runtime/V1Runtime.js +66 -29
  6. package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
  7. package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
  8. package/dist/node/runtime/model/ModelHost.js +81 -9
  9. package/dist/node/runtime/model/ModelHostStream.js +61 -1
  10. package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
  11. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
  12. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
  13. package/dist/node/runtime/play/PlayCallChain.js +904 -369
  14. package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
  15. package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
  16. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
  17. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
  18. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
  19. package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
  20. package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
  21. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
  22. package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
  23. package/dist/node/server/createServer.js +10 -2
  24. package/dist/web/assets/index-D5VqDvjY.js +1192 -0
  25. package/dist/web/assets/index-Xa0s35CJ.css +1 -0
  26. package/dist/web/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
  29. package/dist/web/assets/index-DDx1itzT.js +0 -1192
@@ -1,12 +1,17 @@
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";
6
6
  import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
7
7
  import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
8
8
  import { WorldDocumentStore } from "./WorldDocumentStore.js";
9
+ import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
10
+ import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
11
+ import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
12
+ import { FileNativeAuthorityV3, FileNativeAuthorityV3Error, } from "./FileNativeAuthorityV3.js";
9
13
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
14
+ import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, } from "./FileNativeHistoryProjection.js";
10
15
  const publicationFile = "publication.json";
11
16
  const localMetadataFile = "local.json";
12
17
  export class FileNativeWorldCreationError extends Error {
@@ -29,6 +34,11 @@ export class FileNativeWorldStore {
29
34
  #promptCompiler;
30
35
  #activeControlUsers = new Map();
31
36
  operations;
37
+ playTimeline;
38
+ playAdvances;
39
+ #storageMigrator;
40
+ #storageMigrations = new Map();
41
+ #currentStorage = new Set();
32
42
  constructor(dataRoot, options = {}) {
33
43
  const root = resolve(dataRoot);
34
44
  this.#worldsRoot = join(root, "worlds-file-native");
@@ -36,6 +46,62 @@ export class FileNativeWorldStore {
36
46
  this.#promptCompiler =
37
47
  options.promptCompiler ?? new FileNativePromptCompiler();
38
48
  this.operations = new FileNativeWorldOperationCoordinator(root);
49
+ this.playTimeline = new FileNativePlayTimelineStore(root);
50
+ this.playAdvances = new FileNativePlayAdvanceStore(root);
51
+ this.#storageMigrator = new FileNativeWorldStorageMigrator(root, this.playTimeline);
52
+ }
53
+ /**
54
+ * Upgrade a formally released cumulative layout before current Authority or
55
+ * play-timeline behavior crosses its seam. Concurrent callers share one
56
+ * in-process promise; the durable world lock serializes processes.
57
+ */
58
+ async ensureCurrentStorage(worldId) {
59
+ assertIdentity(worldId, "World ID");
60
+ if (this.#currentStorage.has(worldId))
61
+ return;
62
+ const existing = this.#storageMigrations.get(worldId);
63
+ if (existing !== undefined)
64
+ return existing;
65
+ const migration = this.#ensureCurrentStorageSerialized(worldId);
66
+ this.#storageMigrations.set(worldId, migration);
67
+ try {
68
+ await migration;
69
+ }
70
+ finally {
71
+ if (this.#storageMigrations.get(worldId) === migration)
72
+ this.#storageMigrations.delete(worldId);
73
+ }
74
+ }
75
+ async #ensureCurrentStorageSerialized(worldId) {
76
+ try {
77
+ const root = join(this.#worldsRoot, worldId);
78
+ const authority = new FileNativeAuthorityV3(root);
79
+ if (await authority.exists()) {
80
+ await authority.readHead();
81
+ this.#currentStorage.add(worldId);
82
+ return;
83
+ }
84
+ await this.operations.withWorldAuthorityLock(worldId, async () => {
85
+ await this.#storageMigrator.migrate(worldId);
86
+ });
87
+ this.#currentStorage.add(worldId);
88
+ }
89
+ catch (error) {
90
+ if (error instanceof FileNativeWorldCreationError)
91
+ throw error;
92
+ if (isNodeError(error) && error.code === "ENOENT") {
93
+ const root = await stat(join(this.#worldsRoot, worldId)).catch((statError) => {
94
+ if (isNodeError(statError) && statError.code === "ENOENT")
95
+ return null;
96
+ throw statError;
97
+ });
98
+ if (root === null)
99
+ throw new FileNativeWorldNotFoundError({ cause: error });
100
+ }
101
+ if (error instanceof WorldOperationBusyError)
102
+ throw new FileNativeWorldCreationError("operation_conflict", "World storage is being upgraded by another durable operation", { cause: error });
103
+ throw new FileNativeWorldCreationError("world_corrupt", "Released world storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
104
+ }
39
105
  }
40
106
  async listWorlds() {
41
107
  await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
@@ -90,6 +156,7 @@ export class FileNativeWorldStore {
90
156
  }
91
157
  return this.operations.withExclusiveWorldStateMutation(worldId, async () => {
92
158
  await rm(root, { recursive: true, force: true });
159
+ this.#currentStorage.delete(worldId);
93
160
  return { deleted: true };
94
161
  });
95
162
  }
@@ -148,7 +215,7 @@ export class FileNativeWorldStore {
148
215
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
149
216
  await rm(stagingRoot, { recursive: true, force: true });
150
217
  try {
151
- const openingMessage = genesisOpeningMessage(worldId, openingText(packageFiles));
218
+ const openingMessage = genesisOpeningMessage(openingText(packageFiles));
152
219
  const initialMaterials = [
153
220
  { kind: "history_message", message: openingMessage.messageId },
154
221
  ];
@@ -156,7 +223,7 @@ export class FileNativeWorldStore {
156
223
  await writeSurface(stagingRoot, "control", control);
157
224
  await mkdir(join(stagingRoot, "history"), { recursive: true });
158
225
  await mkdir(join(stagingRoot, "runtime"), { recursive: true });
159
- await writeIdempotentText(join(stagingRoot, "history", `00000000-${historyFileName(1, openingMessage)}`), openingMessage.exactText);
226
+ await writeIdempotentText(join(stagingRoot, "history", historySurfaceFiles([openingMessage])[0].path), openingMessage.exactText);
160
227
  const genesis = {
161
228
  schemaVersion: 1,
162
229
  type: "file_native_genesis",
@@ -169,8 +236,22 @@ export class FileNativeWorldStore {
169
236
  additionalMaterials: initialMaterials,
170
237
  };
171
238
  await writeJson(join(stagingRoot, "runtime", "genesis.json"), genesis);
239
+ await writeJson(join(stagingRoot, "runtime", "play-genesis-timeline.json"), {
240
+ schemaVersion: 1,
241
+ worldId,
242
+ history: genesis.history,
243
+ });
244
+ await new FileNativeAuthorityV3(stagingRoot).initialize({
245
+ operationId: input.operationId,
246
+ state,
247
+ history: [openingMessage],
248
+ additionalMaterials: initialMaterials,
249
+ });
172
250
  await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
251
+ schemaVersion: 3,
173
252
  head: "genesis",
253
+ sequence: 0,
254
+ commitDigest: null,
174
255
  });
175
256
  await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
176
257
  const publication = {
@@ -215,6 +296,7 @@ export class FileNativeWorldStore {
215
296
  }
216
297
  async readSurface(worldId, surface) {
217
298
  assertIdentity(worldId, "World ID");
299
+ await this.ensureCurrentStorage(worldId);
218
300
  const root = join(this.#worldsRoot, worldId);
219
301
  await readPublicationAt(root);
220
302
  if (surface === "runtime") {
@@ -236,28 +318,37 @@ export class FileNativeWorldStore {
236
318
  * must never pair a mutable state tree with a different response head.
237
319
  */
238
320
  async renderPlayerViewsAtHead(worldId, head) {
239
- const endpoint = await this.recoverEndpoint(worldId, head);
240
- const control = await this.readSurface(worldId, "control");
321
+ const root = join(this.#worldsRoot, worldId);
322
+ const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
323
+ const [state, control] = await Promise.all([
324
+ materialized?.head === head
325
+ ? readTree(join(root, "state"))
326
+ : this.recoverEndpoint(worldId, head).then(({ state }) => state),
327
+ this.readSurface(worldId, "control"),
328
+ ]);
241
329
  const source = control.find(({ path }) => path === "player-views.yaml");
242
330
  return new PlayerViewRenderer().render({
243
- snapshot: openWorldDocumentSnapshot(endpoint.state),
331
+ snapshot: openWorldDocumentSnapshot(state),
244
332
  control: source?.contents ?? "",
245
333
  });
246
334
  }
247
335
  async currentHead(worldId) {
248
336
  assertIdentity(worldId, "World ID");
337
+ await this.ensureCurrentStorage(worldId);
249
338
  const root = join(this.#worldsRoot, worldId);
250
339
  await readPublicationAt(root);
251
- return (await readAcceptedAuthority(root))?.head ?? "genesis";
340
+ return (await new FileNativeAuthorityV3(root).readHead()).head;
252
341
  }
253
342
  async currentHeadOperationId(worldId) {
254
343
  assertIdentity(worldId, "World ID");
344
+ await this.ensureCurrentStorage(worldId);
255
345
  const root = join(this.#worldsRoot, worldId);
256
346
  await readPublicationAt(root);
257
- return ((await readAcceptedAuthority(root))?.commits.at(-1)?.operationId ?? null);
347
+ return new FileNativeAuthorityV3(root).currentOperationId();
258
348
  }
259
349
  async saveControlDraft(worldId, files) {
260
350
  assertIdentity(worldId, "World ID");
351
+ await this.ensureCurrentStorage(worldId);
261
352
  const root = join(this.#worldsRoot, worldId);
262
353
  await readPublicationAt(root);
263
354
  for (const file of files)
@@ -350,6 +441,7 @@ export class FileNativeWorldStore {
350
441
  }
351
442
  async #readControlDraft(worldId) {
352
443
  assertIdentity(worldId, "World ID");
444
+ await this.ensureCurrentStorage(worldId);
353
445
  const root = join(this.#worldsRoot, worldId);
354
446
  await readPublicationAt(root);
355
447
  const [state, draft] = await Promise.all([
@@ -363,6 +455,7 @@ export class FileNativeWorldStore {
363
455
  /** Bind the current Authority endpoint for a model-directed play call chain. */
364
456
  async bindPlayCallChain(worldId) {
365
457
  assertIdentity(worldId, "World ID");
458
+ await this.ensureCurrentStorage(worldId);
366
459
  const root = join(this.#worldsRoot, worldId);
367
460
  try {
368
461
  await readPublicationAt(root);
@@ -384,61 +477,47 @@ export class FileNativeWorldStore {
384
477
  }
385
478
  try {
386
479
  return await this.operations.withWorldAuthorityLock(worldId, async () => {
387
- const publishedAuthority = await readAcceptedAuthority(root);
388
- if (publishedAuthority !== null) {
389
- const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
390
- let materializedIndex = 0;
391
- if (materialized !== null && materialized.head !== "genesis") {
392
- const found = publishedAuthority.commits.findIndex(({ head }) => head === materialized.head);
393
- if (found < 0)
394
- throw new FileNativeWorldCreationError("world_corrupt", "World materialized head does not belong to the authority commit chain");
395
- materializedIndex = found + 1;
396
- }
397
- for (let index = materializedIndex; index < publishedAuthority.commits.length; index += 1) {
398
- const commit = publishedAuthority.commits[index];
399
- await materializePlayCommit(root, index + 1, {
400
- mode: commit.mode,
401
- historyAppend: commit.historyAppend,
402
- nextMaterials: commit.nextAdditionalMaterials,
403
- stateChanges: commit.stateChanges ?? [],
404
- ...(commit.timelineRevision === undefined
405
- ? {}
406
- : { timelineRevision: commit.timelineRevision }),
407
- });
408
- await publishJson(this.#operationOutcomePath(commit.operationId), {
409
- outcome: "committed",
410
- worldId,
411
- parentHead: commit.parentHead,
412
- head: commit.head,
413
- historyAppend: commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })),
414
- nextAdditionalMaterials: commit.nextAdditionalMaterials,
415
- mode: commit.mode,
416
- });
480
+ const authorityStore = new FileNativeAuthorityV3(root);
481
+ const authority = await authorityStore.readHead();
482
+ const checkpoint = await readMaterializedCheckpoint(root);
483
+ if (!sameMaterializedEndpoint(checkpoint, authority)) {
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
+ }
417
498
  }
418
- await publishJson(join(root, "runtime", "materialized-head.json"), {
419
- head: publishedAuthority.head,
420
- });
499
+ await materializeRecoveredEndpoint(root, authority, recovered);
500
+ if (operation !== null)
501
+ await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
421
502
  }
422
- const [state, control, authority, materials, recovered] = await Promise.all([
503
+ const [state, control, history, materials] = await Promise.all([
423
504
  readTree(join(root, "state")),
424
505
  readTree(join(root, "control")),
425
- readAcceptedAuthority(root),
506
+ readTree(join(root, "history")),
426
507
  readOptionalJson(join(root, "runtime", "additional-materials.json")),
427
- this.recoverEndpoint(worldId),
428
508
  ]);
509
+ if ((materials?.head ?? "genesis") !== authority.head)
510
+ throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
429
511
  return {
430
512
  worldId,
431
- parentHead: authority?.head ?? "genesis",
513
+ parentHead: authority.head,
432
514
  files: [...state, ...control].reduce((files, { path, contents }, index) => {
433
515
  files[`${index < state.length ? "state" : "control"}/${path}`] =
434
516
  contents;
435
517
  return files;
436
518
  }, {}),
437
519
  additionalMaterials: materials?.items ?? [],
438
- history: Object.fromEntries(recovered.history.map(({ messageId, exactText }) => [
439
- messageId,
440
- exactText,
441
- ])),
520
+ history: materializedHistoryRecord(history),
442
521
  };
443
522
  });
444
523
  }
@@ -479,73 +558,33 @@ export class FileNativeWorldStore {
479
558
  }
480
559
  async recoverEndpoint(worldId, head) {
481
560
  assertIdentity(worldId, "World ID");
561
+ await this.ensureCurrentStorage(worldId);
482
562
  const root = join(this.#worldsRoot, worldId);
483
563
  await readPublicationAt(root);
484
- const genesis = await readJson(join(root, "runtime", "genesis.json"));
485
- for (const file of [...genesis.state, ...genesis.control])
486
- if (sha256(file.canonicalBytes) !== file.sha256)
487
- throw new FileNativeWorldCreationError("world_corrupt", `Genesis file hash does not match: ${file.path}`);
488
- const authority = await readAcceptedAuthority(root);
489
- const target = head ?? authority?.head ?? "genesis";
490
- const commits = authority?.commits ?? [];
491
- const end = target === "genesis"
492
- ? 0
493
- : commits.findIndex((commit) => commit.head === target) + 1;
494
- if (target !== "genesis" && end === 0)
495
- throw new FileNativeWorldCreationError("world_corrupt", `Cannot rebuild endpoint from immutable authority chain: ${target}`);
496
- const state = new Map(genesis.state.map((file) => [file.path, file.canonicalBytes]));
497
- const history = structuredClone(genesis.history);
498
- let additionalMaterials = genesis.additionalMaterials;
499
- for (const commit of commits.slice(0, end)) {
500
- if (commit.mode === "timeline_revision") {
501
- const revision = commit.timelineRevision;
502
- if (revision === undefined)
503
- throw new FileNativeWorldCreationError("world_corrupt", "Timeline-revision commit is missing its recovery snapshot");
504
- const restored = await this.recoverEndpoint(worldId, revision.restoresHead);
505
- if (!isDeepStrictEqual(revision.replacementState, immutableFiles(restored.state)) ||
506
- !isDeepStrictEqual(revision.replacementHistory, restored.history) ||
507
- !isDeepStrictEqual(commit.nextAdditionalMaterials, restored.additionalMaterials))
508
- throw new FileNativeWorldCreationError("world_corrupt", "Timeline-revision recovery snapshot does not match its logical parent endpoint");
509
- state.clear();
510
- for (const file of revision.replacementState) {
511
- if (sha256(file.canonicalBytes) !== file.sha256)
512
- throw new FileNativeWorldCreationError("world_corrupt", `Timeline-revision state hash does not match: ${file.path}`);
513
- state.set(file.path, file.canonicalBytes);
514
- }
515
- history.splice(0, history.length, ...structuredClone(revision.replacementHistory));
516
- }
517
- else {
518
- for (const change of commit.stateChanges) {
519
- const existing = state.get(change.relativePath);
520
- const existingHash = existing === undefined ? null : sha256(existing);
521
- if (existingHash !== change.expectedPreviousHash)
522
- throw new FileNativeWorldCreationError("world_corrupt", `Immutable commit-chain previous hash does not match: ${change.relativePath}`);
523
- if (sha256(change.canonicalNextBytes) !== change.nextHash)
524
- throw new FileNativeWorldCreationError("world_corrupt", `Immutable commit-chain next hash does not match: ${change.relativePath}`);
525
- state.set(change.relativePath, change.canonicalNextBytes);
526
- }
527
- }
528
- history.push(...structuredClone(commit.historyAppend));
529
- 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);
530
578
  }
531
- return {
532
- worldId,
533
- head: target,
534
- state: [...state]
535
- .map(([path, contents]) => ({ path, contents }))
536
- .sort((a, b) => a.path.localeCompare(b.path)),
537
- history,
538
- additionalMaterials,
539
- };
540
579
  }
541
580
  async repairMaterialization(worldId) {
542
581
  assertIdentity(worldId, "World ID");
582
+ await this.ensureCurrentStorage(worldId);
543
583
  try {
544
584
  const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
545
585
  await this.bindPlayCallChain(worldId);
546
586
  const root = join(this.#worldsRoot, worldId);
547
- const authority = await readAcceptedAuthority(root);
548
- return authority?.commits.at(-1)?.operationId ?? null;
587
+ return (await new FileNativeAuthorityV3(root).readHead()).operationId;
549
588
  });
550
589
  return operationId === null
551
590
  ? { outcome: "not_started" }
@@ -572,6 +611,7 @@ export class FileNativeWorldStore {
572
611
  const finalRoot = join(this.#worldsRoot, worldId);
573
612
  const previous = await readOptionalJson(operationPath);
574
613
  if (previous !== null) {
614
+ await this.ensureCurrentStorage(previous.worldId);
575
615
  if (!(await derivationPublicationMatches(join(this.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
576
616
  throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
577
617
  return {
@@ -581,6 +621,7 @@ export class FileNativeWorldStore {
581
621
  }
582
622
  const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
583
623
  if (alreadyPublished !== null) {
624
+ await this.ensureCurrentStorage(alreadyPublished.worldId);
584
625
  if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
585
626
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
586
627
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
@@ -590,98 +631,112 @@ export class FileNativeWorldStore {
590
631
  world: await this.#currentSummary(alreadyPublished),
591
632
  };
592
633
  }
593
- const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
594
- const sourcePublication = await readPublicationAt(sourceRoot);
595
- const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
596
- const [sourceGenesis, selected, sourceAuthority, control] = await Promise.all([
597
- this.recoverEndpoint(input.sourceWorldId, "genesis"),
598
- this.recoverEndpoint(input.sourceWorldId, input.sourceHead),
599
- readAcceptedAuthority(sourceRoot),
600
- readTree(join(sourceRoot, "control")),
601
- ]);
602
- const cloned = cloneAuthorityPrefix({
603
- sourceWorldId: input.sourceWorldId,
604
- sourceHead: input.sourceHead,
605
- targetWorldId: worldId,
606
- sourceGenesis,
607
- sourceAuthority,
608
- });
609
- const staging = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
610
- await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
611
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
612
- try {
613
- await writeSurface(staging, "state", sourceGenesis.state);
614
- await writeSurface(staging, "control", control);
615
- await mkdir(join(staging, "history"), { recursive: true });
616
- await mkdir(join(staging, "runtime"), { recursive: true });
617
- for (const [index, message] of cloned.genesisHistory.entries())
618
- await writeIdempotentText(join(staging, "history", `00000000-${historyFileName(index + 1, message)}`), message.exactText);
619
- const genesis = {
620
- schemaVersion: 1,
621
- type: "file_native_genesis",
622
- worldId,
623
- operationId: input.operationId,
624
- parentEndpoint: "genesis",
625
- state: immutableFiles(sourceGenesis.state),
626
- control: immutableFiles(control),
627
- history: cloned.genesisHistory,
628
- additionalMaterials: cloned.genesisMaterials,
629
- };
630
- await writeJson(join(staging, "runtime", "genesis.json"), genesis);
631
- await writeJson(join(staging, "runtime", "additional-materials.json"), {
632
- head: "genesis",
633
- items: cloned.genesisMaterials,
634
- });
635
- for (const [index, commit] of cloned.commits.entries()) {
636
- await publishImmutableJson(join(staging, "runtime", "play-commits", `${createHash("sha256").update(JSON.stringify(commit)).digest("hex")}.json`), commit);
637
- await materializePlayCommit(staging, index + 1, {
638
- mode: commit.mode,
639
- historyAppend: commit.historyAppend,
640
- nextMaterials: commit.nextAdditionalMaterials,
641
- stateChanges: commit.stateChanges,
642
- ...(commit.timelineRevision === undefined
643
- ? {}
644
- : { timelineRevision: commit.timelineRevision }),
634
+ await this.ensureCurrentStorage(input.sourceWorldId);
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,
645
653
  });
646
- }
647
- if (cloned.commits.length > 0)
648
- await writeJson(join(staging, "runtime", "play-authority.json"), {
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 = {
649
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"), {
650
678
  head: input.sourceHead,
651
- commits: cloned.commits,
679
+ items: selected.additionalMaterials,
652
680
  });
653
- await writeJson(join(staging, "runtime", "materialized-head.json"), {
654
- head: input.sourceHead,
655
- });
656
- const materializedState = await readTree(join(staging, "state"));
657
- if (!isDeepStrictEqual(materializedState, selected.state))
658
- throw new FileNativeWorldCreationError("world_corrupt", "Fork Authority prefix cannot rebuild the source endpoint state");
659
- const publication = {
660
- schemaVersion: 1,
661
- worldId,
662
- title: derivedWorldName(sourceSummary.title),
663
- parentEndpoint: "genesis",
664
- operationId: input.operationId,
665
- sourceFingerprint: requestFingerprint,
666
- };
667
- await writeJson(join(staging, publicationFile), publication);
668
- await durableTree(staging);
669
- crashAtFileNativeAuthorityEdge("derivation_before_publish");
670
- await rename(staging, finalRoot);
671
- await syncDirectory(this.#worldsRoot);
672
- crashAtFileNativeAuthorityEdge("derivation_after_publish");
673
- await publishJson(operationPath, publication);
674
- return { outcome: "derived", world: toSummary(publication) };
681
+ await writeJson(join(staging, "runtime", "materialized-head.json"), {
682
+ schemaVersion: 3,
683
+ head: input.sourceHead,
684
+ sequence: clonedHead.sequence,
685
+ commitDigest: clonedHead.commitDigest,
686
+ });
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));
675
736
  }
676
737
  catch (error) {
677
- await rm(staging, { recursive: true, force: true }).catch(() => undefined);
678
- const published = await readOptionalJson(join(finalRoot, publicationFile));
679
- if (published !== null) {
680
- if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
681
- throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
682
- await publishJson(operationPath, published);
683
- return { outcome: "derived", world: toSummary(published) };
684
- }
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 });
685
740
  throw error;
686
741
  }
687
742
  }
@@ -698,7 +753,15 @@ export class FileNativeWorldStore {
698
753
  assertIdentity(operationId, "operation ID");
699
754
  let directRecord;
700
755
  try {
701
- const value = await readOptionalJson(this.#operationOutcomePath(operationId));
756
+ let value = await readOptionalJson(this.#operationOutcomePath(operationId));
757
+ if (isRecord(value) &&
758
+ (value.outcome === "acceptance_prepared" ||
759
+ value.outcome === "committed" ||
760
+ value.outcome === "committed_materialization_pending") &&
761
+ typeof value.worldId === "string") {
762
+ await this.ensureCurrentStorage(value.worldId);
763
+ value = await readOptionalJson(this.#operationOutcomePath(operationId));
764
+ }
702
765
  if (value === null)
703
766
  directRecord = null;
704
767
  else {
@@ -711,46 +774,18 @@ export class FileNativeWorldStore {
711
774
  throw error;
712
775
  throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
713
776
  }
777
+ if (directRecord?.outcome === "acceptance_prepared")
778
+ return resolvePreparedOperationOutcome(this.#worldsRoot, operationId, directRecord);
714
779
  const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
715
780
  if (direct?.outcome === "committed" ||
716
781
  direct?.outcome === "committed_materialization_pending") {
717
- if (direct.outcome === "committed") {
718
- await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, direct.worldId), operationId, direct);
719
- const materialized = await readOptionalJson(join(this.#worldsRoot, direct.worldId, "runtime", "materialized-head.json"));
720
- if (materialized?.head !== direct.head)
721
- return { ...direct, outcome: "committed_materialization_pending" };
722
- }
723
- else {
724
- await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, direct.worldId), operationId, direct);
725
- await assertPendingMaterializationCompatible(join(this.#worldsRoot, direct.worldId), operationId);
726
- }
727
- return direct;
728
- }
729
- const worlds = await readDirectoryNames(this.#worldsRoot);
730
- for (const worldId of worlds) {
731
- const authority = await readAcceptedAuthority(join(this.#worldsRoot, worldId));
732
- const commitIndex = authority?.commits.findIndex((candidate) => candidate.operationId === operationId) ?? -1;
733
- const commit = authority?.commits[commitIndex];
734
- if (commit !== undefined) {
735
- const materialized = await readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "materialized-head.json"));
736
- const materializedIndex = materialized?.head === "genesis"
737
- ? -1
738
- : (authority?.commits.findIndex(({ head }) => head === materialized?.head) ?? -1);
739
- return {
740
- outcome: materializedIndex >= commitIndex
741
- ? "committed"
742
- : "committed_materialization_pending",
743
- worldId,
744
- parentHead: commit.parentHead,
745
- head: commit.head,
746
- historyAppend: commit.historyAppend.map(({ role, exactText }) => ({
747
- role,
748
- exactText,
749
- })),
750
- nextAdditionalMaterials: commit.nextAdditionalMaterials,
751
- mode: commit.mode,
752
- };
753
- }
782
+ const worldRoot = join(this.#worldsRoot, direct.worldId);
783
+ const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, direct);
784
+ const materialized = await readMaterializedCheckpoint(worldRoot);
785
+ if (materialized !== null && materialized.sequence >= commit.sequence)
786
+ return { ...direct, outcome: "committed" };
787
+ await assertPendingMaterializationCompatible(worldRoot, direct);
788
+ return { ...direct, outcome: "committed_materialization_pending" };
754
789
  }
755
790
  return direct?.outcome === "in_progress"
756
791
  ? { outcome: "in_progress" }
@@ -773,11 +808,13 @@ export class FileNativeWorldStore {
773
808
  throw new TypeError("Edited player message cannot be empty");
774
809
  if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
775
810
  throw new TypeError("Timeline-revision request fingerprint is invalid");
811
+ await this.ensureCurrentStorage(input.worldId);
776
812
  const existing = await this.getOperationOutcome(input.operationId);
777
813
  if (isCommittedOutcome(existing)) {
778
- const authority = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
779
- 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;
780
816
  if (existing.worldId !== input.worldId ||
817
+ fact.digest !== existing.commitDigest ||
781
818
  commit?.mode !== "timeline_revision" ||
782
819
  commit.timelineRevision?.restoresHead !== input.restoresHead ||
783
820
  commit.timelineRevision.replacesHead !== input.replacesHead ||
@@ -791,15 +828,13 @@ export class FileNativeWorldStore {
791
828
  return existing;
792
829
  }
793
830
  const root = join(this.#worldsRoot, input.worldId);
794
- const [authority, replacement] = await Promise.all([
795
- readAcceptedAuthority(root),
796
- 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),
797
835
  ]);
798
- const replaced = authority?.commits.find(({ head }) => head === input.replacesHead);
799
- const logicalParent = replaced?.mode === "timeline_revision"
800
- ? replaced.timelineRevision?.restoresHead
801
- : replaced?.parentHead;
802
- if (replaced === undefined ||
836
+ const logicalParent = replaced?.timelineParent.head;
837
+ if (replaced === null ||
803
838
  !replaced.historyAppend.some(({ role }) => role === "player") ||
804
839
  logicalParent !== input.restoresHead)
805
840
  throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
@@ -808,19 +843,18 @@ export class FileNativeWorldStore {
808
843
  worldId: input.worldId,
809
844
  parentHead: input.expectedCurrentHead,
810
845
  historyAppend: [{ role: "player", exactText: input.replacementText }],
811
- nextMaterials: structuredClone(replacement.additionalMaterials),
846
+ nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
812
847
  stateChanges: [],
813
848
  mode: "timeline_revision",
814
849
  timelineRevision: {
815
850
  restoresHead: input.restoresHead,
816
851
  replacesHead: input.replacesHead,
817
852
  requestFingerprint: input.requestFingerprint,
818
- replacementState: immutableFiles(replacement.state),
819
- replacementHistory: structuredClone(replacement.history),
820
853
  },
821
854
  });
822
855
  }
823
856
  async commitCorrection(input) {
857
+ await this.ensureCurrentStorage(input.worldId);
824
858
  try {
825
859
  const commit = async () => {
826
860
  let operationReserved = input.stateOperationClaim !== undefined;
@@ -833,7 +867,7 @@ export class FileNativeWorldStore {
833
867
  operationId: input.operationId,
834
868
  worldId: input.worldId,
835
869
  parentHead: input.parentHead,
836
- nextMaterials: input.nextMaterials,
870
+ nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
837
871
  stateChanges: input.stateChanges,
838
872
  mode: "correction",
839
873
  historyAppend: [],
@@ -856,40 +890,39 @@ export class FileNativeWorldStore {
856
890
  * separate commits; there is no synthetic call-chain-wide settlement boundary.
857
891
  */
858
892
  async commitPlayStep(input) {
893
+ await this.ensureCurrentStorage(input.worldId);
859
894
  if (input.historyAppend.length === 0 && input.stateChanges.length === 0)
860
895
  throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
861
896
  return this.#commitHistoryChange({
862
897
  ...input,
898
+ nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
863
899
  mode: "play",
864
900
  });
865
901
  }
866
- async readPlayCallChain(worldId) {
867
- assertIdentity(worldId, "World ID");
868
- return readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"));
869
- }
870
- async writePlayCallChain(worldId, value) {
871
- assertIdentity(worldId, "World ID");
872
- await publishJson(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"), value);
873
- }
874
- async removePlayCallChain(worldId) {
875
- assertIdentity(worldId, "World ID");
876
- await rm(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"), { force: true });
877
- }
878
902
  async #commitHistoryChange(input) {
879
903
  const existing = await this.getOperationOutcome(input.operationId);
880
904
  if (isCommittedOutcome(existing)) {
881
905
  assertSameOperationOutcome(existing, input);
882
- const accepted = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
883
- const commit = accepted?.commits.find(({ operationId }) => operationId === input.operationId);
884
- if (commit?.mode !== input.mode ||
885
- JSON.stringify(commit?.stateChanges) !==
886
- JSON.stringify(input.stateChanges) ||
887
- !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))
888
910
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
889
911
  return existing;
890
912
  }
913
+ const prepared = existing.outcome === "in_progress"
914
+ ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
915
+ : null;
916
+ if (prepared !== null) {
917
+ const comparable = {
918
+ ...structuredClone(prepared),
919
+ outcome: "committed_materialization_pending",
920
+ };
921
+ assertSameOperationOutcome(comparable, input);
922
+ }
891
923
  if (existing.outcome !== "not_started" &&
892
- !(input.operationReserved === true && existing.outcome === "in_progress"))
924
+ !(existing.outcome === "in_progress" &&
925
+ (input.operationReserved === true || prepared !== null)))
893
926
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
894
927
  const root = join(this.#worldsRoot, input.worldId);
895
928
  try {
@@ -897,42 +930,40 @@ export class FileNativeWorldStore {
897
930
  const concurrent = await this.#readOperationOutcome(input.operationId);
898
931
  if (isCommittedOutcome(concurrent)) {
899
932
  assertSameOperationOutcome(concurrent, input);
900
- const accepted = await readAcceptedAuthority(join(this.#worldsRoot, concurrent.worldId));
901
- const knownCommit = accepted?.commits.find(({ operationId }) => operationId === input.operationId);
902
- if (knownCommit?.mode !== input.mode ||
903
- !isDeepStrictEqual(knownCommit.stateChanges, input.stateChanges) ||
904
- !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))
905
936
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
906
937
  return concurrent;
907
938
  }
908
- const authorityPath = join(root, "runtime", "play-authority.json");
909
- const authority = (await readAcceptedAuthority(root)) ?? {
910
- schemaVersion: 1,
911
- head: "genesis",
912
- commits: [],
913
- };
939
+ const concurrentPrepared = concurrent.outcome === "in_progress"
940
+ ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
941
+ : null;
942
+ if (concurrentPrepared !== null)
943
+ assertSameOperationOutcome({
944
+ ...structuredClone(concurrentPrepared),
945
+ outcome: "committed_materialization_pending",
946
+ }, input);
947
+ else if (concurrent.outcome === "in_progress" &&
948
+ input.operationReserved !== true)
949
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
950
+ const authorityStore = new FileNativeAuthorityV3(root);
951
+ const authority = await authorityStore.readHead();
914
952
  if (authority.head !== input.parentHead) {
915
953
  throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
916
954
  }
917
955
  const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
918
956
  if ((materialized?.head ?? "genesis") !== authority.head)
919
957
  throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
920
- const [currentState, currentControl] = await Promise.all([
921
- readTree(join(root, "state")),
958
+ const [basisState, currentControl] = await Promise.all([
959
+ authorityStore.recoverState(input.timelineRevision?.restoresHead ?? authority.head),
922
960
  readTree(join(root, "control")),
923
961
  ]);
924
- const candidateState = new Map(currentState.map((file) => [file.path, file.contents]));
962
+ const candidateState = new Map(basisState.map((file) => [file.path, file.contents]));
925
963
  if (input.mode === "timeline_revision") {
926
964
  if (input.timelineRevision === undefined ||
927
965
  input.stateChanges.length !== 0)
928
966
  throw new FileNativeWorldCreationError("operation_conflict", "Timeline-revision commit payload is invalid");
929
- candidateState.clear();
930
- for (const file of input.timelineRevision.replacementState) {
931
- assertRelativePath(file.path);
932
- if (sha256(file.canonicalBytes) !== file.sha256)
933
- throw new FileNativeWorldCreationError("operation_conflict", `Timeline-revision state hash conflicts: ${file.path}`);
934
- candidateState.set(file.path, file.canonicalBytes);
935
- }
936
967
  }
937
968
  else {
938
969
  if (input.timelineRevision !== undefined)
@@ -959,64 +990,58 @@ export class FileNativeWorldStore {
959
990
  ], { requireOpening: false });
960
991
  if (inspection.status !== "usable")
961
992
  throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
962
- const sequence = authority.commits.length + 1;
963
- const head = `commit:${sequence}`;
964
- const commit = {
965
- schemaVersion: 1,
993
+ if (concurrentPrepared === null)
994
+ await authorityStore.discardUnacceptedNextEpoch();
995
+ crashAtFileNativeAuthorityEdge("before_commit_acceptance");
996
+ const preparedAppend = await authorityStore.prepareAppend({
966
997
  operationId: input.operationId,
967
998
  parentHead: input.parentHead,
968
- head,
999
+ ...(input.timelineRevision === undefined
1000
+ ? {}
1001
+ : { timelineParentHead: input.timelineRevision.restoresHead }),
969
1002
  mode: input.mode,
970
- historyAppend: input.historyAppend.map((message, index) => ({
971
- messageId: `${input.worldId}.message.${sequence}.${index + 1}.${message.role}`,
972
- ...structuredClone(message),
973
- })),
1003
+ historyAppend: input.historyAppend,
974
1004
  stateChanges: structuredClone(input.stateChanges),
975
- nextAdditionalMaterials: input.nextMaterials,
1005
+ nextMaterials: input.nextMaterials,
976
1006
  ...(input.mode === "correction"
977
1007
  ? {
978
1008
  correctionTargets: input.stateChanges.map(({ documentId }) => documentId),
979
1009
  corrects: input.parentHead,
980
1010
  }
981
1011
  : {}),
982
- ...(input.mode === "timeline_revision"
983
- ? {
984
- timelineRevision: structuredClone(input.timelineRevision),
985
- }
986
- : {}),
987
- };
988
- crashAtFileNativeAuthorityEdge("before_commit_acceptance");
989
- await publishImmutableJson(join(root, "runtime", "play-commits", `${createHash("sha256").update(JSON.stringify(commit)).digest("hex")}.json`), commit);
990
- await publishJson(authorityPath, {
991
- schemaVersion: 1,
992
- head,
993
- commits: [...authority.commits, commit],
1012
+ ...(input.timelineRevision === undefined
1013
+ ? {}
1014
+ : { timelineRevision: input.timelineRevision }),
994
1015
  });
995
- crashAtFileNativeAuthorityEdge("after_commit_acceptance");
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;
996
1022
  const pending = {
997
1023
  outcome: "committed_materialization_pending",
998
1024
  worldId: input.worldId,
999
1025
  parentHead: input.parentHead,
1000
- head,
1026
+ head: commit.head,
1027
+ commitDigest: preparedAppend.commitDigest,
1001
1028
  historyAppend: structuredClone(input.historyAppend),
1002
1029
  nextAdditionalMaterials: structuredClone(input.nextMaterials),
1003
1030
  mode: input.mode,
1004
1031
  };
1032
+ await publishJson(this.#operationOutcomePath(input.operationId), {
1033
+ ...pending,
1034
+ outcome: "acceptance_prepared",
1035
+ });
1036
+ crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
1037
+ await authorityStore.publishPrepared(preparedAppend);
1038
+ crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1005
1039
  await publishJson(this.#operationOutcomePath(input.operationId), pending);
1006
1040
  try {
1007
- await materializePlayCommit(root, sequence, {
1008
- mode: commit.mode,
1009
- historyAppend: commit.historyAppend,
1010
- nextMaterials: input.nextMaterials,
1011
- stateChanges: input.stateChanges,
1012
- ...(commit.timelineRevision === undefined
1013
- ? {}
1014
- : { timelineRevision: commit.timelineRevision }),
1015
- });
1041
+ const recovered = await authorityStore.recover(commit.head);
1042
+ await assertPendingMaterializationCompatible(root, pending);
1043
+ await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered);
1016
1044
  crashAtFileNativeAuthorityEdge("after_materialization");
1017
- await publishJson(join(root, "runtime", "materialized-head.json"), {
1018
- head,
1019
- });
1020
1045
  const outcome = { ...pending, outcome: "committed" };
1021
1046
  await publishJson(this.#operationOutcomePath(input.operationId), outcome);
1022
1047
  return outcome;
@@ -1033,17 +1058,18 @@ export class FileNativeWorldStore {
1033
1058
  catch (error) {
1034
1059
  if (error instanceof WorldOperationBusyError)
1035
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 });
1036
1063
  throw error;
1037
1064
  }
1038
1065
  }
1039
1066
  async readAuthorityHistory(worldId) {
1040
1067
  assertIdentity(worldId, "World ID");
1068
+ await this.ensureCurrentStorage(worldId);
1041
1069
  const root = join(this.#worldsRoot, worldId);
1042
1070
  await readPublicationAt(root);
1043
- const authority = await readAcceptedAuthority(root);
1044
- return authority === null
1045
- ? { head: "genesis", commits: [] }
1046
- : { head: authority.head, commits: structuredClone(authority.commits) };
1071
+ const authority = await new FileNativeAuthorityV3(root).readHistory();
1072
+ return structuredClone(authority);
1047
1073
  }
1048
1074
  async readAuthorityEndpoint(worldId, endpoint) {
1049
1075
  const recovered = await this.recoverEndpoint(worldId, endpoint);
@@ -1115,7 +1141,7 @@ export class FileNativeWorldStore {
1115
1141
  return join(this.#operationsRoot, `derive-${operationDigest(operationId)}.json`);
1116
1142
  }
1117
1143
  #preview(input, worldId) {
1118
- const openingMessage = genesisOpeningMessage(worldId, openingText(input.packageFiles));
1144
+ const openingMessage = genesisOpeningMessage(openingText(input.packageFiles));
1119
1145
  return this.#promptCompiler.preview({
1120
1146
  endpoint: { id: `${worldId}:genesis`, commit: "genesis" },
1121
1147
  hostBinding: input.prompt.hostBinding,
@@ -1148,9 +1174,9 @@ function openingText(files) {
1148
1174
  throw new FileNativeWorldCreationError("content_package_needs_repair", "Content package is missing a usable opening.md");
1149
1175
  return opening.contents;
1150
1176
  }
1151
- function genesisOpeningMessage(worldId, exactText) {
1177
+ function genesisOpeningMessage(exactText) {
1152
1178
  return {
1153
- messageId: `${worldId}.message.genesis.narrator`,
1179
+ messageId: "message.genesis.narrator",
1154
1180
  role: "narrator",
1155
1181
  exactText,
1156
1182
  };
@@ -1222,19 +1248,6 @@ async function readTree(root, prefix = "") {
1222
1248
  }
1223
1249
  return files;
1224
1250
  }
1225
- async function readDirectoryNames(root) {
1226
- try {
1227
- return (await readdir(root, { withFileTypes: true }))
1228
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
1229
- .map((entry) => entry.name)
1230
- .sort();
1231
- }
1232
- catch (error) {
1233
- if (isNodeError(error) && error.code === "ENOENT")
1234
- return [];
1235
- throw error;
1236
- }
1237
- }
1238
1251
  function immutableFiles(files) {
1239
1252
  return files.map(({ path, contents }) => ({
1240
1253
  path,
@@ -1286,13 +1299,15 @@ function assertOperationOutcomeIntegrity(value) {
1286
1299
  throw corruptOperationOutcome();
1287
1300
  return;
1288
1301
  }
1289
- if (value.outcome === "committed" ||
1302
+ if (value.outcome === "acceptance_prepared" ||
1303
+ value.outcome === "committed" ||
1290
1304
  value.outcome === "committed_materialization_pending") {
1291
1305
  if (!hasExactKeys(value, [
1292
1306
  "outcome",
1293
1307
  "worldId",
1294
1308
  "parentHead",
1295
1309
  "head",
1310
+ "commitDigest",
1296
1311
  "historyAppend",
1297
1312
  "nextAdditionalMaterials",
1298
1313
  "mode",
@@ -1303,6 +1318,8 @@ function assertOperationOutcomeIntegrity(value) {
1303
1318
  !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1304
1319
  typeof value.head !== "string" ||
1305
1320
  !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1321
+ typeof value.commitDigest !== "string" ||
1322
+ !/^[a-f0-9]{64}$/u.test(value.commitDigest) ||
1306
1323
  !Array.isArray(value.historyAppend) ||
1307
1324
  !value.historyAppend.every(isPlayHistoryAppendInput) ||
1308
1325
  !Array.isArray(value.nextAdditionalMaterials) ||
@@ -1318,53 +1335,95 @@ function assertOperationOutcomeIntegrity(value) {
1318
1335
  function projectOperationOutcome(value) {
1319
1336
  return structuredClone(value);
1320
1337
  }
1321
- function corruptOperationOutcome() {
1322
- return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1338
+ async function readAcceptancePreparedOutcome(path) {
1339
+ const value = await readOptionalJson(path);
1340
+ if (value === null)
1341
+ return null;
1342
+ assertOperationOutcomeIntegrity(value);
1343
+ if (value.outcome !== "acceptance_prepared")
1344
+ return null;
1345
+ return structuredClone(value);
1323
1346
  }
1324
- async function assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, outcome) {
1325
- const authority = await readAcceptedAuthority(worldRoot);
1326
- const commit = authority?.commits.find((candidate) => candidate.operationId === operationId);
1327
- if (commit?.parentHead !== outcome.parentHead ||
1328
- commit?.head !== outcome.head ||
1329
- commit?.mode !== outcome.mode ||
1330
- !isDeepStrictEqual(commit?.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
1331
- !isDeepStrictEqual(commit?.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
1347
+ async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
1348
+ const prepared = value;
1349
+ const worldRoot = join(worldsRoot, prepared.worldId);
1350
+ const authorityStore = new FileNativeAuthorityV3(worldRoot);
1351
+ const authority = await authorityStore.readHead();
1352
+ if (authority.head === prepared.parentHead)
1353
+ return { outcome: "in_progress" };
1354
+ if (authority.operationId !== operationId)
1355
+ throw corruptOperationOutcome();
1356
+ const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, prepared);
1357
+ if (authority.sequence < commit.sequence)
1332
1358
  throw corruptOperationOutcome();
1359
+ const materialized = await readMaterializedCheckpoint(worldRoot);
1360
+ const outcome = materialized !== null && materialized.sequence >= commit.sequence
1361
+ ? "committed"
1362
+ : "committed_materialization_pending";
1363
+ const resolved = { ...structuredClone(prepared), outcome };
1364
+ if (outcome === "committed_materialization_pending")
1365
+ await assertPendingMaterializationCompatible(worldRoot, resolved);
1366
+ return resolved;
1333
1367
  }
1334
- async function assertPendingMaterializationCompatible(worldRoot, operationId) {
1335
- const authority = await readAcceptedAuthority(worldRoot);
1336
- const sequence = authority?.commits.findIndex((candidate) => candidate.operationId === operationId) ?? -1;
1337
- const commit = sequence < 0 ? undefined : authority?.commits[sequence];
1338
- if (commit === undefined)
1368
+ function corruptOperationOutcome() {
1369
+ return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1370
+ }
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))
1339
1380
  throw corruptOperationOutcome();
1340
- for (const change of commit.stateChanges) {
1341
- const path = join(worldRoot, "state", change.relativePath);
1342
- const existing = await readOptionalText(path);
1343
- const existingHash = existing === null
1344
- ? null
1345
- : `sha256:${createHash("sha256").update(existing).digest("hex")}`;
1346
- if (existingHash !== change.expectedPreviousHash &&
1347
- existingHash !== change.nextHash)
1348
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World-state materialization conflict: ${change.relativePath}`);
1349
- }
1350
- for (const [index, message] of commit.historyAppend.entries()) {
1351
- const existing = await readOptionalText(join(worldRoot, "history", `${String(sequence + 1).padStart(8, "0")}-${historyFileName(index + 1, message)}`));
1352
- if (existing !== null && existing !== message.exactText)
1353
- throw new FileNativeWorldCreationError("inconsistent_materialization", "History-message materialization content conflict");
1354
- }
1381
+ return commit;
1382
+ }
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));
1355
1394
  const existingMaterials = await readOptionalJson(join(worldRoot, "runtime", "additional-materials.json"));
1356
- if (existingMaterials?.head === commit.head &&
1357
- !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))))
1358
1400
  throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1359
1401
  }
1360
- async function readOptionalText(path) {
1361
- try {
1362
- return await readFile(path, "utf8");
1363
- }
1364
- catch (error) {
1365
- if (isNodeError(error) && error.code === "ENOENT")
1366
- return null;
1367
- 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);
1368
1427
  }
1369
1428
  }
1370
1429
  function isAuthorityMaterialSelection(value) {
@@ -1400,228 +1459,36 @@ async function publishJson(path, value) {
1400
1459
  await rename(temporary, path);
1401
1460
  await syncDirectory(dirname(path));
1402
1461
  }
1403
- async function publishImmutableJson(path, value) {
1404
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
1405
- const contents = `${JSON.stringify(value, null, 2)}\n`;
1406
- const temporary = `${path}.${randomUUID()}.tmp`;
1407
- try {
1408
- await writeFile(temporary, contents, {
1409
- encoding: "utf8",
1410
- flag: "wx",
1411
- mode: 0o600,
1412
- });
1413
- await syncFile(temporary);
1414
- try {
1415
- await link(temporary, path);
1416
- await syncDirectory(dirname(path));
1417
- return true;
1418
- }
1419
- catch (error) {
1420
- if (!isNodeError(error) || error.code !== "EEXIST")
1421
- throw error;
1422
- if ((await readFile(path, "utf8")) !== contents)
1423
- throw new FileNativeWorldCreationError("operation_conflict", "Immutable play-commit file conflicts");
1424
- return false;
1425
- }
1426
- }
1427
- finally {
1428
- await rm(temporary, { force: true });
1429
- }
1430
- }
1431
- async function readAcceptedAuthority(root) {
1432
- try {
1433
- const published = await readOptionalJson(join(root, "runtime", "play-authority.json"));
1434
- if (published === null) {
1435
- const commitRoot = join(root, "runtime", "play-commits");
1436
- const names = await readDirectoryFileNames(commitRoot);
1437
- if (names.length === 0)
1438
- return null;
1439
- const remaining = await Promise.all(names.map(async (name) => {
1440
- const commit = await readJson(join(commitRoot, name));
1441
- assertFileNativePlayCommit(commit);
1442
- const canonicalName = `${createHash("sha256")
1443
- .update(JSON.stringify(commit))
1444
- .digest("hex")}.json`;
1445
- if (name !== canonicalName)
1446
- throw new FileNativeWorldCreationError("world_corrupt", "Immutable commit does not match the durable path identity");
1447
- return commit;
1448
- }));
1449
- const commits = [];
1450
- let parent = "genesis";
1451
- while (remaining.length > 0) {
1452
- const matches = remaining.filter((commit) => commit.parentHead === parent);
1453
- if (matches.length !== 1)
1454
- throw new FileNativeWorldCreationError("world_corrupt", "Immutable commits cannot rebuild one unique authority chain");
1455
- const next = matches[0];
1456
- commits.push(next);
1457
- remaining.splice(remaining.indexOf(next), 1);
1458
- parent = next.head;
1459
- }
1460
- assertFileNativePlayAuthority({
1461
- schemaVersion: 1,
1462
- head: parent,
1463
- commits,
1464
- });
1465
- return { schemaVersion: 1, head: parent, commits };
1466
- }
1467
- assertFileNativePlayAuthority(published);
1468
- const commits = [];
1469
- for (const recorded of published.commits) {
1470
- const digest = createHash("sha256")
1471
- .update(JSON.stringify(recorded))
1472
- .digest("hex");
1473
- const immutable = await readJson(join(root, "runtime", "play-commits", `${digest}.json`)).catch((error) => {
1474
- throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint is missing an immutable commit: ${recorded.head}`, { cause: error });
1475
- });
1476
- assertFileNativePlayCommit(immutable);
1477
- if (JSON.stringify(immutable) !== JSON.stringify(recorded))
1478
- throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint does not match its immutable commit: ${recorded.head}`);
1479
- commits.push(immutable);
1480
- }
1481
- return { ...published, commits };
1482
- }
1483
- catch (error) {
1484
- if (error instanceof FileNativeWorldCreationError)
1485
- throw error;
1486
- throw new FileNativeWorldCreationError("world_corrupt", "Play Authority durable data is corrupt", { cause: error });
1487
- }
1488
- }
1489
- function assertFileNativePlayAuthority(value) {
1490
- if (!isRecord(value) ||
1491
- !hasExactKeys(value, ["schemaVersion", "head", "commits"]) ||
1492
- value.schemaVersion !== 1 ||
1493
- typeof value.head !== "string" ||
1494
- !Array.isArray(value.commits))
1495
- throw new Error("Play Authority has an invalid shape");
1496
- const operationIds = new Set();
1497
- const heads = new Set();
1498
- const commitsByHead = new Map();
1499
- let parent = "genesis";
1500
- for (const [index, commit] of value.commits.entries()) {
1501
- assertFileNativePlayCommit(commit);
1502
- const revision = commit.timelineRevision;
1503
- const replaced = revision === undefined
1504
- ? undefined
1505
- : commitsByHead.get(revision.replacesHead);
1506
- const replacedLogicalParent = replaced?.mode === "timeline_revision"
1507
- ? replaced.timelineRevision.restoresHead
1508
- : replaced?.parentHead;
1509
- if (commit.parentHead !== parent ||
1510
- commit.head !== `commit:${index + 1}` ||
1511
- operationIds.has(commit.operationId) ||
1512
- heads.has(commit.head) ||
1513
- (commit.mode === "timeline_revision" &&
1514
- ((revision.restoresHead !== "genesis" &&
1515
- !heads.has(revision.restoresHead)) ||
1516
- replaced === undefined ||
1517
- !replaced.historyAppend.some(({ role }) => role === "player") ||
1518
- replacedLogicalParent !== revision.restoresHead)))
1519
- throw new Error("Play Authority commit chain is invalid");
1520
- operationIds.add(commit.operationId);
1521
- heads.add(commit.head);
1522
- commitsByHead.set(commit.head, commit);
1523
- parent = commit.head;
1524
- }
1525
- if (value.head !== parent)
1526
- throw new Error("Play Authority head is invalid");
1527
- }
1528
- function assertFileNativePlayCommit(value) {
1529
- if (!isRecord(value))
1530
- throw new Error("Play commit has an invalid shape");
1531
- const correction = value.mode === "correction";
1532
- const timelineRevision = value.mode === "timeline_revision";
1533
- if (!hasExactKeys(value, [
1534
- "schemaVersion",
1535
- "operationId",
1536
- "parentHead",
1537
- "head",
1538
- "mode",
1539
- "historyAppend",
1540
- "stateChanges",
1541
- "nextAdditionalMaterials",
1542
- ...(correction ? ["correctionTargets", "corrects"] : []),
1543
- ...(timelineRevision ? ["timelineRevision"] : []),
1544
- ]) ||
1545
- value.schemaVersion !== 1 ||
1546
- typeof value.operationId !== "string" ||
1547
- value.operationId.trim() === "" ||
1548
- typeof value.parentHead !== "string" ||
1549
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1550
- typeof value.head !== "string" ||
1551
- !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1552
- (value.mode !== "play" &&
1553
- value.mode !== "correction" &&
1554
- value.mode !== "timeline_revision") ||
1555
- !Array.isArray(value.historyAppend) ||
1556
- (correction
1557
- ? value.historyAppend.length !== 0
1558
- : timelineRevision
1559
- ? value.historyAppend.length !== 1 ||
1560
- !isRecord(value.historyAppend[0]) ||
1561
- value.historyAppend[0].role !== "player"
1562
- : value.historyAppend.length === 0 &&
1563
- (!Array.isArray(value.stateChanges) ||
1564
- value.stateChanges.length === 0)) ||
1565
- !value.historyAppend.every(isFileNativeHistoryMessage) ||
1566
- !Array.isArray(value.stateChanges) ||
1567
- !value.stateChanges.every(isFileNativeStateChange) ||
1568
- !Array.isArray(value.nextAdditionalMaterials) ||
1569
- !value.nextAdditionalMaterials.every(isAuthorityMaterialSelection))
1570
- throw new Error("Play commit has an invalid shape");
1571
- if (correction &&
1572
- (!Array.isArray(value.correctionTargets) ||
1573
- !value.correctionTargets.every((target) => typeof target === "string" && target.trim() !== "") ||
1574
- typeof value.corrects !== "string" ||
1575
- value.corrects !== value.parentHead))
1576
- throw new Error("Correction commit has an invalid shape");
1577
- if (timelineRevision &&
1578
- (value.stateChanges.length !== 0 ||
1579
- !isFileNativeTimelineRevision(value.timelineRevision)))
1580
- throw new Error("Timeline-revision commit has an invalid shape");
1581
- }
1582
- function isFileNativeTimelineRevision(value) {
1462
+ async function readMaterializedCheckpoint(root) {
1463
+ const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1464
+ if (value === null)
1465
+ return null;
1583
1466
  if (!isRecord(value) ||
1584
1467
  !hasExactKeys(value, [
1585
- "restoresHead",
1586
- "replacesHead",
1587
- "requestFingerprint",
1588
- "replacementState",
1589
- "replacementHistory",
1468
+ "schemaVersion",
1469
+ "head",
1470
+ "sequence",
1471
+ "commitDigest",
1590
1472
  ]) ||
1591
- typeof value.restoresHead !== "string" ||
1592
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.restoresHead) ||
1593
- typeof value.replacesHead !== "string" ||
1594
- !/^commit:[1-9][0-9]*$/u.test(value.replacesHead) ||
1595
- typeof value.requestFingerprint !== "string" ||
1596
- !/^sha256:[a-f0-9]{64}$/u.test(value.requestFingerprint) ||
1597
- !Array.isArray(value.replacementState) ||
1598
- !Array.isArray(value.replacementHistory) ||
1599
- !value.replacementHistory.every(isFileNativeHistoryMessage))
1600
- return false;
1601
- for (const file of value.replacementState) {
1602
- if (!isRecord(file) ||
1603
- !hasExactKeys(file, ["path", "sha256", "canonicalBytes"]) ||
1604
- typeof file.path !== "string" ||
1605
- typeof file.sha256 !== "string" ||
1606
- typeof file.canonicalBytes !== "string" ||
1607
- sha256(file.canonicalBytes) !== file.sha256)
1608
- return false;
1609
- try {
1610
- assertRelativePath(file.path);
1611
- }
1612
- catch {
1613
- return false;
1614
- }
1615
- }
1616
- return true;
1617
- }
1618
- function isFileNativeHistoryMessage(value) {
1619
- return (isRecord(value) &&
1620
- hasExactKeys(value, ["messageId", "role", "exactText"]) &&
1621
- typeof value.messageId === "string" &&
1622
- value.messageId.trim() !== "" &&
1623
- (value.role === "player" || value.role === "narrator") &&
1624
- typeof value.exactText === "string");
1473
+ value.schemaVersion !== 3 ||
1474
+ typeof value.head !== "string" ||
1475
+ !Number.isSafeInteger(value.sequence) ||
1476
+ Number(value.sequence) < 0 ||
1477
+ (value.commitDigest !== null &&
1478
+ (typeof value.commitDigest !== "string" ||
1479
+ !/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
1480
+ (Number(value.sequence) === 0
1481
+ ? value.head !== "genesis" || value.commitDigest !== null
1482
+ : value.head !== `commit:${String(value.sequence)}` ||
1483
+ value.commitDigest === null))
1484
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized world checkpoint has an invalid shape");
1485
+ return value;
1486
+ }
1487
+ function sameMaterializedEndpoint(checkpoint, authority) {
1488
+ return (checkpoint !== null &&
1489
+ checkpoint.head === authority.head &&
1490
+ checkpoint.sequence === authority.sequence &&
1491
+ checkpoint.commitDigest === authority.commitDigest);
1625
1492
  }
1626
1493
  function isPlayHistoryAppendInput(value) {
1627
1494
  return (isRecord(value) &&
@@ -1629,107 +1496,22 @@ function isPlayHistoryAppendInput(value) {
1629
1496
  (value.role === "player" || value.role === "narrator") &&
1630
1497
  typeof value.exactText === "string");
1631
1498
  }
1632
- function isFileNativeStateChange(value) {
1633
- if (!isRecord(value) ||
1634
- !hasExactKeys(value, [
1635
- "kind",
1636
- "documentId",
1637
- "stableShortRef",
1638
- "relativePath",
1639
- "codec",
1640
- "expectedPreviousHash",
1641
- "nextHash",
1642
- "canonicalNextBytes",
1643
- ]) ||
1644
- (value.kind !== "create" && value.kind !== "replace") ||
1645
- typeof value.documentId !== "string" ||
1646
- value.documentId.trim() === "" ||
1647
- typeof value.stableShortRef !== "string" ||
1648
- value.stableShortRef.trim() === "" ||
1649
- typeof value.relativePath !== "string" ||
1650
- (value.codec !== "yaml" && value.codec !== "markdown") ||
1651
- (value.expectedPreviousHash !== null &&
1652
- (typeof value.expectedPreviousHash !== "string" ||
1653
- !/^sha256:[a-f0-9]{64}$/u.test(value.expectedPreviousHash))) ||
1654
- typeof value.nextHash !== "string" ||
1655
- !/^sha256:[a-f0-9]{64}$/u.test(value.nextHash) ||
1656
- typeof value.canonicalNextBytes !== "string" ||
1657
- `sha256:${createHash("sha256")
1658
- .update(value.canonicalNextBytes)
1659
- .digest("hex")}` !== value.nextHash)
1660
- return false;
1661
- try {
1662
- assertRelativePath(value.relativePath);
1663
- }
1664
- catch {
1665
- return false;
1666
- }
1667
- return true;
1668
- }
1669
- async function readDirectoryFileNames(root) {
1670
- try {
1671
- return (await readdir(root, { withFileTypes: true }))
1672
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
1673
- .map((entry) => entry.name)
1674
- .sort();
1675
- }
1676
- catch (error) {
1677
- if (isNodeError(error) && error.code === "ENOENT")
1678
- return [];
1679
- throw error;
1680
- }
1681
- }
1682
- async function materializePlayCommit(root, sequence, input) {
1683
- if (input.mode === "timeline_revision") {
1684
- if (input.timelineRevision === undefined)
1685
- throw new FileNativeWorldCreationError("world_corrupt", "Timeline-revision commit is missing its materialization snapshot");
1686
- await replaceMaterializedTree(join(root, "state"), input.timelineRevision.replacementState.map(({ path, canonicalBytes }) => ({ path, contents: canonicalBytes })));
1687
- }
1688
- else {
1689
- for (const change of input.stateChanges) {
1690
- assertRelativePath(change.relativePath);
1691
- const path = join(root, "state", change.relativePath);
1692
- let existing = null;
1693
- try {
1694
- existing = await readFile(path, "utf8");
1695
- }
1696
- catch (error) {
1697
- if (!isNodeError(error) || error.code !== "ENOENT")
1698
- throw error;
1699
- }
1700
- const existingHash = existing === null ? null : sha256(existing);
1701
- if (existingHash === change.nextHash)
1702
- continue;
1703
- if (existingHash !== change.expectedPreviousHash)
1704
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World-state materialization conflict: ${change.relativePath}`);
1705
- await publishText(path, change.canonicalNextBytes);
1706
- }
1707
- }
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);
1708
1503
  crashAtFileNativeAuthorityEdge("after_state_materialization");
1709
- const historyRoot = join(root, "history");
1710
- await mkdir(historyRoot, { recursive: true });
1711
- if (input.mode === "timeline_revision") {
1712
- await replaceMaterializedTree(historyRoot, historySurfaceFiles([
1713
- ...input.timelineRevision.replacementHistory,
1714
- ...input.historyAppend,
1715
- ]));
1716
- }
1717
- else {
1718
- for (const [index, message] of input.historyAppend.entries())
1719
- await writeIdempotentText(join(historyRoot, `${String(sequence).padStart(8, "0")}-${historyFileName(index + 1, message)}`), message.exactText);
1720
- }
1721
- const materialsPath = join(root, "runtime", "additional-materials.json");
1722
- const nextMaterials = {
1723
- head: `commit:${sequence}`,
1724
- items: input.nextMaterials,
1725
- };
1726
- const existingMaterials = await readOptionalJson(materialsPath);
1727
- if (existingMaterials?.head === nextMaterials.head &&
1728
- JSON.stringify(existingMaterials.items) !==
1729
- JSON.stringify(nextMaterials.items))
1730
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1731
- if (JSON.stringify(existingMaterials) !== JSON.stringify(nextMaterials))
1732
- 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
+ });
1733
1515
  }
1734
1516
  async function replaceMaterializedTree(root, files) {
1735
1517
  const next = new Map();
@@ -1762,26 +1544,22 @@ async function publishText(path, contents) {
1762
1544
  await syncDirectory(dirname(path));
1763
1545
  }
1764
1546
  function historySurfaceFiles(messages) {
1765
- return messages.map((message) => {
1766
- const genesis = /\.message\.genesis(?:\.([1-9][0-9]*))?\.(player|narrator)$/u.exec(message.messageId);
1767
- const committed = /\.message\.([1-9][0-9]*)\.([1-9][0-9]*)\.(player|narrator)$/u.exec(message.messageId);
1768
- const sequence = genesis !== null ? 0 : Number.parseInt(committed?.[1] ?? "", 10);
1769
- const index = genesis !== null
1770
- ? Number.parseInt(genesis[1] ?? "1", 10)
1771
- : Number.parseInt(committed?.[2] ?? "", 10);
1772
- const role = genesis?.[2] ?? committed?.[3];
1773
- if (!Number.isSafeInteger(sequence) ||
1774
- sequence < 0 ||
1775
- !Number.isSafeInteger(index) ||
1776
- index < 1 ||
1777
- role !== message.role)
1778
- throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${message.messageId}`);
1779
- return {
1780
- path: `${String(sequence).padStart(8, "0")}-${historyFileName(index, message)}`,
1781
- contents: message.exactText,
1782
- };
1547
+ return projectFileNativeHistorySurface(messages, (messageId) => {
1548
+ throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${messageId}`);
1783
1549
  });
1784
1550
  }
1551
+ function materializedHistoryRecord(files) {
1552
+ const result = {};
1553
+ for (const file of files) {
1554
+ const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1555
+ if (messageId === null)
1556
+ throw new FileNativeWorldCreationError("world_corrupt", `Materialized history path is invalid: ${file.path}`);
1557
+ if (messageId in result)
1558
+ throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
1559
+ result[messageId] = file.contents;
1560
+ }
1561
+ return result;
1562
+ }
1785
1563
  async function writeIdempotentText(path, contents) {
1786
1564
  try {
1787
1565
  const existing = await readFile(path, "utf8");
@@ -1798,72 +1576,6 @@ async function writeIdempotentText(path, contents) {
1798
1576
  await syncDirectory(dirname(path));
1799
1577
  }
1800
1578
  }
1801
- function historyFileName(index, message) {
1802
- return `${String(index).padStart(2, "0")}-${message.role}-${operationDigest(message.messageId).slice(0, 12)}.md`;
1803
- }
1804
- function cloneAuthorityPrefix(input) {
1805
- const end = input.sourceHead === "genesis"
1806
- ? 0
1807
- : (input.sourceAuthority?.commits.findIndex(({ head }) => head === input.sourceHead) ?? -1) + 1;
1808
- if (input.sourceHead !== "genesis" && end === 0)
1809
- throw new FileNativeWorldCreationError("world_corrupt", `Cannot copy a nonexistent source Authority endpoint: ${input.sourceHead}`);
1810
- const sourceCommits = input.sourceAuthority?.commits.slice(0, end) ?? [];
1811
- const messageIds = new Map();
1812
- const bindMessageId = (source, target) => {
1813
- if (messageIds.has(source))
1814
- throw new FileNativeWorldCreationError("world_corrupt", `Source Authority contains a duplicate history-message identity: ${source}`);
1815
- messageIds.set(source, target);
1816
- };
1817
- for (const [index, message] of input.sourceGenesis.history.entries())
1818
- bindMessageId(message.messageId, index === 0 && message.role === "narrator"
1819
- ? `${input.targetWorldId}.message.genesis.narrator`
1820
- : `${input.targetWorldId}.message.genesis.${index + 1}.${message.role}`);
1821
- for (const [commitIndex, commit] of sourceCommits.entries())
1822
- for (const [messageIndex, message] of commit.historyAppend.entries())
1823
- bindMessageId(message.messageId, `${input.targetWorldId}.message.${commitIndex + 1}.${messageIndex + 1}.${message.role}`);
1824
- const rewriteHistory = (history) => history.map((message) => ({
1825
- ...structuredClone(message),
1826
- messageId: messageIds.get(message.messageId),
1827
- }));
1828
- return {
1829
- genesisHistory: rewriteHistory(input.sourceGenesis.history),
1830
- genesisMaterials: rewriteMaterials(input.sourceGenesis.additionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
1831
- commits: sourceCommits.map((commit) => ({
1832
- ...structuredClone(commit),
1833
- operationId: `clone-${operationDigest(`${input.targetWorldId}\0${commit.head}`).slice(0, 56)}`,
1834
- historyAppend: rewriteHistory(commit.historyAppend),
1835
- nextAdditionalMaterials: rewriteMaterials(commit.nextAdditionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
1836
- ...(commit.timelineRevision === undefined
1837
- ? {}
1838
- : {
1839
- timelineRevision: {
1840
- ...structuredClone(commit.timelineRevision),
1841
- replacementHistory: rewriteHistory(commit.timelineRevision.replacementHistory),
1842
- },
1843
- }),
1844
- })),
1845
- };
1846
- }
1847
- function rewriteMaterials(materials, messageIds, sourceWorldId, targetWorldId) {
1848
- return materials.map((material) => {
1849
- if (material.kind === "history_message") {
1850
- const message = messageIds.get(material.message);
1851
- if (message === undefined)
1852
- throw new FileNativeWorldCreationError("world_corrupt", `Fork material references a history message that cannot be remapped: ${material.message}`);
1853
- return { ...material, message };
1854
- }
1855
- if (material.kind === "history_commit") {
1856
- const sourcePrefix = `${sourceWorldId}.`;
1857
- return {
1858
- ...material,
1859
- commit: material.commit.startsWith(sourcePrefix)
1860
- ? `${targetWorldId}.${material.commit.slice(sourcePrefix.length)}`
1861
- : material.commit,
1862
- };
1863
- }
1864
- return structuredClone(material);
1865
- });
1866
- }
1867
1579
  function isCommittedOutcome(outcome) {
1868
1580
  return (outcome.outcome === "committed" ||
1869
1581
  outcome.outcome === "committed_materialization_pending");
@@ -1876,6 +1588,63 @@ function assertSameOperationOutcome(outcome, input) {
1876
1588
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different commit payload");
1877
1589
  }
1878
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
+ }
1879
1648
  function openWorldDocumentSnapshot(files) {
1880
1649
  return WorldDocumentStore.open({
1881
1650
  layout: "world_state",
@@ -1923,6 +1692,68 @@ async function durableTree(root) {
1923
1692
  }
1924
1693
  await syncDirectory(root);
1925
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
+ }
1926
1757
  async function readPublicationAt(root) {
1927
1758
  return readJson(join(root, publicationFile));
1928
1759
  }