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,512 @@
1
+ import { parseDocument } from "yaml";
2
+ import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
3
+ const maxOpeningBytes = 64 * 1024;
4
+ export function isContentPackageWorldDocumentPath(path) {
5
+ return path.startsWith("world/");
6
+ }
7
+ export function worldDocumentTextRequirement(path) {
8
+ return `World documents must contain UTF-8 YAML or Markdown source: ${path}`;
9
+ }
10
+ export function inspectContentPackageCurrentTree(files, options = {}) {
11
+ const worldDocumentSnapshot = options.worldDocumentSnapshot ??
12
+ WorldDocumentStore.open({
13
+ layout: "content_package",
14
+ files,
15
+ });
16
+ if (worldDocumentSnapshot.layout !== "content_package")
17
+ throw new Error("Content-package inspection accepts only content_package document snapshots");
18
+ const snapshotFiles = worldDocumentSnapshot.files;
19
+ const issues = [];
20
+ const documents = worldDocumentSnapshotDocuments(worldDocumentSnapshot);
21
+ const textFiles = new Map(snapshotFiles
22
+ .filter(({ encoding }) => encoding === undefined)
23
+ .map((file) => [file.path, file.contents]));
24
+ const openingFiles = snapshotFiles.filter(({ path }) => path === "opening.md");
25
+ let opening = "missing";
26
+ if (openingFiles.length === 0) {
27
+ if (options.requireOpening !== false)
28
+ issue(issues, "missing_opening", "opening.md", "The content package is missing a root-level opening.md introduction");
29
+ }
30
+ else if (openingFiles.length > 1) {
31
+ opening = "invalid";
32
+ issue(issues, "invalid_opening", "opening.md", "A content package may contain only one root-level opening.md file");
33
+ }
34
+ else {
35
+ const openingFile = openingFiles[0];
36
+ if (openingFile.encoding !== undefined ||
37
+ openingFile.contents.trim().length === 0 ||
38
+ Buffer.byteLength(openingFile.contents, "utf8") > maxOpeningBytes ||
39
+ Buffer.from(openingFile.contents, "utf8").toString("utf8") !==
40
+ openingFile.contents) {
41
+ opening = "invalid";
42
+ issue(issues, "invalid_opening", "opening.md", "The introduction must be non-empty, well-formed UTF-8 text no larger than 64 KiB");
43
+ }
44
+ else
45
+ opening = "valid";
46
+ }
47
+ const unsupportedPaths = new Set();
48
+ for (const file of snapshotFiles) {
49
+ if (/\.json$/iu.test(file.path)) {
50
+ unsupportedPaths.add(file.path);
51
+ issue(issues, "unsupported_content_file", file.path, "The current content package does not accept manifest, seven-field JSON, schema, record, or revision files");
52
+ }
53
+ }
54
+ issues.push(...worldDocumentSnapshot.diagnostics
55
+ .filter(({ logicalPath }) => logicalPath === undefined || !unsupportedPaths.has(logicalPath))
56
+ .map(worldDocumentIssue));
57
+ const index = documentIndex(documents);
58
+ const frameSource = textFiles.get("control/frame.yaml");
59
+ let worldFrame = "missing";
60
+ let currentSituation = "missing";
61
+ if (frameSource === undefined) {
62
+ issue(issues, "missing_world_frame", "control/frame.yaml", "The content package is missing its world prompt frame");
63
+ issue(issues, "missing_current_situation", "control/frame.yaml", "The content package has not bound a current-situation document");
64
+ }
65
+ else {
66
+ const frame = parseRestrictedYaml("control/frame.yaml", frameSource, issues);
67
+ if (isRecord(frame) &&
68
+ frame.format === "narraeon.world-frame/v1" &&
69
+ Object.keys(frame).every((key) => ["format", "bindings", "instructions", "context"].includes(key)) &&
70
+ isRecord(frame.bindings) &&
71
+ Object.keys(frame.bindings).length === 1) {
72
+ worldFrame = "valid";
73
+ const binding = isRecord(frame.bindings)
74
+ ? frame.bindings.currentSituation
75
+ : undefined;
76
+ if (typeof binding === "string" && index.resolve(binding) !== undefined)
77
+ currentSituation = "valid";
78
+ else {
79
+ currentSituation = "invalid";
80
+ issue(issues, "missing_current_situation", "control/frame.yaml", "The world frame must bind an existing current-situation document");
81
+ }
82
+ validateFrameMarkdownFiles(frame, textFiles, issues);
83
+ validateFrameContext(frame.context, index, worldDocumentSnapshot, issues);
84
+ }
85
+ else {
86
+ worldFrame = "invalid";
87
+ issue(issues, "invalid_world_frame", "control/frame.yaml", "The world-frame format must be narraeon.world-frame/v1");
88
+ }
89
+ }
90
+ const playerSource = textFiles.get("control/player-views.yaml");
91
+ let playerView = "missing";
92
+ if (playerSource === undefined) {
93
+ issue(issues, "missing_player_view", "control/player-views.yaml", "The content package is missing its player-view control file");
94
+ }
95
+ else {
96
+ const player = parseRestrictedYaml("control/player-views.yaml", playerSource, issues);
97
+ const formatProblems = isRecord(player) && player.format === "narraeon.player-views/v1"
98
+ ? playerViewProblems(player.views, index)
99
+ : ["The file must begin with format: narraeon.player-views/v1"];
100
+ if (formatProblems.length === 0) {
101
+ playerView = "valid";
102
+ }
103
+ else {
104
+ playerView = "invalid";
105
+ // A malformed file can produce one problem per item; report enough to
106
+ // act on and say how many were withheld rather than flooding the author.
107
+ for (const problem of formatProblems.slice(0, maxReportedPlayerViewProblems))
108
+ issue(issues, "invalid_player_view", "control/player-views.yaml", problem);
109
+ if (formatProblems.length > maxReportedPlayerViewProblems)
110
+ issue(issues, "invalid_player_view", "control/player-views.yaml", `${formatProblems.length - maxReportedPlayerViewProblems} more player-view issues were omitted`);
111
+ }
112
+ }
113
+ const displayName = documents.find(({ id }) => id === "situation.current")?.title ??
114
+ documents[0]?.title ??
115
+ "Untitled content package";
116
+ return {
117
+ status: issues.length === 0 ? "usable" : "needs_repair",
118
+ displayName,
119
+ documents: documents.sort((a, b) => a.path.localeCompare(b.path)),
120
+ issues,
121
+ opening,
122
+ control: { worldFrame, currentSituation, playerView },
123
+ worldDocumentSnapshot,
124
+ };
125
+ }
126
+ function worldDocumentSnapshotDocuments(snapshot) {
127
+ const documents = [];
128
+ const pendingDirectories = [""];
129
+ const visitedDirectories = new Set();
130
+ while (pendingDirectories.length > 0) {
131
+ const directory = pendingDirectories.shift();
132
+ if (visitedDirectories.has(directory))
133
+ continue;
134
+ visitedDirectories.add(directory);
135
+ let cursor = null;
136
+ do {
137
+ const result = snapshot.query({
138
+ kind: "catalog",
139
+ directory,
140
+ limit: 100,
141
+ cursor,
142
+ });
143
+ if (!result.ok || result.kind !== "catalog") {
144
+ throw new Error("The WorldDocumentStore catalog query violated the internal snapshot contract");
145
+ }
146
+ for (const entry of result.entries) {
147
+ if (entry.kind === "directory") {
148
+ const relative = relativeSnapshotPath(snapshot, entry.logicalPath);
149
+ if (relative !== null)
150
+ pendingDirectories.push(relative);
151
+ }
152
+ else if (entry.document !== undefined) {
153
+ documents.push(contentDocument(entry.document));
154
+ }
155
+ }
156
+ cursor = result.page.nextCursor;
157
+ } while (cursor !== null);
158
+ }
159
+ return documents.sort((left, right) => left.path.localeCompare(right.path));
160
+ }
161
+ function relativeSnapshotPath(snapshot, logicalPath) {
162
+ const prefix = `${snapshot.logicalRoot}/`;
163
+ if (!logicalPath.startsWith(prefix))
164
+ return null;
165
+ const relative = logicalPath.slice(prefix.length);
166
+ return relative
167
+ .split("/")
168
+ .every((part) => part !== "" && part !== "." && part !== "..")
169
+ ? relative
170
+ : null;
171
+ }
172
+ function documentIndex(documents) {
173
+ const byId = new Map(documents.map((document) => [document.id, document]));
174
+ const byRef = new Map(documents.map((document) => [document.ref, document]));
175
+ return {
176
+ resolve(handle) {
177
+ if (typeof handle !== "string")
178
+ return undefined;
179
+ return handle.startsWith("@")
180
+ ? byRef.get(handle.slice(1))
181
+ : byId.get(handle);
182
+ },
183
+ documents: () => documents,
184
+ };
185
+ }
186
+ function contentDocument(document) {
187
+ return {
188
+ id: document.documentId,
189
+ ref: document.shortRef,
190
+ title: document.title,
191
+ summary: document.summary,
192
+ aliases: [...document.aliases],
193
+ path: document.logicalPath,
194
+ codec: document.codec,
195
+ };
196
+ }
197
+ function worldDocumentIssue(diagnostic) {
198
+ return {
199
+ code: contentIssueCode(diagnostic),
200
+ path: diagnostic.logicalPath ?? "world",
201
+ message: diagnostic.code === "document_reference_invalid"
202
+ ? "An explicit $ref points to a missing document, or the target identity is not unique in the snapshot"
203
+ : diagnostic.message,
204
+ ...(diagnostic.documentId === undefined
205
+ ? {}
206
+ : { documentId: diagnostic.documentId }),
207
+ worldDocumentDiagnostic: diagnostic,
208
+ };
209
+ }
210
+ function contentIssueCode(diagnostic) {
211
+ switch (diagnostic.code) {
212
+ case "world_document_binary":
213
+ return "binary_world_file";
214
+ case "document_reference_invalid":
215
+ return "dangling_reference";
216
+ case "document_identity_duplicate":
217
+ return "duplicate_id";
218
+ case "document_short_ref_duplicate":
219
+ return "duplicate_ref";
220
+ case "markdown_invalid":
221
+ case "locator_ambiguous":
222
+ return "invalid_markdown";
223
+ case "capacity_exceeded":
224
+ case "yaml_invalid":
225
+ return "unsafe_yaml";
226
+ case "document_header_invalid":
227
+ case "document_identity_invalid":
228
+ case "document_short_ref_invalid":
229
+ case "logical_path_duplicate":
230
+ case "logical_path_invalid":
231
+ case "world_document_codec_unsupported":
232
+ case "cursor_invalid":
233
+ case "document_ambiguous":
234
+ case "document_not_found":
235
+ case "locator_invalid":
236
+ case "locator_not_found":
237
+ case "query_invalid":
238
+ return "invalid_document_header";
239
+ }
240
+ }
241
+ const maxReportedPlayerViewProblems = 8;
242
+ /**
243
+ * Reports where a player view is wrong, not merely that it is.
244
+ *
245
+ * This was one `every` chain returning a boolean, so a dozen distinct mistakes
246
+ * all surfaced as the same sentence and the author had to guess which one it
247
+ * hit. Each check now says which view, which item, and what it actually got.
248
+ */
249
+ function playerViewProblems(value, index) {
250
+ if (!Array.isArray(value))
251
+ return ["views must be a list of views"];
252
+ const problems = [];
253
+ for (const [viewIndex, view] of value.entries()) {
254
+ const at = `View ${viewIndex + 1} `;
255
+ if (!isRecord(view)) {
256
+ problems.push(`${at}must be an object`);
257
+ continue;
258
+ }
259
+ const unknownKeys = Object.keys(view).filter((key) => !["id", "title", "items"].includes(key));
260
+ if (unknownKeys.length > 0)
261
+ problems.push(`${at}does not accept fields ${unknownKeys.join(", ")}`);
262
+ if (typeof view.id !== "string")
263
+ problems.push(`${at}is missing a string id`);
264
+ if (typeof view.title !== "string")
265
+ problems.push(`${at}is missing a string title`);
266
+ const named = typeof view.id === "string" ? `View ${view.id} ` : at;
267
+ if (!Array.isArray(view.items)) {
268
+ problems.push(`${named}items must be an array`);
269
+ continue;
270
+ }
271
+ if (view.items.length > 128)
272
+ problems.push(`${named}may contain at most 128 items; found ${view.items.length}`);
273
+ for (const [itemIndex, item] of view.items.entries())
274
+ problems.push(...playerViewItemProblems(item, `${named}item ${itemIndex + 1} `, index));
275
+ }
276
+ return problems;
277
+ }
278
+ function playerViewItemProblems(item, at, index) {
279
+ if (!isRecord(item))
280
+ return [`${at}must be an object`];
281
+ if (typeof item.id !== "string")
282
+ return [`${at}is missing a string id`];
283
+ if (typeof item.label !== "string")
284
+ return [`${at}is missing a string label`];
285
+ if (!isRecord(item.select))
286
+ return [`${at}is missing a select object`];
287
+ if (typeof item.select.document !== "string")
288
+ return [`${at}select.document must be an @short-ref or document ID string`];
289
+ const { locator } = item.select;
290
+ if (locator === undefined)
291
+ return [];
292
+ if (!isRecord(locator) || Object.keys(locator).length !== 1)
293
+ return [`${at}locator must declare exactly one of yaml or markdown`];
294
+ const document = index.resolve(item.select.document);
295
+ if (Array.isArray(locator.yaml)) {
296
+ if (!locator.yaml.every((part) => typeof part === "string"))
297
+ return [`${at}yaml locator entries must all be string map keys`];
298
+ return document !== undefined && document.codec !== "yaml"
299
+ ? [
300
+ `${at}uses a yaml locator for ${document.codec} document ${item.select.document}`,
301
+ ]
302
+ : [];
303
+ }
304
+ if (Array.isArray(locator.markdown)) {
305
+ if (!locator.markdown.every((part) => typeof part === "string" && part.length > 0))
306
+ return [
307
+ `${at}markdown locator entries must all be non-empty heading strings`,
308
+ ];
309
+ return document !== undefined && document.codec !== "markdown"
310
+ ? [
311
+ `${at}uses a markdown locator for ${document.codec} document ${item.select.document}`,
312
+ ]
313
+ : [];
314
+ }
315
+ return [`${at}locator must be a yaml or markdown array`];
316
+ }
317
+ function parseRestrictedYaml(path, source, issues) {
318
+ if (source.includes("\0") ||
319
+ source.includes("\r") ||
320
+ /(^|\s)[&*!][^\s,\]}]+/mu.test(source) ||
321
+ /^\s*<<\s*:/mu.test(source) ||
322
+ /^\.\.\.\s*$/mu.test(source)) {
323
+ issue(issues, "unsafe_yaml", path, "YAML uses a forbidden tag, anchor, alias, merge, multi-document stream, or invalid line break");
324
+ return undefined;
325
+ }
326
+ const document = parseDocument(source, {
327
+ schema: "core",
328
+ uniqueKeys: true,
329
+ strict: true,
330
+ });
331
+ if (document.errors.length > 0 || document.warnings.length > 0) {
332
+ issue(issues, "unsafe_yaml", path, "YAML is not a safe single-document YAML 1.2 core map");
333
+ return undefined;
334
+ }
335
+ const value = document.toJS({ maxAliasCount: 0 });
336
+ if (!isRecord(value) || exceedsShapeLimit(value)) {
337
+ issue(issues, "unsafe_yaml", path, "The YAML root must be a map whose depth and node count stay within the limits");
338
+ return undefined;
339
+ }
340
+ return value;
341
+ }
342
+ function validateFrameMarkdownFiles(frame, files, issues) {
343
+ const instructions = Array.isArray(frame.instructions)
344
+ ? frame.instructions
345
+ : [];
346
+ for (const entry of instructions) {
347
+ if (!isRecord(entry) ||
348
+ Object.keys(entry).length !== 1 ||
349
+ typeof entry.markdown !== "string" ||
350
+ entry.markdown.startsWith("/") ||
351
+ entry.markdown.includes("..") ||
352
+ !files.has(`control/${entry.markdown}`)) {
353
+ issue(issues, "invalid_world_frame", "control/frame.yaml", "A Markdown block referenced by the world frame is missing or escapes the control directory");
354
+ }
355
+ }
356
+ }
357
+ function validateFrameContext(value, index, snapshot, issues) {
358
+ if (!Array.isArray(value)) {
359
+ issue(issues, "invalid_world_frame", "control/frame.yaml", "World-frame context must be a list of slots");
360
+ return;
361
+ }
362
+ const kinds = [];
363
+ for (const entry of value) {
364
+ if (!isRecord(entry) ||
365
+ Object.keys(entry).length !== 1 ||
366
+ !isRecord(entry.slot) ||
367
+ typeof entry.slot.kind !== "string") {
368
+ issue(issues, "invalid_world_frame", "control/frame.yaml", "Each context entry must contain exactly one valid slot");
369
+ continue;
370
+ }
371
+ // Exactly the kinds FileNativePromptCompiler compiles. history_message
372
+ // was accepted here and rejected there, so a candidate could pass its own
373
+ // self-check and then fail the real Prompt Preview it was meant to predict.
374
+ const allowedKeys = {
375
+ current_situation: ["kind"],
376
+ additional_materials: ["kind"],
377
+ document: ["kind", "document", "required"],
378
+ node: ["kind", "document", "locator", "required"],
379
+ catalog: ["kind", "directory", "maxEntries", "required"],
380
+ history: ["kind", "recent"],
381
+ reference_targets: ["kind", "from", "maxEntries", "required"],
382
+ };
383
+ const allowed = allowedKeys[entry.slot.kind];
384
+ if (allowed === undefined) {
385
+ issue(issues, "invalid_world_frame", "control/frame.yaml", `The world frame contains unknown slot kind ${entry.slot.kind}; available kinds are ${Object.keys(allowedKeys).join(", ")}`);
386
+ continue;
387
+ }
388
+ // Naming the offending key matters: reporting the kind reads as "this slot
389
+ // does not exist" and sends the author off deleting a valid slot.
390
+ const unknownKeys = Object.keys(entry.slot).filter((key) => !allowed.includes(key));
391
+ if (unknownKeys.length > 0) {
392
+ issue(issues, "invalid_world_frame", "control/frame.yaml", `Slot ${entry.slot.kind} does not accept parameters ${unknownKeys.join(", ")}; it accepts only ${allowed.join(", ")}`);
393
+ continue;
394
+ }
395
+ if (!validSlotParameters(entry.slot, index, snapshot)) {
396
+ issue(issues, "invalid_world_frame", "control/frame.yaml", `Invalid world-frame slot parameters: ${entry.slot.kind}`);
397
+ continue;
398
+ }
399
+ if (entry.slot.kind === "catalog" &&
400
+ entry.slot.required !== false &&
401
+ catalogDocuments(entry.slot.directory, index.documents(), "world").length === 0) {
402
+ const directory = entry.slot.directory;
403
+ issue(issues, "invalid_world_frame", "control/frame.yaml", `Required catalog ${directory} has no associated documents; it matches only direct children named world/${directory}/<file>.yaml, .yml, or .md, and dots in file names do not create directories`);
404
+ continue;
405
+ }
406
+ kinds.push(entry.slot.kind);
407
+ }
408
+ for (const required of ["current_situation", "additional_materials"]) {
409
+ if (kinds.filter((kind) => kind === required).length !== 1)
410
+ issue(issues, "invalid_world_frame", "control/frame.yaml", `The world frame must contain exactly one ${required} slot`);
411
+ }
412
+ }
413
+ function validSlotParameters(slot, index, snapshot) {
414
+ if (slot.required !== undefined && typeof slot.required !== "boolean")
415
+ return false;
416
+ if (slot.kind === "current_situation" || slot.kind === "additional_materials")
417
+ return true;
418
+ if (slot.kind === "history")
419
+ return slot.recent === undefined || integerInRange(slot.recent, 1, 32);
420
+ if (slot.kind === "document")
421
+ return index.resolve(slot.document) !== undefined;
422
+ if (slot.kind === "catalog")
423
+ return (validRelativeDirectory(slot.directory) &&
424
+ integerInRange(slot.maxEntries, 1, 100));
425
+ if (slot.kind === "reference_targets") {
426
+ const from = isRecord(slot.from)
427
+ ? index.resolve(slot.from.document)
428
+ : undefined;
429
+ return (integerInRange(slot.maxEntries, 1, 64) &&
430
+ isRecord(slot.from) &&
431
+ Object.keys(slot.from).length === 2 &&
432
+ Object.keys(slot.from).every((key) => ["document", "locator"].includes(key)) &&
433
+ from !== undefined &&
434
+ isRecord(slot.from.locator) &&
435
+ Object.keys(slot.from.locator).length === 1 &&
436
+ Array.isArray(slot.from.locator.yaml) &&
437
+ slot.from.locator.yaml.every((part) => typeof part === "string") &&
438
+ snapshotHasNode(snapshot, from.id, {
439
+ yaml: slot.from.locator.yaml.filter((part) => typeof part === "string"),
440
+ }));
441
+ }
442
+ if (slot.kind === "node") {
443
+ const document = index.resolve(slot.document);
444
+ if (document === undefined ||
445
+ !isRecord(slot.locator) ||
446
+ Object.keys(slot.locator).length !== 1)
447
+ return false;
448
+ if (Array.isArray(slot.locator.yaml))
449
+ return (document.codec === "yaml" &&
450
+ slot.locator.yaml.every((part) => typeof part === "string") &&
451
+ snapshotHasNode(snapshot, document.id, {
452
+ yaml: slot.locator.yaml.filter((part) => typeof part === "string"),
453
+ }));
454
+ if (Array.isArray(slot.locator.markdown))
455
+ return (document.codec === "markdown" &&
456
+ slot.locator.markdown.every((part) => typeof part === "string") &&
457
+ snapshotHasNode(snapshot, document.id, {
458
+ markdown: slot.locator.markdown.filter((part) => typeof part === "string"),
459
+ }));
460
+ }
461
+ return false;
462
+ }
463
+ function snapshotHasNode(snapshot, documentId, locator) {
464
+ const result = snapshot.query({
465
+ kind: "select_node",
466
+ document: { documentId },
467
+ locator,
468
+ });
469
+ return result.ok && result.kind === "select_node";
470
+ }
471
+ function validRelativeDirectory(value) {
472
+ return (typeof value === "string" &&
473
+ value.length > 0 &&
474
+ value
475
+ .split("/")
476
+ .every((part) => part !== "" && part !== "." && part !== ".."));
477
+ }
478
+ function catalogDocuments(directory, documents, root) {
479
+ const prefix = `${root}/${directory}/`;
480
+ return [...documents].filter(({ path }) => path.startsWith(prefix) && !path.slice(prefix.length).includes("/"));
481
+ }
482
+ function integerInRange(value, minimum, maximum) {
483
+ return (typeof value === "number" &&
484
+ Number.isInteger(value) &&
485
+ value >= minimum &&
486
+ value <= maximum);
487
+ }
488
+ function exceedsShapeLimit(value) {
489
+ let nodes = 0;
490
+ const visit = (candidate, depth) => {
491
+ nodes += 1;
492
+ if (depth > 64 || nodes > 100_000)
493
+ return true;
494
+ if (Array.isArray(candidate))
495
+ return candidate.some((item) => visit(item, depth + 1));
496
+ if (isRecord(candidate))
497
+ return Object.values(candidate).some((item) => visit(item, depth + 1));
498
+ return typeof candidate === "number" && !Number.isFinite(candidate);
499
+ };
500
+ return visit(value, 0);
501
+ }
502
+ function issue(issues, code, path, message, documentId) {
503
+ issues.push({
504
+ code,
505
+ path,
506
+ message,
507
+ ...(documentId === undefined ? {} : { documentId }),
508
+ });
509
+ }
510
+ function isRecord(value) {
511
+ return typeof value === "object" && value !== null && !Array.isArray(value);
512
+ }