narraeon 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/protocol/v1.js +4 -0
- package/dist/node/runtime/V1Runtime.js +13 -1
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +64 -163
- package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
- package/dist/node/runtime/model/ToolArgumentCoercion.js +138 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +34 -18
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +6 -2
- package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
- package/dist/node/runtime/play/PlayCallChain.js +276 -398
- package/dist/node/runtime/play/PlayDocumentTools.js +19 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +3 -3
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -0
- package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +370 -169
- package/dist/node/runtime/world/FileNativeWorldStore.js +151 -109
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -1
- package/dist/node/runtime/world/WorldDocumentStore.js +1 -1
- package/dist/node/runtime/world/WorldLedgerAuthority.js +37 -3
- package/dist/node/runtime/world/WorldTurnLedger.js +17 -4
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -4
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +19 -241
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +8 -27
- package/dist/node/server/createServer.js +1 -4
- package/dist/node/shared/ordered-play-prompts.js +5 -0
- package/dist/node/shared/turn-guide.js +27 -2
- package/dist/web/assets/index-Cj2JYCia.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-9N9kO8kY.js +0 -1408
|
@@ -19,8 +19,8 @@ import { readWorldActivity, updateWorldActivity, } from "./WorldActivity.js";
|
|
|
19
19
|
import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
|
|
20
20
|
import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
|
|
21
21
|
import { FileNativeWorldStorageMigrator, UnsupportedLegacyStorageError, } from "./FileNativeWorldStorageMigrator.js";
|
|
22
|
-
import { WorldAuthorityError, WorldLedgerAuthority, } from "./WorldLedgerAuthority.js";
|
|
23
|
-
import { forgetLedger, rewindTo, WorldTurnLedgerError, } from "./WorldTurnLedger.js";
|
|
22
|
+
import { isCorrectionCommit, WorldAuthorityError, WorldLedgerAuthority, } from "./WorldLedgerAuthority.js";
|
|
23
|
+
import { forgetLedger, recoverIncomplete, rewindTo, WorldTurnLedgerError, } from "./WorldTurnLedger.js";
|
|
24
24
|
import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
|
|
25
25
|
import { projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
|
|
26
26
|
const publicationFile = "publication.json";
|
|
@@ -76,9 +76,11 @@ export class FileNativeWorldStore {
|
|
|
76
76
|
this.#storageMigrator = new FileNativeWorldStorageMigrator(root);
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* Open a world for this process: upgrade an Authority V3 world to the current
|
|
80
|
+
* ledger layout, then recover any turn that did not finish before the process
|
|
81
|
+
* stopped (ADR-0045). Runs once per world in the world's queue slot, so no
|
|
82
|
+
* in-process advance can interleave with recovery. Concurrent callers share one
|
|
83
|
+
* in-process promise.
|
|
82
84
|
*/
|
|
83
85
|
async ensureCurrentStorage(worldId) {
|
|
84
86
|
assertIdentity(worldId, "World ID");
|
|
@@ -104,19 +106,24 @@ export class FileNativeWorldStore {
|
|
|
104
106
|
async #ensureCurrentStorageSerialized(worldId) {
|
|
105
107
|
try {
|
|
106
108
|
const root = join(this.#worldsRoot, worldId);
|
|
107
|
-
const authority = new WorldLedgerAuthority(root);
|
|
108
|
-
if ((await authority.exists()) &&
|
|
109
|
-
(await this.playTimeline.layoutCurrent(worldId)) &&
|
|
110
|
-
(await this.playAdvances.layoutCurrent(worldId))) {
|
|
111
|
-
await authority.readHead();
|
|
112
|
-
this.#currentStorage.add(worldId);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
109
|
await this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
// A caller queued behind the first open finds the world already open.
|
|
111
|
+
if (this.#currentStorage.has(worldId))
|
|
112
|
+
return;
|
|
113
|
+
const authority = new WorldLedgerAuthority(root);
|
|
114
|
+
if ((await authority.exists()) &&
|
|
115
|
+
(await this.playTimeline.layoutCurrent(worldId)) &&
|
|
116
|
+
(await this.playAdvances.layoutCurrent(worldId))) {
|
|
117
|
+
await authority.readHead();
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
await this.#storageMigrator.migrate(worldId);
|
|
121
|
+
await migrateLegacyArtifactStore(dirname(this.#worldsRoot), worldId);
|
|
122
|
+
}
|
|
123
|
+
// Recovery follows migration; a world with no unfinished turn writes nothing.
|
|
124
|
+
await recoverIncomplete(root);
|
|
125
|
+
this.#currentStorage.add(worldId);
|
|
118
126
|
});
|
|
119
|
-
this.#currentStorage.add(worldId);
|
|
120
127
|
}
|
|
121
128
|
catch (error) {
|
|
122
129
|
if (error instanceof FileNativeWorldCreationError)
|
|
@@ -425,9 +432,9 @@ export class FileNativeWorldStore {
|
|
|
425
432
|
* must never pair a mutable state tree with a different response head.
|
|
426
433
|
*/
|
|
427
434
|
async renderPlayerViewsAtHead(worldId, head) {
|
|
428
|
-
const [state, control] = await Promise.all([
|
|
429
|
-
this.recoverEndpoint(worldId, head)
|
|
430
|
-
this
|
|
435
|
+
const [{ state }, control] = await Promise.all([
|
|
436
|
+
this.recoverEndpoint(worldId, head),
|
|
437
|
+
this.#recoverEndpointControl(worldId, head),
|
|
431
438
|
]);
|
|
432
439
|
const source = control.find(({ path }) => path === "player-views.yaml");
|
|
433
440
|
return new PlayerViewRenderer().render({
|
|
@@ -533,54 +540,6 @@ export class FileNativeWorldStore {
|
|
|
533
540
|
async bindWorldRevision(handle) {
|
|
534
541
|
return this.operations.withWorldRevisionLock(handle, () => this.bindPlayCallChain(handle.worldId));
|
|
535
542
|
}
|
|
536
|
-
/** Replace control while the matching revision epoch retains exclusive ownership. */
|
|
537
|
-
async replaceWorldRevisionControl(input) {
|
|
538
|
-
for (const file of input.files)
|
|
539
|
-
assertRelativePath(file.path);
|
|
540
|
-
return this.operations.withWorldRevisionLock(input.handle, async () => {
|
|
541
|
-
if ((this.#activeControlUsers.get(input.handle.worldId)?.size ?? 0) > 0)
|
|
542
|
-
throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by another operation");
|
|
543
|
-
const root = join(this.#worldsRoot, input.handle.worldId);
|
|
544
|
-
const currentPath = join(root, "control");
|
|
545
|
-
const runtimeRoot = join(root, "runtime");
|
|
546
|
-
const stagingRoot = join(runtimeRoot, `.revision-control-${input.handle.epochId}-next`);
|
|
547
|
-
const stagedPath = join(stagingRoot, "control");
|
|
548
|
-
const previousPath = join(runtimeRoot, `.revision-control-${input.handle.epochId}-previous`);
|
|
549
|
-
let current = await readTree(currentPath);
|
|
550
|
-
let currentFingerprint = fingerprint(current);
|
|
551
|
-
const nextFingerprint = fingerprint(input.files);
|
|
552
|
-
if (currentFingerprint === nextFingerprint) {
|
|
553
|
-
await rm(stagingRoot, { recursive: true, force: true });
|
|
554
|
-
await rm(previousPath, { recursive: true, force: true });
|
|
555
|
-
return { fingerprint: nextFingerprint };
|
|
556
|
-
}
|
|
557
|
-
const previous = await readTree(previousPath);
|
|
558
|
-
const staged = await readTree(stagedPath);
|
|
559
|
-
if (current.length === 0 &&
|
|
560
|
-
fingerprint(previous) === input.expectedFingerprint) {
|
|
561
|
-
if (fingerprint(staged) === nextFingerprint) {
|
|
562
|
-
await rename(stagedPath, currentPath);
|
|
563
|
-
await rm(previousPath, { recursive: true, force: true });
|
|
564
|
-
await rm(stagingRoot, { recursive: true, force: true });
|
|
565
|
-
return { fingerprint: nextFingerprint };
|
|
566
|
-
}
|
|
567
|
-
await rename(previousPath, currentPath);
|
|
568
|
-
current = await readTree(currentPath);
|
|
569
|
-
currentFingerprint = fingerprint(current);
|
|
570
|
-
}
|
|
571
|
-
if (currentFingerprint !== input.expectedFingerprint)
|
|
572
|
-
throw new FileNativeWorldCreationError("operation_conflict", "World control changed after this revision epoch opened");
|
|
573
|
-
assertControlGameplayUnchanged(current, input.files);
|
|
574
|
-
await rm(stagingRoot, { recursive: true, force: true });
|
|
575
|
-
await rm(previousPath, { recursive: true, force: true });
|
|
576
|
-
await writeSurface(stagingRoot, "control", input.files);
|
|
577
|
-
await rename(currentPath, previousPath);
|
|
578
|
-
await rename(stagedPath, currentPath);
|
|
579
|
-
await rm(previousPath, { recursive: true, force: true });
|
|
580
|
-
await rm(stagingRoot, { recursive: true, force: true });
|
|
581
|
-
return { fingerprint: nextFingerprint };
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
543
|
async previewControlDraft(worldId, prompt) {
|
|
585
544
|
const { state, draft } = await this.#readControlDraft(worldId);
|
|
586
545
|
const rendered = new PlayerViewRenderer().render({
|
|
@@ -621,20 +580,25 @@ export class FileNativeWorldStore {
|
|
|
621
580
|
throw new FileNativeWorldCreationError("operation_conflict", "World control is frozen by a running attempt");
|
|
622
581
|
const preview = await this.previewControlDraft(worldId, prompt);
|
|
623
582
|
const root = join(this.#worldsRoot, worldId);
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
await
|
|
628
|
-
|
|
629
|
-
await
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
583
|
+
const draft = await readTree(join(root, "runtime/control-draft"));
|
|
584
|
+
const authority = new WorldLedgerAuthority(root);
|
|
585
|
+
const endpoint = await authority.recover();
|
|
586
|
+
assertControlGameplayUnchanged(await authority.recoverControl(endpoint.head), draft);
|
|
587
|
+
// 控制编辑应用是一轮 `control_apply`:只含 control 改动,与其他轮一样可回退。
|
|
588
|
+
await this.#commitHistoryChange({
|
|
589
|
+
operationId: `control-apply-${randomUUID()}`,
|
|
590
|
+
worldId,
|
|
591
|
+
parentHead: endpoint.head,
|
|
592
|
+
nextMaterials: worldNeutralMaterials(worldId, endpoint.additionalMaterials),
|
|
593
|
+
stateChanges: [],
|
|
594
|
+
mode: "control_apply",
|
|
595
|
+
historyAppend: [],
|
|
596
|
+
control: draft,
|
|
597
|
+
});
|
|
598
|
+
await rm(join(root, "runtime/control-draft"), {
|
|
599
|
+
recursive: true,
|
|
600
|
+
force: true,
|
|
601
|
+
});
|
|
638
602
|
return {
|
|
639
603
|
controlFingerprint: preview.diagnosticBinding.controlFingerprint,
|
|
640
604
|
preview,
|
|
@@ -702,8 +666,9 @@ export class FileNativeWorldStore {
|
|
|
702
666
|
}
|
|
703
667
|
try {
|
|
704
668
|
return await this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
705
|
-
const
|
|
706
|
-
const
|
|
669
|
+
const authority = new WorldLedgerAuthority(root);
|
|
670
|
+
const recovered = await authority.recover();
|
|
671
|
+
const control = await authority.recoverControl(recovered.head);
|
|
707
672
|
return {
|
|
708
673
|
worldId,
|
|
709
674
|
parentHead: recovered.head,
|
|
@@ -728,7 +693,7 @@ export class FileNativeWorldStore {
|
|
|
728
693
|
}
|
|
729
694
|
}
|
|
730
695
|
/**
|
|
731
|
-
* Bind an immutable historical endpoint with
|
|
696
|
+
* Bind an immutable historical endpoint with that endpoint's own control
|
|
732
697
|
* surface. This is the same semantic input as a fresh context started while
|
|
733
698
|
* that endpoint was current, without rewinding today's head.
|
|
734
699
|
*/
|
|
@@ -736,7 +701,7 @@ export class FileNativeWorldStore {
|
|
|
736
701
|
assertIdentity(worldId, "World ID");
|
|
737
702
|
const [endpoint, control] = await Promise.all([
|
|
738
703
|
this.recoverEndpoint(worldId, head),
|
|
739
|
-
this
|
|
704
|
+
this.#recoverEndpointControl(worldId, head),
|
|
740
705
|
]);
|
|
741
706
|
return {
|
|
742
707
|
worldId,
|
|
@@ -795,12 +760,16 @@ export class FileNativeWorldStore {
|
|
|
795
760
|
};
|
|
796
761
|
}
|
|
797
762
|
catch (error) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
763
|
+
throw endpointReadError(error, head);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/** 端点的控制文件;与 recoverEndpoint 的状态取同一个端点,回退与分叉随端点恢复。 */
|
|
767
|
+
async #recoverEndpointControl(worldId, head) {
|
|
768
|
+
try {
|
|
769
|
+
return await new WorldLedgerAuthority(join(this.#worldsRoot, worldId)).recoverControl(head);
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
throw endpointReadError(error, head);
|
|
804
773
|
}
|
|
805
774
|
}
|
|
806
775
|
async deriveWorld(input) {
|
|
@@ -846,10 +815,7 @@ export class FileNativeWorldStore {
|
|
|
846
815
|
const sourceHead = input.sourceHead === "current"
|
|
847
816
|
? (await sourceAuthority.readHead()).head
|
|
848
817
|
: input.sourceHead;
|
|
849
|
-
const
|
|
850
|
-
readGenesisForFork(sourceRoot, input.sourceWorldId),
|
|
851
|
-
readTree(join(sourceRoot, "control")),
|
|
852
|
-
]);
|
|
818
|
+
const sourceGenesis = await readGenesisForFork(sourceRoot, input.sourceWorldId);
|
|
853
819
|
const staging = join(dirname(finalRoot), `.staging-${worldId}-${randomUUID()}`);
|
|
854
820
|
await mkdir(dirname(finalRoot), { recursive: true, mode: 0o700 });
|
|
855
821
|
try {
|
|
@@ -862,7 +828,9 @@ export class FileNativeWorldStore {
|
|
|
862
828
|
});
|
|
863
829
|
// 分叉不信任来源的投影文件:当前文件由副本上的账本端点重建。
|
|
864
830
|
await new WorldLedgerAuthority(staging).materializeCurrentFiles();
|
|
831
|
+
// 控制与状态一样取自所选端点:回退已把副本的 control/ 恢复到该端点。
|
|
865
832
|
const selected = await new WorldLedgerAuthority(staging).recover();
|
|
833
|
+
const selectedControl = await new WorldLedgerAuthority(staging).recoverControl(selected.head);
|
|
866
834
|
await PackageScriptPermissions.write(staging, input.resetPackageScriptPermissions
|
|
867
835
|
? []
|
|
868
836
|
: await PackageScriptPermissions.read(sourceRoot));
|
|
@@ -874,7 +842,7 @@ export class FileNativeWorldStore {
|
|
|
874
842
|
operationId: input.operationId,
|
|
875
843
|
parentEndpoint: "genesis",
|
|
876
844
|
state: structuredClone(sourceGenesis.state),
|
|
877
|
-
control: immutableFiles(
|
|
845
|
+
control: immutableFiles(selectedControl),
|
|
878
846
|
history: structuredClone(sourceGenesis.history),
|
|
879
847
|
additionalMaterials: structuredClone(sourceGenesis.additionalMaterials),
|
|
880
848
|
};
|
|
@@ -891,7 +859,7 @@ export class FileNativeWorldStore {
|
|
|
891
859
|
binding: {
|
|
892
860
|
worldId,
|
|
893
861
|
parentHead: sourceHead,
|
|
894
|
-
files: [...selected.state, ...
|
|
862
|
+
files: [...selected.state, ...selectedControl].reduce((files, { path, contents }, index) => {
|
|
895
863
|
files[`${index < selected.state.length ? "state" : "control"}/${path}`] = contents;
|
|
896
864
|
return files;
|
|
897
865
|
}, {}),
|
|
@@ -1021,9 +989,6 @@ export class FileNativeWorldStore {
|
|
|
1021
989
|
mode: "correction",
|
|
1022
990
|
historyAppend: [],
|
|
1023
991
|
operationReserved,
|
|
1024
|
-
...(input.validationControl === undefined
|
|
1025
|
-
? {}
|
|
1026
|
-
: { validationControl: input.validationControl }),
|
|
1027
992
|
});
|
|
1028
993
|
};
|
|
1029
994
|
if (input.stateOperationClaim !== undefined &&
|
|
@@ -1041,6 +1006,45 @@ export class FileNativeWorldStore {
|
|
|
1041
1006
|
throw error;
|
|
1042
1007
|
}
|
|
1043
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* 世界修订应用:state 与 control 的改动作为同一轮 `revision_apply` 提交。
|
|
1011
|
+
* 操作身份由 epoch 决定,同一身份已是当前端点时直接返回该提交(崩溃后重放不重复推进)。
|
|
1012
|
+
*/
|
|
1013
|
+
async commitWorldRevisionApply(input) {
|
|
1014
|
+
await this.ensureCurrentStorage(input.worldId);
|
|
1015
|
+
try {
|
|
1016
|
+
return await this.operations.withWorldRevisionLock(input.worldRevisionLock, async () => {
|
|
1017
|
+
const operationReserved = (await this.getOperationOutcome(input.operationId)).outcome ===
|
|
1018
|
+
"not_started" && (await this.reserveOperation(input.operationId));
|
|
1019
|
+
try {
|
|
1020
|
+
return await this.#commitHistoryChange({
|
|
1021
|
+
...(input.worldClock === undefined
|
|
1022
|
+
? {}
|
|
1023
|
+
: { worldClock: input.worldClock }),
|
|
1024
|
+
operationId: input.operationId,
|
|
1025
|
+
worldId: input.worldId,
|
|
1026
|
+
parentHead: input.parentHead,
|
|
1027
|
+
nextMaterials: worldNeutralMaterials(input.worldId, input.nextMaterials),
|
|
1028
|
+
stateChanges: input.stateChanges,
|
|
1029
|
+
mode: "revision_apply",
|
|
1030
|
+
historyAppend: [],
|
|
1031
|
+
operationReserved,
|
|
1032
|
+
control: input.control,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
if (operationReserved)
|
|
1037
|
+
await this.releaseOperationReservation(input.operationId);
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
if (error instanceof WorldOperationBusyError)
|
|
1044
|
+
throw new FileNativeWorldCreationError("operation_conflict", "This world already has a durable state operation in progress");
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1044
1048
|
/**
|
|
1045
1049
|
* Persist one already-visible step of the model-directed play call chain.
|
|
1046
1050
|
* Player text, assistant text and world changes deliberately arrive in
|
|
@@ -1094,11 +1098,16 @@ export class FileNativeWorldStore {
|
|
|
1094
1098
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
|
|
1095
1099
|
const authorityStore = new WorldLedgerAuthority(root);
|
|
1096
1100
|
const authority = await authorityStore.readHead();
|
|
1101
|
+
if (input.mode === "revision_apply" &&
|
|
1102
|
+
authority.operationId === input.operationId) {
|
|
1103
|
+
// 同一世界修订应用已经是当前端点:崩溃发生在端点发布之后、阶段记录之前。
|
|
1104
|
+
return this.#committedOutcomeAtHead(authorityStore, input, authority.head);
|
|
1105
|
+
}
|
|
1097
1106
|
if (authority.head !== input.parentHead)
|
|
1098
1107
|
throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
|
|
1099
|
-
const [basisState,
|
|
1108
|
+
const [basisState, basisControl] = await Promise.all([
|
|
1100
1109
|
authorityStore.recoverState(authority.head),
|
|
1101
|
-
|
|
1110
|
+
authorityStore.recoverControl(authority.head),
|
|
1102
1111
|
]);
|
|
1103
1112
|
const candidateState = new Map(basisState.map((file) => [file.path, file.contents]));
|
|
1104
1113
|
for (const change of input.stateChanges) {
|
|
@@ -1115,7 +1124,7 @@ export class FileNativeWorldStore {
|
|
|
1115
1124
|
path: `world/${path}`,
|
|
1116
1125
|
contents,
|
|
1117
1126
|
})),
|
|
1118
|
-
...(input.
|
|
1127
|
+
...(input.control ?? basisControl).map(({ path, contents }) => ({
|
|
1119
1128
|
path: `control/${path}`,
|
|
1120
1129
|
contents,
|
|
1121
1130
|
})),
|
|
@@ -1135,10 +1144,11 @@ export class FileNativeWorldStore {
|
|
|
1135
1144
|
historyAppend: input.historyAppend,
|
|
1136
1145
|
stateChanges: input.stateChanges,
|
|
1137
1146
|
nextMaterials: input.nextMaterials,
|
|
1147
|
+
...(input.control === undefined ? {} : { control: input.control }),
|
|
1138
1148
|
...(input.narrativeCheckpoint === undefined
|
|
1139
1149
|
? {}
|
|
1140
1150
|
: { narrativeCheckpoint: input.narrativeCheckpoint }),
|
|
1141
|
-
...(input.mode
|
|
1151
|
+
...(isCorrectionCommit(input.mode)
|
|
1142
1152
|
? {
|
|
1143
1153
|
correctionTargets: input.stateChanges.map(({ documentId }) => documentId),
|
|
1144
1154
|
corrects: input.parentHead,
|
|
@@ -1169,6 +1179,27 @@ export class FileNativeWorldStore {
|
|
|
1169
1179
|
throw error;
|
|
1170
1180
|
}
|
|
1171
1181
|
}
|
|
1182
|
+
/**
|
|
1183
|
+
* 操作身份已经是当前端点的提交:核对它与本次输入一致,然后按它的结果返回。
|
|
1184
|
+
* 不一致说明同一身份被用于另一份提交,拒绝而不是覆盖或重复推进。
|
|
1185
|
+
*/
|
|
1186
|
+
async #committedOutcomeAtHead(authorityStore, input, head) {
|
|
1187
|
+
const fact = await authorityStore.authorityFactAt(head);
|
|
1188
|
+
if (!authorityCommitMatchesInput(fact.commit, input))
|
|
1189
|
+
throw new FileNativeWorldCreationError("operation_conflict", "The same operation ID is bound to a different complete commit payload");
|
|
1190
|
+
const outcome = {
|
|
1191
|
+
outcome: "committed",
|
|
1192
|
+
worldId: input.worldId,
|
|
1193
|
+
parentHead: fact.commit.parentHead,
|
|
1194
|
+
head: fact.commit.head,
|
|
1195
|
+
commitDigest: fact.digest,
|
|
1196
|
+
historyAppend: structuredClone(input.historyAppend),
|
|
1197
|
+
nextAdditionalMaterials: structuredClone(input.nextMaterials),
|
|
1198
|
+
mode: input.mode,
|
|
1199
|
+
};
|
|
1200
|
+
this.#rememberOperationOutcome(input.worldId, input.operationId, outcome);
|
|
1201
|
+
return outcome;
|
|
1202
|
+
}
|
|
1172
1203
|
async readAuthorityHistory(worldId) {
|
|
1173
1204
|
assertIdentity(worldId, "World ID");
|
|
1174
1205
|
await this.ensureCurrentStorage(worldId);
|
|
@@ -1228,9 +1259,12 @@ export class FileNativeWorldStore {
|
|
|
1228
1259
|
throw new FileNativeWorldCreationError("world_corrupt", `Historical endpoint does not exist: ${endpoint}`);
|
|
1229
1260
|
return {
|
|
1230
1261
|
endpoint,
|
|
1231
|
-
corrects: commit
|
|
1262
|
+
corrects: commit !== undefined && isCorrectionCommit(commit.mode)
|
|
1263
|
+
? (commit.corrects ?? null)
|
|
1264
|
+
: null,
|
|
1232
1265
|
correctedBy: commits
|
|
1233
|
-
.filter((candidate) => candidate.mode
|
|
1266
|
+
.filter((candidate) => isCorrectionCommit(candidate.mode) &&
|
|
1267
|
+
candidate.corrects === endpoint)
|
|
1234
1268
|
.map(({ head }) => head),
|
|
1235
1269
|
};
|
|
1236
1270
|
}
|
|
@@ -1514,7 +1548,8 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
1514
1548
|
nextHash: candidate.nextHash,
|
|
1515
1549
|
});
|
|
1516
1550
|
});
|
|
1517
|
-
const
|
|
1551
|
+
const correction = isCorrectionCommit(input.mode);
|
|
1552
|
+
const expectedCorrection = correction
|
|
1518
1553
|
? input.stateChanges.map(({ documentId }) => documentId)
|
|
1519
1554
|
: undefined;
|
|
1520
1555
|
return (commit.mode === input.mode &&
|
|
@@ -1530,8 +1565,15 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
1530
1565
|
isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
|
|
1531
1566
|
isDeepStrictEqual(commit.narrativeCheckpoint, input.narrativeCheckpoint) &&
|
|
1532
1567
|
isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
|
|
1533
|
-
commit.corrects ===
|
|
1534
|
-
|
|
1568
|
+
commit.corrects === (correction ? input.parentHead : undefined));
|
|
1569
|
+
}
|
|
1570
|
+
/** 读取某个端点失败的错误映射:端点在读取途中被回退丢弃不是损坏,调用方重新取得当前端点即可。 */
|
|
1571
|
+
function endpointReadError(error, head) {
|
|
1572
|
+
if (error instanceof FileNativeWorldCreationError)
|
|
1573
|
+
return error;
|
|
1574
|
+
if (error instanceof WorldTurnLedgerError && error.code === "discarded")
|
|
1575
|
+
return new FileNativeWorldCreationError("endpoint_discarded", `Authority endpoint was rewound away${head === undefined ? "" : `: ${head}`}`, { cause: error });
|
|
1576
|
+
return new FileNativeWorldCreationError("world_corrupt", `Cannot recover immutable Authority endpoint${head === undefined ? "" : `: ${head}`}`, error instanceof Error ? { cause: error } : undefined);
|
|
1535
1577
|
}
|
|
1536
1578
|
function headPosition(head) {
|
|
1537
1579
|
if (head === "genesis")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listWorldDocuments } from "../prompt/WorldMaintenanceReport.js";
|
|
2
2
|
import { WorldDocumentStore } from "./WorldDocumentStore.js";
|
|
3
|
+
import { isCorrectionCommit, } from "./WorldLedgerAuthority.js";
|
|
3
4
|
/** Reachable committed bytes only. Cached prefixes are disposable accelerators. */
|
|
4
5
|
export async function readDocumentMaintenanceFacts(authority, head, cache) {
|
|
5
6
|
const exact = cache.get(head);
|
|
@@ -92,7 +93,7 @@ export async function readDocumentMaintenanceFacts(authority, head, cache) {
|
|
|
92
93
|
files.set(change.relativePath, contents);
|
|
93
94
|
const next = snapshot();
|
|
94
95
|
const position = {
|
|
95
|
-
kind: commit.mode
|
|
96
|
+
kind: isCorrectionCommit(commit.mode)
|
|
96
97
|
? "correction"
|
|
97
98
|
: context === undefined
|
|
98
99
|
? "history"
|
|
@@ -171,7 +171,7 @@ export class WorldDocumentStore {
|
|
|
171
171
|
code: "logical_path_invalid",
|
|
172
172
|
logicalPath: command.toLogicalPath,
|
|
173
173
|
documentId: target.entry.descriptor.documentId,
|
|
174
|
-
message:
|
|
174
|
+
message: `Move target must be a safe logical path under ${this.logicalRoot}/ with the original codec`,
|
|
175
175
|
});
|
|
176
176
|
if (working.some(({ file }) => file !== target.working.file &&
|
|
177
177
|
file.path === command.toLogicalPath))
|
|
@@ -3,6 +3,13 @@ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { projectFileNativeHistorySurface, projectHistoryBackgrounds, } from "./FileNativeHistoryProjection.js";
|
|
5
5
|
import { appendTurn, initializeLedger, listTurns, readEndpoint, readFileAt, readHead as readLedgerHead, readTreeAt, readTurnDigest, resolveEndpoint, } from "./WorldTurnLedger.js";
|
|
6
|
+
/**
|
|
7
|
+
* 世界修订应用与连续性修正同属修订谱系:它们记录 `corrects` 与 `correctionTargets`,
|
|
8
|
+
* 维护报告与修订追溯按此归类。
|
|
9
|
+
*/
|
|
10
|
+
export function isCorrectionCommit(mode) {
|
|
11
|
+
return mode === "correction" || mode === "revision_apply";
|
|
12
|
+
}
|
|
6
13
|
export class WorldAuthorityError extends Error {
|
|
7
14
|
code;
|
|
8
15
|
constructor(code, message) {
|
|
@@ -113,14 +120,38 @@ export class WorldLedgerAuthority {
|
|
|
113
120
|
return structuredClone(parsed.items ?? []);
|
|
114
121
|
}
|
|
115
122
|
async #stateAt(position) {
|
|
116
|
-
|
|
123
|
+
return this.#surfaceAt(position, "state");
|
|
124
|
+
}
|
|
125
|
+
async #controlAt(position) {
|
|
126
|
+
return this.#surfaceAt(position, "control");
|
|
127
|
+
}
|
|
128
|
+
async #surfaceAt(position, surface) {
|
|
129
|
+
const prefix = `${surface}/`;
|
|
130
|
+
const tree = await readTreeAt(this.#root, position, [surface]);
|
|
117
131
|
return [...tree]
|
|
118
132
|
.map(([path, contents]) => ({
|
|
119
|
-
path: path.slice(
|
|
133
|
+
path: path.slice(prefix.length),
|
|
120
134
|
contents,
|
|
121
135
|
}))
|
|
122
136
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
123
137
|
}
|
|
138
|
+
/** 端点的控制文件:与状态同一账本视图,回退与分叉随端点恢复。 */
|
|
139
|
+
async recoverControl(head) {
|
|
140
|
+
return this.#controlAt(await this.#positionOf(head));
|
|
141
|
+
}
|
|
142
|
+
/** 把当前端点的控制文件替换为 next 所需的逐文件改动(写入或删除,未变化的不记录)。 */
|
|
143
|
+
async #controlChanges(position, next) {
|
|
144
|
+
const current = await readTreeAt(this.#root, position, ["control"]);
|
|
145
|
+
const wanted = new Map(next.map((file) => [`control/${file.path}`, file.contents]));
|
|
146
|
+
const changes = [];
|
|
147
|
+
for (const path of current.keys())
|
|
148
|
+
if (!wanted.has(path))
|
|
149
|
+
changes.push({ path, content: null });
|
|
150
|
+
for (const [path, contents] of wanted)
|
|
151
|
+
if (current.get(path) !== contents)
|
|
152
|
+
changes.push({ path, content: contents });
|
|
153
|
+
return changes;
|
|
154
|
+
}
|
|
124
155
|
#checkpointAt(commits) {
|
|
125
156
|
for (let index = commits.length - 1; index >= 0; index -= 1) {
|
|
126
157
|
const commit = commits[index];
|
|
@@ -246,7 +277,8 @@ export class WorldLedgerAuthority {
|
|
|
246
277
|
}
|
|
247
278
|
/**
|
|
248
279
|
* 在当前端点之上追加一轮。调用方先校验内容;这里校验状态的前像哈希,
|
|
249
|
-
*
|
|
280
|
+
* 并一次性计算本轮需要替换的全部文件(状态、控制、历史、历史背景与材料清单)。
|
|
281
|
+
* `control` 是提交后的完整控制文件集合:与端点的差异作为本轮改动,逐文件记录前像。
|
|
250
282
|
*/
|
|
251
283
|
async appendCommit(input) {
|
|
252
284
|
const current = await this.readHead();
|
|
@@ -289,6 +321,8 @@ export class WorldLedgerAuthority {
|
|
|
289
321
|
path: `runtime/history-backgrounds/${file.path}`,
|
|
290
322
|
content: file.contents,
|
|
291
323
|
});
|
|
324
|
+
if (input.control !== undefined)
|
|
325
|
+
changes.push(...(await this.#controlChanges(current.sequence, input.control)));
|
|
292
326
|
changes.push({
|
|
293
327
|
path: materialsFile,
|
|
294
328
|
content: `${JSON.stringify({ head, items: input.nextMaterials }, null, 2)}\n`,
|
|
@@ -194,6 +194,10 @@ async function captureBefore(root, paths) {
|
|
|
194
194
|
// 第一步:写入本轮记录(序号为 head + 1)。记录原子落盘,之后任何一步崩溃都可由 recoverIncomplete 回滚。
|
|
195
195
|
export async function beginTurn(root, input) {
|
|
196
196
|
const head = await readHead(root);
|
|
197
|
+
// head + 1 上还留着记录,说明上一轮没有发布端点(中断,或回滚失败)。
|
|
198
|
+
// 先恢复,否则新记录会把半写入的文件当作前像保存。
|
|
199
|
+
if ((await readTextOrNull(join(root, turnFileName(head + 1)))) !== null)
|
|
200
|
+
await recoverIncomplete(root);
|
|
197
201
|
const record = {
|
|
198
202
|
sequence: head + 1,
|
|
199
203
|
mode: input.mode,
|
|
@@ -226,12 +230,21 @@ export async function commitTurnHead(root, record) {
|
|
|
226
230
|
}
|
|
227
231
|
export async function appendTurn(root, input) {
|
|
228
232
|
const record = await beginTurn(root, input);
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
try {
|
|
234
|
+
await applyChanges(root, input.changes);
|
|
235
|
+
await commitTurnHead(root, record);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// 本轮未发布:用启动恢复的同一规则回滚。回滚本身失败时保留原始错误;
|
|
239
|
+
// 残留的记录留在 head + 1 上,下一次推进开始前会先恢复(见 beginTurn)。
|
|
240
|
+
await recoverIncomplete(root).catch(() => undefined);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
231
243
|
return record;
|
|
232
244
|
}
|
|
233
|
-
//
|
|
234
|
-
//
|
|
245
|
+
// 世界首次打开时调用(FileNativeWorldStore),进程内推进失败时也调用:序号大于 head 的记录
|
|
246
|
+
// 说明对应推进未完成,逆序恢复其修改前像。被丢弃的轮记录(读取时已忽略)在这里清理。
|
|
247
|
+
// 返回是否回滚了未完成的轮。
|
|
235
248
|
export async function recoverIncomplete(root) {
|
|
236
249
|
const head = await readHead(root);
|
|
237
250
|
const turns = await readAllRecords(root);
|
|
@@ -320,9 +320,7 @@ function validApply(value) {
|
|
|
320
320
|
nonEmptyString(value.operationId) &&
|
|
321
321
|
typeof value.expectedRevision === "string" &&
|
|
322
322
|
treeRevisionPattern.test(value.expectedRevision) &&
|
|
323
|
-
(value.phase === "prepared" ||
|
|
324
|
-
value.phase === "state_committed" ||
|
|
325
|
-
value.phase === "control_published") &&
|
|
323
|
+
(value.phase === "prepared" || value.phase === "committed") &&
|
|
326
324
|
(value.phase === "prepared"
|
|
327
325
|
? value.committedHead === null
|
|
328
326
|
: nonEmptyString(value.committedHead))));
|
|
@@ -337,7 +335,7 @@ function validEpochLifecycle(value) {
|
|
|
337
335
|
value.apply.expectedRevision === value.revision);
|
|
338
336
|
if (value.lifecycle === "applied")
|
|
339
337
|
return (isRecord(value.apply) &&
|
|
340
|
-
value.apply.phase === "
|
|
338
|
+
value.apply.phase === "committed" &&
|
|
341
339
|
value.apply.expectedRevision === value.revision &&
|
|
342
340
|
value.finishedAt !== null);
|
|
343
341
|
return (value.lifecycle === "discarded" &&
|