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,2808 @@
1
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual, } from "node:crypto";
2
+ import { isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
3
+ const documentIdPattern = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u;
4
+ const shortRefPattern = /^[a-z][a-z0-9-]*$/u;
5
+ const base64UrlPattern = /^[A-Za-z0-9_-]+$/u;
6
+ const maxWorldDocumentBytes = 4 * 1024 * 1024;
7
+ const maxYamlDepth = 64;
8
+ const maxYamlNodes = 100_000;
9
+ const maxLiteralSearchMatches = 10_000;
10
+ const maxSelectedNodeBytes = 1024 * 1024;
11
+ export class WorldDocumentStore {
12
+ static open(input) {
13
+ return new WorldDocumentStore(input.layout, input.files);
14
+ }
15
+ id = `world-document-snapshot:${randomUUID()}`;
16
+ layout;
17
+ logicalRoot;
18
+ files;
19
+ diagnostics;
20
+ status;
21
+ #entries;
22
+ #cursorSecret = randomBytes(32);
23
+ constructor(layout, files) {
24
+ this.layout = layout;
25
+ this.logicalRoot = layout === "content_package" ? "world" : "state";
26
+ this.files = freeze(files.map((file) => freezeFile(file)));
27
+ this.#entries = this.files
28
+ .filter(({ path }) => path.startsWith(`${this.logicalRoot}/`))
29
+ .map((file) => this.#inspectFile(file));
30
+ this.#diagnoseDuplicateLogicalPaths();
31
+ this.#diagnoseDuplicateHeaders();
32
+ this.#diagnoseReferences();
33
+ for (const entry of this.#entries)
34
+ freeze(entry.diagnostics);
35
+ this.diagnostics = freeze(this.#entries.flatMap(({ diagnostics }) => diagnostics));
36
+ this.status = this.diagnostics.length === 0 ? "usable" : "needs_repair";
37
+ freeze(this.#entries);
38
+ Object.freeze(this);
39
+ }
40
+ revise(request) {
41
+ let activeCommandIndex = null;
42
+ try {
43
+ if (!isRecord(request) ||
44
+ !hasOnlyKeys(request, ["commands"]) ||
45
+ !Array.isArray(request.commands) ||
46
+ request.commands.length === 0 ||
47
+ request.commands.length > 64)
48
+ return this.#revisionFailure(null, {
49
+ code: "query_invalid",
50
+ message: "revise requires a closed candidate batch of 1 to 64 commands",
51
+ });
52
+ const working = this.files.map((file) => ({
53
+ file: { ...file },
54
+ original: file,
55
+ lastCommandIndex: null,
56
+ }));
57
+ for (const [commandIndex, command] of request.commands.entries()) {
58
+ activeCommandIndex = commandIndex;
59
+ if (!isRecord(command) || typeof command.kind !== "string")
60
+ return this.#revisionFailure(commandIndex, {
61
+ code: "query_invalid",
62
+ message: "A revise command must be a closed object with kind",
63
+ });
64
+ const current = WorldDocumentStore.open({
65
+ layout: this.layout,
66
+ files: working.map(({ file }) => file),
67
+ });
68
+ let touchedPath;
69
+ if (command.kind === "create") {
70
+ if (!hasOnlyKeys(command, [
71
+ "kind",
72
+ "temporaryName",
73
+ "logicalPath",
74
+ "codec",
75
+ "refHint",
76
+ "title",
77
+ "summary",
78
+ "aliases",
79
+ "body",
80
+ ]) ||
81
+ typeof command.temporaryName !== "string" ||
82
+ !/^[a-z][a-z0-9_-]{0,63}$/u.test(command.temporaryName) ||
83
+ working.some(({ temporaryName }) => temporaryName === command.temporaryName) ||
84
+ typeof command.logicalPath !== "string" ||
85
+ (command.codec !== "yaml" && command.codec !== "markdown") ||
86
+ !validRevisionLogicalPath(command.logicalPath, command.codec, this.logicalRoot) ||
87
+ typeof command.refHint !== "string" ||
88
+ command.refHint.length < 2 ||
89
+ command.refHint.length > 32 ||
90
+ !shortRefPattern.test(command.refHint) ||
91
+ !validDocumentMetadata(command) ||
92
+ (command.codec === "yaml"
93
+ ? (typeof command.body !== "string" && !isRecord(command.body)) ||
94
+ (isRecord(command.body) &&
95
+ Object.hasOwn(command.body, "$document"))
96
+ : typeof command.body !== "string"))
97
+ return this.#revisionFailure(commandIndex, {
98
+ code: "query_invalid",
99
+ message: "Create-command arguments, temporary name, logical path, codec, or document metadata are invalid",
100
+ });
101
+ if (working.some(({ file }) => file.path === command.logicalPath))
102
+ return this.#revisionFailure(commandIndex, {
103
+ code: "logical_path_duplicate",
104
+ logicalPath: command.logicalPath,
105
+ message: "Create target logical path already exists",
106
+ });
107
+ const createLogicalPath = command.logicalPath;
108
+ const occupiedRefs = new Set(current.#entries.flatMap(({ descriptor }) => descriptor === undefined ? [] : [descriptor.shortRef]));
109
+ const shortRef = nextAvailableShortRef(command.refHint, occupiedRefs);
110
+ const documentId = `doc.${randomUUID().replace(/-/gu, "")}`;
111
+ const header = {
112
+ $document: {
113
+ id: documentId,
114
+ ref: shortRef,
115
+ title: command.title,
116
+ summary: command.summary,
117
+ aliases: [...command.aliases],
118
+ },
119
+ };
120
+ let contents;
121
+ if (command.codec === "yaml") {
122
+ const revisionBody = typeof command.body === "string"
123
+ ? parseRevisionYamlBodySource(command.logicalPath, command.body)
124
+ : { value: command.body };
125
+ if ("diagnostics" in revisionBody)
126
+ return this.#revisionFailure(commandIndex, revisionBody.diagnostics);
127
+ const body = this.#resolveRevisionYamlValue(revisionBody.value, current, working, {
128
+ referenceInput: typeof command.body === "string"
129
+ ? "explicit_short_ref_only"
130
+ : "persistent_or_explicit",
131
+ });
132
+ if ("diagnostics" in body)
133
+ return this.#revisionFailure(commandIndex, body.diagnostics.map((problem) => ({
134
+ ...problem,
135
+ logicalPath: createLogicalPath,
136
+ documentId,
137
+ })));
138
+ if (!isRecord(body.value) || Object.hasOwn(body.value, "$document"))
139
+ return this.#revisionFailure(commandIndex, {
140
+ code: "yaml_invalid",
141
+ logicalPath: command.logicalPath,
142
+ message: "Create YAML body must be a safe map without $document",
143
+ });
144
+ contents = stringify({ ...header, ...body.value }, { indent: 2, lineWidth: 0 });
145
+ }
146
+ else
147
+ contents = `---\n${stringify(header, { indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${command.body.trimEnd()}\n`;
148
+ working.push({
149
+ file: { path: command.logicalPath, contents },
150
+ original: null,
151
+ temporaryName: command.temporaryName,
152
+ lastCommandIndex: commandIndex,
153
+ });
154
+ touchedPath = command.logicalPath;
155
+ }
156
+ else if (command.kind === "move") {
157
+ if (this.layout === "world_state")
158
+ return this.#revisionFailure(commandIndex, {
159
+ code: "query_invalid",
160
+ message: "World state revision does not support move",
161
+ });
162
+ if (!hasOnlyKeys(command, ["kind", "document", "toLogicalPath"]) ||
163
+ typeof command.toLogicalPath !== "string")
164
+ return this.#revisionFailure(commandIndex, {
165
+ code: "query_invalid",
166
+ message: "A move command requires a target and new logical path",
167
+ });
168
+ const target = this.#resolveRevisionTarget(current, working, command.document);
169
+ if ("diagnostics" in target)
170
+ return this.#revisionFailure(commandIndex, target.diagnostics);
171
+ if (!validRevisionLogicalPath(command.toLogicalPath, target.entry.descriptor.codec, this.logicalRoot))
172
+ return this.#revisionFailure(commandIndex, {
173
+ code: "logical_path_invalid",
174
+ logicalPath: command.toLogicalPath,
175
+ documentId: target.entry.descriptor.documentId,
176
+ message: "Move target must be a safe logical path under world/ with the original codec",
177
+ });
178
+ if (working.some(({ file }) => file !== target.working.file &&
179
+ file.path === command.toLogicalPath))
180
+ return this.#revisionFailure(commandIndex, {
181
+ code: "logical_path_duplicate",
182
+ logicalPath: command.toLogicalPath,
183
+ documentId: target.entry.descriptor.documentId,
184
+ message: "Move target logical path already exists",
185
+ });
186
+ target.working.file.path = command.toLogicalPath;
187
+ target.working.lastCommandIndex = commandIndex;
188
+ touchedPath = command.toLogicalPath;
189
+ }
190
+ else if (command.kind === "replace") {
191
+ if (!hasOnlyKeys(command, ["kind", "document", "contents"]) ||
192
+ typeof command.contents !== "string")
193
+ return this.#revisionFailure(commandIndex, {
194
+ code: "query_invalid",
195
+ message: "A replace command requires a target and complete UTF-8 document source",
196
+ });
197
+ const target = this.#resolveRevisionReplaceTarget(current, working, command.document);
198
+ if ("diagnostics" in target)
199
+ return this.#revisionFailure(commandIndex, target.diagnostics);
200
+ const previousDescriptor = target.entry.descriptor;
201
+ target.working.file.contents = command.contents;
202
+ delete target.working.file.encoding;
203
+ target.working.lastCommandIndex = commandIndex;
204
+ touchedPath = target.working.file.path;
205
+ const candidate = WorldDocumentStore.open({
206
+ layout: this.layout,
207
+ files: working.map(({ file }) => file),
208
+ });
209
+ const localProblems = this.#localRevisionDiagnostics(candidate, touchedPath);
210
+ if (localProblems.length > 0)
211
+ return this.#revisionFailure(commandIndex, localProblems);
212
+ const nextDescriptor = candidate.#entries.find(({ file }) => file.path === touchedPath)?.descriptor;
213
+ if (nextDescriptor === undefined)
214
+ return this.#revisionFailure(commandIndex, {
215
+ code: "document_header_invalid",
216
+ logicalPath: touchedPath,
217
+ message: "Replace must produce a world document with a usable identity",
218
+ });
219
+ const identityProblems = [];
220
+ if (previousDescriptor !== undefined &&
221
+ previousDescriptor.documentId !== nextDescriptor.documentId)
222
+ identityProblems.push({
223
+ code: "document_identity_invalid",
224
+ logicalPath: touchedPath,
225
+ documentId: nextDescriptor.documentId,
226
+ message: "Replacing a valid document cannot change its identity",
227
+ });
228
+ if (previousDescriptor !== undefined &&
229
+ previousDescriptor.shortRef !== nextDescriptor.shortRef)
230
+ identityProblems.push({
231
+ code: "document_short_ref_invalid",
232
+ logicalPath: touchedPath,
233
+ documentId: nextDescriptor.documentId,
234
+ message: "Replacing a valid document cannot change its short reference",
235
+ });
236
+ if (identityProblems.length > 0)
237
+ return this.#revisionFailure(commandIndex, identityProblems);
238
+ }
239
+ else if (command.kind === "write") {
240
+ if (!hasOnlyKeys(command, ["kind", "logicalPath", "contents"]) ||
241
+ typeof command.logicalPath !== "string" ||
242
+ typeof command.contents !== "string")
243
+ return this.#revisionFailure(commandIndex, {
244
+ code: "query_invalid",
245
+ message: "A write command requires a logical path and complete UTF-8 document source",
246
+ });
247
+ const writePath = command.logicalPath;
248
+ const codec = documentCodec(writePath);
249
+ if (codec === null ||
250
+ !validRevisionLogicalPath(writePath, codec, this.logicalRoot))
251
+ return this.#revisionFailure(commandIndex, {
252
+ code: "logical_path_invalid",
253
+ logicalPath: writePath,
254
+ message: `Write target must be a safe logical path under ${this.logicalRoot}/ ending in .yaml or .md`,
255
+ });
256
+ const existing = working.find(({ file }) => file.path === writePath);
257
+ const previous = current.#entries.find(({ file }) => file.path === writePath)?.descriptor;
258
+ const source = parseWriteDocumentSource(writePath, codec, command.contents, previous);
259
+ if ("diagnostics" in source)
260
+ return this.#revisionFailure(commandIndex, source.diagnostics);
261
+ // A damaged document has no usable identity to preserve, yet other
262
+ // documents may still reference the identity it used to carry. Only
263
+ // there is the authored id honoured; conflicts surface as diagnostics.
264
+ const repairing = existing !== undefined && previous === undefined;
265
+ const documentId = previous?.documentId ??
266
+ (repairing ? source.authoredId : null) ??
267
+ `doc.${randomUUID().replace(/-/gu, "")}`;
268
+ const shortRef = previous?.shortRef ??
269
+ (repairing
270
+ ? source.refHint
271
+ : nextAvailableShortRef(source.refHint, new Set(current.#entries.flatMap(({ descriptor }) => descriptor === undefined ? [] : [descriptor.shortRef]))));
272
+ const header = {
273
+ $document: {
274
+ id: documentId,
275
+ ref: shortRef,
276
+ title: source.metadata.title,
277
+ summary: source.metadata.summary,
278
+ aliases: [...source.metadata.aliases],
279
+ },
280
+ };
281
+ let contents;
282
+ if (codec === "yaml") {
283
+ // Unlike create, write also overwrites existing documents, whose
284
+ // own source already spells references as document ids. Rejecting
285
+ // that form would break read-edit-write on any referencing file.
286
+ const body = this.#resolveRevisionYamlValue(source.body, current, working, { referenceInput: "persistent_or_explicit" });
287
+ if ("diagnostics" in body)
288
+ return this.#revisionFailure(commandIndex, body.diagnostics.map((problem) => ({
289
+ ...problem,
290
+ logicalPath: writePath,
291
+ documentId,
292
+ })));
293
+ if (!isRecord(body.value))
294
+ return this.#revisionFailure(commandIndex, {
295
+ code: "yaml_invalid",
296
+ logicalPath: writePath,
297
+ documentId,
298
+ message: "Write YAML body must be a safe map without $document",
299
+ });
300
+ contents = stringify({ ...header, ...body.value }, { indent: 2, lineWidth: 0 });
301
+ }
302
+ else
303
+ contents = `---\n${stringify(header, { indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${source.body.trimEnd()}\n`;
304
+ if (existing === undefined)
305
+ working.push({
306
+ file: { path: writePath, contents },
307
+ original: null,
308
+ lastCommandIndex: commandIndex,
309
+ });
310
+ else {
311
+ existing.file.contents = contents;
312
+ delete existing.file.encoding;
313
+ existing.lastCommandIndex = commandIndex;
314
+ }
315
+ touchedPath = writePath;
316
+ }
317
+ else if (command.kind === "patch") {
318
+ if (!hasOnlyKeys(command, ["kind", "document", "edits"]) ||
319
+ !Array.isArray(command.edits) ||
320
+ command.edits.length < 1 ||
321
+ command.edits.length > 32)
322
+ return this.#revisionFailure(commandIndex, {
323
+ code: "query_invalid",
324
+ message: "A patch command requires a target and 1 to 32 closed edits",
325
+ });
326
+ const target = this.#resolveRevisionTarget(current, working, command.document);
327
+ if ("diagnostics" in target)
328
+ return this.#revisionFailure(commandIndex, target.diagnostics);
329
+ if (target.entry.codec === "markdown") {
330
+ const patched = patchRevisionMarkdown(target.working.file.contents, command.edits);
331
+ if ("diagnostics" in patched)
332
+ return this.#revisionFailure(commandIndex, patched.diagnostics.map((problem) => ({
333
+ ...problem,
334
+ logicalPath: target.entry.file.path,
335
+ documentId: target.entry.descriptor.documentId,
336
+ })));
337
+ target.working.file.contents = patched.contents;
338
+ }
339
+ else {
340
+ let document = parseDocument(target.working.file.contents, {
341
+ schema: "core",
342
+ uniqueKeys: true,
343
+ strict: true,
344
+ });
345
+ for (const edit of command.edits) {
346
+ if (!isRecord(edit) || typeof edit.op !== "string")
347
+ return this.#revisionFailure(commandIndex, {
348
+ code: "query_invalid",
349
+ logicalPath: target.entry.file.path,
350
+ documentId: target.entry.descriptor.documentId,
351
+ message: "A patch edit must be a closed object with op",
352
+ });
353
+ if (edit.op === "set_metadata") {
354
+ if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]))
355
+ return this.#revisionFailure(commandIndex, {
356
+ code: "document_header_invalid",
357
+ logicalPath: target.entry.file.path,
358
+ documentId: target.entry.descriptor.documentId,
359
+ message: "set_metadata accepts only op, title, summary, and aliases",
360
+ });
361
+ if (!validDocumentMetadata(edit))
362
+ return this.#revisionFailure(commandIndex, {
363
+ code: "document_header_invalid",
364
+ logicalPath: target.entry.file.path,
365
+ documentId: target.entry.descriptor.documentId,
366
+ message: `set_metadata arguments are invalid: ${documentMetadataProblems(edit).join("; ")}`,
367
+ });
368
+ document.setIn(["$document", "title"], edit.title);
369
+ document.setIn(["$document", "summary"], edit.summary);
370
+ document.setIn(["$document", "aliases"], [...edit.aliases]);
371
+ continue;
372
+ }
373
+ if (!["add", "replace", "append", "remove"].includes(edit.op) ||
374
+ !validYamlLocator(edit.locator) ||
375
+ edit.locator.yaml[0] === "$document")
376
+ return this.#revisionFailure(commandIndex, {
377
+ code: "locator_invalid",
378
+ logicalPath: target.entry.file.path,
379
+ documentId: target.entry.descriptor?.documentId,
380
+ ...(isRecord(edit) && isRecord(edit.locator)
381
+ ? {
382
+ locator: copyLocator(edit.locator),
383
+ }
384
+ : {}),
385
+ message: "A YAML edit requires an exact locator outside the technical header",
386
+ });
387
+ const locator = [...edit.locator.yaml];
388
+ if (locator.length === 0) {
389
+ if ((edit.op !== "add" && edit.op !== "replace") ||
390
+ !hasOnlyKeys(edit, ["op", "locator", "value"]))
391
+ return this.#revisionFailure(commandIndex, {
392
+ code: "locator_invalid",
393
+ logicalPath: target.entry.file.path,
394
+ documentId: target.entry.descriptor.documentId,
395
+ locator: { yaml: [] },
396
+ message: "A whole YAML body supports only add or replace",
397
+ });
398
+ const value = this.#resolveRevisionYamlValue(edit.value, current, working, { referenceInput: "persistent_or_explicit" }, []);
399
+ if ("diagnostics" in value)
400
+ return this.#revisionFailure(commandIndex, value.diagnostics.map((problem) => ({
401
+ ...problem,
402
+ logicalPath: target.entry.file.path,
403
+ documentId: target.entry.descriptor.documentId,
404
+ })));
405
+ if (!isRecord(value.value) ||
406
+ Object.hasOwn(value.value, "$document"))
407
+ return this.#revisionFailure(commandIndex, {
408
+ code: "yaml_invalid",
409
+ logicalPath: target.entry.file.path,
410
+ documentId: target.entry.descriptor.documentId,
411
+ locator: { yaml: [] },
412
+ message: "A whole YAML body must be a map without $document",
413
+ });
414
+ const currentValue = document.toJS({
415
+ maxAliasCount: 0,
416
+ });
417
+ if (!isRecord(currentValue) ||
418
+ !isRecord(currentValue.$document))
419
+ return this.#revisionFailure(commandIndex, {
420
+ code: "document_header_invalid",
421
+ logicalPath: target.entry.file.path,
422
+ documentId: target.entry.descriptor.documentId,
423
+ message: "YAML technical header could not be preserved",
424
+ });
425
+ if (edit.op === "add" &&
426
+ Object.keys(currentValue).some((key) => key !== "$document"))
427
+ return this.#revisionFailure(commandIndex, {
428
+ code: "locator_invalid",
429
+ logicalPath: target.entry.file.path,
430
+ documentId: target.entry.descriptor.documentId,
431
+ locator: { yaml: [] },
432
+ message: "Adding a whole YAML body requires the current body to be empty",
433
+ });
434
+ document = parseDocument(stringify({ $document: currentValue.$document, ...value.value }, { indent: 2, lineWidth: 0 }), { schema: "core", uniqueKeys: true, strict: true });
435
+ continue;
436
+ }
437
+ const exists = document.hasIn(locator);
438
+ if (edit.op === "remove") {
439
+ if (!hasOnlyKeys(edit, ["op", "locator"]) || !exists)
440
+ return this.#revisionFailure(commandIndex, {
441
+ code: "locator_not_found",
442
+ logicalPath: target.entry.file.path,
443
+ documentId: target.entry.descriptor.documentId,
444
+ locator: { yaml: locator },
445
+ message: "YAML remove target must exist",
446
+ });
447
+ document.deleteIn(locator);
448
+ continue;
449
+ }
450
+ if (!hasOnlyKeys(edit, ["op", "locator", "value"]))
451
+ return this.#revisionFailure(commandIndex, {
452
+ code: "query_invalid",
453
+ logicalPath: target.entry.file.path,
454
+ documentId: target.entry.descriptor.documentId,
455
+ locator: { yaml: locator },
456
+ message: "YAML value-edit arguments are invalid",
457
+ });
458
+ let valueLocator = locator;
459
+ if (edit.op === "append") {
460
+ const sequence = document.getIn(locator, true);
461
+ if (!isSeq(sequence))
462
+ return this.#revisionFailure(commandIndex, {
463
+ code: "locator_invalid",
464
+ logicalPath: target.entry.file.path,
465
+ documentId: target.entry.descriptor.documentId,
466
+ locator: { yaml: locator },
467
+ message: "YAML append target must be a sequence",
468
+ });
469
+ valueLocator = [...locator, sequence.items.length];
470
+ }
471
+ const value = this.#resolveRevisionYamlValue(edit.value, current, working, { referenceInput: "persistent_or_explicit" }, valueLocator);
472
+ if ("diagnostics" in value)
473
+ return this.#revisionFailure(commandIndex, value.diagnostics.map((problem) => ({
474
+ ...problem,
475
+ logicalPath: target.entry.file.path,
476
+ documentId: target.entry.descriptor?.documentId,
477
+ })));
478
+ if (edit.op === "add") {
479
+ const parent = locator.length === 1
480
+ ? document.contents
481
+ : document.getIn(locator.slice(0, -1), true);
482
+ const last = locator.at(-1);
483
+ const parentAcceptsLocator = (isMap(parent) && typeof last === "string") ||
484
+ (isSeq(parent) && typeof last === "number");
485
+ if (exists || !parentAcceptsLocator)
486
+ return this.#revisionFailure(commandIndex, {
487
+ code: "locator_invalid",
488
+ logicalPath: target.entry.file.path,
489
+ documentId: target.entry.descriptor.documentId,
490
+ locator: { yaml: locator },
491
+ message: "YAML add target must not exist, and its parent map or sequence must exist",
492
+ });
493
+ document.setIn(locator, value.value);
494
+ }
495
+ else if (edit.op === "replace") {
496
+ if (!exists)
497
+ return this.#revisionFailure(commandIndex, {
498
+ code: "locator_not_found",
499
+ logicalPath: target.entry.file.path,
500
+ documentId: target.entry.descriptor.documentId,
501
+ locator: { yaml: locator },
502
+ message: "YAML replace target must exist",
503
+ });
504
+ document.setIn(locator, value.value);
505
+ }
506
+ else
507
+ document.addIn(locator, value.value);
508
+ }
509
+ target.working.file.contents = document.toString({
510
+ indent: 2,
511
+ lineWidth: 0,
512
+ });
513
+ }
514
+ delete target.working.file.encoding;
515
+ target.working.lastCommandIndex = commandIndex;
516
+ touchedPath = target.working.file.path;
517
+ }
518
+ else
519
+ return this.#revisionFailure(commandIndex, {
520
+ code: "query_invalid",
521
+ message: "World-document candidate batch does not support this command kind",
522
+ });
523
+ const candidate = WorldDocumentStore.open({
524
+ layout: this.layout,
525
+ files: working.map(({ file }) => file),
526
+ });
527
+ const localProblems = this.#localRevisionDiagnostics(candidate, touchedPath);
528
+ if (localProblems.length > 0)
529
+ return this.#revisionFailure(commandIndex, localProblems);
530
+ }
531
+ const snapshot = WorldDocumentStore.open({
532
+ layout: this.layout,
533
+ files: working.map(({ file }) => file),
534
+ });
535
+ if (snapshot.status !== "usable")
536
+ return freeze({
537
+ kind: "error",
538
+ ok: false,
539
+ sourceSnapshotId: this.id,
540
+ snapshotStatus: this.status,
541
+ diagnostics: freeze(snapshot.diagnostics.map((problem) => {
542
+ const commandIndex = working.find(({ file }) => file.path === problem.logicalPath)?.lastCommandIndex;
543
+ return revisionDiagnostic(commandIndex ?? null, problem);
544
+ })),
545
+ });
546
+ const changes = working
547
+ .filter(({ file, original }) => file.path !== original?.path ||
548
+ file.contents !== original?.contents ||
549
+ file.encoding !== original?.encoding)
550
+ .map(({ file, original, lastCommandIndex }) => {
551
+ const descriptor = snapshot.#entries.find(({ file: candidate }) => candidate.path === file.path)?.descriptor;
552
+ if (descriptor === undefined)
553
+ throw new Error("Usable candidate is missing its final world-document identity");
554
+ return freeze({
555
+ documentId: descriptor.documentId,
556
+ shortRef: descriptor.shortRef,
557
+ codec: descriptor.codec,
558
+ commandIndex: lastCommandIndex,
559
+ before: original === null
560
+ ? null
561
+ : freeze({
562
+ logicalPath: original.path,
563
+ contents: original.contents,
564
+ mechanicalHash: mechanicalHash(original.contents),
565
+ }),
566
+ after: freeze({
567
+ logicalPath: file.path,
568
+ contents: file.contents,
569
+ mechanicalHash: mechanicalHash(file.contents),
570
+ }),
571
+ });
572
+ })
573
+ .sort((left, right) => left.after.logicalPath.localeCompare(right.after.logicalPath));
574
+ return freeze({
575
+ kind: "revision",
576
+ ok: true,
577
+ sourceSnapshotId: this.id,
578
+ snapshot,
579
+ snapshotStatus: snapshot.status,
580
+ files: snapshot.files,
581
+ diagnostics: freeze([]),
582
+ changes: freeze(changes),
583
+ });
584
+ }
585
+ catch {
586
+ return this.#revisionFailure(activeCommandIndex, {
587
+ code: "query_invalid",
588
+ message: "Revise command cannot be interpreted as safe closed input",
589
+ });
590
+ }
591
+ }
592
+ #revisionFailure(commandIndex, problem) {
593
+ const diagnostics = Array.isArray(problem)
594
+ ? problem
595
+ : [problem];
596
+ return freeze({
597
+ kind: "error",
598
+ ok: false,
599
+ sourceSnapshotId: this.id,
600
+ snapshotStatus: this.status,
601
+ diagnostics: freeze(diagnostics.map((candidate) => revisionDiagnostic(commandIndex, candidate))),
602
+ });
603
+ }
604
+ #resolveRevisionTarget(snapshot, working, target) {
605
+ let resolved;
606
+ if (isRecord(target) &&
607
+ Object.keys(target).length === 1 &&
608
+ typeof target.temporaryName === "string") {
609
+ const temporary = working.filter(({ temporaryName }) => temporaryName === target.temporaryName);
610
+ if (temporary.length !== 1)
611
+ return {
612
+ diagnostics: freeze([
613
+ diagnostic({
614
+ code: temporary.length === 0
615
+ ? "document_not_found"
616
+ : "document_ambiguous",
617
+ message: "Candidate-batch temporary name does not uniquely identify a document created in the batch",
618
+ }),
619
+ ]),
620
+ };
621
+ resolved = snapshot.#resolveSelector({
622
+ logicalPath: temporary[0].file.path,
623
+ });
624
+ }
625
+ else
626
+ resolved = snapshot.#resolveSelector(target);
627
+ if ("diagnostics" in resolved)
628
+ return resolved;
629
+ if (resolved.entry.descriptor === undefined)
630
+ return {
631
+ diagnostics: freeze([
632
+ diagnostic({
633
+ code: "document_header_invalid",
634
+ logicalPath: resolved.entry.file.path,
635
+ message: "Candidate-batch target has no usable document identity",
636
+ }),
637
+ ]),
638
+ };
639
+ const matches = working.filter(({ file }) => file.path === resolved.entry.file.path);
640
+ if (matches.length !== 1)
641
+ return {
642
+ diagnostics: freeze([
643
+ diagnostic({
644
+ code: "document_ambiguous",
645
+ logicalPath: resolved.entry.file.path,
646
+ documentId: resolved.entry.descriptor.documentId,
647
+ message: "Target is not unique in the complete candidate file set",
648
+ }),
649
+ ]),
650
+ };
651
+ return {
652
+ entry: resolved.entry,
653
+ working: matches[0],
654
+ };
655
+ }
656
+ #resolveRevisionReplaceTarget(snapshot, working, target) {
657
+ if (!isRecord(target) ||
658
+ Object.keys(target).length !== 1 ||
659
+ typeof target.logicalPath !== "string")
660
+ return this.#resolveRevisionTarget(snapshot, working, target);
661
+ const entries = snapshot.#entries.filter(({ file }) => file.path === target.logicalPath);
662
+ const files = working.filter(({ file }) => file.path === target.logicalPath);
663
+ if (entries.length !== 1 || files.length !== 1)
664
+ return {
665
+ diagnostics: freeze([
666
+ diagnostic({
667
+ code: entries.length === 0 || files.length === 0
668
+ ? "document_not_found"
669
+ : "document_ambiguous",
670
+ logicalPath: target.logicalPath,
671
+ message: "Replace logical path does not uniquely identify a candidate world document",
672
+ }),
673
+ ]),
674
+ };
675
+ return { entry: entries[0], working: files[0] };
676
+ }
677
+ #resolveRevisionYamlValue(value, snapshot, working, options = { referenceInput: "persistent_or_explicit" }, locator = [], seen = new WeakSet()) {
678
+ if (value === null ||
679
+ typeof value === "string" ||
680
+ typeof value === "boolean" ||
681
+ (typeof value === "number" && Number.isFinite(value)))
682
+ return { value };
683
+ if (typeof value !== "object" || value === null || seen.has(value))
684
+ return {
685
+ diagnostics: freeze([
686
+ diagnostic({
687
+ code: "yaml_invalid",
688
+ locator: { yaml: [...locator] },
689
+ message: "Candidate YAML value must be an acyclic JSON-shaped safe value",
690
+ }),
691
+ ]),
692
+ };
693
+ seen.add(value);
694
+ if (Array.isArray(value)) {
695
+ const result = [];
696
+ for (const [index, item] of value.entries()) {
697
+ const resolved = this.#resolveRevisionYamlValue(item, snapshot, working, options, [...locator, index], seen);
698
+ if ("diagnostics" in resolved)
699
+ return resolved;
700
+ result.push(resolved.value);
701
+ }
702
+ seen.delete(value);
703
+ return { value: result };
704
+ }
705
+ const prototype = Object.getPrototypeOf(value);
706
+ if (!isRecord(value) ||
707
+ (prototype !== Object.prototype && prototype !== null))
708
+ return {
709
+ diagnostics: freeze([
710
+ diagnostic({
711
+ code: "yaml_invalid",
712
+ locator: { yaml: [...locator] },
713
+ message: "Candidate YAML map must be a plain object",
714
+ }),
715
+ ]),
716
+ };
717
+ if (Object.hasOwn(value, "$ref")) {
718
+ if (Object.keys(value).length !== 1 ||
719
+ (options.referenceInput === "explicit_short_ref_only" &&
720
+ (typeof value.$ref !== "string" || !value.$ref.startsWith("@"))))
721
+ return {
722
+ diagnostics: freeze([
723
+ diagnostic({
724
+ code: "document_reference_invalid",
725
+ locator: { yaml: [...locator] },
726
+ message: options.referenceInput === "explicit_short_ref_only"
727
+ ? "$ref must use an @short-ref returned by Runtime, and the reference object cannot contain other fields"
728
+ : "A $ref object cannot contain other fields",
729
+ }),
730
+ ]),
731
+ };
732
+ if (typeof value.$ref === "string") {
733
+ if (!value.$ref.startsWith("@")) {
734
+ if (!documentIdPattern.test(value.$ref))
735
+ return {
736
+ diagnostics: freeze([
737
+ diagnostic({
738
+ code: "document_reference_invalid",
739
+ locator: { yaml: [...locator] },
740
+ message: "$ref must be a document identity or explicit document selector",
741
+ }),
742
+ ]),
743
+ };
744
+ seen.delete(value);
745
+ return { value: { $ref: value.$ref } };
746
+ }
747
+ const target = this.#resolveRevisionTarget(snapshot, working, {
748
+ shortRef: value.$ref.slice(1),
749
+ });
750
+ if ("diagnostics" in target)
751
+ return {
752
+ diagnostics: freeze(target.diagnostics.map((problem) => diagnostic({
753
+ ...problem,
754
+ locator: { yaml: [...locator] },
755
+ }))),
756
+ };
757
+ seen.delete(value);
758
+ return { value: { $ref: target.entry.descriptor.documentId } };
759
+ }
760
+ const target = this.#resolveRevisionTarget(snapshot, working, value.$ref);
761
+ if ("diagnostics" in target)
762
+ return {
763
+ diagnostics: freeze(target.diagnostics.map((problem) => diagnostic({
764
+ ...problem,
765
+ locator: { yaml: [...locator] },
766
+ }))),
767
+ };
768
+ seen.delete(value);
769
+ return { value: { $ref: target.entry.descriptor.documentId } };
770
+ }
771
+ const result = Object.create(null);
772
+ for (const [key, item] of Object.entries(value)) {
773
+ const resolved = this.#resolveRevisionYamlValue(item, snapshot, working, options, [...locator, key], seen);
774
+ if ("diagnostics" in resolved)
775
+ return resolved;
776
+ result[key] = resolved.value;
777
+ }
778
+ seen.delete(value);
779
+ return { value: result };
780
+ }
781
+ #localRevisionDiagnostics(snapshot, logicalPath) {
782
+ const entries = snapshot.#entries.filter(({ file }) => file.path === logicalPath);
783
+ if (entries.length !== 1)
784
+ return freeze([
785
+ diagnostic({
786
+ code: entries.length === 0
787
+ ? "document_not_found"
788
+ : "logical_path_duplicate",
789
+ logicalPath,
790
+ message: "Command must produce one unique target world document",
791
+ }),
792
+ ]);
793
+ const entry = entries[0];
794
+ const malformedReference = entry.references.some(({ targetId }) => targetId === null);
795
+ return freeze(entry.diagnostics.filter(({ code }) => code !== "document_identity_duplicate" &&
796
+ code !== "document_short_ref_duplicate" &&
797
+ (code !== "document_reference_invalid" || malformedReference)));
798
+ }
799
+ query(request) {
800
+ if (!isRecord(request) || typeof request.kind !== "string")
801
+ return this.#failure(null, null, [
802
+ diagnostic({
803
+ code: "query_invalid",
804
+ message: "WorldDocumentStore.query accepts only closed query requests",
805
+ }),
806
+ ]);
807
+ if (request.kind === "catalog")
808
+ return this.#catalog(request);
809
+ if (request.kind === "literal_search")
810
+ return this.#literalSearch(request);
811
+ if (request.kind === "read_document")
812
+ return this.#readDocument(request);
813
+ if (request.kind === "select_node")
814
+ return this.#selectNode(request);
815
+ return this.#failure(null, null, [
816
+ diagnostic({
817
+ code: "query_invalid",
818
+ message: "WorldDocumentStore.query accepts only closed query requests",
819
+ }),
820
+ ]);
821
+ }
822
+ #catalog(request) {
823
+ if (!hasOnlyKeys(request, ["kind", "directory", "limit", "cursor"]))
824
+ return this.#failure("catalog", request, [
825
+ diagnostic({
826
+ code: "query_invalid",
827
+ message: "Catalog contains undeclared query conditions",
828
+ }),
829
+ ]);
830
+ const directory = request.directory ?? "";
831
+ const limit = request.limit ?? 20;
832
+ if ((directory !== "" && !validRelativePath(directory)) ||
833
+ !Number.isInteger(limit) ||
834
+ limit < 1 ||
835
+ limit > 100)
836
+ return this.#failure("catalog", request, [
837
+ diagnostic({
838
+ code: "query_invalid",
839
+ message: "Catalog directory must be within the layout root, and limit must be 1 to 100",
840
+ }),
841
+ ]);
842
+ const relativePrefix = directory === "" ? "" : `${directory}/`;
843
+ const prefix = `${this.logicalRoot}/${relativePrefix}`;
844
+ const entries = new Map();
845
+ for (const entry of this.#entries) {
846
+ if (!entry.file.path.startsWith(prefix))
847
+ continue;
848
+ const relative = entry.file.path.slice(prefix.length);
849
+ const slash = relative.indexOf("/");
850
+ if (slash >= 0) {
851
+ const child = relative.slice(0, slash);
852
+ entries.set(`directory:${child}`, freeze({
853
+ kind: "directory",
854
+ logicalPath: `${prefix}${child}`,
855
+ }));
856
+ }
857
+ else {
858
+ entries.set(`document:${relative}`, freeze({
859
+ kind: "document",
860
+ logicalPath: entry.file.path,
861
+ status: entry.descriptor !== undefined && entry.diagnostics.length === 0
862
+ ? "queryable"
863
+ : "damaged",
864
+ ...(entry.descriptor === undefined
865
+ ? {}
866
+ : { document: entry.descriptor }),
867
+ diagnostics: entry.diagnostics,
868
+ }));
869
+ }
870
+ }
871
+ const ordered = [...entries.values()].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
872
+ const cursorScope = JSON.stringify({
873
+ kind: "catalog",
874
+ layout: this.layout,
875
+ logicalRoot: this.logicalRoot,
876
+ directory,
877
+ limit,
878
+ });
879
+ const offset = this.#cursorOffset(request.cursor, cursorScope);
880
+ if (offset === null || offset > ordered.length)
881
+ return this.#cursorFailure("catalog", request);
882
+ const pageEntries = ordered.slice(offset, offset + limit);
883
+ const end = offset + pageEntries.length;
884
+ const complete = end >= ordered.length;
885
+ return freeze({
886
+ kind: "catalog",
887
+ ok: true,
888
+ snapshotId: this.id,
889
+ snapshotStatus: this.status,
890
+ scope: freeze({
891
+ layout: this.layout,
892
+ logicalRoot: this.logicalRoot,
893
+ directory,
894
+ }),
895
+ coverage: freeze({
896
+ status: "complete",
897
+ excludedDocuments: 0,
898
+ }),
899
+ page: freeze({
900
+ unit: "entries",
901
+ start: offset,
902
+ end,
903
+ total: ordered.length,
904
+ complete,
905
+ nextCursor: complete ? null : this.#cursor(cursorScope, end),
906
+ }),
907
+ entries: freeze(pageEntries),
908
+ diagnostics: freeze([]),
909
+ });
910
+ }
911
+ #literalSearch(request) {
912
+ if (!hasOnlyKeys(request, [
913
+ "kind",
914
+ "query",
915
+ "caseSensitive",
916
+ "within",
917
+ "limit",
918
+ "cursor",
919
+ ]))
920
+ return this.#failure("literal_search", request, [
921
+ diagnostic({
922
+ code: "query_invalid",
923
+ message: "literal_search contains undeclared query conditions",
924
+ }),
925
+ ]);
926
+ const caseSensitive = request.caseSensitive ?? false;
927
+ const limit = request.limit ?? 20;
928
+ if (typeof request.query !== "string" ||
929
+ request.query.length < 1 ||
930
+ request.query.length > 256 ||
931
+ !Number.isInteger(limit) ||
932
+ limit < 1 ||
933
+ limit > 100 ||
934
+ (request.caseSensitive !== undefined &&
935
+ typeof request.caseSensitive !== "boolean"))
936
+ return this.#failure("literal_search", request, [
937
+ diagnostic({
938
+ code: "query_invalid",
939
+ message: "literal_search requires a 1-to-256-character literal and a limit from 1 to 100",
940
+ }),
941
+ ]);
942
+ let candidates = this.#entries;
943
+ let within = null;
944
+ if (request.within !== undefined) {
945
+ if (!isRecord(request.within))
946
+ return this.#failure("literal_search", request, [
947
+ diagnostic({
948
+ code: "query_invalid",
949
+ message: "literal_search within must be a closed query scope",
950
+ }),
951
+ ]);
952
+ if ("directory" in request.within &&
953
+ Object.keys(request.within).length === 1 &&
954
+ typeof request.within.directory === "string") {
955
+ if (request.within.directory !== "" &&
956
+ !validRelativePath(request.within.directory))
957
+ return this.#failure("literal_search", request, [
958
+ diagnostic({
959
+ code: "query_invalid",
960
+ message: "literal_search directory must be a logical directory within the layout root",
961
+ }),
962
+ ]);
963
+ const relativePrefix = request.within.directory === "" ? "" : `${request.within.directory}/`;
964
+ const prefix = `${this.logicalRoot}/${relativePrefix}`;
965
+ candidates = candidates.filter(({ file }) => file.path.startsWith(prefix));
966
+ within = freeze({
967
+ directory: request.within.directory === ""
968
+ ? this.logicalRoot
969
+ : `${this.logicalRoot}/${request.within.directory}`,
970
+ });
971
+ }
972
+ else if ("document" in request.within &&
973
+ Object.keys(request.within).length === 1 &&
974
+ request.within.document !== undefined) {
975
+ const resolved = this.#resolveSelector(request.within.document);
976
+ if ("diagnostics" in resolved)
977
+ return this.#failure("literal_search", request, resolved.diagnostics, resolved.document);
978
+ candidates = [resolved.entry];
979
+ within = freeze({ document: resolved.entry.descriptor });
980
+ }
981
+ else
982
+ return this.#failure("literal_search", request, [
983
+ diagnostic({
984
+ code: "query_invalid",
985
+ message: "literal_search within must declare only directory or document",
986
+ }),
987
+ ]);
988
+ }
989
+ const query = normalizeSearchQuery(request.query, caseSensitive);
990
+ if (query.length === 0)
991
+ return this.#failure("literal_search", request, [
992
+ diagnostic({
993
+ code: "query_invalid",
994
+ message: "literal_search cannot be empty after normalization",
995
+ }),
996
+ ]);
997
+ const excluded = candidates.filter((entry) => entry.descriptor === undefined ||
998
+ literalSearchBlockingDiagnostics(entry).length > 0);
999
+ const matches = [];
1000
+ const descriptors = this.#descriptorIndex();
1001
+ for (const entry of candidates) {
1002
+ if (entry.descriptor === undefined ||
1003
+ literalSearchBlockingDiagnostics(entry).length > 0)
1004
+ continue;
1005
+ for (const searchableRange of entry.searchRanges ?? []) {
1006
+ const searchable = entry.file.contents.slice(searchableRange.start.offset, searchableRange.end.offset);
1007
+ for (const relativeMatch of literalMatches(searchable, query, caseSensitive)) {
1008
+ if (matches.length >= maxLiteralSearchMatches)
1009
+ return this.#failure("literal_search", request, [
1010
+ diagnostic({
1011
+ code: "capacity_exceeded",
1012
+ message: "Literal-search matches exceed the capacity of one snapshot query",
1013
+ }),
1014
+ ]);
1015
+ const match = {
1016
+ start: searchableRange.start.offset + relativeMatch.start,
1017
+ end: searchableRange.start.offset + relativeMatch.end,
1018
+ };
1019
+ const excerptRange = lineExcerptRange(entry.file.contents, match.start, match.end);
1020
+ const matchRange = sourceRange(entry.file.contents, match.start, match.end);
1021
+ matches.push({
1022
+ document: entry.descriptor,
1023
+ text: entry.file.contents.slice(match.start, match.end),
1024
+ range: matchRange,
1025
+ excerpt: freeze({
1026
+ text: entry.file.contents.slice(excerptRange.start.offset, excerptRange.end.offset),
1027
+ range: excerptRange,
1028
+ }),
1029
+ referenceProjection: freeze({
1030
+ text: projectExplicitReferenceValues(entry.file.contents, matchRange, entry.references, descriptors),
1031
+ excerpt: projectExplicitReferenceValues(entry.file.contents, excerptRange, entry.references, descriptors),
1032
+ }),
1033
+ });
1034
+ }
1035
+ }
1036
+ }
1037
+ const cursorScope = JSON.stringify({
1038
+ kind: "literal_search",
1039
+ layout: this.layout,
1040
+ logicalRoot: this.logicalRoot,
1041
+ query: request.query,
1042
+ caseSensitive,
1043
+ within: request.within ?? null,
1044
+ limit,
1045
+ });
1046
+ const offset = this.#cursorOffset(request.cursor, cursorScope);
1047
+ if (offset === null || offset > matches.length)
1048
+ return this.#cursorFailure("literal_search", request);
1049
+ const pageMatches = matches
1050
+ .slice(offset, offset + limit)
1051
+ .map((match) => freeze(match));
1052
+ const end = offset + pageMatches.length;
1053
+ const complete = end >= matches.length;
1054
+ const diagnostics = freeze(excluded.flatMap((entry) => entry.diagnostics));
1055
+ return freeze({
1056
+ kind: "literal_search",
1057
+ ok: true,
1058
+ snapshotId: this.id,
1059
+ snapshotStatus: this.status,
1060
+ scope: freeze({
1061
+ layout: this.layout,
1062
+ logicalRoot: this.logicalRoot,
1063
+ query: request.query,
1064
+ caseSensitive,
1065
+ within,
1066
+ }),
1067
+ coverage: freeze({
1068
+ status: excluded.length === 0 ? "complete" : "partial",
1069
+ excludedDocuments: excluded.length,
1070
+ }),
1071
+ page: freeze({
1072
+ unit: "matches",
1073
+ start: offset,
1074
+ end,
1075
+ total: matches.length,
1076
+ complete,
1077
+ nextCursor: complete ? null : this.#cursor(cursorScope, end),
1078
+ }),
1079
+ matches: freeze(pageMatches),
1080
+ diagnostics,
1081
+ });
1082
+ }
1083
+ #readDocument(request) {
1084
+ if (!hasOnlyKeys(request, ["kind", "document", "maxBytes", "cursor"]))
1085
+ return this.#failure("read_document", request, [
1086
+ diagnostic({
1087
+ code: "query_invalid",
1088
+ message: "read_document contains undeclared query conditions",
1089
+ }),
1090
+ ]);
1091
+ const maxBytes = request.maxBytes ?? 8192;
1092
+ if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > 65_536)
1093
+ return this.#failure("read_document", request, [
1094
+ diagnostic({
1095
+ code: "query_invalid",
1096
+ message: "read_document maxBytes must be 4 to 65536",
1097
+ }),
1098
+ ]);
1099
+ const resolved = this.#resolveSelector(request.document);
1100
+ if ("diagnostics" in resolved)
1101
+ return this.#failure("read_document", request, resolved.diagnostics, resolved.document);
1102
+ const bodyRange = resolved.entry.bodyRange;
1103
+ if (bodyRange === undefined)
1104
+ return this.#failure("read_document", request, [
1105
+ diagnostic({
1106
+ code: "document_header_invalid",
1107
+ logicalPath: resolved.entry.file.path,
1108
+ documentId: resolved.entry.descriptor.documentId,
1109
+ message: "World document has no readable body range",
1110
+ }),
1111
+ ], 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;
1127
+ return freeze({
1128
+ kind: "read_document",
1129
+ ok: true,
1130
+ snapshotId: this.id,
1131
+ snapshotStatus: this.status,
1132
+ scope: freeze({
1133
+ layout: this.layout,
1134
+ logicalRoot: this.logicalRoot,
1135
+ document: resolved.entry.descriptor,
1136
+ }),
1137
+ coverage: freeze({
1138
+ status: "complete",
1139
+ excludedDocuments: 0,
1140
+ }),
1141
+ document: resolved.entry.descriptor,
1142
+ 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
+ }),
1152
+ diagnostics: freeze([]),
1153
+ });
1154
+ }
1155
+ #selectNode(request) {
1156
+ if (!hasOnlyKeys(request, ["kind", "document", "locator"]))
1157
+ return this.#failure("select_node", request, [
1158
+ diagnostic({
1159
+ code: "query_invalid",
1160
+ message: "select_node contains undeclared query conditions",
1161
+ }),
1162
+ ]);
1163
+ if (!isRecord(request.locator))
1164
+ return this.#failure("select_node", request, [
1165
+ diagnostic({
1166
+ code: "query_invalid",
1167
+ message: "select_node must declare one closed locator",
1168
+ }),
1169
+ ]);
1170
+ const resolved = this.#resolveSelector(request.document);
1171
+ if ("diagnostics" in resolved)
1172
+ return this.#failure("select_node", request, resolved.diagnostics, resolved.document);
1173
+ const entry = resolved.entry;
1174
+ const locator = request.locator;
1175
+ if (entry.codec === "yaml" &&
1176
+ validYamlLocator(locator) &&
1177
+ entry.yaml !== undefined) {
1178
+ const publicLocator = freeze({ yaml: freeze([...locator.yaml]) });
1179
+ const damagedSelection = entry.diagnostics.filter((problem) => problem.code === "document_reference_invalid" &&
1180
+ problem.locator !== undefined &&
1181
+ "yaml" in problem.locator &&
1182
+ yamlLocatorsOverlap(locator.yaml, problem.locator.yaml));
1183
+ if (damagedSelection.length > 0)
1184
+ return this.#failure("select_node", request, damagedSelection, entry.descriptor);
1185
+ const value = locateYamlValue(entry.yaml.body, locator.yaml);
1186
+ const node = entry.yaml.nodes.find(({ locatorKey }) => locatorKey === yamlLocatorKey(locator.yaml));
1187
+ if (value === unresolved || node === undefined)
1188
+ return this.#locatorFailure(entry, publicLocator, "locator_not_found");
1189
+ 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
+ return this.#selected(entry, publicLocator, {
1205
+ codec: "yaml",
1206
+ locator: publicLocator,
1207
+ value: projected,
1208
+ range: node.range,
1209
+ }, this.#selectedReferences(entry, publicLocator));
1210
+ }
1211
+ if (entry.codec === "markdown" &&
1212
+ validMarkdownLocator(locator) &&
1213
+ entry.markdown !== undefined) {
1214
+ const publicLocator = freeze({
1215
+ markdown: freeze([...locator.markdown]),
1216
+ });
1217
+ const key = markdownLocatorKey(locator.markdown);
1218
+ const headings = entry.markdown.headings.filter(({ locatorKey }) => locatorKey === key);
1219
+ if (headings.length === 0)
1220
+ return this.#locatorFailure(entry, publicLocator, "locator_not_found");
1221
+ if (headings.length > 1)
1222
+ return this.#locatorFailure(entry, publicLocator, "locator_ambiguous");
1223
+ const heading = headings[0];
1224
+ 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
+ return this.#selected(entry, publicLocator, {
1237
+ codec: "markdown",
1238
+ locator: publicLocator,
1239
+ markdown,
1240
+ range: heading.range,
1241
+ }, []);
1242
+ }
1243
+ return this.#locatorFailure(entry, copyLocator(locator), "locator_invalid");
1244
+ }
1245
+ #selected(entry, locator, node, references) {
1246
+ return freeze({
1247
+ kind: "select_node",
1248
+ ok: true,
1249
+ snapshotId: this.id,
1250
+ snapshotStatus: this.status,
1251
+ scope: freeze({
1252
+ layout: this.layout,
1253
+ logicalRoot: this.logicalRoot,
1254
+ document: entry.descriptor,
1255
+ locator,
1256
+ }),
1257
+ coverage: freeze({
1258
+ status: "complete",
1259
+ excludedDocuments: 0,
1260
+ }),
1261
+ document: entry.descriptor,
1262
+ node: freeze(node),
1263
+ references: freeze(references),
1264
+ diagnostics: freeze([]),
1265
+ });
1266
+ }
1267
+ #selectedReferences(entry, locator) {
1268
+ const descriptors = this.#descriptorIndex();
1269
+ return entry.references.flatMap((reference) => {
1270
+ if (reference.targetId === null ||
1271
+ !("yaml" in reference.locator) ||
1272
+ !locatorStartsWith(reference.locator.yaml, locator.yaml))
1273
+ return [];
1274
+ const target = descriptors.get(reference.targetId);
1275
+ return target === undefined
1276
+ ? []
1277
+ : [
1278
+ freeze({
1279
+ locator: copyLocator(reference.locator),
1280
+ target,
1281
+ }),
1282
+ ];
1283
+ });
1284
+ }
1285
+ #locatorFailure(entry, locator, code) {
1286
+ return this.#failure("select_node", { document: entry.file.path, locator }, [
1287
+ diagnostic({
1288
+ code,
1289
+ logicalPath: entry.file.path,
1290
+ ...(entry.descriptor === undefined
1291
+ ? {}
1292
+ : { documentId: entry.descriptor.documentId }),
1293
+ locator,
1294
+ message: "Logical-node locator cannot be resolved uniquely for the target codec",
1295
+ }),
1296
+ ], entry.descriptor);
1297
+ }
1298
+ #resolveSelector(selector) {
1299
+ if (!isRecord(selector) || Object.keys(selector).length !== 1)
1300
+ return {
1301
+ diagnostics: freeze([
1302
+ diagnostic({
1303
+ code: "query_invalid",
1304
+ message: "A document selector must declare only documentId, shortRef, or logicalPath",
1305
+ }),
1306
+ ]),
1307
+ };
1308
+ let matches;
1309
+ if (typeof selector.documentId === "string" &&
1310
+ selector.documentId.length > 0)
1311
+ matches = this.#entries.filter(({ descriptor }) => descriptor?.documentId === selector.documentId);
1312
+ else if (typeof selector.shortRef === "string" &&
1313
+ selector.shortRef.length > 0)
1314
+ matches = this.#entries.filter(({ descriptor }) => descriptor?.shortRef === selector.shortRef);
1315
+ else if (typeof selector.logicalPath === "string" &&
1316
+ selector.logicalPath.length > 0)
1317
+ matches = this.#entries.filter(({ file }) => file.path === selector.logicalPath);
1318
+ else
1319
+ return {
1320
+ diagnostics: freeze([
1321
+ diagnostic({
1322
+ code: "query_invalid",
1323
+ message: "Document-selector value must be a non-empty string",
1324
+ }),
1325
+ ]),
1326
+ };
1327
+ if (matches.length === 0)
1328
+ return {
1329
+ diagnostics: freeze([
1330
+ diagnostic({
1331
+ code: "document_not_found",
1332
+ message: "Target world document does not exist in the snapshot",
1333
+ }),
1334
+ ]),
1335
+ };
1336
+ if (matches.length > 1)
1337
+ return {
1338
+ diagnostics: freeze([
1339
+ diagnostic({
1340
+ code: "document_ambiguous",
1341
+ message: "Target world document is not unique in the snapshot",
1342
+ }),
1343
+ ...matches.flatMap(({ diagnostics }) => diagnostics),
1344
+ ]),
1345
+ };
1346
+ const entry = matches[0];
1347
+ const blockingDiagnostics = blockingDocumentDiagnostics(entry);
1348
+ if (entry.descriptor === undefined || blockingDiagnostics.length > 0)
1349
+ return {
1350
+ diagnostics: blockingDiagnostics,
1351
+ ...(entry.descriptor === undefined
1352
+ ? {}
1353
+ : { document: entry.descriptor }),
1354
+ };
1355
+ return { entry };
1356
+ }
1357
+ #descriptorIndex() {
1358
+ const descriptors = this.#entries.flatMap(({ descriptor }) => descriptor === undefined ? [] : [descriptor]);
1359
+ return new Map([...groupBy(descriptors, ({ documentId }) => documentId).entries()]
1360
+ .filter(([, matches]) => matches.length === 1)
1361
+ .map(([documentId, matches]) => [documentId, matches[0]]));
1362
+ }
1363
+ #cursor(scope, offset) {
1364
+ const payload = Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
1365
+ const signature = createHmac("sha256", this.#cursorSecret)
1366
+ .update(this.id)
1367
+ .update("\0")
1368
+ .update(scope)
1369
+ .update("\0")
1370
+ .update(payload)
1371
+ .digest("base64url");
1372
+ return `${payload}.${signature}`;
1373
+ }
1374
+ #cursorOffset(cursor, scope) {
1375
+ if (cursor === undefined || cursor === null)
1376
+ return 0;
1377
+ if (typeof cursor !== "string")
1378
+ return null;
1379
+ const parts = cursor.split(".");
1380
+ if (parts.length !== 2)
1381
+ return null;
1382
+ const [payload, suppliedSignature] = parts;
1383
+ if (payload === undefined ||
1384
+ suppliedSignature === undefined ||
1385
+ !base64UrlPattern.test(payload) ||
1386
+ !base64UrlPattern.test(suppliedSignature))
1387
+ return null;
1388
+ const expectedSignature = createHmac("sha256", this.#cursorSecret)
1389
+ .update(this.id)
1390
+ .update("\0")
1391
+ .update(scope)
1392
+ .update("\0")
1393
+ .update(payload)
1394
+ .digest();
1395
+ let supplied;
1396
+ try {
1397
+ supplied = Buffer.from(suppliedSignature, "base64url");
1398
+ if (supplied.toString("base64url") !== suppliedSignature)
1399
+ return null;
1400
+ }
1401
+ catch {
1402
+ return null;
1403
+ }
1404
+ if (supplied.length !== expectedSignature.length ||
1405
+ !timingSafeEqual(supplied, expectedSignature))
1406
+ return null;
1407
+ try {
1408
+ const decodedPayload = Buffer.from(payload, "base64url");
1409
+ if (decodedPayload.toString("base64url") !== payload)
1410
+ return null;
1411
+ const value = JSON.parse(decodedPayload.toString("utf8"));
1412
+ return isRecord(value) &&
1413
+ Object.keys(value).length === 1 &&
1414
+ Number.isSafeInteger(value.offset) &&
1415
+ Number(value.offset) >= 0
1416
+ ? Number(value.offset)
1417
+ : null;
1418
+ }
1419
+ catch {
1420
+ return null;
1421
+ }
1422
+ }
1423
+ #cursorFailure(requestKind, scope) {
1424
+ return this.#failure(requestKind, scope, [
1425
+ diagnostic({
1426
+ code: "cursor_invalid",
1427
+ message: "Cursor does not match the current snapshot or all query conditions",
1428
+ }),
1429
+ ]);
1430
+ }
1431
+ #failure(requestKind, scope, diagnostics, document) {
1432
+ return freeze({
1433
+ kind: "error",
1434
+ ok: false,
1435
+ requestKind,
1436
+ snapshotId: this.id,
1437
+ snapshotStatus: this.status,
1438
+ scope: freeze(copyPublicValue(scope)),
1439
+ ...(document === undefined ? {} : { document }),
1440
+ diagnostics: freeze([...diagnostics]),
1441
+ });
1442
+ }
1443
+ #inspectFile(file) {
1444
+ const entry = {
1445
+ file,
1446
+ relativePath: file.path.slice(this.logicalRoot.length + 1),
1447
+ codec: null,
1448
+ references: [],
1449
+ diagnostics: [],
1450
+ };
1451
+ if (!validRelativePath(entry.relativePath)) {
1452
+ entry.diagnostics.push(diagnostic({
1453
+ code: "logical_path_invalid",
1454
+ logicalPath: file.path,
1455
+ message: "World-document logical path must remain within the layout root",
1456
+ }));
1457
+ return entry;
1458
+ }
1459
+ if (file.encoding !== undefined) {
1460
+ entry.diagnostics.push(diagnostic({
1461
+ code: "world_document_binary",
1462
+ logicalPath: file.path,
1463
+ range: sourceRange(file.contents, 0, file.contents.length),
1464
+ message: "World documents accept only UTF-8 YAML or Markdown source",
1465
+ }));
1466
+ return entry;
1467
+ }
1468
+ entry.codec = documentCodec(file.path);
1469
+ if (entry.codec === null) {
1470
+ entry.diagnostics.push(diagnostic({
1471
+ code: "world_document_codec_unsupported",
1472
+ logicalPath: file.path,
1473
+ range: sourceRange(file.contents, 0, file.contents.length),
1474
+ message: "World documents accept only .yaml, .yml, or .md codecs",
1475
+ }));
1476
+ return entry;
1477
+ }
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
+ if (entry.codec === "yaml")
1488
+ inspectYamlDocument(entry);
1489
+ else
1490
+ inspectMarkdownDocument(entry);
1491
+ return entry;
1492
+ }
1493
+ #diagnoseDuplicateLogicalPaths() {
1494
+ const groups = groupBy(this.#entries, ({ file }) => file.path);
1495
+ for (const entries of groups.values()) {
1496
+ if (entries.length < 2)
1497
+ continue;
1498
+ for (const entry of entries)
1499
+ entry.diagnostics.push(diagnostic({
1500
+ code: "logical_path_duplicate",
1501
+ logicalPath: entry.file.path,
1502
+ range: sourceRange(entry.file.contents, 0, entry.file.contents.length),
1503
+ message: "The fixed file set contains duplicate world-document logical paths",
1504
+ }));
1505
+ }
1506
+ }
1507
+ #diagnoseDuplicateHeaders() {
1508
+ const withDescriptors = this.#entries.filter((entry) => entry.descriptor !== undefined);
1509
+ for (const entries of groupBy(withDescriptors, ({ descriptor }) => descriptor.documentId).values()) {
1510
+ if (entries.length < 2)
1511
+ continue;
1512
+ for (const entry of entries)
1513
+ entry.diagnostics.push(diagnostic({
1514
+ code: "document_identity_duplicate",
1515
+ logicalPath: entry.file.path,
1516
+ documentId: entry.descriptor.documentId,
1517
+ ...(entry.headerRanges?.documentId === undefined
1518
+ ? {}
1519
+ : { range: entry.headerRanges.documentId }),
1520
+ message: "Document identity must be unique within the snapshot",
1521
+ }));
1522
+ }
1523
+ for (const entries of groupBy(withDescriptors, ({ descriptor }) => descriptor.shortRef).values()) {
1524
+ if (entries.length < 2)
1525
+ continue;
1526
+ for (const entry of entries)
1527
+ entry.diagnostics.push(diagnostic({
1528
+ code: "document_short_ref_duplicate",
1529
+ logicalPath: entry.file.path,
1530
+ documentId: entry.descriptor.documentId,
1531
+ ...(entry.headerRanges?.shortRef === undefined
1532
+ ? {}
1533
+ : { range: entry.headerRanges.shortRef }),
1534
+ message: "Document short reference must be unique within the snapshot",
1535
+ }));
1536
+ }
1537
+ }
1538
+ #diagnoseReferences() {
1539
+ const byId = groupBy(this.#entries.filter((entry) => entry.descriptor !== undefined), ({ descriptor }) => descriptor.documentId);
1540
+ for (const entry of this.#entries) {
1541
+ for (const reference of entry.references) {
1542
+ if (reference.targetId !== null &&
1543
+ byId.get(reference.targetId)?.length === 1)
1544
+ continue;
1545
+ entry.diagnostics.push(diagnostic({
1546
+ code: "document_reference_invalid",
1547
+ logicalPath: entry.file.path,
1548
+ ...(entry.descriptor === undefined
1549
+ ? {}
1550
+ : { documentId: entry.descriptor.documentId }),
1551
+ locator: reference.locator,
1552
+ range: reference.range,
1553
+ message: "An explicit $ref must uniquely identify a whole document in the snapshot",
1554
+ }));
1555
+ }
1556
+ }
1557
+ }
1558
+ }
1559
+ function inspectYamlDocument(entry) {
1560
+ const parsed = parseRestrictedYaml(entry, entry.file.contents, 0);
1561
+ if (parsed === null)
1562
+ return;
1563
+ const header = readHeader(entry, parsed, false);
1564
+ if (header === null)
1565
+ return;
1566
+ entry.descriptor = header.descriptor;
1567
+ entry.headerRanges = header.ranges;
1568
+ entry.bodyRange = yamlBodyRange(entry.file.contents, parsed.root);
1569
+ entry.searchRanges = freeze([
1570
+ ...headerSearchRanges(header.ranges),
1571
+ entry.bodyRange,
1572
+ ]);
1573
+ inspectYamlReferences(entry, parsed.root, []);
1574
+ entry.yaml = {
1575
+ body: freeze(Object.fromEntries(Object.entries(parsed.value).filter(([key]) => key !== "$document"))),
1576
+ nodes: freeze(indexYamlNodes(entry.file.contents, parsed.root)),
1577
+ };
1578
+ }
1579
+ function inspectMarkdownDocument(entry) {
1580
+ const source = entry.file.contents;
1581
+ if (!source.startsWith("---\n")) {
1582
+ entry.diagnostics.push(diagnostic({
1583
+ code: "markdown_invalid",
1584
+ logicalPath: entry.file.path,
1585
+ range: sourceRange(source, 0, Math.min(source.length, 3)),
1586
+ message: "Markdown must begin with $document front matter",
1587
+ }));
1588
+ return;
1589
+ }
1590
+ const end = source.indexOf("\n---\n", 4);
1591
+ if (end < 0) {
1592
+ entry.diagnostics.push(diagnostic({
1593
+ code: "markdown_invalid",
1594
+ logicalPath: entry.file.path,
1595
+ range: sourceRange(source, 0, Math.min(source.length, 3)),
1596
+ message: "Markdown front matter is not closed",
1597
+ }));
1598
+ return;
1599
+ }
1600
+ const parsed = parseRestrictedYaml(entry, source.slice(4, end), 4);
1601
+ if (parsed === null)
1602
+ return;
1603
+ const header = readHeader(entry, parsed, true);
1604
+ if (header === null)
1605
+ return;
1606
+ entry.descriptor = header.descriptor;
1607
+ entry.headerRanges = header.ranges;
1608
+ const bodyStart = end + 5;
1609
+ entry.bodyRange = sourceRange(source, bodyStart, source.length);
1610
+ entry.searchRanges = freeze([
1611
+ ...headerSearchRanges(header.ranges),
1612
+ entry.bodyRange,
1613
+ ]);
1614
+ const headings = scanMarkdownHeadings(source, bodyStart).headings;
1615
+ const topLevel = headings.filter(({ level }) => level === 1);
1616
+ if (headings.length === 0 ||
1617
+ headings[0].level !== 1 ||
1618
+ topLevel.length !== 1 ||
1619
+ headings[0].title.normalize("NFKC") !==
1620
+ header.descriptor.title.normalize("NFKC") ||
1621
+ headings.some((heading, index) => index > 0 && heading.level > headings[index - 1].level + 1))
1622
+ entry.diagnostics.push(diagnostic({
1623
+ code: "markdown_invalid",
1624
+ logicalPath: entry.file.path,
1625
+ documentId: header.descriptor.documentId,
1626
+ range: headings[0]?.range ?? sourceRange(source, bodyStart, source.length),
1627
+ message: "Markdown must have one h1 matching the technical title, and heading levels cannot be skipped",
1628
+ }));
1629
+ const seen = new Map();
1630
+ const indexed = [];
1631
+ const stack = [];
1632
+ for (const [index, heading] of headings.entries()) {
1633
+ if (heading.level === 1) {
1634
+ stack.length = 0;
1635
+ continue;
1636
+ }
1637
+ stack.length = heading.level - 2;
1638
+ stack[heading.level - 2] = heading.title.normalize("NFKC");
1639
+ const locator = [...stack];
1640
+ const key = JSON.stringify(locator);
1641
+ if (seen.has(key))
1642
+ entry.diagnostics.push(diagnostic({
1643
+ code: "locator_ambiguous",
1644
+ logicalPath: entry.file.path,
1645
+ documentId: header.descriptor.documentId,
1646
+ locator: { markdown: locator },
1647
+ range: heading.range,
1648
+ message: "Markdown heading paths must be unique within the document",
1649
+ }));
1650
+ else
1651
+ seen.set(key, heading.range);
1652
+ const next = headings
1653
+ .slice(index + 1)
1654
+ .find((candidate) => candidate.level <= heading.level);
1655
+ const rawEnd = next?.range.start.offset ?? source.length;
1656
+ const end = trimTrailingWhitespaceOffset(source, heading.range.start.offset, rawEnd);
1657
+ indexed.push({
1658
+ locator: freeze(locator),
1659
+ locatorKey: key,
1660
+ range: sourceRange(source, heading.range.start.offset, end),
1661
+ });
1662
+ }
1663
+ entry.markdown = { bodyStart, headings: freeze(indexed) };
1664
+ }
1665
+ function parseRevisionYamlBodySource(logicalPath, source) {
1666
+ const entry = {
1667
+ file: { path: logicalPath, contents: source },
1668
+ relativePath: logicalPath,
1669
+ codec: "yaml",
1670
+ references: [],
1671
+ diagnostics: [],
1672
+ };
1673
+ const parsed = parseRestrictedYaml(entry, source, 0);
1674
+ if (parsed === null || entry.diagnostics.length > 0)
1675
+ return { diagnostics: freeze([...entry.diagnostics]) };
1676
+ if (Object.hasOwn(parsed.value, "$document"))
1677
+ return {
1678
+ diagnostics: freeze([
1679
+ diagnostic({
1680
+ code: "yaml_invalid",
1681
+ logicalPath,
1682
+ message: "Create YAML body cannot include a $document technical header",
1683
+ }),
1684
+ ]),
1685
+ };
1686
+ return { value: parsed.value };
1687
+ }
1688
+ // Splits one complete document source into the metadata the author owns and
1689
+ // the body it wrote. `id` is deliberately not read back: the store assigns it.
1690
+ // A source with no `$document` at all is what read_document hands back, so
1691
+ // overwriting an existing document may omit it and inherit that document's
1692
+ // metadata; only a new document has nothing to inherit and must supply one.
1693
+ function parseWriteDocumentSource(logicalPath, codec, source, existing) {
1694
+ let header;
1695
+ let body;
1696
+ if (codec === "yaml") {
1697
+ const entry = {
1698
+ file: { path: logicalPath, contents: source },
1699
+ relativePath: logicalPath,
1700
+ codec: "yaml",
1701
+ references: [],
1702
+ diagnostics: [],
1703
+ };
1704
+ const parsed = parseRestrictedYaml(entry, source, 0);
1705
+ if (parsed === null || entry.diagnostics.length > 0)
1706
+ return { diagnostics: freeze([...entry.diagnostics]) };
1707
+ const { $document, ...rest } = parsed.value;
1708
+ header = $document;
1709
+ body = rest;
1710
+ }
1711
+ else if (source.startsWith("---\n")) {
1712
+ const envelope = markdownEnvelope(source);
1713
+ if ("diagnostics" in envelope)
1714
+ return {
1715
+ diagnostics: freeze(envelope.diagnostics.map((problem) => ({ ...problem, logicalPath }))),
1716
+ };
1717
+ const parsedHeader = envelope.header.toJSON();
1718
+ header = isRecord(parsedHeader) ? parsedHeader.$document : undefined;
1719
+ body = source.slice(envelope.bodyStart);
1720
+ }
1721
+ else {
1722
+ header = undefined;
1723
+ body = source;
1724
+ }
1725
+ if (header === undefined && existing !== undefined)
1726
+ return {
1727
+ metadata: {
1728
+ title: existing.title,
1729
+ summary: existing.summary,
1730
+ aliases: [...existing.aliases],
1731
+ },
1732
+ refHint: existing.shortRef,
1733
+ authoredId: existing.documentId,
1734
+ body,
1735
+ };
1736
+ if (!isRecord(header))
1737
+ return {
1738
+ diagnostics: freeze([
1739
+ diagnostic({
1740
+ code: "document_header_invalid",
1741
+ logicalPath,
1742
+ message: "Write source for a new document must begin with a $document technical header; replacing an existing document may omit it to preserve title, summary, and aliases",
1743
+ }),
1744
+ ]),
1745
+ };
1746
+ const normalized = { ...header, aliases: normalizeAliases(header.aliases) };
1747
+ if (!validDocumentMetadata(normalized))
1748
+ return {
1749
+ diagnostics: freeze([
1750
+ diagnostic({
1751
+ code: "document_header_invalid",
1752
+ logicalPath,
1753
+ message: `$document technical header is invalid: ${documentMetadataProblems(normalized).join("; ")}`,
1754
+ }),
1755
+ ]),
1756
+ };
1757
+ const { ref } = header;
1758
+ if (typeof ref !== "string" ||
1759
+ ref.length < 2 ||
1760
+ ref.length > 32 ||
1761
+ !shortRefPattern.test(ref))
1762
+ return {
1763
+ diagnostics: freeze([
1764
+ diagnostic({
1765
+ code: "document_short_ref_invalid",
1766
+ logicalPath,
1767
+ message: `$document.ref must be a 2-to-32-character short reference containing only lowercase letters, digits, and hyphens; received ${describeValue(ref)}`,
1768
+ }),
1769
+ ]),
1770
+ };
1771
+ const { id } = header;
1772
+ return {
1773
+ metadata: {
1774
+ title: normalized.title,
1775
+ summary: normalized.summary,
1776
+ aliases: normalized.aliases,
1777
+ },
1778
+ refHint: ref,
1779
+ authoredId: typeof id === "string" && id.length > 0 ? id : null,
1780
+ body,
1781
+ };
1782
+ }
1783
+ function parseRestrictedYaml(entry, source, baseOffset) {
1784
+ const forbidden = forbiddenYamlRange(source);
1785
+ if (forbidden !== null) {
1786
+ entry.diagnostics.push(diagnostic({
1787
+ code: "yaml_invalid",
1788
+ logicalPath: entry.file.path,
1789
+ range: sourceRange(entry.file.contents, baseOffset + forbidden.start, baseOffset + forbidden.end),
1790
+ message: "YAML uses a forbidden tag, anchor, alias, merge, multiple documents, or invalid line break",
1791
+ }));
1792
+ return null;
1793
+ }
1794
+ const document = parseDocument(source, {
1795
+ schema: "core",
1796
+ uniqueKeys: true,
1797
+ strict: true,
1798
+ });
1799
+ if (document.errors.length > 0 || document.warnings.length > 0) {
1800
+ for (const problem of [...document.errors, ...document.warnings])
1801
+ entry.diagnostics.push(diagnostic({
1802
+ code: "yaml_invalid",
1803
+ logicalPath: entry.file.path,
1804
+ range: sourceRange(entry.file.contents, baseOffset + problem.pos[0], baseOffset + problem.pos[1]),
1805
+ message: "YAML is not a safe single-document YAML 1.2 core map",
1806
+ }));
1807
+ return null;
1808
+ }
1809
+ if (!isMap(document.contents)) {
1810
+ entry.diagnostics.push(diagnostic({
1811
+ code: "yaml_invalid",
1812
+ logicalPath: entry.file.path,
1813
+ range: sourceRange(entry.file.contents, baseOffset, baseOffset + source.length),
1814
+ message: "Top-level YAML must be a map",
1815
+ }));
1816
+ return null;
1817
+ }
1818
+ const value = document.toJS({ maxAliasCount: 0 });
1819
+ if (!isRecord(value))
1820
+ return null;
1821
+ const shape = inspectShape(value);
1822
+ if (shape.capacityExceeded)
1823
+ entry.diagnostics.push(diagnostic({
1824
+ code: "capacity_exceeded",
1825
+ logicalPath: entry.file.path,
1826
+ range: nodeRange(entry.file.contents, document.contents, baseOffset),
1827
+ message: "YAML depth or node count exceeds world-document capacity",
1828
+ }));
1829
+ if (shape.invalidNumber)
1830
+ entry.diagnostics.push(diagnostic({
1831
+ code: "yaml_invalid",
1832
+ logicalPath: entry.file.path,
1833
+ range: nodeRange(entry.file.contents, document.contents, baseOffset),
1834
+ message: "YAML numbers must be finite",
1835
+ }));
1836
+ return {
1837
+ root: document.contents,
1838
+ value,
1839
+ baseOffset,
1840
+ };
1841
+ }
1842
+ function readHeader(entry, parsed, headerOnly) {
1843
+ const first = parsed.root.items[0];
1844
+ if (first === undefined ||
1845
+ !scalarEquals(first.key, "$document") ||
1846
+ !isMap(first.value)) {
1847
+ entry.diagnostics.push(diagnostic({
1848
+ code: "document_header_invalid",
1849
+ logicalPath: entry.file.path,
1850
+ range: nodeRange(entry.file.contents, parsed.root, parsed.baseOffset),
1851
+ message: "$document must be the first document entry",
1852
+ }));
1853
+ return null;
1854
+ }
1855
+ const header = first.value;
1856
+ const pairs = stringPairMap(header);
1857
+ const allowed = new Set(["id", "ref", "title", "summary", "aliases"]);
1858
+ const exactShape = pairs.size === allowed.size &&
1859
+ [...pairs.keys()].every((key) => allowed.has(key)) &&
1860
+ (!headerOnly || parsed.root.items.length === 1);
1861
+ const idPair = pairs.get("id");
1862
+ const refPair = pairs.get("ref");
1863
+ const titlePair = pairs.get("title");
1864
+ const summaryPair = pairs.get("summary");
1865
+ const aliasesPair = pairs.get("aliases");
1866
+ const id = scalarString(idPair?.value);
1867
+ const shortRef = scalarString(refPair?.value);
1868
+ const title = scalarString(titlePair?.value);
1869
+ const summary = scalarString(summaryPair?.value);
1870
+ const aliases = stringSequence(aliasesPair?.value);
1871
+ let valid = exactShape;
1872
+ if (id === null ||
1873
+ id.length < 3 ||
1874
+ id.length > 128 ||
1875
+ !documentIdPattern.test(id)) {
1876
+ valid = false;
1877
+ entry.diagnostics.push(diagnostic({
1878
+ code: "document_identity_invalid",
1879
+ logicalPath: entry.file.path,
1880
+ ...(idPair?.value === undefined
1881
+ ? {}
1882
+ : {
1883
+ range: nodeRange(entry.file.contents, idPair.value, parsed.baseOffset),
1884
+ }),
1885
+ message: "$document.id must be a stable lowercase identity within the world",
1886
+ }));
1887
+ }
1888
+ if (shortRef === null ||
1889
+ shortRef.length < 2 ||
1890
+ shortRef.length > 32 ||
1891
+ !shortRefPattern.test(shortRef)) {
1892
+ valid = false;
1893
+ entry.diagnostics.push(diagnostic({
1894
+ code: "document_short_ref_invalid",
1895
+ logicalPath: entry.file.path,
1896
+ ...(refPair?.value === undefined
1897
+ ? {}
1898
+ : {
1899
+ range: nodeRange(entry.file.contents, refPair.value, parsed.baseOffset),
1900
+ }),
1901
+ message: "$document.ref must be a stable lowercase short reference",
1902
+ }));
1903
+ }
1904
+ if (title === null ||
1905
+ [...title].length < 1 ||
1906
+ [...title].length > 120 ||
1907
+ summary === null ||
1908
+ [...summary].length < 1 ||
1909
+ [...summary].length > 240 ||
1910
+ aliases === null ||
1911
+ aliases.length > 16 ||
1912
+ aliases.some((alias) => [...alias].length < 1 || [...alias].length > 64))
1913
+ valid = false;
1914
+ if (!valid) {
1915
+ entry.diagnostics.push(diagnostic({
1916
+ code: "document_header_invalid",
1917
+ logicalPath: entry.file.path,
1918
+ ...(id !== null && documentIdPattern.test(id)
1919
+ ? { documentId: id }
1920
+ : {}),
1921
+ range: nodeRange(entry.file.contents, header, parsed.baseOffset),
1922
+ message: "$document must contain only valid id, ref, title, summary, and aliases",
1923
+ }));
1924
+ return null;
1925
+ }
1926
+ const descriptor = freeze({
1927
+ documentId: id,
1928
+ shortRef: shortRef,
1929
+ title: title,
1930
+ summary: summary,
1931
+ aliases: freeze(aliases),
1932
+ codec: entry.codec,
1933
+ logicalPath: entry.file.path,
1934
+ });
1935
+ return {
1936
+ descriptor,
1937
+ ranges: freeze({
1938
+ ...(idPair?.value === undefined
1939
+ ? {}
1940
+ : {
1941
+ documentId: nodeRange(entry.file.contents, idPair.value, parsed.baseOffset),
1942
+ }),
1943
+ ...(refPair?.value === undefined
1944
+ ? {}
1945
+ : {
1946
+ shortRef: nodeRange(entry.file.contents, refPair.value, parsed.baseOffset),
1947
+ }),
1948
+ ...(titlePair?.value === undefined
1949
+ ? {}
1950
+ : {
1951
+ title: nodeRange(entry.file.contents, titlePair.value, parsed.baseOffset),
1952
+ }),
1953
+ ...(summaryPair?.value === undefined
1954
+ ? {}
1955
+ : {
1956
+ summary: nodeRange(entry.file.contents, summaryPair.value, parsed.baseOffset),
1957
+ }),
1958
+ ...(aliasesPair?.value === undefined
1959
+ ? {}
1960
+ : {
1961
+ aliases: nodeRange(entry.file.contents, aliasesPair.value, parsed.baseOffset),
1962
+ }),
1963
+ }),
1964
+ };
1965
+ }
1966
+ function inspectYamlReferences(entry, node, locator) {
1967
+ if (isMap(node)) {
1968
+ const refPair = node.items.find(({ key }) => scalarEquals(key, "$ref"));
1969
+ if (refPair !== undefined) {
1970
+ const target = scalarString(refPair.value);
1971
+ const startNode = refPair.key;
1972
+ const endNode = refPair.value ?? refPair.key;
1973
+ entry.references.push({
1974
+ targetId: node.items.length === 1 ? target : null,
1975
+ locator: freeze({ yaml: freeze([...locator]) }),
1976
+ range: joinedNodeRange(entry.file.contents, startNode, endNode, 0),
1977
+ valueRange: nodeRange(entry.file.contents, refPair.value ?? refPair.key, 0),
1978
+ });
1979
+ return;
1980
+ }
1981
+ for (const pair of node.items) {
1982
+ const key = scalarString(pair.key);
1983
+ if (key === null) {
1984
+ entry.diagnostics.push(diagnostic({
1985
+ code: "yaml_invalid",
1986
+ logicalPath: entry.file.path,
1987
+ ...(entry.descriptor === undefined
1988
+ ? {}
1989
+ : { documentId: entry.descriptor.documentId }),
1990
+ range: nodeRange(entry.file.contents, pair.key, 0),
1991
+ message: "YAML map keys must be strings",
1992
+ }));
1993
+ continue;
1994
+ }
1995
+ if (locator.length === 0 && key === "$document")
1996
+ continue;
1997
+ inspectYamlReferences(entry, pair.value, [...locator, key]);
1998
+ }
1999
+ return;
2000
+ }
2001
+ if (isSeq(node))
2002
+ node.items.forEach((child, index) => inspectYamlReferences(entry, child, [...locator, index]));
2003
+ }
2004
+ function projectExplicitReferenceValues(source, selectedRange, references, descriptors) {
2005
+ const selectedStart = selectedRange.start.offset;
2006
+ const selectedEnd = selectedRange.end.offset;
2007
+ const replacements = references
2008
+ .flatMap((reference) => {
2009
+ const descriptor = reference.targetId === null
2010
+ ? undefined
2011
+ : descriptors.get(reference.targetId);
2012
+ const start = Math.max(selectedStart, reference.valueRange.start.offset);
2013
+ const end = Math.min(selectedEnd, reference.valueRange.end.offset);
2014
+ return start < end
2015
+ ? [
2016
+ {
2017
+ start,
2018
+ end,
2019
+ projection: descriptor === undefined
2020
+ ? "(invalid document reference)"
2021
+ : `@${descriptor.shortRef}`,
2022
+ },
2023
+ ]
2024
+ : [];
2025
+ })
2026
+ .sort((left, right) => left.start - right.start);
2027
+ let cursor = selectedStart;
2028
+ let projected = "";
2029
+ for (const replacement of replacements) {
2030
+ if (replacement.start < cursor)
2031
+ continue;
2032
+ projected += source.slice(cursor, replacement.start);
2033
+ projected += replacement.projection;
2034
+ cursor = replacement.end;
2035
+ }
2036
+ return projected + source.slice(cursor, selectedEnd);
2037
+ }
2038
+ function yamlBodyRange(source, root) {
2039
+ const firstBodyPair = root.items.find(({ key }) => !scalarEquals(key, "$document"));
2040
+ const start = firstBodyPair === undefined || !isRangedNode(firstBodyPair.key)
2041
+ ? source.length
2042
+ : (firstBodyPair.key.range?.[0] ?? source.length);
2043
+ return sourceRange(source, start, source.length);
2044
+ }
2045
+ function headerSearchRanges(ranges) {
2046
+ return [ranges.title, ranges.summary, ranges.aliases].filter((range) => range !== undefined);
2047
+ }
2048
+ function indexYamlNodes(source, root) {
2049
+ const result = [];
2050
+ const bodyPairs = root.items.filter(({ key }) => !scalarEquals(key, "$document"));
2051
+ if (bodyPairs.length === 0)
2052
+ result.push({
2053
+ locatorKey: yamlLocatorKey([]),
2054
+ range: sourceRange(source, source.length, source.length),
2055
+ });
2056
+ else {
2057
+ const first = bodyPairs[0];
2058
+ const last = bodyPairs.at(-1);
2059
+ result.push({
2060
+ locatorKey: yamlLocatorKey([]),
2061
+ range: joinedNodeRange(source, first.key, last.value ?? last.key, 0),
2062
+ });
2063
+ }
2064
+ const visit = (node, locator) => {
2065
+ if (isMap(node)) {
2066
+ if (node.items.some(({ key }) => scalarEquals(key, "$ref")))
2067
+ return;
2068
+ for (const pair of node.items) {
2069
+ const key = scalarString(pair.key);
2070
+ if (key === null || (locator.length === 0 && key === "$document"))
2071
+ continue;
2072
+ const childLocator = [...locator, key];
2073
+ result.push({
2074
+ locatorKey: yamlLocatorKey(childLocator),
2075
+ range: nodeRange(source, pair.value ?? pair.key, 0),
2076
+ });
2077
+ visit(pair.value, childLocator);
2078
+ }
2079
+ return;
2080
+ }
2081
+ if (isSeq(node))
2082
+ node.items.forEach((child, index) => {
2083
+ const childLocator = [...locator, index];
2084
+ result.push({
2085
+ locatorKey: yamlLocatorKey(childLocator),
2086
+ range: nodeRange(source, child, 0),
2087
+ });
2088
+ visit(child, childLocator);
2089
+ });
2090
+ };
2091
+ visit(root, []);
2092
+ return result;
2093
+ }
2094
+ const unresolved = Symbol("world-document-node-unresolved");
2095
+ function locateYamlValue(body, locator) {
2096
+ let current = body;
2097
+ for (const segment of locator) {
2098
+ if (typeof segment === "string" &&
2099
+ isRecord(current) &&
2100
+ Object.hasOwn(current, segment))
2101
+ current = current[segment];
2102
+ else if (typeof segment === "number" &&
2103
+ Array.isArray(current) &&
2104
+ segment >= 0 &&
2105
+ segment < current.length)
2106
+ current = current[segment];
2107
+ else
2108
+ return unresolved;
2109
+ }
2110
+ return current;
2111
+ }
2112
+ function projectYamlValue(value, documents) {
2113
+ if (value === null ||
2114
+ typeof value === "string" ||
2115
+ typeof value === "number" ||
2116
+ typeof value === "boolean")
2117
+ return value;
2118
+ if (Array.isArray(value))
2119
+ return freeze(value.map((child) => projectYamlValue(child, documents)));
2120
+ if (isRecord(value)) {
2121
+ if (Object.keys(value).length === 1 && typeof value.$ref === "string") {
2122
+ const target = documents.get(value.$ref);
2123
+ if (target !== undefined)
2124
+ return freeze({
2125
+ $ref: value.$ref,
2126
+ target: freeze({
2127
+ documentId: target.documentId,
2128
+ shortRef: target.shortRef,
2129
+ title: target.title,
2130
+ }),
2131
+ });
2132
+ }
2133
+ return freeze(Object.fromEntries(Object.entries(value)
2134
+ .filter(([key]) => key !== "$document")
2135
+ .map(([key, child]) => [key, projectYamlValue(child, documents)])));
2136
+ }
2137
+ return null;
2138
+ }
2139
+ function yamlLocatorKey(locator) {
2140
+ return JSON.stringify(locator);
2141
+ }
2142
+ function markdownLocatorKey(locator) {
2143
+ return JSON.stringify(locator.map((part) => part.normalize("NFKC")));
2144
+ }
2145
+ function trimTrailingWhitespaceOffset(source, start, rawEnd) {
2146
+ let end = rawEnd;
2147
+ while (end > start && /\s/u.test(source[end - 1]))
2148
+ end -= 1;
2149
+ return end;
2150
+ }
2151
+ function normalizeSearchQuery(value, caseSensitive) {
2152
+ const normalized = value.normalize("NFKC");
2153
+ return caseSensitive ? normalized : normalized.toLocaleLowerCase("und");
2154
+ }
2155
+ function literalMatches(source, normalizedQuery, caseSensitive) {
2156
+ const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
2157
+ let normalized = "";
2158
+ const starts = [];
2159
+ const ends = [];
2160
+ for (const segment of segmenter.segment(source)) {
2161
+ const folded = normalizeSearchQuery(segment.segment, caseSensitive);
2162
+ normalized += folded;
2163
+ let remainingCodeUnits = folded.length;
2164
+ while (remainingCodeUnits > 0) {
2165
+ starts.push(segment.index);
2166
+ ends.push(segment.index + segment.segment.length);
2167
+ remainingCodeUnits -= 1;
2168
+ }
2169
+ }
2170
+ const result = [];
2171
+ const seen = new Set();
2172
+ let from = 0;
2173
+ while (from <= normalized.length - normalizedQuery.length) {
2174
+ const found = normalized.indexOf(normalizedQuery, from);
2175
+ if (found < 0)
2176
+ break;
2177
+ const start = starts[found];
2178
+ const end = ends[found + normalizedQuery.length - 1];
2179
+ if (start !== undefined && end !== undefined) {
2180
+ const key = `${start}:${end}`;
2181
+ if (!seen.has(key)) {
2182
+ seen.add(key);
2183
+ result.push({ start, end });
2184
+ }
2185
+ }
2186
+ from = found + Math.max(1, normalizedQuery.length);
2187
+ }
2188
+ return result;
2189
+ }
2190
+ function lineExcerptRange(source, matchStart, matchEnd) {
2191
+ const lineStart = source.lastIndexOf("\n", Math.max(0, matchStart - 1)) + 1;
2192
+ const followingNewline = source.indexOf("\n", matchEnd);
2193
+ const lineEnd = followingNewline < 0 ? source.length : followingNewline;
2194
+ const start = Math.max(lineStart, matchStart - 160);
2195
+ const end = Math.min(lineEnd, matchEnd + 160);
2196
+ return sourceRange(source, start, end);
2197
+ }
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
+ function scanMarkdownHeadings(source, bodyStart) {
2205
+ const headings = [];
2206
+ let fence = null;
2207
+ let lineStart = bodyStart;
2208
+ while (lineStart < source.length) {
2209
+ const nextNewline = source.indexOf("\n", lineStart);
2210
+ const lineEnd = nextNewline < 0 ? source.length : nextNewline;
2211
+ const line = source.slice(lineStart, lineEnd);
2212
+ const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line);
2213
+ if (fence !== null) {
2214
+ const closing = /^ {0,3}(`+|~+)[ \t]*$/u.exec(line);
2215
+ if (closing !== null &&
2216
+ closing[1].startsWith(fence.marker) &&
2217
+ closing[1].length >= fence.length)
2218
+ fence = null;
2219
+ }
2220
+ else if (fenceMatch !== null) {
2221
+ const run = fenceMatch[1];
2222
+ fence = {
2223
+ marker: run[0],
2224
+ length: run.length,
2225
+ };
2226
+ }
2227
+ else {
2228
+ const heading = /^ {0,3}(#{1,6})(?:[ \t]+(.*?)|[ \t]*)$/u.exec(line);
2229
+ if (heading !== null) {
2230
+ const rawTitle = (heading[2] ?? "").replace(/[ \t]+#+[ \t]*$/u, "");
2231
+ headings.push({
2232
+ level: heading[1].length,
2233
+ title: rawTitle.trim(),
2234
+ range: sourceRange(source, lineStart, lineEnd),
2235
+ });
2236
+ }
2237
+ }
2238
+ if (nextNewline < 0)
2239
+ break;
2240
+ lineStart = nextNewline + 1;
2241
+ }
2242
+ return { headings, hasUnclosedFence: fence !== null };
2243
+ }
2244
+ function patchRevisionMarkdown(source, edits) {
2245
+ let contents = source;
2246
+ for (const edit of edits) {
2247
+ if (!isRecord(edit) || typeof edit.op !== "string")
2248
+ return {
2249
+ diagnostics: [
2250
+ diagnostic({
2251
+ code: "query_invalid",
2252
+ message: "A Markdown patch edit must be a closed object with op",
2253
+ }),
2254
+ ],
2255
+ };
2256
+ if (edit.op === "set_metadata") {
2257
+ if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]) ||
2258
+ !validDocumentMetadata(edit))
2259
+ return {
2260
+ diagnostics: [
2261
+ diagnostic({
2262
+ 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",
2266
+ }),
2267
+ ],
2268
+ };
2269
+ const envelope = markdownEnvelope(contents);
2270
+ if ("diagnostics" in envelope)
2271
+ 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]);
2275
+ const body = contents.slice(envelope.bodyStart);
2276
+ const firstHeading = scanMarkdownHeadings(body, 0).headings[0];
2277
+ if (firstHeading?.level !== 1)
2278
+ return {
2279
+ diagnostics: [
2280
+ diagnostic({
2281
+ code: "markdown_invalid",
2282
+ message: "Markdown body is missing an updateable h1",
2283
+ }),
2284
+ ],
2285
+ };
2286
+ const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${edit.title}${body.slice(firstHeading.range.end.offset)}`;
2287
+ contents = `---\n${envelope.header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${nextBody}`;
2288
+ continue;
2289
+ }
2290
+ if (edit.op === "replace_body" || edit.op === "replace_preamble") {
2291
+ if (!hasOnlyKeys(edit, ["op", "markdown"]) ||
2292
+ typeof edit.markdown !== "string")
2293
+ return {
2294
+ diagnostics: [
2295
+ diagnostic({
2296
+ code: "query_invalid",
2297
+ message: "A Markdown body edit requires markdown source",
2298
+ }),
2299
+ ],
2300
+ };
2301
+ const envelope = markdownEnvelope(contents);
2302
+ if ("diagnostics" in envelope)
2303
+ return envelope;
2304
+ if (edit.op === "replace_body") {
2305
+ contents = `${contents.slice(0, envelope.bodyStart)}${edit.markdown.trimEnd()}\n`;
2306
+ continue;
2307
+ }
2308
+ const headings = scanMarkdownHeadings(contents, envelope.bodyStart).headings;
2309
+ const h1 = headings.find(({ level }) => level === 1);
2310
+ if (h1 === undefined)
2311
+ return {
2312
+ diagnostics: [
2313
+ diagnostic({
2314
+ code: "markdown_invalid",
2315
+ message: "Markdown body is missing an updateable h1",
2316
+ }),
2317
+ ],
2318
+ };
2319
+ const nextH2 = headings.find(({ level, range }) => level === 2 && range.start.offset > h1.range.start.offset);
2320
+ const end = nextH2?.range.start.offset ?? contents.length;
2321
+ contents = `${contents.slice(0, h1.range.end.offset)}\n\n${edit.markdown.trim()}\n\n${contents.slice(end)}`;
2322
+ continue;
2323
+ }
2324
+ if (![
2325
+ "replace_section",
2326
+ "add_section",
2327
+ "rename_section",
2328
+ "remove_section",
2329
+ ].includes(edit.op) ||
2330
+ !validMarkdownLocator(edit.locator))
2331
+ return {
2332
+ diagnostics: [
2333
+ diagnostic({
2334
+ code: "locator_invalid",
2335
+ ...(isRecord(edit.locator)
2336
+ ? {
2337
+ locator: copyLocator(edit.locator),
2338
+ }
2339
+ : {}),
2340
+ message: "A Markdown patch requires an exact heading locator",
2341
+ }),
2342
+ ],
2343
+ };
2344
+ const locator = { markdown: [...edit.locator.markdown] };
2345
+ const section = markdownRevisionSection(contents, locator.markdown);
2346
+ if ("diagnostics" in section)
2347
+ return section;
2348
+ if (edit.op === "remove_section") {
2349
+ if (!hasOnlyKeys(edit, ["op", "locator"]))
2350
+ return {
2351
+ diagnostics: [
2352
+ diagnostic({
2353
+ code: "query_invalid",
2354
+ locator,
2355
+ message: "remove_section contains undeclared arguments",
2356
+ }),
2357
+ ],
2358
+ };
2359
+ contents = `${contents.slice(0, section.start)}${contents.slice(section.rawEnd)}`;
2360
+ continue;
2361
+ }
2362
+ if (edit.op === "rename_section") {
2363
+ if (!hasOnlyKeys(edit, ["op", "locator", "title"]) ||
2364
+ !validMarkdownHeadingTitle(edit.title))
2365
+ return {
2366
+ diagnostics: [
2367
+ diagnostic({
2368
+ code: "query_invalid",
2369
+ locator,
2370
+ message: "rename_section requires a valid new heading",
2371
+ }),
2372
+ ],
2373
+ };
2374
+ contents = `${contents.slice(0, section.start)}${"#".repeat(section.level)} ${edit.title}${contents.slice(section.headingEnd)}`;
2375
+ continue;
2376
+ }
2377
+ if (!hasOnlyKeys(edit, ["op", "locator", "markdown"]) ||
2378
+ typeof edit.markdown !== "string")
2379
+ return {
2380
+ diagnostics: [
2381
+ diagnostic({
2382
+ code: "query_invalid",
2383
+ locator,
2384
+ message: "A Markdown section edit requires a complete Markdown block",
2385
+ }),
2386
+ ],
2387
+ };
2388
+ const block = edit.markdown.trim();
2389
+ const { headings, hasUnclosedFence } = scanMarkdownHeadings(block, 0);
2390
+ const first = headings[0];
2391
+ const escapesSectionSubtree = headings
2392
+ .slice(1)
2393
+ .some(({ level }) => first !== undefined && level <= first.level);
2394
+ if (edit.op === "replace_section") {
2395
+ if (first?.range.start.offset !== 0 ||
2396
+ first.level !== section.level ||
2397
+ first.title.normalize("NFKC") !==
2398
+ locator.markdown.at(-1).normalize("NFKC") ||
2399
+ escapesSectionSubtree ||
2400
+ hasUnclosedFence)
2401
+ return {
2402
+ diagnostics: [
2403
+ diagnostic({
2404
+ code: "locator_invalid",
2405
+ locator,
2406
+ message: "replace_section first heading must preserve the exact original heading path",
2407
+ }),
2408
+ ],
2409
+ };
2410
+ contents = `${contents.slice(0, section.start)}${block}${contents.slice(section.trimmedEnd)}`;
2411
+ }
2412
+ else {
2413
+ if (first?.range.start.offset !== 0 ||
2414
+ first.level !== section.level + 1 ||
2415
+ escapesSectionSubtree ||
2416
+ hasUnclosedFence)
2417
+ return {
2418
+ diagnostics: [
2419
+ diagnostic({
2420
+ code: "locator_invalid",
2421
+ locator,
2422
+ message: "add_section must add a complete block with a heading one level lower",
2423
+ }),
2424
+ ],
2425
+ };
2426
+ contents = `${contents.slice(0, section.trimmedEnd)}\n\n${block}${contents.slice(section.trimmedEnd)}`;
2427
+ }
2428
+ }
2429
+ return { contents: contents.trimEnd().concat("\n") };
2430
+ }
2431
+ function markdownEnvelope(source) {
2432
+ if (!source.startsWith("---\n"))
2433
+ return {
2434
+ diagnostics: [
2435
+ diagnostic({
2436
+ code: "markdown_invalid",
2437
+ message: "Markdown must begin with technical front matter",
2438
+ }),
2439
+ ],
2440
+ };
2441
+ const end = source.indexOf("\n---\n", 4);
2442
+ if (end < 0)
2443
+ return {
2444
+ diagnostics: [
2445
+ diagnostic({
2446
+ code: "markdown_invalid",
2447
+ message: "Markdown technical front matter is not closed",
2448
+ }),
2449
+ ],
2450
+ };
2451
+ const header = parseDocument(source.slice(4, end), {
2452
+ schema: "core",
2453
+ uniqueKeys: true,
2454
+ strict: true,
2455
+ });
2456
+ if (header.errors.length > 0 || header.warnings.length > 0)
2457
+ return {
2458
+ diagnostics: [
2459
+ diagnostic({
2460
+ code: "yaml_invalid",
2461
+ message: "Markdown technical front matter could not be parsed safely",
2462
+ }),
2463
+ ],
2464
+ };
2465
+ return { header, bodyStart: end + 5 };
2466
+ }
2467
+ function markdownRevisionSection(source, locator) {
2468
+ const envelope = markdownEnvelope(source);
2469
+ if ("diagnostics" in envelope)
2470
+ return envelope;
2471
+ const headings = scanMarkdownHeadings(source, envelope.bodyStart).headings;
2472
+ const stack = [];
2473
+ const matches = [];
2474
+ for (const [index, heading] of headings.entries()) {
2475
+ if (heading.level === 1) {
2476
+ stack.length = 0;
2477
+ continue;
2478
+ }
2479
+ stack.length = heading.level - 2;
2480
+ stack[heading.level - 2] = heading.title.normalize("NFKC");
2481
+ if (stack.length === locator.length &&
2482
+ locator.every((part, locatorIndex) => stack[locatorIndex] === part.normalize("NFKC")))
2483
+ matches.push({ index, level: heading.level });
2484
+ }
2485
+ const publicLocator = { markdown: [...locator] };
2486
+ if (matches.length !== 1)
2487
+ return {
2488
+ diagnostics: [
2489
+ diagnostic({
2490
+ code: matches.length === 0 ? "locator_not_found" : "locator_ambiguous",
2491
+ locator: publicLocator,
2492
+ message: "Markdown heading path cannot be located uniquely",
2493
+ }),
2494
+ ],
2495
+ };
2496
+ const match = matches[0];
2497
+ const heading = headings[match.index];
2498
+ const next = headings
2499
+ .slice(match.index + 1)
2500
+ .find(({ level }) => level <= match.level);
2501
+ const rawEnd = next?.range.start.offset ?? source.length;
2502
+ return {
2503
+ start: heading.range.start.offset,
2504
+ headingEnd: heading.range.end.offset,
2505
+ trimmedEnd: trimTrailingWhitespaceOffset(source, heading.range.start.offset, rawEnd),
2506
+ rawEnd,
2507
+ level: match.level,
2508
+ };
2509
+ }
2510
+ function forbiddenYamlRange(source) {
2511
+ const nul = source.indexOf("\0");
2512
+ if (nul >= 0)
2513
+ return { start: nul, end: nul + 1 };
2514
+ const carriage = source.indexOf("\r");
2515
+ if (carriage >= 0)
2516
+ return { start: carriage, end: carriage + 1 };
2517
+ for (const pattern of [
2518
+ /(^|\s)[&*!][^\s,\]}]+/mu,
2519
+ /^\s*<<\s*:/mu,
2520
+ /^\.\.\.\s*$/mu,
2521
+ ]) {
2522
+ const match = pattern.exec(source);
2523
+ if (match !== null)
2524
+ return { start: match.index, end: match.index + match[0].length };
2525
+ }
2526
+ return null;
2527
+ }
2528
+ function inspectShape(value) {
2529
+ let nodes = 0;
2530
+ let capacityExceeded = false;
2531
+ let invalidNumber = false;
2532
+ const visit = (candidate, depth) => {
2533
+ nodes += 1;
2534
+ if (depth > maxYamlDepth || nodes > maxYamlNodes) {
2535
+ capacityExceeded = true;
2536
+ return;
2537
+ }
2538
+ if (typeof candidate === "number" && !Number.isFinite(candidate))
2539
+ invalidNumber = true;
2540
+ else if (Array.isArray(candidate))
2541
+ candidate.forEach((child) => visit(child, depth + 1));
2542
+ else if (isRecord(candidate))
2543
+ Object.values(candidate).forEach((child) => visit(child, depth + 1));
2544
+ };
2545
+ visit(value, 0);
2546
+ return { capacityExceeded, invalidNumber };
2547
+ }
2548
+ function stringPairMap(map) {
2549
+ const result = new Map();
2550
+ for (const pair of map.items) {
2551
+ const key = scalarString(pair.key);
2552
+ if (key !== null)
2553
+ result.set(key, pair);
2554
+ }
2555
+ return result;
2556
+ }
2557
+ function scalarEquals(value, expected) {
2558
+ return isScalar(value) && value.value === expected;
2559
+ }
2560
+ function scalarString(value) {
2561
+ return isScalar(value) && typeof value.value === "string"
2562
+ ? value.value
2563
+ : null;
2564
+ }
2565
+ function stringSequence(value) {
2566
+ if (!isSeq(value))
2567
+ return null;
2568
+ const result = [];
2569
+ for (const item of value.items) {
2570
+ const text = scalarString(item);
2571
+ if (text === null)
2572
+ return null;
2573
+ result.push(text);
2574
+ }
2575
+ return result;
2576
+ }
2577
+ function nodeRange(source, node, baseOffset) {
2578
+ const range = isRangedNode(node) ? node.range : null;
2579
+ return sourceRange(source, baseOffset + (range?.[0] ?? 0), baseOffset + (range?.[1] ?? range?.[0] ?? 0));
2580
+ }
2581
+ function joinedNodeRange(source, startNode, endNode, baseOffset) {
2582
+ const start = isRangedNode(startNode) ? (startNode.range?.[0] ?? 0) : 0;
2583
+ const end = isRangedNode(endNode)
2584
+ ? (endNode.range?.[1] ?? endNode.range?.[0] ?? start)
2585
+ : start;
2586
+ return sourceRange(source, baseOffset + start, baseOffset + end);
2587
+ }
2588
+ function isRangedNode(value) {
2589
+ return isScalar(value) || isMap(value) || isSeq(value);
2590
+ }
2591
+ function sourceRange(source, rawStart, rawEnd) {
2592
+ const start = Math.max(0, Math.min(source.length, rawStart));
2593
+ const end = Math.max(start, Math.min(source.length, rawEnd));
2594
+ return freeze({
2595
+ start: sourcePoint(source, start),
2596
+ end: sourcePoint(source, end),
2597
+ });
2598
+ }
2599
+ function sourcePoint(source, offset) {
2600
+ const before = source.slice(0, offset);
2601
+ const lineStart = before.lastIndexOf("\n") + 1;
2602
+ return freeze({
2603
+ offset,
2604
+ byteOffset: Buffer.byteLength(before, "utf8"),
2605
+ line: before.split("\n").length,
2606
+ column: offset - lineStart + 1,
2607
+ });
2608
+ }
2609
+ function diagnostic(value) {
2610
+ return freeze(value);
2611
+ }
2612
+ function revisionDiagnostic(commandIndex, value) {
2613
+ return freeze({ ...value, commandIndex });
2614
+ }
2615
+ function mechanicalHash(contents) {
2616
+ return `sha256:${createHash("sha256").update(contents).digest("hex")}`;
2617
+ }
2618
+ function validDocumentMetadata(value) {
2619
+ return documentMetadataProblems(value).length === 0;
2620
+ }
2621
+ /**
2622
+ * Says which of the three fields is wrong and what arrived instead.
2623
+ *
2624
+ * One shared sentence covering title, summary and aliases left the author
2625
+ * guessing among six failure modes, and guessing costs a whole exchange.
2626
+ */
2627
+ function documentMetadataProblems(value) {
2628
+ const problems = [];
2629
+ problems.push(...boundedTextProblems("title", value.title, 120));
2630
+ problems.push(...boundedTextProblems("summary", value.summary, 240));
2631
+ if (!Array.isArray(value.aliases))
2632
+ problems.push(`aliases must be an array; received ${describeValue(value.aliases)}`);
2633
+ else if (value.aliases.length > 16)
2634
+ problems.push(`aliases may contain at most 16 items; received ${value.aliases.length}`);
2635
+ else
2636
+ for (const [index, alias] of value.aliases.entries()) {
2637
+ if (typeof alias !== "string")
2638
+ problems.push(`aliases item ${index + 1} must be a string; received ${describeValue(alias)}`);
2639
+ else if ([...alias].length < 1 || [...alias].length > 64)
2640
+ problems.push(`aliases item ${index + 1} must contain 1 to 64 characters; received ${[...alias].length}`);
2641
+ }
2642
+ return problems;
2643
+ }
2644
+ function boundedTextProblems(field, value, limit) {
2645
+ if (typeof value !== "string")
2646
+ return [`${field} must be a string; received ${describeValue(value)}`];
2647
+ const length = [...value].length;
2648
+ if (length < 1)
2649
+ return [`${field} cannot be empty`];
2650
+ return length > limit
2651
+ ? [`${field} may contain at most ${limit} characters; received ${length}`]
2652
+ : [];
2653
+ }
2654
+ // aliases feeds the literal search index, so entries must end up as strings.
2655
+ // A numeric alias such as a room number is still worth searching for, so it is
2656
+ // coerced rather than used as grounds to reject the whole document.
2657
+ function normalizeAliases(value) {
2658
+ if (value === undefined)
2659
+ return [];
2660
+ if (!Array.isArray(value))
2661
+ return value;
2662
+ return value.map((alias) => typeof alias === "number" || typeof alias === "boolean"
2663
+ ? String(alias)
2664
+ : alias);
2665
+ }
2666
+ function describeValue(value) {
2667
+ if (value === undefined)
2668
+ return "missing";
2669
+ if (value === null)
2670
+ return "null";
2671
+ if (Array.isArray(value))
2672
+ return `array (${value.length} items)`;
2673
+ if (typeof value === "object")
2674
+ return "object";
2675
+ return `${typeof value} ${JSON.stringify(value)}`;
2676
+ }
2677
+ function validRevisionLogicalPath(logicalPath, codec, logicalRoot) {
2678
+ const prefix = `${logicalRoot}/`;
2679
+ return (logicalPath.startsWith(prefix) &&
2680
+ validRelativePath(logicalPath.slice(prefix.length)) &&
2681
+ documentCodec(logicalPath) === codec);
2682
+ }
2683
+ function nextAvailableShortRef(hint, occupied) {
2684
+ if (!occupied.has(hint))
2685
+ return hint;
2686
+ for (let suffix = 2;; suffix += 1) {
2687
+ const suffixText = `-${suffix}`;
2688
+ const candidate = `${hint.slice(0, 32 - suffixText.length)}${suffixText}`;
2689
+ if (!occupied.has(candidate))
2690
+ return candidate;
2691
+ }
2692
+ }
2693
+ function freezeFile(file) {
2694
+ return freeze({
2695
+ path: file.path,
2696
+ contents: file.contents,
2697
+ ...(file.encoding === undefined ? {} : { encoding: file.encoding }),
2698
+ });
2699
+ }
2700
+ function documentCodec(path) {
2701
+ if (/\.ya?ml$/iu.test(path))
2702
+ return "yaml";
2703
+ if (/\.md$/iu.test(path))
2704
+ return "markdown";
2705
+ return null;
2706
+ }
2707
+ function validRelativePath(path) {
2708
+ return (path.length > 0 &&
2709
+ !path.includes("\\") &&
2710
+ path
2711
+ .split("/")
2712
+ .every((segment) => segment !== "" && segment !== "." && segment !== ".."));
2713
+ }
2714
+ function blockingDocumentDiagnostics(entry) {
2715
+ return entry.diagnostics.filter(({ code }) => code !== "locator_ambiguous" && code !== "document_reference_invalid");
2716
+ }
2717
+ function literalSearchBlockingDiagnostics(entry) {
2718
+ return entry.diagnostics.filter(({ code }) => code !== "locator_ambiguous");
2719
+ }
2720
+ function yamlLocatorsOverlap(left, right) {
2721
+ return locatorStartsWith(left, right) || locatorStartsWith(right, left);
2722
+ }
2723
+ function locatorStartsWith(value, prefix) {
2724
+ return (value.length >= prefix.length &&
2725
+ prefix.every((segment, index) => value[index] === segment));
2726
+ }
2727
+ function validYamlLocator(value) {
2728
+ return (isRecord(value) &&
2729
+ Object.keys(value).length === 1 &&
2730
+ Array.isArray(value.yaml) &&
2731
+ value.yaml.every((segment) => typeof segment === "string" ||
2732
+ (Number.isSafeInteger(segment) && Number(segment) >= 0)));
2733
+ }
2734
+ function validMarkdownLocator(value) {
2735
+ return (isRecord(value) &&
2736
+ Object.keys(value).length === 1 &&
2737
+ Array.isArray(value.markdown) &&
2738
+ value.markdown.length > 0 &&
2739
+ value.markdown.every((segment) => typeof segment === "string" && segment.length > 0));
2740
+ }
2741
+ function validMarkdownHeadingTitle(value) {
2742
+ return (typeof value === "string" &&
2743
+ value.trim() === value &&
2744
+ !/[\r\n]/u.test(value) &&
2745
+ [...value].length >= 1 &&
2746
+ [...value].length <= 120);
2747
+ }
2748
+ function copyLocator(value) {
2749
+ if ("yaml" in value && Array.isArray(value.yaml))
2750
+ return freeze({
2751
+ yaml: freeze(value.yaml.filter((segment) => typeof segment === "string" || typeof segment === "number")),
2752
+ });
2753
+ if ("markdown" in value && Array.isArray(value.markdown))
2754
+ return freeze({
2755
+ markdown: freeze(value.markdown.filter((segment) => typeof segment === "string")),
2756
+ });
2757
+ return freeze({ yaml: freeze([]) });
2758
+ }
2759
+ function hasOnlyKeys(value, allowed) {
2760
+ const allowedSet = new Set(allowed);
2761
+ return Object.keys(value).every((key) => allowedSet.has(key));
2762
+ }
2763
+ function copyPublicValue(value, seen = new WeakSet()) {
2764
+ if (value === null ||
2765
+ typeof value === "string" ||
2766
+ typeof value === "number" ||
2767
+ typeof value === "boolean" ||
2768
+ value === undefined)
2769
+ return value;
2770
+ if (typeof value !== "object")
2771
+ return null;
2772
+ if (seen.has(value))
2773
+ return "[circular]";
2774
+ seen.add(value);
2775
+ if (Array.isArray(value))
2776
+ return value.map((entry) => copyPublicValue(entry, seen));
2777
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
2778
+ key,
2779
+ copyPublicValue(entry, seen),
2780
+ ]));
2781
+ }
2782
+ function groupBy(values, keyFor) {
2783
+ const result = new Map();
2784
+ for (const value of values) {
2785
+ const key = keyFor(value);
2786
+ const group = result.get(key);
2787
+ if (group === undefined)
2788
+ result.set(key, [value]);
2789
+ else
2790
+ group.push(value);
2791
+ }
2792
+ return result;
2793
+ }
2794
+ function freeze(value) {
2795
+ const candidate = value;
2796
+ if (typeof candidate !== "object" ||
2797
+ candidate === null ||
2798
+ Object.isFrozen(candidate))
2799
+ return value;
2800
+ if (Array.isArray(candidate))
2801
+ candidate.forEach((entry) => freeze(entry));
2802
+ else if (isRecord(candidate))
2803
+ Object.values(candidate).forEach((entry) => freeze(entry));
2804
+ return Object.freeze(candidate);
2805
+ }
2806
+ function isRecord(value) {
2807
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2808
+ }