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
|
@@ -3,11 +3,9 @@ import { isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
|
|
|
3
3
|
const documentIdPattern = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u;
|
|
4
4
|
const shortRefPattern = /^[a-z][a-z0-9-]*$/u;
|
|
5
5
|
const base64UrlPattern = /^[A-Za-z0-9_-]+$/u;
|
|
6
|
-
const maxWorldDocumentBytes = 4 * 1024 * 1024;
|
|
7
6
|
const maxYamlDepth = 64;
|
|
8
7
|
const maxYamlNodes = 100_000;
|
|
9
8
|
const maxLiteralSearchMatches = 10_000;
|
|
10
|
-
const maxSelectedNodeBytes = 1024 * 1024;
|
|
11
9
|
export class WorldDocumentStore {
|
|
12
10
|
static open(input) {
|
|
13
11
|
return new WorldDocumentStore(input.layout, input.files);
|
|
@@ -276,6 +274,7 @@ export class WorldDocumentStore {
|
|
|
276
274
|
title: source.metadata.title,
|
|
277
275
|
summary: source.metadata.summary,
|
|
278
276
|
aliases: [...source.metadata.aliases],
|
|
277
|
+
...(previous?.retired === true ? { retired: true } : {}),
|
|
279
278
|
},
|
|
280
279
|
};
|
|
281
280
|
let contents;
|
|
@@ -314,6 +313,23 @@ export class WorldDocumentStore {
|
|
|
314
313
|
}
|
|
315
314
|
touchedPath = writePath;
|
|
316
315
|
}
|
|
316
|
+
else if (command.kind === "retire") {
|
|
317
|
+
if (this.layout !== "world_state" ||
|
|
318
|
+
!hasOnlyKeys(command, ["kind", "document", "retired"]) ||
|
|
319
|
+
typeof command.retired !== "boolean")
|
|
320
|
+
return this.#revisionFailure(commandIndex, {
|
|
321
|
+
code: "query_invalid",
|
|
322
|
+
message: "A retire command requires a world-state target and boolean retired value",
|
|
323
|
+
});
|
|
324
|
+
const target = this.#resolveRevisionTarget(current, working, command.document);
|
|
325
|
+
if ("diagnostics" in target)
|
|
326
|
+
return this.#revisionFailure(commandIndex, target.diagnostics);
|
|
327
|
+
if (target.entry.descriptor.retired !== command.retired)
|
|
328
|
+
target.working.file.contents = setDocumentRetired(target.working.file.contents, target.entry.descriptor.codec, command.retired);
|
|
329
|
+
delete target.working.file.encoding;
|
|
330
|
+
target.working.lastCommandIndex = commandIndex;
|
|
331
|
+
touchedPath = target.working.file.path;
|
|
332
|
+
}
|
|
317
333
|
else if (command.kind === "patch") {
|
|
318
334
|
if (!hasOnlyKeys(command, ["kind", "document", "edits"]) ||
|
|
319
335
|
!Array.isArray(command.edits) ||
|
|
@@ -327,7 +343,7 @@ export class WorldDocumentStore {
|
|
|
327
343
|
if ("diagnostics" in target)
|
|
328
344
|
return this.#revisionFailure(commandIndex, target.diagnostics);
|
|
329
345
|
if (target.entry.codec === "markdown") {
|
|
330
|
-
const patched = patchRevisionMarkdown(target.working.file.contents, command.edits);
|
|
346
|
+
const patched = patchRevisionMarkdown(target.working.file.contents, command.edits, target.entry.descriptor);
|
|
331
347
|
if ("diagnostics" in patched)
|
|
332
348
|
return this.#revisionFailure(commandIndex, patched.diagnostics.map((problem) => ({
|
|
333
349
|
...problem,
|
|
@@ -342,6 +358,11 @@ export class WorldDocumentStore {
|
|
|
342
358
|
uniqueKeys: true,
|
|
343
359
|
strict: true,
|
|
344
360
|
});
|
|
361
|
+
let metadata = {
|
|
362
|
+
title: target.entry.descriptor.title,
|
|
363
|
+
summary: target.entry.descriptor.summary,
|
|
364
|
+
aliases: [...target.entry.descriptor.aliases],
|
|
365
|
+
};
|
|
345
366
|
for (const edit of command.edits) {
|
|
346
367
|
if (!isRecord(edit) || typeof edit.op !== "string")
|
|
347
368
|
return this.#revisionFailure(commandIndex, {
|
|
@@ -358,16 +379,18 @@ export class WorldDocumentStore {
|
|
|
358
379
|
documentId: target.entry.descriptor.documentId,
|
|
359
380
|
message: "set_metadata accepts only op, title, summary, and aliases",
|
|
360
381
|
});
|
|
361
|
-
|
|
382
|
+
const resolved = resolveDocumentMetadataRevision(metadata, edit);
|
|
383
|
+
if ("problems" in resolved)
|
|
362
384
|
return this.#revisionFailure(commandIndex, {
|
|
363
385
|
code: "document_header_invalid",
|
|
364
386
|
logicalPath: target.entry.file.path,
|
|
365
387
|
documentId: target.entry.descriptor.documentId,
|
|
366
|
-
message: `set_metadata arguments are invalid: ${
|
|
388
|
+
message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
|
|
367
389
|
});
|
|
368
|
-
|
|
369
|
-
document.setIn(["$document", "
|
|
370
|
-
document.setIn(["$document", "
|
|
390
|
+
metadata = resolved.metadata;
|
|
391
|
+
document.setIn(["$document", "title"], metadata.title);
|
|
392
|
+
document.setIn(["$document", "summary"], metadata.summary);
|
|
393
|
+
document.setIn(["$document", "aliases"], [...metadata.aliases]);
|
|
371
394
|
continue;
|
|
372
395
|
}
|
|
373
396
|
if (!["add", "replace", "append", "remove"].includes(edit.op) ||
|
|
@@ -820,7 +843,14 @@ export class WorldDocumentStore {
|
|
|
820
843
|
]);
|
|
821
844
|
}
|
|
822
845
|
#catalog(request) {
|
|
823
|
-
if (!hasOnlyKeys(request, [
|
|
846
|
+
if (!hasOnlyKeys(request, [
|
|
847
|
+
"kind",
|
|
848
|
+
"directory",
|
|
849
|
+
"includeRetired",
|
|
850
|
+
"declaredDirectories",
|
|
851
|
+
"limit",
|
|
852
|
+
"cursor",
|
|
853
|
+
]))
|
|
824
854
|
return this.#failure("catalog", request, [
|
|
825
855
|
diagnostic({
|
|
826
856
|
code: "query_invalid",
|
|
@@ -828,20 +858,28 @@ export class WorldDocumentStore {
|
|
|
828
858
|
}),
|
|
829
859
|
]);
|
|
830
860
|
const directory = request.directory ?? "";
|
|
861
|
+
const includeRetired = request.includeRetired ?? true;
|
|
862
|
+
const declaredDirectories = request.declaredDirectories ?? [];
|
|
831
863
|
const limit = request.limit ?? 20;
|
|
832
864
|
if ((directory !== "" && !validRelativePath(directory)) ||
|
|
865
|
+
typeof includeRetired !== "boolean" ||
|
|
866
|
+
!validDeclaredDirectories(declaredDirectories) ||
|
|
833
867
|
!Number.isInteger(limit) ||
|
|
834
868
|
limit < 1 ||
|
|
835
869
|
limit > 100)
|
|
836
870
|
return this.#failure("catalog", request, [
|
|
837
871
|
diagnostic({
|
|
838
872
|
code: "query_invalid",
|
|
839
|
-
message: "Catalog
|
|
873
|
+
message: "Catalog directories must be within the layout root, and limit must be 1 to 100",
|
|
840
874
|
}),
|
|
841
875
|
]);
|
|
876
|
+
const normalizedDeclaredDirectories = [
|
|
877
|
+
...new Set(declaredDirectories),
|
|
878
|
+
].sort((left, right) => left.localeCompare(right));
|
|
842
879
|
const relativePrefix = directory === "" ? "" : `${directory}/`;
|
|
843
880
|
const prefix = `${this.logicalRoot}/${relativePrefix}`;
|
|
844
881
|
const entries = new Map();
|
|
882
|
+
let excludedDocuments = 0;
|
|
845
883
|
for (const entry of this.#entries) {
|
|
846
884
|
if (!entry.file.path.startsWith(prefix))
|
|
847
885
|
continue;
|
|
@@ -855,6 +893,10 @@ export class WorldDocumentStore {
|
|
|
855
893
|
}));
|
|
856
894
|
}
|
|
857
895
|
else {
|
|
896
|
+
if (entry.descriptor?.retired === true && !includeRetired) {
|
|
897
|
+
excludedDocuments += 1;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
858
900
|
entries.set(`document:${relative}`, freeze({
|
|
859
901
|
kind: "document",
|
|
860
902
|
logicalPath: entry.file.path,
|
|
@@ -868,12 +910,27 @@ export class WorldDocumentStore {
|
|
|
868
910
|
}));
|
|
869
911
|
}
|
|
870
912
|
}
|
|
913
|
+
for (const declaredDirectory of normalizedDeclaredDirectories) {
|
|
914
|
+
if (!declaredDirectory.startsWith(relativePrefix))
|
|
915
|
+
continue;
|
|
916
|
+
const relative = declaredDirectory.slice(relativePrefix.length);
|
|
917
|
+
if (relative === "")
|
|
918
|
+
continue;
|
|
919
|
+
const slash = relative.indexOf("/");
|
|
920
|
+
const child = slash < 0 ? relative : relative.slice(0, slash);
|
|
921
|
+
entries.set(`directory:${child}`, freeze({
|
|
922
|
+
kind: "directory",
|
|
923
|
+
logicalPath: `${prefix}${child}`,
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
871
926
|
const ordered = [...entries.values()].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
872
927
|
const cursorScope = JSON.stringify({
|
|
873
928
|
kind: "catalog",
|
|
874
929
|
layout: this.layout,
|
|
875
930
|
logicalRoot: this.logicalRoot,
|
|
876
931
|
directory,
|
|
932
|
+
declaredDirectories: normalizedDeclaredDirectories,
|
|
933
|
+
includeRetired,
|
|
877
934
|
limit,
|
|
878
935
|
});
|
|
879
936
|
const offset = this.#cursorOffset(request.cursor, cursorScope);
|
|
@@ -894,7 +951,7 @@ export class WorldDocumentStore {
|
|
|
894
951
|
}),
|
|
895
952
|
coverage: freeze({
|
|
896
953
|
status: "complete",
|
|
897
|
-
excludedDocuments
|
|
954
|
+
excludedDocuments,
|
|
898
955
|
}),
|
|
899
956
|
page: freeze({
|
|
900
957
|
unit: "entries",
|
|
@@ -1081,19 +1138,19 @@ export class WorldDocumentStore {
|
|
|
1081
1138
|
});
|
|
1082
1139
|
}
|
|
1083
1140
|
#readDocument(request) {
|
|
1084
|
-
if (!hasOnlyKeys(request, ["kind", "document", "
|
|
1141
|
+
if (!hasOnlyKeys(request, ["kind", "document", "referenceProjection"]))
|
|
1085
1142
|
return this.#failure("read_document", request, [
|
|
1086
1143
|
diagnostic({
|
|
1087
1144
|
code: "query_invalid",
|
|
1088
1145
|
message: "read_document contains undeclared query conditions",
|
|
1089
1146
|
}),
|
|
1090
1147
|
]);
|
|
1091
|
-
|
|
1092
|
-
|
|
1148
|
+
if (request.referenceProjection !== undefined &&
|
|
1149
|
+
request.referenceProjection !== "short_ref")
|
|
1093
1150
|
return this.#failure("read_document", request, [
|
|
1094
1151
|
diagnostic({
|
|
1095
1152
|
code: "query_invalid",
|
|
1096
|
-
message: "read_document
|
|
1153
|
+
message: "read_document referenceProjection, when present, must be short_ref",
|
|
1097
1154
|
}),
|
|
1098
1155
|
]);
|
|
1099
1156
|
const resolved = this.#resolveSelector(request.document);
|
|
@@ -1109,21 +1166,9 @@ export class WorldDocumentStore {
|
|
|
1109
1166
|
message: "World document has no readable body range",
|
|
1110
1167
|
}),
|
|
1111
1168
|
], resolved.entry.descriptor);
|
|
1112
|
-
const completeBody =
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
kind: "read_document",
|
|
1116
|
-
layout: this.layout,
|
|
1117
|
-
logicalRoot: this.logicalRoot,
|
|
1118
|
-
document: request.document,
|
|
1119
|
-
maxBytes,
|
|
1120
|
-
});
|
|
1121
|
-
const offset = this.#cursorOffset(request.cursor, cursorScope);
|
|
1122
|
-
if (offset === null || offset > bytes.length)
|
|
1123
|
-
return this.#cursorFailure("read_document", request);
|
|
1124
|
-
const end = safeUtf8PageEnd(bytes, offset, maxBytes);
|
|
1125
|
-
const body = bytes.subarray(offset, end).toString("utf8");
|
|
1126
|
-
const complete = end === bytes.length;
|
|
1169
|
+
const completeBody = request.referenceProjection === "short_ref"
|
|
1170
|
+
? projectExplicitReferenceValues(resolved.entry.file.contents, bodyRange, resolved.entry.references, this.#descriptorIndex(), true)
|
|
1171
|
+
: resolved.entry.file.contents.slice(bodyRange.start.offset, bodyRange.end.offset);
|
|
1127
1172
|
return freeze({
|
|
1128
1173
|
kind: "read_document",
|
|
1129
1174
|
ok: true,
|
|
@@ -1140,15 +1185,7 @@ export class WorldDocumentStore {
|
|
|
1140
1185
|
}),
|
|
1141
1186
|
document: resolved.entry.descriptor,
|
|
1142
1187
|
codec: resolved.entry.codec,
|
|
1143
|
-
body,
|
|
1144
|
-
page: freeze({
|
|
1145
|
-
unit: "utf8_bytes",
|
|
1146
|
-
start: offset,
|
|
1147
|
-
end,
|
|
1148
|
-
total: bytes.length,
|
|
1149
|
-
complete,
|
|
1150
|
-
nextCursor: complete ? null : this.#cursor(cursorScope, end),
|
|
1151
|
-
}),
|
|
1188
|
+
body: completeBody,
|
|
1152
1189
|
diagnostics: freeze([]),
|
|
1153
1190
|
});
|
|
1154
1191
|
}
|
|
@@ -1187,20 +1224,6 @@ export class WorldDocumentStore {
|
|
|
1187
1224
|
if (value === unresolved || node === undefined)
|
|
1188
1225
|
return this.#locatorFailure(entry, publicLocator, "locator_not_found");
|
|
1189
1226
|
const projected = projectYamlValue(value, this.#descriptorIndex());
|
|
1190
|
-
const isWholeDocumentProjection = locator.yaml.length === 0;
|
|
1191
|
-
if (!isWholeDocumentProjection &&
|
|
1192
|
-
Buffer.byteLength(JSON.stringify(projected), "utf8") >
|
|
1193
|
-
maxSelectedNodeBytes)
|
|
1194
|
-
return this.#failure("select_node", request, [
|
|
1195
|
-
diagnostic({
|
|
1196
|
-
code: "capacity_exceeded",
|
|
1197
|
-
logicalPath: entry.file.path,
|
|
1198
|
-
documentId: entry.descriptor.documentId,
|
|
1199
|
-
locator: publicLocator,
|
|
1200
|
-
range: node.range,
|
|
1201
|
-
message: "Exact-node projection exceeds the capacity of one query",
|
|
1202
|
-
}),
|
|
1203
|
-
], entry.descriptor);
|
|
1204
1227
|
return this.#selected(entry, publicLocator, {
|
|
1205
1228
|
codec: "yaml",
|
|
1206
1229
|
locator: publicLocator,
|
|
@@ -1222,17 +1245,6 @@ export class WorldDocumentStore {
|
|
|
1222
1245
|
return this.#locatorFailure(entry, publicLocator, "locator_ambiguous");
|
|
1223
1246
|
const heading = headings[0];
|
|
1224
1247
|
const markdown = entry.file.contents.slice(heading.range.start.offset, heading.range.end.offset);
|
|
1225
|
-
if (Buffer.byteLength(markdown, "utf8") > maxSelectedNodeBytes)
|
|
1226
|
-
return this.#failure("select_node", request, [
|
|
1227
|
-
diagnostic({
|
|
1228
|
-
code: "capacity_exceeded",
|
|
1229
|
-
logicalPath: entry.file.path,
|
|
1230
|
-
documentId: entry.descriptor.documentId,
|
|
1231
|
-
locator: publicLocator,
|
|
1232
|
-
range: heading.range,
|
|
1233
|
-
message: "Exact-node projection exceeds the capacity of one query",
|
|
1234
|
-
}),
|
|
1235
|
-
], entry.descriptor);
|
|
1236
1248
|
return this.#selected(entry, publicLocator, {
|
|
1237
1249
|
codec: "markdown",
|
|
1238
1250
|
locator: publicLocator,
|
|
@@ -1475,15 +1487,6 @@ export class WorldDocumentStore {
|
|
|
1475
1487
|
}));
|
|
1476
1488
|
return entry;
|
|
1477
1489
|
}
|
|
1478
|
-
if (Buffer.byteLength(file.contents, "utf8") > maxWorldDocumentBytes) {
|
|
1479
|
-
entry.diagnostics.push(diagnostic({
|
|
1480
|
-
code: "capacity_exceeded",
|
|
1481
|
-
logicalPath: file.path,
|
|
1482
|
-
range: sourceRange(file.contents, 0, file.contents.length),
|
|
1483
|
-
message: "One world document cannot exceed 4 MiB",
|
|
1484
|
-
}));
|
|
1485
|
-
return entry;
|
|
1486
|
-
}
|
|
1487
1490
|
if (entry.codec === "yaml")
|
|
1488
1491
|
inspectYamlDocument(entry);
|
|
1489
1492
|
else
|
|
@@ -1854,8 +1857,10 @@ function readHeader(entry, parsed, headerOnly) {
|
|
|
1854
1857
|
}
|
|
1855
1858
|
const header = first.value;
|
|
1856
1859
|
const pairs = stringPairMap(header);
|
|
1857
|
-
const
|
|
1858
|
-
const
|
|
1860
|
+
const required = new Set(["id", "ref", "title", "summary", "aliases"]);
|
|
1861
|
+
const allowed = new Set([...required, "retired"]);
|
|
1862
|
+
const exactShape = (pairs.size === required.size || pairs.size === allowed.size) &&
|
|
1863
|
+
[...required].every((key) => pairs.has(key)) &&
|
|
1859
1864
|
[...pairs.keys()].every((key) => allowed.has(key)) &&
|
|
1860
1865
|
(!headerOnly || parsed.root.items.length === 1);
|
|
1861
1866
|
const idPair = pairs.get("id");
|
|
@@ -1863,12 +1868,14 @@ function readHeader(entry, parsed, headerOnly) {
|
|
|
1863
1868
|
const titlePair = pairs.get("title");
|
|
1864
1869
|
const summaryPair = pairs.get("summary");
|
|
1865
1870
|
const aliasesPair = pairs.get("aliases");
|
|
1871
|
+
const retiredPair = pairs.get("retired");
|
|
1866
1872
|
const id = scalarString(idPair?.value);
|
|
1867
1873
|
const shortRef = scalarString(refPair?.value);
|
|
1868
1874
|
const title = scalarString(titlePair?.value);
|
|
1869
1875
|
const summary = scalarString(summaryPair?.value);
|
|
1870
1876
|
const aliases = stringSequence(aliasesPair?.value);
|
|
1871
|
-
|
|
1877
|
+
const retired = retiredPair === undefined ? false : scalarBoolean(retiredPair.value);
|
|
1878
|
+
let valid = exactShape && retired !== null;
|
|
1872
1879
|
if (id === null ||
|
|
1873
1880
|
id.length < 3 ||
|
|
1874
1881
|
id.length > 128 ||
|
|
@@ -1919,7 +1926,7 @@ function readHeader(entry, parsed, headerOnly) {
|
|
|
1919
1926
|
? { documentId: id }
|
|
1920
1927
|
: {}),
|
|
1921
1928
|
range: nodeRange(entry.file.contents, header, parsed.baseOffset),
|
|
1922
|
-
message: "$document must contain
|
|
1929
|
+
message: "$document must contain valid id, ref, title, summary, aliases, and an optional boolean retired flag",
|
|
1923
1930
|
}));
|
|
1924
1931
|
return null;
|
|
1925
1932
|
}
|
|
@@ -1929,6 +1936,7 @@ function readHeader(entry, parsed, headerOnly) {
|
|
|
1929
1936
|
title: title,
|
|
1930
1937
|
summary: summary,
|
|
1931
1938
|
aliases: freeze(aliases),
|
|
1939
|
+
retired: retired,
|
|
1932
1940
|
codec: entry.codec,
|
|
1933
1941
|
logicalPath: entry.file.path,
|
|
1934
1942
|
});
|
|
@@ -2001,7 +2009,7 @@ function inspectYamlReferences(entry, node, locator) {
|
|
|
2001
2009
|
if (isSeq(node))
|
|
2002
2010
|
node.items.forEach((child, index) => inspectYamlReferences(entry, child, [...locator, index]));
|
|
2003
2011
|
}
|
|
2004
|
-
function projectExplicitReferenceValues(source, selectedRange, references, descriptors) {
|
|
2012
|
+
function projectExplicitReferenceValues(source, selectedRange, references, descriptors, quoteProjection = false) {
|
|
2005
2013
|
const selectedStart = selectedRange.start.offset;
|
|
2006
2014
|
const selectedEnd = selectedRange.end.offset;
|
|
2007
2015
|
const replacements = references
|
|
@@ -2017,8 +2025,12 @@ function projectExplicitReferenceValues(source, selectedRange, references, descr
|
|
|
2017
2025
|
start,
|
|
2018
2026
|
end,
|
|
2019
2027
|
projection: descriptor === undefined
|
|
2020
|
-
?
|
|
2021
|
-
|
|
2028
|
+
? quoteProjection
|
|
2029
|
+
? JSON.stringify("(invalid document reference)")
|
|
2030
|
+
: "(invalid document reference)"
|
|
2031
|
+
: quoteProjection
|
|
2032
|
+
? JSON.stringify(`@${descriptor.shortRef}`)
|
|
2033
|
+
: `@${descriptor.shortRef}`,
|
|
2022
2034
|
},
|
|
2023
2035
|
]
|
|
2024
2036
|
: [];
|
|
@@ -2195,12 +2207,6 @@ function lineExcerptRange(source, matchStart, matchEnd) {
|
|
|
2195
2207
|
const end = Math.min(lineEnd, matchEnd + 160);
|
|
2196
2208
|
return sourceRange(source, start, end);
|
|
2197
2209
|
}
|
|
2198
|
-
function safeUtf8PageEnd(bytes, offset, maxBytes) {
|
|
2199
|
-
let end = Math.min(bytes.length, offset + maxBytes);
|
|
2200
|
-
while (end > offset && (bytes[end] ?? 0) >= 0x80 && (bytes[end] ?? 0) < 0xc0)
|
|
2201
|
-
end -= 1;
|
|
2202
|
-
return end;
|
|
2203
|
-
}
|
|
2204
2210
|
function scanMarkdownHeadings(source, bodyStart) {
|
|
2205
2211
|
const headings = [];
|
|
2206
2212
|
let fence = null;
|
|
@@ -2241,8 +2247,13 @@ function scanMarkdownHeadings(source, bodyStart) {
|
|
|
2241
2247
|
}
|
|
2242
2248
|
return { headings, hasUnclosedFence: fence !== null };
|
|
2243
2249
|
}
|
|
2244
|
-
function patchRevisionMarkdown(source, edits) {
|
|
2250
|
+
function patchRevisionMarkdown(source, edits, initialMetadata) {
|
|
2245
2251
|
let contents = source;
|
|
2252
|
+
let metadata = {
|
|
2253
|
+
title: initialMetadata.title,
|
|
2254
|
+
summary: initialMetadata.summary,
|
|
2255
|
+
aliases: [...initialMetadata.aliases],
|
|
2256
|
+
};
|
|
2246
2257
|
for (const edit of edits) {
|
|
2247
2258
|
if (!isRecord(edit) || typeof edit.op !== "string")
|
|
2248
2259
|
return {
|
|
@@ -2254,24 +2265,32 @@ function patchRevisionMarkdown(source, edits) {
|
|
|
2254
2265
|
],
|
|
2255
2266
|
};
|
|
2256
2267
|
if (edit.op === "set_metadata") {
|
|
2257
|
-
if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"])
|
|
2258
|
-
|
|
2268
|
+
if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]))
|
|
2269
|
+
return {
|
|
2270
|
+
diagnostics: [
|
|
2271
|
+
diagnostic({
|
|
2272
|
+
code: "document_header_invalid",
|
|
2273
|
+
message: "set_metadata accepts only op, title, summary, and aliases",
|
|
2274
|
+
}),
|
|
2275
|
+
],
|
|
2276
|
+
};
|
|
2277
|
+
const resolved = resolveDocumentMetadataRevision(metadata, edit);
|
|
2278
|
+
if ("problems" in resolved)
|
|
2259
2279
|
return {
|
|
2260
2280
|
diagnostics: [
|
|
2261
2281
|
diagnostic({
|
|
2262
2282
|
code: "document_header_invalid",
|
|
2263
|
-
message:
|
|
2264
|
-
? `set_metadata arguments are invalid: ${documentMetadataProblems(edit).join("; ")}`
|
|
2265
|
-
: "set_metadata accepts only op, title, summary, and aliases",
|
|
2283
|
+
message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
|
|
2266
2284
|
}),
|
|
2267
2285
|
],
|
|
2268
2286
|
};
|
|
2287
|
+
metadata = resolved.metadata;
|
|
2269
2288
|
const envelope = markdownEnvelope(contents);
|
|
2270
2289
|
if ("diagnostics" in envelope)
|
|
2271
2290
|
return envelope;
|
|
2272
|
-
envelope.header.setIn(["$document", "title"],
|
|
2273
|
-
envelope.header.setIn(["$document", "summary"],
|
|
2274
|
-
envelope.header.setIn(["$document", "aliases"], [...
|
|
2291
|
+
envelope.header.setIn(["$document", "title"], metadata.title);
|
|
2292
|
+
envelope.header.setIn(["$document", "summary"], metadata.summary);
|
|
2293
|
+
envelope.header.setIn(["$document", "aliases"], [...metadata.aliases]);
|
|
2275
2294
|
const body = contents.slice(envelope.bodyStart);
|
|
2276
2295
|
const firstHeading = scanMarkdownHeadings(body, 0).headings[0];
|
|
2277
2296
|
if (firstHeading?.level !== 1)
|
|
@@ -2283,7 +2302,7 @@ function patchRevisionMarkdown(source, edits) {
|
|
|
2283
2302
|
}),
|
|
2284
2303
|
],
|
|
2285
2304
|
};
|
|
2286
|
-
const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${
|
|
2305
|
+
const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${metadata.title}${body.slice(firstHeading.range.end.offset)}`;
|
|
2287
2306
|
contents = `---\n${envelope.header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${nextBody}`;
|
|
2288
2307
|
continue;
|
|
2289
2308
|
}
|
|
@@ -2464,6 +2483,31 @@ function markdownEnvelope(source) {
|
|
|
2464
2483
|
};
|
|
2465
2484
|
return { header, bodyStart: end + 5 };
|
|
2466
2485
|
}
|
|
2486
|
+
function setDocumentRetired(source, codec, retired) {
|
|
2487
|
+
if (codec === "yaml") {
|
|
2488
|
+
const document = parseDocument(source, {
|
|
2489
|
+
schema: "core",
|
|
2490
|
+
uniqueKeys: true,
|
|
2491
|
+
strict: true,
|
|
2492
|
+
});
|
|
2493
|
+
if (retired)
|
|
2494
|
+
document.setIn(["$document", "retired"], true);
|
|
2495
|
+
else
|
|
2496
|
+
document.deleteIn(["$document", "retired"]);
|
|
2497
|
+
return document.toString({ indent: 2, lineWidth: 0 });
|
|
2498
|
+
}
|
|
2499
|
+
const headerEnd = source.indexOf("\n---\n", 4);
|
|
2500
|
+
const header = parseDocument(source.slice(4, headerEnd), {
|
|
2501
|
+
schema: "core",
|
|
2502
|
+
uniqueKeys: true,
|
|
2503
|
+
strict: true,
|
|
2504
|
+
});
|
|
2505
|
+
if (retired)
|
|
2506
|
+
header.setIn(["$document", "retired"], true);
|
|
2507
|
+
else
|
|
2508
|
+
header.deleteIn(["$document", "retired"]);
|
|
2509
|
+
return `---\n${header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}${source.slice(headerEnd)}`;
|
|
2510
|
+
}
|
|
2467
2511
|
function markdownRevisionSection(source, locator) {
|
|
2468
2512
|
const envelope = markdownEnvelope(source);
|
|
2469
2513
|
if ("diagnostics" in envelope)
|
|
@@ -2562,6 +2606,11 @@ function scalarString(value) {
|
|
|
2562
2606
|
? value.value
|
|
2563
2607
|
: null;
|
|
2564
2608
|
}
|
|
2609
|
+
function scalarBoolean(value) {
|
|
2610
|
+
return isScalar(value) && typeof value.value === "boolean"
|
|
2611
|
+
? value.value
|
|
2612
|
+
: null;
|
|
2613
|
+
}
|
|
2565
2614
|
function stringSequence(value) {
|
|
2566
2615
|
if (!isSeq(value))
|
|
2567
2616
|
return null;
|
|
@@ -2618,6 +2667,23 @@ function mechanicalHash(contents) {
|
|
|
2618
2667
|
function validDocumentMetadata(value) {
|
|
2619
2668
|
return documentMetadataProblems(value).length === 0;
|
|
2620
2669
|
}
|
|
2670
|
+
function resolveDocumentMetadataRevision(current, edit) {
|
|
2671
|
+
if (!Object.hasOwn(edit, "title") &&
|
|
2672
|
+
!Object.hasOwn(edit, "summary") &&
|
|
2673
|
+
!Object.hasOwn(edit, "aliases"))
|
|
2674
|
+
return {
|
|
2675
|
+
problems: ["at least one of title, summary, or aliases must be provided"],
|
|
2676
|
+
};
|
|
2677
|
+
const metadata = {
|
|
2678
|
+
title: Object.hasOwn(edit, "title") ? edit.title : current.title,
|
|
2679
|
+
summary: Object.hasOwn(edit, "summary") ? edit.summary : current.summary,
|
|
2680
|
+
aliases: Object.hasOwn(edit, "aliases") ? edit.aliases : current.aliases,
|
|
2681
|
+
};
|
|
2682
|
+
const problems = documentMetadataProblems(metadata);
|
|
2683
|
+
return problems.length === 0
|
|
2684
|
+
? { metadata: metadata }
|
|
2685
|
+
: { problems };
|
|
2686
|
+
}
|
|
2621
2687
|
/**
|
|
2622
2688
|
* Says which of the three fields is wrong and what arrived instead.
|
|
2623
2689
|
*
|
|
@@ -2711,6 +2777,10 @@ function validRelativePath(path) {
|
|
|
2711
2777
|
.split("/")
|
|
2712
2778
|
.every((segment) => segment !== "" && segment !== "." && segment !== ".."));
|
|
2713
2779
|
}
|
|
2780
|
+
function validDeclaredDirectories(value) {
|
|
2781
|
+
return (Array.isArray(value) &&
|
|
2782
|
+
value.every((candidate) => typeof candidate === "string" && validRelativePath(candidate)));
|
|
2783
|
+
}
|
|
2714
2784
|
function blockingDocumentDiagnostics(entry) {
|
|
2715
2785
|
return entry.diagnostics.filter(({ code }) => code !== "locator_ambiguous" && code !== "document_reference_invalid");
|
|
2716
2786
|
}
|
|
@@ -24,6 +24,7 @@ const worldLockHeartbeatStaleMilliseconds = 30_000;
|
|
|
24
24
|
*/
|
|
25
25
|
export class FileNativeWorldOperationCoordinator {
|
|
26
26
|
#correctionClaimsRoot;
|
|
27
|
+
#revisionLocksRoot;
|
|
27
28
|
#locksRoot;
|
|
28
29
|
#authorityLocksRoot;
|
|
29
30
|
#now;
|
|
@@ -32,6 +33,7 @@ export class FileNativeWorldOperationCoordinator {
|
|
|
32
33
|
constructor(dataRoot, options = {}) {
|
|
33
34
|
const root = join(resolve(dataRoot), "operations");
|
|
34
35
|
this.#correctionClaimsRoot = join(root, "correction-claims");
|
|
36
|
+
this.#revisionLocksRoot = join(root, "world-revision-locks");
|
|
35
37
|
this.#locksRoot = join(root, "world-locks");
|
|
36
38
|
this.#authorityLocksRoot = join(root, "authority-locks");
|
|
37
39
|
this.#now = options.now ?? Date.now;
|
|
@@ -45,7 +47,8 @@ export class FileNativeWorldOperationCoordinator {
|
|
|
45
47
|
assertNonEmptyIdentity(worldId, "world ID");
|
|
46
48
|
assertNonEmptyIdentity(operationId, "correction operation ID");
|
|
47
49
|
return this.#withWorldLock(worldId, async () => {
|
|
48
|
-
if ((await this.#reconcileCorrectionClaim(worldId)) !== null
|
|
50
|
+
if ((await this.#reconcileCorrectionClaim(worldId)) !== null ||
|
|
51
|
+
(await this.#readRevisionLock(worldId)) !== null)
|
|
49
52
|
return { kind: "busy" };
|
|
50
53
|
const now = this.#now();
|
|
51
54
|
const record = {
|
|
@@ -107,11 +110,70 @@ export class FileNativeWorldOperationCoordinator {
|
|
|
107
110
|
async withExclusiveWorldStateMutation(worldId, action) {
|
|
108
111
|
assertNonEmptyIdentity(worldId, "world ID");
|
|
109
112
|
return this.#withWorldLock(worldId, async () => {
|
|
110
|
-
if ((await this.#reconcileCorrectionClaim(worldId)) !== null
|
|
113
|
+
if ((await this.#reconcileCorrectionClaim(worldId)) !== null ||
|
|
114
|
+
(await this.#readRevisionLock(worldId)) !== null)
|
|
111
115
|
throw new WorldOperationBusyError();
|
|
112
116
|
return action();
|
|
113
117
|
});
|
|
114
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Acquire or resume a persistent revision lock. The lock deliberately has no
|
|
121
|
+
* lease timeout: only Apply or Discard may release its epoch.
|
|
122
|
+
*/
|
|
123
|
+
async acquireWorldRevision(worldId, epochId) {
|
|
124
|
+
assertNonEmptyIdentity(worldId, "world ID");
|
|
125
|
+
assertNonEmptyIdentity(epochId, "world-revision epoch ID");
|
|
126
|
+
return this.#withWorldLock(worldId, async () => {
|
|
127
|
+
const existing = await this.#readRevisionLock(worldId);
|
|
128
|
+
if (existing !== null)
|
|
129
|
+
return existing.epochId === epochId
|
|
130
|
+
? {
|
|
131
|
+
kind: "acquired",
|
|
132
|
+
handle: revisionLockHandle(existing),
|
|
133
|
+
}
|
|
134
|
+
: { kind: "busy", epochId: existing.epochId };
|
|
135
|
+
if ((await this.#reconcileCorrectionClaim(worldId)) !== null)
|
|
136
|
+
return { kind: "busy" };
|
|
137
|
+
const record = {
|
|
138
|
+
schemaVersion: 1,
|
|
139
|
+
kind: "world_revision_lock",
|
|
140
|
+
worldId,
|
|
141
|
+
epochId,
|
|
142
|
+
acquiredAt: this.#now(),
|
|
143
|
+
};
|
|
144
|
+
await publishJson(this.#revisionLockPath(worldId), record);
|
|
145
|
+
return { kind: "acquired", handle: revisionLockHandle(record) };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async readWorldRevisionLock(worldId) {
|
|
149
|
+
assertNonEmptyIdentity(worldId, "world ID");
|
|
150
|
+
const record = await this.#readRevisionLock(worldId);
|
|
151
|
+
return record === null ? null : revisionLockHandle(record);
|
|
152
|
+
}
|
|
153
|
+
async assertWorldRevisionUnlocked(worldId) {
|
|
154
|
+
assertNonEmptyIdentity(worldId, "world ID");
|
|
155
|
+
if ((await this.#readRevisionLock(worldId)) !== null)
|
|
156
|
+
throw new WorldOperationBusyError();
|
|
157
|
+
}
|
|
158
|
+
async withWorldRevisionLock(handle, action) {
|
|
159
|
+
assertWorldRevisionLockHandle(handle);
|
|
160
|
+
return this.#withWorldLock(handle.worldId, async () => {
|
|
161
|
+
const current = await this.#readRevisionLock(handle.worldId);
|
|
162
|
+
if (current?.epochId !== handle.epochId)
|
|
163
|
+
throw new WorldOperationClaimLostError();
|
|
164
|
+
return action();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async releaseWorldRevision(handle) {
|
|
168
|
+
assertWorldRevisionLockHandle(handle);
|
|
169
|
+
await this.#withWorldLock(handle.worldId, async () => {
|
|
170
|
+
const current = await this.#readRevisionLock(handle.worldId);
|
|
171
|
+
if (current?.epochId !== handle.epochId)
|
|
172
|
+
return;
|
|
173
|
+
await rm(this.#revisionLockPath(handle.worldId), { force: true });
|
|
174
|
+
await syncDirectory(this.#revisionLocksRoot);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
115
177
|
async withWorldLocalMetadataMutation(worldId, action) {
|
|
116
178
|
assertNonEmptyIdentity(worldId, "world ID");
|
|
117
179
|
return this.#withWorldLock(worldId, action);
|
|
@@ -134,6 +196,20 @@ export class FileNativeWorldOperationCoordinator {
|
|
|
134
196
|
throw new Error("invalid continuity correction claim");
|
|
135
197
|
return value;
|
|
136
198
|
}
|
|
199
|
+
async #readRevisionLock(worldId) {
|
|
200
|
+
let value;
|
|
201
|
+
try {
|
|
202
|
+
value = JSON.parse(await readFile(this.#revisionLockPath(worldId), "utf8"));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
206
|
+
return null;
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
if (!isWorldRevisionLockRecord(value) || value.worldId !== worldId)
|
|
210
|
+
throw new Error("invalid world revision lock");
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
137
213
|
async #reconcileCorrectionClaim(worldId) {
|
|
138
214
|
const claim = await this.#readCorrectionClaim(worldId);
|
|
139
215
|
if (claim === null)
|
|
@@ -216,6 +292,31 @@ export class FileNativeWorldOperationCoordinator {
|
|
|
216
292
|
#correctionClaimPath(worldId) {
|
|
217
293
|
return join(this.#correctionClaimsRoot, `${digest(worldId)}.json`);
|
|
218
294
|
}
|
|
295
|
+
#revisionLockPath(worldId) {
|
|
296
|
+
return join(this.#revisionLocksRoot, `${digest(worldId)}.json`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isWorldRevisionLockRecord(value) {
|
|
300
|
+
return (record(value) &&
|
|
301
|
+
hasExactKeys(value, [
|
|
302
|
+
"schemaVersion",
|
|
303
|
+
"kind",
|
|
304
|
+
"worldId",
|
|
305
|
+
"epochId",
|
|
306
|
+
"acquiredAt",
|
|
307
|
+
]) &&
|
|
308
|
+
value.schemaVersion === 1 &&
|
|
309
|
+
value.kind === "world_revision_lock" &&
|
|
310
|
+
nonEmptyString(value.worldId) &&
|
|
311
|
+
nonEmptyString(value.epochId) &&
|
|
312
|
+
finiteNumber(value.acquiredAt));
|
|
313
|
+
}
|
|
314
|
+
function assertWorldRevisionLockHandle(handle) {
|
|
315
|
+
assertNonEmptyIdentity(handle.worldId, "world ID");
|
|
316
|
+
assertNonEmptyIdentity(handle.epochId, "world-revision epoch ID");
|
|
317
|
+
}
|
|
318
|
+
function revisionLockHandle(record) {
|
|
319
|
+
return { worldId: record.worldId, epochId: record.epochId };
|
|
219
320
|
}
|
|
220
321
|
function isCorrectionClaimRecord(value) {
|
|
221
322
|
return (record(value) &&
|