narraeon 0.7.4 → 0.7.6

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 (30) hide show
  1. package/dist/node/protocol/v1.js +4 -0
  2. package/dist/node/runtime/V1Runtime.js +13 -1
  3. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +64 -163
  4. package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
  5. package/dist/node/runtime/model/ToolArgumentCoercion.js +138 -0
  6. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +34 -18
  7. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +6 -2
  8. package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
  9. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
  10. package/dist/node/runtime/play/PlayCallChain.js +276 -398
  11. package/dist/node/runtime/play/PlayDocumentTools.js +19 -0
  12. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +3 -3
  13. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -0
  14. package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
  15. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +370 -169
  16. package/dist/node/runtime/world/FileNativeWorldStore.js +151 -109
  17. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -1
  18. package/dist/node/runtime/world/WorldDocumentStore.js +1 -1
  19. package/dist/node/runtime/world/WorldLedgerAuthority.js +37 -3
  20. package/dist/node/runtime/world/WorldTurnLedger.js +17 -4
  21. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -4
  22. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +19 -241
  23. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +8 -27
  24. package/dist/node/server/createServer.js +1 -4
  25. package/dist/node/shared/ordered-play-prompts.js +5 -0
  26. package/dist/node/shared/turn-guide.js +27 -2
  27. package/dist/web/assets/index-Cj2JYCia.js +1408 -0
  28. package/dist/web/index.html +1 -1
  29. package/package.json +1 -1
  30. package/dist/web/assets/index-9N9kO8kY.js +0 -1408
@@ -2,10 +2,12 @@ import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
2
2
  import { renderWorldMaintenanceReport } from "../prompt/WorldMaintenanceReport.js";
3
3
  import { listWorldDocuments as listQueryableWorldDocuments } from "../prompt/WorldMaintenanceReport.js";
4
4
  import { defaultAppLocale } from "../../protocol/appPreferences.js";
5
- import { gameplayLabel, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
5
+ import { fictionGameplay, gameplayLabel, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
6
6
  import { isScalar, isSeq, parseDocument, stringify } from "yaml";
7
7
  import { tabletopSchemeReceipt } from "../play/TabletopRules.js";
8
8
  import { inspectContentPackageCurrentTree } from "../content/FileNativeContentTree.js";
9
+ import { coerceToolArguments, renderCoercionNote, } from "../model/ToolArgumentCoercion.js";
10
+ import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
9
11
  import { worldMaterialCoverage } from "../prompt/WorldMaterialCoverage.js";
10
12
  import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
11
13
  export const settingImprovementToolNames = [
@@ -18,6 +20,16 @@ export const settingImprovementToolNames = [
18
20
  "setting_move",
19
21
  "setting_delete",
20
22
  ];
23
+ const authoringOperations = [
24
+ "list",
25
+ "search",
26
+ "read",
27
+ "create",
28
+ "write_file",
29
+ "patch",
30
+ "move",
31
+ "delete",
32
+ ];
21
33
  const frameExample = `format: narraeon.world-frame/v1
22
34
  bindings:
23
35
  currentSituation: "@current-situation"
@@ -153,10 +165,13 @@ export function settingImprovementRuntimeContract(locale, gameplay) {
153
165
  - Passing mechanical checks does not make content discoverable during play. Whenever important information is created or reorganized, decide its natural owner, initial injection or discovery path, future update location, and player-visible projection together.`;
154
166
  }
155
167
  export function settingImprovementToolDefinitions(locale, gameplay) {
168
+ return authoringToolDefinitions(contentPackageAuthoringSurface, locale, gameplay);
169
+ }
170
+ /** Tool definitions a surface exposes; names, paths and descriptions come from the surface. */
171
+ export function authoringToolDefinitions(surface, locale, gameplay) {
156
172
  const tabletop = gameplay.mode === "tabletop";
157
- const descriptions = locale === "zh-CN"
158
- ? toolDescriptionsZhCN(tabletop)
159
- : toolDescriptionsEn(tabletop);
173
+ const descriptions = surface.descriptions[locale](tabletop);
174
+ const name = (operation) => authoringToolName(surface, operation);
160
175
  const schema = (properties, required) => ({
161
176
  type: "object",
162
177
  additionalProperties: false,
@@ -169,17 +184,20 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
169
184
  const limit = { type: "integer", minimum: 1, maximum: 100 };
170
185
  return [
171
186
  {
172
- name: "setting_list",
173
- description: descriptions.setting_list,
187
+ name: name("list"),
188
+ description: descriptions.list,
174
189
  inputSchema: schema({
175
- directory: { type: "string", pattern: "^world(?:/[^\\\\]+)?$" },
190
+ directory: {
191
+ type: "string",
192
+ pattern: `^${surface.root}(?:/[^\\\\]+)?$`,
193
+ },
176
194
  limit,
177
195
  cursor,
178
196
  }, []),
179
197
  },
180
198
  {
181
- name: "setting_search",
182
- description: descriptions.setting_search,
199
+ name: name("search"),
200
+ description: descriptions.search,
183
201
  inputSchema: schema({
184
202
  query: { type: "string", minLength: 1, maxLength: 256 },
185
203
  within: path,
@@ -189,17 +207,17 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
189
207
  }, ["query"]),
190
208
  },
191
209
  {
192
- name: "setting_read",
193
- description: descriptions.setting_read,
210
+ name: name("read"),
211
+ description: descriptions.read,
194
212
  inputSchema: schema({ path }, ["path"]),
195
213
  },
196
214
  {
197
- name: "setting_create",
198
- description: descriptions.setting_create,
215
+ name: name("create"),
216
+ description: descriptions.create,
199
217
  inputSchema: schema({
200
218
  path: {
201
219
  type: "string",
202
- pattern: "^world/.+\\.(?:ya?ml|md)$",
220
+ pattern: `^${surface.root}/.+\\.(?:ya?ml|md)$`,
203
221
  },
204
222
  ref: { type: "string", pattern: "^[a-z][a-z0-9-]{1,31}$" },
205
223
  title: { type: "string", minLength: 1, maxLength: 120 },
@@ -213,8 +231,8 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
213
231
  }, ["path", "ref", "title", "summary", "aliases", "body"]),
214
232
  },
215
233
  {
216
- name: "setting_write_file",
217
- description: descriptions.setting_write_file,
234
+ name: name("write_file"),
235
+ description: descriptions.write_file,
218
236
  inputSchema: schema({
219
237
  path,
220
238
  contents: text,
@@ -229,8 +247,8 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
229
247
  }, ["path", "contents"]),
230
248
  },
231
249
  {
232
- name: "setting_patch",
233
- description: descriptions.setting_patch,
250
+ name: name("patch"),
251
+ description: descriptions.patch,
234
252
  inputSchema: schema({
235
253
  document: text,
236
254
  op: {
@@ -253,40 +271,130 @@ export function settingImprovementToolDefinitions(locale, gameplay) {
253
271
  }, ["document", "op"]),
254
272
  },
255
273
  {
256
- name: "setting_move",
257
- description: descriptions.setting_move,
274
+ name: name("move"),
275
+ description: descriptions.move,
258
276
  inputSchema: schema({ from: text, to: path }, ["from", "to"]),
259
277
  },
260
278
  {
261
- name: "setting_delete",
262
- description: descriptions.setting_delete,
279
+ name: name("delete"),
280
+ description: descriptions.delete,
263
281
  inputSchema: schema({ document: text }, ["document"]),
264
282
  },
265
283
  ];
266
284
  }
267
285
  const toolDescriptionsEn = (tabletop) => ({
268
- setting_list: "List the world root or a world/ subdirectory from the content package's current tree. The root also lists opening and control files. A cursor is valid only for the same query and current-tree revision.",
269
- setting_search: "Search literal source text in world documents in the content package's current tree. within may be world, a world/ directory or path, or @ref.",
270
- setting_read: "Read a complete world document by world/ path or @ref, or read a complete opening/control file by exact path. Existing document selectors are projected to @refs. The result is never truncated or paginated.",
271
- setting_create: "Create one world/ .yaml or .md document. Supply a unique lowercase ASCII ref, title, summary, aliases, and body without a technical header; Runtime completes the storage envelope. The created document is already fully read.",
272
- setting_write_file: `Replace a completely read world/ .yaml or .md document body while preserving its identity and metadata, or create/replace opening.md, control/frame.yaml, control/player-views.yaml, ${tabletop ? "control/blocks/*.md, or control/gameplay.json (only a tabletop resolution scheme may change)" : "or control/blocks/*.md"}, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. To repair a damaged world document, also provide ref, title, summary, and aliases; Runtime preserves any recoverable storage identity. Create new world documents with setting_create.`,
273
- setting_patch: 'Update a completely read YAML world document without rewriting unrelated content. locator is a non-empty path whose segments are map keys or zero-based array indexes. Use add for a missing map key or array index, replace for an existing node, append for the end of an existing array, and remove for an existing map key or array item. When title, summary, or aliases are stale, update all three together with op "set_metadata". Rewrite Markdown bodies with setting_write_file.',
274
- setting_move: "Move a completely read world document to a new world/ .yaml or .md logical path while preserving its identity and contents.",
275
- setting_delete: "Permanently delete a completely read world document from the content-package current tree. Runtime rejects deletion while currentSituation, a frame slot, a player-view selector, or another document's $ref still points to it, and reports every blocking location. Delete or redirect those references first.",
286
+ list: "List the world root or a world/ subdirectory from the content package's current tree. The root also lists opening and control files. A cursor is valid only for the same query and current-tree revision.",
287
+ search: "Search literal source text in world documents in the content package's current tree. within may be world, a world/ directory or path, or @ref.",
288
+ read: "Read a complete world document by world/ path or @ref, or read a complete opening/control file by exact path. Existing document selectors are projected to @refs. The result is never truncated or paginated.",
289
+ create: "Create one world/ .yaml or .md document. Supply a unique lowercase ASCII ref, title, summary, aliases, and body without a technical header; Runtime completes the storage envelope. The created document is already fully read.",
290
+ write_file: `Replace a completely read world/ .yaml or .md document body while preserving its identity and metadata, or create/replace opening.md, control/frame.yaml, control/player-views.yaml, ${tabletop ? "control/blocks/*.md, or control/gameplay.json (only a tabletop resolution scheme may change)" : "or control/blocks/*.md"}, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. To repair a damaged world document, also provide ref, title, summary, and aliases; Runtime preserves any recoverable storage identity. Create new world documents with setting_create.`,
291
+ patch: 'Update a completely read YAML world document without rewriting unrelated content. locator is a non-empty path whose segments are map keys or zero-based array indexes. Use add for a missing map key or array index, replace for an existing node, append for the end of an existing array, and remove for an existing map key or array item. When title, summary, or aliases are stale, update all three together with op "set_metadata". Rewrite Markdown bodies with setting_write_file.',
292
+ move: "Move a completely read world document to a new world/ .yaml or .md logical path while preserving its identity and contents.",
293
+ delete: "Permanently delete a completely read world document from the content-package current tree. Runtime rejects deletion while currentSituation, a frame slot, a player-view selector, or another document's $ref still points to it, and reports every blocking location. Delete or redirect those references first.",
276
294
  });
277
295
  const toolDescriptionsZhCN = (tabletop) => ({
278
- setting_list: "列出内容包当前树的 world 根目录或 world/ 子目录;根目录同时列出 opening 和 control 专用文件。cursor 只对同一查询和当前树 revision 有效。",
279
- setting_search: "在内容包当前树的 world 文档中按原文字面搜索;within 可为 world、world/ 目录或路径或 @ref。",
280
- setting_read: "按 world/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 opening/control;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
281
- setting_create: "创建一份 world/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
282
- setting_write_file: `整份替换已完整读取的 world/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md${tabletop ? "、control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)" : ""},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 setting_create。`,
283
- setting_patch: '细粒度更新已完整读取的 YAML 世界文档,不重写无关内容。locator 是非空路径,每段可以是 map key 或从 0 开始的数组下标;add 新建 map key 或数组位置,replace 更新既有节点,append 追加既有数组,remove 删除既有 map key 或数组项。title、summary 或 aliases 过时时用 op "set_metadata" 整组更新三项。Markdown 正文用 setting_write_file 整份重写。',
284
- setting_move: "把已完整读取的世界文档移动到新的 world/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
285
- setting_delete: "从内容包当前树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
296
+ list: "列出内容包当前树的 world 根目录或 world/ 子目录;根目录同时列出 opening 和 control 专用文件。cursor 只对同一查询和当前树 revision 有效。",
297
+ search: "在内容包当前树的 world 文档中按原文字面搜索;within 可为 world、world/ 目录或路径或 @ref。",
298
+ read: "按 world/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 opening/control;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
299
+ create: "创建一份 world/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
300
+ write_file: `整份替换已完整读取的 world/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md${tabletop ? "、control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)" : ""},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 setting_create。`,
301
+ patch: '细粒度更新已完整读取的 YAML 世界文档,不重写无关内容。locator 是非空路径,每段可以是 map key 或从 0 开始的数组下标;add 新建 map key 或数组位置,replace 更新既有节点,append 追加既有数组,remove 删除既有 map key 或数组项。title、summary 或 aliases 过时时用 op "set_metadata" 整组更新三项。Markdown 正文用 setting_write_file 整份重写。',
302
+ move: "把已完整读取的世界文档移动到新的 world/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
303
+ delete: "从内容包当前树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
304
+ });
305
+ const worldRevisionDescriptionsEn = (tabletop) => ({
306
+ list: "List the state root or a state/ subdirectory from the world revision worktree. The root also lists control files. A cursor is valid only for the same query and worktree revision.",
307
+ search: "Search literal source text in world documents in the world revision worktree. within may be state, a state/ directory or path, or @ref.",
308
+ read: "Read a complete world document by state/ path or @ref, or read a complete control file by exact path. Existing document selectors are projected to @refs. The result is never truncated or paginated.",
309
+ create: "Create one state/ .yaml or .md document. Supply a unique lowercase ASCII ref, title, summary, aliases, and body without a technical header; Runtime completes the storage envelope. The created document is already fully read.",
310
+ write_file: `Replace a completely read state/ .yaml or .md document body while preserving its identity and metadata, or create/replace control/frame.yaml, control/player-views.yaml, ${tabletop ? "control/blocks/*.md, or control/gameplay.json (only a tabletop resolution scheme may change)" : "or control/blocks/*.md"}, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. To repair a damaged world document, also provide ref, title, summary, and aliases; Runtime preserves any recoverable storage identity. Create new world documents with world_revision_create.`,
311
+ patch: 'Update a completely read YAML world document without rewriting unrelated content. locator is a non-empty path whose segments are map keys or zero-based array indexes. Use add for a missing map key or array index, replace for an existing node, append for the end of an existing array, and remove for an existing map key or array item. When title, summary, or aliases are stale, update all three together with op "set_metadata". Rewrite Markdown bodies with world_revision_write_file.',
312
+ move: "Move a completely read world document to a new state/ .yaml or .md logical path while preserving its identity and contents.",
313
+ delete: "Permanently delete a completely read world document from the world revision worktree. Runtime rejects deletion while currentSituation, a frame slot, a player-view selector, or another document's $ref still points to it, and reports every blocking location. Delete or redirect those references first.",
286
314
  });
315
+ const worldRevisionDescriptionsZhCN = (tabletop) => ({
316
+ list: "列出世界修订工作树的 state 根目录或 state/ 子目录;根目录同时列出 control 文件。cursor 只对同一查询和工作树 revision 有效。",
317
+ search: "在世界修订工作树的 state 文档中按原文字面搜索;within 可为 state、state/ 目录或路径或 @ref。",
318
+ read: "按 state/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 control 文件;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
319
+ create: "创建一份 state/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
320
+ write_file: `整份替换已完整读取的 state/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 control/frame.yaml、control/player-views.yaml、control/blocks/*.md${tabletop ? "、control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)" : ""},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 world_revision_create。`,
321
+ patch: '细粒度更新已完整读取的 YAML 世界文档,不重写无关内容。locator 是非空路径,每段可以是 map key 或从 0 开始的数组下标;add 新建 map key 或数组位置,replace 更新既有节点,append 追加既有数组,remove 删除既有 map key 或数组项。title、summary 或 aliases 过时时用 op "set_metadata" 整组更新三项。Markdown 正文用 world_revision_write_file 整份重写。',
322
+ move: "把已完整读取的世界文档移动到新的 state/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
323
+ delete: "从世界修订工作树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
324
+ });
325
+ function worldRevisionUnavailablePath(path, locale) {
326
+ if (path === "opening.md")
327
+ return localized(locale, "opening.md is immutable after a world has been created. Edit state/* or control/* instead.", "世界创建后 opening.md 已成为不可变创世叙事。请改写 state/* 或 control/*。");
328
+ if (path === "world" || path.startsWith("world/"))
329
+ return localized(locale, "World revision exposes only state/* and control/* logical paths; use those instead.", "世界修订只公开 state/* 与 control/* 逻辑路径;请改用这两类路径。");
330
+ return null;
331
+ }
332
+ /** The content package's setting improvement: templates it edits, `world/` paths, and `opening.md`. */
333
+ export const contentPackageAuthoringSurface = {
334
+ layout: "content_package",
335
+ root: "world",
336
+ toolPrefix: "setting",
337
+ opening: true,
338
+ phrases: {
339
+ en: {
340
+ tree: "the content package's current tree",
341
+ bareTree: "the content package current tree",
342
+ noun: "the content package",
343
+ slug: "content-package",
344
+ directoryHeading: "Setting directory",
345
+ },
346
+ "zh-CN": {
347
+ tree: "内容包当前树",
348
+ bareTree: "内容包当前树",
349
+ noun: "内容包",
350
+ slug: "content-package",
351
+ directoryHeading: "设定目录",
352
+ },
353
+ },
354
+ descriptions: {
355
+ en: toolDescriptionsEn,
356
+ "zh-CN": toolDescriptionsZhCN,
357
+ },
358
+ unavailablePath: () => null,
359
+ };
360
+ /** A world revision: the state/control worktree of a world already in play. */
361
+ export const worldRevisionAuthoringSurface = {
362
+ layout: "world_revision",
363
+ root: "state",
364
+ toolPrefix: "world_revision",
365
+ opening: false,
366
+ phrases: {
367
+ en: {
368
+ tree: "the world revision worktree",
369
+ bareTree: "the world revision worktree",
370
+ noun: "the world revision worktree",
371
+ slug: "world-revision",
372
+ directoryHeading: "World revision directory",
373
+ },
374
+ "zh-CN": {
375
+ tree: "世界修订工作树",
376
+ bareTree: "世界修订工作树",
377
+ noun: "世界修订工作树",
378
+ slug: "world-revision",
379
+ directoryHeading: "世界修订目录",
380
+ },
381
+ },
382
+ descriptions: {
383
+ en: worldRevisionDescriptionsEn,
384
+ "zh-CN": worldRevisionDescriptionsZhCN,
385
+ },
386
+ unavailablePath: worldRevisionUnavailablePath,
387
+ };
388
+ function authoringToolName(surface, operation) {
389
+ return `${surface.toolPrefix}_${operation}`;
390
+ }
391
+ function authoringOperation(surface, name) {
392
+ return (authoringOperations.find((operation) => authoringToolName(surface, operation) === name) ?? null);
393
+ }
287
394
  export class SettingAuthoringTransaction {
288
395
  #baseFiles;
289
396
  #locale;
397
+ #surface;
290
398
  #preview;
291
399
  #validateFiles;
292
400
  #snapshot;
@@ -295,10 +403,11 @@ export class SettingAuthoringTransaction {
295
403
  constructor(input) {
296
404
  this.#baseFiles = cloneFiles(input.baseFiles);
297
405
  this.#locale = input.locale ?? defaultAppLocale;
406
+ this.#surface = input.surface ?? contentPackageAuthoringSurface;
298
407
  this.#preview = input.preview;
299
408
  this.#validateFiles = input.validateFiles ?? (() => undefined);
300
409
  this.#snapshot = WorldDocumentStore.open({
301
- layout: "content_package",
410
+ layout: this.#surface.layout,
302
411
  files: cloneFiles(input.baseFiles),
303
412
  });
304
413
  const authorization = input.authorization?.revision === (input.revision ?? this.#snapshot.id)
@@ -327,13 +436,32 @@ export class SettingAuthoringTransaction {
327
436
  };
328
437
  }
329
438
  execute(calls) {
330
- const normalized = calls.map(normalizeCall);
439
+ const normalized = calls.map((call) => normalizeCall(call, this.#surface, this.#locale));
331
440
  const results = [];
332
441
  const responseBase = this.files();
333
442
  let lastSuccessfulWrite = -1;
334
443
  for (const call of normalized) {
444
+ if (call.rejection !== undefined) {
445
+ results.push({
446
+ toolCallId: call.id,
447
+ markdown: `# Runtime tool rejected\n\n${call.rejection}`,
448
+ isError: true,
449
+ changes: [],
450
+ });
451
+ continue;
452
+ }
335
453
  const before = this.files();
336
- if (isReadTool(call.name)) {
454
+ const unavailable = unavailableArgument(this.#surface, call.arguments, this.#locale);
455
+ if (unavailable !== null) {
456
+ results.push({
457
+ toolCallId: call.id,
458
+ markdown: `# Runtime tool rejected\n\n${unavailable}`,
459
+ isError: true,
460
+ changes: [],
461
+ });
462
+ continue;
463
+ }
464
+ if (call.operation !== null && isReadOperation(call.operation)) {
337
465
  results.push(toToolResult(call.id, this.#executeRead(call), []));
338
466
  continue;
339
467
  }
@@ -347,39 +475,55 @@ export class SettingAuthoringTransaction {
347
475
  const settled = results[lastSuccessfulWrite];
348
476
  // The stored review stays cumulative for the whole user request; the
349
477
  // model only needs what this tool response changed.
350
- settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#locale)}`;
478
+ settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#surface, this.#locale)}`;
351
479
  }
480
+ normalized.forEach((call, index) => {
481
+ const notes = [
482
+ ...(call.coerced.length === 0
483
+ ? []
484
+ : [renderCoercionNote(call.coerced, this.#locale)]),
485
+ ...(call.alias === undefined
486
+ ? []
487
+ : [renderToolAliasNote(call.alias, call.name, this.#locale)]),
488
+ ];
489
+ if (notes.length === 0)
490
+ return;
491
+ const result = results[index];
492
+ result.markdown = `${result.markdown}\n\n${notes.join("\n\n")}`;
493
+ });
352
494
  return results;
353
495
  }
354
496
  #executeRead(call) {
355
- if (call.name === "setting_list")
356
- return listDocuments(this.#snapshot, call.arguments, this.#locale);
357
- if (call.name === "setting_search")
358
- return searchDocuments(this.#snapshot, call.arguments, this.#locale);
359
- if (call.name === "setting_read")
360
- return readDocument(this.#snapshot, call.arguments, this.#reads, this.#locale);
497
+ if (call.operation === "list")
498
+ return listDocuments(this.#snapshot, call.arguments, this.#surface, this.#locale);
499
+ if (call.operation === "search")
500
+ return searchDocuments(this.#snapshot, call.arguments, this.#surface, this.#locale);
501
+ if (call.operation === "read")
502
+ return readDocument(this.#snapshot, call.arguments, this.#reads, this.#surface, this.#locale);
361
503
  return failure(`Unsupported read tool: ${call.name}`, this.#locale);
362
504
  }
363
505
  #executeWrite(call) {
364
506
  const snapshot = this.#snapshot;
365
507
  const reads = cloneReads(this.#reads);
366
508
  try {
367
- if (isWorldWriteCall(call)) {
368
- if (call.name === "setting_delete") {
369
- const deleted = deleteWorldDocument(snapshot, call, reads, this.#locale);
509
+ if (isWorldWriteCall(call, this.#surface)) {
510
+ if (call.operation === "delete") {
511
+ const deleted = deleteWorldDocument(snapshot, call, reads, this.#surface, this.#locale);
370
512
  this.#validateFiles(deleted.snapshot.files);
371
513
  this.#snapshot = deleted.snapshot;
372
514
  this.#reads = reads;
373
515
  return success(deleted.markdown);
374
516
  }
375
- const requestedRef = call.name === "setting_create"
517
+ const requestedRef = call.operation === "create"
376
518
  ? requiredSettingRef(call.arguments.ref, this.#locale)
377
519
  : null;
378
520
  const revised = snapshot.revise({
379
- commands: [worldRevisionCommand(snapshot, call, this.#locale)],
521
+ commands: [
522
+ worldRevisionCommand(snapshot, call, this.#surface, this.#locale),
523
+ ],
380
524
  });
381
525
  if (!revised.ok)
382
- throw new SettingAuthoringError(renderRevisionFailure(revised.diagnostics, this.#locale));
526
+ throw new SettingAuthoringError(renderRevisionFailure(revised.diagnostics, this.#surface, this.#locale));
383
527
  if (requestedRef !== null &&
384
528
  revised.changes.some(({ shortRef }) => shortRef !== requestedRef))
385
529
  throw new SettingAuthoringError(localized(this.#locale, `ref @${requestedRef} already exists; choose a different ref`, `ref @${requestedRef} 已存在;请换一个 ref`));
@@ -392,9 +536,9 @@ export class SettingAuthoringTransaction {
392
536
  this.#reads = reads;
393
537
  return success(renderWorldWriteSuccess(revised.changes, this.#locale));
394
538
  }
395
- if (call.name !== "setting_write_file")
539
+ if (call.operation !== "write_file")
396
540
  throw new SettingAuthoringError(`Unsupported write tool: ${call.name}`);
397
- const changed = writeOpaque(snapshot, call, reads, this.#locale);
541
+ const changed = writeOpaque(snapshot, call, reads, this.#surface, this.#locale);
398
542
  this.#validateFiles(changed.snapshot.files);
399
543
  this.#snapshot = changed.snapshot;
400
544
  this.#reads = reads;
@@ -403,12 +547,13 @@ export class SettingAuthoringTransaction {
403
547
  catch (error) {
404
548
  const message = error instanceof Error
405
549
  ? error.message
406
- : "The content-package write failed";
550
+ : `The ${this.#surface.phrases.en.slug} write failed`;
407
551
  return failure(message, this.#locale);
408
552
  }
409
553
  }
410
554
  #inspect() {
411
555
  const inspection = inspectContentPackageCurrentTree(this.#snapshot.files, {
556
+ requireOpening: this.#surface.opening,
412
557
  worldDocumentSnapshot: this.#snapshot,
413
558
  });
414
559
  const diff = fileDiff(this.#baseFiles, this.#snapshot.files);
@@ -419,7 +564,7 @@ export class SettingAuthoringTransaction {
419
564
  diagnostics: inspection.issues.map(({ code, path, message }) => ({
420
565
  code,
421
566
  path,
422
- message: publicSettingDiagnosticMessage(code, message, this.#locale),
567
+ message: publicSettingDiagnosticMessage(code, message, this.#surface, this.#locale),
423
568
  })),
424
569
  preview: null,
425
570
  playCoverage: null,
@@ -431,7 +576,7 @@ export class SettingAuthoringTransaction {
431
576
  diff,
432
577
  diagnostics: [],
433
578
  preview,
434
- playCoverage: buildSettingAuthoringPlayCoverage(this.#snapshot, preview, diff),
579
+ playCoverage: buildSettingAuthoringPlayCoverage(this.#snapshot, preview, diff, this.#surface),
435
580
  };
436
581
  }
437
582
  catch (error) {
@@ -453,7 +598,7 @@ export class SettingAuthoringTransaction {
453
598
  }
454
599
  }
455
600
  }
456
- function renderAutomaticReview(review, responseDiff, snapshot, locale) {
601
+ function renderAutomaticReview(review, responseDiff, snapshot, surface, locale) {
457
602
  if (review.status === "usable" && review.preview !== null) {
458
603
  const changed = responseDiff.length;
459
604
  const cumulative = review.diff.length;
@@ -461,7 +606,7 @@ function renderAutomaticReview(review, responseDiff, snapshot, locale) {
461
606
  const changedShortRefs = new Set(listQueryableWorldDocuments(snapshot)
462
607
  .filter(({ logicalPath }) => changedPaths.has(logicalPath))
463
608
  .map(({ shortRef }) => shortRef));
464
- return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changed ${changed} file(s) in the content package${cumulative === changed ? "" : `; ${cumulative} file(s) changed so far in this user request`}. Use this review and the coverage below to decide whether the current user request is satisfied or further edits are needed.`, `# 当前树自动检查通过\n\n内容树校验和真实 Prompt Preview 均已通过;本次工具响应修改内容包中的 ${changed} 个文件${cumulative === changed ? "" : `,本次用户请求累计改动 ${cumulative} 个文件`}。请根据本次检查与下方覆盖报告判断当前用户要求是否已经满足,或是否还需修改。`)}\n\n${renderPlayCoverage(buildSettingAuthoringPlayCoverage(snapshot, review.preview, responseDiff), locale)}\n\n${renderWorldMaintenanceReport(review.preview.compilation.maintenance, changedShortRefs, locale)}`;
609
+ return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changed ${changed} file(s) in ${surface.phrases.en.noun}${cumulative === changed ? "" : `; ${cumulative} file(s) changed so far in this user request`}. Use this review and the coverage below to decide whether the current user request is satisfied or further edits are needed.`, `# 当前树自动检查通过\n\n内容树校验和真实 Prompt Preview 均已通过;本次工具响应修改${surface.phrases["zh-CN"].noun}中的 ${changed} 个文件${cumulative === changed ? "" : `,本次用户请求累计改动 ${cumulative} 个文件`}。请根据本次检查与下方覆盖报告判断当前用户要求是否已经满足,或是否还需修改。`)}\n\n${renderPlayCoverage(buildSettingAuthoringPlayCoverage(snapshot, review.preview, responseDiff, surface), surface, locale)}\n\n${renderWorldMaintenanceReport(review.preview.compilation.maintenance, changedShortRefs, locale)}`;
465
610
  }
466
611
  return [
467
612
  localized(locale, "# Current-tree review needs repair", "# 当前树自动检查需要修复"),
@@ -471,7 +616,7 @@ function renderAutomaticReview(review, responseDiff, snapshot, locale) {
471
616
  localized(locale, "Repair these diagnostics with the ordinary read/write tools. Runtime will run the review again after the next successful write call.", "请用普通读写工具修复这些诊断;下一个写调用成功后 Runtime 会再次自动检查。"),
472
617
  ].join("\n");
473
618
  }
474
- function buildSettingAuthoringPlayCoverage(snapshot, preview, diff) {
619
+ function buildSettingAuthoringPlayCoverage(snapshot, preview, diff, surface) {
475
620
  const documents = listQueryableWorldDocuments(snapshot);
476
621
  const byShortRef = new Map(documents.map((document) => [document.shortRef, document]));
477
622
  const { fullInjected, nodeInjected, catalogSummary, injectedSelections } = worldMaterialCoverage(preview.compilation.coverage);
@@ -524,7 +669,11 @@ function buildSettingAuthoringPlayCoverage(snapshot, preview, diff) {
524
669
  totals,
525
670
  changed: diff.map(({ path, after }) => {
526
671
  const access = changedPathPlayAccess(path, after, accessByPath, enabledWorldInstructionPaths);
527
- return { path, access, detail: playAccessDetail(access) };
672
+ return {
673
+ path,
674
+ access,
675
+ detail: playAccessDetail(access, surface),
676
+ };
528
677
  }),
529
678
  };
530
679
  }
@@ -555,7 +704,7 @@ function incrementPlayCoverageTotal(totals, access) {
555
704
  else if (access === "on_demand")
556
705
  totals.onDemand += 1;
557
706
  }
558
- function playAccessDetail(access) {
707
+ function playAccessDetail(access, surface) {
559
708
  const details = {
560
709
  full_injected: "complete body is injected into every fresh play context",
561
710
  node_injected: "only a selected node is injected into fresh play context",
@@ -566,11 +715,11 @@ function playAccessDetail(access) {
566
715
  play_control: "compiled as immutable play-author control",
567
716
  unused_control: "not enabled by control/frame.yaml and unavailable to ordinary play AI",
568
717
  player_view: "projected through exact player-view selectors",
569
- removed: "removed from the content package current tree",
718
+ removed: `removed from ${surface.phrases.en.bareTree}`,
570
719
  };
571
720
  return details[access];
572
721
  }
573
- function renderPlayCoverage(coverage, locale) {
722
+ function renderPlayCoverage(coverage, surface, locale) {
574
723
  const labels = locale === "zh-CN"
575
724
  ? {
576
725
  full_injected: "全文注入",
@@ -582,7 +731,7 @@ function renderPlayCoverage(coverage, locale) {
582
731
  play_control: "游玩控制",
583
732
  unused_control: "未被 control/frame.yaml 启用",
584
733
  player_view: "玩家视图精确投影",
585
- removed: "已从内容包当前树删除",
734
+ removed: `已从${surface.phrases["zh-CN"].tree}删除`,
586
735
  }
587
736
  : {
588
737
  full_injected: "full text injected",
@@ -594,7 +743,7 @@ function renderPlayCoverage(coverage, locale) {
594
743
  play_control: "play-author control",
595
744
  unused_control: "not enabled by control/frame.yaml",
596
745
  player_view: "exact player-view projection",
597
- removed: "removed from the content package current tree",
746
+ removed: `removed from ${surface.phrases.en.bareTree}`,
598
747
  };
599
748
  const maximumReportedChanges = 64;
600
749
  const reportedChanges = coverage.changed.slice(0, maximumReportedChanges);
@@ -634,32 +783,69 @@ class SettingAuthoringError extends Error {
634
783
  this.name = "SettingAuthoringError";
635
784
  }
636
785
  }
637
- function normalizeCall(call) {
638
- if (!isRecord(call.arguments))
639
- return { id: call.id, name: call.name, arguments: {} };
786
+ function normalizeCall(call, surface, locale) {
787
+ const cloned = isRecord(call.arguments)
788
+ ? structuredClone(call.arguments)
789
+ : {};
790
+ const definitions = authoringToolDefinitions(surface, locale, fictionGameplay);
791
+ const provided = definitions.map(({ name }) => name);
792
+ const resolved = resolveToolName(call.name, provided);
793
+ if (resolved.kind === "rejected")
794
+ return {
795
+ id: call.id,
796
+ name: call.name,
797
+ operation: null,
798
+ arguments: cloned,
799
+ coerced: [],
800
+ rejection: unknownToolMessage(call.name, provided, locale),
801
+ };
802
+ const name = resolved.name;
803
+ const schema = definitions.find((definition) => definition.name === name)?.inputSchema;
804
+ const repaired = schema === undefined
805
+ ? { arguments: cloned, coerced: [] }
806
+ : coerceToolArguments(schema, cloned);
640
807
  return {
641
808
  id: call.id,
642
- name: call.name,
643
- arguments: structuredClone(call.arguments),
809
+ name,
810
+ operation: authoringOperation(surface, name),
811
+ arguments: repaired.arguments,
812
+ coerced: repaired.coerced,
813
+ ...(resolved.kind === "alias" ? { alias: resolved.declared } : {}),
644
814
  };
645
815
  }
646
- function isReadTool(name) {
647
- return (name === "setting_list" ||
648
- name === "setting_search" ||
649
- name === "setting_read");
816
+ function unknownToolMessage(name, provided, locale) {
817
+ return locale === "zh-CN"
818
+ ? `${name} 不是本界面提供的工具。可用工具:${provided.join(", ")}。`
819
+ : `${name} is not a tool on this surface. Available tools: ${provided.join(", ")}.`;
820
+ }
821
+ function isReadOperation(operation) {
822
+ return operation === "list" || operation === "search" || operation === "read";
650
823
  }
651
- function isWorldWriteCall(call) {
652
- if (call.name === "setting_create" ||
653
- call.name === "setting_patch" ||
654
- call.name === "setting_move" ||
655
- call.name === "setting_delete")
824
+ /**
825
+ * Rejects a call that names a path outside the surface before any store work.
826
+ * The check covers every path-like argument, so a read and a write fail alike.
827
+ */
828
+ function unavailableArgument(surface, args, locale) {
829
+ for (const field of ["path", "directory", "within", "from", "to", "document"])
830
+ if (typeof args[field] === "string") {
831
+ const message = surface.unavailablePath(args[field], locale);
832
+ if (message !== null)
833
+ return message;
834
+ }
835
+ return null;
836
+ }
837
+ function isWorldWriteCall(call, surface) {
838
+ if (call.operation === "create" ||
839
+ call.operation === "patch" ||
840
+ call.operation === "move" ||
841
+ call.operation === "delete")
656
842
  return true;
657
- return (call.name === "setting_write_file" &&
843
+ return (call.operation === "write_file" &&
658
844
  typeof call.arguments.path === "string" &&
659
- call.arguments.path.startsWith("world/"));
845
+ call.arguments.path.startsWith(`${surface.root}/`));
660
846
  }
661
- function worldRevisionCommand(snapshot, call, locale) {
662
- if (call.name === "setting_create") {
847
+ function worldRevisionCommand(snapshot, call, surface, locale) {
848
+ if (call.operation === "create") {
663
849
  if (!hasOnly(call.arguments, [
664
850
  "path",
665
851
  "ref",
@@ -668,8 +854,8 @@ function worldRevisionCommand(snapshot, call, locale) {
668
854
  "aliases",
669
855
  "body",
670
856
  ]))
671
- throw new SettingAuthoringError("setting_create accepts only path, ref, title, summary, aliases, and body");
672
- const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), locale);
857
+ throw new SettingAuthoringError(`${authoringToolName(surface, "create")} accepts only path, ref, title, summary, aliases, and body`);
858
+ const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), surface, locale);
673
859
  const ref = requiredSettingRef(call.arguments.ref, locale);
674
860
  const title = requiredString(call.arguments.title, "title");
675
861
  const summary = requiredString(call.arguments.summary, "summary");
@@ -688,9 +874,9 @@ function worldRevisionCommand(snapshot, call, locale) {
688
874
  body,
689
875
  };
690
876
  }
691
- if (call.name === "setting_write_file") {
692
- const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), locale);
693
- const contents = worldWriteContents(snapshot, logicalPath, call.arguments, locale);
877
+ if (call.operation === "write_file") {
878
+ const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), surface, locale);
879
+ const contents = worldWriteContents(snapshot, logicalPath, call.arguments, surface, locale);
694
880
  assertNoLiteralUnicodeEscapes(contents, locale);
695
881
  return {
696
882
  kind: "write",
@@ -698,17 +884,17 @@ function worldRevisionCommand(snapshot, call, locale) {
698
884
  contents,
699
885
  };
700
886
  }
701
- if (call.name === "setting_move") {
702
- const toLogicalPath = safePath(requiredString(call.arguments.to, "to"));
703
- if (!/^world\/.+\.(?:ya?ml|md)$/u.test(toLogicalPath))
704
- throw new SettingAuthoringError("setting_move.to must be a world/ .yaml or .md path");
887
+ if (call.operation === "move") {
888
+ const toLogicalPath = safePath(requiredString(call.arguments.to, "to"), surface);
889
+ if (!documentPathPattern(surface).test(toLogicalPath))
890
+ throw new SettingAuthoringError(`${authoringToolName(surface, "move")}.to must be a ${surface.root}/ .yaml or .md path`);
705
891
  return {
706
892
  kind: "move",
707
- document: revisionTarget(requiredString(call.arguments.from, "from")),
893
+ document: revisionTarget(requiredString(call.arguments.from, "from"), surface),
708
894
  toLogicalPath,
709
895
  };
710
896
  }
711
- if (call.name === "setting_patch") {
897
+ if (call.operation === "patch") {
712
898
  const op = requiredString(call.arguments.op, "op");
713
899
  if (op === "set_metadata") {
714
900
  const title = requiredString(call.arguments.title, "title");
@@ -717,31 +903,31 @@ function worldRevisionCommand(snapshot, call, locale) {
717
903
  assertNoLiteralUnicodeEscapes({ title, summary, aliases }, locale);
718
904
  return {
719
905
  kind: "patch",
720
- document: revisionTarget(requiredString(call.arguments.document, "document")),
906
+ document: revisionTarget(requiredString(call.arguments.document, "document"), surface),
721
907
  edits: [{ op: "set_metadata", title, summary, aliases }],
722
908
  };
723
909
  }
724
910
  if (op !== "add" && op !== "replace" && op !== "append" && op !== "remove")
725
- throw new SettingAuthoringError("setting_patch.op must be add, replace, append, remove, or set_metadata");
911
+ throw new SettingAuthoringError(`${authoringToolName(surface, "patch")}.op must be add, replace, append, remove, or set_metadata`);
726
912
  const locator = requiredYamlLocatorPath(call.arguments.locator, "locator");
727
913
  if (op === "remove") {
728
914
  if (!hasOnly(call.arguments, ["document", "op", "locator"]))
729
- throw new SettingAuthoringError("setting_patch remove accepts only document, op, and locator");
915
+ throw new SettingAuthoringError(`${authoringToolName(surface, "patch")} remove accepts only document, op, and locator`);
730
916
  assertNoLiteralUnicodeEscapes(locator, locale);
731
917
  return {
732
918
  kind: "patch",
733
- document: revisionTarget(requiredString(call.arguments.document, "document")),
919
+ document: revisionTarget(requiredString(call.arguments.document, "document"), surface),
734
920
  edits: [{ op: "remove", locator: { yaml: locator } }],
735
921
  };
736
922
  }
737
923
  if (!hasOnly(call.arguments, ["document", "op", "locator", "value"]) ||
738
924
  !Object.hasOwn(call.arguments, "value"))
739
- throw new SettingAuthoringError(`setting_patch ${op} requires document, op, locator, and value`);
925
+ throw new SettingAuthoringError(`${authoringToolName(surface, "patch")} ${op} requires document, op, locator, and value`);
740
926
  const value = structuredClone(call.arguments.value);
741
927
  assertNoLiteralUnicodeEscapes({ locator, value }, locale);
742
928
  return {
743
929
  kind: "patch",
744
- document: revisionTarget(requiredString(call.arguments.document, "document")),
930
+ document: revisionTarget(requiredString(call.arguments.document, "document"), surface),
745
931
  edits: [
746
932
  {
747
933
  op,
@@ -755,15 +941,19 @@ function worldRevisionCommand(snapshot, call, locale) {
755
941
  }
756
942
  throw new SettingAuthoringError(`Unsupported write tool: ${call.name}`);
757
943
  }
758
- function safeWorldDocumentPath(path, locale) {
759
- const logicalPath = safePath(path);
760
- if (!/^world\/.+\.(?:ya?ml|md)$/u.test(logicalPath))
761
- throw new SettingAuthoringError(localized(locale, "World files must be .yaml or .md documents below world/", "world 文件必须是 world/ 下的 .yaml 或 .md 文档"));
944
+ /** The world-document path pattern for a surface, e.g. `world/…yaml` or `state/…md`. */
945
+ function documentPathPattern(surface) {
946
+ return new RegExp(`^${surface.root}/.+\\.(?:ya?ml|md)$`, "u");
947
+ }
948
+ function safeWorldDocumentPath(path, surface, locale) {
949
+ const logicalPath = safePath(path, surface);
950
+ if (!documentPathPattern(surface).test(logicalPath))
951
+ throw new SettingAuthoringError(localized(locale, `World files must be .yaml or .md documents below ${surface.root}/`, `world 文件必须是 ${surface.root}/ 下的 .yaml 或 .md 文档`));
762
952
  return logicalPath;
763
953
  }
764
- function worldWriteContents(snapshot, logicalPath, args, locale) {
954
+ function worldWriteContents(snapshot, logicalPath, args, surface, locale) {
765
955
  if (!hasOnly(args, ["path", "contents", "ref", "title", "summary", "aliases"]))
766
- throw new SettingAuthoringError("setting_write_file accepts only its declared arguments");
956
+ throw new SettingAuthoringError(`${authoringToolName(surface, "write_file")} accepts only its declared arguments`);
767
957
  const body = requiredString(args.contents, "contents");
768
958
  const metadataKeys = ["ref", "title", "summary", "aliases"];
769
959
  const provided = metadataKeys.filter((key) => args[key] !== undefined);
@@ -771,7 +961,7 @@ function worldWriteContents(snapshot, logicalPath, args, locale) {
771
961
  const existingDocument = listQueryableWorldDocuments(snapshot).some(({ logicalPath: existingPath }) => existingPath === logicalPath);
772
962
  if (provided.length === 0) {
773
963
  if (!existingFile && !containsTechnicalDocumentHeader(logicalPath, body))
774
- throw new SettingAuthoringError(localized(locale, "Create a new world document with setting_create", "新建世界文档请使用 setting_create"));
964
+ throw new SettingAuthoringError(localized(locale, `Create a new world document with ${authoringToolName(surface, "create")}`, `新建世界文档请使用 ${authoringToolName(surface, "create")}`));
775
965
  if (existingFile &&
776
966
  !existingDocument &&
777
967
  !containsTechnicalDocumentHeader(logicalPath, body))
@@ -781,9 +971,9 @@ function worldWriteContents(snapshot, logicalPath, args, locale) {
781
971
  if (provided.length !== metadataKeys.length)
782
972
  throw new SettingAuthoringError(localized(locale, "Repair metadata must provide ref, title, summary, and aliases together", "修复元信息时必须同时提供 ref、title、summary 和 aliases"));
783
973
  if (!existingFile)
784
- throw new SettingAuthoringError(localized(locale, "Create a new world document with setting_create", "新建世界文档请使用 setting_create"));
974
+ throw new SettingAuthoringError(localized(locale, `Create a new world document with ${authoringToolName(surface, "create")}`, `新建世界文档请使用 ${authoringToolName(surface, "create")}`));
785
975
  if (existingDocument)
786
- throw new SettingAuthoringError(localized(locale, "Repair metadata is only for a damaged document; update valid metadata with setting_patch set_metadata", "修复元信息只用于损坏文档;有效文档请用 setting_patch set_metadata 更新元信息"));
976
+ throw new SettingAuthoringError(localized(locale, `Repair metadata is only for a damaged document; update valid metadata with ${authoringToolName(surface, "patch")} set_metadata`, `修复元信息只用于损坏文档;有效文档请用 ${authoringToolName(surface, "patch")} set_metadata 更新元信息`));
787
977
  const ref = requiredSettingRef(args.ref, locale);
788
978
  const title = requiredString(args.title, "title");
789
979
  const summary = requiredString(args.summary, "summary");
@@ -826,25 +1016,26 @@ function recoverStoredDocumentId(snapshot, logicalPath) {
826
1016
  ? id
827
1017
  : null;
828
1018
  }
829
- function revisionTarget(value) {
830
- const selector = documentSelector(value);
1019
+ function revisionTarget(value, surface) {
1020
+ const selector = documentSelector(value, surface);
831
1021
  if (selector === null)
832
1022
  throw new SettingAuthoringError(`Invalid world-document selector: ${value}`);
833
1023
  return selector;
834
1024
  }
835
- function deleteWorldDocument(snapshot, call, reads, locale) {
1025
+ function deleteWorldDocument(snapshot, call, reads, surface, locale) {
1026
+ const tool = authoringToolName(surface, "delete");
836
1027
  if (!hasOnly(call.arguments, ["document"]))
837
- throw new SettingAuthoringError("setting_delete accepts only the document argument");
1028
+ throw new SettingAuthoringError(`${tool} accepts only the document argument`);
838
1029
  const supplied = requiredString(call.arguments.document, "document");
839
- const selector = documentSelector(supplied);
1030
+ const selector = documentSelector(supplied, surface);
840
1031
  if (selector === null)
841
- throw new SettingAuthoringError(localized(locale, "setting_delete.document must be an @ref or world/ document path", "setting_delete.document 必须是 @ref 或 world/ 文档路径"));
1032
+ throw new SettingAuthoringError(localized(locale, `${tool}.document must be an @ref or ${surface.root}/ document path`, `${tool}.document 必须是 @ref 或 ${surface.root}/ 文档路径`));
842
1033
  const resolved = snapshot.query({
843
1034
  kind: "read_document",
844
1035
  document: selector,
845
1036
  });
846
1037
  if (resolved.kind === "error")
847
- throw new SettingAuthoringError(renderRevisionFailure(resolved.diagnostics, locale));
1038
+ throw new SettingAuthoringError(renderRevisionFailure(resolved.diagnostics, surface, locale));
848
1039
  if (resolved.kind !== "read_document")
849
1040
  throw new Error("Unexpected world-document delete lookup result");
850
1041
  const target = resolved.document;
@@ -859,7 +1050,7 @@ function deleteWorldDocument(snapshot, call, reads, locale) {
859
1050
  throw new SettingAuthoringError(localized(locale, `Cannot delete @${target.shortRef}; remove or redirect every reference first:\n${locations}`, `无法删除 @${target.shortRef};请先删除或改向全部引用:\n${locations}`));
860
1051
  }
861
1052
  const revised = WorldDocumentStore.open({
862
- layout: "content_package",
1053
+ layout: surface.layout,
863
1054
  files: sortFiles(snapshot.files.filter(({ path }) => path !== target.logicalPath)),
864
1055
  });
865
1056
  rebaseReads(reads, revised);
@@ -942,12 +1133,14 @@ function deletionLocator(locator) {
942
1133
  ? `yaml:${JSON.stringify(locator.yaml)}`
943
1134
  : `markdown:${JSON.stringify(locator.markdown)}`;
944
1135
  }
945
- function writeOpaque(snapshot, call, reads, locale) {
1136
+ function writeOpaque(snapshot, call, reads, surface, locale) {
946
1137
  if (!hasOnly(call.arguments, ["path", "contents"]))
947
1138
  throw new SettingAuthoringError("Special-file writes accept only path and contents");
948
- const path = safePath(requiredString(call.arguments.path, "path"));
949
- if (!writableOpaquePath(path))
950
- throw new SettingAuthoringError(localized(locale, "Special-file writes accept only opening.md, control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or control/gameplay.json", "专用文件只允许写入 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md 或 control/gameplay.json"));
1139
+ const path = safePath(requiredString(call.arguments.path, "path"), surface);
1140
+ if (!writableOpaquePath(path, surface))
1141
+ throw new SettingAuthoringError(surface.opening
1142
+ ? localized(locale, "Special-file writes accept only opening.md, control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or control/gameplay.json", "专用文件只允许写入 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md 或 control/gameplay.json")
1143
+ : localized(locale, "Special-file writes accept only control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or control/gameplay.json", "专用文件只允许写入 control/frame.yaml、control/player-views.yaml、control/blocks/*.md 或 control/gameplay.json"));
951
1144
  const authoredContents = requiredString(call.arguments.contents, "contents");
952
1145
  assertNoLiteralUnicodeEscapes(authoredContents, locale);
953
1146
  const contents = projectOpaqueDocumentIds(snapshot, path, authoredContents);
@@ -964,7 +1157,7 @@ function writeOpaque(snapshot, call, reads, locale) {
964
1157
  delete existing.encoding;
965
1158
  }
966
1159
  const revised = WorldDocumentStore.open({
967
- layout: "content_package",
1160
+ layout: surface.layout,
968
1161
  files: sortFiles(next),
969
1162
  });
970
1163
  rebaseReads(reads, revised);
@@ -973,7 +1166,7 @@ function writeOpaque(snapshot, call, reads, locale) {
973
1166
  return {
974
1167
  snapshot: revised,
975
1168
  markdown: [
976
- localized(locale, `# Current-tree write accepted\n\n${existing === undefined ? "Created" : "Updated"} ${path} in the content package's current tree. This file is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.`, `# 当前树写入已接受\n\n已在内容包当前树中${existing === undefined ? "创建" : "更新"} ${path}。该文件已视为完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。`),
1169
+ localized(locale, `# Current-tree write accepted\n\n${existing === undefined ? "Created" : "Updated"} ${path} in ${surface.phrases.en.tree}. This file is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.`, `# 当前树写入已接受\n\n已在${surface.phrases["zh-CN"].tree}中${existing === undefined ? "创建" : "更新"} ${path}。该文件已视为完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。`),
977
1170
  ...(gameplay === null
978
1171
  ? []
979
1172
  : [
@@ -993,9 +1186,9 @@ function assertGameplayWriteKeepsMode(files, contents, locale) {
993
1186
  if (before.mode !== after.mode)
994
1187
  throw new SettingAuthoringError(localized(locale, "Gameplay mode is fixed; control/gameplay.json may change only the resolution scheme of a tabletop package", "玩法模式固定;control/gameplay.json 只能修改跑团的判定方案,不能在互动式小说与跑团之间切换"));
995
1188
  }
996
- function writableOpaquePath(path) {
1189
+ function writableOpaquePath(path, surface) {
997
1190
  return (isPackageFollowupControlPath(path) ||
998
- path === "opening.md" ||
1191
+ (surface.opening && path === "opening.md") ||
999
1192
  path === "control/frame.yaml" ||
1000
1193
  path === "control/player-views.yaml" ||
1001
1194
  path === gameplayPath ||
@@ -1029,25 +1222,27 @@ function renderWorldWriteSuccess(changes, locale) {
1029
1222
  localized(locale, "This call was accepted independently. Every created or changed document above is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.", "本调用已独立接受。上方新建或修改的文档均视为已经完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。"),
1030
1223
  ].join("\n");
1031
1224
  }
1032
- function renderRevisionFailure(diagnostics, locale) {
1225
+ function renderRevisionFailure(diagnostics, surface, locale) {
1033
1226
  return diagnostics
1034
- .map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, locale)}`)
1227
+ .map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, surface, locale)}`)
1035
1228
  .join("\n");
1036
1229
  }
1037
- function listDocuments(snapshot, args, locale) {
1230
+ function listDocuments(snapshot, args, surface, locale) {
1231
+ const tool = authoringToolName(surface, "list");
1232
+ const root = surface.root;
1038
1233
  if (!hasOnly(args, ["directory", "limit", "cursor"]))
1039
- return failure("setting_list accepts only directory, limit, and cursor", locale);
1040
- const directory = args.directory ?? "world";
1234
+ return failure(`${tool} accepts only directory, limit, and cursor`, locale);
1235
+ const directory = args.directory ?? root;
1041
1236
  const limit = args.limit ?? 100;
1042
1237
  if (typeof directory !== "string" ||
1043
- (directory !== "world" && !directory.startsWith("world/")) ||
1238
+ (directory !== root && !directory.startsWith(`${root}/`)) ||
1044
1239
  typeof limit !== "number" ||
1045
1240
  !Number.isInteger(limit) ||
1046
1241
  limit < 1 ||
1047
1242
  limit > 100 ||
1048
1243
  !validCursor(args.cursor))
1049
- return failure("Invalid setting_list directory, limit, or cursor", locale);
1050
- const relative = directory === "world" ? "" : directory.slice(6);
1244
+ return failure(`Invalid ${tool} directory, limit, or cursor`, locale);
1245
+ const relative = directory === root ? "" : directory.slice(root.length + 1);
1051
1246
  const result = snapshot.query({
1052
1247
  kind: "catalog",
1053
1248
  directory: relative,
@@ -1055,7 +1250,7 @@ function listDocuments(snapshot, args, locale) {
1055
1250
  ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
1056
1251
  });
1057
1252
  if (result.kind === "error")
1058
- return renderQueryFailure(result, locale);
1253
+ return renderQueryFailure(result, surface, locale);
1059
1254
  if (result.kind !== "catalog")
1060
1255
  throw new Error("Unexpected catalog result");
1061
1256
  const entries = result.entries.map((entry) => {
@@ -1067,11 +1262,12 @@ function listDocuments(snapshot, args, locale) {
1067
1262
  });
1068
1263
  if (relative === "" && result.page.start === 0)
1069
1264
  entries.unshift(...snapshot.files
1070
- .filter(({ path }) => !path.startsWith("world/"))
1265
+ .filter(({ path }) => !path.startsWith(`${root}/`))
1071
1266
  .map(({ path, encoding }) => encoding === undefined ? `- [special] ${path}` : `- [binary] ${path}`));
1072
- return success(`# ${localized(locale, "Setting directory", "设定目录")}\n\n${entries.join("\n") || localized(locale, "(empty)", "(空)")}\n\n${pageFooter(result.page, locale)}`);
1267
+ return success(`# ${localized(locale, surface.phrases.en.directoryHeading, surface.phrases["zh-CN"].directoryHeading)}\n\n${entries.join("\n") || localized(locale, "(empty)", "(空)")}\n\n${pageFooter(result.page, locale)}`);
1073
1268
  }
1074
- function searchDocuments(snapshot, args, locale) {
1269
+ function searchDocuments(snapshot, args, surface, locale) {
1270
+ const tool = authoringToolName(surface, "search");
1075
1271
  const query = args.query;
1076
1272
  const limit = args.limit ?? 20;
1077
1273
  if (!hasOnly(args, ["query", "within", "caseSensitive", "limit", "cursor"]) ||
@@ -1085,10 +1281,10 @@ function searchDocuments(snapshot, args, locale) {
1085
1281
  (args.caseSensitive !== undefined &&
1086
1282
  typeof args.caseSensitive !== "boolean") ||
1087
1283
  !validCursor(args.cursor))
1088
- return failure("Invalid setting_search arguments", locale);
1089
- const within = searchScope(args.within);
1284
+ return failure(`Invalid ${tool} arguments`, locale);
1285
+ const within = searchScope(args.within, surface);
1090
1286
  if (within === invalidScope)
1091
- return failure("Invalid setting_search within scope", locale);
1287
+ return failure(`Invalid ${tool} within scope`, locale);
1092
1288
  const result = snapshot.query({
1093
1289
  kind: "literal_search",
1094
1290
  query,
@@ -1098,39 +1294,39 @@ function searchDocuments(snapshot, args, locale) {
1098
1294
  ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
1099
1295
  });
1100
1296
  if (result.kind === "error")
1101
- return renderQueryFailure(result, locale);
1297
+ return renderQueryFailure(result, surface, locale);
1102
1298
  if (result.kind !== "literal_search")
1103
1299
  throw new Error("Unexpected literal search result");
1104
1300
  const lines = result.matches.map(({ document, referenceProjection, range }) => `- @${document.shortRef} · ${document.logicalPath} · ${range.start.line}:${range.start.column}\n ${JSON.stringify(referenceProjection.excerpt)}`);
1105
1301
  return success(`# ${localized(locale, "Literal setting search", "设定字面搜索")}\n\n${lines.join("\n") || localized(locale, "Zero literal matches do not prove the fact is absent.", "0 个字面命中不证明设定中不存在该事实。")}\n\n${pageFooter(result.page, locale)}`);
1106
1302
  }
1107
1303
  const invalidScope = Symbol("invalid-scope");
1108
- function searchScope(value) {
1304
+ function searchScope(value, surface) {
1109
1305
  if (value === undefined)
1110
1306
  return undefined;
1111
1307
  if (typeof value !== "string" || value.length === 0)
1112
1308
  return invalidScope;
1113
- if (value === "world")
1309
+ if (value === surface.root)
1114
1310
  return { directory: "" };
1115
1311
  if (value.startsWith("@"))
1116
1312
  return value.length > 1
1117
1313
  ? { document: { shortRef: value.slice(1) } }
1118
1314
  : invalidScope;
1119
- if (value.startsWith("world/")) {
1120
- const path = safePath(value);
1315
+ if (value.startsWith(`${surface.root}/`)) {
1316
+ const path = safePath(value, surface);
1121
1317
  return /\.(?:ya?ml|md)$/u.test(path)
1122
1318
  ? { document: { logicalPath: path } }
1123
- : { directory: path.slice(6) };
1319
+ : { directory: path.slice(surface.root.length + 1) };
1124
1320
  }
1125
1321
  return invalidScope;
1126
1322
  }
1127
- function readDocument(snapshot, args, reads, locale) {
1323
+ function readDocument(snapshot, args, reads, surface, locale) {
1128
1324
  const path = args.path;
1129
1325
  if (!hasOnly(args, ["path", "maxBytes", "cursor"]) ||
1130
1326
  typeof path !== "string" ||
1131
1327
  path.length === 0)
1132
- return failure("Invalid setting_read arguments", locale);
1133
- const opaque = snapshot.files.find((file) => file.path === path && !file.path.startsWith("world/"));
1328
+ return failure(`Invalid ${authoringToolName(surface, "read")} arguments`, locale);
1329
+ const opaque = snapshot.files.find((file) => file.path === path && !file.path.startsWith(`${surface.root}/`));
1134
1330
  if (opaque !== undefined) {
1135
1331
  if (opaque.encoding !== undefined)
1136
1332
  return failure("Special files require one complete text read", locale);
@@ -1138,28 +1334,28 @@ function readDocument(snapshot, args, reads, locale) {
1138
1334
  const projected = projectOpaqueDocumentIds(snapshot, path, opaque.contents);
1139
1335
  return success(`# ${localized(locale, "Special-file source", "专用文件原文")} ${path}\n\n${projected}\n\n---\nComplete: yes`);
1140
1336
  }
1141
- const selector = documentSelector(path);
1337
+ const selector = documentSelector(path, surface);
1142
1338
  if (selector === null)
1143
1339
  return failure(selector === null && typeof path === "string" && !path.includes("/")
1144
- ? "Use @ref or a world/ logical path"
1145
- : "Invalid setting_read path", locale);
1340
+ ? `Use @ref or a ${surface.root}/ logical path`
1341
+ : `Invalid ${authoringToolName(surface, "read")} path`, locale);
1146
1342
  const result = snapshot.query({
1147
1343
  kind: "read_document",
1148
1344
  document: selector,
1149
1345
  referenceProjection: "short_ref",
1150
1346
  });
1151
1347
  if (result.kind === "error") {
1152
- if (repairableDamagedWorldRead(snapshot, path, result))
1348
+ if (repairableDamagedWorldRead(snapshot, path, surface, result))
1153
1349
  reads.damagedWorldPaths.add(path);
1154
- return renderQueryFailure(result, locale);
1350
+ return renderQueryFailure(result, surface, locale);
1155
1351
  }
1156
1352
  if (result.kind !== "read_document")
1157
1353
  throw new Error("Unexpected read result");
1158
1354
  reads.worldDocumentIds.add(result.document.documentId);
1159
1355
  return success(`# ${localized(locale, "Exact read", "精确读取")} @${result.document.shortRef}\n\ntitle: ${result.document.title}\nsummary: ${result.document.summary}\ncodec: ${result.document.codec}\nlogicalPath: ${result.document.logicalPath}\n[body]\n${result.body.trimEnd()}\n[/body]\n\n---\nComplete: yes`);
1160
1356
  }
1161
- function repairableDamagedWorldRead(snapshot, path, result) {
1162
- if (!path.startsWith("world/") ||
1357
+ function repairableDamagedWorldRead(snapshot, path, surface, result) {
1358
+ if (!path.startsWith(`${surface.root}/`) ||
1163
1359
  !snapshot.files.some(({ path: candidate }) => candidate === path))
1164
1360
  return false;
1165
1361
  // A failed exact read authorizes replacement only when the source itself is
@@ -1170,11 +1366,11 @@ function repairableDamagedWorldRead(snapshot, path, result) {
1170
1366
  code !== "document_not_found" &&
1171
1367
  code !== "document_ambiguous");
1172
1368
  }
1173
- function documentSelector(value) {
1369
+ function documentSelector(value, surface) {
1174
1370
  if (value.startsWith("@"))
1175
1371
  return value.length > 1 ? { shortRef: value.slice(1) } : null;
1176
- if (value.startsWith("world/")) {
1177
- const path = safePath(value);
1372
+ if (value.startsWith(`${surface.root}/`)) {
1373
+ const path = safePath(value, surface);
1178
1374
  return /\.(?:ya?ml|md)$/u.test(path) ? { logicalPath: path } : null;
1179
1375
  }
1180
1376
  return null;
@@ -1231,20 +1427,21 @@ function projectOpaqueDocumentIds(snapshot, path, source) {
1231
1427
  }
1232
1428
  return changed ? document.toString({ indent: 2, lineWidth: 0 }) : source;
1233
1429
  }
1234
- function renderQueryFailure(result, locale) {
1430
+ function renderQueryFailure(result, surface, locale) {
1235
1431
  return failure(result.diagnostics
1236
- .map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, locale)}`)
1432
+ .map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, surface, locale)}`)
1237
1433
  .join("\n"), locale);
1238
1434
  }
1239
- function publicSettingDiagnosticMessage(code, message, locale) {
1435
+ function publicSettingDiagnosticMessage(code, message, surface, locale) {
1436
+ const writeFile = authoringToolName(surface, "write_file");
1240
1437
  if (code === "document_header_invalid" ||
1241
1438
  code === "document_identity_invalid" ||
1242
1439
  code === "invalid_document_header")
1243
- return localized(locale, "Document storage metadata is missing or damaged. Repair this document with setting_write_file using its ref, title, summary, aliases, and body", "文档存储元信息缺失或损坏;请用 setting_write_file 提供 ref、title、summary、aliases 和正文进行修复");
1440
+ return localized(locale, `Document storage metadata is missing or damaged. Repair this document with ${writeFile} using its ref, title, summary, aliases, and body`, `文档存储元信息缺失或损坏;请用 ${writeFile} 提供 ref、title、summary、aliases 和正文进行修复`);
1244
1441
  if (code === "document_identity_duplicate" || code === "duplicate_id")
1245
1442
  return localized(locale, "Runtime-owned storage identity is duplicated; repair the affected document without supplying an identity", "Runtime 维护的存储身份发生重复;请修复受影响文档,不要提供内部身份");
1246
1443
  if (code === "document_short_ref_invalid")
1247
- return localized(locale, "Document ref must be a 2-to-32-character lowercase ASCII short handle; repair it with setting_write_file and public metadata", "文档 ref 必须是 2~32 位小写 ASCII 短句柄;请用 setting_write_file 和公开元信息修复");
1444
+ return localized(locale, `Document ref must be a 2-to-32-character lowercase ASCII short handle; repair it with ${writeFile} and public metadata`, `文档 ref 必须是 2~32 位小写 ASCII 短句柄;请用 ${writeFile} 和公开元信息修复`);
1248
1445
  if (code === "document_short_ref_duplicate" || code === "duplicate_ref")
1249
1446
  return localized(locale, "Document refs must be unique; choose a different ref for the affected document", "文档 ref 必须唯一;请为受影响文档选择另一个 ref");
1250
1447
  return message
@@ -1306,13 +1503,13 @@ function fileDiff(baseFiles, draftFiles) {
1306
1503
  ];
1307
1504
  });
1308
1505
  }
1309
- function safePath(path) {
1506
+ function safePath(path, surface) {
1310
1507
  if (path.startsWith("/") ||
1311
1508
  path.includes("\\") ||
1312
1509
  path
1313
1510
  .split("/")
1314
1511
  .some((part) => part === "." || part === ".." || part.length === 0))
1315
- throw new SettingAuthoringError(`Unsafe content-package path: ${path}`);
1512
+ throw new SettingAuthoringError(`Unsafe ${surface.phrases.en.slug} path: ${path}`);
1316
1513
  return path;
1317
1514
  }
1318
1515
  function requiredString(value, name) {
@@ -1329,6 +1526,8 @@ function requiredSettingRef(value, locale) {
1329
1526
  return value;
1330
1527
  }
1331
1528
  function requiredAliases(value) {
1529
+ if (typeof value === "string")
1530
+ throw new SettingAuthoringError("aliases must be a JSON array of at most 16 strings, not a string");
1332
1531
  if (!Array.isArray(value) ||
1333
1532
  value.length > 16 ||
1334
1533
  !value.every((item) => typeof item === "string" &&
@@ -1363,6 +1562,8 @@ function assertNoLiteralUnicodeEscapes(value, locale) {
1363
1562
  walk(value);
1364
1563
  }
1365
1564
  function requiredYamlLocatorPath(value, name) {
1565
+ if (typeof value === "string")
1566
+ throw new SettingAuthoringError(`${name} must be a JSON array of map keys or zero-based array indexes, not a string`);
1366
1567
  if (!Array.isArray(value) ||
1367
1568
  value.length === 0 ||
1368
1569
  !value.every((item) => (typeof item === "string" && item.length > 0) ||