narraeon 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +186 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +445 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-DG85hpWC.css +1 -0
  70. package/dist/web/assets/index-DfxvCjb6.js +1207 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D8YKfmNG.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -0,0 +1,218 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { aggregateModelUsage, } from "../../protocol/modelUsage.js";
3
+ import { ModelHostCancelledError } from "../model/ModelHost.js";
4
+ const maximumExchangesPerMessage = 64;
5
+ const maximumStreamTailCharacters = 240;
6
+ /**
7
+ * Deep Module for the shared authoring-conversation loop. Its Interface leaves
8
+ * content-package and world-revision publication behind the settlement seam.
9
+ */
10
+ export class ModelToolConversation {
11
+ #runs = new Map();
12
+ isRunning(sessionId) {
13
+ return this.#runs.has(sessionId);
14
+ }
15
+ runningRequestId(sessionId) {
16
+ return this.#runs.get(sessionId)?.requestId ?? null;
17
+ }
18
+ running(sessionId) {
19
+ const run = this.#runs.get(sessionId);
20
+ return run === undefined
21
+ ? null
22
+ : { requestId: run.requestId, promise: run.promise };
23
+ }
24
+ streaming(sessionId) {
25
+ const streaming = this.#runs.get(sessionId)?.streaming;
26
+ return streaming === undefined ? null : structuredClone(streaming);
27
+ }
28
+ run(input) {
29
+ const existing = this.#runs.get(input.session.sessionId);
30
+ if (existing !== undefined) {
31
+ if (existing.requestId !== input.requestId)
32
+ throw new Error("Another message in this authoring conversation is still running");
33
+ return existing.promise;
34
+ }
35
+ const live = {
36
+ requestId: input.requestId,
37
+ controller: new AbortController(),
38
+ promise: Promise.resolve(undefined),
39
+ streaming: emptyStreaming(),
40
+ };
41
+ live.promise = this.#execute(input, live).finally(() => {
42
+ if (this.#runs.get(input.session.sessionId) === live)
43
+ this.#runs.delete(input.session.sessionId);
44
+ input.changed?.(true);
45
+ });
46
+ this.#runs.set(input.session.sessionId, live);
47
+ input.changed?.(true);
48
+ return live.promise;
49
+ }
50
+ cancel(sessionId) {
51
+ const running = this.#runs.get(sessionId);
52
+ if (running === undefined)
53
+ return null;
54
+ running.controller.abort();
55
+ return running.promise;
56
+ }
57
+ async #execute(input, live) {
58
+ const { session } = input;
59
+ const bootstrap = session.requests?.at(-1)?.bootstrap ?? session.bootstrap;
60
+ try {
61
+ for (let round = 0; round < maximumExchangesPerMessage; round += 1) {
62
+ session.exchange += 1;
63
+ live.streaming = emptyStreaming();
64
+ session.updatedAt = Date.now();
65
+ await input.save(session);
66
+ input.changed?.(true);
67
+ const response = await input.host.exchange({
68
+ bootstrap,
69
+ toolUniverse: bootstrap.toolUniverse,
70
+ allowedTools: bootstrap.toolUniverse.map(({ name }) => name),
71
+ toolStrategy: bootstrap.toolStrategy,
72
+ tools: bootstrap.tools,
73
+ appended: session.modelItems,
74
+ requestId: live.requestId,
75
+ operationId: session.sessionId,
76
+ requestAttempt: 1,
77
+ exchange: session.exchange,
78
+ maxOutputTokens: session.modelBinding.maxOutputTokens,
79
+ }, {
80
+ signal: live.controller.signal,
81
+ onDelta: (delta) => {
82
+ recordDelta(live, delta);
83
+ input.changed?.(false);
84
+ },
85
+ });
86
+ assertCompleteResponse(response);
87
+ const toolCalls = response.toolCalls ?? [];
88
+ assertUniqueToolCallIds(toolCalls);
89
+ const visibleText = response.text ?? "";
90
+ if (toolCalls.length === 0 && visibleText.trim().length === 0)
91
+ throw new Error(input.failure?.emptyResponse ??
92
+ "The model returned an empty tool-free authoring response");
93
+ session.usage = aggregateModelUsage(session.usage, response.usage);
94
+ session.modelItems.push({
95
+ kind: "assistant",
96
+ text: visibleText,
97
+ ...(response.reasoningContent === undefined
98
+ ? {}
99
+ : { reasoningContent: response.reasoningContent }),
100
+ providerState: response.providerState,
101
+ toolCalls: structuredClone(toolCalls),
102
+ });
103
+ session.updatedAt = Date.now();
104
+ if (toolCalls.length === 0) {
105
+ session.messages.push({
106
+ id: `message-${randomUUID()}`,
107
+ role: "assistant",
108
+ text: visibleText,
109
+ createdAt: session.updatedAt,
110
+ });
111
+ session.runStatus = "ready";
112
+ session.activeRequestId = null;
113
+ session.completedRequestIds = rememberRequest(session.completedRequestIds, live.requestId);
114
+ session.lastFailure = null;
115
+ await input.save(session);
116
+ input.changed?.(true);
117
+ return input.view(session);
118
+ }
119
+ await input.settleToolResponse(session, session.modelItems.length - 1, toolCalls);
120
+ input.changed?.(true);
121
+ }
122
+ throw new Error(input.failure?.exchangeLimit ??
123
+ `Authoring exceeded ${maximumExchangesPerMessage} model exchanges for one user message`);
124
+ }
125
+ catch (error) {
126
+ session.runStatus = "interrupted";
127
+ session.activeRequestId = null;
128
+ session.completedRequestIds = rememberRequest(session.completedRequestIds, live.requestId);
129
+ session.lastFailure =
130
+ error instanceof ModelHostCancelledError
131
+ ? (input.failure?.cancelled ??
132
+ "The authoring response was cancelled. Complete tool responses already settled were retained.")
133
+ : error instanceof Error
134
+ ? error.message
135
+ : (input.failure?.interrupted ??
136
+ "The authoring response was interrupted");
137
+ session.updatedAt = Date.now();
138
+ await input.save(session);
139
+ return input.view(session);
140
+ }
141
+ }
142
+ }
143
+ export function appendAuthoringUserMessage(session, message, requestId) {
144
+ const now = Date.now();
145
+ session.messages.push({
146
+ id: `message-${randomUUID()}`,
147
+ role: "user",
148
+ text: requiredAuthoringMessage(message),
149
+ createdAt: now,
150
+ });
151
+ session.modelItems.push({ kind: "user", text: message });
152
+ session.runStatus = "running";
153
+ session.activeRequestId = requiredAuthoringRequestId(requestId);
154
+ session.lastFailure = null;
155
+ session.updatedAt = now;
156
+ }
157
+ export function requiredAuthoringMessage(message) {
158
+ if (message.trim().length === 0)
159
+ throw new Error("An authoring message cannot be empty");
160
+ if (message.length > 64 * 1024)
161
+ throw new Error("The authoring message is too large");
162
+ return message;
163
+ }
164
+ export function requiredAuthoringRequestId(requestId) {
165
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(requestId))
166
+ throw new Error("Invalid authoring request ID");
167
+ return requestId;
168
+ }
169
+ export function rememberAuthoringRequest(existing, requestId) {
170
+ return rememberRequest(existing, requestId);
171
+ }
172
+ function assertCompleteResponse(response) {
173
+ if (response.providerState === undefined)
174
+ throw new Error("The complete authoring response is missing provider continuation state");
175
+ }
176
+ function assertUniqueToolCallIds(calls) {
177
+ const ids = new Set();
178
+ for (const call of calls) {
179
+ if (call.id.length === 0 || call.name.length === 0 || ids.has(call.id))
180
+ throw new Error("The model returned invalid or duplicate tool-call IDs");
181
+ ids.add(call.id);
182
+ }
183
+ }
184
+ function rememberRequest(existing, requestId) {
185
+ return [
186
+ ...existing.filter((candidate) => candidate !== requestId),
187
+ requestId,
188
+ ];
189
+ }
190
+ function emptyStreaming() {
191
+ return {
192
+ reasoningChars: 0,
193
+ textChars: 0,
194
+ toolChars: 0,
195
+ tail: "",
196
+ reasoningText: "",
197
+ visibleText: "",
198
+ toolFragment: "",
199
+ receivedAt: Date.now(),
200
+ };
201
+ }
202
+ function recordDelta(live, delta) {
203
+ if (delta.kind === "reasoning") {
204
+ live.streaming.reasoningChars += delta.text.length;
205
+ live.streaming.reasoningText = `${live.streaming.reasoningText ?? ""}${delta.text}`;
206
+ }
207
+ else if (delta.kind === "text") {
208
+ live.streaming.textChars += delta.text.length;
209
+ live.streaming.visibleText = `${live.streaming.visibleText ?? ""}${delta.text}`;
210
+ }
211
+ else {
212
+ live.streaming.toolChars += delta.text.length;
213
+ live.streaming.toolFragment = `${live.streaming.toolFragment ?? ""}${delta.text}`;
214
+ }
215
+ if (delta.kind === "text")
216
+ live.streaming.tail = `${live.streaming.tail}${delta.text}`.slice(-maximumStreamTailCharacters);
217
+ live.streaming.receivedAt = Date.now();
218
+ }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmod, mkdir, open, readFile, rename, stat, unlink, } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { defaultAppLocale, isAppLocale, } from "../../protocol/appPreferences.js";
4
+ import { defaultAppLocale, defaultAppReadingPreferences, isAppLocale, isAppReadingPreferences, } from "../../protocol/appPreferences.js";
5
5
  const preferencesFileName = "preferences-v1.json";
6
6
  export class AppPreferencesStore {
7
7
  #configRoot;
@@ -15,11 +15,20 @@ export class AppPreferencesStore {
15
15
  await this.#mutationTail;
16
16
  return toView(await this.#readDocument());
17
17
  }
18
- save(locale) {
18
+ save(update) {
19
19
  return this.#mutate(async () => {
20
- if (!isAppLocale(locale))
21
- throw new Error("Unsupported app locale");
22
- const document = { schemaVersion: 1, locale };
20
+ if (Object.keys(update).length === 0 ||
21
+ Object.keys(update).some((key) => key !== "locale" && key !== "reading") ||
22
+ (update.locale !== undefined && !isAppLocale(update.locale)) ||
23
+ (update.reading !== undefined &&
24
+ !isAppReadingPreferences(update.reading)))
25
+ throw new Error("Unsupported app preferences update");
26
+ const current = toView(await this.#readDocument());
27
+ const document = {
28
+ schemaVersion: 2,
29
+ locale: update.locale ?? current.locale,
30
+ reading: structuredClone(update.reading ?? current.reading),
31
+ };
23
32
  await this.#writeDocument(document);
24
33
  return toView(document);
25
34
  });
@@ -49,7 +58,11 @@ export class AppPreferencesStore {
49
58
  if (!isMissingFile(error))
50
59
  throw error;
51
60
  }
52
- return { schemaVersion: 1, locale: defaultAppLocale };
61
+ return {
62
+ schemaVersion: 2,
63
+ locale: defaultAppLocale,
64
+ reading: structuredClone(defaultAppReadingPreferences),
65
+ };
53
66
  }
54
67
  async #writeDocument(document) {
55
68
  await mkdir(this.#configRoot, { recursive: true, mode: 0o700 });
@@ -73,19 +86,35 @@ export class AppPreferencesStore {
73
86
  }
74
87
  }
75
88
  function validateDocument(value) {
76
- if (typeof value !== "object" ||
77
- value === null ||
78
- Array.isArray(value) ||
79
- !("schemaVersion" in value) ||
80
- value.schemaVersion !== 1 ||
81
- !("locale" in value) ||
82
- !isAppLocale(value.locale) ||
83
- Object.keys(value).some((key) => key !== "schemaVersion" && key !== "locale"))
84
- throw new Error("App preferences file does not match the v1 schema");
85
- return { schemaVersion: 1, locale: value.locale };
89
+ if (typeof value !== "object" || value === null || Array.isArray(value))
90
+ throw new Error("App preferences file does not match a supported schema");
91
+ if ("schemaVersion" in value &&
92
+ value.schemaVersion === 1 &&
93
+ "locale" in value &&
94
+ isAppLocale(value.locale) &&
95
+ Object.keys(value).every((key) => key === "schemaVersion" || key === "locale"))
96
+ return { schemaVersion: 1, locale: value.locale };
97
+ if ("schemaVersion" in value &&
98
+ value.schemaVersion === 2 &&
99
+ "locale" in value &&
100
+ isAppLocale(value.locale) &&
101
+ "reading" in value &&
102
+ isAppReadingPreferences(value.reading) &&
103
+ Object.keys(value).every((key) => key === "schemaVersion" || key === "locale" || key === "reading"))
104
+ return {
105
+ schemaVersion: 2,
106
+ locale: value.locale,
107
+ reading: structuredClone(value.reading),
108
+ };
109
+ throw new Error("App preferences file does not match a supported schema");
86
110
  }
87
111
  function toView(document) {
88
- return { locale: document.locale };
112
+ return {
113
+ locale: document.locale,
114
+ reading: document.schemaVersion === 1
115
+ ? structuredClone(defaultAppReadingPreferences)
116
+ : structuredClone(document.reading),
117
+ };
89
118
  }
90
119
  function isMissingFile(error) {
91
120
  return error.code === "ENOENT";
@@ -0,0 +1,13 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Stable identity for one complete portable content tree revision. */
3
+ export function contentTreeFingerprint(files) {
4
+ return createHash("sha256")
5
+ .update(JSON.stringify([...files]
6
+ .map(({ path, contents, encoding }) => ({
7
+ path,
8
+ contents,
9
+ ...(encoding === undefined ? {} : { encoding }),
10
+ }))
11
+ .sort((left, right) => left.path === right.path ? 0 : left.path < right.path ? -1 : 1)))
12
+ .digest("hex");
13
+ }
@@ -3,7 +3,7 @@ import { inflateRawSync } from "node:zlib";
3
3
  import { portableContentTreePathKey } from "../../protocol/contentTree.js";
4
4
  import { defaultAppLocale, } from "../../protocol/appPreferences.js";
5
5
  import { defaultContentWorkspaceLimits, maximumPortableContentArchiveBytes, } from "./ContentLimits.js";
6
- import { CurrentTreeContentLibrary, InvalidContentTreeError, } from "./CurrentTreeContentLibrary.js";
6
+ import { CurrentTreeContentLibrary, InvalidContentTreeError, normalizeCurrentContentTreeFiles, } from "./CurrentTreeContentLibrary.js";
7
7
  import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, worldDocumentTextRequirement, } from "./FileNativeContentTree.js";
8
8
  export { CurrentTreeContentPackageBusyError, CurrentTreeContentLibraryError, CurrentTreeContentPackageNotFoundError, InvalidContentTreeError, } from "./CurrentTreeContentLibrary.js";
9
9
  const portableTreeUtf8Decoder = new TextDecoder("utf-8", {
@@ -36,11 +36,21 @@ export class ContentWorkspace {
36
36
  });
37
37
  }
38
38
  createCurrentTreeContentPackage() {
39
- return this.#currentTreeLibrary.createPackage(minimalFileNativeContentScaffold(this.#locale()));
39
+ const locale = this.#locale();
40
+ return this.#currentTreeLibrary.createPackage({
41
+ title: defaultContentPackageTitle(locale, "new"),
42
+ files: minimalFileNativeContentScaffold(locale),
43
+ });
40
44
  }
41
45
  listCurrentTreeContentPackages() {
42
46
  return this.#currentTreeLibrary.listPackages();
43
47
  }
48
+ readPackageScriptGrants(localId) {
49
+ return this.#currentTreeLibrary.readScriptGrants(localId);
50
+ }
51
+ packageScriptPermissions(localId, enabled) {
52
+ return this.#currentTreeLibrary.scriptPermissions(localId, enabled);
53
+ }
44
54
  readCurrentTreeContentPackage(localId) {
45
55
  return this.#currentTreeLibrary.readPackage(localId);
46
56
  }
@@ -50,6 +60,14 @@ export class ContentWorkspace {
50
60
  inspectCurrentTreeContentPackage(files) {
51
61
  return inspectContentPackageCurrentTree(files);
52
62
  }
63
+ /**
64
+ * Apply the exact storage boundary used by current-tree publication without
65
+ * mutating Authority. Model-directed transactions use this after every write
66
+ * so one over-limit call cannot invalidate accepted sibling calls.
67
+ */
68
+ validateCurrentTreeContentPackage(files) {
69
+ normalizeCurrentContentTreeFiles(files, this.#limits);
70
+ }
53
71
  replaceCurrentTreeContentPackage(localId, files, options = {}) {
54
72
  return this.#currentTreeLibrary.replaceCurrentTree(localId, files, options);
55
73
  }
@@ -58,7 +76,11 @@ export class ContentWorkspace {
58
76
  const files = readPortableContentArchive(archive, this.#limits, options.signal);
59
77
  options.signal?.throwIfAborted();
60
78
  const currentTree = files.map(toCurrentTreeFile);
61
- return this.#currentTreeLibrary.importPackage(currentTree, options.signal === undefined ? {} : { signal: options.signal });
79
+ return this.#currentTreeLibrary.importPackage({
80
+ title: options.title ??
81
+ defaultContentPackageTitle(this.#locale(), "imported"),
82
+ files: currentTree,
83
+ }, options.signal === undefined ? {} : { signal: options.signal });
62
84
  });
63
85
  }
64
86
  copyCurrentTreeContentPackage(localId, options = {}) {
@@ -67,8 +89,8 @@ export class ContentWorkspace {
67
89
  deleteCurrentTreeContentPackage(localId, options = {}) {
68
90
  return this.#mutate(() => this.#currentTreeLibrary.deletePackage(localId, options));
69
91
  }
70
- renameCurrentTreeContentPackage(localId, name, options = {}) {
71
- return this.#mutate(() => this.#currentTreeLibrary.renamePackage(localId, name, options));
92
+ renameCurrentTreeContentPackage(localId, title, options = {}) {
93
+ return this.#mutate(() => this.#currentTreeLibrary.renamePackage(localId, title, options));
72
94
  }
73
95
  exportCurrentTreeContentPackage(localId, options = {}) {
74
96
  return this.#mutate(async () => {
@@ -83,7 +105,7 @@ export class ContentWorkspace {
83
105
  await crashProcessAtPortableContentExportEdge("after_archive_created");
84
106
  options.signal?.throwIfAborted();
85
107
  return {
86
- fileName: `${portableArchiveBaseName(package_.displayName)}.zip`,
108
+ fileName: `${portableArchiveBaseName(package_.title)}.zip`,
87
109
  archive,
88
110
  };
89
111
  });
@@ -103,6 +125,13 @@ export class ContentWorkspace {
103
125
  }
104
126
  }
105
127
  }
128
+ function defaultContentPackageTitle(locale, source) {
129
+ if (locale === "zh-CN")
130
+ return source === "new" ? "待创作内容包" : "导入的内容包";
131
+ return source === "new"
132
+ ? "Untitled content package"
133
+ : "Imported content package";
134
+ }
106
135
  /**
107
136
  * A new package is immediately valid and editable. These files are a protocol
108
137
  * scaffold, not a sample story: an authoring model replaces the placeholders
@@ -122,7 +151,7 @@ export function minimalFileNativeContentScaffold(locale = defaultAppLocale) {
122
151
  contents: `$document:
123
152
  id: situation.current
124
153
  ref: current-situation
125
- title: 待创作世界
154
+ title: 当前情境
126
155
  summary: 当前正在发生且接下来不能忘记的短期局面。
127
156
  aliases: []
128
157
  地点: 未设定
@@ -137,7 +166,7 @@ export function minimalFileNativeContentScaffold(locale = defaultAppLocale) {
137
166
  },
138
167
  {
139
168
  path: "control/blocks/world.md",
140
- contents: `<!-- 本文件只写这个世界特有的规则。通用裁决与状态维护判据由主持预设提供,每条游玩调用链都会加载,不要在这里重复。 -->
169
+ contents: `<!-- 本文件只写这个世界特有的规则。按当前预设实际启用的作者块核对通用裁决与状态维护判据,不要在这里重复。 -->
141
170
 
142
171
  # 本世界的题材与专属规则
143
172
 
@@ -171,7 +200,7 @@ export function minimalFileNativeContentScaffold(locale = defaultAppLocale) {
171
200
  contents: `$document:
172
201
  id: situation.current
173
202
  ref: current-situation
174
- title: Unwritten world
203
+ title: Current situation
175
204
  summary: The short-term situation happening now that must not be forgotten next.
176
205
  aliases: []
177
206
  location: Not set
@@ -186,7 +215,7 @@ short_term_continuity: []
186
215
  },
187
216
  {
188
217
  path: "control/blocks/world.md",
189
- contents: `<!-- Keep only rules unique to this world in this file. The host preset supplies general adjudication and state-maintenance criteria to every play call chain; do not repeat them here. -->
218
+ contents: `<!-- Keep only rules unique to this world in this file. Check the current preset’s actually enabled author blocks for general adjudication and state-maintenance criteria; do not repeat them here. -->
190
219
 
191
220
  # This world's genre and special rules
192
221
 
@@ -211,7 +240,7 @@ short_term_continuity: []
211
240
  }
212
241
  const defaultWorldFrame = `format: narraeon.world-frame/v1
213
242
  bindings:
214
- currentSituation: situation.current
243
+ currentSituation: "@current-situation"
215
244
  instructions:
216
245
  - markdown: blocks/world.md
217
246
  context:
@@ -219,6 +248,7 @@ context:
219
248
  - slot: { kind: catalog, directory: locations, maxEntries: 24, required: false }
220
249
  - slot: { kind: catalog, directory: items, maxEntries: 24, required: false }
221
250
  - slot: { kind: catalog, directory: rules, maxEntries: 24, required: false }
251
+ - slot: { kind: catalog, directory: events, maxEntries: 24, required: false }
222
252
  - slot: { kind: current_situation }
223
253
  - slot: { kind: history, recent: 2 }
224
254
  - slot: { kind: additional_materials }
@@ -295,8 +325,8 @@ function createPortableContentArchive(files) {
295
325
  end.writeUInt32LE(localOffset, 16);
296
326
  return Buffer.concat([...localRecords, centralDirectory, end]);
297
327
  }
298
- function portableArchiveBaseName(displayName) {
299
- const normalized = displayName
328
+ function portableArchiveBaseName(title) {
329
+ const normalized = title
300
330
  .normalize("NFKC")
301
331
  .replaceAll(/[^\p{L}\p{N}._-]+/gu, "-")
302
332
  .replaceAll(/^[.-]+|[.-]+$/gu, "")