narraeon 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +188 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +447 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-BXfelCi9.js +1207 -0
  70. package/dist/web/assets/index-BYnhUxlk.css +1 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D5VqDvjY.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,3 +1,4 @@
1
+ import { encodePlayPromptRun, decodePlayPromptRun, } from "./PlayPromptRun.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
3
4
  import { dirname, join, resolve } from "node:path";
@@ -52,6 +53,10 @@ export class FileNativePlayTimelineStore {
52
53
  async persist(value, cursor) {
53
54
  return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor);
54
55
  }
56
+ /** Write a sealed context segment without making it the active timeline. */
57
+ async persistDetached(value, cursor) {
58
+ return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor, false);
59
+ }
55
60
  async persistStaged(targetWorldRoot, value, cursor) {
56
61
  return this.#persistAtRuntimeRoot(join(resolve(targetWorldRoot), "runtime"), value, cursor);
57
62
  }
@@ -65,6 +70,16 @@ export class FileNativePlayTimelineStore {
65
70
  const eventCount = prefixLength(input.source.events, input.target.events);
66
71
  const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
67
72
  const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
73
+ if (input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
74
+ const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
75
+ if (encoding !== null)
76
+ await publishImmutableJson(join(targetRoot, "initial-request-encoding.json"), encoding);
77
+ }
78
+ for (const run of input.target.promptRuns ?? []) {
79
+ const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId, run.exchange).catch(() => null);
80
+ if (encoding !== null)
81
+ await publishImmutableJson(join(targetRoot, `request-encoding-${run.exchange}.json`), encoding);
82
+ }
68
83
  await Promise.all([
69
84
  cloneNumberedPrefix({
70
85
  sourceRoot,
@@ -109,7 +124,7 @@ export class FileNativePlayTimelineStore {
109
124
  authorizationCheckpointCount,
110
125
  });
111
126
  }
112
- async #persistAtRuntimeRoot(runtimeRoot, value, cursor) {
127
+ async #persistAtRuntimeRoot(runtimeRoot, value, cursor, publishHead = true) {
113
128
  assertIdentity(value.worldId, "World ID");
114
129
  assertIdentity(value.chainId, "Call-chain ID");
115
130
  const root = join(runtimeRoot, "play-contexts", createHash("sha256").update(value.chainId).digest("hex"));
@@ -125,6 +140,9 @@ export class FileNativePlayTimelineStore {
125
140
  await publishImmutableJson(join(root, "base.json"), base);
126
141
  else if (!isDeepStrictEqual(existingBase, base))
127
142
  throw new Error("A play context identity is bound to different frozen data");
143
+ await publishImmutableJson(join(root, "reply-context.json"), {
144
+ contextId: value.continuityContextId ?? value.chainId,
145
+ });
128
146
  const supplied = cursor ?? emptyPersistenceCursor();
129
147
  // Recovery may replace the unresolved tail (for example, a generic crash
130
148
  // failure becomes the already-returned Provider response). Numbered files
@@ -164,6 +182,10 @@ export class FileNativePlayTimelineStore {
164
182
  const checkpoints = value.documentAuthorizationCheckpoints ?? [];
165
183
  for (let index = previous.authorizationCheckpointCount; index < checkpoints.length; index += 1)
166
184
  await publishImmutableJson(join(root, "authorization", numbered(index + 1)), checkpoints[index]);
185
+ const promptRuns = value.promptRuns ?? [];
186
+ for (let index = supplied.promptRunCount ?? 0; index < promptRuns.length; index += 1) {
187
+ await publishImmutableJson(join(root, "prompt-runs", numbered(index + 1)), encodePlayPromptRun(promptRuns[index]));
188
+ }
167
189
  const state = {
168
190
  schemaVersion: 3,
169
191
  chainId: value.chainId,
@@ -174,6 +196,7 @@ export class FileNativePlayTimelineStore {
174
196
  transcriptCount: value.transcript.length,
175
197
  completedToolCount: value.completedTools.length,
176
198
  authorizationCheckpointCount: checkpoints.length,
199
+ promptRunCount: promptRuns.length,
177
200
  changedDocuments: structuredClone(value.changedDocuments),
178
201
  nextEventId: value.nextEventId,
179
202
  exchange: value.exchange,
@@ -183,17 +206,19 @@ export class FileNativePlayTimelineStore {
183
206
  updatedAt: value.updatedAt,
184
207
  };
185
208
  await publishJson(join(root, "state.json"), state);
186
- await publishJson(join(runtimeRoot, "play-timeline-head.json"), {
187
- schemaVersion: 3,
188
- worldId: value.worldId,
189
- chainId: value.chainId,
190
- generation: value.timelineGeneration,
191
- });
209
+ if (publishHead)
210
+ await publishJson(join(runtimeRoot, "play-timeline-head.json"), {
211
+ schemaVersion: 3,
212
+ worldId: value.worldId,
213
+ chainId: value.chainId,
214
+ generation: value.timelineGeneration,
215
+ });
192
216
  return {
193
217
  eventCount: value.events.length,
194
218
  transcriptCount: value.transcript.length,
195
219
  completedToolCount: value.completedTools.length,
196
220
  authorizationCheckpointCount: checkpoints.length,
221
+ promptRunCount: promptRuns.length,
197
222
  };
198
223
  }
199
224
  async readCurrent(worldId) {
@@ -213,13 +238,14 @@ export class FileNativePlayTimelineStore {
213
238
  if (metadata === null)
214
239
  return null;
215
240
  const { index: indexValue, state: stateValue, root } = metadata;
216
- const [baseValue, continuationValue, transcript, rawEvents, completedTools, checkpoints,] = await Promise.all([
241
+ const [baseValue, continuationValue, transcript, rawEvents, completedTools, checkpoints, promptRuns,] = await Promise.all([
217
242
  readJson(join(root, "base.json")),
218
243
  readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`)),
219
244
  readNumbered(join(root, "transcript"), stateValue.transcriptCount),
220
245
  readNumberedValidated(join(root, "events"), stateValue.eventCount, assertPlayEvent),
221
246
  readNumbered(join(root, "completed-tools"), stateValue.completedToolCount),
222
247
  readNumbered(join(root, "authorization"), stateValue.authorizationCheckpointCount),
248
+ readNumbered(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0).then((records) => records.map(decodePlayPromptRun)),
223
249
  ]);
224
250
  const events = rawEvents.map(normalizePlayEvent);
225
251
  assertContextBase(baseValue);
@@ -247,6 +273,12 @@ export class FileNativePlayTimelineStore {
247
273
  chainId,
248
274
  previousChainId: indexValue.previousChainId,
249
275
  timelineGeneration: indexValue.timelineGeneration,
276
+ ...(baseValue.continuityContextId === undefined
277
+ ? {}
278
+ : { continuityContextId: baseValue.continuityContextId }),
279
+ ...(baseValue.presetFiles === undefined
280
+ ? {}
281
+ : { presetFiles: structuredClone(baseValue.presetFiles) }),
250
282
  baselineHead: baseValue.baselineHead,
251
283
  parentHead: stateValue.parentHead,
252
284
  ...(baseValue.baselineHistoryLength === undefined
@@ -278,6 +310,7 @@ export class FileNativePlayTimelineStore {
278
310
  events,
279
311
  completedTools,
280
312
  documentAuthorizationCheckpoints: checkpoints,
313
+ promptRuns,
281
314
  changedDocuments: structuredClone(stateValue.changedDocuments),
282
315
  nextMaterials: structuredClone(continuationValue.nextMaterials),
283
316
  nextEventId: stateValue.nextEventId,
@@ -294,9 +327,33 @@ export class FileNativePlayTimelineStore {
294
327
  transcriptCount: stateValue.transcriptCount,
295
328
  completedToolCount: stateValue.completedToolCount,
296
329
  authorizationCheckpointCount: stateValue.authorizationCheckpointCount,
330
+ promptRunCount: stateValue.promptRunCount ?? 0,
297
331
  },
298
332
  };
299
333
  }
334
+ /** The first dispatched request encoding is diagnostic evidence, not Authority. */
335
+ async recordInitialEncoding(worldId, chainId, encoding, exchange = 1) {
336
+ const path = join(this.#contextRoot(worldId, chainId), exchange === 1
337
+ ? "initial-request-encoding.json"
338
+ : `request-encoding-${exchange}.json`);
339
+ await publishImmutableJson(path, encoding);
340
+ }
341
+ async readInitialEncoding(worldId, chainId, exchange = 1) {
342
+ const value = await readOptionalJson(join(this.#contextRoot(worldId, chainId), exchange === 1
343
+ ? "initial-request-encoding.json"
344
+ : `request-encoding-${exchange}.json`));
345
+ if (value === null)
346
+ return null;
347
+ if (!isRecord(value) ||
348
+ value.method !== "POST" ||
349
+ !["chat_completions", "openai_responses", "anthropic_messages"].includes(String(value.provider)) ||
350
+ !isRecord(value.body) ||
351
+ typeof value.endpointPath !== "string" ||
352
+ !Array.isArray(value.headerNames) ||
353
+ !value.headerNames.every((name) => typeof name === "string"))
354
+ throw new Error("Initial request encoding has an invalid shape");
355
+ return value;
356
+ }
300
357
  async readAllContexts(worldId) {
301
358
  const current = await this.readCurrent(worldId);
302
359
  if (current === null)
@@ -312,6 +369,40 @@ export class FileNativePlayTimelineStore {
312
369
  }
313
370
  return reverse.reverse();
314
371
  }
372
+ /** Resolve only requested reply summaries; decoration reads never load Provider transcripts. */
373
+ async resolveReplies(worldId, targets) {
374
+ if (targets.length === 0)
375
+ return [];
376
+ const result = [];
377
+ let chainId = (await this.#readTimelineHead(worldId))?.chainId ?? null;
378
+ while (chainId !== null) {
379
+ const metadata = await this.#readContextMetadata(worldId, chainId);
380
+ if (metadata === null)
381
+ throw new Error("Play timeline points to a missing context");
382
+ const identity = await readOptionalJson(join(metadata.root, "reply-context.json"));
383
+ if (identity !== null &&
384
+ (!isRecord(identity) || typeof identity.contextId !== "string"))
385
+ throw new Error("Play reply context identity is invalid");
386
+ const contextId = identity === null
387
+ ? null
388
+ : identity.contextId;
389
+ for (const target of targets.filter((item) => item.contextId === contextId &&
390
+ item.eventId <= metadata.state.eventCount)) {
391
+ const event = await readJson(join(metadata.root, "summaries", numbered(target.eventId)));
392
+ assertTimelineEventSummary(event);
393
+ if (event.id === target.eventId &&
394
+ event.kind === "assistant" &&
395
+ event.committedHead === target.head)
396
+ result.push({ ...target, chainId });
397
+ }
398
+ if (targets.every((target) => result.some((item) => item.contextId === target.contextId &&
399
+ item.eventId === target.eventId &&
400
+ item.head === target.head)))
401
+ break;
402
+ chainId = metadata.index.previousChainId;
403
+ }
404
+ return result;
405
+ }
315
406
  async readPage(worldId, limit, cursor) {
316
407
  assertIdentity(worldId, "World ID");
317
408
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
@@ -517,6 +608,12 @@ function contextBase(value) {
517
608
  kind: "play_context_frozen",
518
609
  worldId: value.worldId,
519
610
  chainId: value.chainId,
611
+ ...(value.continuityContextId === undefined
612
+ ? {}
613
+ : { continuityContextId: value.continuityContextId }),
614
+ ...(value.presetFiles === undefined
615
+ ? {}
616
+ : { presetFiles: structuredClone(value.presetFiles) }),
520
617
  baselineHead: value.baselineHead,
521
618
  ...(value.baselineHistoryLength === undefined
522
619
  ? {}
@@ -633,6 +730,9 @@ function assertContextBase(value) {
633
730
  typeof value.worldId !== "string" ||
634
731
  typeof value.chainId !== "string" ||
635
732
  typeof value.baselineHead !== "string" ||
733
+ (value.continuityContextId !== undefined &&
734
+ (typeof value.continuityContextId !== "string" ||
735
+ value.continuityContextId.length === 0)) ||
636
736
  (value.modelBinding !== undefined &&
637
737
  !validModelHostBinding(value.modelBinding)) ||
638
738
  !isRecord(value.bootstrap) ||
@@ -652,6 +752,7 @@ function assertContextState(value) {
652
752
  !validCount(value.transcriptCount) ||
653
753
  !validCount(value.completedToolCount) ||
654
754
  !validCount(value.authorizationCheckpointCount) ||
755
+ (value.promptRunCount !== undefined && !validCount(value.promptRunCount)) ||
655
756
  !Array.isArray(value.changedDocuments) ||
656
757
  !validCount(value.nextEventId) ||
657
758
  !validCount(value.exchange) ||
@@ -697,6 +798,9 @@ function isReleasedPlayContext(value) {
697
798
  return false;
698
799
  if (typeof value.chainId !== "string" ||
699
800
  typeof value.baselineHead !== "string" ||
801
+ (value.continuityContextId !== undefined &&
802
+ (typeof value.continuityContextId !== "string" ||
803
+ value.continuityContextId.length === 0)) ||
700
804
  (value.baselineHistoryLength !== undefined &&
701
805
  !validCount(value.baselineHistoryLength)) ||
702
806
  typeof value.parentHead !== "string" ||
@@ -918,7 +1022,12 @@ function assertTimelineEventSummary(value) {
918
1022
  throw new Error("Follow-up timeline summary has an invalid shape");
919
1023
  }
920
1024
  function validAssistantEventCore(value) {
921
- return (typeof value.text === "string" &&
1025
+ return ((value.checkpoint === undefined ||
1026
+ (value.checkpoint === true &&
1027
+ value.status === "completed" &&
1028
+ value.responseKind === "narrative" &&
1029
+ typeof value.committedHead === "string")) &&
1030
+ typeof value.text === "string" &&
922
1031
  (value.status === "streaming" ||
923
1032
  value.status === "completed" ||
924
1033
  value.status === "interrupted") &&
@@ -0,0 +1,71 @@
1
+ import { readPackageFollowups, packageFollowupResources, } from "../content/PackageFollowups.js";
2
+ import { builtinFollowupExample, defaultFollowupItems, } from "../../shared/ordered-followups.js";
3
+ /** Strict portable permissions, shared by import and structured saves. */
4
+ export function parseFollowupItems(value, definitions) {
5
+ if (!Array.isArray(value))
6
+ throw new Error("Follow-up order must be an array");
7
+ const items = value.map((raw) => {
8
+ if (typeof raw !== "object" ||
9
+ raw === null ||
10
+ !("id" in raw) ||
11
+ !("kind" in raw) ||
12
+ !("enabled" in raw) ||
13
+ Object.keys(raw).some((key) => !["id", "kind", "enabled"].includes(key)) ||
14
+ typeof raw.id !== "string" ||
15
+ typeof raw.enabled !== "boolean" ||
16
+ !["user", "builtin", "content-package"].includes(String(raw.kind)))
17
+ throw new Error("Invalid follow-up reference; system content is read-only");
18
+ return {
19
+ id: raw.id,
20
+ kind: raw.kind,
21
+ enabled: raw.enabled,
22
+ };
23
+ });
24
+ if (new Set(items.map((item) => item.id)).size !== items.length ||
25
+ items.filter((item) => item.kind === "builtin").length !== 1 ||
26
+ !items.some((item) => item.kind === "builtin" && item.id === "builtin:summary") ||
27
+ items.filter((item) => item.kind === "content-package").length !== 1 ||
28
+ !items.some((item) => item.kind === "content-package" && item.id === "package:followups") ||
29
+ items.filter((item) => item.kind === "user").length !==
30
+ definitions.length ||
31
+ definitions.some((def) => !items.some((item) => item.kind === "user" && item.id === def.id)))
32
+ throw new Error("Follow-up order must contain every user request, the read-only system example and content-package group exactly once");
33
+ return items;
34
+ }
35
+ /** Resolve each source against its own files, before requests become immutable snapshots. */
36
+ export function effectiveFollowupDefinitions(definition, locale, worldFiles = [], enabled) {
37
+ const packageSource = readPackageFollowups(worldFiles);
38
+ return (definition.followupItems ?? defaultFollowupItems(definition.followups)).flatMap((item) => {
39
+ if (!(item.kind === "content-package"
40
+ ? (enabled?.packageGroup ?? item.enabled)
41
+ : (enabled?.requests.get(item.id) ?? item.enabled)))
42
+ return [];
43
+ if (item.kind === "content-package")
44
+ return packageSource.followups
45
+ .filter((item) => enabled?.requests.get(`package:${item.definition.id}`) ??
46
+ item.enabled)
47
+ .map(({ definition: entry, mount }) => {
48
+ return {
49
+ definition: {
50
+ ...entry,
51
+ id: `package:${entry.id}`,
52
+ artifacts: entry.artifacts.map((artifact) => ({
53
+ ...artifact,
54
+ channel: `package:${artifact.channel}`,
55
+ })),
56
+ },
57
+ body: packageSource.files[entry.prompt.path],
58
+ frozenResources: {
59
+ files: packageFollowupResources(entry, packageSource.files),
60
+ mount,
61
+ },
62
+ };
63
+ });
64
+ if (item.kind === "builtin")
65
+ return [builtinFollowupExample(locale)];
66
+ const entry = definition.followups.find((entry) => entry.id === item.id);
67
+ if (entry === undefined)
68
+ throw new Error(`Missing follow-up definition: ${item.id}`);
69
+ return [{ definition: entry, body: definition.files[entry.prompt.path] }];
70
+ });
71
+ }
@@ -0,0 +1,180 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parseDocument } from "yaml";
3
+ import { builtinPlayPrompts, } from "../../shared/ordered-play-prompts.js";
4
+ /** The same strict permission gate is used by portable imports and structured saves. */
5
+ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
6
+ const fail = () => {
7
+ throw new Error("Invalid ordered play prompts: unique identities, current builtins, enabled mechanics and one world placeholder are required");
8
+ };
9
+ if (!Array.isArray(value) || value.length > 256)
10
+ return fail();
11
+ const ids = new Set();
12
+ const builtins = new Set();
13
+ let worlds = 0;
14
+ const entries = value.map((raw) => {
15
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
16
+ return fail();
17
+ const item = raw;
18
+ if (typeof item.id !== "string" ||
19
+ !/^[a-zA-Z0-9._-]{1,128}$/u.test(item.id) ||
20
+ ids.has(item.id))
21
+ return fail();
22
+ ids.add(item.id);
23
+ const exact = (keys) => Object.keys(item).length === keys.length &&
24
+ keys.every((key) => Object.hasOwn(item, key));
25
+ if (item.kind === "world" && exact(["id", "kind"])) {
26
+ worlds++;
27
+ return { id: item.id, kind: "world" };
28
+ }
29
+ if (item.kind === "user" &&
30
+ exact(["id", "kind", "name", "enabled", "body"]) &&
31
+ typeof item.name === "string" &&
32
+ item.name.trim().length > 0 &&
33
+ /^[^\r\n\0]{1,160}$/u.test(item.name) &&
34
+ typeof item.body === "string" &&
35
+ typeof item.enabled === "boolean")
36
+ return {
37
+ id: item.id,
38
+ kind: "user",
39
+ name: item.name,
40
+ enabled: item.enabled,
41
+ body: item.body,
42
+ };
43
+ const builtin = catalog.find((entry) => entry.id === item.builtin);
44
+ if (item.kind !== "builtin" ||
45
+ !exact(["id", "kind", "builtin", "enabled"]) ||
46
+ !builtin ||
47
+ builtins.has(builtin.id) ||
48
+ typeof item.enabled !== "boolean" ||
49
+ (builtin.required && !item.enabled))
50
+ return fail();
51
+ builtins.add(builtin.id);
52
+ return {
53
+ id: item.id,
54
+ kind: "builtin",
55
+ builtin: builtin.id,
56
+ enabled: item.enabled,
57
+ };
58
+ });
59
+ if (worlds !== (worldRequired ? 1 : 0) ||
60
+ catalog.some((item) => item.required && !builtins.has(item.id)))
61
+ return fail();
62
+ return entries;
63
+ }
64
+ /** Recognized v1 content becomes user text, never a locked builtin inferred from a path. */
65
+ export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
66
+ const document = parseDocument(files["frame.yaml"] ?? "", {
67
+ uniqueKeys: true,
68
+ strict: true,
69
+ });
70
+ const frame = document.toJS({ maxAliasCount: 0 });
71
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
72
+ const roles = ["runtime_system", "author_instruction", "world_context"];
73
+ if (document.errors.length ||
74
+ document.warnings.length ||
75
+ !record(frame) ||
76
+ frame.format !== "narraeon.host-frame/v1" ||
77
+ !record(frame.roles) ||
78
+ Object.keys(frame.roles).length !== 3)
79
+ throw new Error("Legacy host frame is invalid; repair it before migrating");
80
+ const items = {};
81
+ const runtime = new Set();
82
+ let worldInstructions = 0;
83
+ let worldContext = 0;
84
+ let coverage = 0;
85
+ for (const role of roles) {
86
+ items[role] = [];
87
+ const roleItems = frame.roles[role];
88
+ if (!Array.isArray(roleItems))
89
+ throw new Error("Legacy host role must be an array");
90
+ for (const item of roleItems) {
91
+ if (!record(item) || Object.keys(item).length !== 1)
92
+ throw new Error("Legacy host entry is invalid");
93
+ if (role === "author_instruction" && typeof item.markdown === "string")
94
+ items[role].push({ markdown: item.markdown });
95
+ else if (role === "author_instruction" &&
96
+ item.include === "world.instructions") {
97
+ worldInstructions++;
98
+ items[role].push({ include: item.include });
99
+ }
100
+ else if (role === "world_context" && item.include === "world.context") {
101
+ worldContext++;
102
+ items[role].push({ include: item.include });
103
+ }
104
+ else if (role === "world_context" &&
105
+ item.builtin === "runtime.coverage")
106
+ coverage++;
107
+ else if (role === "runtime_system" &&
108
+ typeof item.builtin === "string" &&
109
+ [
110
+ "runtime.play-contract",
111
+ "runtime.tool-contract",
112
+ "runtime.operation-contract",
113
+ ].includes(item.builtin) &&
114
+ !runtime.has(item.builtin))
115
+ runtime.add(item.builtin);
116
+ else
117
+ throw new Error("Legacy host entry is unknown or misplaced");
118
+ }
119
+ }
120
+ if (runtime.size !== 3 ||
121
+ coverage !== 1 ||
122
+ worldInstructions !== 1 ||
123
+ worldContext !== 1)
124
+ throw new Error("Legacy host frame is missing or duplicates required includes");
125
+ const entries = [
126
+ {
127
+ id: "play.mechanics",
128
+ kind: "builtin",
129
+ builtin: "play.mechanics",
130
+ enabled: true,
131
+ },
132
+ ];
133
+ const used = new Set(excludedPaths);
134
+ const occurrences = new Map();
135
+ let world = false;
136
+ const add = (path, enabled) => {
137
+ if (!enabled && used.has(path))
138
+ return;
139
+ const occurrence = occurrences.get(path) ?? 0;
140
+ occurrences.set(path, occurrence + 1);
141
+ used.add(path);
142
+ const body = files[path];
143
+ if (body === undefined)
144
+ throw new Error(`Missing legacy prompt: ${path}`);
145
+ const heading = /^#[ \t]+([^\r\n]+)$/mu.exec(body)?.[1]?.trim() ?? "";
146
+ const stem = path.replace(/^.*\//u, "").replace(/\.md$/u, "");
147
+ const name = heading.length > 0 ? heading : stem.length > 0 ? stem : "Prompt";
148
+ entries.push({
149
+ id: `legacy-${createHash("sha256").update(path).digest("hex").slice(0, 32)}-${occurrence}`,
150
+ kind: "user",
151
+ name: name.slice(0, 160),
152
+ enabled,
153
+ body,
154
+ });
155
+ };
156
+ for (const role of [
157
+ "runtime_system",
158
+ "author_instruction",
159
+ "world_context",
160
+ ]) {
161
+ for (const item of items[role] ?? []) {
162
+ if (item.markdown)
163
+ add(item.markdown, true);
164
+ if (item.include?.startsWith("world.") && !world) {
165
+ entries.push({ id: "world", kind: "world" });
166
+ world = true;
167
+ }
168
+ }
169
+ if (role === "author_instruction")
170
+ for (const prompt of narrative)
171
+ add(prompt.path, true);
172
+ }
173
+ if (!world)
174
+ entries.push({ id: "world", kind: "world" });
175
+ for (const path of Object.keys(files).sort())
176
+ if ((path.startsWith("blocks/") || path.startsWith("prompts/")) &&
177
+ path.endsWith(".md"))
178
+ add(path, false);
179
+ return entries;
180
+ }