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
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
|
|
2
|
+
import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
|
|
3
|
+
import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
|
|
4
|
+
import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
|
|
1
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
6
|
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
7
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -29,10 +33,13 @@ export class FileNativeWorldNotFoundError extends Error {
|
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
export class FileNativeWorldStore {
|
|
36
|
+
extensionRequests;
|
|
37
|
+
#documentMaintenanceCache = new Map();
|
|
32
38
|
#worldsRoot;
|
|
33
39
|
#operationsRoot;
|
|
34
40
|
#promptCompiler;
|
|
35
41
|
#activeControlUsers = new Map();
|
|
42
|
+
#openingWorldRevisions = new Set();
|
|
36
43
|
operations;
|
|
37
44
|
playTimeline;
|
|
38
45
|
playAdvances;
|
|
@@ -42,6 +49,7 @@ export class FileNativeWorldStore {
|
|
|
42
49
|
constructor(dataRoot, options = {}) {
|
|
43
50
|
const root = resolve(dataRoot);
|
|
44
51
|
this.#worldsRoot = join(root, "worlds-file-native");
|
|
52
|
+
this.extensionRequests = new WorldExtensionRequests(this.#worldsRoot);
|
|
45
53
|
this.#operationsRoot = join(root, "operations");
|
|
46
54
|
this.#promptCompiler =
|
|
47
55
|
options.promptCompiler ?? new FileNativePromptCompiler();
|
|
@@ -154,15 +162,56 @@ export class FileNativeWorldStore {
|
|
|
154
162
|
throw new FileNativeWorldNotFoundError({ cause: error });
|
|
155
163
|
throw error;
|
|
156
164
|
}
|
|
157
|
-
return this.operations.withExclusiveWorldStateMutation(worldId, async () => {
|
|
165
|
+
return this.operations.withExclusiveWorldStateMutation(worldId, () => this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
158
166
|
await rm(root, { recursive: true, force: true });
|
|
159
167
|
this.#currentStorage.delete(worldId);
|
|
160
168
|
return { deleted: true };
|
|
161
|
-
});
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
async extensionControls(worldId, binding, locale, change) {
|
|
172
|
+
assertIdentity(worldId, "World ID");
|
|
173
|
+
const head = await this.currentHead(worldId);
|
|
174
|
+
const [control, playerViews] = await Promise.all([
|
|
175
|
+
this.readSurface(worldId, "control"),
|
|
176
|
+
this.renderPlayerViewsAtHead(worldId, head),
|
|
177
|
+
]);
|
|
178
|
+
const definitions = extensionDefinitions(binding, control.map((file) => ({ ...file, path: `control/${file.path}` })), playerViews.views, locale);
|
|
179
|
+
return this.operations.withWorldAuthorityLock(worldId, () => FileNativeArtifactStore.withWorldControlsMutation(dirname(this.#worldsRoot), worldId, async () => {
|
|
180
|
+
// Definitions were loaded before taking the lock; deletion may have
|
|
181
|
+
// completed while this reader was waiting. Never recreate its shell.
|
|
182
|
+
await readPublicationAt(join(this.#worldsRoot, worldId));
|
|
183
|
+
const view = await WorldExtensionControls.resolve(join(this.#worldsRoot, worldId), definitions, binding.id, change);
|
|
184
|
+
await this.extensionRequests.changed(worldId);
|
|
185
|
+
return view;
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
async extensionControlsRevision(worldId) {
|
|
189
|
+
assertIdentity(worldId, "World ID");
|
|
190
|
+
return (await WorldExtensionControls.read(join(this.#worldsRoot, worldId)))
|
|
191
|
+
.revision;
|
|
192
|
+
}
|
|
193
|
+
async readPackageScriptGrants(worldId) {
|
|
194
|
+
assertIdentity(worldId, "World ID");
|
|
195
|
+
await this.currentHead(worldId);
|
|
196
|
+
return PackageScriptPermissions.read(join(this.#worldsRoot, worldId));
|
|
197
|
+
}
|
|
198
|
+
async packageScriptPermissions(worldId, enabled) {
|
|
199
|
+
assertIdentity(worldId, "World ID");
|
|
200
|
+
const files = (await this.readSurface(worldId, "control")).map((file) => ({
|
|
201
|
+
...file,
|
|
202
|
+
path: `control/${file.path}`,
|
|
203
|
+
}));
|
|
204
|
+
const root = join(this.#worldsRoot, worldId);
|
|
205
|
+
if (enabled !== undefined)
|
|
206
|
+
await PackageScriptPermissions.write(root, enabled ? PackageScriptPermissions.currentGrants(files) : []);
|
|
207
|
+
return PackageScriptPermissions.status(files, await PackageScriptPermissions.read(root));
|
|
162
208
|
}
|
|
163
209
|
async createFromContentPackage(input) {
|
|
164
210
|
assertIdentity(input.operationId, "operation ID");
|
|
165
211
|
assertIdentity(input.sourcePackageId, "Content-package local identity");
|
|
212
|
+
if (input.sourcePackageTitle !== input.sourcePackageTitle.trim() ||
|
|
213
|
+
!validWorldName(input.sourcePackageTitle))
|
|
214
|
+
throw new FileNativeWorldCreationError("content_package_title_invalid", "The source content package has no valid standalone title");
|
|
166
215
|
const inspection = inspectContentPackageCurrentTree(input.packageFiles);
|
|
167
216
|
if (inspection.status !== "usable") {
|
|
168
217
|
throw new FileNativeWorldCreationError("content_package_needs_repair", `Only a content package that passes all validation can create a world: ${inspection.issues
|
|
@@ -191,10 +240,12 @@ export class FileNativeWorldStore {
|
|
|
191
240
|
...input,
|
|
192
241
|
packageFiles,
|
|
193
242
|
};
|
|
194
|
-
const
|
|
243
|
+
const legacySourceFingerprint = fingerprint(packageFiles);
|
|
244
|
+
const sourceFingerprint = contentPackageCreationFingerprint(input.sourcePackageTitle, packageFiles);
|
|
195
245
|
const previous = await this.#readPublication(input.operationId);
|
|
196
246
|
if (previous !== null) {
|
|
197
|
-
if (previous.sourceFingerprint !== sourceFingerprint
|
|
247
|
+
if (previous.sourceFingerprint !== sourceFingerprint &&
|
|
248
|
+
previous.sourceFingerprint !== legacySourceFingerprint) {
|
|
198
249
|
throw new FileNativeWorldCreationError("operation_conflict", "The same operation ID is bound to a different creation payload");
|
|
199
250
|
}
|
|
200
251
|
const preview = this.#preview(boundInput, previous.worldId);
|
|
@@ -221,6 +272,7 @@ export class FileNativeWorldStore {
|
|
|
221
272
|
];
|
|
222
273
|
await writeSurface(stagingRoot, "state", state);
|
|
223
274
|
await writeSurface(stagingRoot, "control", control);
|
|
275
|
+
await PackageScriptPermissions.write(stagingRoot, (input.packageScriptGrants ?? []).filter((grant) => PackageScriptPermissions.currentGrants(packageFiles).includes(grant)));
|
|
224
276
|
await mkdir(join(stagingRoot, "history"), { recursive: true });
|
|
225
277
|
await mkdir(join(stagingRoot, "runtime"), { recursive: true });
|
|
226
278
|
await writeIdempotentText(join(stagingRoot, "history", historySurfaceFiles([openingMessage])[0].path), openingMessage.exactText);
|
|
@@ -257,7 +309,7 @@ export class FileNativeWorldStore {
|
|
|
257
309
|
const publication = {
|
|
258
310
|
schemaVersion: 1,
|
|
259
311
|
worldId,
|
|
260
|
-
title:
|
|
312
|
+
title: input.sourcePackageTitle,
|
|
261
313
|
parentEndpoint: "genesis",
|
|
262
314
|
operationId: input.operationId,
|
|
263
315
|
sourceFingerprint,
|
|
@@ -275,7 +327,8 @@ export class FileNativeWorldStore {
|
|
|
275
327
|
await rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
276
328
|
const recovered = await this.#readPublication(input.operationId);
|
|
277
329
|
if (recovered !== null) {
|
|
278
|
-
if (recovered.sourceFingerprint === sourceFingerprint
|
|
330
|
+
if (recovered.sourceFingerprint === sourceFingerprint ||
|
|
331
|
+
recovered.sourceFingerprint === legacySourceFingerprint) {
|
|
279
332
|
return {
|
|
280
333
|
outcome: "created",
|
|
281
334
|
world: await this.#currentSummary(recovered),
|
|
@@ -348,18 +401,117 @@ export class FileNativeWorldStore {
|
|
|
348
401
|
}
|
|
349
402
|
async saveControlDraft(worldId, files) {
|
|
350
403
|
assertIdentity(worldId, "World ID");
|
|
351
|
-
await this.ensureCurrentStorage(worldId);
|
|
352
|
-
const root = join(this.#worldsRoot, worldId);
|
|
353
|
-
await readPublicationAt(root);
|
|
354
404
|
for (const file of files)
|
|
355
405
|
assertRelativePath(file.path);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
try {
|
|
407
|
+
return await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
|
|
408
|
+
await this.ensureCurrentStorage(worldId);
|
|
409
|
+
const root = join(this.#worldsRoot, worldId);
|
|
410
|
+
await readPublicationAt(root);
|
|
411
|
+
const staging = join(root, `runtime/.control-draft-${randomUUID()}`);
|
|
412
|
+
await writeSurface(staging, "control", files);
|
|
413
|
+
const final = join(root, "runtime/control-draft");
|
|
414
|
+
await rm(final, { recursive: true, force: true });
|
|
415
|
+
await rename(join(staging, "control"), final);
|
|
416
|
+
await rm(staging, { recursive: true, force: true });
|
|
417
|
+
return { fingerprint: fingerprint(files) };
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
if (error instanceof WorldOperationBusyError)
|
|
422
|
+
throw new FileNativeWorldCreationError("operation_conflict", "World control drafts are locked while a world revision is open");
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async acquireWorldRevisionLock(worldId, epochId) {
|
|
427
|
+
if ((this.#activeControlUsers.get(worldId)?.size ?? 0) > 0 ||
|
|
428
|
+
this.#openingWorldRevisions.has(worldId))
|
|
429
|
+
return { kind: "busy" };
|
|
430
|
+
this.#openingWorldRevisions.add(worldId);
|
|
431
|
+
try {
|
|
432
|
+
return await this.operations.acquireWorldRevision(worldId, epochId);
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
this.#openingWorldRevisions.delete(worldId);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
readWorldRevisionLock(worldId) {
|
|
439
|
+
return this.operations.readWorldRevisionLock(worldId);
|
|
440
|
+
}
|
|
441
|
+
releaseWorldRevisionLock(handle) {
|
|
442
|
+
return this.operations.releaseWorldRevision(handle);
|
|
443
|
+
}
|
|
444
|
+
async assertWorldRevisionUnlocked(worldId) {
|
|
445
|
+
try {
|
|
446
|
+
await this.operations.assertWorldRevisionUnlocked(worldId);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (error instanceof WorldOperationBusyError)
|
|
450
|
+
throw new FileNativeWorldCreationError("operation_conflict", "World play and other changes are locked while a world revision is open");
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async bindWorldRevision(handle) {
|
|
455
|
+
return this.operations.withWorldRevisionLock(handle, () => this.bindPlayCallChain(handle.worldId));
|
|
456
|
+
}
|
|
457
|
+
/** Replace control while the matching revision epoch retains exclusive ownership. */
|
|
458
|
+
async replaceWorldRevisionControl(input) {
|
|
459
|
+
for (const file of input.files)
|
|
460
|
+
assertRelativePath(file.path);
|
|
461
|
+
return this.operations.withWorldRevisionLock(input.handle, async () => {
|
|
462
|
+
if ((this.#activeControlUsers.get(input.handle.worldId)?.size ?? 0) > 0)
|
|
463
|
+
throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by another operation");
|
|
464
|
+
const root = join(this.#worldsRoot, input.handle.worldId);
|
|
465
|
+
const currentPath = join(root, "control");
|
|
466
|
+
const runtimeRoot = join(root, "runtime");
|
|
467
|
+
const stagingRoot = join(runtimeRoot, `.revision-control-${input.handle.epochId}-next`);
|
|
468
|
+
const stagedPath = join(stagingRoot, "control");
|
|
469
|
+
const previousPath = join(runtimeRoot, `.revision-control-${input.handle.epochId}-previous`);
|
|
470
|
+
let current = await readTree(currentPath);
|
|
471
|
+
let currentFingerprint = fingerprint(current);
|
|
472
|
+
const nextFingerprint = fingerprint(input.files);
|
|
473
|
+
if (currentFingerprint === nextFingerprint) {
|
|
474
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
475
|
+
await rm(previousPath, { recursive: true, force: true });
|
|
476
|
+
await syncDirectory(runtimeRoot);
|
|
477
|
+
return { fingerprint: nextFingerprint };
|
|
478
|
+
}
|
|
479
|
+
const previous = await readTree(previousPath);
|
|
480
|
+
const staged = await readTree(stagedPath);
|
|
481
|
+
if (current.length === 0 &&
|
|
482
|
+
fingerprint(previous) === input.expectedFingerprint) {
|
|
483
|
+
if (fingerprint(staged) === nextFingerprint) {
|
|
484
|
+
await rename(stagedPath, currentPath);
|
|
485
|
+
await syncDirectory(root);
|
|
486
|
+
await syncDirectory(stagingRoot);
|
|
487
|
+
await rm(previousPath, { recursive: true, force: true });
|
|
488
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
489
|
+
await syncDirectory(runtimeRoot);
|
|
490
|
+
return { fingerprint: nextFingerprint };
|
|
491
|
+
}
|
|
492
|
+
await rename(previousPath, currentPath);
|
|
493
|
+
await syncDirectory(root);
|
|
494
|
+
await syncDirectory(runtimeRoot);
|
|
495
|
+
current = await readTree(currentPath);
|
|
496
|
+
currentFingerprint = fingerprint(current);
|
|
497
|
+
}
|
|
498
|
+
if (currentFingerprint !== input.expectedFingerprint)
|
|
499
|
+
throw new FileNativeWorldCreationError("operation_conflict", "World control changed after this revision epoch opened");
|
|
500
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
501
|
+
await rm(previousPath, { recursive: true, force: true });
|
|
502
|
+
await writeSurface(stagingRoot, "control", input.files);
|
|
503
|
+
await durableTree(stagingRoot);
|
|
504
|
+
await rename(currentPath, previousPath);
|
|
505
|
+
await syncDirectory(root);
|
|
506
|
+
await syncDirectory(runtimeRoot);
|
|
507
|
+
await rename(stagedPath, currentPath);
|
|
508
|
+
await syncDirectory(root);
|
|
509
|
+
await syncDirectory(stagingRoot);
|
|
510
|
+
await rm(previousPath, { recursive: true, force: true });
|
|
511
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
512
|
+
await syncDirectory(runtimeRoot);
|
|
513
|
+
return { fingerprint: nextFingerprint };
|
|
514
|
+
});
|
|
363
515
|
}
|
|
364
516
|
async previewControlDraft(worldId, prompt) {
|
|
365
517
|
const { state, draft } = await this.#readControlDraft(worldId);
|
|
@@ -429,6 +581,8 @@ export class FileNativeWorldStore {
|
|
|
429
581
|
freezeControl(worldId, operationId) {
|
|
430
582
|
assertIdentity(worldId, "World ID");
|
|
431
583
|
assertIdentity(operationId, "operation ID");
|
|
584
|
+
if (this.#openingWorldRevisions.has(worldId))
|
|
585
|
+
throw new FileNativeWorldCreationError("operation_conflict", "World control is locked while a world revision opens");
|
|
432
586
|
const users = this.#activeControlUsers.get(worldId) ?? new Set();
|
|
433
587
|
users.add(operationId);
|
|
434
588
|
this.#activeControlUsers.set(worldId, users);
|
|
@@ -518,6 +672,7 @@ export class FileNativeWorldStore {
|
|
|
518
672
|
}, {}),
|
|
519
673
|
additionalMaterials: materials?.items ?? [],
|
|
520
674
|
history: materializedHistoryRecord(history),
|
|
675
|
+
narrativeCheckpoint: await authorityStore.readNarrativeCheckpoint(),
|
|
521
676
|
};
|
|
522
677
|
});
|
|
523
678
|
}
|
|
@@ -527,6 +682,32 @@ export class FileNativeWorldStore {
|
|
|
527
682
|
throw error;
|
|
528
683
|
}
|
|
529
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Bind an immutable historical endpoint with the world's current control
|
|
687
|
+
* surface. This is the same semantic input as a fresh context started while
|
|
688
|
+
* that endpoint was current, without rematerializing it over today's head.
|
|
689
|
+
*/
|
|
690
|
+
async bindPlayCallChainAt(worldId, head) {
|
|
691
|
+
assertIdentity(worldId, "World ID");
|
|
692
|
+
const [endpoint, control] = await Promise.all([
|
|
693
|
+
this.recoverEndpoint(worldId, head),
|
|
694
|
+
this.readSurface(worldId, "control"),
|
|
695
|
+
]);
|
|
696
|
+
return {
|
|
697
|
+
worldId,
|
|
698
|
+
parentHead: endpoint.head,
|
|
699
|
+
files: [...endpoint.state, ...control].reduce((files, { path, contents }, index) => {
|
|
700
|
+
files[`${index < endpoint.state.length ? "state" : "control"}/${path}`] = contents;
|
|
701
|
+
return files;
|
|
702
|
+
}, {}),
|
|
703
|
+
additionalMaterials: structuredClone(endpoint.additionalMaterials),
|
|
704
|
+
narrativeCheckpoint: endpoint.narrativeCheckpoint,
|
|
705
|
+
history: Object.fromEntries(endpoint.history.map(({ messageId, exactText }) => [
|
|
706
|
+
messageId,
|
|
707
|
+
exactText,
|
|
708
|
+
])),
|
|
709
|
+
};
|
|
710
|
+
}
|
|
530
711
|
async reserveOperation(operationId) {
|
|
531
712
|
assertIdentity(operationId, "operation ID");
|
|
532
713
|
await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
|
|
@@ -569,6 +750,7 @@ export class FileNativeWorldStore {
|
|
|
569
750
|
state: recovered.state,
|
|
570
751
|
history: recovered.history,
|
|
571
752
|
additionalMaterials: recovered.additionalMaterials,
|
|
753
|
+
narrativeCheckpoint: recovered.narrativeCheckpoint,
|
|
572
754
|
};
|
|
573
755
|
}
|
|
574
756
|
catch (error) {
|
|
@@ -654,6 +836,8 @@ export class FileNativeWorldStore {
|
|
|
654
836
|
const selected = await new FileNativeAuthorityV3(staging).recoverHeadResult();
|
|
655
837
|
await writeSurface(staging, "state", selected.state);
|
|
656
838
|
await writeSurface(staging, "control", control);
|
|
839
|
+
await PackageScriptPermissions.write(staging, await PackageScriptPermissions.read(sourceRoot));
|
|
840
|
+
await WorldExtensionControls.copy(sourceRoot, staging);
|
|
657
841
|
await mkdir(join(staging, "history"), { recursive: true });
|
|
658
842
|
for (const file of historySurfaceFiles(selected.history))
|
|
659
843
|
await writeIdempotentText(join(staging, "history", file.path), file.contents);
|
|
@@ -809,6 +993,16 @@ export class FileNativeWorldStore {
|
|
|
809
993
|
if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
|
|
810
994
|
throw new TypeError("Timeline-revision request fingerprint is invalid");
|
|
811
995
|
await this.ensureCurrentStorage(input.worldId);
|
|
996
|
+
try {
|
|
997
|
+
return await this.operations.withExclusiveWorldStateMutation(input.worldId, () => this.#reviseTimelineUnlocked(input));
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
if (error instanceof WorldOperationBusyError)
|
|
1001
|
+
throw new FileNativeWorldCreationError("operation_conflict", "Timeline editing is locked while a world revision is open");
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async #reviseTimelineUnlocked(input) {
|
|
812
1006
|
const existing = await this.getOperationOutcome(input.operationId);
|
|
813
1007
|
if (isCommittedOutcome(existing)) {
|
|
814
1008
|
const fact = await new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId)).authorityFactAt(existing.head);
|
|
@@ -864,6 +1058,9 @@ export class FileNativeWorldStore {
|
|
|
864
1058
|
operationReserved = await this.reserveOperation(input.operationId);
|
|
865
1059
|
}
|
|
866
1060
|
return this.#commitHistoryChange({
|
|
1061
|
+
...(input.worldClock === undefined
|
|
1062
|
+
? {}
|
|
1063
|
+
: { worldClock: input.worldClock }),
|
|
867
1064
|
operationId: input.operationId,
|
|
868
1065
|
worldId: input.worldId,
|
|
869
1066
|
parentHead: input.parentHead,
|
|
@@ -872,11 +1069,19 @@ export class FileNativeWorldStore {
|
|
|
872
1069
|
mode: "correction",
|
|
873
1070
|
historyAppend: [],
|
|
874
1071
|
operationReserved,
|
|
1072
|
+
...(input.validationControl === undefined
|
|
1073
|
+
? {}
|
|
1074
|
+
: { validationControl: input.validationControl }),
|
|
875
1075
|
});
|
|
876
1076
|
};
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1077
|
+
if (input.stateOperationClaim !== undefined &&
|
|
1078
|
+
input.worldRevisionLock !== undefined)
|
|
1079
|
+
throw new TypeError("A correction cannot use two world-operation claims");
|
|
1080
|
+
if (input.worldRevisionLock !== undefined)
|
|
1081
|
+
return await this.operations.withWorldRevisionLock(input.worldRevisionLock, commit);
|
|
1082
|
+
if (input.stateOperationClaim !== undefined)
|
|
1083
|
+
return await this.operations.withCorrectionWorldClaim(input.stateOperationClaim, commit);
|
|
1084
|
+
return await this.operations.withExclusiveWorldStateMutation(input.worldId, commit);
|
|
880
1085
|
}
|
|
881
1086
|
catch (error) {
|
|
882
1087
|
if (error instanceof WorldOperationBusyError)
|
|
@@ -893,11 +1098,18 @@ export class FileNativeWorldStore {
|
|
|
893
1098
|
await this.ensureCurrentStorage(input.worldId);
|
|
894
1099
|
if (input.historyAppend.length === 0 && input.stateChanges.length === 0)
|
|
895
1100
|
throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1101
|
+
try {
|
|
1102
|
+
return await this.operations.withExclusiveWorldStateMutation(input.worldId, () => this.#commitHistoryChange({
|
|
1103
|
+
...input,
|
|
1104
|
+
nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
|
|
1105
|
+
mode: "play",
|
|
1106
|
+
}));
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
if (error instanceof WorldOperationBusyError)
|
|
1110
|
+
throw new FileNativeWorldCreationError("operation_conflict", "Play is locked while a world revision is open");
|
|
1111
|
+
throw error;
|
|
1112
|
+
}
|
|
901
1113
|
}
|
|
902
1114
|
async #commitHistoryChange(input) {
|
|
903
1115
|
const existing = await this.getOperationOutcome(input.operationId);
|
|
@@ -955,7 +1167,7 @@ export class FileNativeWorldStore {
|
|
|
955
1167
|
const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
|
|
956
1168
|
if ((materialized?.head ?? "genesis") !== authority.head)
|
|
957
1169
|
throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
|
|
958
|
-
const [basisState,
|
|
1170
|
+
const [basisState, storedControl] = await Promise.all([
|
|
959
1171
|
authorityStore.recoverState(input.timelineRevision?.restoresHead ?? authority.head),
|
|
960
1172
|
readTree(join(root, "control")),
|
|
961
1173
|
]);
|
|
@@ -983,7 +1195,7 @@ export class FileNativeWorldStore {
|
|
|
983
1195
|
path: `world/${path}`,
|
|
984
1196
|
contents,
|
|
985
1197
|
})),
|
|
986
|
-
...
|
|
1198
|
+
...(input.validationControl ?? storedControl).map(({ path, contents }) => ({
|
|
987
1199
|
path: `control/${path}`,
|
|
988
1200
|
contents,
|
|
989
1201
|
})),
|
|
@@ -994,6 +1206,12 @@ export class FileNativeWorldStore {
|
|
|
994
1206
|
await authorityStore.discardUnacceptedNextEpoch();
|
|
995
1207
|
crashAtFileNativeAuthorityEdge("before_commit_acceptance");
|
|
996
1208
|
const preparedAppend = await authorityStore.prepareAppend({
|
|
1209
|
+
...(input.playContext === undefined
|
|
1210
|
+
? {}
|
|
1211
|
+
: { playContext: input.playContext }),
|
|
1212
|
+
...(input.worldClock === undefined
|
|
1213
|
+
? {}
|
|
1214
|
+
: { worldClock: input.worldClock }),
|
|
997
1215
|
operationId: input.operationId,
|
|
998
1216
|
parentHead: input.parentHead,
|
|
999
1217
|
...(input.timelineRevision === undefined
|
|
@@ -1003,6 +1221,9 @@ export class FileNativeWorldStore {
|
|
|
1003
1221
|
historyAppend: input.historyAppend,
|
|
1004
1222
|
stateChanges: structuredClone(input.stateChanges),
|
|
1005
1223
|
nextMaterials: input.nextMaterials,
|
|
1224
|
+
...(input.narrativeCheckpoint === undefined
|
|
1225
|
+
? {}
|
|
1226
|
+
: { narrativeCheckpoint: input.narrativeCheckpoint }),
|
|
1006
1227
|
...(input.mode === "correction"
|
|
1007
1228
|
? {
|
|
1008
1229
|
correctionTargets: input.stateChanges.map(({ documentId }) => documentId),
|
|
@@ -1071,6 +1292,37 @@ export class FileNativeWorldStore {
|
|
|
1071
1292
|
const authority = await new FileNativeAuthorityV3(root).readHistory();
|
|
1072
1293
|
return structuredClone(authority);
|
|
1073
1294
|
}
|
|
1295
|
+
async inspectDocumentMaintenance(worldId, head) {
|
|
1296
|
+
try {
|
|
1297
|
+
return {
|
|
1298
|
+
documentMaintenance: await this.readDocumentMaintenance(worldId, head),
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
return {
|
|
1303
|
+
documentMaintenance: {},
|
|
1304
|
+
documentMaintenanceUnavailableReason: error instanceof Error
|
|
1305
|
+
? error.message
|
|
1306
|
+
: "Document diagnostics unavailable",
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async readDocumentMaintenance(worldId, head) {
|
|
1311
|
+
assertIdentity(worldId, "World ID");
|
|
1312
|
+
await this.ensureCurrentStorage(worldId);
|
|
1313
|
+
const root = join(this.#worldsRoot, worldId);
|
|
1314
|
+
await readPublicationAt(root);
|
|
1315
|
+
const authority = new FileNativeAuthorityV3(root);
|
|
1316
|
+
const selectedHead = head ?? (await authority.readHead()).head;
|
|
1317
|
+
let cache = this.#documentMaintenanceCache.get(worldId);
|
|
1318
|
+
if (cache === undefined) {
|
|
1319
|
+
cache = new Map();
|
|
1320
|
+
if (this.#documentMaintenanceCache.size >= 8)
|
|
1321
|
+
this.#documentMaintenanceCache.delete(this.#documentMaintenanceCache.keys().next().value);
|
|
1322
|
+
this.#documentMaintenanceCache.set(worldId, cache);
|
|
1323
|
+
}
|
|
1324
|
+
return readDocumentMaintenanceFacts(authority, selectedHead, cache);
|
|
1325
|
+
}
|
|
1074
1326
|
async readAuthorityEndpoint(worldId, endpoint) {
|
|
1075
1327
|
const recovered = await this.recoverEndpoint(worldId, endpoint);
|
|
1076
1328
|
return {
|
|
@@ -1206,7 +1458,6 @@ function assertEquivalentWorldDocumentSnapshots(contentDocuments, worldState) {
|
|
|
1206
1458
|
document: {
|
|
1207
1459
|
logicalPath: `state/${document.path.slice("world/".length)}`,
|
|
1208
1460
|
},
|
|
1209
|
-
maxBytes: 4,
|
|
1210
1461
|
});
|
|
1211
1462
|
return (!result.ok ||
|
|
1212
1463
|
result.kind !== "read_document" ||
|
|
@@ -1265,6 +1516,16 @@ function fingerprint(files) {
|
|
|
1265
1516
|
}
|
|
1266
1517
|
return `sha256:${hash.digest("hex")}`;
|
|
1267
1518
|
}
|
|
1519
|
+
function contentPackageCreationFingerprint(title, files) {
|
|
1520
|
+
const hash = createHash("sha256");
|
|
1521
|
+
hash
|
|
1522
|
+
.update("narraeon.file-native-world-creation/v2")
|
|
1523
|
+
.update("\0")
|
|
1524
|
+
.update(title)
|
|
1525
|
+
.update("\0")
|
|
1526
|
+
.update(fingerprint(files));
|
|
1527
|
+
return `sha256:${hash.digest("hex")}`;
|
|
1528
|
+
}
|
|
1268
1529
|
function operationDigest(operationId) {
|
|
1269
1530
|
return createHash("sha256").update(operationId).digest("hex");
|
|
1270
1531
|
}
|
|
@@ -1634,6 +1895,8 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
1634
1895
|
? input.stateChanges.map(({ documentId }) => documentId)
|
|
1635
1896
|
: undefined;
|
|
1636
1897
|
return (commit.mode === input.mode &&
|
|
1898
|
+
commit.playContext === input.playContext &&
|
|
1899
|
+
commit.worldClock === input.worldClock &&
|
|
1637
1900
|
commit.auditParent.head === input.parentHead &&
|
|
1638
1901
|
commit.timelineParent.head ===
|
|
1639
1902
|
(input.timelineRevision?.restoresHead ?? input.parentHead) &&
|
|
@@ -1641,6 +1904,7 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
1641
1904
|
stateChangesMatch &&
|
|
1642
1905
|
isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
|
|
1643
1906
|
isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
|
|
1907
|
+
isDeepStrictEqual(commit.narrativeCheckpoint, input.narrativeCheckpoint) &&
|
|
1644
1908
|
isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
|
|
1645
1909
|
commit.corrects ===
|
|
1646
1910
|
(input.mode === "correction" ? input.parentHead : undefined));
|
|
@@ -1857,3 +2121,4 @@ function crashAtFileNativeAuthorityEdge(edge) {
|
|
|
1857
2121
|
process.kill(process.pid, "SIGKILL");
|
|
1858
2122
|
}
|
|
1859
2123
|
}
|
|
2124
|
+
import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
|
|
2
|
+
/** An annotation about UI reads, never document content or write permission. */
|
|
3
|
+
export function renderPlayerViewBindings(snapshot, shortRef, locale = "en") {
|
|
4
|
+
const control = snapshot.files.find(({ path }) => path === "control/player-views.yaml")?.contents;
|
|
5
|
+
if (control === undefined)
|
|
6
|
+
return "";
|
|
7
|
+
const { bindings } = new PlayerViewRenderer().inspect({ snapshot, control });
|
|
8
|
+
const selected = bindings.filter((binding) => shortRef === undefined || binding.shortRef === shortRef);
|
|
9
|
+
if (selected.length === 0)
|
|
10
|
+
return "";
|
|
11
|
+
const zh = locale === "zh-CN";
|
|
12
|
+
return [
|
|
13
|
+
zh
|
|
14
|
+
? "## 玩家视图读取绑定(Runtime 标记,非正文)"
|
|
15
|
+
: "## Player-view read bindings (Runtime annotation, outside document bodies)",
|
|
16
|
+
zh
|
|
17
|
+
? "以下范围直接用于玩家界面;这里只标明读取绑定,不授予读取或写入权限,也不展开引用目标。"
|
|
18
|
+
: "These scopes feed the player interface. This annotation grants no read or write permission and does not expand reference targets.",
|
|
19
|
+
...selected.map(({ viewTitle, label, shortRef: ref, locator }) => {
|
|
20
|
+
const scope = locator === null
|
|
21
|
+
? zh
|
|
22
|
+
? "(整份文档)"
|
|
23
|
+
: " (whole document)"
|
|
24
|
+
: "yaml" in locator
|
|
25
|
+
? `#yaml:${JSON.stringify(locator.yaml)}`
|
|
26
|
+
: `#markdown:${JSON.stringify(locator.markdown)}`;
|
|
27
|
+
return `- ${viewTitle} · ${label} → @${ref}${scope}`;
|
|
28
|
+
}),
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
@@ -2,16 +2,22 @@ import { parseDocument } from "yaml";
|
|
|
2
2
|
const maxItemsPerView = 128;
|
|
3
3
|
const maxRenderedBytesPerView = 64 * 1024;
|
|
4
4
|
const maxRenderedDepth = 16;
|
|
5
|
-
const maxDocumentReadBytes = 65_536;
|
|
6
5
|
export class PlayerViewRenderer {
|
|
7
6
|
render(input) {
|
|
7
|
+
const { views, diagnostics } = this.inspect(input);
|
|
8
|
+
return { views, diagnostics };
|
|
9
|
+
}
|
|
10
|
+
/** Binding facts come from the exact successful UI selections. */
|
|
11
|
+
inspect(input) {
|
|
8
12
|
const diagnostics = [];
|
|
13
|
+
const bindings = [];
|
|
9
14
|
const control = parseControlYaml(input.control);
|
|
10
15
|
if (!isRecord(control) ||
|
|
11
16
|
control.format !== "narraeon.player-views/v1" ||
|
|
12
17
|
!Array.isArray(control.views)) {
|
|
13
18
|
return {
|
|
14
19
|
views: [],
|
|
20
|
+
bindings,
|
|
15
21
|
diagnostics: [
|
|
16
22
|
{
|
|
17
23
|
code: "invalid_control",
|
|
@@ -73,10 +79,18 @@ export class PlayerViewRenderer {
|
|
|
73
79
|
}
|
|
74
80
|
renderedBytes += itemBytes;
|
|
75
81
|
view.items.push(resolved.item);
|
|
82
|
+
bindings.push({
|
|
83
|
+
viewId: view.id,
|
|
84
|
+
viewTitle: view.title,
|
|
85
|
+
itemId: resolved.item.id,
|
|
86
|
+
label: resolved.item.label,
|
|
87
|
+
shortRef: resolved.shortRef,
|
|
88
|
+
locator: resolved.locator,
|
|
89
|
+
});
|
|
76
90
|
}
|
|
77
91
|
views.push(view);
|
|
78
92
|
}
|
|
79
|
-
return { views, diagnostics };
|
|
93
|
+
return { views, diagnostics, bindings };
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
96
|
function resolveItem(item, snapshot) {
|
|
@@ -99,6 +113,8 @@ function resolveItem(item, snapshot) {
|
|
|
99
113
|
return wholeDocument.ok
|
|
100
114
|
? {
|
|
101
115
|
ok: true,
|
|
116
|
+
shortRef: wholeDocument.shortRef,
|
|
117
|
+
locator: null,
|
|
102
118
|
item: {
|
|
103
119
|
id: itemId,
|
|
104
120
|
label: item.label,
|
|
@@ -131,6 +147,8 @@ function resolveItem(item, snapshot) {
|
|
|
131
147
|
};
|
|
132
148
|
return {
|
|
133
149
|
ok: true,
|
|
150
|
+
shortRef: selected.scope.document.shortRef,
|
|
151
|
+
locator: selected.node.locator,
|
|
134
152
|
item: {
|
|
135
153
|
id: itemId,
|
|
136
154
|
label: item.label,
|
|
@@ -152,7 +170,6 @@ function resolveWholeDocument(snapshot, selector, handle, expectedCodec) {
|
|
|
152
170
|
const read = snapshot.query({
|
|
153
171
|
kind: "read_document",
|
|
154
172
|
document: selector,
|
|
155
|
-
maxBytes: maxDocumentReadBytes,
|
|
156
173
|
});
|
|
157
174
|
if (read.kind === "error")
|
|
158
175
|
return selectionFailure(read, handle);
|
|
@@ -169,37 +186,13 @@ function resolveWholeDocument(snapshot, selector, handle, expectedCodec) {
|
|
|
169
186
|
message: "The selector codec does not match the target document",
|
|
170
187
|
};
|
|
171
188
|
if (read.codec === "markdown")
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
if (page.page.nextCursor === null)
|
|
180
|
-
return {
|
|
181
|
-
ok: false,
|
|
182
|
-
code: "unresolved_selector",
|
|
183
|
-
message: "The exact selector cannot currently be resolved",
|
|
184
|
-
};
|
|
185
|
-
const nextPage = snapshot.query({
|
|
186
|
-
kind: "read_document",
|
|
187
|
-
document: selector,
|
|
188
|
-
maxBytes: maxDocumentReadBytes,
|
|
189
|
-
cursor: page.page.nextCursor,
|
|
190
|
-
});
|
|
191
|
-
if (nextPage.kind === "error")
|
|
192
|
-
return selectionFailure(nextPage, handle);
|
|
193
|
-
if (nextPage.kind !== "read_document" || nextPage.codec !== "markdown")
|
|
194
|
-
return {
|
|
195
|
-
ok: false,
|
|
196
|
-
code: "unresolved_selector",
|
|
197
|
-
message: "The exact selector cannot currently be resolved",
|
|
198
|
-
};
|
|
199
|
-
chunks.push(nextPage.body);
|
|
200
|
-
page = nextPage;
|
|
201
|
-
}
|
|
202
|
-
return { ok: true, value: chunks.join("").trim() };
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
value: read.body.trim(),
|
|
192
|
+
shortRef: read.document.shortRef,
|
|
193
|
+
};
|
|
194
|
+
const root = selectYamlRoot(snapshot, selector, handle);
|
|
195
|
+
return root.ok ? { ...root, shortRef: read.document.shortRef } : root;
|
|
203
196
|
}
|
|
204
197
|
function selectYamlRoot(snapshot, selector, handle) {
|
|
205
198
|
const selected = snapshot.query({
|