narraeon 0.1.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 (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +164 -0
  3. package/README.zh-CN.md +164 -0
  4. package/dist/node/cli/NarraeonCli.js +163 -0
  5. package/dist/node/cli/main.js +38 -0
  6. package/dist/node/protocol/appPreferences.js +5 -0
  7. package/dist/node/protocol/contentTree.js +14 -0
  8. package/dist/node/protocol/modelConnections.js +110 -0
  9. package/dist/node/protocol/playerViews.js +1 -0
  10. package/dist/node/protocol/v1.js +278 -0
  11. package/dist/node/runtime/V1Runtime.js +625 -0
  12. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
  14. package/dist/node/runtime/config/appPaths.js +19 -0
  15. package/dist/node/runtime/content/ContentLimits.js +1 -0
  16. package/dist/node/runtime/content/ContentTreeFile.js +1 -0
  17. package/dist/node/runtime/content/ContentWorkspace.js +601 -0
  18. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
  19. package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
  20. package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
  21. package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
  22. package/dist/node/runtime/model/AiFailureLog.js +332 -0
  23. package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
  24. package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
  25. package/dist/node/runtime/model/ModelHost.js +89 -0
  26. package/dist/node/runtime/model/ModelHostStream.js +367 -0
  27. package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
  28. package/dist/node/runtime/model/ProviderStream.js +90 -0
  29. package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
  30. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
  31. package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
  32. package/dist/node/runtime/play/PlayCallChain.js +1280 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
  36. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
  37. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
  38. package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
  39. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
  40. package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
  41. package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
  42. package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
  43. package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
  44. package/dist/node/runtime/world/WorldStateRevision.js +75 -0
  45. package/dist/node/server/createRuntime.js +6 -0
  46. package/dist/node/server/createServer.js +218 -0
  47. package/dist/node/server/main.js +25 -0
  48. package/dist/node/server/startWebServer.js +28 -0
  49. package/dist/node/shared/default-play-prompts.js +85 -0
  50. package/dist/node/shared/default-preset-host.js +538 -0
  51. package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
  52. package/dist/node/shared/first-party-action-choices.js +164 -0
  53. package/dist/node/shared/first-party-generic-panels.js +105 -0
  54. package/dist/node/shared/first-party-play-preset-templates.js +41 -0
  55. package/dist/node/shared/first-party-player-view.js +179 -0
  56. package/dist/node/shared/play-preset-prompt-roles.js +4 -0
  57. package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
  58. package/dist/web/assets/index-DDx1itzT.js +1192 -0
  59. package/dist/web/index.html +14 -0
  60. package/package.json +86 -0
@@ -0,0 +1,2028 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, } from "../content/FileNativeContentTree.js";
6
+ import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
7
+ import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
8
+ import { WorldDocumentStore } from "./WorldDocumentStore.js";
9
+ import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
10
+ const publicationFile = "publication.json";
11
+ const localMetadataFile = "local.json";
12
+ export class FileNativeWorldCreationError extends Error {
13
+ code;
14
+ constructor(code, message, options) {
15
+ super(message, options);
16
+ this.name = "FileNativeWorldCreationError";
17
+ this.code = code;
18
+ }
19
+ }
20
+ export class FileNativeWorldNotFoundError extends Error {
21
+ constructor(options) {
22
+ super("World does not exist", options);
23
+ this.name = "FileNativeWorldNotFoundError";
24
+ }
25
+ }
26
+ export class FileNativeWorldStore {
27
+ #worldsRoot;
28
+ #operationsRoot;
29
+ #promptCompiler;
30
+ #activeControlUsers = new Map();
31
+ operations;
32
+ constructor(dataRoot, options = {}) {
33
+ const root = resolve(dataRoot);
34
+ this.#worldsRoot = join(root, "worlds-file-native");
35
+ this.#operationsRoot = join(root, "operations");
36
+ this.#promptCompiler =
37
+ options.promptCompiler ?? new FileNativePromptCompiler();
38
+ this.operations = new FileNativeWorldOperationCoordinator(root);
39
+ }
40
+ async listWorlds() {
41
+ await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
42
+ const entries = await readdir(this.#worldsRoot, { withFileTypes: true });
43
+ const worlds = [];
44
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
45
+ if (!entry.isDirectory() || entry.name.startsWith("."))
46
+ continue;
47
+ worlds.push(await readWorldSummaryAt(join(this.#worldsRoot, entry.name)));
48
+ }
49
+ return worlds;
50
+ }
51
+ async renameWorld(worldId, name) {
52
+ assertIdentity(worldId, "World ID");
53
+ const trimmed = name.trim();
54
+ if (!validWorldName(trimmed))
55
+ throw new TypeError("World name must contain 1 to 160 characters and no line breaks");
56
+ return this.operations.withWorldLocalMetadataMutation(worldId, async () => {
57
+ const root = join(this.#worldsRoot, worldId);
58
+ let publication;
59
+ try {
60
+ publication = await readPublicationAt(root);
61
+ }
62
+ catch (error) {
63
+ if (isNodeError(error) && error.code === "ENOENT")
64
+ throw new FileNativeWorldNotFoundError({ cause: error });
65
+ throw error;
66
+ }
67
+ await publishJson(join(root, localMetadataFile), {
68
+ schemaVersion: 1,
69
+ worldId,
70
+ name: trimmed,
71
+ });
72
+ return { ...toSummary(publication), title: trimmed };
73
+ });
74
+ }
75
+ /**
76
+ * Delete a world and everything the runtime keeps under it. The exclusive
77
+ * world-state mutation prevents deletion from racing an active play or
78
+ * correction operation. Expired claims do not block cleanup.
79
+ */
80
+ async deleteWorld(worldId) {
81
+ assertIdentity(worldId, "World ID");
82
+ const root = join(this.#worldsRoot, worldId);
83
+ try {
84
+ await readPublicationAt(root);
85
+ }
86
+ catch (error) {
87
+ if (isNodeError(error) && error.code === "ENOENT")
88
+ throw new FileNativeWorldNotFoundError({ cause: error });
89
+ throw error;
90
+ }
91
+ return this.operations.withExclusiveWorldStateMutation(worldId, async () => {
92
+ await rm(root, { recursive: true, force: true });
93
+ return { deleted: true };
94
+ });
95
+ }
96
+ async createFromContentPackage(input) {
97
+ assertIdentity(input.operationId, "operation ID");
98
+ assertIdentity(input.sourcePackageId, "Content-package local identity");
99
+ const inspection = inspectContentPackageCurrentTree(input.packageFiles);
100
+ if (inspection.status !== "usable") {
101
+ throw new FileNativeWorldCreationError("content_package_needs_repair", `Only a content package that passes all validation can create a world: ${inspection.issues
102
+ .map(({ message }) => message)
103
+ .join(";")}`);
104
+ }
105
+ const packageFiles = inspection.worldDocumentSnapshot.files.map(({ path, contents, encoding }) => ({
106
+ path,
107
+ contents,
108
+ ...(encoding === undefined ? {} : { encoding }),
109
+ }));
110
+ const stateSnapshot = WorldDocumentStore.open({
111
+ layout: "world_state",
112
+ files: packageFiles
113
+ .filter(({ path }) => isContentPackageWorldDocumentPath(path))
114
+ .map(({ path, contents, encoding }) => ({
115
+ path: toWorldStatePath(path),
116
+ contents,
117
+ ...(encoding === undefined ? {} : { encoding }),
118
+ })),
119
+ });
120
+ assertEquivalentWorldDocumentSnapshots(inspection.documents, stateSnapshot);
121
+ const state = selectSurface(stateSnapshot.files, "state/");
122
+ const control = selectSurface(packageFiles, "control/");
123
+ const boundInput = {
124
+ ...input,
125
+ packageFiles,
126
+ };
127
+ const sourceFingerprint = fingerprint(packageFiles);
128
+ const previous = await this.#readPublication(input.operationId);
129
+ if (previous !== null) {
130
+ if (previous.sourceFingerprint !== sourceFingerprint) {
131
+ throw new FileNativeWorldCreationError("operation_conflict", "The same operation ID is bound to a different creation payload");
132
+ }
133
+ const preview = this.#preview(boundInput, previous.worldId);
134
+ return {
135
+ outcome: "created",
136
+ world: await this.#currentSummary(previous),
137
+ preview,
138
+ };
139
+ }
140
+ // Preview happens before any staging write, so a slot/overlap/budget failure
141
+ // cannot leave a partially published world.
142
+ const worldId = `world-${operationDigest(input.operationId).slice(0, 24)}`;
143
+ const preview = this.#preview(boundInput, worldId);
144
+ const finalRoot = join(this.#worldsRoot, worldId);
145
+ const stagingRoot = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
146
+ const operationPath = this.#operationPath(input.operationId);
147
+ await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
148
+ await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
149
+ await rm(stagingRoot, { recursive: true, force: true });
150
+ try {
151
+ const openingMessage = genesisOpeningMessage(worldId, openingText(packageFiles));
152
+ const initialMaterials = [
153
+ { kind: "history_message", message: openingMessage.messageId },
154
+ ];
155
+ await writeSurface(stagingRoot, "state", state);
156
+ await writeSurface(stagingRoot, "control", control);
157
+ await mkdir(join(stagingRoot, "history"), { recursive: true });
158
+ await mkdir(join(stagingRoot, "runtime"), { recursive: true });
159
+ await writeIdempotentText(join(stagingRoot, "history", `00000000-${historyFileName(1, openingMessage)}`), openingMessage.exactText);
160
+ const genesis = {
161
+ schemaVersion: 1,
162
+ type: "file_native_genesis",
163
+ worldId,
164
+ operationId: input.operationId,
165
+ parentEndpoint: "genesis",
166
+ state: immutableFiles(state),
167
+ control: immutableFiles(control),
168
+ history: [openingMessage],
169
+ additionalMaterials: initialMaterials,
170
+ };
171
+ await writeJson(join(stagingRoot, "runtime", "genesis.json"), genesis);
172
+ await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
173
+ head: "genesis",
174
+ });
175
+ await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
176
+ const publication = {
177
+ schemaVersion: 1,
178
+ worldId,
179
+ title: inspection.displayName,
180
+ parentEndpoint: "genesis",
181
+ operationId: input.operationId,
182
+ sourceFingerprint,
183
+ };
184
+ await writeJson(join(stagingRoot, publicationFile), publication);
185
+ await durableTree(stagingRoot);
186
+ crashAtFileNativeWorldCreationEdge("after_staging");
187
+ await rename(stagingRoot, finalRoot);
188
+ await syncDirectory(this.#worldsRoot);
189
+ crashAtFileNativeWorldCreationEdge("after_atomic_publish");
190
+ await publishOperation(operationPath, publication);
191
+ return { outcome: "created", world: toSummary(publication), preview };
192
+ }
193
+ catch (error) {
194
+ await rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
195
+ const recovered = await this.#readPublication(input.operationId);
196
+ if (recovered !== null) {
197
+ if (recovered.sourceFingerprint === sourceFingerprint) {
198
+ return {
199
+ outcome: "created",
200
+ world: await this.#currentSummary(recovered),
201
+ preview,
202
+ };
203
+ }
204
+ throw new FileNativeWorldCreationError("operation_conflict", "The same operation ID is bound to a different creation payload", { cause: error });
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+ async getCreationOutcome(operationId) {
210
+ assertIdentity(operationId, "operation ID");
211
+ const publication = await this.#readPublication(operationId);
212
+ return publication === null
213
+ ? { outcome: "not_created" }
214
+ : { outcome: "created", world: await this.#currentSummary(publication) };
215
+ }
216
+ async readSurface(worldId, surface) {
217
+ assertIdentity(worldId, "World ID");
218
+ const root = join(this.#worldsRoot, worldId);
219
+ await readPublicationAt(root);
220
+ if (surface === "runtime") {
221
+ const genesis = await readJson(join(root, "runtime", "genesis.json"));
222
+ return {
223
+ ...genesis,
224
+ historyEntries: (await readTree(join(root, "history"))).length,
225
+ };
226
+ }
227
+ return readTree(join(root, surface));
228
+ }
229
+ async renderPlayerViews(worldId) {
230
+ const head = await this.currentHead(worldId);
231
+ return this.renderPlayerViewsAtHead(worldId, head);
232
+ }
233
+ /**
234
+ * Render only from the immutable Authority endpoint named by `head`.
235
+ * Materialized files may lag an accepted commit, so player-view projections
236
+ * must never pair a mutable state tree with a different response head.
237
+ */
238
+ async renderPlayerViewsAtHead(worldId, head) {
239
+ const endpoint = await this.recoverEndpoint(worldId, head);
240
+ const control = await this.readSurface(worldId, "control");
241
+ const source = control.find(({ path }) => path === "player-views.yaml");
242
+ return new PlayerViewRenderer().render({
243
+ snapshot: openWorldDocumentSnapshot(endpoint.state),
244
+ control: source?.contents ?? "",
245
+ });
246
+ }
247
+ async currentHead(worldId) {
248
+ assertIdentity(worldId, "World ID");
249
+ const root = join(this.#worldsRoot, worldId);
250
+ await readPublicationAt(root);
251
+ return (await readAcceptedAuthority(root))?.head ?? "genesis";
252
+ }
253
+ async currentHeadOperationId(worldId) {
254
+ assertIdentity(worldId, "World ID");
255
+ const root = join(this.#worldsRoot, worldId);
256
+ await readPublicationAt(root);
257
+ return ((await readAcceptedAuthority(root))?.commits.at(-1)?.operationId ?? null);
258
+ }
259
+ async saveControlDraft(worldId, files) {
260
+ assertIdentity(worldId, "World ID");
261
+ const root = join(this.#worldsRoot, worldId);
262
+ await readPublicationAt(root);
263
+ for (const file of files)
264
+ assertRelativePath(file.path);
265
+ const staging = join(root, `runtime/.control-draft-${randomUUID()}`);
266
+ await writeSurface(staging, "control", files);
267
+ const final = join(root, "runtime/control-draft");
268
+ await rm(final, { recursive: true, force: true });
269
+ await rename(join(staging, "control"), final);
270
+ await rm(staging, { recursive: true, force: true });
271
+ return { fingerprint: fingerprint(files) };
272
+ }
273
+ async previewControlDraft(worldId, prompt) {
274
+ const { state, draft } = await this.#readControlDraft(worldId);
275
+ const rendered = new PlayerViewRenderer().render({
276
+ snapshot: openWorldDocumentSnapshot(state),
277
+ control: draft.find(({ path }) => path === "player-views.yaml")?.contents ?? "",
278
+ });
279
+ if (rendered.diagnostics.length > 0)
280
+ throw new FileNativeWorldCreationError("content_package_needs_repair", `Player views in the control draft cannot be applied: ${rendered.diagnostics.map(({ message }) => message).join("; ")}`);
281
+ return this.#promptCompiler.preview({
282
+ endpoint: { id: `${worldId}:current`, commit: "current" },
283
+ hostBinding: prompt.hostBinding,
284
+ world: {
285
+ controlFingerprint: fingerprint(draft),
286
+ documentSnapshot: WorldDocumentStore.open({
287
+ layout: "world_state",
288
+ files: [
289
+ ...state.map(({ path, contents }) => ({
290
+ path: `state/${path}`,
291
+ contents,
292
+ })),
293
+ ...draft.map(({ path, contents }) => ({
294
+ path: `control/${path}`,
295
+ contents,
296
+ })),
297
+ ],
298
+ }),
299
+ additionalMaterials: [],
300
+ },
301
+ playerInputPlacement: "bootstrap",
302
+ playerInput: prompt.playerInput ?? "Preview world control.",
303
+ modelBinding: prompt.modelBinding,
304
+ });
305
+ }
306
+ async applyControlDraft(worldId, prompt) {
307
+ try {
308
+ return await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
309
+ if ((this.#activeControlUsers.get(worldId)?.size ?? 0) > 0)
310
+ throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by a running attempt");
311
+ const preview = await this.previewControlDraft(worldId, prompt);
312
+ const root = join(this.#worldsRoot, worldId);
313
+ const draft = join(root, "runtime/control-draft");
314
+ const replacement = join(root, `runtime/.control-apply-${randomUUID()}`);
315
+ await rename(draft, replacement);
316
+ const old = join(root, `runtime/.control-old-${randomUUID()}`);
317
+ await rename(join(root, "control"), old);
318
+ try {
319
+ await rename(replacement, join(root, "control"));
320
+ await rm(old, { recursive: true, force: true }).catch(() => undefined);
321
+ }
322
+ catch (error) {
323
+ await rename(old, join(root, "control")).catch(() => undefined);
324
+ throw error;
325
+ }
326
+ return {
327
+ controlFingerprint: preview.diagnosticBinding.controlFingerprint,
328
+ preview,
329
+ };
330
+ });
331
+ }
332
+ catch (error) {
333
+ if (error instanceof WorldOperationBusyError)
334
+ throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by a running play call chain");
335
+ throw error;
336
+ }
337
+ }
338
+ freezeControl(worldId, operationId) {
339
+ assertIdentity(worldId, "World ID");
340
+ assertIdentity(operationId, "operation ID");
341
+ const users = this.#activeControlUsers.get(worldId) ?? new Set();
342
+ users.add(operationId);
343
+ this.#activeControlUsers.set(worldId, users);
344
+ }
345
+ releaseControl(worldId, operationId) {
346
+ const users = this.#activeControlUsers.get(worldId);
347
+ users?.delete(operationId);
348
+ if (users?.size === 0)
349
+ this.#activeControlUsers.delete(worldId);
350
+ }
351
+ async #readControlDraft(worldId) {
352
+ assertIdentity(worldId, "World ID");
353
+ const root = join(this.#worldsRoot, worldId);
354
+ await readPublicationAt(root);
355
+ const [state, draft] = await Promise.all([
356
+ readTree(join(root, "state")),
357
+ readTree(join(root, "runtime/control-draft")),
358
+ ]);
359
+ if (draft.length === 0)
360
+ throw new FileNativeWorldCreationError("content_package_needs_repair", "World-control draft has not been saved");
361
+ return { state, draft };
362
+ }
363
+ /** Bind the current Authority endpoint for a model-directed play call chain. */
364
+ async bindPlayCallChain(worldId) {
365
+ assertIdentity(worldId, "World ID");
366
+ const root = join(this.#worldsRoot, worldId);
367
+ try {
368
+ await readPublicationAt(root);
369
+ }
370
+ catch (error) {
371
+ if (isNodeError(error) && error.code === "ENOENT") {
372
+ const worldRoot = await stat(root).catch((rootError) => {
373
+ if (isNodeError(rootError) && rootError.code === "ENOENT")
374
+ return null;
375
+ throw rootError;
376
+ });
377
+ if (worldRoot === null)
378
+ throw new FileNativeWorldNotFoundError({ cause: error });
379
+ throw new FileNativeWorldCreationError("world_corrupt", worldRoot.isDirectory()
380
+ ? "World publication is missing"
381
+ : "World storage root is not a directory", { cause: error });
382
+ }
383
+ throw error;
384
+ }
385
+ try {
386
+ 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
+ });
417
+ }
418
+ await publishJson(join(root, "runtime", "materialized-head.json"), {
419
+ head: publishedAuthority.head,
420
+ });
421
+ }
422
+ const [state, control, authority, materials, recovered] = await Promise.all([
423
+ readTree(join(root, "state")),
424
+ readTree(join(root, "control")),
425
+ readAcceptedAuthority(root),
426
+ readOptionalJson(join(root, "runtime", "additional-materials.json")),
427
+ this.recoverEndpoint(worldId),
428
+ ]);
429
+ return {
430
+ worldId,
431
+ parentHead: authority?.head ?? "genesis",
432
+ files: [...state, ...control].reduce((files, { path, contents }, index) => {
433
+ files[`${index < state.length ? "state" : "control"}/${path}`] =
434
+ contents;
435
+ return files;
436
+ }, {}),
437
+ additionalMaterials: materials?.items ?? [],
438
+ history: Object.fromEntries(recovered.history.map(({ messageId, exactText }) => [
439
+ messageId,
440
+ exactText,
441
+ ])),
442
+ };
443
+ });
444
+ }
445
+ catch (error) {
446
+ if (error instanceof WorldOperationBusyError)
447
+ throw new FileNativeWorldCreationError("operation_conflict", "World Authority is being updated by another durable operation");
448
+ throw error;
449
+ }
450
+ }
451
+ async reserveOperation(operationId) {
452
+ assertIdentity(operationId, "operation ID");
453
+ await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
454
+ const path = this.#operationOutcomePath(operationId);
455
+ try {
456
+ await writeFile(path, `${JSON.stringify({ outcome: "in_progress" }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
457
+ await syncFile(path);
458
+ await syncDirectory(this.#operationsRoot);
459
+ return true;
460
+ }
461
+ catch (error) {
462
+ if (isNodeError(error) && error.code === "EEXIST")
463
+ return false;
464
+ throw error;
465
+ }
466
+ }
467
+ async releaseOperationReservation(operationId) {
468
+ assertIdentity(operationId, "operation ID");
469
+ const path = this.#operationOutcomePath(operationId);
470
+ const current = await readOptionalJson(path);
471
+ if (current === null)
472
+ return;
473
+ if (!isRecord(current) ||
474
+ !hasExactKeys(current, ["outcome"]) ||
475
+ current.outcome !== "in_progress")
476
+ throw new FileNativeWorldCreationError("world_corrupt", "Operation reservation to release has an invalid shape");
477
+ await rm(path, { force: true });
478
+ await syncDirectory(this.#operationsRoot);
479
+ }
480
+ async recoverEndpoint(worldId, head) {
481
+ assertIdentity(worldId, "World ID");
482
+ const root = join(this.#worldsRoot, worldId);
483
+ 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);
530
+ }
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
+ }
541
+ async repairMaterialization(worldId) {
542
+ assertIdentity(worldId, "World ID");
543
+ try {
544
+ const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
545
+ await this.bindPlayCallChain(worldId);
546
+ const root = join(this.#worldsRoot, worldId);
547
+ const authority = await readAcceptedAuthority(root);
548
+ return authority?.commits.at(-1)?.operationId ?? null;
549
+ });
550
+ return operationId === null
551
+ ? { outcome: "not_started" }
552
+ : this.getOperationOutcome(operationId);
553
+ }
554
+ catch (error) {
555
+ if (error instanceof WorldOperationBusyError)
556
+ throw new FileNativeWorldCreationError("operation_conflict", "This world has a durable state operation and cannot repair materialization yet");
557
+ throw error;
558
+ }
559
+ }
560
+ async deriveWorld(input) {
561
+ assertIdentity(input.operationId, "Fork operation ID");
562
+ assertIdentity(input.sourceWorldId, "Source world ID");
563
+ assertIdentity(input.hostPresetId, "Host-preset ID");
564
+ if (input.requestDiscriminator !== undefined)
565
+ assertIdentity(input.requestDiscriminator, "Fork request discriminator");
566
+ if (input.sourceHead !== "genesis" &&
567
+ !/^commit:[1-9][0-9]*$/u.test(input.sourceHead))
568
+ throw new TypeError("Source endpoint is invalid");
569
+ const operationPath = this.#derivationOperationPath(input.operationId);
570
+ const requestFingerprint = derivationRequestFingerprint(input);
571
+ const worldId = `world-${operationDigest(`derive:${input.operationId}`).slice(0, 24)}`;
572
+ const finalRoot = join(this.#worldsRoot, worldId);
573
+ const previous = await readOptionalJson(operationPath);
574
+ if (previous !== null) {
575
+ if (!(await derivationPublicationMatches(join(this.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
576
+ throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
577
+ return {
578
+ outcome: "derived",
579
+ world: await this.#currentSummary(previous),
580
+ };
581
+ }
582
+ const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
583
+ if (alreadyPublished !== null) {
584
+ if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
585
+ throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
586
+ await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
587
+ await publishJson(operationPath, alreadyPublished);
588
+ return {
589
+ outcome: "derived",
590
+ world: await this.#currentSummary(alreadyPublished),
591
+ };
592
+ }
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 }),
645
+ });
646
+ }
647
+ if (cloned.commits.length > 0)
648
+ await writeJson(join(staging, "runtime", "play-authority.json"), {
649
+ schemaVersion: 1,
650
+ head: input.sourceHead,
651
+ commits: cloned.commits,
652
+ });
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) };
675
+ }
676
+ 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
+ }
685
+ throw error;
686
+ }
687
+ }
688
+ async failOperation(operationId, outcome = "failed") {
689
+ const current = await this.getOperationOutcome(operationId);
690
+ if (isCommittedOutcome(current))
691
+ return;
692
+ await writeJson(this.#operationOutcomePath(operationId), { outcome });
693
+ }
694
+ async getOperationOutcome(operationId) {
695
+ return this.#readOperationOutcome(operationId);
696
+ }
697
+ async #readOperationOutcome(operationId) {
698
+ assertIdentity(operationId, "operation ID");
699
+ let directRecord;
700
+ try {
701
+ const value = await readOptionalJson(this.#operationOutcomePath(operationId));
702
+ if (value === null)
703
+ directRecord = null;
704
+ else {
705
+ assertOperationOutcomeIntegrity(value);
706
+ directRecord = value;
707
+ }
708
+ }
709
+ catch (error) {
710
+ if (error instanceof FileNativeWorldCreationError)
711
+ throw error;
712
+ throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
713
+ }
714
+ const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
715
+ if (direct?.outcome === "committed" ||
716
+ 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
+ }
754
+ }
755
+ return direct?.outcome === "in_progress"
756
+ ? { outcome: "in_progress" }
757
+ : (direct ?? { outcome: "not_started" });
758
+ }
759
+ /**
760
+ * Replace one committed player message on the world's active timeline
761
+ * without changing world identity or destructively rewriting Authority.
762
+ * The new immutable commit restores the selected player's logical parent
763
+ * snapshot and appends the replacement player text in one acceptance step.
764
+ */
765
+ async reviseTimeline(input) {
766
+ assertIdentity(input.operationId, "Timeline-revision operation ID");
767
+ assertIdentity(input.worldId, "World ID");
768
+ if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(input.expectedCurrentHead) ||
769
+ !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(input.restoresHead) ||
770
+ !/^commit:[1-9][0-9]*$/u.test(input.replacesHead))
771
+ throw new TypeError("Timeline-revision endpoint is invalid");
772
+ if (input.replacementText.trim() === "")
773
+ throw new TypeError("Edited player message cannot be empty");
774
+ if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
775
+ throw new TypeError("Timeline-revision request fingerprint is invalid");
776
+ const existing = await this.getOperationOutcome(input.operationId);
777
+ if (isCommittedOutcome(existing)) {
778
+ const authority = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
779
+ const commit = authority?.commits.find(({ operationId }) => operationId === input.operationId);
780
+ if (existing.worldId !== input.worldId ||
781
+ commit?.mode !== "timeline_revision" ||
782
+ commit.timelineRevision?.restoresHead !== input.restoresHead ||
783
+ commit.timelineRevision.replacesHead !== input.replacesHead ||
784
+ commit.timelineRevision.requestFingerprint !==
785
+ input.requestFingerprint ||
786
+ !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({
787
+ role,
788
+ exactText,
789
+ })), [{ role: "player", exactText: input.replacementText }]))
790
+ throw new FileNativeWorldCreationError("operation_conflict", "The same timeline-revision operation ID is bound to a different request");
791
+ return existing;
792
+ }
793
+ const root = join(this.#worldsRoot, input.worldId);
794
+ const [authority, replacement] = await Promise.all([
795
+ readAcceptedAuthority(root),
796
+ this.recoverEndpoint(input.worldId, input.restoresHead),
797
+ ]);
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 ||
803
+ !replaced.historyAppend.some(({ role }) => role === "player") ||
804
+ logicalParent !== input.restoresHead)
805
+ throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
806
+ return this.#commitHistoryChange({
807
+ operationId: input.operationId,
808
+ worldId: input.worldId,
809
+ parentHead: input.expectedCurrentHead,
810
+ historyAppend: [{ role: "player", exactText: input.replacementText }],
811
+ nextMaterials: structuredClone(replacement.additionalMaterials),
812
+ stateChanges: [],
813
+ mode: "timeline_revision",
814
+ timelineRevision: {
815
+ restoresHead: input.restoresHead,
816
+ replacesHead: input.replacesHead,
817
+ requestFingerprint: input.requestFingerprint,
818
+ replacementState: immutableFiles(replacement.state),
819
+ replacementHistory: structuredClone(replacement.history),
820
+ },
821
+ });
822
+ }
823
+ async commitCorrection(input) {
824
+ try {
825
+ const commit = async () => {
826
+ let operationReserved = input.stateOperationClaim !== undefined;
827
+ if (!operationReserved) {
828
+ const existing = await this.getOperationOutcome(input.operationId);
829
+ if (existing.outcome === "not_started")
830
+ operationReserved = await this.reserveOperation(input.operationId);
831
+ }
832
+ return this.#commitHistoryChange({
833
+ operationId: input.operationId,
834
+ worldId: input.worldId,
835
+ parentHead: input.parentHead,
836
+ nextMaterials: input.nextMaterials,
837
+ stateChanges: input.stateChanges,
838
+ mode: "correction",
839
+ historyAppend: [],
840
+ operationReserved,
841
+ });
842
+ };
843
+ return input.stateOperationClaim === undefined
844
+ ? await this.operations.withExclusiveWorldStateMutation(input.worldId, commit)
845
+ : await this.operations.withCorrectionWorldClaim(input.stateOperationClaim, commit);
846
+ }
847
+ catch (error) {
848
+ if (error instanceof WorldOperationBusyError)
849
+ throw new FileNativeWorldCreationError("operation_conflict", "This world already has a durable state operation in progress");
850
+ throw error;
851
+ }
852
+ }
853
+ /**
854
+ * Persist one already-visible step of the model-directed play call chain.
855
+ * Player text, assistant text and world changes deliberately arrive in
856
+ * separate commits; there is no synthetic call-chain-wide settlement boundary.
857
+ */
858
+ async commitPlayStep(input) {
859
+ if (input.historyAppend.length === 0 && input.stateChanges.length === 0)
860
+ throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
861
+ return this.#commitHistoryChange({
862
+ ...input,
863
+ mode: "play",
864
+ });
865
+ }
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
+ async #commitHistoryChange(input) {
879
+ const existing = await this.getOperationOutcome(input.operationId);
880
+ if (isCommittedOutcome(existing)) {
881
+ 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))
888
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
889
+ return existing;
890
+ }
891
+ if (existing.outcome !== "not_started" &&
892
+ !(input.operationReserved === true && existing.outcome === "in_progress"))
893
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
894
+ const root = join(this.#worldsRoot, input.worldId);
895
+ try {
896
+ return await this.operations.withWorldAuthorityLock(input.worldId, async () => {
897
+ const concurrent = await this.#readOperationOutcome(input.operationId);
898
+ if (isCommittedOutcome(concurrent)) {
899
+ 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))
905
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
906
+ return concurrent;
907
+ }
908
+ const authorityPath = join(root, "runtime", "play-authority.json");
909
+ const authority = (await readAcceptedAuthority(root)) ?? {
910
+ schemaVersion: 1,
911
+ head: "genesis",
912
+ commits: [],
913
+ };
914
+ if (authority.head !== input.parentHead) {
915
+ throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
916
+ }
917
+ const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
918
+ if ((materialized?.head ?? "genesis") !== authority.head)
919
+ 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")),
922
+ readTree(join(root, "control")),
923
+ ]);
924
+ const candidateState = new Map(currentState.map((file) => [file.path, file.contents]));
925
+ if (input.mode === "timeline_revision") {
926
+ if (input.timelineRevision === undefined ||
927
+ input.stateChanges.length !== 0)
928
+ 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
+ }
937
+ else {
938
+ if (input.timelineRevision !== undefined)
939
+ throw new FileNativeWorldCreationError("operation_conflict", "A regular commit cannot carry a timeline-revision payload");
940
+ for (const change of input.stateChanges) {
941
+ assertRelativePath(change.relativePath);
942
+ const previous = candidateState.get(change.relativePath);
943
+ const previousHash = previous === undefined ? null : sha256(previous);
944
+ if (previousHash !== change.expectedPreviousHash ||
945
+ sha256(change.canonicalNextBytes) !== change.nextHash)
946
+ throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
947
+ candidateState.set(change.relativePath, change.canonicalNextBytes);
948
+ }
949
+ }
950
+ const inspection = inspectContentPackageCurrentTree([
951
+ ...[...candidateState].map(([path, contents]) => ({
952
+ path: `world/${path}`,
953
+ contents,
954
+ })),
955
+ ...currentControl.map(({ path, contents }) => ({
956
+ path: `control/${path}`,
957
+ contents,
958
+ })),
959
+ ], { requireOpening: false });
960
+ if (inspection.status !== "usable")
961
+ 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,
966
+ operationId: input.operationId,
967
+ parentHead: input.parentHead,
968
+ head,
969
+ mode: input.mode,
970
+ historyAppend: input.historyAppend.map((message, index) => ({
971
+ messageId: `${input.worldId}.message.${sequence}.${index + 1}.${message.role}`,
972
+ ...structuredClone(message),
973
+ })),
974
+ stateChanges: structuredClone(input.stateChanges),
975
+ nextAdditionalMaterials: input.nextMaterials,
976
+ ...(input.mode === "correction"
977
+ ? {
978
+ correctionTargets: input.stateChanges.map(({ documentId }) => documentId),
979
+ corrects: input.parentHead,
980
+ }
981
+ : {}),
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],
994
+ });
995
+ crashAtFileNativeAuthorityEdge("after_commit_acceptance");
996
+ const pending = {
997
+ outcome: "committed_materialization_pending",
998
+ worldId: input.worldId,
999
+ parentHead: input.parentHead,
1000
+ head,
1001
+ historyAppend: structuredClone(input.historyAppend),
1002
+ nextAdditionalMaterials: structuredClone(input.nextMaterials),
1003
+ mode: input.mode,
1004
+ };
1005
+ await publishJson(this.#operationOutcomePath(input.operationId), pending);
1006
+ 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
+ });
1016
+ crashAtFileNativeAuthorityEdge("after_materialization");
1017
+ await publishJson(join(root, "runtime", "materialized-head.json"), {
1018
+ head,
1019
+ });
1020
+ const outcome = { ...pending, outcome: "committed" };
1021
+ await publishJson(this.#operationOutcomePath(input.operationId), outcome);
1022
+ return outcome;
1023
+ }
1024
+ catch (error) {
1025
+ if (error instanceof FileNativeWorldCreationError &&
1026
+ (error.code === "world_corrupt" ||
1027
+ error.code === "inconsistent_materialization"))
1028
+ throw error;
1029
+ return pending;
1030
+ }
1031
+ });
1032
+ }
1033
+ catch (error) {
1034
+ if (error instanceof WorldOperationBusyError)
1035
+ throw new FileNativeWorldCreationError("operation_conflict", "World Authority commit is being serialized by another process");
1036
+ throw error;
1037
+ }
1038
+ }
1039
+ async readAuthorityHistory(worldId) {
1040
+ assertIdentity(worldId, "World ID");
1041
+ const root = join(this.#worldsRoot, worldId);
1042
+ 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) };
1047
+ }
1048
+ async readAuthorityEndpoint(worldId, endpoint) {
1049
+ const recovered = await this.recoverEndpoint(worldId, endpoint);
1050
+ return {
1051
+ endpoint: recovered.head,
1052
+ state: recovered.state,
1053
+ history: recovered.history.map(({ role, exactText }) => ({
1054
+ role,
1055
+ exactText,
1056
+ })),
1057
+ additionalMaterials: recovered.additionalMaterials,
1058
+ };
1059
+ }
1060
+ async traceCorrections(worldId, endpoint) {
1061
+ const { commits } = await this.readAuthorityHistory(worldId);
1062
+ const commit = commits.find(({ head }) => head === endpoint);
1063
+ if (endpoint !== "genesis" && commit === undefined)
1064
+ throw new FileNativeWorldCreationError("world_corrupt", `Historical endpoint does not exist: ${endpoint}`);
1065
+ return {
1066
+ endpoint,
1067
+ corrects: commit?.mode === "correction" ? (commit.corrects ?? null) : null,
1068
+ correctedBy: commits
1069
+ .filter((candidate) => candidate.mode === "correction" && candidate.corrects === endpoint)
1070
+ .map(({ head }) => head),
1071
+ };
1072
+ }
1073
+ async #readPublication(operationId) {
1074
+ let publication;
1075
+ try {
1076
+ publication = await readJson(this.#operationPath(operationId));
1077
+ }
1078
+ catch (error) {
1079
+ if (isNodeError(error) && error.code === "ENOENT") {
1080
+ const worldId = `world-${operationDigest(operationId).slice(0, 24)}`;
1081
+ try {
1082
+ publication = await readPublicationAt(join(this.#worldsRoot, worldId));
1083
+ }
1084
+ catch (worldError) {
1085
+ if (isNodeError(worldError) && worldError.code === "ENOENT")
1086
+ return null;
1087
+ throw worldError;
1088
+ }
1089
+ if (publication.operationId !== operationId) {
1090
+ throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
1091
+ }
1092
+ await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
1093
+ await publishOperation(this.#operationPath(operationId), publication);
1094
+ }
1095
+ else {
1096
+ throw error;
1097
+ }
1098
+ }
1099
+ const worldPublication = await readPublicationAt(join(this.#worldsRoot, publication.worldId));
1100
+ if (JSON.stringify(worldPublication) !== JSON.stringify(publication)) {
1101
+ throw new FileNativeWorldCreationError("world_corrupt", "Creation operation does not match the world publication record");
1102
+ }
1103
+ return publication;
1104
+ }
1105
+ #currentSummary(publication) {
1106
+ return readWorldSummaryAt(join(this.#worldsRoot, publication.worldId), publication);
1107
+ }
1108
+ #operationPath(operationId) {
1109
+ return join(this.#operationsRoot, `${operationDigest(operationId)}.json`);
1110
+ }
1111
+ #operationOutcomePath(operationId) {
1112
+ return join(this.#operationsRoot, `outcome-${operationDigest(operationId)}.json`);
1113
+ }
1114
+ #derivationOperationPath(operationId) {
1115
+ return join(this.#operationsRoot, `derive-${operationDigest(operationId)}.json`);
1116
+ }
1117
+ #preview(input, worldId) {
1118
+ const openingMessage = genesisOpeningMessage(worldId, openingText(input.packageFiles));
1119
+ return this.#promptCompiler.preview({
1120
+ endpoint: { id: `${worldId}:genesis`, commit: "genesis" },
1121
+ hostBinding: input.prompt.hostBinding,
1122
+ world: {
1123
+ controlFingerprint: fingerprint(input.packageFiles.filter(({ path }) => path.startsWith("control/"))),
1124
+ documentSnapshot: WorldDocumentStore.open({
1125
+ layout: "content_package",
1126
+ files: input.packageFiles,
1127
+ }),
1128
+ history: { [openingMessage.messageId]: openingMessage.exactText },
1129
+ additionalMaterials: [
1130
+ { kind: "history_message", message: openingMessage.messageId },
1131
+ ],
1132
+ },
1133
+ playerInputPlacement: "bootstrap",
1134
+ playerInput: input.prompt.playerInput ??
1135
+ "(Preview placeholder: the player's first real action after creating the world)",
1136
+ modelBinding: input.prompt.modelBinding,
1137
+ });
1138
+ }
1139
+ }
1140
+ function toWorldStatePath(path) {
1141
+ return path.startsWith("world/")
1142
+ ? `state/${path.slice("world/".length)}`
1143
+ : path;
1144
+ }
1145
+ function openingText(files) {
1146
+ const opening = files.find(({ path, encoding }) => path === "opening.md" && encoding === undefined);
1147
+ if (opening === undefined)
1148
+ throw new FileNativeWorldCreationError("content_package_needs_repair", "Content package is missing a usable opening.md");
1149
+ return opening.contents;
1150
+ }
1151
+ function genesisOpeningMessage(worldId, exactText) {
1152
+ return {
1153
+ messageId: `${worldId}.message.genesis.narrator`,
1154
+ role: "narrator",
1155
+ exactText,
1156
+ };
1157
+ }
1158
+ function selectSurface(files, prefix) {
1159
+ return files
1160
+ .filter(({ path }) => path.startsWith(prefix))
1161
+ .map(({ path, contents, encoding }) => ({
1162
+ path: path.slice(prefix.length),
1163
+ contents,
1164
+ ...(encoding === undefined ? {} : { encoding }),
1165
+ }));
1166
+ }
1167
+ function assertEquivalentWorldDocumentSnapshots(contentDocuments, worldState) {
1168
+ if (worldState.status !== "usable") {
1169
+ throw new FileNativeWorldCreationError("content_package_needs_repair", `Copied state world-document snapshot failed validation: ${worldState.diagnostics
1170
+ .map(({ message }) => message)
1171
+ .join(";")}`);
1172
+ }
1173
+ const stateFiles = worldState.files.filter(({ path }) => path.startsWith("state/"));
1174
+ const mismatch = stateFiles.length !== contentDocuments.length ||
1175
+ contentDocuments.some((document) => {
1176
+ if (!document.path.startsWith("world/"))
1177
+ return true;
1178
+ const result = worldState.query({
1179
+ kind: "read_document",
1180
+ document: {
1181
+ logicalPath: `state/${document.path.slice("world/".length)}`,
1182
+ },
1183
+ maxBytes: 4,
1184
+ });
1185
+ return (!result.ok ||
1186
+ result.kind !== "read_document" ||
1187
+ result.document.documentId !== document.id ||
1188
+ result.document.shortRef !== document.ref ||
1189
+ result.document.codec !== document.codec);
1190
+ });
1191
+ if (mismatch) {
1192
+ throw new FileNativeWorldCreationError("content_package_needs_repair", "Copied state world-document identity, short reference, or codec does not match the content-package snapshot");
1193
+ }
1194
+ }
1195
+ async function writeSurface(root, surface, files) {
1196
+ for (const file of files) {
1197
+ assertRelativePath(file.path);
1198
+ const path = join(root, surface, file.path);
1199
+ await mkdir(dirname(path), { recursive: true });
1200
+ await writeFile(path, file.encoding === "base64"
1201
+ ? Buffer.from(file.contents, "base64")
1202
+ : file.contents);
1203
+ }
1204
+ }
1205
+ async function readTree(root, prefix = "") {
1206
+ let entries;
1207
+ try {
1208
+ entries = await readdir(join(root, prefix), { withFileTypes: true });
1209
+ }
1210
+ catch (error) {
1211
+ if (isNodeError(error) && error.code === "ENOENT")
1212
+ return [];
1213
+ throw error;
1214
+ }
1215
+ const files = [];
1216
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1217
+ const path = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
1218
+ if (entry.isDirectory())
1219
+ files.push(...(await readTree(root, path)));
1220
+ else if (entry.isFile())
1221
+ files.push({ path, contents: await readFile(join(root, path), "utf8") });
1222
+ }
1223
+ return files;
1224
+ }
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
+ function immutableFiles(files) {
1239
+ return files.map(({ path, contents }) => ({
1240
+ path,
1241
+ sha256: sha256(contents),
1242
+ canonicalBytes: contents,
1243
+ }));
1244
+ }
1245
+ function sha256(contents) {
1246
+ return `sha256:${createHash("sha256").update(contents).digest("hex")}`;
1247
+ }
1248
+ function fingerprint(files) {
1249
+ const hash = createHash("sha256");
1250
+ for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
1251
+ hash.update(file.path).update("\0").update(file.contents).update("\0");
1252
+ }
1253
+ return `sha256:${hash.digest("hex")}`;
1254
+ }
1255
+ function operationDigest(operationId) {
1256
+ return createHash("sha256").update(operationId).digest("hex");
1257
+ }
1258
+ async function publishOperation(path, value) {
1259
+ const temporary = `${path}.${randomUUID()}.tmp`;
1260
+ await writeJson(temporary, value);
1261
+ await rename(temporary, path);
1262
+ await syncDirectory(dirname(path));
1263
+ }
1264
+ function isRecord(value) {
1265
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1266
+ }
1267
+ function hasExactKeys(value, keys) {
1268
+ return (Object.keys(value).length === keys.length &&
1269
+ keys.every((key) => Object.hasOwn(value, key)));
1270
+ }
1271
+ function hasRequiredAndOptionalKeys(value, required, optional) {
1272
+ const allowed = new Set([...required, ...optional]);
1273
+ return (required.every((key) => Object.hasOwn(value, key)) &&
1274
+ Object.keys(value).every((key) => allowed.has(key)));
1275
+ }
1276
+ function assertOperationOutcomeIntegrity(value) {
1277
+ if (!isRecord(value) || typeof value.outcome !== "string")
1278
+ throw corruptOperationOutcome();
1279
+ if (value.outcome === "in_progress") {
1280
+ if (!hasExactKeys(value, ["outcome"]))
1281
+ throw corruptOperationOutcome();
1282
+ return;
1283
+ }
1284
+ if (value.outcome === "failed" || value.outcome === "cancelled") {
1285
+ if (!hasExactKeys(value, ["outcome"]))
1286
+ throw corruptOperationOutcome();
1287
+ return;
1288
+ }
1289
+ if (value.outcome === "committed" ||
1290
+ value.outcome === "committed_materialization_pending") {
1291
+ if (!hasExactKeys(value, [
1292
+ "outcome",
1293
+ "worldId",
1294
+ "parentHead",
1295
+ "head",
1296
+ "historyAppend",
1297
+ "nextAdditionalMaterials",
1298
+ "mode",
1299
+ ]) ||
1300
+ typeof value.worldId !== "string" ||
1301
+ value.worldId.length === 0 ||
1302
+ typeof value.parentHead !== "string" ||
1303
+ !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1304
+ typeof value.head !== "string" ||
1305
+ !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1306
+ !Array.isArray(value.historyAppend) ||
1307
+ !value.historyAppend.every(isPlayHistoryAppendInput) ||
1308
+ !Array.isArray(value.nextAdditionalMaterials) ||
1309
+ !value.nextAdditionalMaterials.every(isAuthorityMaterialSelection) ||
1310
+ !(value.mode === "play" ||
1311
+ value.mode === "correction" ||
1312
+ value.mode === "timeline_revision"))
1313
+ throw corruptOperationOutcome();
1314
+ return;
1315
+ }
1316
+ throw corruptOperationOutcome();
1317
+ }
1318
+ function projectOperationOutcome(value) {
1319
+ return structuredClone(value);
1320
+ }
1321
+ function corruptOperationOutcome() {
1322
+ return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1323
+ }
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))
1332
+ throw corruptOperationOutcome();
1333
+ }
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)
1339
+ 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
+ }
1355
+ const existingMaterials = await readOptionalJson(join(worldRoot, "runtime", "additional-materials.json"));
1356
+ if (existingMaterials?.head === commit.head &&
1357
+ !isDeepStrictEqual(existingMaterials.items, commit.nextAdditionalMaterials))
1358
+ throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1359
+ }
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;
1368
+ }
1369
+ }
1370
+ function isAuthorityMaterialSelection(value) {
1371
+ if (!isRecord(value))
1372
+ return false;
1373
+ if (value.kind === "document")
1374
+ return (hasExactKeys(value, ["kind", "document"]) &&
1375
+ typeof value.document === "string");
1376
+ if (value.kind === "node")
1377
+ return (hasExactKeys(value, ["kind", "document", "locator"]) &&
1378
+ typeof value.document === "string" &&
1379
+ isAuthorityMaterialLocator(value.locator));
1380
+ if (value.kind === "history_message")
1381
+ return (hasExactKeys(value, ["kind", "message"]) &&
1382
+ typeof value.message === "string");
1383
+ return (value.kind === "history_commit" &&
1384
+ hasExactKeys(value, ["kind", "commit"]) &&
1385
+ typeof value.commit === "string");
1386
+ }
1387
+ function isAuthorityMaterialLocator(value) {
1388
+ return (isRecord(value) &&
1389
+ hasRequiredAndOptionalKeys(value, [], ["yaml", "markdown"]) &&
1390
+ (value.yaml === undefined ||
1391
+ (Array.isArray(value.yaml) &&
1392
+ value.yaml.every((segment) => typeof segment === "string" || Number.isSafeInteger(segment)))) &&
1393
+ (value.markdown === undefined ||
1394
+ (Array.isArray(value.markdown) &&
1395
+ value.markdown.every((segment) => typeof segment === "string"))));
1396
+ }
1397
+ async function publishJson(path, value) {
1398
+ const temporary = `${path}.${randomUUID()}.tmp`;
1399
+ await writeJson(temporary, value);
1400
+ await rename(temporary, path);
1401
+ await syncDirectory(dirname(path));
1402
+ }
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) {
1583
+ if (!isRecord(value) ||
1584
+ !hasExactKeys(value, [
1585
+ "restoresHead",
1586
+ "replacesHead",
1587
+ "requestFingerprint",
1588
+ "replacementState",
1589
+ "replacementHistory",
1590
+ ]) ||
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");
1625
+ }
1626
+ function isPlayHistoryAppendInput(value) {
1627
+ return (isRecord(value) &&
1628
+ hasExactKeys(value, ["role", "exactText"]) &&
1629
+ (value.role === "player" || value.role === "narrator") &&
1630
+ typeof value.exactText === "string");
1631
+ }
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
+ }
1708
+ 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);
1733
+ }
1734
+ async function replaceMaterializedTree(root, files) {
1735
+ const next = new Map();
1736
+ for (const file of files) {
1737
+ assertRelativePath(file.path);
1738
+ if (next.has(file.path))
1739
+ throw new FileNativeWorldCreationError("world_corrupt", `Timeline-revision materialization contains a duplicate path: ${file.path}`);
1740
+ next.set(file.path, file.contents);
1741
+ }
1742
+ const current = await readTree(root);
1743
+ for (const [path, contents] of next) {
1744
+ const existing = current.find((file) => file.path === path)?.contents;
1745
+ if (existing !== contents)
1746
+ await publishText(join(root, path), contents);
1747
+ }
1748
+ for (const file of current) {
1749
+ if (next.has(file.path))
1750
+ continue;
1751
+ const path = join(root, file.path);
1752
+ await rm(path, { force: true });
1753
+ await syncDirectory(dirname(path));
1754
+ }
1755
+ }
1756
+ async function publishText(path, contents) {
1757
+ await mkdir(dirname(path), { recursive: true });
1758
+ const temporary = `${path}.${randomUUID()}.tmp`;
1759
+ await writeFile(temporary, contents, "utf8");
1760
+ await syncFile(temporary);
1761
+ await rename(temporary, path);
1762
+ await syncDirectory(dirname(path));
1763
+ }
1764
+ 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
+ };
1783
+ });
1784
+ }
1785
+ async function writeIdempotentText(path, contents) {
1786
+ try {
1787
+ const existing = await readFile(path, "utf8");
1788
+ if (existing !== contents)
1789
+ throw new FileNativeWorldCreationError("inconsistent_materialization", "History-message materialization content conflict");
1790
+ }
1791
+ catch (error) {
1792
+ if (!isNodeError(error) || error.code !== "ENOENT")
1793
+ throw error;
1794
+ const temporary = `${path}.${randomUUID()}.tmp`;
1795
+ await writeFile(temporary, contents, "utf8");
1796
+ await syncFile(temporary);
1797
+ await rename(temporary, path);
1798
+ await syncDirectory(dirname(path));
1799
+ }
1800
+ }
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
+ function isCommittedOutcome(outcome) {
1868
+ return (outcome.outcome === "committed" ||
1869
+ outcome.outcome === "committed_materialization_pending");
1870
+ }
1871
+ function assertSameOperationOutcome(outcome, input) {
1872
+ if (outcome.worldId !== input.worldId ||
1873
+ outcome.parentHead !== input.parentHead ||
1874
+ !isDeepStrictEqual(outcome.historyAppend, input.historyAppend) ||
1875
+ !isDeepStrictEqual(outcome.nextAdditionalMaterials, input.nextMaterials)) {
1876
+ throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different commit payload");
1877
+ }
1878
+ }
1879
+ function openWorldDocumentSnapshot(files) {
1880
+ return WorldDocumentStore.open({
1881
+ layout: "world_state",
1882
+ files: files.map((file) => ({
1883
+ path: `state/${file.path}`,
1884
+ contents: file.contents,
1885
+ ...(file.encoding === undefined ? {} : { encoding: file.encoding }),
1886
+ })),
1887
+ });
1888
+ }
1889
+ async function writeJson(path, value) {
1890
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
1891
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
1892
+ encoding: "utf8",
1893
+ mode: 0o600,
1894
+ });
1895
+ await syncFile(path);
1896
+ }
1897
+ async function syncFile(path) {
1898
+ const handle = await open(path, "r");
1899
+ try {
1900
+ await handle.sync();
1901
+ }
1902
+ finally {
1903
+ await handle.close();
1904
+ }
1905
+ }
1906
+ async function syncDirectory(path) {
1907
+ const handle = await open(path, "r");
1908
+ try {
1909
+ await handle.sync();
1910
+ }
1911
+ finally {
1912
+ await handle.close();
1913
+ }
1914
+ }
1915
+ async function durableTree(root) {
1916
+ const entries = await readdir(root, { withFileTypes: true });
1917
+ for (const entry of entries) {
1918
+ const path = join(root, entry.name);
1919
+ if (entry.isDirectory())
1920
+ await durableTree(path);
1921
+ else if (entry.isFile())
1922
+ await syncFile(path);
1923
+ }
1924
+ await syncDirectory(root);
1925
+ }
1926
+ async function readPublicationAt(root) {
1927
+ return readJson(join(root, publicationFile));
1928
+ }
1929
+ async function readWorldSummaryAt(root, publication) {
1930
+ const published = publication ?? (await readPublicationAt(root));
1931
+ const metadata = await readOptionalJson(join(root, localMetadataFile));
1932
+ if (metadata === null)
1933
+ return toSummary(published);
1934
+ if (!isWorldLocalMetadata(metadata) || metadata.worldId !== published.worldId)
1935
+ throw new FileNativeWorldCreationError("world_corrupt", "Local world shell is corrupt");
1936
+ return { ...toSummary(published), title: metadata.name };
1937
+ }
1938
+ async function readJson(path) {
1939
+ return JSON.parse(await readFile(path, "utf8"));
1940
+ }
1941
+ async function readOptionalJson(path) {
1942
+ try {
1943
+ return await readJson(path);
1944
+ }
1945
+ catch (error) {
1946
+ if (isNodeError(error) && error.code === "ENOENT")
1947
+ return null;
1948
+ throw error;
1949
+ }
1950
+ }
1951
+ function toSummary(publication) {
1952
+ return {
1953
+ worldId: publication.worldId,
1954
+ title: publication.title,
1955
+ parentEndpoint: publication.parentEndpoint,
1956
+ };
1957
+ }
1958
+ const worldNamePattern = /^[^\r\n]{1,160}$/u;
1959
+ function validWorldName(name) {
1960
+ return worldNamePattern.test(name);
1961
+ }
1962
+ function derivedWorldName(sourceName) {
1963
+ const suffix = " (fork)";
1964
+ return `${Array.from(sourceName)
1965
+ .slice(0, 160 - Array.from(suffix).length)
1966
+ .join("")}${suffix}`;
1967
+ }
1968
+ function derivationRequestFingerprint(input) {
1969
+ return sha256(JSON.stringify({
1970
+ schema: "narraeon.file-native-derivation-request/v2",
1971
+ sourceWorldId: input.sourceWorldId,
1972
+ sourceHead: input.sourceHead,
1973
+ hostPresetId: input.hostPresetId,
1974
+ requestDiscriminator: input.requestDiscriminator ?? null,
1975
+ }));
1976
+ }
1977
+ async function derivationPublicationMatches(worldRoot, publication, input, requestFingerprint) {
1978
+ if (publication.operationId !== input.operationId)
1979
+ return false;
1980
+ if (publication.sourceFingerprint === requestFingerprint)
1981
+ return true;
1982
+ // V1 briefly persisted raw provenance inside genesis. It remains readable
1983
+ // only so a previously published operation can finish an idempotent retry;
1984
+ // newly derived worlds never write or consult this relationship.
1985
+ const legacy = await readOptionalJson(join(worldRoot, "runtime", "genesis.json"));
1986
+ return (legacy?.derivedFrom?.worldId === input.sourceWorldId &&
1987
+ legacy.derivedFrom.head === input.sourceHead &&
1988
+ legacy.hostPresetId === input.hostPresetId &&
1989
+ input.requestDiscriminator === undefined);
1990
+ }
1991
+ function isWorldLocalMetadata(value) {
1992
+ return (isRecord(value) &&
1993
+ hasExactKeys(value, ["schemaVersion", "worldId", "name"]) &&
1994
+ value.schemaVersion === 1 &&
1995
+ typeof value.worldId === "string" &&
1996
+ typeof value.name === "string" &&
1997
+ validWorldName(value.name));
1998
+ }
1999
+ function assertIdentity(value, label) {
2000
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(value)) {
2001
+ throw new TypeError(`${label} is invalid`);
2002
+ }
2003
+ }
2004
+ function assertRelativePath(path) {
2005
+ if (path.length === 0 ||
2006
+ path.startsWith("/") ||
2007
+ path.includes("\\") ||
2008
+ path.split("/").some((part) => part === "" || part === "." || part === "..")) {
2009
+ throw new TypeError("World file path is invalid");
2010
+ }
2011
+ }
2012
+ function isNodeError(error) {
2013
+ return error instanceof Error && "code" in error;
2014
+ }
2015
+ function crashAtFileNativeWorldCreationEdge(edge) {
2016
+ if (process.env.NODE_ENV === "test" &&
2017
+ process.env
2018
+ .NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_WORLD_CREATION_EDGE === edge) {
2019
+ process.kill(process.pid, "SIGKILL");
2020
+ }
2021
+ }
2022
+ function crashAtFileNativeAuthorityEdge(edge) {
2023
+ if (process.env.NODE_ENV === "test" &&
2024
+ process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_AUTHORITY_EDGE ===
2025
+ edge) {
2026
+ process.kill(process.pid, "SIGKILL");
2027
+ }
2028
+ }