narraeon 0.5.7 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +13 -0
  2. package/README.zh-CN.md +10 -0
  3. package/dist/node/protocol/gameplay.js +69 -0
  4. package/dist/node/protocol/resolution.js +274 -0
  5. package/dist/node/protocol/v1.js +15 -2
  6. package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
  7. package/dist/node/runtime/RuntimeErrorLog.js +95 -0
  8. package/dist/node/runtime/V1Runtime.js +58 -7
  9. package/dist/node/runtime/archive/WorldArchives.js +14 -46
  10. package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
  11. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
  12. package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
  14. package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
  15. package/dist/node/runtime/config/dataRootLock.js +108 -0
  16. package/dist/node/runtime/content/ContentWorkspace.js +10 -12
  17. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +50 -46
  18. package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
  21. package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
  22. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
  23. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +31 -170
  24. package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
  25. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +36 -239
  26. package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
  27. package/dist/node/runtime/play/PlayCallChain.js +359 -359
  28. package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
  29. package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
  30. package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
  31. package/dist/node/runtime/play/TabletopResolution.js +372 -0
  32. package/dist/node/runtime/play/TabletopRules.js +141 -0
  33. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -10
  34. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +35 -2
  35. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
  36. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
  37. package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
  38. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
  39. package/dist/node/runtime/world/FileNativeWorldStore.js +266 -783
  40. package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
  41. package/dist/node/runtime/world/WorldActivity.js +14 -22
  42. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
  43. package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
  44. package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
  45. package/dist/node/runtime/world/WorldQueue.js +46 -0
  46. package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
  47. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -12
  48. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
  49. package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
  50. package/dist/node/server/ConversationStream.js +2 -0
  51. package/dist/node/server/mobile.js +20 -0
  52. package/dist/node/server/startWebServer.js +15 -8
  53. package/dist/node/shared/ordered-author-prompts.js +8 -0
  54. package/dist/node/shared/ordered-play-prompts.js +23 -0
  55. package/dist/node/shared/tabletop-guidance.js +100 -0
  56. package/dist/web/assets/index-7GzLSUpU.js +1323 -0
  57. package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
  58. package/dist/web/index.html +2 -2
  59. package/package.json +2 -1
  60. package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
  61. package/dist/web/assets/index-DxNtxzLR.js +0 -1255
  62. package/dist/web/assets/index-DzOnu9uv.css +0 -1
@@ -1,12 +1,14 @@
1
1
  import { captureNarrativeOrigin, snapshotBackground, isHistoryBackground, } from "../history/HistoryBackground.js";
2
- import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
2
+ import { FileNativeArtifactStore, forkArtifactClosure, } from "../artifact/FileNativeArtifactStore.js";
3
+ import { migrateLegacyArtifactStore } from "../artifact/LegacyArtifactStoreMigration.js";
3
4
  import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
4
5
  import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
6
+ import { assertSameGameplay, readGameplay, } from "../../protocol/gameplay.js";
5
7
  import { HistoryQuery } from "../history/HistoryQuery.js";
6
8
  import { exportStory } from "../history/StoryExport.js";
7
9
  import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
8
10
  import { createHash, randomUUID } from "node:crypto";
9
- import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
11
+ import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
10
12
  import { dirname, join, resolve } from "node:path";
11
13
  import { isDeepStrictEqual } from "node:util";
12
14
  import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, } from "../content/FileNativeContentTree.js";
@@ -16,10 +18,11 @@ import { WorldDocumentStore } from "./WorldDocumentStore.js";
16
18
  import { readWorldActivity, updateWorldActivity, } from "./WorldActivity.js";
17
19
  import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
18
20
  import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
19
- import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
20
- import { FileNativeAuthorityV3, FileNativeAuthorityV3Error, } from "./FileNativeAuthorityV3.js";
21
+ import { FileNativeWorldStorageMigrator, UnsupportedLegacyStorageError, } from "./FileNativeWorldStorageMigrator.js";
22
+ import { WorldAuthorityError, WorldLedgerAuthority, } from "./WorldLedgerAuthority.js";
23
+ import { forgetLedger, rewindTo } from "./WorldTurnLedger.js";
21
24
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
22
- import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
25
+ import { projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
23
26
  const publicationFile = "publication.json";
24
27
  const localMetadataFile = "local.json";
25
28
  export class FileNativeWorldCreationError extends Error {
@@ -36,11 +39,21 @@ export class FileNativeWorldNotFoundError extends Error {
36
39
  this.name = "FileNativeWorldNotFoundError";
37
40
  }
38
41
  }
42
+ /**
43
+ * Completed operations kept per world for same-process duplicate guards. A
44
+ * pending player-revision intent resumes against its own world's entry, so
45
+ * retention is per world: another world's traffic cannot evict it.
46
+ */
47
+ const RETAINED_OPERATION_OUTCOMES_PER_WORLD = 256;
39
48
  export class FileNativeWorldStore {
40
49
  extensionRequests;
41
50
  #documentMaintenanceCache = new Map();
42
51
  #worldsRoot;
43
- #operationsRoot;
52
+ // Process-local duplicate guard keyed by operation ID (ADR-0044): entries are
53
+ // not persisted, so a restart forgets them and a repeated operation redoes.
54
+ // Completed entries are bounded per world; in-progress entries are not.
55
+ #operationOutcomes = new Map();
56
+ #retainedOperationIds = new Map();
44
57
  #promptCompiler;
45
58
  #activeControlUsers = new Map();
46
59
  #openingWorldRevisions = new Set();
@@ -55,23 +68,26 @@ export class FileNativeWorldStore {
55
68
  const root = resolve(dataRoot);
56
69
  this.#worldsRoot = join(root, "worlds-file-native");
57
70
  this.extensionRequests = new WorldExtensionRequests(this.#worldsRoot);
58
- this.#operationsRoot = join(root, "operations");
59
71
  this.#promptCompiler =
60
72
  options.promptCompiler ?? new FileNativePromptCompiler();
61
73
  this.operations = new FileNativeWorldOperationCoordinator(root);
62
74
  this.playTimeline = new FileNativePlayTimelineStore(root);
63
75
  this.playAdvances = new FileNativePlayAdvanceStore(root);
64
- this.#storageMigrator = new FileNativeWorldStorageMigrator(root, this.playTimeline);
76
+ this.#storageMigrator = new FileNativeWorldStorageMigrator(root);
65
77
  }
66
78
  /**
67
- * Upgrade a formally released cumulative layout before current Authority or
68
- * play-timeline behavior crosses its seam. Concurrent callers share one
79
+ * Upgrade an Authority V3 world to the current ledger layout before current
80
+ * Authority or play-timeline behavior crosses its seam. Concurrent callers share one
69
81
  * in-process promise; the durable world lock serializes processes.
70
82
  */
71
83
  async ensureCurrentStorage(worldId) {
72
84
  assertIdentity(worldId, "World ID");
73
85
  if (this.#currentStorage.has(worldId))
74
86
  return;
87
+ // A shared migration may be queued behind this caller; when the caller
88
+ // already holds the world, run the migration inline instead of awaiting it.
89
+ if (this.operations.isWorldHeld(worldId))
90
+ return this.#ensureCurrentStorageSerialized(worldId);
75
91
  const existing = this.#storageMigrations.get(worldId);
76
92
  if (existing !== undefined)
77
93
  return existing;
@@ -88,7 +104,7 @@ export class FileNativeWorldStore {
88
104
  async #ensureCurrentStorageSerialized(worldId) {
89
105
  try {
90
106
  const root = join(this.#worldsRoot, worldId);
91
- const authority = new FileNativeAuthorityV3(root);
107
+ const authority = new WorldLedgerAuthority(root);
92
108
  if (await authority.exists()) {
93
109
  await authority.readHead();
94
110
  this.#currentStorage.add(worldId);
@@ -96,6 +112,7 @@ export class FileNativeWorldStore {
96
112
  }
97
113
  await this.operations.withWorldAuthorityLock(worldId, async () => {
98
114
  await this.#storageMigrator.migrate(worldId);
115
+ await migrateLegacyArtifactStore(dirname(this.#worldsRoot), worldId);
99
116
  });
100
117
  this.#currentStorage.add(worldId);
101
118
  }
@@ -113,9 +130,18 @@ export class FileNativeWorldStore {
113
130
  }
114
131
  if (error instanceof WorldOperationBusyError)
115
132
  throw new FileNativeWorldCreationError("operation_conflict", "World storage is being upgraded by another durable operation", { cause: error });
116
- throw new FileNativeWorldCreationError("world_corrupt", "Released world storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
133
+ if (error instanceof UnsupportedLegacyStorageError)
134
+ throw new FileNativeWorldCreationError("world_corrupt", "This world was created by v0.2.0 or earlier; open it once with v0.3.0–v0.6.0 before upgrading", { cause: error });
135
+ throw new FileNativeWorldCreationError("world_corrupt", "World storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
117
136
  }
118
137
  }
138
+ async gameplay(worldId) {
139
+ assertIdentity(worldId, "World ID");
140
+ await this.ensureCurrentStorage(worldId);
141
+ const root = join(this.#worldsRoot, worldId);
142
+ await readPublicationAt(root);
143
+ return readWorldGameplayAt(root);
144
+ }
119
145
  async readWorldSummary(worldId) {
120
146
  assertIdentity(worldId, "World ID");
121
147
  return readWorldSummaryAt(join(this.#worldsRoot, worldId));
@@ -142,7 +168,7 @@ export class FileNativeWorldStore {
142
168
  const root = join(this.#worldsRoot, worldId);
143
169
  const publication = await readPublicationAt(root);
144
170
  const title = await readWorldTitleAt(root, publication);
145
- const endpoint = await new FileNativeAuthorityV3(root).recoverHeadResult();
171
+ const endpoint = await new WorldLedgerAuthority(root).recoverHeadResult();
146
172
  const history = new HistoryQuery(worldId, endpoint.history.map(({ messageId, role, exactText }) => ({
147
173
  id: messageId,
148
174
  role,
@@ -192,8 +218,14 @@ export class FileNativeWorldStore {
192
218
  throw error;
193
219
  }
194
220
  return this.operations.withExclusiveWorldStateMutation(worldId, () => this.operations.withWorldAuthorityLock(worldId, async () => {
221
+ forgetLedger(root);
195
222
  await rm(root, { recursive: true, force: true });
223
+ // 删除期间的并发读取可能重新填充缓存,删除后再失效一次。
224
+ forgetLedger(root);
196
225
  this.#currentStorage.delete(worldId);
226
+ for (const operationId of this.#retainedOperationIds.get(worldId) ?? [])
227
+ this.#operationOutcomes.delete(operationId);
228
+ this.#retainedOperationIds.delete(worldId);
197
229
  return { deleted: true };
198
230
  }));
199
231
  }
@@ -290,9 +322,7 @@ export class FileNativeWorldStore {
290
322
  const preview = this.#preview(boundInput, worldId);
291
323
  const finalRoot = join(this.#worldsRoot, worldId);
292
324
  const stagingRoot = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
293
- const operationPath = this.#operationPath(input.operationId);
294
325
  await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
295
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
296
326
  await rm(stagingRoot, { recursive: true, force: true });
297
327
  try {
298
328
  const openingMessage = {
@@ -326,19 +356,7 @@ export class FileNativeWorldStore {
326
356
  worldId,
327
357
  history: genesis.history,
328
358
  });
329
- await new FileNativeAuthorityV3(stagingRoot).initialize({
330
- operationId: input.operationId,
331
- state,
332
- history: [openingMessage],
333
- additionalMaterials: initialMaterials,
334
- });
335
- await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
336
- schemaVersion: 3,
337
- head: "genesis",
338
- sequence: 0,
339
- commitDigest: null,
340
- ...backgroundCount([openingMessage]),
341
- });
359
+ await new WorldLedgerAuthority(stagingRoot).initialize();
342
360
  await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
343
361
  const publication = {
344
362
  schemaVersion: 1,
@@ -350,12 +368,7 @@ export class FileNativeWorldStore {
350
368
  };
351
369
  await writeJson(join(stagingRoot, publicationFile), publication);
352
370
  await updateWorldActivity(stagingRoot, "genesis", [openingMessage], Date.now());
353
- await durableTree(stagingRoot);
354
- crashAtFileNativeWorldCreationEdge("after_staging");
355
371
  await rename(stagingRoot, finalRoot);
356
- await syncDirectory(this.#worldsRoot);
357
- crashAtFileNativeWorldCreationEdge("after_atomic_publish");
358
- await publishOperation(operationPath, publication);
359
372
  return {
360
373
  outcome: "created",
361
374
  world: await this.#currentSummary(publication),
@@ -410,12 +423,8 @@ export class FileNativeWorldStore {
410
423
  * must never pair a mutable state tree with a different response head.
411
424
  */
412
425
  async renderPlayerViewsAtHead(worldId, head) {
413
- const root = join(this.#worldsRoot, worldId);
414
- const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
415
426
  const [state, control] = await Promise.all([
416
- materialized?.head === head
417
- ? readTree(join(root, "state"))
418
- : this.recoverEndpoint(worldId, head).then(({ state }) => state),
427
+ this.recoverEndpoint(worldId, head).then(({ state }) => state),
419
428
  this.readSurface(worldId, "control"),
420
429
  ]);
421
430
  const source = control.find(({ path }) => path === "player-views.yaml");
@@ -429,7 +438,7 @@ export class FileNativeWorldStore {
429
438
  await this.ensureCurrentStorage(worldId);
430
439
  const root = join(this.#worldsRoot, worldId);
431
440
  await readPublicationAt(root);
432
- return (await new FileNativeAuthorityV3(root).readHead()).head;
441
+ return (await new WorldLedgerAuthority(root).readHead()).head;
433
442
  }
434
443
  async withArchiveExport(worldId, action) {
435
444
  assertIdentity(worldId, "World ID");
@@ -461,7 +470,7 @@ export class FileNativeWorldStore {
461
470
  await this.ensureCurrentStorage(worldId);
462
471
  const root = join(this.#worldsRoot, worldId);
463
472
  await readPublicationAt(root);
464
- return new FileNativeAuthorityV3(root).currentOperationId();
473
+ return new WorldLedgerAuthority(root).currentOperationId();
465
474
  }
466
475
  async saveControlDraft(worldId, files) {
467
476
  assertIdentity(worldId, "World ID");
@@ -472,6 +481,7 @@ export class FileNativeWorldStore {
472
481
  await this.ensureCurrentStorage(worldId);
473
482
  const root = join(this.#worldsRoot, worldId);
474
483
  await readPublicationAt(root);
484
+ assertControlGameplayUnchanged(await readTree(join(root, "control")), files);
475
485
  const staging = join(root, `runtime/.control-draft-${randomUUID()}`);
476
486
  await writeSurface(staging, "control", files);
477
487
  const final = join(root, "runtime/control-draft");
@@ -538,7 +548,6 @@ export class FileNativeWorldStore {
538
548
  if (currentFingerprint === nextFingerprint) {
539
549
  await rm(stagingRoot, { recursive: true, force: true });
540
550
  await rm(previousPath, { recursive: true, force: true });
541
- await syncDirectory(runtimeRoot);
542
551
  return { fingerprint: nextFingerprint };
543
552
  }
544
553
  const previous = await readTree(previousPath);
@@ -547,34 +556,24 @@ export class FileNativeWorldStore {
547
556
  fingerprint(previous) === input.expectedFingerprint) {
548
557
  if (fingerprint(staged) === nextFingerprint) {
549
558
  await rename(stagedPath, currentPath);
550
- await syncDirectory(root);
551
- await syncDirectory(stagingRoot);
552
559
  await rm(previousPath, { recursive: true, force: true });
553
560
  await rm(stagingRoot, { recursive: true, force: true });
554
- await syncDirectory(runtimeRoot);
555
561
  return { fingerprint: nextFingerprint };
556
562
  }
557
563
  await rename(previousPath, currentPath);
558
- await syncDirectory(root);
559
- await syncDirectory(runtimeRoot);
560
564
  current = await readTree(currentPath);
561
565
  currentFingerprint = fingerprint(current);
562
566
  }
563
567
  if (currentFingerprint !== input.expectedFingerprint)
564
568
  throw new FileNativeWorldCreationError("operation_conflict", "World control changed after this revision epoch opened");
569
+ assertControlGameplayUnchanged(current, input.files);
565
570
  await rm(stagingRoot, { recursive: true, force: true });
566
571
  await rm(previousPath, { recursive: true, force: true });
567
572
  await writeSurface(stagingRoot, "control", input.files);
568
- await durableTree(stagingRoot);
569
573
  await rename(currentPath, previousPath);
570
- await syncDirectory(root);
571
- await syncDirectory(runtimeRoot);
572
574
  await rename(stagedPath, currentPath);
573
- await syncDirectory(root);
574
- await syncDirectory(stagingRoot);
575
575
  await rm(previousPath, { recursive: true, force: true });
576
576
  await rm(stagingRoot, { recursive: true, force: true });
577
- await syncDirectory(runtimeRoot);
578
577
  return { fingerprint: nextFingerprint };
579
578
  });
580
579
  }
@@ -618,6 +617,7 @@ export class FileNativeWorldStore {
618
617
  throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by a running attempt");
619
618
  const preview = await this.previewControlDraft(worldId, prompt);
620
619
  const root = join(this.#worldsRoot, worldId);
620
+ assertControlGameplayUnchanged(await readTree(join(root, "control")), await readTree(join(root, "runtime/control-draft")));
621
621
  const draft = join(root, "runtime/control-draft");
622
622
  const replacement = join(root, `runtime/.control-apply-${randomUUID()}`);
623
623
  await rename(draft, replacement);
@@ -698,52 +698,22 @@ export class FileNativeWorldStore {
698
698
  }
699
699
  try {
700
700
  return await this.operations.withWorldAuthorityLock(worldId, async () => {
701
- const authorityStore = new FileNativeAuthorityV3(root);
702
- const authority = await authorityStore.readHead();
703
- const checkpoint = await readMaterializedCheckpoint(root);
704
- const cachedBackgrounds = await readTree(join(root, "runtime", "history-backgrounds"));
705
- if (!sameMaterializedEndpoint(checkpoint, authority) ||
706
- (checkpoint?.historyBackgroundCount ?? 0) !== cachedBackgrounds.length) {
707
- const recovered = await authorityStore.recover();
708
- let operation = null;
709
- if (authority.operationId !== null && authority.sequence > 0) {
710
- const candidate = await this.getOperationOutcome(authority.operationId);
711
- if (isCommittedOutcome(candidate)) {
712
- if (candidate.worldId !== worldId ||
713
- candidate.head !== authority.head ||
714
- candidate.commitDigest !== authority.commitDigest)
715
- throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head is bound to a different operation receipt");
716
- operation = candidate;
717
- }
718
- else if (candidate.outcome !== "not_started") {
719
- throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head has no recoverable acceptance receipt");
720
- }
721
- }
722
- await materializeRecoveredEndpoint(root, authority, recovered);
723
- if (operation !== null)
724
- await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
725
- }
726
- const [state, control, history, materials, backgrounds] = await Promise.all([
727
- readTree(join(root, "state")),
728
- readTree(join(root, "control")),
729
- readTree(join(root, "history")),
730
- readOptionalJson(join(root, "runtime", "additional-materials.json")),
731
- readTree(join(root, "runtime", "history-backgrounds")),
732
- ]);
733
- if ((materials?.head ?? "genesis") !== authority.head)
734
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
701
+ const recovered = await new WorldLedgerAuthority(root).recover();
702
+ const control = await readTree(join(root, "control"));
735
703
  return {
736
704
  worldId,
737
- parentHead: authority.head,
738
- files: [...state, ...control].reduce((files, { path, contents }, index) => {
739
- files[`${index < state.length ? "state" : "control"}/${path}`] =
740
- contents;
705
+ parentHead: recovered.head,
706
+ files: [...recovered.state, ...control].reduce((files, { path, contents }, index) => {
707
+ files[`${index < recovered.state.length ? "state" : "control"}/${path}`] = contents;
741
708
  return files;
742
709
  }, {}),
743
- additionalMaterials: materials?.items ?? [],
744
- history: materializedHistoryRecord(history),
745
- historyBackgrounds: materializedBackgroundRecord(backgrounds, materializedHistoryRecord(history)),
746
- narrativeCheckpoint: await authorityStore.readNarrativeCheckpoint(),
710
+ additionalMaterials: recovered.additionalMaterials,
711
+ history: Object.fromEntries(recovered.history.map(({ messageId, exactText }) => [
712
+ messageId,
713
+ exactText,
714
+ ])),
715
+ historyBackgrounds: historyBackgroundRecord(recovered.history),
716
+ narrativeCheckpoint: recovered.narrativeCheckpoint,
747
717
  };
748
718
  });
749
719
  }
@@ -756,7 +726,7 @@ export class FileNativeWorldStore {
756
726
  /**
757
727
  * Bind an immutable historical endpoint with the world's current control
758
728
  * surface. This is the same semantic input as a fresh context started while
759
- * that endpoint was current, without rematerializing it over today's head.
729
+ * that endpoint was current, without rewinding today's head.
760
730
  */
761
731
  async bindPlayCallChainAt(worldId, head) {
762
732
  assertIdentity(worldId, "World ID");
@@ -780,34 +750,29 @@ export class FileNativeWorldStore {
780
750
  ])),
781
751
  };
782
752
  }
783
- async reserveOperation(operationId) {
753
+ reserveOperation(operationId) {
784
754
  assertIdentity(operationId, "operation ID");
785
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
786
- const path = this.#operationOutcomePath(operationId);
787
- try {
788
- await writeFile(path, `${JSON.stringify({ outcome: "in_progress" }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
789
- await syncFile(path);
790
- await syncDirectory(this.#operationsRoot);
791
- return true;
792
- }
793
- catch (error) {
794
- if (isNodeError(error) && error.code === "EEXIST")
795
- return false;
796
- throw error;
755
+ if (this.#operationOutcomes.has(operationId))
756
+ return Promise.resolve(false);
757
+ this.#operationOutcomes.set(operationId, { phase: "in_progress" });
758
+ return Promise.resolve(true);
759
+ }
760
+ #rememberOperationOutcome(worldId, operationId, outcome) {
761
+ this.#operationOutcomes.set(operationId, { phase: "done", outcome });
762
+ const retained = this.#retainedOperationIds.get(worldId) ?? [];
763
+ retained.push(operationId);
764
+ while (retained.length > RETAINED_OPERATION_OUTCOMES_PER_WORLD) {
765
+ const evicted = retained.shift();
766
+ if (evicted !== undefined)
767
+ this.#operationOutcomes.delete(evicted);
797
768
  }
769
+ this.#retainedOperationIds.set(worldId, retained);
798
770
  }
799
- async releaseOperationReservation(operationId) {
771
+ releaseOperationReservation(operationId) {
800
772
  assertIdentity(operationId, "operation ID");
801
- const path = this.#operationOutcomePath(operationId);
802
- const current = await readOptionalJson(path);
803
- if (current === null)
804
- return;
805
- if (!isRecord(current) ||
806
- !hasExactKeys(current, ["outcome"]) ||
807
- current.outcome !== "in_progress")
808
- throw new FileNativeWorldCreationError("world_corrupt", "Operation reservation to release has an invalid shape");
809
- await rm(path, { force: true });
810
- await syncDirectory(this.#operationsRoot);
773
+ if (this.#operationOutcomes.get(operationId)?.phase === "in_progress")
774
+ this.#operationOutcomes.delete(operationId);
775
+ return Promise.resolve();
811
776
  }
812
777
  async recoverEndpoint(worldId, head) {
813
778
  assertIdentity(worldId, "World ID");
@@ -815,7 +780,7 @@ export class FileNativeWorldStore {
815
780
  const root = join(this.#worldsRoot, worldId);
816
781
  await readPublicationAt(root);
817
782
  try {
818
- const recovered = await new FileNativeAuthorityV3(root).recover(head);
783
+ const recovered = await new WorldLedgerAuthority(root).recover(head);
819
784
  return {
820
785
  worldId,
821
786
  head: recovered.head,
@@ -831,25 +796,6 @@ export class FileNativeWorldStore {
831
796
  throw new FileNativeWorldCreationError("world_corrupt", `Cannot recover immutable Authority endpoint${head === undefined ? "" : `: ${head}`}`, error instanceof Error ? { cause: error } : undefined);
832
797
  }
833
798
  }
834
- async repairMaterialization(worldId) {
835
- assertIdentity(worldId, "World ID");
836
- await this.ensureCurrentStorage(worldId);
837
- try {
838
- const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
839
- await this.bindPlayCallChain(worldId);
840
- const root = join(this.#worldsRoot, worldId);
841
- return (await new FileNativeAuthorityV3(root).readHead()).operationId;
842
- });
843
- return operationId === null
844
- ? { outcome: "not_started" }
845
- : this.getOperationOutcome(operationId);
846
- }
847
- catch (error) {
848
- if (error instanceof WorldOperationBusyError)
849
- throw new FileNativeWorldCreationError("operation_conflict", "This world has a durable state operation and cannot repair materialization yet");
850
- throw error;
851
- }
852
- }
853
799
  async deriveWorld(input) {
854
800
  assertIdentity(input.operationId, "Fork operation ID");
855
801
  assertIdentity(input.sourceWorldId, "Source world ID");
@@ -867,37 +813,17 @@ export class FileNativeWorldStore {
867
813
  const target = input.targetDataRoot === undefined
868
814
  ? this
869
815
  : new FileNativeWorldStore(input.targetDataRoot);
870
- const operationPath = input.identityPrefix === "import"
871
- ? join(target.#operationsRoot, `import-${operationDigest(input.operationId)}.json`)
872
- : target.#derivationOperationPath(input.operationId);
873
816
  const requestFingerprint = derivationRequestFingerprint(input);
874
817
  const worldId = `world-${operationDigest(`${input.identityPrefix ?? "derive"}:${input.operationId}`).slice(0, 24)}`;
875
818
  const finalRoot = input.publish === false
876
819
  ? resolve(input.targetWorldRoot)
877
820
  : join(target.#worldsRoot, worldId);
878
- const previous = input.publish === false
879
- ? null
880
- : await readOptionalJson(operationPath);
881
- if (previous !== null) {
882
- await target.ensureCurrentStorage(previous.worldId);
883
- if (!(await derivationPublicationMatches(join(target.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
884
- throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
885
- return {
886
- outcome: "derived",
887
- world: await target.#currentSummary(previous),
888
- worldRoot: finalRoot,
889
- };
890
- }
891
821
  const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
892
822
  if (alreadyPublished !== null) {
893
823
  if (input.publish !== false)
894
824
  await target.ensureCurrentStorage(alreadyPublished.worldId);
895
825
  if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
896
826
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
897
- if (input.publish !== false) {
898
- await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
899
- await publishJson(operationPath, alreadyPublished);
900
- }
901
827
  return {
902
828
  outcome: "derived",
903
829
  world: await readWorldSummaryAt(finalRoot, alreadyPublished),
@@ -909,43 +835,31 @@ export class FileNativeWorldStore {
909
835
  const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
910
836
  const sourcePublication = await readPublicationAt(sourceRoot);
911
837
  const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
912
- const sourceAuthority = new FileNativeAuthorityV3(sourceRoot);
838
+ const sourceAuthority = new WorldLedgerAuthority(sourceRoot);
913
839
  const sourceHead = input.sourceHead === "current"
914
840
  ? (await sourceAuthority.readHead()).head
915
841
  : input.sourceHead;
916
- if (input.requireMaterialized) {
917
- const authorityHead = await sourceAuthority.readHead();
918
- const materialized = await readMaterializedCheckpoint(sourceRoot);
919
- if (materialized?.head !== authorityHead.head ||
920
- materialized.commitDigest !== authorityHead.commitDigest)
921
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Please repair the world before exporting a backup");
922
- }
923
842
  const [sourceGenesis, control] = await Promise.all([
924
843
  readGenesisForFork(sourceRoot, input.sourceWorldId),
925
844
  readTree(join(sourceRoot, "control")),
926
845
  ]);
927
846
  const staging = join(dirname(finalRoot), `.staging-${worldId}-${randomUUID()}`);
928
847
  await mkdir(dirname(finalRoot), { recursive: true, mode: 0o700 });
929
- if (input.publish !== false)
930
- await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
931
848
  try {
932
- await mkdir(join(staging, "runtime"), { recursive: true });
933
- const clonedHead = await sourceAuthority.clonePrefixTo({
934
- targetWorldRoot: staging,
935
- selectedHead: sourceHead,
936
- operationId: input.operationId,
849
+ await copyForkClosure(sourceRoot, staging);
850
+ await rewindTo(staging, headPosition(sourceHead));
851
+ await forkArtifactClosure(staging, {
852
+ sourceWorldId: input.sourceWorldId,
853
+ targetWorldId: worldId,
854
+ restoredHead: sourceHead,
937
855
  });
938
- const selected = await new FileNativeAuthorityV3(staging).recoverHeadResult();
939
- await writeSurface(staging, "state", selected.state);
940
- await writeSurface(staging, "control", control);
856
+ // 分叉不信任来源的投影文件:当前文件由副本上的账本端点重建。
857
+ await new WorldLedgerAuthority(staging).materializeCurrentFiles();
858
+ const selected = await new WorldLedgerAuthority(staging).recover();
941
859
  await PackageScriptPermissions.write(staging, input.resetPackageScriptPermissions
942
860
  ? []
943
861
  : await PackageScriptPermissions.read(sourceRoot));
944
862
  await WorldExtensionControls.copy(sourceRoot, staging);
945
- await mkdir(join(staging, "history"), { recursive: true });
946
- for (const file of historySurfaceFiles(selected.history))
947
- await writeIdempotentText(join(staging, "history", file.path), file.contents);
948
- await replaceMaterializedTree(join(staging, "runtime", "history-backgrounds"), projectHistoryBackgrounds(selected.history));
949
863
  const genesis = {
950
864
  schemaVersion: 1,
951
865
  type: "file_native_genesis",
@@ -963,17 +877,6 @@ export class FileNativeWorldStore {
963
877
  worldId,
964
878
  history: genesis.history,
965
879
  });
966
- await writeJson(join(staging, "runtime", "additional-materials.json"), {
967
- head: sourceHead,
968
- items: selected.additionalMaterials,
969
- });
970
- await writeJson(join(staging, "runtime", "materialized-head.json"), {
971
- schemaVersion: 3,
972
- head: sourceHead,
973
- sequence: clonedHead.sequence,
974
- commitDigest: clonedHead.commitDigest,
975
- ...backgroundCount(selected.history),
976
- });
977
880
  await input.stageTarget?.({
978
881
  sourceHead,
979
882
  targetWorldRoot: staging,
@@ -998,7 +901,6 @@ export class FileNativeWorldStore {
998
901
  await writeJson(join(staging, "runtime", "archive-import.json"), {
999
902
  schemaVersion: 1,
1000
903
  });
1001
- crashAtFileNativeAuthorityEdge("derivation_after_target_staging");
1002
904
  const publication = {
1003
905
  schemaVersion: 1,
1004
906
  worldId,
@@ -1009,13 +911,8 @@ export class FileNativeWorldStore {
1009
911
  };
1010
912
  await writeJson(join(staging, publicationFile), publication);
1011
913
  await updateWorldActivity(staging, sourceHead, selected.history, Date.now());
1012
- await durableTree(staging);
1013
- crashAtFileNativeAuthorityEdge("derivation_before_publish");
914
+ forgetLedger(finalRoot);
1014
915
  await rename(staging, finalRoot);
1015
- await syncDirectory(dirname(finalRoot));
1016
- crashAtFileNativeAuthorityEdge("derivation_after_publish");
1017
- if (input.publish !== false)
1018
- await publishJson(operationPath, publication);
1019
916
  return {
1020
917
  outcome: "derived",
1021
918
  world: await readWorldSummaryAt(finalRoot, publication),
@@ -1028,8 +925,6 @@ export class FileNativeWorldStore {
1028
925
  if (published !== null) {
1029
926
  if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
1030
927
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
1031
- if (input.publish !== false)
1032
- await publishJson(operationPath, published);
1033
928
  return {
1034
929
  outcome: "derived",
1035
930
  world: await readWorldSummaryAt(finalRoot, published),
@@ -1048,142 +943,53 @@ export class FileNativeWorldStore {
1048
943
  throw error;
1049
944
  }
1050
945
  }
1051
- async failOperation(operationId, outcome = "failed") {
1052
- const current = await this.getOperationOutcome(operationId);
1053
- if (isCommittedOutcome(current))
1054
- return;
1055
- await writeJson(this.#operationOutcomePath(operationId), { outcome });
1056
- }
1057
946
  async getOperationOutcome(operationId) {
1058
- return this.#readOperationOutcome(operationId);
1059
- }
1060
- async #readOperationOutcome(operationId) {
1061
947
  assertIdentity(operationId, "operation ID");
1062
- let directRecord;
1063
- try {
1064
- let value = await readOptionalJson(this.#operationOutcomePath(operationId));
1065
- if (isRecord(value) &&
1066
- (value.outcome === "acceptance_prepared" ||
1067
- value.outcome === "committed" ||
1068
- value.outcome === "committed_materialization_pending") &&
1069
- typeof value.worldId === "string") {
1070
- await this.ensureCurrentStorage(value.worldId);
1071
- value = await readOptionalJson(this.#operationOutcomePath(operationId));
1072
- }
1073
- if (value === null)
1074
- directRecord = null;
1075
- else {
1076
- assertOperationOutcomeIntegrity(value);
1077
- directRecord = value;
1078
- }
1079
- }
1080
- catch (error) {
1081
- if (error instanceof FileNativeWorldCreationError)
1082
- throw error;
1083
- throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
1084
- }
1085
- if (directRecord?.outcome === "acceptance_prepared")
1086
- return resolvePreparedOperationOutcome(this.#worldsRoot, operationId, directRecord);
1087
- const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
1088
- if (direct?.outcome === "committed" ||
1089
- direct?.outcome === "committed_materialization_pending") {
1090
- const worldRoot = join(this.#worldsRoot, direct.worldId);
1091
- const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, direct);
1092
- const materialized = await readMaterializedCheckpoint(worldRoot);
1093
- if (materialized !== null && materialized.sequence >= commit.sequence)
1094
- return { ...direct, outcome: "committed" };
1095
- await assertPendingMaterializationCompatible(worldRoot, direct);
1096
- return { ...direct, outcome: "committed_materialization_pending" };
1097
- }
1098
- return direct?.outcome === "in_progress"
1099
- ? { outcome: "in_progress" }
1100
- : (direct ?? { outcome: "not_started" });
948
+ const entry = this.#operationOutcomes.get(operationId);
949
+ if (entry === undefined)
950
+ return { outcome: "not_started" };
951
+ if (entry.phase === "in_progress")
952
+ return { outcome: "in_progress" };
953
+ const outcome = structuredClone(entry.outcome);
954
+ await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, outcome.worldId), outcome);
955
+ return outcome;
1101
956
  }
1102
957
  /**
1103
- * Replace one committed player message on the world's active timeline
1104
- * without changing world identity or destructively rewriting Authority.
1105
- * The new immutable commit restores the selected player's logical parent
1106
- * snapshot and appends the replacement player text in one acceptance step.
958
+ * Roll the world back to a player message's logical parent endpoint before an
959
+ * edited replacement is submitted (ADR-0045). Rewinding to the current head is
960
+ * a no-op, so the edit's steps can repeat safely.
1107
961
  */
1108
- async reviseTimeline(input) {
1109
- assertIdentity(input.operationId, "Timeline-revision operation ID");
1110
- assertIdentity(input.worldId, "World ID");
1111
- if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(input.expectedCurrentHead) ||
1112
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(input.restoresHead) ||
1113
- !/^commit:[1-9][0-9]*$/u.test(input.replacesHead))
1114
- throw new TypeError("Timeline-revision endpoint is invalid");
1115
- if (input.replacementText.trim() === "")
1116
- throw new TypeError("Edited player message cannot be empty");
1117
- if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
1118
- throw new TypeError("Timeline-revision request fingerprint is invalid");
1119
- await this.ensureCurrentStorage(input.worldId);
962
+ async rewindPlayerRevision(worldId, restoresHead) {
963
+ assertIdentity(worldId, "World ID");
964
+ if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(restoresHead))
965
+ throw new TypeError("Player-revision endpoint is invalid");
966
+ await this.ensureCurrentStorage(worldId);
1120
967
  try {
1121
- return await this.operations.withExclusiveWorldStateMutation(input.worldId, () => this.#reviseTimelineUnlocked(input));
968
+ await this.operations.withExclusiveWorldStateMutation(worldId, () => rewindTo(join(this.#worldsRoot, worldId), headPosition(restoresHead)));
1122
969
  }
1123
970
  catch (error) {
1124
971
  if (error instanceof WorldOperationBusyError)
1125
- throw new FileNativeWorldCreationError("operation_conflict", "Timeline editing is locked while a world revision is open");
972
+ throw new FileNativeWorldCreationError("operation_conflict", "Player edits are locked while a world revision is open");
1126
973
  throw error;
1127
974
  }
1128
975
  }
1129
- async #reviseTimelineUnlocked(input) {
1130
- const existing = await this.getOperationOutcome(input.operationId);
1131
- if (isCommittedOutcome(existing)) {
1132
- const fact = await new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId)).authorityFactAt(existing.head);
1133
- const commit = fact.commit;
1134
- if (existing.worldId !== input.worldId ||
1135
- fact.digest !== existing.commitDigest ||
1136
- commit?.mode !== "timeline_revision" ||
1137
- commit.timelineRevision?.restoresHead !== input.restoresHead ||
1138
- commit.timelineRevision.replacesHead !== input.replacesHead ||
1139
- commit.timelineRevision.requestFingerprint !==
1140
- input.requestFingerprint ||
1141
- !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({
1142
- role,
1143
- exactText,
1144
- })), [{ role: "player", exactText: input.replacementText }]))
1145
- throw new FileNativeWorldCreationError("operation_conflict", "The same timeline-revision operation ID is bound to a different request");
1146
- return existing;
1147
- }
1148
- const root = join(this.#worldsRoot, input.worldId);
1149
- const authorityStore = new FileNativeAuthorityV3(root);
1150
- const [replaced, replacementMaterials] = await Promise.all([
1151
- authorityStore.commitAt(input.replacesHead),
1152
- authorityStore.recoverMaterials(input.restoresHead),
1153
- ]);
1154
- const logicalParent = replaced?.timelineParent.head;
1155
- if (replaced === null ||
1156
- !replaced.historyAppend.some(({ role }) => role === "player") ||
1157
- logicalParent !== input.restoresHead)
1158
- throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
1159
- const binding = await this.bindPlayCallChainAt(input.worldId, input.restoresHead);
1160
- const snapshot = WorldDocumentStore.open({
1161
- layout: "world_state",
1162
- files: Object.entries(binding.files).map(([path, contents]) => ({
1163
- path,
1164
- contents,
1165
- })),
1166
- });
1167
- const background = snapshotBackground(captureNarrativeOrigin(snapshot, 1).value);
1168
- return this.#commitHistoryChange({
1169
- operationId: input.operationId,
1170
- worldId: input.worldId,
1171
- parentHead: input.expectedCurrentHead,
1172
- historyAppend: [
1173
- {
1174
- role: "player",
1175
- exactText: input.replacementText,
1176
- ...backgroundField(background),
1177
- },
1178
- ],
1179
- nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
1180
- stateChanges: [],
1181
- mode: "timeline_revision",
1182
- timelineRevision: {
1183
- restoresHead: input.restoresHead,
1184
- replacesHead: input.replacesHead,
1185
- requestFingerprint: input.requestFingerprint,
1186
- },
976
+ /** The in-progress player edit record; written before the first world change. */
977
+ async readPlayerRevisionIntent(worldId) {
978
+ assertIdentity(worldId, "World ID");
979
+ return readOptionalJsonFile(playerRevisionIntentPath(this.#worldsRoot, worldId));
980
+ }
981
+ async writePlayerRevisionIntent(worldId, intent) {
982
+ assertIdentity(worldId, "World ID");
983
+ const path = playerRevisionIntentPath(this.#worldsRoot, worldId);
984
+ await mkdir(dirname(path), { recursive: true });
985
+ const temporary = `${path}.${randomUUID()}.tmp`;
986
+ await writeFile(temporary, `${JSON.stringify(intent)}\n`, "utf8");
987
+ await rename(temporary, path);
988
+ }
989
+ async clearPlayerRevisionIntent(worldId) {
990
+ assertIdentity(worldId, "World ID");
991
+ await rm(playerRevisionIntentPath(this.#worldsRoot, worldId), {
992
+ force: true,
1187
993
  });
1188
994
  }
1189
995
  async commitCorrection(input) {
@@ -1251,83 +1057,51 @@ export class FileNativeWorldStore {
1251
1057
  }
1252
1058
  }
1253
1059
  async #commitHistoryChange(input) {
1254
- const existing = await this.getOperationOutcome(input.operationId);
1255
- if (isCommittedOutcome(existing)) {
1256
- assertSameOperationOutcome(existing, input);
1257
- const authority = new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId));
1258
- const fact = await authority.authorityFactAt(existing.head);
1259
- if (fact.digest !== existing.commitDigest ||
1060
+ const existing = this.#operationOutcomes.get(input.operationId);
1061
+ if (existing?.phase === "done") {
1062
+ const outcome = existing.outcome;
1063
+ assertSameOperationOutcome(outcome, input);
1064
+ const fact = await new WorldLedgerAuthority(join(this.#worldsRoot, outcome.worldId)).authorityFactAt(outcome.head);
1065
+ if (fact.digest !== outcome.commitDigest ||
1260
1066
  !authorityCommitMatchesInput(fact.commit, input))
1261
1067
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
1262
- return existing;
1263
- }
1264
- const prepared = existing.outcome === "in_progress"
1265
- ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
1266
- : null;
1267
- if (prepared !== null) {
1268
- const comparable = {
1269
- ...structuredClone(prepared),
1270
- outcome: "committed_materialization_pending",
1271
- };
1272
- assertSameOperationOutcome(comparable, input);
1068
+ return outcome;
1273
1069
  }
1274
- if (existing.outcome !== "not_started" &&
1275
- !(existing.outcome === "in_progress" &&
1276
- (input.operationReserved === true || prepared !== null)))
1070
+ if (existing?.phase === "in_progress" && input.operationReserved !== true)
1277
1071
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
1278
1072
  const root = join(this.#worldsRoot, input.worldId);
1279
1073
  try {
1280
1074
  return await this.operations.withWorldAuthorityLock(input.worldId, async () => {
1281
- const concurrent = await this.#readOperationOutcome(input.operationId);
1282
- if (isCommittedOutcome(concurrent)) {
1283
- assertSameOperationOutcome(concurrent, input);
1284
- const known = await new FileNativeAuthorityV3(join(this.#worldsRoot, concurrent.worldId)).authorityFactAt(concurrent.head);
1285
- if (known.digest !== concurrent.commitDigest ||
1075
+ const concurrent = this.#operationOutcomes.get(input.operationId);
1076
+ if (concurrent?.phase === "done") {
1077
+ const outcome = concurrent.outcome;
1078
+ assertSameOperationOutcome(outcome, input);
1079
+ const known = await new WorldLedgerAuthority(join(this.#worldsRoot, outcome.worldId)).authorityFactAt(outcome.head);
1080
+ if (known.digest !== outcome.commitDigest ||
1286
1081
  !authorityCommitMatchesInput(known.commit, input))
1287
1082
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
1288
- return concurrent;
1083
+ return outcome;
1289
1084
  }
1290
- const concurrentPrepared = concurrent.outcome === "in_progress"
1291
- ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
1292
- : null;
1293
- if (concurrentPrepared !== null)
1294
- assertSameOperationOutcome({
1295
- ...structuredClone(concurrentPrepared),
1296
- outcome: "committed_materialization_pending",
1297
- }, input);
1298
- else if (concurrent.outcome === "in_progress" &&
1085
+ if (concurrent?.phase === "in_progress" &&
1299
1086
  input.operationReserved !== true)
1300
1087
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
1301
- const authorityStore = new FileNativeAuthorityV3(root);
1088
+ const authorityStore = new WorldLedgerAuthority(root);
1302
1089
  const authority = await authorityStore.readHead();
1303
- if (authority.head !== input.parentHead) {
1090
+ if (authority.head !== input.parentHead)
1304
1091
  throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
1305
- }
1306
- const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1307
- if ((materialized?.head ?? "genesis") !== authority.head)
1308
- throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
1309
1092
  const [basisState, storedControl] = await Promise.all([
1310
- authorityStore.recoverState(input.timelineRevision?.restoresHead ?? authority.head),
1093
+ authorityStore.recoverState(authority.head),
1311
1094
  readTree(join(root, "control")),
1312
1095
  ]);
1313
1096
  const candidateState = new Map(basisState.map((file) => [file.path, file.contents]));
1314
- if (input.mode === "timeline_revision") {
1315
- if (input.timelineRevision === undefined ||
1316
- input.stateChanges.length !== 0)
1317
- throw new FileNativeWorldCreationError("operation_conflict", "Timeline-revision commit payload is invalid");
1318
- }
1319
- else {
1320
- if (input.timelineRevision !== undefined)
1321
- throw new FileNativeWorldCreationError("operation_conflict", "A regular commit cannot carry a timeline-revision payload");
1322
- for (const change of input.stateChanges) {
1323
- assertRelativePath(change.relativePath);
1324
- const previous = candidateState.get(change.relativePath);
1325
- const previousHash = previous === undefined ? null : sha256(previous);
1326
- if (previousHash !== change.expectedPreviousHash ||
1327
- sha256(change.canonicalNextBytes) !== change.nextHash)
1328
- throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
1329
- candidateState.set(change.relativePath, change.canonicalNextBytes);
1330
- }
1097
+ for (const change of input.stateChanges) {
1098
+ assertRelativePath(change.relativePath);
1099
+ const previous = candidateState.get(change.relativePath);
1100
+ const previousHash = previous === undefined ? null : sha256(previous);
1101
+ if (previousHash !== change.expectedPreviousHash ||
1102
+ sha256(change.canonicalNextBytes) !== change.nextHash)
1103
+ throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
1104
+ candidateState.set(change.relativePath, change.canonicalNextBytes);
1331
1105
  }
1332
1106
  const inspection = inspectContentPackageCurrentTree([
1333
1107
  ...[...candidateState].map(([path, contents]) => ({
@@ -1341,10 +1115,7 @@ export class FileNativeWorldStore {
1341
1115
  ], { requireOpening: false });
1342
1116
  if (inspection.status !== "usable")
1343
1117
  throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
1344
- if (concurrentPrepared === null)
1345
- await authorityStore.discardUnacceptedNextEpoch();
1346
- crashAtFileNativeAuthorityEdge("before_commit_acceptance");
1347
- const preparedAppend = await authorityStore.prepareAppend({
1118
+ const { commit, commitDigest } = await authorityStore.appendCommit({
1348
1119
  ...(input.playContext === undefined
1349
1120
  ? {}
1350
1121
  : { playContext: input.playContext }),
@@ -1353,12 +1124,9 @@ export class FileNativeWorldStore {
1353
1124
  : { worldClock: input.worldClock }),
1354
1125
  operationId: input.operationId,
1355
1126
  parentHead: input.parentHead,
1356
- ...(input.timelineRevision === undefined
1357
- ? {}
1358
- : { timelineParentHead: input.timelineRevision.restoresHead }),
1359
1127
  mode: input.mode,
1360
1128
  historyAppend: input.historyAppend,
1361
- stateChanges: structuredClone(input.stateChanges),
1129
+ stateChanges: input.stateChanges,
1362
1130
  nextMaterials: input.nextMaterials,
1363
1131
  ...(input.narrativeCheckpoint === undefined
1364
1132
  ? {}
@@ -1369,57 +1137,27 @@ export class FileNativeWorldStore {
1369
1137
  corrects: input.parentHead,
1370
1138
  }
1371
1139
  : {}),
1372
- ...(input.timelineRevision === undefined
1373
- ? {}
1374
- : { timelineRevision: input.timelineRevision }),
1375
1140
  });
1376
- crashAtFileNativeAuthorityEdge("after_authority_objects");
1377
- const acceptedPreparation = concurrentPrepared ?? prepared;
1378
- if (acceptedPreparation !== null &&
1379
- acceptedPreparation.commitDigest !== preparedAppend.commitDigest)
1380
- throw new FileNativeWorldCreationError("operation_conflict", "The prepared Authority fact does not match the reserved operation");
1381
- const { commit } = preparedAppend;
1382
- const pending = {
1383
- outcome: "committed_materialization_pending",
1141
+ const outcome = {
1142
+ outcome: "committed",
1384
1143
  worldId: input.worldId,
1385
1144
  parentHead: input.parentHead,
1386
1145
  head: commit.head,
1387
- commitDigest: preparedAppend.commitDigest,
1146
+ commitDigest,
1388
1147
  historyAppend: structuredClone(input.historyAppend),
1389
1148
  nextAdditionalMaterials: structuredClone(input.nextMaterials),
1390
1149
  mode: input.mode,
1391
1150
  };
1392
- await publishJson(this.#operationOutcomePath(input.operationId), {
1393
- ...pending,
1394
- outcome: "acceptance_prepared",
1395
- });
1396
- crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
1397
- await authorityStore.publishPrepared(preparedAppend);
1398
- const acceptedAt = Date.now();
1399
- crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1400
- await publishJson(this.#operationOutcomePath(input.operationId), pending);
1401
- try {
1402
- const recovered = await authorityStore.recover(commit.head);
1403
- await assertPendingMaterializationCompatible(root, pending);
1404
- await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered, acceptedAt);
1405
- crashAtFileNativeAuthorityEdge("after_materialization");
1406
- const outcome = { ...pending, outcome: "committed" };
1407
- await publishJson(this.#operationOutcomePath(input.operationId), outcome);
1408
- return outcome;
1409
- }
1410
- catch (error) {
1411
- if (error instanceof FileNativeWorldCreationError &&
1412
- (error.code === "world_corrupt" ||
1413
- error.code === "inconsistent_materialization"))
1414
- throw error;
1415
- return pending;
1416
- }
1151
+ this.#rememberOperationOutcome(input.worldId, input.operationId, outcome);
1152
+ const recovered = await authorityStore.recover(commit.head);
1153
+ await updateWorldActivity(root, commit.head, recovered.history, Date.now());
1154
+ return outcome;
1417
1155
  });
1418
1156
  }
1419
1157
  catch (error) {
1420
1158
  if (error instanceof WorldOperationBusyError)
1421
1159
  throw new FileNativeWorldCreationError("operation_conflict", "World Authority commit is being serialized by another process");
1422
- if (error instanceof FileNativeAuthorityV3Error)
1160
+ if (error instanceof WorldAuthorityError)
1423
1161
  throw new FileNativeWorldCreationError(error.code === "corrupt" ? "world_corrupt" : "operation_conflict", error.message, { cause: error });
1424
1162
  throw error;
1425
1163
  }
@@ -1429,7 +1167,7 @@ export class FileNativeWorldStore {
1429
1167
  await this.ensureCurrentStorage(worldId);
1430
1168
  const root = join(this.#worldsRoot, worldId);
1431
1169
  await readPublicationAt(root);
1432
- const authority = await new FileNativeAuthorityV3(root).readHistory();
1170
+ const authority = await new WorldLedgerAuthority(root).readHistory();
1433
1171
  return structuredClone(authority);
1434
1172
  }
1435
1173
  async inspectDocumentMaintenance(worldId, head) {
@@ -1452,7 +1190,7 @@ export class FileNativeWorldStore {
1452
1190
  await this.ensureCurrentStorage(worldId);
1453
1191
  const root = join(this.#worldsRoot, worldId);
1454
1192
  await readPublicationAt(root);
1455
- const authority = new FileNativeAuthorityV3(root);
1193
+ const authority = new WorldLedgerAuthority(root);
1456
1194
  const selectedHead = head ?? (await authority.readHead()).head;
1457
1195
  let cache = this.#documentMaintenanceCache.get(worldId);
1458
1196
  if (cache === undefined) {
@@ -1490,49 +1228,25 @@ export class FileNativeWorldStore {
1490
1228
  };
1491
1229
  }
1492
1230
  async #readPublication(operationId) {
1231
+ // The world root's own publication record is the only creation receipt.
1232
+ const worldId = `world-${operationDigest(operationId).slice(0, 24)}`;
1493
1233
  let publication;
1494
1234
  try {
1495
- publication = await readJson(this.#operationPath(operationId));
1235
+ publication = await readPublicationAt(join(this.#worldsRoot, worldId));
1496
1236
  }
1497
1237
  catch (error) {
1498
- if (isNodeError(error) && error.code === "ENOENT") {
1499
- const worldId = `world-${operationDigest(operationId).slice(0, 24)}`;
1500
- try {
1501
- publication = await readPublicationAt(join(this.#worldsRoot, worldId));
1502
- }
1503
- catch (worldError) {
1504
- if (isNodeError(worldError) && worldError.code === "ENOENT")
1505
- return null;
1506
- throw worldError;
1507
- }
1508
- if (publication.operationId !== operationId) {
1509
- throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
1510
- }
1511
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
1512
- await publishOperation(this.#operationPath(operationId), publication);
1513
- }
1514
- else {
1515
- throw error;
1516
- }
1238
+ if (isNodeError(error) && error.code === "ENOENT")
1239
+ return null;
1240
+ throw error;
1517
1241
  }
1518
- const worldPublication = await readPublicationAt(join(this.#worldsRoot, publication.worldId));
1519
- if (JSON.stringify(worldPublication) !== JSON.stringify(publication)) {
1520
- throw new FileNativeWorldCreationError("world_corrupt", "Creation operation does not match the world publication record");
1242
+ if (publication.operationId !== operationId) {
1243
+ throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
1521
1244
  }
1522
1245
  return publication;
1523
1246
  }
1524
1247
  #currentSummary(publication) {
1525
1248
  return readWorldSummaryAt(join(this.#worldsRoot, publication.worldId), publication);
1526
1249
  }
1527
- #operationPath(operationId) {
1528
- return join(this.#operationsRoot, `${operationDigest(operationId)}.json`);
1529
- }
1530
- #operationOutcomePath(operationId) {
1531
- return join(this.#operationsRoot, `outcome-${operationDigest(operationId)}.json`);
1532
- }
1533
- #derivationOperationPath(operationId) {
1534
- return join(this.#operationsRoot, `derive-${operationDigest(operationId)}.json`);
1535
- }
1536
1250
  #preview(input, worldId) {
1537
1251
  const openingMessage = genesisOpeningMessage(openingText(input.packageFiles));
1538
1252
  return this.#promptCompiler.preview({
@@ -1670,12 +1384,6 @@ function contentPackageCreationFingerprint(title, files) {
1670
1384
  function operationDigest(operationId) {
1671
1385
  return createHash("sha256").update(operationId).digest("hex");
1672
1386
  }
1673
- async function publishOperation(path, value) {
1674
- const temporary = `${path}.${randomUUID()}.tmp`;
1675
- await writeJson(temporary, value);
1676
- await rename(temporary, path);
1677
- await syncDirectory(dirname(path));
1678
- }
1679
1387
  function isRecord(value) {
1680
1388
  return typeof value === "object" && value !== null && !Array.isArray(value);
1681
1389
  }
@@ -1688,93 +1396,14 @@ function hasRequiredAndOptionalKeys(value, required, optional) {
1688
1396
  return (required.every((key) => Object.hasOwn(value, key)) &&
1689
1397
  Object.keys(value).every((key) => allowed.has(key)));
1690
1398
  }
1691
- function assertOperationOutcomeIntegrity(value) {
1692
- if (!isRecord(value) || typeof value.outcome !== "string")
1693
- throw corruptOperationOutcome();
1694
- if (value.outcome === "in_progress") {
1695
- if (!hasExactKeys(value, ["outcome"]))
1696
- throw corruptOperationOutcome();
1697
- return;
1698
- }
1699
- if (value.outcome === "failed" || value.outcome === "cancelled") {
1700
- if (!hasExactKeys(value, ["outcome"]))
1701
- throw corruptOperationOutcome();
1702
- return;
1703
- }
1704
- if (value.outcome === "acceptance_prepared" ||
1705
- value.outcome === "committed" ||
1706
- value.outcome === "committed_materialization_pending") {
1707
- if (!hasExactKeys(value, [
1708
- "outcome",
1709
- "worldId",
1710
- "parentHead",
1711
- "head",
1712
- "commitDigest",
1713
- "historyAppend",
1714
- "nextAdditionalMaterials",
1715
- "mode",
1716
- ]) ||
1717
- typeof value.worldId !== "string" ||
1718
- value.worldId.length === 0 ||
1719
- typeof value.parentHead !== "string" ||
1720
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1721
- typeof value.head !== "string" ||
1722
- !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1723
- typeof value.commitDigest !== "string" ||
1724
- !/^[a-f0-9]{64}$/u.test(value.commitDigest) ||
1725
- !Array.isArray(value.historyAppend) ||
1726
- !value.historyAppend.every(isPlayHistoryAppendInput) ||
1727
- !Array.isArray(value.nextAdditionalMaterials) ||
1728
- !value.nextAdditionalMaterials.every(isAuthorityMaterialSelection) ||
1729
- !(value.mode === "play" ||
1730
- value.mode === "correction" ||
1731
- value.mode === "timeline_revision"))
1732
- throw corruptOperationOutcome();
1733
- return;
1734
- }
1735
- throw corruptOperationOutcome();
1736
- }
1737
- function projectOperationOutcome(value) {
1738
- return structuredClone(value);
1739
- }
1740
- async function readAcceptancePreparedOutcome(path) {
1741
- const value = await readOptionalJson(path);
1742
- if (value === null)
1743
- return null;
1744
- assertOperationOutcomeIntegrity(value);
1745
- if (value.outcome !== "acceptance_prepared")
1746
- return null;
1747
- return structuredClone(value);
1748
- }
1749
- async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
1750
- const prepared = value;
1751
- const worldRoot = join(worldsRoot, prepared.worldId);
1752
- const authorityStore = new FileNativeAuthorityV3(worldRoot);
1753
- const authority = await authorityStore.readHead();
1754
- if (authority.head === prepared.parentHead)
1755
- return { outcome: "in_progress" };
1756
- if (authority.operationId !== operationId)
1757
- throw corruptOperationOutcome();
1758
- const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, prepared);
1759
- if (authority.sequence < commit.sequence)
1760
- throw corruptOperationOutcome();
1761
- const materialized = await readMaterializedCheckpoint(worldRoot);
1762
- const outcome = materialized !== null && materialized.sequence >= commit.sequence
1763
- ? "committed"
1764
- : "committed_materialization_pending";
1765
- const resolved = { ...structuredClone(prepared), outcome };
1766
- if (outcome === "committed_materialization_pending")
1767
- await assertPendingMaterializationCompatible(worldRoot, resolved);
1768
- return resolved;
1769
- }
1770
1399
  function corruptOperationOutcome() {
1771
1400
  return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1772
1401
  }
1773
1402
  async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1774
- const fact = await new FileNativeAuthorityV3(worldRoot).authorityFactAt(outcome.head);
1403
+ const fact = await new WorldLedgerAuthority(worldRoot).authorityFactAt(outcome.head);
1775
1404
  const { commit } = fact;
1776
1405
  if (fact.digest !== outcome.commitDigest ||
1777
- commit.auditParent.head !== outcome.parentHead ||
1406
+ commit.parentHead !== outcome.parentHead ||
1778
1407
  commit.head !== outcome.head ||
1779
1408
  commit.mode !== outcome.mode ||
1780
1409
  !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
@@ -1786,143 +1415,6 @@ async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1786
1415
  throw corruptOperationOutcome();
1787
1416
  return commit;
1788
1417
  }
1789
- async function assertPendingMaterializationCompatible(worldRoot, outcome) {
1790
- const authority = new FileNativeAuthorityV3(worldRoot);
1791
- await assertCommittedAuthorityOutcomeMatches(worldRoot, outcome);
1792
- const [parent, target, state, history] = await Promise.all([
1793
- authority.recover(outcome.parentHead),
1794
- authority.recover(outcome.head),
1795
- readTree(join(worldRoot, "state")),
1796
- readTree(join(worldRoot, "history")),
1797
- ]);
1798
- assertProjectionBetweenEndpoints("state", state, parent.state, target.state);
1799
- assertHistoryProjectionBetweenEndpoints(history, historySurfaceFiles(parent.history), historySurfaceFiles(target.history));
1800
- const existingMaterials = await readOptionalJson(join(worldRoot, "runtime", "additional-materials.json"));
1801
- if (existingMaterials !== null &&
1802
- !((existingMaterials.head === parent.head &&
1803
- isDeepStrictEqual(existingMaterials.items, parent.additionalMaterials)) ||
1804
- (existingMaterials.head === target.head &&
1805
- isDeepStrictEqual(existingMaterials.items, target.additionalMaterials))))
1806
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1807
- }
1808
- function assertProjectionBetweenEndpoints(surface, current, parent, target) {
1809
- const parentFiles = new Map(parent.map((file) => [file.path, file.contents]));
1810
- const targetFiles = new Map(target.map((file) => [file.path, file.contents]));
1811
- for (const file of current) {
1812
- if (file.contents !== parentFiles.get(file.path) &&
1813
- file.contents !== targetFiles.get(file.path))
1814
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World ${surface} projection conflicts with both accepted endpoints: ${file.path}`);
1815
- }
1816
- }
1817
- function assertHistoryProjectionBetweenEndpoints(current, parent, target) {
1818
- const byMessageId = (files) => new Map(files.map((file) => [
1819
- fileNativeHistoryMessageIdFromProjectionPath(file.path),
1820
- file.contents,
1821
- ]));
1822
- const parentFiles = byMessageId(parent);
1823
- const targetFiles = byMessageId(target);
1824
- const seen = new Set();
1825
- for (const file of current) {
1826
- const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1827
- if (messageId === null ||
1828
- seen.has(messageId) ||
1829
- (file.contents !== parentFiles.get(messageId) &&
1830
- file.contents !== targetFiles.get(messageId)))
1831
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World history projection conflicts with both accepted endpoints: ${file.path}`);
1832
- seen.add(messageId);
1833
- }
1834
- }
1835
- function isAuthorityMaterialSelection(value) {
1836
- if (!isRecord(value))
1837
- return false;
1838
- if (value.kind === "document")
1839
- return (hasExactKeys(value, ["kind", "document"]) &&
1840
- typeof value.document === "string");
1841
- if (value.kind === "node")
1842
- return (hasExactKeys(value, ["kind", "document", "locator"]) &&
1843
- typeof value.document === "string" &&
1844
- isAuthorityMaterialLocator(value.locator));
1845
- if (value.kind === "history_message")
1846
- return (hasExactKeys(value, ["kind", "message"]) &&
1847
- typeof value.message === "string");
1848
- return (value.kind === "history_commit" &&
1849
- hasExactKeys(value, ["kind", "commit"]) &&
1850
- typeof value.commit === "string");
1851
- }
1852
- function isAuthorityMaterialLocator(value) {
1853
- return (isRecord(value) &&
1854
- hasRequiredAndOptionalKeys(value, [], ["yaml", "markdown"]) &&
1855
- (value.yaml === undefined ||
1856
- (Array.isArray(value.yaml) &&
1857
- value.yaml.every((segment) => typeof segment === "string" || Number.isSafeInteger(segment)))) &&
1858
- (value.markdown === undefined ||
1859
- (Array.isArray(value.markdown) &&
1860
- value.markdown.every((segment) => typeof segment === "string"))));
1861
- }
1862
- async function publishJson(path, value) {
1863
- const temporary = `${path}.${randomUUID()}.tmp`;
1864
- await writeJson(temporary, value);
1865
- await rename(temporary, path);
1866
- await syncDirectory(dirname(path));
1867
- }
1868
- async function readMaterializedCheckpoint(root) {
1869
- const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1870
- if (value === null)
1871
- return null;
1872
- if (!isRecord(value) ||
1873
- !hasRequiredAndOptionalKeys(value, ["schemaVersion", "head", "sequence", "commitDigest"], ["historyBackgroundCount"]) ||
1874
- (value.historyBackgroundCount !== undefined &&
1875
- (!Number.isSafeInteger(value.historyBackgroundCount) ||
1876
- Number(value.historyBackgroundCount) < 1)) ||
1877
- value.schemaVersion !== 3 ||
1878
- typeof value.head !== "string" ||
1879
- !Number.isSafeInteger(value.sequence) ||
1880
- Number(value.sequence) < 0 ||
1881
- (value.commitDigest !== null &&
1882
- (typeof value.commitDigest !== "string" ||
1883
- !/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
1884
- (Number(value.sequence) === 0
1885
- ? value.head !== "genesis" || value.commitDigest !== null
1886
- : value.head !== `commit:${String(value.sequence)}` ||
1887
- value.commitDigest === null))
1888
- throw new FileNativeWorldCreationError("world_corrupt", "Materialized world checkpoint has an invalid shape");
1889
- return value;
1890
- }
1891
- function sameMaterializedEndpoint(checkpoint, authority) {
1892
- return (checkpoint !== null &&
1893
- checkpoint.head === authority.head &&
1894
- checkpoint.sequence === authority.sequence &&
1895
- checkpoint.commitDigest === authority.commitDigest);
1896
- }
1897
- function isPlayHistoryAppendInput(value) {
1898
- return (isRecord(value) &&
1899
- hasRequiredAndOptionalKeys(value, ["role", "exactText"], ["background"]) &&
1900
- (value.background === undefined ||
1901
- (isHistoryBackground(value.background) &&
1902
- (value.role !== "player" || value.background.kind === "snapshot"))) &&
1903
- (value.role === "player" || value.role === "narrator") &&
1904
- typeof value.exactText === "string");
1905
- }
1906
- async function materializeRecoveredEndpoint(root, authority, endpoint, lastActivityAt = null) {
1907
- if (authority.head !== endpoint.head)
1908
- throw new FileNativeWorldCreationError("world_corrupt", "Recovered endpoint does not match the Authority head to materialize");
1909
- await replaceMaterializedTree(join(root, "state"), endpoint.state);
1910
- crashAtFileNativeAuthorityEdge("after_state_materialization");
1911
- await replaceMaterializedTree(join(root, "history"), historySurfaceFiles(endpoint.history));
1912
- await replaceMaterializedTree(join(root, "runtime", "history-backgrounds"), projectHistoryBackgrounds(endpoint.history));
1913
- await publishJson(join(root, "runtime", "additional-materials.json"), {
1914
- head: authority.head,
1915
- items: endpoint.additionalMaterials,
1916
- });
1917
- await publishJson(join(root, "runtime", "materialized-head.json"), {
1918
- schemaVersion: 3,
1919
- head: authority.head,
1920
- sequence: authority.sequence,
1921
- commitDigest: authority.commitDigest,
1922
- ...backgroundCount(endpoint.history),
1923
- });
1924
- await updateWorldActivity(root, authority.head, endpoint.history, lastActivityAt);
1925
- }
1926
1418
  async function replaceMaterializedTree(root, files) {
1927
1419
  const next = new Map();
1928
1420
  for (const file of files) {
@@ -1942,34 +1434,19 @@ async function replaceMaterializedTree(root, files) {
1942
1434
  continue;
1943
1435
  const path = join(root, file.path);
1944
1436
  await rm(path, { force: true });
1945
- await syncDirectory(dirname(path));
1946
1437
  }
1947
1438
  }
1948
1439
  async function publishText(path, contents) {
1949
1440
  await mkdir(dirname(path), { recursive: true });
1950
1441
  const temporary = `${path}.${randomUUID()}.tmp`;
1951
1442
  await writeFile(temporary, contents, "utf8");
1952
- await syncFile(temporary);
1953
1443
  await rename(temporary, path);
1954
- await syncDirectory(dirname(path));
1955
1444
  }
1956
1445
  function historySurfaceFiles(messages) {
1957
1446
  return projectFileNativeHistorySurface(messages, (messageId) => {
1958
1447
  throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${messageId}`);
1959
1448
  });
1960
1449
  }
1961
- function materializedHistoryRecord(files) {
1962
- const result = {};
1963
- for (const file of files) {
1964
- const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1965
- if (messageId === null)
1966
- throw new FileNativeWorldCreationError("world_corrupt", `Materialized history path is invalid: ${file.path}`);
1967
- if (messageId in result)
1968
- throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
1969
- result[messageId] = file.contents;
1970
- }
1971
- return result;
1972
- }
1973
1450
  async function writeIdempotentText(path, contents) {
1974
1451
  try {
1975
1452
  const existing = await readFile(path, "utf8");
@@ -1981,15 +1458,9 @@ async function writeIdempotentText(path, contents) {
1981
1458
  throw error;
1982
1459
  const temporary = `${path}.${randomUUID()}.tmp`;
1983
1460
  await writeFile(temporary, contents, "utf8");
1984
- await syncFile(temporary);
1985
1461
  await rename(temporary, path);
1986
- await syncDirectory(dirname(path));
1987
1462
  }
1988
1463
  }
1989
- function isCommittedOutcome(outcome) {
1990
- return (outcome.outcome === "committed" ||
1991
- outcome.outcome === "committed_materialization_pending");
1992
- }
1993
1464
  function assertSameOperationOutcome(outcome, input) {
1994
1465
  if (outcome.worldId !== input.worldId ||
1995
1466
  outcome.parentHead !== input.parentHead ||
@@ -2026,8 +1497,7 @@ function authorityCommitMatchesInput(commit, input) {
2026
1497
  const candidate = input.stateChanges[index];
2027
1498
  if (candidate === undefined)
2028
1499
  return false;
2029
- const { nextBlob, ...metadata } = stored;
2030
- const expected = {
1500
+ return isDeepStrictEqual(stored, {
2031
1501
  kind: candidate.kind,
2032
1502
  documentId: candidate.documentId,
2033
1503
  stableShortRef: candidate.stableShortRef,
@@ -2035,10 +1505,7 @@ function authorityCommitMatchesInput(commit, input) {
2035
1505
  codec: candidate.codec,
2036
1506
  expectedPreviousHash: candidate.expectedPreviousHash,
2037
1507
  nextHash: candidate.nextHash,
2038
- };
2039
- return (nextBlob.epoch === commit.sequence &&
2040
- `sha256:${nextBlob.digest}` === candidate.nextHash &&
2041
- isDeepStrictEqual(metadata, expected));
1508
+ });
2042
1509
  });
2043
1510
  const expectedCorrection = input.mode === "correction"
2044
1511
  ? input.stateChanges.map(({ documentId }) => documentId)
@@ -2046,9 +1513,7 @@ function authorityCommitMatchesInput(commit, input) {
2046
1513
  return (commit.mode === input.mode &&
2047
1514
  commit.playContext === input.playContext &&
2048
1515
  commit.worldClock === input.worldClock &&
2049
- commit.auditParent.head === input.parentHead &&
2050
- commit.timelineParent.head ===
2051
- (input.timelineRevision?.restoresHead ?? input.parentHead) &&
1516
+ commit.parentHead === input.parentHead &&
2052
1517
  isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
2053
1518
  role,
2054
1519
  exactText,
@@ -2056,12 +1521,48 @@ function authorityCommitMatchesInput(commit, input) {
2056
1521
  })), input.historyAppend) &&
2057
1522
  stateChangesMatch &&
2058
1523
  isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
2059
- isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
2060
1524
  isDeepStrictEqual(commit.narrativeCheckpoint, input.narrativeCheckpoint) &&
2061
1525
  isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
2062
1526
  commit.corrects ===
2063
1527
  (input.mode === "correction" ? input.parentHead : undefined));
2064
1528
  }
1529
+ function headPosition(head) {
1530
+ if (head === "genesis")
1531
+ return 0;
1532
+ const match = /^commit:([1-9][0-9]*)$/u.exec(head);
1533
+ if (match === null)
1534
+ throw new TypeError(`Endpoint is invalid: ${head}`);
1535
+ return Number.parseInt(match[1], 10);
1536
+ }
1537
+ // 分叉只复制一个世界的闭包;回退与身份重分配由调用方在副本上完成。
1538
+ const forkClosureEntries = [
1539
+ "state",
1540
+ "control",
1541
+ "history",
1542
+ join("runtime", "head.json"),
1543
+ join("runtime", "turns"),
1544
+ join("runtime", "history-backgrounds"),
1545
+ join("runtime", "additional-materials.json"),
1546
+ join("runtime", "genesis.json"),
1547
+ join("runtime", "play-genesis-timeline.json"),
1548
+ "artifacts",
1549
+ ];
1550
+ async function copyForkClosure(sourceRoot, target) {
1551
+ await mkdir(join(target, "runtime"), { recursive: true, mode: 0o700 });
1552
+ for (const entry of forkClosureEntries) {
1553
+ const source = join(sourceRoot, entry);
1554
+ try {
1555
+ await stat(source);
1556
+ }
1557
+ catch (error) {
1558
+ if (isNodeError(error) && error.code === "ENOENT")
1559
+ continue;
1560
+ throw error;
1561
+ }
1562
+ await mkdir(dirname(join(target, entry)), { recursive: true, mode: 0o700 });
1563
+ await cp(source, join(target, entry), { recursive: true });
1564
+ }
1565
+ }
2065
1566
  function openWorldDocumentSnapshot(files) {
2066
1567
  return WorldDocumentStore.open({
2067
1568
  layout: "world_state",
@@ -2078,36 +1579,11 @@ async function writeJson(path, value) {
2078
1579
  encoding: "utf8",
2079
1580
  mode: 0o600,
2080
1581
  });
2081
- await syncFile(path);
2082
1582
  }
2083
- async function syncFile(path) {
2084
- const handle = await open(path, "r");
2085
- try {
2086
- await handle.sync();
2087
- }
2088
- finally {
2089
- await handle.close();
2090
- }
2091
- }
2092
- async function syncDirectory(path) {
2093
- const handle = await open(path, "r");
2094
- try {
2095
- await handle.sync();
2096
- }
2097
- finally {
2098
- await handle.close();
2099
- }
2100
- }
2101
- async function durableTree(root) {
2102
- const entries = await readdir(root, { withFileTypes: true });
2103
- for (const entry of entries) {
2104
- const path = join(root, entry.name);
2105
- if (entry.isDirectory())
2106
- await durableTree(path);
2107
- else if (entry.isFile())
2108
- await syncFile(path);
2109
- }
2110
- await syncDirectory(root);
1583
+ async function publishJson(path, value) {
1584
+ const temporary = `${path}.${randomUUID()}.tmp`;
1585
+ await writeJson(temporary, value);
1586
+ await rename(temporary, path);
2111
1587
  }
2112
1588
  async function readGenesisForFork(worldRoot, worldId) {
2113
1589
  const value = await readJson(join(worldRoot, "runtime", "genesis.json"));
@@ -2179,15 +1655,16 @@ async function readPublicationAt(root) {
2179
1655
  }
2180
1656
  async function readWorldSummaryAt(root, publication, serializeRebuild) {
2181
1657
  const published = publication ?? (await readPublicationAt(root));
2182
- const { lastActivityAt, excerpt, playerTurns } = await readWorldActivity(root, serializeRebuild);
2183
- const summary = {
2184
- ...toSummary(published),
1658
+ const gameplay = await readWorldGameplayAt(root);
1659
+ const activity = await readWorldActivity(root, serializeRebuild);
1660
+ return {
1661
+ ...toSummary(published, gameplay),
2185
1662
  title: await readWorldTitleAt(root, published),
2186
- lastActivityAt,
2187
- excerpt,
2188
- playerTurns,
1663
+ lastActivityAt: activity.lastActivityAt,
1664
+ excerpt: activity.excerpt,
1665
+ playerTurns: activity.playerTurns,
1666
+ ...(activity.pendingUpgrade === true ? { pendingUpgrade: true } : {}),
2189
1667
  };
2190
- return summary;
2191
1668
  }
2192
1669
  async function readWorldTitleAt(root, published) {
2193
1670
  const metadata = await readOptionalJson(join(root, localMetadataFile));
@@ -2197,6 +1674,15 @@ async function readWorldTitleAt(root, published) {
2197
1674
  throw new FileNativeWorldCreationError("world_corrupt", "Local world shell is corrupt");
2198
1675
  return metadata.name;
2199
1676
  }
1677
+ /** Worlds copy the package's gameplay identity with control; its absence means interactive fiction. */
1678
+ async function readWorldGameplayAt(root) {
1679
+ try {
1680
+ return readGameplay(controlGameplayFiles(await readTree(join(root, "control"))));
1681
+ }
1682
+ catch (error) {
1683
+ throw new FileNativeWorldCreationError("world_corrupt", "World gameplay identity is corrupt", { cause: error });
1684
+ }
1685
+ }
2200
1686
  async function readJson(path) {
2201
1687
  return JSON.parse(await readFile(path, "utf8"));
2202
1688
  }
@@ -2210,13 +1696,26 @@ async function readOptionalJson(path) {
2210
1696
  throw error;
2211
1697
  }
2212
1698
  }
2213
- function toSummary(publication) {
1699
+ function toSummary(publication, gameplay) {
2214
1700
  return {
2215
1701
  worldId: publication.worldId,
2216
1702
  title: publication.title,
2217
1703
  parentEndpoint: publication.parentEndpoint,
1704
+ gameplay,
2218
1705
  };
2219
1706
  }
1707
+ /** World control is a fixed mode and scheme: drafts, revisions and applies may not change either. */
1708
+ function assertControlGameplayUnchanged(current, next) {
1709
+ try {
1710
+ assertSameGameplay(controlGameplayFiles(current), controlGameplayFiles(next));
1711
+ }
1712
+ catch (error) {
1713
+ throw new FileNativeWorldCreationError("candidate_validation_failed", error instanceof Error ? error.message : "World gameplay is fixed", { cause: error });
1714
+ }
1715
+ }
1716
+ function controlGameplayFiles(files) {
1717
+ return files.map((file) => ({ ...file, path: `control/${file.path}` }));
1718
+ }
2220
1719
  const worldNamePattern = /^[^\r\n]{1,160}$/u;
2221
1720
  function validWorldName(name) {
2222
1721
  return worldNamePattern.test(name);
@@ -2284,36 +1783,20 @@ function assertRelativePath(path) {
2284
1783
  function isNodeError(error) {
2285
1784
  return error instanceof Error && "code" in error;
2286
1785
  }
2287
- function crashAtFileNativeWorldCreationEdge(edge) {
2288
- if (process.env.NODE_ENV === "test" &&
2289
- process.env
2290
- .NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_WORLD_CREATION_EDGE === edge) {
2291
- process.kill(process.pid, "SIGKILL");
2292
- }
2293
- }
2294
- function crashAtFileNativeAuthorityEdge(edge) {
2295
- if (process.env.NODE_ENV === "test" &&
2296
- process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_AUTHORITY_EDGE ===
2297
- edge) {
2298
- process.kill(process.pid, "SIGKILL");
2299
- }
2300
- }
2301
1786
  import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
2302
1787
  function backgroundField(background) {
2303
1788
  return background === undefined ? {} : { background };
2304
1789
  }
2305
- function materializedBackgroundRecord(files, history) {
2306
- return Object.fromEntries(files.map(({ path, contents }) => {
2307
- const id = path.replace(/\.json$/u, "");
2308
- const value = JSON.parse(contents);
2309
- if (path !== `${id}.json` ||
2310
- !Object.hasOwn(history, id) ||
2311
- !isHistoryBackground(value))
2312
- throw new FileNativeWorldCreationError("world_corrupt", "Materialized history background is invalid or unreachable");
2313
- return [id, value];
2314
- }));
1790
+ function playerRevisionIntentPath(worldsRoot, worldId) {
1791
+ return join(worldsRoot, worldId, "runtime", "player-revision-intent.json");
2315
1792
  }
2316
- function backgroundCount(messages) {
2317
- const count = messages.filter(({ background }) => background !== undefined).length;
2318
- return count === 0 ? {} : { historyBackgroundCount: count };
1793
+ async function readOptionalJsonFile(path) {
1794
+ try {
1795
+ return JSON.parse(await readFile(path, "utf8"));
1796
+ }
1797
+ catch (error) {
1798
+ if (error.code === "ENOENT")
1799
+ return null;
1800
+ throw error;
1801
+ }
2319
1802
  }