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.
- package/dist/node/protocol/v1.js +4 -0
- package/dist/node/runtime/V1Runtime.js +13 -1
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +64 -163
- package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
- package/dist/node/runtime/model/ToolArgumentCoercion.js +138 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +34 -18
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +6 -2
- package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
- package/dist/node/runtime/play/PlayCallChain.js +276 -398
- package/dist/node/runtime/play/PlayDocumentTools.js +19 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +3 -3
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -0
- package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +370 -169
- package/dist/node/runtime/world/FileNativeWorldStore.js +151 -109
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -1
- package/dist/node/runtime/world/WorldDocumentStore.js +1 -1
- package/dist/node/runtime/world/WorldLedgerAuthority.js +37 -3
- package/dist/node/runtime/world/WorldTurnLedger.js +17 -4
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -4
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +19 -241
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +8 -27
- package/dist/node/server/createServer.js +1 -4
- package/dist/node/shared/ordered-play-prompts.js +5 -0
- package/dist/node/shared/turn-guide.js +27 -2
- package/dist/web/assets/index-Cj2JYCia.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- 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
|
|
158
|
-
|
|
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: "
|
|
173
|
-
description: descriptions.
|
|
187
|
+
name: name("list"),
|
|
188
|
+
description: descriptions.list,
|
|
174
189
|
inputSchema: schema({
|
|
175
|
-
directory: {
|
|
190
|
+
directory: {
|
|
191
|
+
type: "string",
|
|
192
|
+
pattern: `^${surface.root}(?:/[^\\\\]+)?$`,
|
|
193
|
+
},
|
|
176
194
|
limit,
|
|
177
195
|
cursor,
|
|
178
196
|
}, []),
|
|
179
197
|
},
|
|
180
198
|
{
|
|
181
|
-
name: "
|
|
182
|
-
description: descriptions.
|
|
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: "
|
|
193
|
-
description: descriptions.
|
|
210
|
+
name: name("read"),
|
|
211
|
+
description: descriptions.read,
|
|
194
212
|
inputSchema: schema({ path }, ["path"]),
|
|
195
213
|
},
|
|
196
214
|
{
|
|
197
|
-
name: "
|
|
198
|
-
description: descriptions.
|
|
215
|
+
name: name("create"),
|
|
216
|
+
description: descriptions.create,
|
|
199
217
|
inputSchema: schema({
|
|
200
218
|
path: {
|
|
201
219
|
type: "string",
|
|
202
|
-
pattern:
|
|
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: "
|
|
217
|
-
description: descriptions.
|
|
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: "
|
|
233
|
-
description: descriptions.
|
|
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: "
|
|
257
|
-
description: descriptions.
|
|
274
|
+
name: name("move"),
|
|
275
|
+
description: descriptions.move,
|
|
258
276
|
inputSchema: schema({ from: text, to: path }, ["from", "to"]),
|
|
259
277
|
},
|
|
260
278
|
{
|
|
261
|
-
name: "
|
|
262
|
-
description: descriptions.
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
356
|
-
return listDocuments(this.#snapshot, call.arguments, this.#locale);
|
|
357
|
-
if (call.
|
|
358
|
-
return searchDocuments(this.#snapshot, call.arguments, this.#locale);
|
|
359
|
-
if (call.
|
|
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.
|
|
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.
|
|
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: [
|
|
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.
|
|
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
|
-
:
|
|
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
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
639
|
-
|
|
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
|
|
643
|
-
|
|
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
|
|
647
|
-
return
|
|
648
|
-
name
|
|
649
|
-
name
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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.
|
|
843
|
+
return (call.operation === "write_file" &&
|
|
658
844
|
typeof call.arguments.path === "string" &&
|
|
659
|
-
call.arguments.path.startsWith(
|
|
845
|
+
call.arguments.path.startsWith(`${surface.root}/`));
|
|
660
846
|
}
|
|
661
|
-
function worldRevisionCommand(snapshot, call, locale) {
|
|
662
|
-
if (call.
|
|
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("
|
|
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.
|
|
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.
|
|
702
|
-
const toLogicalPath = safePath(requiredString(call.arguments.to, "to"));
|
|
703
|
-
if (
|
|
704
|
-
throw new SettingAuthoringError("
|
|
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.
|
|
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("
|
|
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("
|
|
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(
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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("
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
1040
|
-
const directory = args.directory ??
|
|
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 !==
|
|
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(
|
|
1050
|
-
const relative = directory ===
|
|
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(
|
|
1265
|
+
.filter(({ path }) => !path.startsWith(`${root}/`))
|
|
1071
1266
|
.map(({ path, encoding }) => encoding === undefined ? `- [special] ${path}` : `- [binary] ${path}`));
|
|
1072
|
-
return success(`# ${localized(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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
1133
|
-
const opaque = snapshot.files.find((file) => file.path === path && !file.path.startsWith(
|
|
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
|
-
?
|
|
1145
|
-
:
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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) ||
|