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.
- package/dist/node/protocol/appPreferences.js +25 -0
- package/dist/node/protocol/conversationObservation.js +67 -0
- package/dist/node/protocol/v1.js +186 -34
- package/dist/node/protocol/worldExtensions.js +16 -0
- package/dist/node/protocol/worldMaintenance.js +77 -0
- package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
- package/dist/node/runtime/ConversationChanges.js +24 -0
- package/dist/node/runtime/V1Runtime.js +445 -121
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
- package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
- package/dist/node/runtime/content/ContentWorkspace.js +43 -13
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
- package/dist/node/runtime/content/PackageFollowups.js +80 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
- package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
- package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
- package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
- package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
- package/dist/node/runtime/model/ModelHost.js +31 -1
- package/dist/node/runtime/model/ModelHostStream.js +51 -5
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
- package/dist/node/runtime/play/OrderedFollowups.js +71 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
- package/dist/node/runtime/play/PlayCallChain.js +814 -61
- package/dist/node/runtime/play/PlayContinuity.js +88 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
- package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
- package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
- package/dist/node/runtime/play/PlayPromptRun.js +86 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
- package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
- package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
- package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
- package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
- package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
- package/dist/node/server/ConversationStream.js +109 -0
- package/dist/node/server/createServer.js +5 -1
- package/dist/node/shared/artifact-payload-contract.js +123 -0
- package/dist/node/shared/default-play-prompts.js +25 -45
- package/dist/node/shared/default-preset-host.js +114 -138
- package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
- package/dist/node/shared/display-regex.js +114 -0
- package/dist/node/shared/first-party-action-choices.js +1 -6
- package/dist/node/shared/first-party-player-view.js +1 -6
- package/dist/node/shared/ordered-author-prompts.js +64 -0
- package/dist/node/shared/ordered-followups.js +42 -0
- package/dist/node/shared/ordered-play-prompts.js +73 -0
- package/dist/web/assets/index-DG85hpWC.css +1 -0
- package/dist/web/assets/index-DfxvCjb6.js +1207 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
- package/dist/web/assets/index-D8YKfmNG.js +0 -1192
- 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(
|
|
18
|
+
save(update) {
|
|
19
19
|
return this.#mutate(async () => {
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
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 {
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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,
|
|
71
|
-
return this.#mutate(() => this.#currentTreeLibrary.renamePackage(localId,
|
|
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_.
|
|
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:
|
|
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.
|
|
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
|
|
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(
|
|
299
|
-
const normalized =
|
|
328
|
+
function portableArchiveBaseName(title) {
|
|
329
|
+
const normalized = title
|
|
300
330
|
.normalize("NFKC")
|
|
301
331
|
.replaceAll(/[^\p{L}\p{N}._-]+/gu, "-")
|
|
302
332
|
.replaceAll(/^[.-]+|[.-]+$/gu, "")
|