narraeon 0.4.0 → 0.5.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 (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +186 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +445 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-BXfelCi9.js +1207 -0
  70. package/dist/web/assets/index-BYnhUxlk.css +1 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D8YKfmNG.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,3 +1,7 @@
1
+ import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
2
+ import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
3
+ import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
4
+ import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
1
5
  import { createHash, randomUUID } from "node:crypto";
2
6
  import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
3
7
  import { dirname, join, resolve } from "node:path";
@@ -29,10 +33,13 @@ export class FileNativeWorldNotFoundError extends Error {
29
33
  }
30
34
  }
31
35
  export class FileNativeWorldStore {
36
+ extensionRequests;
37
+ #documentMaintenanceCache = new Map();
32
38
  #worldsRoot;
33
39
  #operationsRoot;
34
40
  #promptCompiler;
35
41
  #activeControlUsers = new Map();
42
+ #openingWorldRevisions = new Set();
36
43
  operations;
37
44
  playTimeline;
38
45
  playAdvances;
@@ -42,6 +49,7 @@ export class FileNativeWorldStore {
42
49
  constructor(dataRoot, options = {}) {
43
50
  const root = resolve(dataRoot);
44
51
  this.#worldsRoot = join(root, "worlds-file-native");
52
+ this.extensionRequests = new WorldExtensionRequests(this.#worldsRoot);
45
53
  this.#operationsRoot = join(root, "operations");
46
54
  this.#promptCompiler =
47
55
  options.promptCompiler ?? new FileNativePromptCompiler();
@@ -154,15 +162,56 @@ export class FileNativeWorldStore {
154
162
  throw new FileNativeWorldNotFoundError({ cause: error });
155
163
  throw error;
156
164
  }
157
- return this.operations.withExclusiveWorldStateMutation(worldId, async () => {
165
+ return this.operations.withExclusiveWorldStateMutation(worldId, () => this.operations.withWorldAuthorityLock(worldId, async () => {
158
166
  await rm(root, { recursive: true, force: true });
159
167
  this.#currentStorage.delete(worldId);
160
168
  return { deleted: true };
161
- });
169
+ }));
170
+ }
171
+ async extensionControls(worldId, binding, locale, change) {
172
+ assertIdentity(worldId, "World ID");
173
+ const head = await this.currentHead(worldId);
174
+ const [control, playerViews] = await Promise.all([
175
+ this.readSurface(worldId, "control"),
176
+ this.renderPlayerViewsAtHead(worldId, head),
177
+ ]);
178
+ const definitions = extensionDefinitions(binding, control.map((file) => ({ ...file, path: `control/${file.path}` })), playerViews.views, locale);
179
+ return this.operations.withWorldAuthorityLock(worldId, () => FileNativeArtifactStore.withWorldControlsMutation(dirname(this.#worldsRoot), worldId, async () => {
180
+ // Definitions were loaded before taking the lock; deletion may have
181
+ // completed while this reader was waiting. Never recreate its shell.
182
+ await readPublicationAt(join(this.#worldsRoot, worldId));
183
+ const view = await WorldExtensionControls.resolve(join(this.#worldsRoot, worldId), definitions, binding.id, change);
184
+ await this.extensionRequests.changed(worldId);
185
+ return view;
186
+ }));
187
+ }
188
+ async extensionControlsRevision(worldId) {
189
+ assertIdentity(worldId, "World ID");
190
+ return (await WorldExtensionControls.read(join(this.#worldsRoot, worldId)))
191
+ .revision;
192
+ }
193
+ async readPackageScriptGrants(worldId) {
194
+ assertIdentity(worldId, "World ID");
195
+ await this.currentHead(worldId);
196
+ return PackageScriptPermissions.read(join(this.#worldsRoot, worldId));
197
+ }
198
+ async packageScriptPermissions(worldId, enabled) {
199
+ assertIdentity(worldId, "World ID");
200
+ const files = (await this.readSurface(worldId, "control")).map((file) => ({
201
+ ...file,
202
+ path: `control/${file.path}`,
203
+ }));
204
+ const root = join(this.#worldsRoot, worldId);
205
+ if (enabled !== undefined)
206
+ await PackageScriptPermissions.write(root, enabled ? PackageScriptPermissions.currentGrants(files) : []);
207
+ return PackageScriptPermissions.status(files, await PackageScriptPermissions.read(root));
162
208
  }
163
209
  async createFromContentPackage(input) {
164
210
  assertIdentity(input.operationId, "operation ID");
165
211
  assertIdentity(input.sourcePackageId, "Content-package local identity");
212
+ if (input.sourcePackageTitle !== input.sourcePackageTitle.trim() ||
213
+ !validWorldName(input.sourcePackageTitle))
214
+ throw new FileNativeWorldCreationError("content_package_title_invalid", "The source content package has no valid standalone title");
166
215
  const inspection = inspectContentPackageCurrentTree(input.packageFiles);
167
216
  if (inspection.status !== "usable") {
168
217
  throw new FileNativeWorldCreationError("content_package_needs_repair", `Only a content package that passes all validation can create a world: ${inspection.issues
@@ -191,10 +240,12 @@ export class FileNativeWorldStore {
191
240
  ...input,
192
241
  packageFiles,
193
242
  };
194
- const sourceFingerprint = fingerprint(packageFiles);
243
+ const legacySourceFingerprint = fingerprint(packageFiles);
244
+ const sourceFingerprint = contentPackageCreationFingerprint(input.sourcePackageTitle, packageFiles);
195
245
  const previous = await this.#readPublication(input.operationId);
196
246
  if (previous !== null) {
197
- if (previous.sourceFingerprint !== sourceFingerprint) {
247
+ if (previous.sourceFingerprint !== sourceFingerprint &&
248
+ previous.sourceFingerprint !== legacySourceFingerprint) {
198
249
  throw new FileNativeWorldCreationError("operation_conflict", "The same operation ID is bound to a different creation payload");
199
250
  }
200
251
  const preview = this.#preview(boundInput, previous.worldId);
@@ -221,6 +272,7 @@ export class FileNativeWorldStore {
221
272
  ];
222
273
  await writeSurface(stagingRoot, "state", state);
223
274
  await writeSurface(stagingRoot, "control", control);
275
+ await PackageScriptPermissions.write(stagingRoot, (input.packageScriptGrants ?? []).filter((grant) => PackageScriptPermissions.currentGrants(packageFiles).includes(grant)));
224
276
  await mkdir(join(stagingRoot, "history"), { recursive: true });
225
277
  await mkdir(join(stagingRoot, "runtime"), { recursive: true });
226
278
  await writeIdempotentText(join(stagingRoot, "history", historySurfaceFiles([openingMessage])[0].path), openingMessage.exactText);
@@ -257,7 +309,7 @@ export class FileNativeWorldStore {
257
309
  const publication = {
258
310
  schemaVersion: 1,
259
311
  worldId,
260
- title: inspection.displayName,
312
+ title: input.sourcePackageTitle,
261
313
  parentEndpoint: "genesis",
262
314
  operationId: input.operationId,
263
315
  sourceFingerprint,
@@ -275,7 +327,8 @@ export class FileNativeWorldStore {
275
327
  await rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
276
328
  const recovered = await this.#readPublication(input.operationId);
277
329
  if (recovered !== null) {
278
- if (recovered.sourceFingerprint === sourceFingerprint) {
330
+ if (recovered.sourceFingerprint === sourceFingerprint ||
331
+ recovered.sourceFingerprint === legacySourceFingerprint) {
279
332
  return {
280
333
  outcome: "created",
281
334
  world: await this.#currentSummary(recovered),
@@ -348,18 +401,117 @@ export class FileNativeWorldStore {
348
401
  }
349
402
  async saveControlDraft(worldId, files) {
350
403
  assertIdentity(worldId, "World ID");
351
- await this.ensureCurrentStorage(worldId);
352
- const root = join(this.#worldsRoot, worldId);
353
- await readPublicationAt(root);
354
404
  for (const file of files)
355
405
  assertRelativePath(file.path);
356
- const staging = join(root, `runtime/.control-draft-${randomUUID()}`);
357
- await writeSurface(staging, "control", files);
358
- const final = join(root, "runtime/control-draft");
359
- await rm(final, { recursive: true, force: true });
360
- await rename(join(staging, "control"), final);
361
- await rm(staging, { recursive: true, force: true });
362
- return { fingerprint: fingerprint(files) };
406
+ try {
407
+ return await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
408
+ await this.ensureCurrentStorage(worldId);
409
+ const root = join(this.#worldsRoot, worldId);
410
+ await readPublicationAt(root);
411
+ const staging = join(root, `runtime/.control-draft-${randomUUID()}`);
412
+ await writeSurface(staging, "control", files);
413
+ const final = join(root, "runtime/control-draft");
414
+ await rm(final, { recursive: true, force: true });
415
+ await rename(join(staging, "control"), final);
416
+ await rm(staging, { recursive: true, force: true });
417
+ return { fingerprint: fingerprint(files) };
418
+ });
419
+ }
420
+ catch (error) {
421
+ if (error instanceof WorldOperationBusyError)
422
+ throw new FileNativeWorldCreationError("operation_conflict", "World control drafts are locked while a world revision is open");
423
+ throw error;
424
+ }
425
+ }
426
+ async acquireWorldRevisionLock(worldId, epochId) {
427
+ if ((this.#activeControlUsers.get(worldId)?.size ?? 0) > 0 ||
428
+ this.#openingWorldRevisions.has(worldId))
429
+ return { kind: "busy" };
430
+ this.#openingWorldRevisions.add(worldId);
431
+ try {
432
+ return await this.operations.acquireWorldRevision(worldId, epochId);
433
+ }
434
+ finally {
435
+ this.#openingWorldRevisions.delete(worldId);
436
+ }
437
+ }
438
+ readWorldRevisionLock(worldId) {
439
+ return this.operations.readWorldRevisionLock(worldId);
440
+ }
441
+ releaseWorldRevisionLock(handle) {
442
+ return this.operations.releaseWorldRevision(handle);
443
+ }
444
+ async assertWorldRevisionUnlocked(worldId) {
445
+ try {
446
+ await this.operations.assertWorldRevisionUnlocked(worldId);
447
+ }
448
+ catch (error) {
449
+ if (error instanceof WorldOperationBusyError)
450
+ throw new FileNativeWorldCreationError("operation_conflict", "World play and other changes are locked while a world revision is open");
451
+ throw error;
452
+ }
453
+ }
454
+ async bindWorldRevision(handle) {
455
+ return this.operations.withWorldRevisionLock(handle, () => this.bindPlayCallChain(handle.worldId));
456
+ }
457
+ /** Replace control while the matching revision epoch retains exclusive ownership. */
458
+ async replaceWorldRevisionControl(input) {
459
+ for (const file of input.files)
460
+ assertRelativePath(file.path);
461
+ return this.operations.withWorldRevisionLock(input.handle, async () => {
462
+ if ((this.#activeControlUsers.get(input.handle.worldId)?.size ?? 0) > 0)
463
+ throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by another operation");
464
+ const root = join(this.#worldsRoot, input.handle.worldId);
465
+ const currentPath = join(root, "control");
466
+ const runtimeRoot = join(root, "runtime");
467
+ const stagingRoot = join(runtimeRoot, `.revision-control-${input.handle.epochId}-next`);
468
+ const stagedPath = join(stagingRoot, "control");
469
+ const previousPath = join(runtimeRoot, `.revision-control-${input.handle.epochId}-previous`);
470
+ let current = await readTree(currentPath);
471
+ let currentFingerprint = fingerprint(current);
472
+ const nextFingerprint = fingerprint(input.files);
473
+ if (currentFingerprint === nextFingerprint) {
474
+ await rm(stagingRoot, { recursive: true, force: true });
475
+ await rm(previousPath, { recursive: true, force: true });
476
+ await syncDirectory(runtimeRoot);
477
+ return { fingerprint: nextFingerprint };
478
+ }
479
+ const previous = await readTree(previousPath);
480
+ const staged = await readTree(stagedPath);
481
+ if (current.length === 0 &&
482
+ fingerprint(previous) === input.expectedFingerprint) {
483
+ if (fingerprint(staged) === nextFingerprint) {
484
+ await rename(stagedPath, currentPath);
485
+ await syncDirectory(root);
486
+ await syncDirectory(stagingRoot);
487
+ await rm(previousPath, { recursive: true, force: true });
488
+ await rm(stagingRoot, { recursive: true, force: true });
489
+ await syncDirectory(runtimeRoot);
490
+ return { fingerprint: nextFingerprint };
491
+ }
492
+ await rename(previousPath, currentPath);
493
+ await syncDirectory(root);
494
+ await syncDirectory(runtimeRoot);
495
+ current = await readTree(currentPath);
496
+ currentFingerprint = fingerprint(current);
497
+ }
498
+ if (currentFingerprint !== input.expectedFingerprint)
499
+ throw new FileNativeWorldCreationError("operation_conflict", "World control changed after this revision epoch opened");
500
+ await rm(stagingRoot, { recursive: true, force: true });
501
+ await rm(previousPath, { recursive: true, force: true });
502
+ await writeSurface(stagingRoot, "control", input.files);
503
+ await durableTree(stagingRoot);
504
+ await rename(currentPath, previousPath);
505
+ await syncDirectory(root);
506
+ await syncDirectory(runtimeRoot);
507
+ await rename(stagedPath, currentPath);
508
+ await syncDirectory(root);
509
+ await syncDirectory(stagingRoot);
510
+ await rm(previousPath, { recursive: true, force: true });
511
+ await rm(stagingRoot, { recursive: true, force: true });
512
+ await syncDirectory(runtimeRoot);
513
+ return { fingerprint: nextFingerprint };
514
+ });
363
515
  }
364
516
  async previewControlDraft(worldId, prompt) {
365
517
  const { state, draft } = await this.#readControlDraft(worldId);
@@ -429,6 +581,8 @@ export class FileNativeWorldStore {
429
581
  freezeControl(worldId, operationId) {
430
582
  assertIdentity(worldId, "World ID");
431
583
  assertIdentity(operationId, "operation ID");
584
+ if (this.#openingWorldRevisions.has(worldId))
585
+ throw new FileNativeWorldCreationError("operation_conflict", "World control is locked while a world revision opens");
432
586
  const users = this.#activeControlUsers.get(worldId) ?? new Set();
433
587
  users.add(operationId);
434
588
  this.#activeControlUsers.set(worldId, users);
@@ -518,6 +672,7 @@ export class FileNativeWorldStore {
518
672
  }, {}),
519
673
  additionalMaterials: materials?.items ?? [],
520
674
  history: materializedHistoryRecord(history),
675
+ narrativeCheckpoint: await authorityStore.readNarrativeCheckpoint(),
521
676
  };
522
677
  });
523
678
  }
@@ -527,6 +682,32 @@ export class FileNativeWorldStore {
527
682
  throw error;
528
683
  }
529
684
  }
685
+ /**
686
+ * Bind an immutable historical endpoint with the world's current control
687
+ * surface. This is the same semantic input as a fresh context started while
688
+ * that endpoint was current, without rematerializing it over today's head.
689
+ */
690
+ async bindPlayCallChainAt(worldId, head) {
691
+ assertIdentity(worldId, "World ID");
692
+ const [endpoint, control] = await Promise.all([
693
+ this.recoverEndpoint(worldId, head),
694
+ this.readSurface(worldId, "control"),
695
+ ]);
696
+ return {
697
+ worldId,
698
+ parentHead: endpoint.head,
699
+ files: [...endpoint.state, ...control].reduce((files, { path, contents }, index) => {
700
+ files[`${index < endpoint.state.length ? "state" : "control"}/${path}`] = contents;
701
+ return files;
702
+ }, {}),
703
+ additionalMaterials: structuredClone(endpoint.additionalMaterials),
704
+ narrativeCheckpoint: endpoint.narrativeCheckpoint,
705
+ history: Object.fromEntries(endpoint.history.map(({ messageId, exactText }) => [
706
+ messageId,
707
+ exactText,
708
+ ])),
709
+ };
710
+ }
530
711
  async reserveOperation(operationId) {
531
712
  assertIdentity(operationId, "operation ID");
532
713
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
@@ -569,6 +750,7 @@ export class FileNativeWorldStore {
569
750
  state: recovered.state,
570
751
  history: recovered.history,
571
752
  additionalMaterials: recovered.additionalMaterials,
753
+ narrativeCheckpoint: recovered.narrativeCheckpoint,
572
754
  };
573
755
  }
574
756
  catch (error) {
@@ -654,6 +836,8 @@ export class FileNativeWorldStore {
654
836
  const selected = await new FileNativeAuthorityV3(staging).recoverHeadResult();
655
837
  await writeSurface(staging, "state", selected.state);
656
838
  await writeSurface(staging, "control", control);
839
+ await PackageScriptPermissions.write(staging, await PackageScriptPermissions.read(sourceRoot));
840
+ await WorldExtensionControls.copy(sourceRoot, staging);
657
841
  await mkdir(join(staging, "history"), { recursive: true });
658
842
  for (const file of historySurfaceFiles(selected.history))
659
843
  await writeIdempotentText(join(staging, "history", file.path), file.contents);
@@ -809,6 +993,16 @@ export class FileNativeWorldStore {
809
993
  if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
810
994
  throw new TypeError("Timeline-revision request fingerprint is invalid");
811
995
  await this.ensureCurrentStorage(input.worldId);
996
+ try {
997
+ return await this.operations.withExclusiveWorldStateMutation(input.worldId, () => this.#reviseTimelineUnlocked(input));
998
+ }
999
+ catch (error) {
1000
+ if (error instanceof WorldOperationBusyError)
1001
+ throw new FileNativeWorldCreationError("operation_conflict", "Timeline editing is locked while a world revision is open");
1002
+ throw error;
1003
+ }
1004
+ }
1005
+ async #reviseTimelineUnlocked(input) {
812
1006
  const existing = await this.getOperationOutcome(input.operationId);
813
1007
  if (isCommittedOutcome(existing)) {
814
1008
  const fact = await new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId)).authorityFactAt(existing.head);
@@ -864,6 +1058,9 @@ export class FileNativeWorldStore {
864
1058
  operationReserved = await this.reserveOperation(input.operationId);
865
1059
  }
866
1060
  return this.#commitHistoryChange({
1061
+ ...(input.worldClock === undefined
1062
+ ? {}
1063
+ : { worldClock: input.worldClock }),
867
1064
  operationId: input.operationId,
868
1065
  worldId: input.worldId,
869
1066
  parentHead: input.parentHead,
@@ -872,11 +1069,19 @@ export class FileNativeWorldStore {
872
1069
  mode: "correction",
873
1070
  historyAppend: [],
874
1071
  operationReserved,
1072
+ ...(input.validationControl === undefined
1073
+ ? {}
1074
+ : { validationControl: input.validationControl }),
875
1075
  });
876
1076
  };
877
- return input.stateOperationClaim === undefined
878
- ? await this.operations.withExclusiveWorldStateMutation(input.worldId, commit)
879
- : await this.operations.withCorrectionWorldClaim(input.stateOperationClaim, commit);
1077
+ if (input.stateOperationClaim !== undefined &&
1078
+ input.worldRevisionLock !== undefined)
1079
+ throw new TypeError("A correction cannot use two world-operation claims");
1080
+ if (input.worldRevisionLock !== undefined)
1081
+ return await this.operations.withWorldRevisionLock(input.worldRevisionLock, commit);
1082
+ if (input.stateOperationClaim !== undefined)
1083
+ return await this.operations.withCorrectionWorldClaim(input.stateOperationClaim, commit);
1084
+ return await this.operations.withExclusiveWorldStateMutation(input.worldId, commit);
880
1085
  }
881
1086
  catch (error) {
882
1087
  if (error instanceof WorldOperationBusyError)
@@ -893,11 +1098,18 @@ export class FileNativeWorldStore {
893
1098
  await this.ensureCurrentStorage(input.worldId);
894
1099
  if (input.historyAppend.length === 0 && input.stateChanges.length === 0)
895
1100
  throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
896
- return this.#commitHistoryChange({
897
- ...input,
898
- nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
899
- mode: "play",
900
- });
1101
+ try {
1102
+ return await this.operations.withExclusiveWorldStateMutation(input.worldId, () => this.#commitHistoryChange({
1103
+ ...input,
1104
+ nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
1105
+ mode: "play",
1106
+ }));
1107
+ }
1108
+ catch (error) {
1109
+ if (error instanceof WorldOperationBusyError)
1110
+ throw new FileNativeWorldCreationError("operation_conflict", "Play is locked while a world revision is open");
1111
+ throw error;
1112
+ }
901
1113
  }
902
1114
  async #commitHistoryChange(input) {
903
1115
  const existing = await this.getOperationOutcome(input.operationId);
@@ -955,7 +1167,7 @@ export class FileNativeWorldStore {
955
1167
  const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
956
1168
  if ((materialized?.head ?? "genesis") !== authority.head)
957
1169
  throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
958
- const [basisState, currentControl] = await Promise.all([
1170
+ const [basisState, storedControl] = await Promise.all([
959
1171
  authorityStore.recoverState(input.timelineRevision?.restoresHead ?? authority.head),
960
1172
  readTree(join(root, "control")),
961
1173
  ]);
@@ -983,7 +1195,7 @@ export class FileNativeWorldStore {
983
1195
  path: `world/${path}`,
984
1196
  contents,
985
1197
  })),
986
- ...currentControl.map(({ path, contents }) => ({
1198
+ ...(input.validationControl ?? storedControl).map(({ path, contents }) => ({
987
1199
  path: `control/${path}`,
988
1200
  contents,
989
1201
  })),
@@ -994,6 +1206,12 @@ export class FileNativeWorldStore {
994
1206
  await authorityStore.discardUnacceptedNextEpoch();
995
1207
  crashAtFileNativeAuthorityEdge("before_commit_acceptance");
996
1208
  const preparedAppend = await authorityStore.prepareAppend({
1209
+ ...(input.playContext === undefined
1210
+ ? {}
1211
+ : { playContext: input.playContext }),
1212
+ ...(input.worldClock === undefined
1213
+ ? {}
1214
+ : { worldClock: input.worldClock }),
997
1215
  operationId: input.operationId,
998
1216
  parentHead: input.parentHead,
999
1217
  ...(input.timelineRevision === undefined
@@ -1003,6 +1221,9 @@ export class FileNativeWorldStore {
1003
1221
  historyAppend: input.historyAppend,
1004
1222
  stateChanges: structuredClone(input.stateChanges),
1005
1223
  nextMaterials: input.nextMaterials,
1224
+ ...(input.narrativeCheckpoint === undefined
1225
+ ? {}
1226
+ : { narrativeCheckpoint: input.narrativeCheckpoint }),
1006
1227
  ...(input.mode === "correction"
1007
1228
  ? {
1008
1229
  correctionTargets: input.stateChanges.map(({ documentId }) => documentId),
@@ -1071,6 +1292,37 @@ export class FileNativeWorldStore {
1071
1292
  const authority = await new FileNativeAuthorityV3(root).readHistory();
1072
1293
  return structuredClone(authority);
1073
1294
  }
1295
+ async inspectDocumentMaintenance(worldId, head) {
1296
+ try {
1297
+ return {
1298
+ documentMaintenance: await this.readDocumentMaintenance(worldId, head),
1299
+ };
1300
+ }
1301
+ catch (error) {
1302
+ return {
1303
+ documentMaintenance: {},
1304
+ documentMaintenanceUnavailableReason: error instanceof Error
1305
+ ? error.message
1306
+ : "Document diagnostics unavailable",
1307
+ };
1308
+ }
1309
+ }
1310
+ async readDocumentMaintenance(worldId, head) {
1311
+ assertIdentity(worldId, "World ID");
1312
+ await this.ensureCurrentStorage(worldId);
1313
+ const root = join(this.#worldsRoot, worldId);
1314
+ await readPublicationAt(root);
1315
+ const authority = new FileNativeAuthorityV3(root);
1316
+ const selectedHead = head ?? (await authority.readHead()).head;
1317
+ let cache = this.#documentMaintenanceCache.get(worldId);
1318
+ if (cache === undefined) {
1319
+ cache = new Map();
1320
+ if (this.#documentMaintenanceCache.size >= 8)
1321
+ this.#documentMaintenanceCache.delete(this.#documentMaintenanceCache.keys().next().value);
1322
+ this.#documentMaintenanceCache.set(worldId, cache);
1323
+ }
1324
+ return readDocumentMaintenanceFacts(authority, selectedHead, cache);
1325
+ }
1074
1326
  async readAuthorityEndpoint(worldId, endpoint) {
1075
1327
  const recovered = await this.recoverEndpoint(worldId, endpoint);
1076
1328
  return {
@@ -1206,7 +1458,6 @@ function assertEquivalentWorldDocumentSnapshots(contentDocuments, worldState) {
1206
1458
  document: {
1207
1459
  logicalPath: `state/${document.path.slice("world/".length)}`,
1208
1460
  },
1209
- maxBytes: 4,
1210
1461
  });
1211
1462
  return (!result.ok ||
1212
1463
  result.kind !== "read_document" ||
@@ -1265,6 +1516,16 @@ function fingerprint(files) {
1265
1516
  }
1266
1517
  return `sha256:${hash.digest("hex")}`;
1267
1518
  }
1519
+ function contentPackageCreationFingerprint(title, files) {
1520
+ const hash = createHash("sha256");
1521
+ hash
1522
+ .update("narraeon.file-native-world-creation/v2")
1523
+ .update("\0")
1524
+ .update(title)
1525
+ .update("\0")
1526
+ .update(fingerprint(files));
1527
+ return `sha256:${hash.digest("hex")}`;
1528
+ }
1268
1529
  function operationDigest(operationId) {
1269
1530
  return createHash("sha256").update(operationId).digest("hex");
1270
1531
  }
@@ -1634,6 +1895,8 @@ function authorityCommitMatchesInput(commit, input) {
1634
1895
  ? input.stateChanges.map(({ documentId }) => documentId)
1635
1896
  : undefined;
1636
1897
  return (commit.mode === input.mode &&
1898
+ commit.playContext === input.playContext &&
1899
+ commit.worldClock === input.worldClock &&
1637
1900
  commit.auditParent.head === input.parentHead &&
1638
1901
  commit.timelineParent.head ===
1639
1902
  (input.timelineRevision?.restoresHead ?? input.parentHead) &&
@@ -1641,6 +1904,7 @@ function authorityCommitMatchesInput(commit, input) {
1641
1904
  stateChangesMatch &&
1642
1905
  isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
1643
1906
  isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
1907
+ isDeepStrictEqual(commit.narrativeCheckpoint, input.narrativeCheckpoint) &&
1644
1908
  isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
1645
1909
  commit.corrects ===
1646
1910
  (input.mode === "correction" ? input.parentHead : undefined));
@@ -1857,3 +2121,4 @@ function crashAtFileNativeAuthorityEdge(edge) {
1857
2121
  process.kill(process.pid, "SIGKILL");
1858
2122
  }
1859
2123
  }
2124
+ import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
@@ -0,0 +1,30 @@
1
+ import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
2
+ /** An annotation about UI reads, never document content or write permission. */
3
+ export function renderPlayerViewBindings(snapshot, shortRef, locale = "en") {
4
+ const control = snapshot.files.find(({ path }) => path === "control/player-views.yaml")?.contents;
5
+ if (control === undefined)
6
+ return "";
7
+ const { bindings } = new PlayerViewRenderer().inspect({ snapshot, control });
8
+ const selected = bindings.filter((binding) => shortRef === undefined || binding.shortRef === shortRef);
9
+ if (selected.length === 0)
10
+ return "";
11
+ const zh = locale === "zh-CN";
12
+ return [
13
+ zh
14
+ ? "## 玩家视图读取绑定(Runtime 标记,非正文)"
15
+ : "## Player-view read bindings (Runtime annotation, outside document bodies)",
16
+ zh
17
+ ? "以下范围直接用于玩家界面;这里只标明读取绑定,不授予读取或写入权限,也不展开引用目标。"
18
+ : "These scopes feed the player interface. This annotation grants no read or write permission and does not expand reference targets.",
19
+ ...selected.map(({ viewTitle, label, shortRef: ref, locator }) => {
20
+ const scope = locator === null
21
+ ? zh
22
+ ? "(整份文档)"
23
+ : " (whole document)"
24
+ : "yaml" in locator
25
+ ? `#yaml:${JSON.stringify(locator.yaml)}`
26
+ : `#markdown:${JSON.stringify(locator.markdown)}`;
27
+ return `- ${viewTitle} · ${label} → @${ref}${scope}`;
28
+ }),
29
+ ].join("\n");
30
+ }
@@ -2,16 +2,22 @@ import { parseDocument } from "yaml";
2
2
  const maxItemsPerView = 128;
3
3
  const maxRenderedBytesPerView = 64 * 1024;
4
4
  const maxRenderedDepth = 16;
5
- const maxDocumentReadBytes = 65_536;
6
5
  export class PlayerViewRenderer {
7
6
  render(input) {
7
+ const { views, diagnostics } = this.inspect(input);
8
+ return { views, diagnostics };
9
+ }
10
+ /** Binding facts come from the exact successful UI selections. */
11
+ inspect(input) {
8
12
  const diagnostics = [];
13
+ const bindings = [];
9
14
  const control = parseControlYaml(input.control);
10
15
  if (!isRecord(control) ||
11
16
  control.format !== "narraeon.player-views/v1" ||
12
17
  !Array.isArray(control.views)) {
13
18
  return {
14
19
  views: [],
20
+ bindings,
15
21
  diagnostics: [
16
22
  {
17
23
  code: "invalid_control",
@@ -73,10 +79,18 @@ export class PlayerViewRenderer {
73
79
  }
74
80
  renderedBytes += itemBytes;
75
81
  view.items.push(resolved.item);
82
+ bindings.push({
83
+ viewId: view.id,
84
+ viewTitle: view.title,
85
+ itemId: resolved.item.id,
86
+ label: resolved.item.label,
87
+ shortRef: resolved.shortRef,
88
+ locator: resolved.locator,
89
+ });
76
90
  }
77
91
  views.push(view);
78
92
  }
79
- return { views, diagnostics };
93
+ return { views, diagnostics, bindings };
80
94
  }
81
95
  }
82
96
  function resolveItem(item, snapshot) {
@@ -99,6 +113,8 @@ function resolveItem(item, snapshot) {
99
113
  return wholeDocument.ok
100
114
  ? {
101
115
  ok: true,
116
+ shortRef: wholeDocument.shortRef,
117
+ locator: null,
102
118
  item: {
103
119
  id: itemId,
104
120
  label: item.label,
@@ -131,6 +147,8 @@ function resolveItem(item, snapshot) {
131
147
  };
132
148
  return {
133
149
  ok: true,
150
+ shortRef: selected.scope.document.shortRef,
151
+ locator: selected.node.locator,
134
152
  item: {
135
153
  id: itemId,
136
154
  label: item.label,
@@ -152,7 +170,6 @@ function resolveWholeDocument(snapshot, selector, handle, expectedCodec) {
152
170
  const read = snapshot.query({
153
171
  kind: "read_document",
154
172
  document: selector,
155
- maxBytes: maxDocumentReadBytes,
156
173
  });
157
174
  if (read.kind === "error")
158
175
  return selectionFailure(read, handle);
@@ -169,37 +186,13 @@ function resolveWholeDocument(snapshot, selector, handle, expectedCodec) {
169
186
  message: "The selector codec does not match the target document",
170
187
  };
171
188
  if (read.codec === "markdown")
172
- return readWholeMarkdown(snapshot, selector, handle, read);
173
- return selectYamlRoot(snapshot, selector, handle);
174
- }
175
- function readWholeMarkdown(snapshot, selector, handle, firstPage) {
176
- let page = firstPage;
177
- const chunks = [page.body];
178
- while (!page.page.complete) {
179
- if (page.page.nextCursor === null)
180
- return {
181
- ok: false,
182
- code: "unresolved_selector",
183
- message: "The exact selector cannot currently be resolved",
184
- };
185
- const nextPage = snapshot.query({
186
- kind: "read_document",
187
- document: selector,
188
- maxBytes: maxDocumentReadBytes,
189
- cursor: page.page.nextCursor,
190
- });
191
- if (nextPage.kind === "error")
192
- return selectionFailure(nextPage, handle);
193
- if (nextPage.kind !== "read_document" || nextPage.codec !== "markdown")
194
- return {
195
- ok: false,
196
- code: "unresolved_selector",
197
- message: "The exact selector cannot currently be resolved",
198
- };
199
- chunks.push(nextPage.body);
200
- page = nextPage;
201
- }
202
- return { ok: true, value: chunks.join("").trim() };
189
+ return {
190
+ ok: true,
191
+ value: read.body.trim(),
192
+ shortRef: read.document.shortRef,
193
+ };
194
+ const root = selectYamlRoot(snapshot, selector, handle);
195
+ return root.ok ? { ...root, shortRef: read.document.shortRef } : root;
203
196
  }
204
197
  function selectYamlRoot(snapshot, selector, handle) {
205
198
  const selected = snapshot.query({