narraeon 0.3.1 → 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 +188 -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 +447 -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 +44 -10
  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-D5VqDvjY.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -3,11 +3,9 @@ import { isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
3
3
  const documentIdPattern = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u;
4
4
  const shortRefPattern = /^[a-z][a-z0-9-]*$/u;
5
5
  const base64UrlPattern = /^[A-Za-z0-9_-]+$/u;
6
- const maxWorldDocumentBytes = 4 * 1024 * 1024;
7
6
  const maxYamlDepth = 64;
8
7
  const maxYamlNodes = 100_000;
9
8
  const maxLiteralSearchMatches = 10_000;
10
- const maxSelectedNodeBytes = 1024 * 1024;
11
9
  export class WorldDocumentStore {
12
10
  static open(input) {
13
11
  return new WorldDocumentStore(input.layout, input.files);
@@ -276,6 +274,7 @@ export class WorldDocumentStore {
276
274
  title: source.metadata.title,
277
275
  summary: source.metadata.summary,
278
276
  aliases: [...source.metadata.aliases],
277
+ ...(previous?.retired === true ? { retired: true } : {}),
279
278
  },
280
279
  };
281
280
  let contents;
@@ -314,6 +313,23 @@ export class WorldDocumentStore {
314
313
  }
315
314
  touchedPath = writePath;
316
315
  }
316
+ else if (command.kind === "retire") {
317
+ if (this.layout !== "world_state" ||
318
+ !hasOnlyKeys(command, ["kind", "document", "retired"]) ||
319
+ typeof command.retired !== "boolean")
320
+ return this.#revisionFailure(commandIndex, {
321
+ code: "query_invalid",
322
+ message: "A retire command requires a world-state target and boolean retired value",
323
+ });
324
+ const target = this.#resolveRevisionTarget(current, working, command.document);
325
+ if ("diagnostics" in target)
326
+ return this.#revisionFailure(commandIndex, target.diagnostics);
327
+ if (target.entry.descriptor.retired !== command.retired)
328
+ target.working.file.contents = setDocumentRetired(target.working.file.contents, target.entry.descriptor.codec, command.retired);
329
+ delete target.working.file.encoding;
330
+ target.working.lastCommandIndex = commandIndex;
331
+ touchedPath = target.working.file.path;
332
+ }
317
333
  else if (command.kind === "patch") {
318
334
  if (!hasOnlyKeys(command, ["kind", "document", "edits"]) ||
319
335
  !Array.isArray(command.edits) ||
@@ -327,7 +343,7 @@ export class WorldDocumentStore {
327
343
  if ("diagnostics" in target)
328
344
  return this.#revisionFailure(commandIndex, target.diagnostics);
329
345
  if (target.entry.codec === "markdown") {
330
- const patched = patchRevisionMarkdown(target.working.file.contents, command.edits);
346
+ const patched = patchRevisionMarkdown(target.working.file.contents, command.edits, target.entry.descriptor);
331
347
  if ("diagnostics" in patched)
332
348
  return this.#revisionFailure(commandIndex, patched.diagnostics.map((problem) => ({
333
349
  ...problem,
@@ -342,6 +358,11 @@ export class WorldDocumentStore {
342
358
  uniqueKeys: true,
343
359
  strict: true,
344
360
  });
361
+ let metadata = {
362
+ title: target.entry.descriptor.title,
363
+ summary: target.entry.descriptor.summary,
364
+ aliases: [...target.entry.descriptor.aliases],
365
+ };
345
366
  for (const edit of command.edits) {
346
367
  if (!isRecord(edit) || typeof edit.op !== "string")
347
368
  return this.#revisionFailure(commandIndex, {
@@ -358,16 +379,18 @@ export class WorldDocumentStore {
358
379
  documentId: target.entry.descriptor.documentId,
359
380
  message: "set_metadata accepts only op, title, summary, and aliases",
360
381
  });
361
- if (!validDocumentMetadata(edit))
382
+ const resolved = resolveDocumentMetadataRevision(metadata, edit);
383
+ if ("problems" in resolved)
362
384
  return this.#revisionFailure(commandIndex, {
363
385
  code: "document_header_invalid",
364
386
  logicalPath: target.entry.file.path,
365
387
  documentId: target.entry.descriptor.documentId,
366
- message: `set_metadata arguments are invalid: ${documentMetadataProblems(edit).join("; ")}`,
388
+ message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
367
389
  });
368
- document.setIn(["$document", "title"], edit.title);
369
- document.setIn(["$document", "summary"], edit.summary);
370
- document.setIn(["$document", "aliases"], [...edit.aliases]);
390
+ metadata = resolved.metadata;
391
+ document.setIn(["$document", "title"], metadata.title);
392
+ document.setIn(["$document", "summary"], metadata.summary);
393
+ document.setIn(["$document", "aliases"], [...metadata.aliases]);
371
394
  continue;
372
395
  }
373
396
  if (!["add", "replace", "append", "remove"].includes(edit.op) ||
@@ -820,7 +843,14 @@ export class WorldDocumentStore {
820
843
  ]);
821
844
  }
822
845
  #catalog(request) {
823
- if (!hasOnlyKeys(request, ["kind", "directory", "limit", "cursor"]))
846
+ if (!hasOnlyKeys(request, [
847
+ "kind",
848
+ "directory",
849
+ "includeRetired",
850
+ "declaredDirectories",
851
+ "limit",
852
+ "cursor",
853
+ ]))
824
854
  return this.#failure("catalog", request, [
825
855
  diagnostic({
826
856
  code: "query_invalid",
@@ -828,20 +858,28 @@ export class WorldDocumentStore {
828
858
  }),
829
859
  ]);
830
860
  const directory = request.directory ?? "";
861
+ const includeRetired = request.includeRetired ?? true;
862
+ const declaredDirectories = request.declaredDirectories ?? [];
831
863
  const limit = request.limit ?? 20;
832
864
  if ((directory !== "" && !validRelativePath(directory)) ||
865
+ typeof includeRetired !== "boolean" ||
866
+ !validDeclaredDirectories(declaredDirectories) ||
833
867
  !Number.isInteger(limit) ||
834
868
  limit < 1 ||
835
869
  limit > 100)
836
870
  return this.#failure("catalog", request, [
837
871
  diagnostic({
838
872
  code: "query_invalid",
839
- message: "Catalog directory must be within the layout root, and limit must be 1 to 100",
873
+ message: "Catalog directories must be within the layout root, and limit must be 1 to 100",
840
874
  }),
841
875
  ]);
876
+ const normalizedDeclaredDirectories = [
877
+ ...new Set(declaredDirectories),
878
+ ].sort((left, right) => left.localeCompare(right));
842
879
  const relativePrefix = directory === "" ? "" : `${directory}/`;
843
880
  const prefix = `${this.logicalRoot}/${relativePrefix}`;
844
881
  const entries = new Map();
882
+ let excludedDocuments = 0;
845
883
  for (const entry of this.#entries) {
846
884
  if (!entry.file.path.startsWith(prefix))
847
885
  continue;
@@ -855,6 +893,10 @@ export class WorldDocumentStore {
855
893
  }));
856
894
  }
857
895
  else {
896
+ if (entry.descriptor?.retired === true && !includeRetired) {
897
+ excludedDocuments += 1;
898
+ continue;
899
+ }
858
900
  entries.set(`document:${relative}`, freeze({
859
901
  kind: "document",
860
902
  logicalPath: entry.file.path,
@@ -868,12 +910,27 @@ export class WorldDocumentStore {
868
910
  }));
869
911
  }
870
912
  }
913
+ for (const declaredDirectory of normalizedDeclaredDirectories) {
914
+ if (!declaredDirectory.startsWith(relativePrefix))
915
+ continue;
916
+ const relative = declaredDirectory.slice(relativePrefix.length);
917
+ if (relative === "")
918
+ continue;
919
+ const slash = relative.indexOf("/");
920
+ const child = slash < 0 ? relative : relative.slice(0, slash);
921
+ entries.set(`directory:${child}`, freeze({
922
+ kind: "directory",
923
+ logicalPath: `${prefix}${child}`,
924
+ }));
925
+ }
871
926
  const ordered = [...entries.values()].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
872
927
  const cursorScope = JSON.stringify({
873
928
  kind: "catalog",
874
929
  layout: this.layout,
875
930
  logicalRoot: this.logicalRoot,
876
931
  directory,
932
+ declaredDirectories: normalizedDeclaredDirectories,
933
+ includeRetired,
877
934
  limit,
878
935
  });
879
936
  const offset = this.#cursorOffset(request.cursor, cursorScope);
@@ -894,7 +951,7 @@ export class WorldDocumentStore {
894
951
  }),
895
952
  coverage: freeze({
896
953
  status: "complete",
897
- excludedDocuments: 0,
954
+ excludedDocuments,
898
955
  }),
899
956
  page: freeze({
900
957
  unit: "entries",
@@ -1081,19 +1138,19 @@ export class WorldDocumentStore {
1081
1138
  });
1082
1139
  }
1083
1140
  #readDocument(request) {
1084
- if (!hasOnlyKeys(request, ["kind", "document", "maxBytes", "cursor"]))
1141
+ if (!hasOnlyKeys(request, ["kind", "document", "referenceProjection"]))
1085
1142
  return this.#failure("read_document", request, [
1086
1143
  diagnostic({
1087
1144
  code: "query_invalid",
1088
1145
  message: "read_document contains undeclared query conditions",
1089
1146
  }),
1090
1147
  ]);
1091
- const maxBytes = request.maxBytes ?? 8192;
1092
- if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > 65_536)
1148
+ if (request.referenceProjection !== undefined &&
1149
+ request.referenceProjection !== "short_ref")
1093
1150
  return this.#failure("read_document", request, [
1094
1151
  diagnostic({
1095
1152
  code: "query_invalid",
1096
- message: "read_document maxBytes must be 4 to 65536",
1153
+ message: "read_document referenceProjection, when present, must be short_ref",
1097
1154
  }),
1098
1155
  ]);
1099
1156
  const resolved = this.#resolveSelector(request.document);
@@ -1109,21 +1166,9 @@ export class WorldDocumentStore {
1109
1166
  message: "World document has no readable body range",
1110
1167
  }),
1111
1168
  ], resolved.entry.descriptor);
1112
- const completeBody = resolved.entry.file.contents.slice(bodyRange.start.offset, bodyRange.end.offset);
1113
- const bytes = Buffer.from(completeBody, "utf8");
1114
- const cursorScope = JSON.stringify({
1115
- kind: "read_document",
1116
- layout: this.layout,
1117
- logicalRoot: this.logicalRoot,
1118
- document: request.document,
1119
- maxBytes,
1120
- });
1121
- const offset = this.#cursorOffset(request.cursor, cursorScope);
1122
- if (offset === null || offset > bytes.length)
1123
- return this.#cursorFailure("read_document", request);
1124
- const end = safeUtf8PageEnd(bytes, offset, maxBytes);
1125
- const body = bytes.subarray(offset, end).toString("utf8");
1126
- const complete = end === bytes.length;
1169
+ const completeBody = request.referenceProjection === "short_ref"
1170
+ ? projectExplicitReferenceValues(resolved.entry.file.contents, bodyRange, resolved.entry.references, this.#descriptorIndex(), true)
1171
+ : resolved.entry.file.contents.slice(bodyRange.start.offset, bodyRange.end.offset);
1127
1172
  return freeze({
1128
1173
  kind: "read_document",
1129
1174
  ok: true,
@@ -1140,15 +1185,7 @@ export class WorldDocumentStore {
1140
1185
  }),
1141
1186
  document: resolved.entry.descriptor,
1142
1187
  codec: resolved.entry.codec,
1143
- body,
1144
- page: freeze({
1145
- unit: "utf8_bytes",
1146
- start: offset,
1147
- end,
1148
- total: bytes.length,
1149
- complete,
1150
- nextCursor: complete ? null : this.#cursor(cursorScope, end),
1151
- }),
1188
+ body: completeBody,
1152
1189
  diagnostics: freeze([]),
1153
1190
  });
1154
1191
  }
@@ -1187,20 +1224,6 @@ export class WorldDocumentStore {
1187
1224
  if (value === unresolved || node === undefined)
1188
1225
  return this.#locatorFailure(entry, publicLocator, "locator_not_found");
1189
1226
  const projected = projectYamlValue(value, this.#descriptorIndex());
1190
- const isWholeDocumentProjection = locator.yaml.length === 0;
1191
- if (!isWholeDocumentProjection &&
1192
- Buffer.byteLength(JSON.stringify(projected), "utf8") >
1193
- maxSelectedNodeBytes)
1194
- return this.#failure("select_node", request, [
1195
- diagnostic({
1196
- code: "capacity_exceeded",
1197
- logicalPath: entry.file.path,
1198
- documentId: entry.descriptor.documentId,
1199
- locator: publicLocator,
1200
- range: node.range,
1201
- message: "Exact-node projection exceeds the capacity of one query",
1202
- }),
1203
- ], entry.descriptor);
1204
1227
  return this.#selected(entry, publicLocator, {
1205
1228
  codec: "yaml",
1206
1229
  locator: publicLocator,
@@ -1222,17 +1245,6 @@ export class WorldDocumentStore {
1222
1245
  return this.#locatorFailure(entry, publicLocator, "locator_ambiguous");
1223
1246
  const heading = headings[0];
1224
1247
  const markdown = entry.file.contents.slice(heading.range.start.offset, heading.range.end.offset);
1225
- if (Buffer.byteLength(markdown, "utf8") > maxSelectedNodeBytes)
1226
- return this.#failure("select_node", request, [
1227
- diagnostic({
1228
- code: "capacity_exceeded",
1229
- logicalPath: entry.file.path,
1230
- documentId: entry.descriptor.documentId,
1231
- locator: publicLocator,
1232
- range: heading.range,
1233
- message: "Exact-node projection exceeds the capacity of one query",
1234
- }),
1235
- ], entry.descriptor);
1236
1248
  return this.#selected(entry, publicLocator, {
1237
1249
  codec: "markdown",
1238
1250
  locator: publicLocator,
@@ -1475,15 +1487,6 @@ export class WorldDocumentStore {
1475
1487
  }));
1476
1488
  return entry;
1477
1489
  }
1478
- if (Buffer.byteLength(file.contents, "utf8") > maxWorldDocumentBytes) {
1479
- entry.diagnostics.push(diagnostic({
1480
- code: "capacity_exceeded",
1481
- logicalPath: file.path,
1482
- range: sourceRange(file.contents, 0, file.contents.length),
1483
- message: "One world document cannot exceed 4 MiB",
1484
- }));
1485
- return entry;
1486
- }
1487
1490
  if (entry.codec === "yaml")
1488
1491
  inspectYamlDocument(entry);
1489
1492
  else
@@ -1854,8 +1857,10 @@ function readHeader(entry, parsed, headerOnly) {
1854
1857
  }
1855
1858
  const header = first.value;
1856
1859
  const pairs = stringPairMap(header);
1857
- const allowed = new Set(["id", "ref", "title", "summary", "aliases"]);
1858
- const exactShape = pairs.size === allowed.size &&
1860
+ const required = new Set(["id", "ref", "title", "summary", "aliases"]);
1861
+ const allowed = new Set([...required, "retired"]);
1862
+ const exactShape = (pairs.size === required.size || pairs.size === allowed.size) &&
1863
+ [...required].every((key) => pairs.has(key)) &&
1859
1864
  [...pairs.keys()].every((key) => allowed.has(key)) &&
1860
1865
  (!headerOnly || parsed.root.items.length === 1);
1861
1866
  const idPair = pairs.get("id");
@@ -1863,12 +1868,14 @@ function readHeader(entry, parsed, headerOnly) {
1863
1868
  const titlePair = pairs.get("title");
1864
1869
  const summaryPair = pairs.get("summary");
1865
1870
  const aliasesPair = pairs.get("aliases");
1871
+ const retiredPair = pairs.get("retired");
1866
1872
  const id = scalarString(idPair?.value);
1867
1873
  const shortRef = scalarString(refPair?.value);
1868
1874
  const title = scalarString(titlePair?.value);
1869
1875
  const summary = scalarString(summaryPair?.value);
1870
1876
  const aliases = stringSequence(aliasesPair?.value);
1871
- let valid = exactShape;
1877
+ const retired = retiredPair === undefined ? false : scalarBoolean(retiredPair.value);
1878
+ let valid = exactShape && retired !== null;
1872
1879
  if (id === null ||
1873
1880
  id.length < 3 ||
1874
1881
  id.length > 128 ||
@@ -1919,7 +1926,7 @@ function readHeader(entry, parsed, headerOnly) {
1919
1926
  ? { documentId: id }
1920
1927
  : {}),
1921
1928
  range: nodeRange(entry.file.contents, header, parsed.baseOffset),
1922
- message: "$document must contain only valid id, ref, title, summary, and aliases",
1929
+ message: "$document must contain valid id, ref, title, summary, aliases, and an optional boolean retired flag",
1923
1930
  }));
1924
1931
  return null;
1925
1932
  }
@@ -1929,6 +1936,7 @@ function readHeader(entry, parsed, headerOnly) {
1929
1936
  title: title,
1930
1937
  summary: summary,
1931
1938
  aliases: freeze(aliases),
1939
+ retired: retired,
1932
1940
  codec: entry.codec,
1933
1941
  logicalPath: entry.file.path,
1934
1942
  });
@@ -2001,7 +2009,7 @@ function inspectYamlReferences(entry, node, locator) {
2001
2009
  if (isSeq(node))
2002
2010
  node.items.forEach((child, index) => inspectYamlReferences(entry, child, [...locator, index]));
2003
2011
  }
2004
- function projectExplicitReferenceValues(source, selectedRange, references, descriptors) {
2012
+ function projectExplicitReferenceValues(source, selectedRange, references, descriptors, quoteProjection = false) {
2005
2013
  const selectedStart = selectedRange.start.offset;
2006
2014
  const selectedEnd = selectedRange.end.offset;
2007
2015
  const replacements = references
@@ -2017,8 +2025,12 @@ function projectExplicitReferenceValues(source, selectedRange, references, descr
2017
2025
  start,
2018
2026
  end,
2019
2027
  projection: descriptor === undefined
2020
- ? "(invalid document reference)"
2021
- : `@${descriptor.shortRef}`,
2028
+ ? quoteProjection
2029
+ ? JSON.stringify("(invalid document reference)")
2030
+ : "(invalid document reference)"
2031
+ : quoteProjection
2032
+ ? JSON.stringify(`@${descriptor.shortRef}`)
2033
+ : `@${descriptor.shortRef}`,
2022
2034
  },
2023
2035
  ]
2024
2036
  : [];
@@ -2195,12 +2207,6 @@ function lineExcerptRange(source, matchStart, matchEnd) {
2195
2207
  const end = Math.min(lineEnd, matchEnd + 160);
2196
2208
  return sourceRange(source, start, end);
2197
2209
  }
2198
- function safeUtf8PageEnd(bytes, offset, maxBytes) {
2199
- let end = Math.min(bytes.length, offset + maxBytes);
2200
- while (end > offset && (bytes[end] ?? 0) >= 0x80 && (bytes[end] ?? 0) < 0xc0)
2201
- end -= 1;
2202
- return end;
2203
- }
2204
2210
  function scanMarkdownHeadings(source, bodyStart) {
2205
2211
  const headings = [];
2206
2212
  let fence = null;
@@ -2241,8 +2247,13 @@ function scanMarkdownHeadings(source, bodyStart) {
2241
2247
  }
2242
2248
  return { headings, hasUnclosedFence: fence !== null };
2243
2249
  }
2244
- function patchRevisionMarkdown(source, edits) {
2250
+ function patchRevisionMarkdown(source, edits, initialMetadata) {
2245
2251
  let contents = source;
2252
+ let metadata = {
2253
+ title: initialMetadata.title,
2254
+ summary: initialMetadata.summary,
2255
+ aliases: [...initialMetadata.aliases],
2256
+ };
2246
2257
  for (const edit of edits) {
2247
2258
  if (!isRecord(edit) || typeof edit.op !== "string")
2248
2259
  return {
@@ -2254,24 +2265,32 @@ function patchRevisionMarkdown(source, edits) {
2254
2265
  ],
2255
2266
  };
2256
2267
  if (edit.op === "set_metadata") {
2257
- if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]) ||
2258
- !validDocumentMetadata(edit))
2268
+ if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]))
2269
+ return {
2270
+ diagnostics: [
2271
+ diagnostic({
2272
+ code: "document_header_invalid",
2273
+ message: "set_metadata accepts only op, title, summary, and aliases",
2274
+ }),
2275
+ ],
2276
+ };
2277
+ const resolved = resolveDocumentMetadataRevision(metadata, edit);
2278
+ if ("problems" in resolved)
2259
2279
  return {
2260
2280
  diagnostics: [
2261
2281
  diagnostic({
2262
2282
  code: "document_header_invalid",
2263
- message: hasOnlyKeys(edit, ["op", "title", "summary", "aliases"])
2264
- ? `set_metadata arguments are invalid: ${documentMetadataProblems(edit).join("; ")}`
2265
- : "set_metadata accepts only op, title, summary, and aliases",
2283
+ message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
2266
2284
  }),
2267
2285
  ],
2268
2286
  };
2287
+ metadata = resolved.metadata;
2269
2288
  const envelope = markdownEnvelope(contents);
2270
2289
  if ("diagnostics" in envelope)
2271
2290
  return envelope;
2272
- envelope.header.setIn(["$document", "title"], edit.title);
2273
- envelope.header.setIn(["$document", "summary"], edit.summary);
2274
- envelope.header.setIn(["$document", "aliases"], [...edit.aliases]);
2291
+ envelope.header.setIn(["$document", "title"], metadata.title);
2292
+ envelope.header.setIn(["$document", "summary"], metadata.summary);
2293
+ envelope.header.setIn(["$document", "aliases"], [...metadata.aliases]);
2275
2294
  const body = contents.slice(envelope.bodyStart);
2276
2295
  const firstHeading = scanMarkdownHeadings(body, 0).headings[0];
2277
2296
  if (firstHeading?.level !== 1)
@@ -2283,7 +2302,7 @@ function patchRevisionMarkdown(source, edits) {
2283
2302
  }),
2284
2303
  ],
2285
2304
  };
2286
- const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${edit.title}${body.slice(firstHeading.range.end.offset)}`;
2305
+ const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${metadata.title}${body.slice(firstHeading.range.end.offset)}`;
2287
2306
  contents = `---\n${envelope.header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${nextBody}`;
2288
2307
  continue;
2289
2308
  }
@@ -2464,6 +2483,31 @@ function markdownEnvelope(source) {
2464
2483
  };
2465
2484
  return { header, bodyStart: end + 5 };
2466
2485
  }
2486
+ function setDocumentRetired(source, codec, retired) {
2487
+ if (codec === "yaml") {
2488
+ const document = parseDocument(source, {
2489
+ schema: "core",
2490
+ uniqueKeys: true,
2491
+ strict: true,
2492
+ });
2493
+ if (retired)
2494
+ document.setIn(["$document", "retired"], true);
2495
+ else
2496
+ document.deleteIn(["$document", "retired"]);
2497
+ return document.toString({ indent: 2, lineWidth: 0 });
2498
+ }
2499
+ const headerEnd = source.indexOf("\n---\n", 4);
2500
+ const header = parseDocument(source.slice(4, headerEnd), {
2501
+ schema: "core",
2502
+ uniqueKeys: true,
2503
+ strict: true,
2504
+ });
2505
+ if (retired)
2506
+ header.setIn(["$document", "retired"], true);
2507
+ else
2508
+ header.deleteIn(["$document", "retired"]);
2509
+ return `---\n${header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}${source.slice(headerEnd)}`;
2510
+ }
2467
2511
  function markdownRevisionSection(source, locator) {
2468
2512
  const envelope = markdownEnvelope(source);
2469
2513
  if ("diagnostics" in envelope)
@@ -2562,6 +2606,11 @@ function scalarString(value) {
2562
2606
  ? value.value
2563
2607
  : null;
2564
2608
  }
2609
+ function scalarBoolean(value) {
2610
+ return isScalar(value) && typeof value.value === "boolean"
2611
+ ? value.value
2612
+ : null;
2613
+ }
2565
2614
  function stringSequence(value) {
2566
2615
  if (!isSeq(value))
2567
2616
  return null;
@@ -2618,6 +2667,23 @@ function mechanicalHash(contents) {
2618
2667
  function validDocumentMetadata(value) {
2619
2668
  return documentMetadataProblems(value).length === 0;
2620
2669
  }
2670
+ function resolveDocumentMetadataRevision(current, edit) {
2671
+ if (!Object.hasOwn(edit, "title") &&
2672
+ !Object.hasOwn(edit, "summary") &&
2673
+ !Object.hasOwn(edit, "aliases"))
2674
+ return {
2675
+ problems: ["at least one of title, summary, or aliases must be provided"],
2676
+ };
2677
+ const metadata = {
2678
+ title: Object.hasOwn(edit, "title") ? edit.title : current.title,
2679
+ summary: Object.hasOwn(edit, "summary") ? edit.summary : current.summary,
2680
+ aliases: Object.hasOwn(edit, "aliases") ? edit.aliases : current.aliases,
2681
+ };
2682
+ const problems = documentMetadataProblems(metadata);
2683
+ return problems.length === 0
2684
+ ? { metadata: metadata }
2685
+ : { problems };
2686
+ }
2621
2687
  /**
2622
2688
  * Says which of the three fields is wrong and what arrived instead.
2623
2689
  *
@@ -2711,6 +2777,10 @@ function validRelativePath(path) {
2711
2777
  .split("/")
2712
2778
  .every((segment) => segment !== "" && segment !== "." && segment !== ".."));
2713
2779
  }
2780
+ function validDeclaredDirectories(value) {
2781
+ return (Array.isArray(value) &&
2782
+ value.every((candidate) => typeof candidate === "string" && validRelativePath(candidate)));
2783
+ }
2714
2784
  function blockingDocumentDiagnostics(entry) {
2715
2785
  return entry.diagnostics.filter(({ code }) => code !== "locator_ambiguous" && code !== "document_reference_invalid");
2716
2786
  }
@@ -24,6 +24,7 @@ const worldLockHeartbeatStaleMilliseconds = 30_000;
24
24
  */
25
25
  export class FileNativeWorldOperationCoordinator {
26
26
  #correctionClaimsRoot;
27
+ #revisionLocksRoot;
27
28
  #locksRoot;
28
29
  #authorityLocksRoot;
29
30
  #now;
@@ -32,6 +33,7 @@ export class FileNativeWorldOperationCoordinator {
32
33
  constructor(dataRoot, options = {}) {
33
34
  const root = join(resolve(dataRoot), "operations");
34
35
  this.#correctionClaimsRoot = join(root, "correction-claims");
36
+ this.#revisionLocksRoot = join(root, "world-revision-locks");
35
37
  this.#locksRoot = join(root, "world-locks");
36
38
  this.#authorityLocksRoot = join(root, "authority-locks");
37
39
  this.#now = options.now ?? Date.now;
@@ -45,7 +47,8 @@ export class FileNativeWorldOperationCoordinator {
45
47
  assertNonEmptyIdentity(worldId, "world ID");
46
48
  assertNonEmptyIdentity(operationId, "correction operation ID");
47
49
  return this.#withWorldLock(worldId, async () => {
48
- if ((await this.#reconcileCorrectionClaim(worldId)) !== null)
50
+ if ((await this.#reconcileCorrectionClaim(worldId)) !== null ||
51
+ (await this.#readRevisionLock(worldId)) !== null)
49
52
  return { kind: "busy" };
50
53
  const now = this.#now();
51
54
  const record = {
@@ -107,11 +110,70 @@ export class FileNativeWorldOperationCoordinator {
107
110
  async withExclusiveWorldStateMutation(worldId, action) {
108
111
  assertNonEmptyIdentity(worldId, "world ID");
109
112
  return this.#withWorldLock(worldId, async () => {
110
- if ((await this.#reconcileCorrectionClaim(worldId)) !== null)
113
+ if ((await this.#reconcileCorrectionClaim(worldId)) !== null ||
114
+ (await this.#readRevisionLock(worldId)) !== null)
111
115
  throw new WorldOperationBusyError();
112
116
  return action();
113
117
  });
114
118
  }
119
+ /**
120
+ * Acquire or resume a persistent revision lock. The lock deliberately has no
121
+ * lease timeout: only Apply or Discard may release its epoch.
122
+ */
123
+ async acquireWorldRevision(worldId, epochId) {
124
+ assertNonEmptyIdentity(worldId, "world ID");
125
+ assertNonEmptyIdentity(epochId, "world-revision epoch ID");
126
+ return this.#withWorldLock(worldId, async () => {
127
+ const existing = await this.#readRevisionLock(worldId);
128
+ if (existing !== null)
129
+ return existing.epochId === epochId
130
+ ? {
131
+ kind: "acquired",
132
+ handle: revisionLockHandle(existing),
133
+ }
134
+ : { kind: "busy", epochId: existing.epochId };
135
+ if ((await this.#reconcileCorrectionClaim(worldId)) !== null)
136
+ return { kind: "busy" };
137
+ const record = {
138
+ schemaVersion: 1,
139
+ kind: "world_revision_lock",
140
+ worldId,
141
+ epochId,
142
+ acquiredAt: this.#now(),
143
+ };
144
+ await publishJson(this.#revisionLockPath(worldId), record);
145
+ return { kind: "acquired", handle: revisionLockHandle(record) };
146
+ });
147
+ }
148
+ async readWorldRevisionLock(worldId) {
149
+ assertNonEmptyIdentity(worldId, "world ID");
150
+ const record = await this.#readRevisionLock(worldId);
151
+ return record === null ? null : revisionLockHandle(record);
152
+ }
153
+ async assertWorldRevisionUnlocked(worldId) {
154
+ assertNonEmptyIdentity(worldId, "world ID");
155
+ if ((await this.#readRevisionLock(worldId)) !== null)
156
+ throw new WorldOperationBusyError();
157
+ }
158
+ async withWorldRevisionLock(handle, action) {
159
+ assertWorldRevisionLockHandle(handle);
160
+ return this.#withWorldLock(handle.worldId, async () => {
161
+ const current = await this.#readRevisionLock(handle.worldId);
162
+ if (current?.epochId !== handle.epochId)
163
+ throw new WorldOperationClaimLostError();
164
+ return action();
165
+ });
166
+ }
167
+ async releaseWorldRevision(handle) {
168
+ assertWorldRevisionLockHandle(handle);
169
+ await this.#withWorldLock(handle.worldId, async () => {
170
+ const current = await this.#readRevisionLock(handle.worldId);
171
+ if (current?.epochId !== handle.epochId)
172
+ return;
173
+ await rm(this.#revisionLockPath(handle.worldId), { force: true });
174
+ await syncDirectory(this.#revisionLocksRoot);
175
+ });
176
+ }
115
177
  async withWorldLocalMetadataMutation(worldId, action) {
116
178
  assertNonEmptyIdentity(worldId, "world ID");
117
179
  return this.#withWorldLock(worldId, action);
@@ -134,6 +196,20 @@ export class FileNativeWorldOperationCoordinator {
134
196
  throw new Error("invalid continuity correction claim");
135
197
  return value;
136
198
  }
199
+ async #readRevisionLock(worldId) {
200
+ let value;
201
+ try {
202
+ value = JSON.parse(await readFile(this.#revisionLockPath(worldId), "utf8"));
203
+ }
204
+ catch (error) {
205
+ if (isNodeError(error) && error.code === "ENOENT")
206
+ return null;
207
+ throw error;
208
+ }
209
+ if (!isWorldRevisionLockRecord(value) || value.worldId !== worldId)
210
+ throw new Error("invalid world revision lock");
211
+ return value;
212
+ }
137
213
  async #reconcileCorrectionClaim(worldId) {
138
214
  const claim = await this.#readCorrectionClaim(worldId);
139
215
  if (claim === null)
@@ -216,6 +292,31 @@ export class FileNativeWorldOperationCoordinator {
216
292
  #correctionClaimPath(worldId) {
217
293
  return join(this.#correctionClaimsRoot, `${digest(worldId)}.json`);
218
294
  }
295
+ #revisionLockPath(worldId) {
296
+ return join(this.#revisionLocksRoot, `${digest(worldId)}.json`);
297
+ }
298
+ }
299
+ function isWorldRevisionLockRecord(value) {
300
+ return (record(value) &&
301
+ hasExactKeys(value, [
302
+ "schemaVersion",
303
+ "kind",
304
+ "worldId",
305
+ "epochId",
306
+ "acquiredAt",
307
+ ]) &&
308
+ value.schemaVersion === 1 &&
309
+ value.kind === "world_revision_lock" &&
310
+ nonEmptyString(value.worldId) &&
311
+ nonEmptyString(value.epochId) &&
312
+ finiteNumber(value.acquiredAt));
313
+ }
314
+ function assertWorldRevisionLockHandle(handle) {
315
+ assertNonEmptyIdentity(handle.worldId, "world ID");
316
+ assertNonEmptyIdentity(handle.epochId, "world-revision epoch ID");
317
+ }
318
+ function revisionLockHandle(record) {
319
+ return { worldId: record.worldId, epochId: record.epochId };
219
320
  }
220
321
  function isCorrectionClaimRecord(value) {
221
322
  return (record(value) &&