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.
- package/dist/node/protocol/appPreferences.js +25 -0
- package/dist/node/protocol/conversationObservation.js +67 -0
- package/dist/node/protocol/v1.js +188 -34
- package/dist/node/protocol/worldExtensions.js +16 -0
- package/dist/node/protocol/worldMaintenance.js +77 -0
- package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
- package/dist/node/runtime/ConversationChanges.js +24 -0
- package/dist/node/runtime/V1Runtime.js +447 -121
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
- package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
- package/dist/node/runtime/content/ContentWorkspace.js +43 -13
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
- package/dist/node/runtime/content/PackageFollowups.js +80 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
- package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
- package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
- package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
- package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
- package/dist/node/runtime/model/ModelHost.js +31 -1
- package/dist/node/runtime/model/ModelHostStream.js +51 -5
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
- package/dist/node/runtime/play/OrderedFollowups.js +71 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
- package/dist/node/runtime/play/PlayCallChain.js +814 -61
- package/dist/node/runtime/play/PlayContinuity.js +88 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
- package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
- package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
- package/dist/node/runtime/play/PlayPromptRun.js +86 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
- package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
- package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
- package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
- package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
- package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
- package/dist/node/server/ConversationStream.js +109 -0
- package/dist/node/server/createServer.js +5 -1
- package/dist/node/shared/artifact-payload-contract.js +123 -0
- package/dist/node/shared/default-play-prompts.js +25 -45
- package/dist/node/shared/default-preset-host.js +114 -138
- package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
- package/dist/node/shared/display-regex.js +114 -0
- package/dist/node/shared/first-party-action-choices.js +1 -6
- package/dist/node/shared/first-party-player-view.js +1 -6
- package/dist/node/shared/ordered-author-prompts.js +64 -0
- package/dist/node/shared/ordered-followups.js +42 -0
- package/dist/node/shared/ordered-play-prompts.js +73 -0
- package/dist/web/assets/index-BXfelCi9.js +1207 -0
- package/dist/web/assets/index-BYnhUxlk.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
- package/dist/web/assets/index-D5VqDvjY.js +0 -1192
- 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
|
|
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:
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
+
}
|