narraeon 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +186 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +445 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-DG85hpWC.css +1 -0
  70. package/dist/web/assets/index-DfxvCjb6.js +1207 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D8YKfmNG.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,14 +1,56 @@
1
+ import { parseDisplayRegex, DisplayRegexError, } from "../../shared/display-regex.js";
2
+ export { validatePlayPresetArtifactPayload } from "../../shared/artifact-payload-contract.js";
3
+ import { defaultFollowupItems, } from "../../shared/ordered-followups.js";
4
+ import { parseFollowupItems } from "./OrderedFollowups.js";
5
+ import { builtinAuthorPrompts, defaultOrderedAuthorPrompts, } from "../../shared/ordered-author-prompts.js";
6
+ import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts, } from "./OrderedPlayPrompts.js";
7
+ import { defaultOrderedPlayPrompts, } from "../../shared/ordered-play-prompts.js";
1
8
  import { createHash, randomUUID } from "node:crypto";
2
9
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
10
  import { join } from "node:path";
4
11
  import { isDeepStrictEqual } from "node:util";
5
- import { parseDocument } from "yaml";
12
+ import { parseDocument, stringify, visit, isAlias } from "yaml";
6
13
  import { defaultPortableContentTreeLimits, portableContentTreePathKey, } from "../../protocol/contentTree.js";
7
14
  import { defaultAppLocale, } from "../../protocol/appPreferences.js";
8
15
  import { defaultNarrationPromptForLocale } from "../../shared/default-play-prompts.js";
9
16
  import { defaultPresetHostFilesForLocale } from "../../shared/default-preset-host.js";
10
17
  import { defaultSettingImprovementPromptForLocale, defaultSettingImprovementPromptPath, } from "../../shared/default-setting-improvement-prompt.js";
11
18
  import { isPlayPresetPromptRole, } from "../../shared/play-preset-prompt-roles.js";
19
+ export function isFrozenArtifactPresentation(value) {
20
+ if (!isRecord(value) ||
21
+ !hasExactKeys(value, ["declaration", "files", "mount"]) ||
22
+ !isRecord(value.declaration) ||
23
+ !isRecord(value.files) ||
24
+ !Object.values(value.files).every((body) => typeof body === "string"))
25
+ return false;
26
+ try {
27
+ const original = value.declaration;
28
+ const parsed = parseArtifacts([
29
+ {
30
+ ...original,
31
+ ...(original.channel === "builtin:summary" ||
32
+ (typeof original.channel === "string" &&
33
+ /^package:[a-z][a-z0-9._/-]{1,127}$/u.test(original.channel))
34
+ ? { channel: "frozen.presentation" }
35
+ : {}),
36
+ },
37
+ ], value.files, "frozen artifact")[0];
38
+ parsed.channel = String(original.channel);
39
+ return (isDeepStrictEqual(parsed, original) &&
40
+ typeof value.mount === "string" &&
41
+ [
42
+ "story",
43
+ "sidebar",
44
+ "composer_above",
45
+ "composer_below",
46
+ "overlay",
47
+ "debug",
48
+ ].includes(String(value.mount)));
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
12
54
  /**
13
55
  * The frame plus its block library, projected in the shape the prompt compiler
14
56
  * already consumes. A preset owns both halves now: `frame.yaml` decides which
@@ -55,7 +97,9 @@ export function isPlayPresetBinding(value) {
55
97
  value.revision === "builtin-default-v1" &&
56
98
  value.scriptsEnabled === true &&
57
99
  (isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("en")) ||
58
- isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("zh-CN")));
100
+ isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("zh-CN")) ||
101
+ isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("en")) ||
102
+ isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("zh-CN")));
59
103
  return (parsed.kind === "valid" &&
60
104
  isDeepStrictEqual(value.definition, parsed.definition) &&
61
105
  (builtInDefault || value.revision === revisionForPlayPresetFiles(files)));
@@ -68,7 +112,7 @@ export class FileNativePlayPresetError extends Error {
68
112
  this.name = "FileNativePlayPresetError";
69
113
  }
70
114
  }
71
- export function defaultPlayPresetFilesForLocale(locale) {
115
+ export function legacyDefaultPlayPresetFilesForLocale(locale) {
72
116
  return {
73
117
  "preset.yaml": `format: narraeon.play-preset/v1
74
118
  name: ${locale === "zh-CN" ? "系统推荐" : "System recommended"}
@@ -88,6 +132,15 @@ followups: []
88
132
  [defaultSettingImprovementPromptPath]: defaultSettingImprovementPromptForLocale(locale),
89
133
  };
90
134
  }
135
+ export function defaultPlayPresetFilesForLocale(locale) {
136
+ const files = legacyDefaultPlayPresetFilesForLocale(locale);
137
+ const preset = parseDocument(files["preset.yaml"]);
138
+ preset.set("format", "narraeon.play-preset/v2");
139
+ preset.set("playPrompts", defaultOrderedPlayPrompts());
140
+ preset.set("authorPrompts", defaultOrderedAuthorPrompts());
141
+ files["preset.yaml"] = stringify(preset.toJS());
142
+ return files;
143
+ }
91
144
  export const defaultPlayPresetFiles = defaultPlayPresetFilesForLocale(defaultAppLocale);
92
145
  /**
93
146
  * Resolve the author-owned part of setting improvement from a frozen preset.
@@ -112,7 +165,7 @@ export function builtinDefaultPlayPresetBinding(locale = defaultAppLocale) {
112
165
  return {
113
166
  id: "builtin-default",
114
167
  name: "default",
115
- revision: "builtin-default-v1",
168
+ revision: revisionForPlayPresetFiles(files),
116
169
  definition: parsed.definition,
117
170
  files,
118
171
  scriptsEnabled: true,
@@ -217,6 +270,42 @@ export class FileNativePlayPresetStore {
217
270
  if (input.structure !== undefined) {
218
271
  files = applyPlayPresetStructuredEditor(input.files, parsePlayPresetStructuredEditor(input.structure));
219
272
  }
273
+ const previous = parsePlayPresetFiles(stored.revisions[stored.currentRevision]);
274
+ if (previous.kind === "valid" &&
275
+ previous.definition.followupItems !== undefined) {
276
+ const candidate = readYaml(files, "call-chain.yaml", "call-chain.yaml");
277
+ if (candidate.format !== "narraeon.play-call-chain/v2")
278
+ throw new FileNativePlayPresetError("readonly_followup_removed", "Ordered followups cannot downgrade to v1");
279
+ parseFollowupItems(candidate.followupItems, parseFollowups(candidate.followups, files));
280
+ }
281
+ if (previous.kind === "valid" &&
282
+ previous.definition.playPrompts !== undefined) {
283
+ const candidate = readYaml(files, "preset.yaml", "preset.yaml");
284
+ if (candidate.format !== "narraeon.play-preset/v2")
285
+ throw new FileNativePlayPresetError("readonly_prompt_removed", "Ordered presets cannot downgrade to v1");
286
+ const candidates = parseOrderedPlayPrompts(candidate.playPrompts);
287
+ for (const entry of previous.definition.playPrompts.filter((entry) => entry.kind !== "user")) {
288
+ const retained = candidates.find((item) => item.id === entry.id);
289
+ if (retained?.kind !== entry.kind ||
290
+ (entry.kind === "builtin" &&
291
+ retained.kind === "builtin" &&
292
+ retained.builtin !== entry.builtin))
293
+ throw new FileNativePlayPresetError("readonly_prompt_removed", "System prompts and the world placeholder cannot be deleted or replaced; disable optional prompts or clone them instead");
294
+ }
295
+ }
296
+ if (previous.kind === "valid" &&
297
+ previous.definition.authorPrompts !== undefined) {
298
+ const candidate = readYaml(files, "preset.yaml", "preset.yaml");
299
+ const entries = parseOrderedAuthorPrompts(candidate.authorPrompts);
300
+ for (const entry of previous.definition.authorPrompts) {
301
+ if (entry.kind !== "builtin")
302
+ continue;
303
+ const retained = entries.find((item) => item.id === entry.id);
304
+ if (retained?.kind !== "builtin" ||
305
+ retained.builtin !== entry.builtin)
306
+ throw new FileNativePlayPresetError("readonly_prompt_removed", "Authoring system prompts cannot be deleted or replaced; disable or clone optional prompts");
307
+ }
308
+ }
220
309
  const revision = revisionForFiles(files, this.#limits);
221
310
  stored.name = normalizeName(input.name);
222
311
  stored.revisions[revision] = cloneFiles(files);
@@ -363,6 +452,10 @@ export class FileNativePlayPresetStore {
363
452
  }
364
453
  async importPortable(input) {
365
454
  const files = toFileMap(input.files, this.#limits);
455
+ const parsed = parsePlayPresetFiles(files, this.#limits);
456
+ if (parsed.kind === "invalid")
457
+ throw parsed.error;
458
+ toPlayPresetStructuredEditor(parsed.definition);
366
459
  return this.#change(async () => {
367
460
  const document = await this.#read();
368
461
  const stored = this.#stored(randomUUID(), input.name, files);
@@ -460,7 +553,9 @@ export class FileNativePlayPresetStore {
460
553
  const files = preset.revisions[preset.currentRevision];
461
554
  if (files === undefined ||
462
555
  (!isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("en")) &&
463
- !isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("zh-CN"))))
556
+ !isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("zh-CN"))) ||
557
+ isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("en")) ||
558
+ isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("zh-CN")))
464
559
  return false;
465
560
  preset.builtinDefault = true;
466
561
  return true;
@@ -469,13 +564,10 @@ export class FileNativePlayPresetStore {
469
564
  const preset = document.presets.find(({ builtinDefault }) => builtinDefault === true);
470
565
  if (preset === undefined || preset.draftRevision !== undefined)
471
566
  return false;
472
- const current = preset.revisions[preset.currentRevision];
473
- if (current === undefined ||
474
- (!isDeepStrictEqual(current, defaultPlayPresetFilesForLocale("en")) &&
475
- !isDeepStrictEqual(current, defaultPlayPresetFilesForLocale("zh-CN")))) {
476
- delete preset.builtinDefault;
477
- return true;
478
- }
567
+ // The marker, not equality with this build's defaults, identifies the
568
+ // Runtime-owned seed. A prior application version necessarily carries a
569
+ // different file revision after shipped prompts change. Every user edit or
570
+ // rename clears the marker before saving, so those presets remain intact.
479
571
  const desired = defaultPlayPresetFilesForLocale(this.#locale());
480
572
  const revision = revisionForFiles(desired, this.#limits);
481
573
  if (preset.currentRevision === revision)
@@ -501,21 +593,15 @@ export class FileNativePlayPresetStore {
501
593
  const files = stored.revisions[stored.currentRevision];
502
594
  if (files === undefined)
503
595
  throw new FileNativePlayPresetError("store_invalid", "The current play-preset revision does not exist");
504
- const validation = validatePlayPresetFiles(files, this.#limits);
505
596
  const view = {
506
597
  id: stored.id,
507
598
  name: stored.name,
508
599
  revision: stored.currentRevision,
509
600
  files: cloneFiles(files),
510
- validation,
601
+ ...editorProjection(files, this.#limits),
511
602
  enabled: stored.enabled,
512
603
  scriptsEnabled: stored.scriptsEnabled,
513
604
  };
514
- if (validation.status === "valid") {
515
- const parsed = parsePlayPresetFiles(files, this.#limits);
516
- if (parsed.kind === "valid")
517
- view.structure = toPlayPresetStructuredEditor(parsed.definition);
518
- }
519
605
  if (stored.draftRevision !== undefined) {
520
606
  const draftFiles = stored.revisions[stored.draftRevision];
521
607
  if (draftFiles === undefined)
@@ -523,13 +609,8 @@ export class FileNativePlayPresetStore {
523
609
  view.draft = {
524
610
  revision: stored.draftRevision,
525
611
  files: cloneFiles(draftFiles),
526
- validation: validatePlayPresetFiles(draftFiles, this.#limits),
612
+ ...editorProjection(draftFiles, this.#limits),
527
613
  };
528
- if (view.draft.validation.status === "valid") {
529
- const parsedDraft = parsePlayPresetFiles(draftFiles, this.#limits);
530
- if (parsedDraft.kind === "valid")
531
- view.draft.structure = toPlayPresetStructuredEditor(parsedDraft.definition);
532
- }
533
614
  }
534
615
  return view;
535
616
  }
@@ -566,9 +647,47 @@ export function validatePlayPresetFiles(files, limits = {}) {
566
647
  };
567
648
  }
568
649
  }
650
+ function editorProjection(files, limits) {
651
+ const validation = validatePlayPresetFiles(files, limits);
652
+ if (validation.status === "invalid")
653
+ return { validation };
654
+ const parsed = parsePlayPresetFiles(files, limits);
655
+ if (parsed.kind === "invalid")
656
+ return { validation };
657
+ try {
658
+ return {
659
+ validation,
660
+ structure: toPlayPresetStructuredEditor(parsed.definition),
661
+ };
662
+ }
663
+ catch (error) {
664
+ return {
665
+ validation: {
666
+ status: "invalid",
667
+ code: "legacy_preset_migration_invalid",
668
+ location: "frame.yaml",
669
+ message: error instanceof Error
670
+ ? error.message
671
+ : "Legacy preset cannot be migrated",
672
+ },
673
+ };
674
+ }
675
+ }
569
676
  export function toPlayPresetStructuredEditor(definition) {
570
677
  return {
571
678
  name: definition.name,
679
+ authorPrompts: structuredClone(definition.authorPrompts ?? legacyAuthorPrompts(definition)),
680
+ playPrompts: structuredClone(definition.playPrompts ??
681
+ migrateLegacyPlayPrompts(definition.files, definition.narrativePrompts, [
682
+ definition.settingImprovementPrompt?.path ??
683
+ defaultSettingImprovementPromptPath,
684
+ ...definition.followups.map((followup) => followup.prompt.path),
685
+ ])),
686
+ ...(definition.playPrompts
687
+ ? {}
688
+ : {
689
+ migrationNotice: "旧世界指令与材料归并到首次世界位置,其他提示相对顺序及正文保留;历史请求不变。保存后采用统一编排。",
690
+ }),
572
691
  callChainPath: definition.callChainPath,
573
692
  ...(definition.settingImprovementPrompt === undefined
574
693
  ? {}
@@ -580,6 +699,7 @@ export function toPlayPresetStructuredEditor(definition) {
580
699
  extensionRefs: [...definition.extensionRefs],
581
700
  narrativePrompts: structuredClone(definition.narrativePrompts),
582
701
  followups: structuredClone(definition.followups),
702
+ followupItems: structuredClone(definition.followupItems ?? defaultFollowupItems(definition.followups)),
583
703
  };
584
704
  }
585
705
  /**
@@ -606,6 +726,12 @@ export function applyPlayPresetStructuredEditor(files, input) {
606
726
  if (preset.errors.length > 0 || callChain.errors.length > 0)
607
727
  throw new FileNativePlayPresetError("structured_editor_yaml_invalid", "Structured editing can be applied only to parseable preset/call-chain YAML");
608
728
  preset.set("name", input.name);
729
+ if (input.authorPrompts !== undefined)
730
+ preset.set("authorPrompts", parseOrderedAuthorPrompts(input.authorPrompts));
731
+ if (input.playPrompts !== undefined) {
732
+ preset.set("format", "narraeon.play-preset/v2");
733
+ preset.set("playPrompts", parseOrderedPlayPrompts(input.playPrompts));
734
+ }
609
735
  preset.set("callChain", input.callChainPath);
610
736
  if (input.settingImprovementPrompt === undefined)
611
737
  preset.delete("settingImprovement");
@@ -621,6 +747,10 @@ export function applyPlayPresetStructuredEditor(files, input) {
621
747
  role,
622
748
  markdown: path,
623
749
  })));
750
+ if (input.followupItems !== undefined) {
751
+ callChain.set("format", "narraeon.play-call-chain/v2");
752
+ callChain.set("followupItems", parseFollowupItems(input.followupItems, input.followups));
753
+ }
624
754
  callChain.set("followups", input.followups.map((followup) => ({
625
755
  id: followup.id,
626
756
  displayName: followup.displayName,
@@ -705,7 +835,16 @@ export function parsePlayPresetStructuredEditor(value) {
705
835
  value.playerViewPanels.some((panel) => !isRecord(panel))))
706
836
  throw new FileNativePlayPresetError("structured_editor_invalid", "Structured play playerViewPanels must be an array of maps");
707
837
  return {
838
+ ...(value.followupItems === undefined
839
+ ? {}
840
+ : { followupItems: parseFollowupItems(value.followupItems, followups) }),
708
841
  name: value.name,
842
+ ...(value.authorPrompts === undefined
843
+ ? {}
844
+ : { authorPrompts: parseOrderedAuthorPrompts(value.authorPrompts) }),
845
+ ...(value.playPrompts === undefined
846
+ ? {}
847
+ : { playPrompts: parseOrderedPlayPrompts(value.playPrompts) }),
709
848
  callChainPath: value.callChainPath,
710
849
  ...(settingImprovementPrompt === undefined
711
850
  ? {}
@@ -732,6 +871,8 @@ export function parsePlayPresetFiles(files, limits = {}) {
732
871
  "name",
733
872
  "callChain",
734
873
  "settingImprovement",
874
+ "playPrompts",
875
+ "authorPrompts",
735
876
  "mounts",
736
877
  "playerViewPanels",
737
878
  "extensions",
@@ -739,15 +880,20 @@ export function parsePlayPresetFiles(files, limits = {}) {
739
880
  const callChainPath = stringValue(preset.callChain);
740
881
  if (callChainPath !== "call-chain.yaml")
741
882
  invalid("call_chain_path_invalid", "Entry callChain must be call-chain.yaml", "preset.yaml");
742
- if (preset.format !== "narraeon.play-preset/v1")
883
+ if (preset.format !== "narraeon.play-preset/v1" &&
884
+ preset.format !== "narraeon.play-preset/v2")
743
885
  invalid("preset_format_invalid", "preset.yaml format must be narraeon.play-preset/v1", "preset.yaml");
886
+ if (preset.format === "narraeon.play-preset/v1" &&
887
+ (preset.playPrompts !== undefined || preset.authorPrompts !== undefined))
888
+ invalid("preset_format_invalid", "Ordered prompts require v2", "preset.yaml");
744
889
  const name = stringValue(preset.name);
745
890
  if (!namePattern.test(name.trim()) || name.includes(String.fromCharCode(0)))
746
891
  invalid("invalid_name", "Play-preset name must contain 1 to 160 characters", "preset.yaml");
747
892
  const callChain = readYaml(files, callChainPath, "call-chain.yaml");
748
893
  assertNoEditableMechanics(callChain, callChainPath);
749
- assertKnownKeys(callChain, ["format", "narrative", "followups"], callChainPath);
750
- if (callChain.format !== "narraeon.play-call-chain/v1")
894
+ assertKnownKeys(callChain, ["format", "narrative", "followups", "followupItems"], callChainPath);
895
+ if (callChain.format !== "narraeon.play-call-chain/v1" &&
896
+ callChain.format !== "narraeon.play-call-chain/v2")
751
897
  invalid("call_chain_format_invalid", "call-chain.yaml format must be narraeon.play-call-chain/v1", callChainPath);
752
898
  // The host preset already carries the general narrative criteria, so a
753
899
  // preset that adds nothing of its own is legitimate.
@@ -756,6 +902,12 @@ export function parsePlayPresetFiles(files, limits = {}) {
756
902
  ? undefined
757
903
  : parsePromptBlocks([preset.settingImprovement], files, "preset.yaml#settingImprovement")[0];
758
904
  const followups = parseFollowups(callChain.followups, files);
905
+ if (callChain.format === "narraeon.play-call-chain/v1" &&
906
+ callChain.followupItems !== undefined)
907
+ invalid("followups_invalid", "Ordered followups require call-chain v2", callChainPath);
908
+ const followupItems = callChain.format === "narraeon.play-call-chain/v2"
909
+ ? parseFollowupItems(callChain.followupItems, followups)
910
+ : undefined;
759
911
  const mounts = parseMounts(preset.mounts);
760
912
  const playerViewPanels = parsePlayerViewPanels(preset.playerViewPanels, files);
761
913
  validatePanelChannels(followups, playerViewPanels);
@@ -764,7 +916,14 @@ export function parsePlayPresetFiles(files, limits = {}) {
764
916
  return {
765
917
  kind: "valid",
766
918
  definition: {
767
- format: "narraeon.play-preset/v1",
919
+ ...(followupItems === undefined ? {} : { followupItems }),
920
+ format: preset.format,
921
+ ...(preset.format === "narraeon.play-preset/v2"
922
+ ? { playPrompts: parseOrderedPlayPrompts(preset.playPrompts) }
923
+ : {}),
924
+ ...(preset.authorPrompts === undefined
925
+ ? {}
926
+ : { authorPrompts: parseOrderedAuthorPrompts(preset.authorPrompts) }),
768
927
  name: name.trim(),
769
928
  callChainPath,
770
929
  ...(settingImprovementPrompt === undefined
@@ -814,10 +973,10 @@ function parsePromptBlocks(value, files, location, allowEmpty = false) {
814
973
  });
815
974
  }
816
975
  /**
817
- * Follow-ups have no ordering contract. The parser needs only each request's
976
+ * User definitions retain their resources independently of order and enablement. The parser needs each request's
818
977
  * identity, its single author prompt, and the artifacts it may emit.
819
978
  */
820
- function parseFollowups(value, files) {
979
+ export function parseFollowups(value, files) {
821
980
  if (value === undefined || value === null)
822
981
  return [];
823
982
  if (!Array.isArray(value))
@@ -855,7 +1014,7 @@ function parseFollowups(value, files) {
855
1014
  invalid("duplicate_followup_id", "Follow-up request id is duplicated", "call-chain.yaml");
856
1015
  // One channel has one projection meaning. Two followups writing the same
857
1016
  // channel with different strategies would make the visible set depend on
858
- // dispatch order, which followups deliberately do not have.
1017
+ // dispatch order; ordering must not create a data dependency.
859
1018
  const strategies = new Map();
860
1019
  for (const { artifacts } of followups)
861
1020
  for (const { channel, strategy } of artifacts) {
@@ -903,7 +1062,19 @@ function parseArtifacts(value, files, location) {
903
1062
  "required",
904
1063
  "maxEmits",
905
1064
  "payloadContract",
1065
+ "displayName",
1066
+ "purpose",
906
1067
  ], artifactLocation);
1068
+ for (const [field, limit] of [
1069
+ ["displayName", 160],
1070
+ ["purpose", 16000],
1071
+ ]) {
1072
+ if (raw[field] !== undefined &&
1073
+ (typeof raw[field] !== "string" ||
1074
+ raw[field].length > limit ||
1075
+ raw[field].includes("\0")))
1076
+ invalid("artifact_metadata_invalid", `${field} must be text of at most ${limit} characters`, artifactLocation);
1077
+ }
907
1078
  const name = stringValue(raw.name).trim();
908
1079
  if (!/^[a-z][a-z0-9._/-]{1,127}$/u.test(name))
909
1080
  invalid("artifact_output_name_invalid", "An artifact must declare a stable lowercase output name", artifactLocation);
@@ -914,7 +1085,8 @@ function parseArtifacts(value, files, location) {
914
1085
  if (!/^[a-z][a-z0-9._/-]{1,127}$/u.test(channel))
915
1086
  invalid("channel_invalid", "Artifact channel must be a stable lowercase address", artifactLocation);
916
1087
  const strategy = raw.strategy;
917
- if (!["append", "replace", "upsert", "transient", "hidden"].includes(String(strategy)))
1088
+ if (typeof strategy !== "string" ||
1089
+ !["append", "replace", "upsert", "transient", "hidden"].includes(String(strategy)))
918
1090
  invalid("channel_strategy_invalid", "Artifact channel policy is invalid", artifactLocation);
919
1091
  const contentType = raw.contentType ?? "text/plain";
920
1092
  if (typeof contentType !== "string" ||
@@ -984,6 +1156,10 @@ function parseArtifacts(value, files, location) {
984
1156
  invalid("artifact_payload_contract_type_invalid", "A structured payload contract can be used only with an application/json artifact", artifactLocation);
985
1157
  return {
986
1158
  name,
1159
+ ...(raw.displayName === undefined
1160
+ ? {}
1161
+ : { displayName: raw.displayName }),
1162
+ ...(raw.purpose === undefined ? {} : { purpose: raw.purpose }),
987
1163
  channel,
988
1164
  strategy: strategy,
989
1165
  ...(key === undefined ? {} : { key }),
@@ -1012,6 +1188,19 @@ function parseArtifactPayloadContract(value, location, state = { nodes: 0 }, dep
1012
1188
  invalid("artifact_payload_contract_too_complex", "payloadContract contains more than 512 nodes", location);
1013
1189
  if (!isRecord(value))
1014
1190
  invalid("artifact_payload_contract_invalid", "payloadContract must be a map", location);
1191
+ assertKnownKeys(value, [
1192
+ "type",
1193
+ "properties",
1194
+ "required",
1195
+ "additionalProperties",
1196
+ "items",
1197
+ "minItems",
1198
+ "maxItems",
1199
+ "minLength",
1200
+ "maxLength",
1201
+ "uniqueBy",
1202
+ "maxBytes",
1203
+ ], location);
1015
1204
  const type = value.type;
1016
1205
  if (type !== "object" &&
1017
1206
  type !== "array" &&
@@ -1387,83 +1576,14 @@ function validatePresetReferences(preset, files) {
1387
1576
  return paths;
1388
1577
  }
1389
1578
  export function parsePlayPresetRegexAsset(source, path = "regex/inline.yaml") {
1390
- const document = parseDocument(source, {
1391
- schema: "core",
1392
- uniqueKeys: true,
1393
- strict: true,
1394
- });
1395
- if (document.errors.length > 0 || document.warnings.length > 0)
1396
- invalid("regex_asset_invalid", `Regex-resource YAML is invalid: ${path}`, path);
1397
- const value = document.toJS({ maxAliasCount: 0 });
1398
- const rules = Array.isArray(value)
1399
- ? value
1400
- : isRecord(value) && Array.isArray(value.rules)
1401
- ? value.rules
1402
- : null;
1403
- if (rules === null || rules.length > 256)
1404
- invalid("regex_rules_invalid", `A regex resource may contain at most 256 rules: ${path}`, path);
1405
- const orders = new Set();
1406
- const parsed = rules.map((raw, index) => {
1407
- const location = `${path}#rules[${index}]`;
1408
- if (!isRecord(raw))
1409
- invalid("regex_rule_invalid", "A regex rule must be a map", location);
1410
- assertKnownKeys(raw, [
1411
- "order",
1412
- "scope",
1413
- "pattern",
1414
- "flags",
1415
- "replace",
1416
- "maxMatches",
1417
- "errorPolicy",
1418
- ], location);
1419
- const order = raw.order;
1420
- if (!Number.isSafeInteger(order) || order < 0)
1421
- invalid("regex_order_invalid", "A regex rule must declare a non-negative integer order", location);
1422
- if (orders.has(order))
1423
- invalid("regex_order_duplicate", "Regex rule order cannot be duplicated", location);
1424
- orders.add(order);
1425
- const scope = raw.scope;
1426
- if (scope !== "raw_text" &&
1427
- scope !== "markdown_html" &&
1428
- scope !== "structured_payload")
1429
- invalid("regex_scope_invalid", "A regex rule must declare raw_text, markdown_html, or structured_payload scope", location);
1430
- const pattern = raw.pattern;
1431
- if (typeof pattern !== "string" ||
1432
- pattern.length === 0 ||
1433
- pattern.length > 4_096)
1434
- invalid("regex_pattern_invalid", "Regex pattern must be a bounded non-empty string", location);
1435
- const flags = raw.flags === undefined ? "" : raw.flags;
1436
- if (typeof flags !== "string" || flags.length > 16)
1437
- invalid("regex_flags_invalid", "Regex flags are invalid", location);
1438
- try {
1439
- new RegExp(pattern, flags);
1440
- }
1441
- catch {
1442
- invalid("regex_pattern_invalid", "Regex pattern could not be compiled", location);
1443
- }
1444
- if (typeof raw.replace !== "string")
1445
- invalid("regex_replace_invalid", "Regex replace must be text", location);
1446
- const maxMatches = raw.maxMatches ?? 1;
1447
- if (!Number.isSafeInteger(maxMatches) ||
1448
- maxMatches < 1 ||
1449
- maxMatches > 1_024)
1450
- invalid("regex_limit_invalid", "Regex replacement count must be a bounded positive integer", location);
1451
- const errorPolicy = raw.errorPolicy;
1452
- if (errorPolicy !== "fallback" &&
1453
- errorPolicy !== "skip" &&
1454
- errorPolicy !== "fail")
1455
- invalid("regex_error_policy_invalid", "A regex rule must declare fallback, skip, or fail errorPolicy", location);
1456
- return {
1457
- order: order,
1458
- scope,
1459
- pattern,
1460
- flags,
1461
- replace: raw.replace,
1462
- maxMatches: maxMatches,
1463
- errorPolicy,
1464
- };
1465
- });
1466
- return parsed.sort((left, right) => left.order - right.order);
1579
+ try {
1580
+ return parseDisplayRegex(source, path);
1581
+ }
1582
+ catch (error) {
1583
+ if (error instanceof DisplayRegexError)
1584
+ throw new FileNativePlayPresetError(error.code, error.message);
1585
+ throw error;
1586
+ }
1467
1587
  }
1468
1588
  function validateRegexAsset(source, path) {
1469
1589
  parsePlayPresetRegexAsset(source, path);
@@ -1562,11 +1682,6 @@ function readYaml(files, path, location) {
1562
1682
  const source = files[path];
1563
1683
  if (source === undefined || source.trim() === "")
1564
1684
  invalid("required_file_missing", `Play-preset file does not exist: ${path}`, location);
1565
- if (/(^|\s)[&*!][^\s,\]}]+/mu.test(source) ||
1566
- /^\s*<<\s*:/mu.test(source) ||
1567
- /^---\s*$/mu.test(source) ||
1568
- /^\.\.\.\s*$/mu.test(source))
1569
- invalid("unsafe_yaml", `Play-preset YAML uses syntax forbidden by the restricted codec: ${path}`, location);
1570
1685
  const document = parseDocument(source, {
1571
1686
  schema: "core",
1572
1687
  uniqueKeys: true,
@@ -1574,6 +1689,16 @@ function readYaml(files, path, location) {
1574
1689
  });
1575
1690
  if (document.errors.length > 0 || document.warnings.length > 0)
1576
1691
  invalid("unsafe_yaml", `Play-preset YAML is invalid: ${path}`, location);
1692
+ visit(document, {
1693
+ Node(_, node) {
1694
+ if (isAlias(node) || ("anchor" in node && node.anchor) || node.tag)
1695
+ invalid("unsafe_yaml", `Play-preset YAML uses forbidden anchors, aliases or tags: ${path}`, location);
1696
+ },
1697
+ Pair(_, pair) {
1698
+ if (String(pair.key) === "<<")
1699
+ invalid("unsafe_yaml", `Play-preset YAML uses forbidden merge keys: ${path}`, location);
1700
+ },
1701
+ });
1577
1702
  const value = document.toJS({ maxAliasCount: 0 });
1578
1703
  if (!isRecord(value))
1579
1704
  invalid("yaml_shape_invalid", `Top-level play-preset YAML must be a map: ${path}`, location);
@@ -1719,129 +1844,6 @@ function stringValue(value) {
1719
1844
  function cloneFiles(files) {
1720
1845
  return Object.fromEntries(Object.entries(files).map(([path, contents]) => [path, contents]));
1721
1846
  }
1722
- export function validatePlayPresetArtifactPayload(contract, value) {
1723
- if (contract === undefined)
1724
- return { ok: true };
1725
- if (!isJsonValueValue(value))
1726
- return {
1727
- ok: false,
1728
- message: "JSON artifact payload must be a valid JSON value",
1729
- };
1730
- return validatePayloadNode(contract, value, "$", 0);
1731
- }
1732
- function validatePayloadNode(contract, value, path, depth) {
1733
- if (depth > 32)
1734
- return { ok: false, message: `${path} payload nesting exceeds 32 levels` };
1735
- const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1736
- const typeMatches = contract.type === actual ||
1737
- (contract.type === "number" && actual === "number") ||
1738
- (contract.type === "integer" &&
1739
- actual === "number" &&
1740
- Number.isInteger(value));
1741
- if (!typeMatches)
1742
- return {
1743
- ok: false,
1744
- message: `${path} payload type must be ${contract.type}; received ${actual}`,
1745
- };
1746
- if (contract.maxBytes !== undefined) {
1747
- const encoded = JSON.stringify(value);
1748
- if (encoded === undefined ||
1749
- Buffer.byteLength(encoded, "utf8") > contract.maxBytes)
1750
- return {
1751
- ok: false,
1752
- message: `${path} payload exceeds its declared ${contract.maxBytes}-byte limit`,
1753
- };
1754
- }
1755
- if (contract.type === "string") {
1756
- const text = value;
1757
- if (contract.minLength !== undefined &&
1758
- [...text].length < contract.minLength)
1759
- return {
1760
- ok: false,
1761
- message: `${path} text length is less than ${contract.minLength}`,
1762
- };
1763
- if (contract.maxLength !== undefined &&
1764
- [...text].length > contract.maxLength)
1765
- return {
1766
- ok: false,
1767
- message: `${path} text length exceeds ${contract.maxLength}`,
1768
- };
1769
- }
1770
- if (contract.type === "array") {
1771
- const entries = value;
1772
- if (contract.minItems !== undefined && entries.length < contract.minItems)
1773
- return {
1774
- ok: false,
1775
- message: `${path} has fewer than ${contract.minItems} items`,
1776
- };
1777
- if (contract.maxItems !== undefined && entries.length > contract.maxItems)
1778
- return {
1779
- ok: false,
1780
- message: `${path} has more than ${contract.maxItems} items`,
1781
- };
1782
- const seen = new Set();
1783
- for (const [index, entry] of entries.entries()) {
1784
- if (contract.items !== undefined) {
1785
- const result = validatePayloadNode(contract.items, entry, `${path}[${index}]`, depth + 1);
1786
- if (!result.ok)
1787
- return result;
1788
- }
1789
- if (contract.uniqueBy !== undefined && isRecordValue(entry)) {
1790
- const unique = entry[contract.uniqueBy];
1791
- if (typeof unique !== "string" &&
1792
- typeof unique !== "number" &&
1793
- typeof unique !== "boolean")
1794
- return {
1795
- ok: false,
1796
- message: `${path}[${index}].${contract.uniqueBy} must be a uniquely comparable scalar`,
1797
- };
1798
- const key = JSON.stringify(unique);
1799
- if (seen.has(key))
1800
- return {
1801
- ok: false,
1802
- message: `${path} ${contract.uniqueBy} values cannot be duplicated`,
1803
- };
1804
- seen.add(key);
1805
- }
1806
- }
1807
- }
1808
- if (contract.type === "object") {
1809
- const object = value;
1810
- const properties = contract.properties ?? {};
1811
- for (const required of contract.required ?? [])
1812
- if (!(required in object))
1813
- return { ok: false, message: `${path}.${required} is required` };
1814
- if (contract.additionalProperties === false)
1815
- for (const key of Object.keys(object))
1816
- if (!(key in properties))
1817
- return {
1818
- ok: false,
1819
- message: `${path}.${key} is not a declared field`,
1820
- };
1821
- for (const [key, child] of Object.entries(properties)) {
1822
- if (!(key in object))
1823
- continue;
1824
- const result = validatePayloadNode(child, object[key], `${path}.${key}`, depth + 1);
1825
- if (!result.ok)
1826
- return result;
1827
- }
1828
- }
1829
- return { ok: true };
1830
- }
1831
- function isJsonValueValue(value) {
1832
- if (value === null || typeof value === "string" || typeof value === "boolean")
1833
- return true;
1834
- if (typeof value === "number")
1835
- return Number.isFinite(value);
1836
- if (Array.isArray(value))
1837
- return value.every(isJsonValueValue);
1838
- if (!isRecordValue(value))
1839
- return false;
1840
- return Object.values(value).every(isJsonValueValue);
1841
- }
1842
- function isRecordValue(value) {
1843
- return typeof value === "object" && value !== null && !Array.isArray(value);
1844
- }
1845
1847
  function invalid(code, message, location) {
1846
1848
  void location;
1847
1849
  throw new FileNativePlayPresetError(code, message);
@@ -1854,3 +1856,19 @@ function hasExactKeys(value, required, optional = []) {
1854
1856
  return (required.every((key) => Object.hasOwn(value, key)) &&
1855
1857
  Object.keys(value).every((key) => allowed.has(key)));
1856
1858
  }
1859
+ export function parseOrderedAuthorPrompts(value) {
1860
+ return parseOrderedPlayPrompts(value, builtinAuthorPrompts("en"), false);
1861
+ }
1862
+ export function legacyAuthorPrompts(definition) {
1863
+ const entries = defaultOrderedAuthorPrompts();
1864
+ const path = definition.settingImprovementPrompt?.path;
1865
+ if (path !== undefined)
1866
+ entries[1] = {
1867
+ id: "legacy-author",
1868
+ kind: "user",
1869
+ name: "Authoring",
1870
+ body: definition.files[path],
1871
+ enabled: true,
1872
+ };
1873
+ return entries;
1874
+ }