narraeon 0.7.7 → 0.7.9

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 (24) hide show
  1. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +14 -1
  2. package/dist/node/runtime/history/HistoryRendering.js +25 -6
  3. package/dist/node/runtime/history/HistoryTools.js +31 -0
  4. package/dist/node/runtime/play/PlayCallChain.js +4 -9
  5. package/dist/node/runtime/play/PlayDocumentTools.js +35 -37
  6. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +33 -17
  7. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +5 -9
  8. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +326 -62
  9. package/dist/node/runtime/setting/SettingImprovementSession.js +40 -3
  10. package/dist/node/runtime/world/FileNativeWorldStore.js +13 -3
  11. package/dist/node/runtime/world/WorldDocumentMaintenance.js +9 -2
  12. package/dist/node/runtime/world/WorldLedgerAuthority.js +18 -2
  13. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +61 -17
  14. package/dist/node/runtime/world-revision/LegacyWorldRevisionEpochUpgrade.js +46 -0
  15. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +83 -30
  16. package/dist/node/runtime/world-revision/WorldRevisionSession.js +64 -4
  17. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +32 -11
  18. package/dist/node/shared/default-setting-improvement-prompt.js +23 -8
  19. package/dist/node/shared/ordered-author-prompts.js +9 -7
  20. package/dist/web/assets/index-B2kbaOGe.js +1408 -0
  21. package/dist/web/assets/{index-BjxBYYQi.css → index-DH2BVsYd.css} +1 -1
  22. package/dist/web/index.html +2 -2
  23. package/package.json +1 -1
  24. package/dist/web/assets/index-Da0tdDij.js +0 -1408
@@ -5,9 +5,11 @@ import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringReq
5
5
  import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
6
6
  import { equalModelHostBinding } from "../model/ModelHost.js";
7
7
  import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
8
+ import { authoringPreloadAuthorization, buildAuthoringPreload, worldRevisionAuthoringSurface, } from "../setting/SettingAuthoringTransaction.js";
8
9
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
9
10
  import { WorldRevisionAuthoringTransaction, worldRevisionRuntimeContract, worldRevisionToolDefinitions, } from "./WorldRevisionAuthoringTransaction.js";
10
11
  import { WorldRevisionWorkspaceError } from "./WorldRevisionWorkspace.js";
12
+ import { historyEntries } from "../history/HistoryTools.js";
11
13
  /** Conversation orchestration over the shared locked revision worktree. */
12
14
  export class WorldRevisionSession {
13
15
  #store;
@@ -197,7 +199,7 @@ export class WorldRevisionSession {
197
199
  const host = await this.#bindExistingModelHost(session.modelBinding);
198
200
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
199
201
  throw new Error("No saved model connection matches this world-revision conversation");
200
- const { playPreset, bootstrap } = await this.#compileCurrent(input.worldId, session.modelBinding);
202
+ const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.worldId, session.modelBinding);
201
203
  session.schemaVersion = 2;
202
204
  session.requests ??= [];
203
205
  session.requests.push({
@@ -205,7 +207,12 @@ export class WorldRevisionSession {
205
207
  modelItemStart: session.modelItems.length,
206
208
  bootstrap,
207
209
  playPreset: structuredClone(playPreset),
210
+ ...(preload === null
211
+ ? {}
212
+ : { preload: structuredClone(preload.record) }),
208
213
  });
214
+ if (preload !== null)
215
+ session.authorization = authoringPreloadAuthorization(session.authorization, preload.record);
209
216
  appendAuthoringUserMessage(session, message, requestId);
210
217
  await this.#store.saveSession(session);
211
218
  return {
@@ -262,6 +269,9 @@ export class WorldRevisionSession {
262
269
  const playPreset = await this.#bindPlayPreset();
263
270
  const locale = this.#locale();
264
271
  const gameplay = await this.#worlds.gameplay(worldId);
272
+ const preload = epoch === null
273
+ ? null
274
+ : await this.#preload(epoch, modelBinding, playPreset, locale);
265
275
  const bootstrap = this.#compiler.compileWorldRevision({
266
276
  ...(epoch === null
267
277
  ? {}
@@ -273,8 +283,54 @@ export class WorldRevisionSession {
273
283
  playPreset,
274
284
  modelBinding: modelBinding,
275
285
  tools: worldRevisionToolDefinitions(locale, gameplay),
286
+ ...(preload === null ? {} : { preload: preload.blocks }),
287
+ });
288
+ return { bootstrap, playPreset, preload };
289
+ }
290
+ /**
291
+ * Preloads the control documents and the documents the world frame injects.
292
+ * A preview that cannot be built degrades to the directory and control files.
293
+ */
294
+ async #preload(epoch, modelBinding, playPreset, locale) {
295
+ const [binding, maintenance] = await Promise.all([
296
+ this.#worlds.bindWorldRevision({
297
+ worldId: epoch.worldId,
298
+ epochId: epoch.epochId,
299
+ }),
300
+ this.#worlds.inspectDocumentMaintenance(epoch.worldId, epoch.baseHead),
301
+ ]);
302
+ const snapshot = WorldDocumentStore.open({
303
+ layout: "world_state",
304
+ files: epoch.files,
305
+ });
306
+ let injected = [];
307
+ let injectedUnavailable;
308
+ try {
309
+ const preview = this.#preview({
310
+ snapshot,
311
+ modelBinding,
312
+ playPreset,
313
+ binding,
314
+ epoch,
315
+ maintenance,
316
+ });
317
+ injected = preview.compilation.coverage.flatMap(({ slot, status, readAuthorization }) => status === "resolved" && readAuthorization !== undefined
318
+ ? [{ shortRef: readAuthorization.shortRef, slot }]
319
+ : []);
320
+ }
321
+ catch (error) {
322
+ injectedUnavailable =
323
+ error instanceof Error ? error.message : "unknown preview failure";
324
+ }
325
+ return buildAuthoringPreload({
326
+ snapshot,
327
+ surface: worldRevisionAuthoringSurface,
328
+ locale,
329
+ revision: epoch.revision,
330
+ sourcePrefix: "world-revision:preload",
331
+ injected,
332
+ ...(injectedUnavailable === undefined ? {} : { injectedUnavailable }),
276
333
  });
277
- return { bootstrap, playPreset };
278
334
  }
279
335
  async cancel(sessionId) {
280
336
  const running = this.#conversation.cancel(sessionId);
@@ -408,6 +464,10 @@ export class WorldRevisionSession {
408
464
  return new WorldRevisionAuthoringTransaction({
409
465
  baseFiles: epoch.files,
410
466
  immutableBaseFiles: epoch.baseFiles,
467
+ history: {
468
+ scope: epoch.worldId,
469
+ entries: historyEntries(binding.history, binding.historyBackgrounds),
470
+ },
411
471
  locale: session.locale,
412
472
  revision: epoch.revision,
413
473
  authorization: session.authorization,
@@ -565,8 +625,8 @@ export class WorldRevisionSession {
565
625
  }
566
626
  function retargetSession(session, epoch, locale) {
567
627
  const markdown = locale === "zh-CN"
568
- ? `# 新的世界修订 epoch\n\n上一轮修订已经应用或放弃。当前工作树是 ${epoch.epochId},从世界端点 ${epoch.baseHead} 重新建立。此前全部 list/search cursor 和 read 写授权已失效;修改前必须重新读取。`
569
- : `# New world-revision epoch\n\nThe previous revision was applied or discarded. Worktree ${epoch.epochId} was rebuilt from world endpoint ${epoch.baseHead}. Every prior list/search cursor and read authorization is invalid; re-read before writing.`;
628
+ ? `# 新的世界修订 epoch\n\n上一轮修订已经应用或放弃。当前工作树是 ${epoch.epochId},从世界端点 ${epoch.baseHead} 重新建立。此前全部 search cursor 和 read 写授权已失效;修改前必须重新读取。`
629
+ : `# New world-revision epoch\n\nThe previous revision was applied or discarded. Worktree ${epoch.epochId} was rebuilt from world endpoint ${epoch.baseHead}. Every prior search cursor and read authorization is invalid; re-read before writing.`;
570
630
  session.modelItems.push({
571
631
  kind: "prompt_delta",
572
632
  logicalMessages: [
@@ -353,18 +353,21 @@ function assertExpectedRevision(epoch, expected) {
353
353
  throw new WorldRevisionWorkspaceError("The world-revision worktree changed; refresh it before writing");
354
354
  }
355
355
  function stateChanges(baseFiles, files) {
356
- const base = new Map(baseFiles
357
- .filter(({ path }) => path.startsWith("state/"))
358
- .map((file) => [file.path, file]));
356
+ const baseStateFiles = baseFiles.filter(({ path }) => path.startsWith("state/"));
357
+ const base = new Map(baseStateFiles.map((file) => [file.path, file]));
358
+ const candidateStateFiles = files.filter(({ path }) => path.startsWith("state/"));
359
359
  const snapshot = WorldDocumentStore.open({
360
360
  layout: "world_state",
361
- files: files.filter(({ path }) => path.startsWith("state/")),
361
+ files: candidateStateFiles,
362
362
  });
363
363
  if (snapshot.status !== "usable")
364
364
  throw new WorldRevisionWorkspaceError("Only a usable world-state snapshot can be applied");
365
- return files
366
- .filter(({ path }) => path.startsWith("state/"))
367
- .flatMap((file) => {
365
+ const baseSnapshot = WorldDocumentStore.open({
366
+ layout: "world_state",
367
+ files: baseStateFiles,
368
+ });
369
+ const candidatePaths = new Set(candidateStateFiles.map(({ path }) => path));
370
+ const changes = candidateStateFiles.flatMap((file) => {
368
371
  const previous = base.get(file.path);
369
372
  if (previous?.contents === file.contents)
370
373
  return [];
@@ -375,10 +378,6 @@ function stateChanges(baseFiles, files) {
375
378
  if (result.kind !== "read_document" || !result.ok)
376
379
  throw new WorldRevisionWorkspaceError(`Cannot resolve changed world document: ${file.path}`);
377
380
  if (previous !== undefined) {
378
- const baseSnapshot = WorldDocumentStore.open({
379
- layout: "world_state",
380
- files: baseFiles.filter(({ path }) => path.startsWith("state/")),
381
- });
382
381
  const before = baseSnapshot.query({
383
382
  kind: "read_document",
384
383
  document: { logicalPath: file.path },
@@ -401,6 +400,28 @@ function stateChanges(baseFiles, files) {
401
400
  },
402
401
  ];
403
402
  });
403
+ // A base document whose path is gone was deleted or moved away; a move's new path is a create above.
404
+ const removals = baseStateFiles
405
+ .filter(({ path }) => !candidatePaths.has(path))
406
+ .map((file) => {
407
+ const before = baseSnapshot.query({
408
+ kind: "read_document",
409
+ document: { logicalPath: file.path },
410
+ });
411
+ if (before.kind !== "read_document" || !before.ok)
412
+ throw new WorldRevisionWorkspaceError(`Cannot resolve removed world document: ${file.path}`);
413
+ return {
414
+ kind: "delete",
415
+ documentId: before.document.documentId,
416
+ stableShortRef: before.document.shortRef,
417
+ relativePath: file.path.slice("state/".length),
418
+ codec: before.document.codec,
419
+ expectedPreviousHash: sha256(file.contents),
420
+ nextHash: null,
421
+ canonicalNextBytes: null,
422
+ };
423
+ });
424
+ return [...changes, ...removals];
404
425
  }
405
426
  function controlFiles(files) {
406
427
  return files
@@ -7,17 +7,22 @@
7
7
  * mechanical setting-improvement boundary.
8
8
  */
9
9
  export const defaultSettingImprovementPromptPath = "prompts/setting-improvement.md";
10
+ /** Content-package-only paragraphs; a running-world revision omits them. */
11
+ const contentPackageWorldStartZhCN = "内容包设定完善定义开场结束处的世界起点,可以包含已有历史、当前状态、持续过程和各人的认知。简单起点足以开玩,普通未定义内容可在游玩中生长;不预写未来必然发生的经历或强制剧情阶段。";
12
+ const contentPackageOpeningZhCN = "仅在内容包分支检查 opening.md:写玩家当时可感知的具体局面,保留尚待回答的请求与必要原话,不替玩家决定行动、台词或内心。独处、安静和等待也可形成开场。开场与文档停在同一时点,将约束首次行动的结果写给自然所有者,无需复制整篇叙事。";
13
+ const contentPackageWorldStartEn = "Content-package setting improvement defines the world where the opening ends, including established history, current state, ongoing processes and individual knowledge. A simple starting point can be playable and ordinary undefined content can grow during play. Do not prewrite inevitable future experiences or mandatory plot stages.";
14
+ const contentPackageOpeningEn = "Inspect opening.md only in the content-package branch. Show a concrete situation the player can perceive, retaining unanswered requests and consequential wording without choosing player action, speech or inner thoughts. Solitude, quiet and waiting can also open a story. Documents and opening stop at the same moment; give outcomes constraining the first action to natural owners without copying the whole narrative.";
10
15
  export const defaultSettingImprovementPromptZhCN = `# 系统推荐的创作方法
11
16
 
12
17
  以当前编辑目标、已有内容和用户目标为起点。讨论时说明必要判断,落实时定向修改;用户要求审查或重组时,相关结构与重复表达也在范围内。不为填满结构制造内容,无需修改也是合法结果。
13
18
 
14
19
  ## 编辑目标与世界起点
15
20
 
16
- 内容包设定完善定义开场结束处的世界起点,可以包含已有历史、当前状态、持续过程和各人的认知。简单起点足以开玩,普通未定义内容可在游玩中生长;不预写未来必然发生的经历或强制剧情阶段。
21
+ ${contentPackageWorldStartZhCN}
17
22
 
18
- 运行中世界修订维护当前已经演变的世界,按用户要求调整并保留未受影响的事实、经历与认知,不退回初始取值。此分支不读取或修改 opening.md。明确修订不是世界内事件,不需虚构行动解释它。
23
+ 运行中世界修订维护当前已经演变的世界,按用户要求调整并保留未受影响的事实、经历与认知,不退回初始取值。明确修订不是世界内事件,不需虚构行动解释它。
19
24
 
20
- 涉及结构、材料发现或更新位置时,先列出编辑目标根目录,完整读取 control/frame.yaml、启用的世界提示块、绑定的当前情境及受影响文档。实际操作遵守本轮工具边界。
25
+ 涉及结构、材料发现或更新位置时,以本请求 bootstrap 已提供的目录、控制文档与注入文档为起点;未提供而受影响的文档再用工具读取。实际操作遵守本轮工具边界。
21
26
 
22
27
  ## 世界信息怎样组织
23
28
 
@@ -41,7 +46,7 @@ world/ 或 state/ 承载虚构事实,control/ 安排本世界的取材、规
41
46
 
42
47
  ## 开场与整理
43
48
 
44
- 仅在内容包分支检查 opening.md:写玩家当时可感知的具体局面,保留尚待回答的请求与必要原话,不替玩家决定行动、台词或内心。独处、安静和等待也可形成开场。开场与文档停在同一时点,将约束首次行动的结果写给自然所有者,无需复制整篇叙事。
49
+ ${contentPackageOpeningZhCN}
45
50
 
46
51
  整理结束已完成或失效的进程、约定和限制,保留仍有意义的成果、后遗影响、经历与引用。补录、去重和归档不让事情再发生。长材料按需发现,软体积提示不授权截断;暂时离场不等于退役,退役也不会移除显式全文槽。
47
52
 
@@ -53,11 +58,11 @@ Start with the editing target, existing content and the user's goal. Discuss nec
53
58
 
54
59
  ## Editing target and world starting point
55
60
 
56
- Content-package setting improvement defines the world where the opening ends, including established history, current state, ongoing processes and individual knowledge. A simple starting point can be playable and ordinary undefined content can grow during play. Do not prewrite inevitable future experiences or mandatory plot stages.
61
+ ${contentPackageWorldStartEn}
57
62
 
58
- Running-world revision maintains the world as it has evolved: apply requested changes while preserving unaffected facts, events and knowledge instead of resetting initial values. This branch does not read or modify opening.md. Explicit revision is not an in-world event and needs no invented action to explain it.
63
+ Running-world revision maintains the world as it has evolved: apply requested changes while preserving unaffected facts, events and knowledge instead of resetting initial values. Explicit revision is not an in-world event and needs no invented action to explain it.
59
64
 
60
- When structure, discovery or update locations are affected, list the editing target's root and completely read control/frame.yaml, enabled world instructions, the bound current situation and affected documents. Follow this request's actual tool boundary.
65
+ When structure, discovery or update locations are affected, start from the directory, control documents and injected documents this request's bootstrap already provides; read affected documents with tools only when they were not provided. Follow this request's actual tool boundary.
61
66
 
62
67
  ## Organizing world information
63
68
 
@@ -81,7 +86,7 @@ world/ or state/ carries fictional facts. control/ arranges world-specific mater
81
86
 
82
87
  ## Opening and consolidation
83
88
 
84
- Inspect opening.md only in the content-package branch. Show a concrete situation the player can perceive, retaining unanswered requests and consequential wording without choosing player action, speech or inner thoughts. Solitude, quiet and waiting can also open a story. Documents and opening stop at the same moment; give outcomes constraining the first action to natural owners without copying the whole narrative.
89
+ ${contentPackageOpeningEn}
85
90
 
86
91
  Consolidation closes completed or expired processes, commitments and restrictions while retaining meaningful results, lasting effects, events and references. Recording, deduplication and archiving do not perform events again. Keep long material discoverable on demand; advisory size limits do not authorize truncation. Temporary absence is not retirement, and retirement does not remove explicit full-document slots.
87
92
 
@@ -93,3 +98,13 @@ export function defaultSettingImprovementPromptForLocale(locale) {
93
98
  ? defaultSettingImprovementPromptZhCN
94
99
  : defaultSettingImprovementPromptEn;
95
100
  }
101
+ /**
102
+ * The same guidance for a running-world revision: the content-package-only
103
+ * paragraphs are omitted, and everything else stays byte-identical.
104
+ */
105
+ export function revisionSettingImprovementPromptForLocale(locale) {
106
+ const paragraphs = locale === "zh-CN"
107
+ ? [contentPackageWorldStartZhCN, contentPackageOpeningZhCN]
108
+ : [contentPackageWorldStartEn, contentPackageOpeningEn];
109
+ return paragraphs.reduce((text, paragraph) => text.replace(`${paragraph}\n\n`, ""), defaultSettingImprovementPromptForLocale(locale));
110
+ }
@@ -1,7 +1,7 @@
1
- import { defaultSettingImprovementPromptForLocale } from "./default-setting-improvement-prompt.js";
1
+ import { defaultSettingImprovementPromptForLocale, revisionSettingImprovementPromptForLocale, } from "./default-setting-improvement-prompt.js";
2
2
  import { tabletopAuthorBody } from "./tabletop-guidance.js";
3
3
  /** Authoring uses the same permissions as play, with target-specific dynamic references. */
4
- export function builtinAuthorPrompts(locale) {
4
+ export function builtinAuthorPrompts(locale, target = "setting") {
5
5
  const zh = locale === "zh-CN";
6
6
  return [
7
7
  {
@@ -18,7 +18,9 @@ export function builtinAuthorPrompts(locale) {
18
18
  name: zh ? "创作建议" : "Authoring guidance",
19
19
  required: false,
20
20
  category: "policy",
21
- body: defaultSettingImprovementPromptForLocale(locale),
21
+ body: target === "world-revision"
22
+ ? revisionSettingImprovementPromptForLocale(locale)
23
+ : defaultSettingImprovementPromptForLocale(locale),
22
24
  },
23
25
  {
24
26
  id: "author.tabletop",
@@ -53,15 +55,15 @@ export function builtinAuthorPrompts(locale) {
53
55
  export function authoringMechanics(locale, target) {
54
56
  const zh = locale === "zh-CN";
55
57
  const common = zh
56
- ? `只使用请求随附的工具及其参数契约。含工具调用的响应是中间步;收到全部工具结果后继续,非空且无工具调用的响应结束本次发送。完整工具响应按调用顺序结算,失败调用不撤销成功同级调用。修改既有文件前必须完整读取;cursor 只属于产生它的树 revision;本事务自身成功修改后的完整读取授权由 Runtime 承接。外部树变化或跨 epoch 后旧授权失效,需要重新读取。工具返回逻辑路径,不能访问宿主文件。文档引用使用返回的 @ref;YAML 整文档引用为单键 {$ref: "@ref"},locator 使用 map key 或从零开始的数组下标。成功回执决定当前值,模型文字不构成提交。`
57
- : `Use only the attached tools and their parameter contracts. Responses with tool calls are intermediate steps; continue after all results. A nonempty tool-free response ends this send. Complete tool responses settle calls in order; a failed call does not undo successful siblings. Completely read existing files before writing. Cursors belong to their producing tree revision. Runtime carries complete-read authorization through this transaction’s own successful changes. External tree changes or an epoch transition invalidate old authorization and require new reads. Tools expose logical paths, never host files. Use returned @refs for documents; YAML whole-document references are single-key {$ref: "@ref"} maps, and locators use map keys or zero-based array indexes. Actual receipts determine current values; model prose cannot commit changes.`;
58
+ ? `只使用请求随附的工具及其参数契约。含工具调用的响应是中间步;收到全部工具结果后继续,非空且无工具调用的响应结束本次发送。完整工具响应按调用顺序结算,失败调用不撤销成功同级调用。修改既有文件前必须完整读取;cursor 只属于产生它的树 revision;本事务自身成功修改后的完整读取授权由 Runtime 承接。外部树变化或跨 epoch 后旧授权失效,需要重新读取。bootstrap 中 Runtime 预载的完整文档视为已在该 revision 完整读取,可直接修改;目录不算读取。工具返回逻辑路径,不能访问宿主文件。文档引用使用返回的 @ref;YAML 整文档引用为单键 {$ref: "@ref"},locator 使用 map key 或从零开始的数组下标。成功回执决定当前值,模型文字不构成提交。`
59
+ : `Use only the attached tools and their parameter contracts. Responses with tool calls are intermediate steps; continue after all results. A nonempty tool-free response ends this send. Complete tool responses settle calls in order; a failed call does not undo successful siblings. Completely read existing files before writing. Cursors belong to their producing tree revision. Runtime carries complete-read authorization through this transaction’s own successful changes. External tree changes or an epoch transition invalidate old authorization and require new reads. Runtime-preloaded complete documents in the bootstrap count as completely read at that revision; the directory does not. Tools expose logical paths, never host files. Use returned @refs for documents; YAML whole-document references are single-key {$ref: "@ref"} maps, and locators use map keys or zero-based array indexes. Actual receipts determine current values; model prose cannot commit changes.`;
58
60
  const boundary = target === "setting"
59
61
  ? zh
60
62
  ? "内容包工具的成功变化在完整响应结算时原子发布到当前树,无需应用。setting_create 建立新文档身份;删除仅在无绑定与引用阻挡时接受。"
61
63
  : "Successful content-package changes publish atomically to the current tree when the complete response settles, with no Apply step. setting_create establishes new document identities; deletion is accepted only without binding or reference blockers."
62
64
  : zh
63
- ? "世界修订工具只修改持久独占 epoch 的 state/* 与 control/* 工作树。opening.md 不可读取或修改,既有状态文档不可删除或移动。只有玩家应用或放弃才结束 epoch;应用提交世界,放弃不改变世界。跨 epoch 全部旧读取授权失效。"
64
- : "World-revision tools modify only state/* and control/* in the durably locked exclusive epoch. opening.md cannot be read or changed; existing state documents cannot be deleted or moved. Only player Apply or Discard ends the epoch; Apply commits the world, Discard leaves it unchanged. Every old read authorization expires across epochs.";
65
+ ? "世界修订工具只修改持久独占 epoch 的 state/* 与 control/* 工作树。opening.md 不可读取或修改;既有状态文档可以删除或移动,删除须无绑定与引用阻挡。只有玩家应用或放弃才结束 epoch;应用提交世界,放弃不改变世界。跨 epoch 全部旧读取授权失效。世界修订可用 history_list、history_search、history_read 只读导航已提交历史,历史不能被改写。"
66
+ : "World-revision tools modify only state/* and control/* in the durably locked exclusive epoch. opening.md cannot be read or changed; existing state documents can be deleted or moved, and deletion requires no binding or reference blockers. Only player Apply or Discard ends the epoch; Apply commits the world, Discard leaves it unchanged. Every old read authorization expires across epochs. World revision may read committed history with history_list, history_search and history_read; history cannot be rewritten.";
65
67
  const views = zh
66
68
  ? 'control/player-views.yaml 的 format 为 narraeon.player-views/v1;views 中每项有 id、title 和 items,items 中每项有 id、label、select。精确选择一个 YAML 字段使用 select: {document: "@ref", locator: {yaml: [字段名]}};Markdown 小节使用 locator: {markdown: [标题]}。document 必须来自实际返回的引用,locator 必须对应已读正文;省略 locator 会展示整份文档,选择容器会递归展示子树。'
67
69
  : 'control/player-views.yaml uses format: narraeon.player-views/v1. Each views entry has id, title and items; each item has id, label and select. Select one YAML field with select: {document: "@ref", locator: {yaml: [field]}}; select a Markdown section with locator: {markdown: [heading]}. Use a returned document reference and a locator from the actual read body. Omitting locator displays the whole document; selecting a container recursively displays its subtree.';