narraeon 0.7.6 → 0.7.9
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/runtime/authoring/AuthoringRequestSnapshot.js +14 -1
- package/dist/node/runtime/history/HistoryRendering.js +25 -6
- package/dist/node/runtime/history/HistoryTools.js +31 -0
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +12 -4
- package/dist/node/runtime/play/PlayCallChain.js +10 -21
- package/dist/node/runtime/play/PlayDocumentTools.js +72 -43
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +35 -19
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -12
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +0 -67
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +326 -62
- package/dist/node/runtime/setting/SettingImprovementSession.js +40 -3
- package/dist/node/runtime/world/FileNativeWorldStore.js +13 -3
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +9 -2
- package/dist/node/runtime/world/WorldDocumentStore.js +441 -389
- package/dist/node/runtime/world/WorldLedgerAuthority.js +18 -2
- package/dist/node/runtime/world/WorldStateRevision.js +6 -3
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +61 -17
- package/dist/node/runtime/world-revision/LegacyWorldRevisionEpochUpgrade.js +46 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +83 -30
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +64 -4
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +32 -11
- package/dist/node/shared/default-setting-improvement-prompt.js +23 -8
- package/dist/node/shared/ordered-author-prompts.js +9 -7
- package/dist/node/shared/ordered-play-prompts.js +2 -2
- package/dist/web/assets/index-B2kbaOGe.js +1408 -0
- package/dist/web/assets/{index-BjxBYYQi.css → index-DH2BVsYd.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Cj2JYCia.js +0 -1408
|
@@ -52,6 +52,7 @@ export class WorldDocumentStore {
|
|
|
52
52
|
original: file,
|
|
53
53
|
lastCommandIndex: null,
|
|
54
54
|
}));
|
|
55
|
+
const editOutcomes = [];
|
|
55
56
|
for (const [commandIndex, command] of request.commands.entries()) {
|
|
56
57
|
activeCommandIndex = commandIndex;
|
|
57
58
|
if (!isRecord(command) || typeof command.kind !== "string")
|
|
@@ -342,201 +343,50 @@ export class WorldDocumentStore {
|
|
|
342
343
|
const target = this.#resolveRevisionTarget(current, working, command.document);
|
|
343
344
|
if ("diagnostics" in target)
|
|
344
345
|
return this.#revisionFailure(commandIndex, target.diagnostics);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
logicalPath: target.entry.file.path,
|
|
371
|
-
documentId: target.entry.descriptor.documentId,
|
|
372
|
-
message: "A patch edit must be a closed object with op",
|
|
373
|
-
});
|
|
374
|
-
if (edit.op === "set_metadata") {
|
|
375
|
-
if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]))
|
|
376
|
-
return this.#revisionFailure(commandIndex, {
|
|
377
|
-
code: "document_header_invalid",
|
|
378
|
-
logicalPath: target.entry.file.path,
|
|
379
|
-
documentId: target.entry.descriptor.documentId,
|
|
380
|
-
message: "set_metadata accepts only op, title, summary, and aliases",
|
|
381
|
-
});
|
|
382
|
-
const resolved = resolveDocumentMetadataRevision(metadata, edit);
|
|
383
|
-
if ("problems" in resolved)
|
|
384
|
-
return this.#revisionFailure(commandIndex, {
|
|
385
|
-
code: "document_header_invalid",
|
|
386
|
-
logicalPath: target.entry.file.path,
|
|
387
|
-
documentId: target.entry.descriptor.documentId,
|
|
388
|
-
message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
|
|
389
|
-
});
|
|
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]);
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (!["add", "replace", "append", "remove"].includes(edit.op) ||
|
|
397
|
-
!validYamlLocator(edit.locator) ||
|
|
398
|
-
edit.locator.yaml[0] === "$document")
|
|
399
|
-
return this.#revisionFailure(commandIndex, {
|
|
400
|
-
code: "locator_invalid",
|
|
401
|
-
logicalPath: target.entry.file.path,
|
|
402
|
-
documentId: target.entry.descriptor?.documentId,
|
|
403
|
-
...(isRecord(edit) && isRecord(edit.locator)
|
|
404
|
-
? {
|
|
405
|
-
locator: copyLocator(edit.locator),
|
|
406
|
-
}
|
|
407
|
-
: {}),
|
|
408
|
-
message: "A YAML edit requires an exact locator outside the technical header",
|
|
409
|
-
});
|
|
410
|
-
const locator = [...edit.locator.yaml];
|
|
411
|
-
if (locator.length === 0) {
|
|
412
|
-
if ((edit.op !== "add" && edit.op !== "replace") ||
|
|
413
|
-
!hasOnlyKeys(edit, ["op", "locator", "value"]))
|
|
414
|
-
return this.#revisionFailure(commandIndex, {
|
|
415
|
-
code: "locator_invalid",
|
|
416
|
-
logicalPath: target.entry.file.path,
|
|
417
|
-
documentId: target.entry.descriptor.documentId,
|
|
418
|
-
locator: { yaml: [] },
|
|
419
|
-
message: "A whole YAML body supports only add or replace",
|
|
420
|
-
});
|
|
421
|
-
const value = this.#resolveRevisionYamlValue(edit.value, current, working, { referenceInput: "persistent_or_explicit" }, []);
|
|
422
|
-
if ("diagnostics" in value)
|
|
423
|
-
return this.#revisionFailure(commandIndex, value.diagnostics.map((problem) => ({
|
|
424
|
-
...problem,
|
|
425
|
-
logicalPath: target.entry.file.path,
|
|
426
|
-
documentId: target.entry.descriptor.documentId,
|
|
427
|
-
})));
|
|
428
|
-
if (!isRecord(value.value) ||
|
|
429
|
-
Object.hasOwn(value.value, "$document"))
|
|
430
|
-
return this.#revisionFailure(commandIndex, {
|
|
431
|
-
code: "yaml_invalid",
|
|
432
|
-
logicalPath: target.entry.file.path,
|
|
433
|
-
documentId: target.entry.descriptor.documentId,
|
|
434
|
-
locator: { yaml: [] },
|
|
435
|
-
message: "A whole YAML body must be a map without $document",
|
|
436
|
-
});
|
|
437
|
-
const currentValue = document.toJS({
|
|
438
|
-
maxAliasCount: 0,
|
|
439
|
-
});
|
|
440
|
-
if (!isRecord(currentValue) ||
|
|
441
|
-
!isRecord(currentValue.$document))
|
|
442
|
-
return this.#revisionFailure(commandIndex, {
|
|
443
|
-
code: "document_header_invalid",
|
|
444
|
-
logicalPath: target.entry.file.path,
|
|
445
|
-
documentId: target.entry.descriptor.documentId,
|
|
446
|
-
message: "YAML technical header could not be preserved",
|
|
447
|
-
});
|
|
448
|
-
if (edit.op === "add" &&
|
|
449
|
-
Object.keys(currentValue).some((key) => key !== "$document"))
|
|
450
|
-
return this.#revisionFailure(commandIndex, {
|
|
451
|
-
code: "locator_invalid",
|
|
452
|
-
logicalPath: target.entry.file.path,
|
|
453
|
-
documentId: target.entry.descriptor.documentId,
|
|
454
|
-
locator: { yaml: [] },
|
|
455
|
-
message: "Adding a whole YAML body requires the current body to be empty",
|
|
456
|
-
});
|
|
457
|
-
document = parseDocument(stringify({ $document: currentValue.$document, ...value.value }, { indent: 2, lineWidth: 0 }), { schema: "core", uniqueKeys: true, strict: true });
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
const exists = document.hasIn(locator);
|
|
461
|
-
if (edit.op === "remove") {
|
|
462
|
-
if (!hasOnlyKeys(edit, ["op", "locator"]) || !exists)
|
|
463
|
-
return this.#revisionFailure(commandIndex, {
|
|
464
|
-
code: "locator_not_found",
|
|
465
|
-
logicalPath: target.entry.file.path,
|
|
466
|
-
documentId: target.entry.descriptor.documentId,
|
|
467
|
-
locator: { yaml: locator },
|
|
468
|
-
message: "YAML remove target must exist",
|
|
469
|
-
});
|
|
470
|
-
document.deleteIn(locator);
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
if (!hasOnlyKeys(edit, ["op", "locator", "value"]))
|
|
474
|
-
return this.#revisionFailure(commandIndex, {
|
|
475
|
-
code: "query_invalid",
|
|
476
|
-
logicalPath: target.entry.file.path,
|
|
477
|
-
documentId: target.entry.descriptor.documentId,
|
|
478
|
-
locator: { yaml: locator },
|
|
479
|
-
message: "YAML value-edit arguments are invalid",
|
|
480
|
-
});
|
|
481
|
-
let valueLocator = locator;
|
|
482
|
-
if (edit.op === "append") {
|
|
483
|
-
const sequence = document.getIn(locator, true);
|
|
484
|
-
if (!isSeq(sequence))
|
|
485
|
-
return this.#revisionFailure(commandIndex, {
|
|
486
|
-
code: "locator_invalid",
|
|
487
|
-
logicalPath: target.entry.file.path,
|
|
488
|
-
documentId: target.entry.descriptor.documentId,
|
|
489
|
-
locator: { yaml: locator },
|
|
490
|
-
message: "YAML append target must be a sequence",
|
|
491
|
-
});
|
|
492
|
-
valueLocator = [...locator, sequence.items.length];
|
|
493
|
-
}
|
|
494
|
-
const value = this.#resolveRevisionYamlValue(edit.value, current, working, { referenceInput: "persistent_or_explicit" }, valueLocator);
|
|
495
|
-
if ("diagnostics" in value)
|
|
496
|
-
return this.#revisionFailure(commandIndex, value.diagnostics.map((problem) => ({
|
|
497
|
-
...problem,
|
|
498
|
-
logicalPath: target.entry.file.path,
|
|
499
|
-
documentId: target.entry.descriptor?.documentId,
|
|
500
|
-
})));
|
|
501
|
-
if (edit.op === "add") {
|
|
502
|
-
const parent = locator.length === 1
|
|
503
|
-
? document.contents
|
|
504
|
-
: document.getIn(locator.slice(0, -1), true);
|
|
505
|
-
const last = locator.at(-1);
|
|
506
|
-
const parentAcceptsLocator = (isMap(parent) && typeof last === "string") ||
|
|
507
|
-
(isSeq(parent) && typeof last === "number");
|
|
508
|
-
if (exists || !parentAcceptsLocator)
|
|
509
|
-
return this.#revisionFailure(commandIndex, {
|
|
510
|
-
code: "locator_invalid",
|
|
511
|
-
logicalPath: target.entry.file.path,
|
|
512
|
-
documentId: target.entry.descriptor.documentId,
|
|
513
|
-
locator: { yaml: locator },
|
|
514
|
-
message: "YAML add target must not exist, and its parent map or sequence must exist",
|
|
515
|
-
});
|
|
516
|
-
document.setIn(locator, value.value);
|
|
517
|
-
}
|
|
518
|
-
else if (edit.op === "replace") {
|
|
519
|
-
if (!exists)
|
|
520
|
-
return this.#revisionFailure(commandIndex, {
|
|
521
|
-
code: "locator_not_found",
|
|
522
|
-
logicalPath: target.entry.file.path,
|
|
523
|
-
documentId: target.entry.descriptor.documentId,
|
|
524
|
-
locator: { yaml: locator },
|
|
525
|
-
message: "YAML replace target must exist",
|
|
526
|
-
});
|
|
527
|
-
document.setIn(locator, value.value);
|
|
528
|
-
}
|
|
529
|
-
else
|
|
530
|
-
document.addIn(locator, value.value);
|
|
346
|
+
const logicalPath = target.working.file.path;
|
|
347
|
+
const probe = (contents) => this.#localRevisionDiagnostics(WorldDocumentStore.open({
|
|
348
|
+
layout: this.layout,
|
|
349
|
+
files: working.map(({ file }) => file.path === logicalPath
|
|
350
|
+
? { path: logicalPath, contents }
|
|
351
|
+
: file),
|
|
352
|
+
}), logicalPath);
|
|
353
|
+
const resolveValue = (value, locator) => this.#resolveRevisionYamlValue(value, current, working, { referenceInput: "persistent_or_explicit" }, locator);
|
|
354
|
+
// Each edit settles on its own: it is applied to the text left by the
|
|
355
|
+
// edits before it, validated alone, and skipped if it cannot apply.
|
|
356
|
+
let contents = target.working.file.contents;
|
|
357
|
+
let metadata = {
|
|
358
|
+
title: target.entry.descriptor.title,
|
|
359
|
+
summary: target.entry.descriptor.summary,
|
|
360
|
+
aliases: [...target.entry.descriptor.aliases],
|
|
361
|
+
};
|
|
362
|
+
const outcomes = [];
|
|
363
|
+
for (const [editIndex, edit] of command.edits.entries()) {
|
|
364
|
+
let settled = target.entry.codec === "markdown"
|
|
365
|
+
? applyMarkdownRevisionEdit(contents, metadata, edit)
|
|
366
|
+
: applyYamlRevisionEdit(contents, metadata, edit, resolveValue);
|
|
367
|
+
if (settled.kind === "applied") {
|
|
368
|
+
const problems = probe(settled.contents);
|
|
369
|
+
if (problems.length > 0)
|
|
370
|
+
settled = { kind: "failed", diagnostic: problems[0] };
|
|
531
371
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
372
|
+
if (settled.kind === "applied") {
|
|
373
|
+
contents = settled.contents;
|
|
374
|
+
metadata = settled.metadata;
|
|
375
|
+
}
|
|
376
|
+
outcomes.push(revisionEditOutcome(commandIndex, editIndex, settled, {
|
|
377
|
+
logicalPath,
|
|
378
|
+
documentId: target.entry.descriptor.documentId,
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
if (outcomes.some(({ status }) => status === "applied")) {
|
|
382
|
+
target.working.file.contents = contents;
|
|
383
|
+
delete target.working.file.encoding;
|
|
536
384
|
}
|
|
537
|
-
delete target.working.file.encoding;
|
|
538
385
|
target.working.lastCommandIndex = commandIndex;
|
|
539
|
-
touchedPath =
|
|
386
|
+
touchedPath = logicalPath;
|
|
387
|
+
editOutcomes.push(...outcomes);
|
|
388
|
+
if (outcomes.every(({ status }) => status === "failed"))
|
|
389
|
+
return this.#revisionFailure(commandIndex, outcomes.flatMap(({ diagnostic }) => diagnostic === undefined ? [] : [diagnostic]), outcomes);
|
|
540
390
|
}
|
|
541
391
|
else
|
|
542
392
|
return this.#revisionFailure(commandIndex, {
|
|
@@ -565,6 +415,7 @@ export class WorldDocumentStore {
|
|
|
565
415
|
const commandIndex = working.find(({ file }) => file.path === problem.logicalPath)?.lastCommandIndex;
|
|
566
416
|
return revisionDiagnostic(commandIndex ?? null, problem);
|
|
567
417
|
})),
|
|
418
|
+
editOutcomes: freeze([...editOutcomes]),
|
|
568
419
|
});
|
|
569
420
|
const changes = working
|
|
570
421
|
.filter(({ file, original }) => file.path !== original?.path ||
|
|
@@ -603,6 +454,7 @@ export class WorldDocumentStore {
|
|
|
603
454
|
files: snapshot.files,
|
|
604
455
|
diagnostics: freeze([]),
|
|
605
456
|
changes: freeze(changes),
|
|
457
|
+
editOutcomes: freeze(editOutcomes),
|
|
606
458
|
});
|
|
607
459
|
}
|
|
608
460
|
catch {
|
|
@@ -612,7 +464,7 @@ export class WorldDocumentStore {
|
|
|
612
464
|
});
|
|
613
465
|
}
|
|
614
466
|
}
|
|
615
|
-
#revisionFailure(commandIndex, problem) {
|
|
467
|
+
#revisionFailure(commandIndex, problem, editOutcomes = []) {
|
|
616
468
|
const diagnostics = Array.isArray(problem)
|
|
617
469
|
? problem
|
|
618
470
|
: [problem];
|
|
@@ -622,6 +474,7 @@ export class WorldDocumentStore {
|
|
|
622
474
|
sourceSnapshotId: this.id,
|
|
623
475
|
snapshotStatus: this.status,
|
|
624
476
|
diagnostics: freeze(diagnostics.map((candidate) => revisionDiagnostic(commandIndex, candidate))),
|
|
477
|
+
editOutcomes: freeze([...editOutcomes]),
|
|
625
478
|
});
|
|
626
479
|
}
|
|
627
480
|
#resolveRevisionTarget(snapshot, working, target) {
|
|
@@ -2247,205 +2100,362 @@ function scanMarkdownHeadings(source, bodyStart) {
|
|
|
2247
2100
|
}
|
|
2248
2101
|
return { headings, hasUnclosedFence: fence !== null };
|
|
2249
2102
|
}
|
|
2250
|
-
function
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2103
|
+
function revisionEditOutcome(commandIndex, editIndex, result, document) {
|
|
2104
|
+
return freeze(result.kind === "applied"
|
|
2105
|
+
? { commandIndex, editIndex, status: "applied" }
|
|
2106
|
+
: {
|
|
2107
|
+
commandIndex,
|
|
2108
|
+
editIndex,
|
|
2109
|
+
status: result.kind,
|
|
2110
|
+
diagnostic: freeze({ ...result.diagnostic, ...document }),
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
function editFailed(problem) {
|
|
2114
|
+
return { kind: "failed", diagnostic: problem };
|
|
2115
|
+
}
|
|
2116
|
+
function editTargetAbsent(locator) {
|
|
2117
|
+
return {
|
|
2118
|
+
kind: "no_op",
|
|
2119
|
+
diagnostic: diagnostic({
|
|
2120
|
+
code: "locator_not_found",
|
|
2121
|
+
locator,
|
|
2122
|
+
message: "target absent",
|
|
2123
|
+
}),
|
|
2256
2124
|
};
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2125
|
+
}
|
|
2126
|
+
function applyYamlRevisionEdit(contents, metadata, edit, resolveValue) {
|
|
2127
|
+
if (!isRecord(edit) || typeof edit.op !== "string")
|
|
2128
|
+
return editFailed(diagnostic({
|
|
2129
|
+
code: "query_invalid",
|
|
2130
|
+
message: "A patch edit must be a closed object with op",
|
|
2131
|
+
}));
|
|
2132
|
+
const document = parseDocument(contents, {
|
|
2133
|
+
schema: "core",
|
|
2134
|
+
uniqueKeys: true,
|
|
2135
|
+
strict: true,
|
|
2136
|
+
});
|
|
2137
|
+
const serialized = (nextMetadata) => ({
|
|
2138
|
+
kind: "applied",
|
|
2139
|
+
contents: document.toString({ indent: 2, lineWidth: 0 }),
|
|
2140
|
+
metadata: nextMetadata,
|
|
2141
|
+
});
|
|
2142
|
+
if (edit.op === "set_metadata") {
|
|
2143
|
+
if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]))
|
|
2144
|
+
return editFailed(diagnostic({
|
|
2145
|
+
code: "document_header_invalid",
|
|
2146
|
+
message: "set_metadata accepts only op, title, summary, and aliases",
|
|
2147
|
+
}));
|
|
2148
|
+
const resolved = resolveDocumentMetadataRevision(metadata, edit);
|
|
2149
|
+
if ("problems" in resolved)
|
|
2150
|
+
return editFailed(diagnostic({
|
|
2151
|
+
code: "document_header_invalid",
|
|
2152
|
+
message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
|
|
2153
|
+
}));
|
|
2154
|
+
document.setIn(["$document", "title"], resolved.metadata.title);
|
|
2155
|
+
document.setIn(["$document", "summary"], resolved.metadata.summary);
|
|
2156
|
+
document.setIn(["$document", "aliases"], [...resolved.metadata.aliases]);
|
|
2157
|
+
return serialized(resolved.metadata);
|
|
2158
|
+
}
|
|
2159
|
+
if (!["add", "replace", "append", "remove"].includes(edit.op) ||
|
|
2160
|
+
!validYamlLocator(edit.locator) ||
|
|
2161
|
+
edit.locator.yaml[0] === "$document")
|
|
2162
|
+
return editFailed(diagnostic({
|
|
2163
|
+
code: "locator_invalid",
|
|
2164
|
+
...(isRecord(edit.locator)
|
|
2165
|
+
? {
|
|
2166
|
+
locator: copyLocator(edit.locator),
|
|
2167
|
+
}
|
|
2168
|
+
: {}),
|
|
2169
|
+
message: "A YAML edit requires an exact locator outside the technical header",
|
|
2170
|
+
}));
|
|
2171
|
+
const locator = [...edit.locator.yaml];
|
|
2172
|
+
if (locator.length === 0) {
|
|
2173
|
+
if ((edit.op !== "add" && edit.op !== "replace") ||
|
|
2174
|
+
!hasOnlyKeys(edit, ["op", "locator", "value"]))
|
|
2175
|
+
return editFailed(diagnostic({
|
|
2176
|
+
code: "locator_invalid",
|
|
2177
|
+
locator: { yaml: [] },
|
|
2178
|
+
message: "A whole YAML body supports only add or replace",
|
|
2179
|
+
}));
|
|
2180
|
+
const value = resolveValue(edit.value, []);
|
|
2181
|
+
if ("diagnostics" in value)
|
|
2182
|
+
return editFailed(value.diagnostics[0]);
|
|
2183
|
+
if (!isRecord(value.value) || Object.hasOwn(value.value, "$document"))
|
|
2184
|
+
return editFailed(diagnostic({
|
|
2185
|
+
code: "yaml_invalid",
|
|
2186
|
+
locator: { yaml: [] },
|
|
2187
|
+
message: "A whole YAML body must be a map without $document",
|
|
2188
|
+
}));
|
|
2189
|
+
const currentValue = document.toJS({ maxAliasCount: 0 });
|
|
2190
|
+
if (!isRecord(currentValue) || !isRecord(currentValue.$document))
|
|
2191
|
+
return editFailed(diagnostic({
|
|
2192
|
+
code: "document_header_invalid",
|
|
2193
|
+
message: "YAML technical header could not be preserved",
|
|
2194
|
+
}));
|
|
2195
|
+
const body = parseDocument(stringify({ $document: currentValue.$document, ...value.value }, { indent: 2, lineWidth: 0 }), { schema: "core", uniqueKeys: true, strict: true });
|
|
2196
|
+
return {
|
|
2197
|
+
kind: "applied",
|
|
2198
|
+
contents: body.toString({ indent: 2, lineWidth: 0 }),
|
|
2199
|
+
metadata,
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
if (edit.op === "remove") {
|
|
2203
|
+
if (!hasOnlyKeys(edit, ["op", "locator"]))
|
|
2204
|
+
return editFailed(diagnostic({
|
|
2205
|
+
code: "query_invalid",
|
|
2206
|
+
locator: { yaml: locator },
|
|
2207
|
+
message: "YAML remove accepts only op and locator",
|
|
2208
|
+
}));
|
|
2209
|
+
if (!document.hasIn(locator))
|
|
2210
|
+
return editTargetAbsent({ yaml: locator });
|
|
2211
|
+
document.deleteIn(locator);
|
|
2212
|
+
return serialized(metadata);
|
|
2213
|
+
}
|
|
2214
|
+
if (!hasOnlyKeys(edit, ["op", "locator", "value"]))
|
|
2215
|
+
return editFailed(diagnostic({
|
|
2216
|
+
code: "query_invalid",
|
|
2217
|
+
locator: { yaml: locator },
|
|
2218
|
+
message: "YAML value-edit arguments are invalid",
|
|
2219
|
+
}));
|
|
2220
|
+
if (edit.op === "append") {
|
|
2221
|
+
const sequence = document.getIn(locator, true);
|
|
2222
|
+
if (!isSeq(sequence))
|
|
2223
|
+
return editFailed(diagnostic({
|
|
2224
|
+
code: "locator_invalid",
|
|
2225
|
+
locator: { yaml: locator },
|
|
2226
|
+
message: "YAML append target must be a sequence",
|
|
2227
|
+
}));
|
|
2228
|
+
const value = resolveValue(edit.value, [...locator, sequence.items.length]);
|
|
2229
|
+
if ("diagnostics" in value)
|
|
2230
|
+
return editFailed(value.diagnostics[0]);
|
|
2231
|
+
sequence.add(document.createNode(value.value));
|
|
2232
|
+
return serialized(metadata);
|
|
2233
|
+
}
|
|
2234
|
+
const value = resolveValue(edit.value, locator);
|
|
2235
|
+
if ("diagnostics" in value)
|
|
2236
|
+
return editFailed(value.diagnostics[0]);
|
|
2237
|
+
if (!upsertYamlValue(document, locator, value.value))
|
|
2238
|
+
return editFailed(diagnostic({
|
|
2239
|
+
code: "locator_invalid",
|
|
2240
|
+
locator: { yaml: locator },
|
|
2241
|
+
message: "YAML add or replace parent must be a map or sequence",
|
|
2242
|
+
}));
|
|
2243
|
+
return serialized(metadata);
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Writes value at locator, creating what is missing: absent map keys and
|
|
2247
|
+
* parent maps are created, and a sequence index at or past the end appends.
|
|
2248
|
+
* Returns false when an existing parent is neither a map nor a sequence.
|
|
2249
|
+
*/
|
|
2250
|
+
function upsertYamlValue(document, locator, value) {
|
|
2251
|
+
let node = document.contents;
|
|
2252
|
+
for (const [depth, segment] of locator.entries()) {
|
|
2253
|
+
const rest = locator.slice(depth + 1);
|
|
2254
|
+
if (isMap(node) && typeof segment === "string") {
|
|
2255
|
+
if (rest.length === 0) {
|
|
2256
|
+
node.set(segment, document.createNode(value));
|
|
2257
|
+
return true;
|
|
2326
2258
|
}
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
return
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
}),
|
|
2336
|
-
],
|
|
2337
|
-
};
|
|
2338
|
-
const nextH2 = headings.find(({ level, range }) => level === 2 && range.start.offset > h1.range.start.offset);
|
|
2339
|
-
const end = nextH2?.range.start.offset ?? contents.length;
|
|
2340
|
-
contents = `${contents.slice(0, h1.range.end.offset)}\n\n${edit.markdown.trim()}\n\n${contents.slice(end)}`;
|
|
2341
|
-
continue;
|
|
2342
|
-
}
|
|
2343
|
-
if (![
|
|
2344
|
-
"replace_section",
|
|
2345
|
-
"add_section",
|
|
2346
|
-
"rename_section",
|
|
2347
|
-
"remove_section",
|
|
2348
|
-
].includes(edit.op) ||
|
|
2349
|
-
!validMarkdownLocator(edit.locator))
|
|
2350
|
-
return {
|
|
2351
|
-
diagnostics: [
|
|
2352
|
-
diagnostic({
|
|
2353
|
-
code: "locator_invalid",
|
|
2354
|
-
...(isRecord(edit.locator)
|
|
2355
|
-
? {
|
|
2356
|
-
locator: copyLocator(edit.locator),
|
|
2357
|
-
}
|
|
2358
|
-
: {}),
|
|
2359
|
-
message: "A Markdown patch requires an exact heading locator",
|
|
2360
|
-
}),
|
|
2361
|
-
],
|
|
2362
|
-
};
|
|
2363
|
-
const locator = { markdown: [...edit.locator.markdown] };
|
|
2364
|
-
const section = markdownRevisionSection(contents, locator.markdown);
|
|
2365
|
-
if ("diagnostics" in section)
|
|
2366
|
-
return section;
|
|
2367
|
-
if (edit.op === "remove_section") {
|
|
2368
|
-
if (!hasOnlyKeys(edit, ["op", "locator"]))
|
|
2369
|
-
return {
|
|
2370
|
-
diagnostics: [
|
|
2371
|
-
diagnostic({
|
|
2372
|
-
code: "query_invalid",
|
|
2373
|
-
locator,
|
|
2374
|
-
message: "remove_section contains undeclared arguments",
|
|
2375
|
-
}),
|
|
2376
|
-
],
|
|
2377
|
-
};
|
|
2378
|
-
contents = `${contents.slice(0, section.start)}${contents.slice(section.rawEnd)}`;
|
|
2379
|
-
continue;
|
|
2259
|
+
const child = node.get(segment, true);
|
|
2260
|
+
if (child === undefined) {
|
|
2261
|
+
node.set(segment, document.createNode(yamlTailValue(rest, value)));
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
if (!isMap(child) && !isSeq(child))
|
|
2265
|
+
return false;
|
|
2266
|
+
node = child;
|
|
2380
2267
|
}
|
|
2381
|
-
if (
|
|
2382
|
-
if (
|
|
2383
|
-
|
|
2384
|
-
return
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
continue;
|
|
2268
|
+
else if (isSeq(node) && typeof segment === "number") {
|
|
2269
|
+
if (segment >= node.items.length) {
|
|
2270
|
+
node.items.push(document.createNode(yamlTailValue(rest, value)));
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
if (rest.length === 0) {
|
|
2274
|
+
node.items[segment] = document.createNode(value);
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
const child = node.items[segment];
|
|
2278
|
+
if (!isMap(child) && !isSeq(child))
|
|
2279
|
+
return false;
|
|
2280
|
+
node = child;
|
|
2395
2281
|
}
|
|
2396
|
-
|
|
2282
|
+
else
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
/** The value a missing path creates: maps for keys, a one-item list for indexes. */
|
|
2288
|
+
function yamlTailValue(path, value) {
|
|
2289
|
+
return path.reduceRight((child, segment) => typeof segment === "number" ? [child] : { [segment]: child }, value);
|
|
2290
|
+
}
|
|
2291
|
+
function applyMarkdownRevisionEdit(source, metadata, edit) {
|
|
2292
|
+
if (!isRecord(edit) || typeof edit.op !== "string")
|
|
2293
|
+
return editFailed(diagnostic({
|
|
2294
|
+
code: "query_invalid",
|
|
2295
|
+
message: "A Markdown patch edit must be a closed object with op",
|
|
2296
|
+
}));
|
|
2297
|
+
// Every applied Markdown edit keeps a single trailing newline.
|
|
2298
|
+
const applied = (contents, nextMetadata) => ({
|
|
2299
|
+
kind: "applied",
|
|
2300
|
+
contents: `${contents.trimEnd()}\n`,
|
|
2301
|
+
metadata: nextMetadata,
|
|
2302
|
+
});
|
|
2303
|
+
if (edit.op === "set_metadata") {
|
|
2304
|
+
if (!hasOnlyKeys(edit, ["op", "title", "summary", "aliases"]))
|
|
2305
|
+
return editFailed(diagnostic({
|
|
2306
|
+
code: "document_header_invalid",
|
|
2307
|
+
message: "set_metadata accepts only op, title, summary, and aliases",
|
|
2308
|
+
}));
|
|
2309
|
+
const resolved = resolveDocumentMetadataRevision(metadata, edit);
|
|
2310
|
+
if ("problems" in resolved)
|
|
2311
|
+
return editFailed(diagnostic({
|
|
2312
|
+
code: "document_header_invalid",
|
|
2313
|
+
message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
|
|
2314
|
+
}));
|
|
2315
|
+
const envelope = markdownEnvelope(source);
|
|
2316
|
+
if ("diagnostics" in envelope)
|
|
2317
|
+
return editFailed(envelope.diagnostics[0]);
|
|
2318
|
+
envelope.header.setIn(["$document", "title"], resolved.metadata.title);
|
|
2319
|
+
envelope.header.setIn(["$document", "summary"], resolved.metadata.summary);
|
|
2320
|
+
envelope.header.setIn(["$document", "aliases"], [...resolved.metadata.aliases]);
|
|
2321
|
+
const body = source.slice(envelope.bodyStart);
|
|
2322
|
+
const firstHeading = scanMarkdownHeadings(body, 0).headings[0];
|
|
2323
|
+
if (firstHeading?.level !== 1)
|
|
2324
|
+
return editFailed(diagnostic({
|
|
2325
|
+
code: "markdown_invalid",
|
|
2326
|
+
message: "Markdown body is missing an updateable h1",
|
|
2327
|
+
}));
|
|
2328
|
+
const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${resolved.metadata.title}${body.slice(firstHeading.range.end.offset)}`;
|
|
2329
|
+
return applied(`---\n${envelope.header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${nextBody}`, resolved.metadata);
|
|
2330
|
+
}
|
|
2331
|
+
if (edit.op === "replace_body" || edit.op === "replace_preamble") {
|
|
2332
|
+
if (!hasOnlyKeys(edit, ["op", "markdown"]) ||
|
|
2397
2333
|
typeof edit.markdown !== "string")
|
|
2398
|
-
return {
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
};
|
|
2407
|
-
const
|
|
2408
|
-
const
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2334
|
+
return editFailed(diagnostic({
|
|
2335
|
+
code: "query_invalid",
|
|
2336
|
+
message: "A Markdown body edit requires markdown source",
|
|
2337
|
+
}));
|
|
2338
|
+
const envelope = markdownEnvelope(source);
|
|
2339
|
+
if ("diagnostics" in envelope)
|
|
2340
|
+
return editFailed(envelope.diagnostics[0]);
|
|
2341
|
+
if (edit.op === "replace_body")
|
|
2342
|
+
return applied(`${source.slice(0, envelope.bodyStart)}${edit.markdown.trimEnd()}\n`, metadata);
|
|
2343
|
+
const headings = scanMarkdownHeadings(source, envelope.bodyStart).headings;
|
|
2344
|
+
const h1 = headings.find(({ level }) => level === 1);
|
|
2345
|
+
if (h1 === undefined)
|
|
2346
|
+
return editFailed(diagnostic({
|
|
2347
|
+
code: "markdown_invalid",
|
|
2348
|
+
message: "Markdown body is missing an updateable h1",
|
|
2349
|
+
}));
|
|
2350
|
+
const nextH2 = headings.find(({ level, range }) => level === 2 && range.start.offset > h1.range.start.offset);
|
|
2351
|
+
const end = nextH2?.range.start.offset ?? source.length;
|
|
2352
|
+
return applied(`${source.slice(0, h1.range.end.offset)}\n\n${edit.markdown.trim()}\n\n${source.slice(end)}`, metadata);
|
|
2353
|
+
}
|
|
2354
|
+
if (![
|
|
2355
|
+
"replace_section",
|
|
2356
|
+
"add_section",
|
|
2357
|
+
"rename_section",
|
|
2358
|
+
"remove_section",
|
|
2359
|
+
].includes(edit.op) ||
|
|
2360
|
+
!validMarkdownLocator(edit.locator, edit.op === "add_section"))
|
|
2361
|
+
return editFailed(diagnostic({
|
|
2362
|
+
code: "locator_invalid",
|
|
2363
|
+
...(isRecord(edit.locator)
|
|
2364
|
+
? {
|
|
2365
|
+
locator: copyLocator(edit.locator),
|
|
2366
|
+
}
|
|
2367
|
+
: {}),
|
|
2368
|
+
message: "A Markdown patch requires an exact heading locator",
|
|
2369
|
+
}));
|
|
2370
|
+
const locator = { markdown: [...edit.locator.markdown] };
|
|
2371
|
+
if (edit.op === "remove_section") {
|
|
2372
|
+
if (!hasOnlyKeys(edit, ["op", "locator"]))
|
|
2373
|
+
return editFailed(diagnostic({
|
|
2374
|
+
code: "query_invalid",
|
|
2375
|
+
locator,
|
|
2376
|
+
message: "remove_section contains undeclared arguments",
|
|
2377
|
+
}));
|
|
2378
|
+
const section = markdownRevisionSection(source, locator.markdown);
|
|
2379
|
+
if ("diagnostics" in section) {
|
|
2380
|
+
const [problem] = section.diagnostics;
|
|
2381
|
+
return problem?.code === "locator_not_found"
|
|
2382
|
+
? editTargetAbsent(locator)
|
|
2383
|
+
: editFailed(problem);
|
|
2430
2384
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2385
|
+
return applied(`${source.slice(0, section.start)}${source.slice(section.rawEnd)}`, metadata);
|
|
2386
|
+
}
|
|
2387
|
+
if (edit.op === "rename_section") {
|
|
2388
|
+
if (!hasOnlyKeys(edit, ["op", "locator", "title"]) ||
|
|
2389
|
+
!validMarkdownHeadingTitle(edit.title))
|
|
2390
|
+
return editFailed(diagnostic({
|
|
2391
|
+
code: "query_invalid",
|
|
2392
|
+
locator,
|
|
2393
|
+
message: "rename_section requires a valid new heading",
|
|
2394
|
+
}));
|
|
2395
|
+
const section = markdownRevisionSection(source, locator.markdown);
|
|
2396
|
+
if ("diagnostics" in section)
|
|
2397
|
+
return editFailed(section.diagnostics[0]);
|
|
2398
|
+
return applied(`${source.slice(0, section.start)}${"#".repeat(section.level)} ${edit.title}${source.slice(section.headingEnd)}`, metadata);
|
|
2399
|
+
}
|
|
2400
|
+
if (!hasOnlyKeys(edit, ["op", "locator", "markdown"]) ||
|
|
2401
|
+
typeof edit.markdown !== "string")
|
|
2402
|
+
return editFailed(diagnostic({
|
|
2403
|
+
code: "query_invalid",
|
|
2404
|
+
locator,
|
|
2405
|
+
message: "A Markdown section edit requires a complete Markdown block",
|
|
2406
|
+
}));
|
|
2407
|
+
const block = edit.markdown.trim();
|
|
2408
|
+
const { headings, hasUnclosedFence } = scanMarkdownHeadings(block, 0);
|
|
2409
|
+
const first = headings[0];
|
|
2410
|
+
const escapesSectionSubtree = headings
|
|
2411
|
+
.slice(1)
|
|
2412
|
+
.some(({ level }) => first !== undefined && level <= first.level);
|
|
2413
|
+
const blockIsSubtree = first?.range.start.offset === 0 &&
|
|
2414
|
+
!escapesSectionSubtree &&
|
|
2415
|
+
!hasUnclosedFence;
|
|
2416
|
+
const requestedTitle = locator.markdown.at(-1)?.normalize("NFKC");
|
|
2417
|
+
if (edit.op === "replace_section") {
|
|
2418
|
+
const section = markdownRevisionSection(source, locator.markdown);
|
|
2419
|
+
if ("diagnostics" in section) {
|
|
2420
|
+
if (section.diagnostics[0]?.code !== "locator_not_found")
|
|
2421
|
+
return editFailed(section.diagnostics[0]);
|
|
2422
|
+
// A heading that does not exist yet is added at the end of its parent.
|
|
2423
|
+
const parent = markdownRevisionSection(source, locator.markdown.slice(0, -1));
|
|
2424
|
+
if ("diagnostics" in parent)
|
|
2425
|
+
return editFailed(parent.diagnostics[0]);
|
|
2426
|
+
if (!blockIsSubtree ||
|
|
2427
|
+
first.level !== parent.level + 1 ||
|
|
2428
|
+
first.title.normalize("NFKC") !== requestedTitle)
|
|
2429
|
+
return editFailed(diagnostic({
|
|
2430
|
+
code: "locator_invalid",
|
|
2431
|
+
locator,
|
|
2432
|
+
message: "replace_section new heading must keep the requested title and sit one level below its parent",
|
|
2433
|
+
}));
|
|
2434
|
+
return applied(insertMarkdownBlock(source, parent.trimmedEnd, block), metadata);
|
|
2446
2435
|
}
|
|
2436
|
+
if (!blockIsSubtree ||
|
|
2437
|
+
first.level !== section.level ||
|
|
2438
|
+
first.title.normalize("NFKC") !== requestedTitle)
|
|
2439
|
+
return editFailed(diagnostic({
|
|
2440
|
+
code: "locator_invalid",
|
|
2441
|
+
locator,
|
|
2442
|
+
message: "replace_section first heading must preserve the exact original heading path",
|
|
2443
|
+
}));
|
|
2444
|
+
return applied(`${source.slice(0, section.start)}${block}${source.slice(section.trimmedEnd)}`, metadata);
|
|
2447
2445
|
}
|
|
2448
|
-
|
|
2446
|
+
const parent = markdownRevisionSection(source, locator.markdown);
|
|
2447
|
+
if ("diagnostics" in parent)
|
|
2448
|
+
return editFailed(parent.diagnostics[0]);
|
|
2449
|
+
if (!blockIsSubtree || first.level !== parent.level + 1)
|
|
2450
|
+
return editFailed(diagnostic({
|
|
2451
|
+
code: "locator_invalid",
|
|
2452
|
+
locator,
|
|
2453
|
+
message: "add_section must add a complete block with a heading one level lower",
|
|
2454
|
+
}));
|
|
2455
|
+
return applied(insertMarkdownBlock(source, parent.trimmedEnd, block), metadata);
|
|
2456
|
+
}
|
|
2457
|
+
function insertMarkdownBlock(source, offset, block) {
|
|
2458
|
+
return `${source.slice(0, offset)}\n\n${block}${source.slice(offset)}`;
|
|
2449
2459
|
}
|
|
2450
2460
|
function markdownEnvelope(source) {
|
|
2451
2461
|
if (!source.startsWith("---\n"))
|
|
@@ -2513,6 +2523,26 @@ function markdownRevisionSection(source, locator) {
|
|
|
2513
2523
|
if ("diagnostics" in envelope)
|
|
2514
2524
|
return envelope;
|
|
2515
2525
|
const headings = scanMarkdownHeadings(source, envelope.bodyStart).headings;
|
|
2526
|
+
if (locator.length === 0) {
|
|
2527
|
+
// The empty locator names the document body, whose sections are the h2s.
|
|
2528
|
+
const h1 = headings.find(({ level }) => level === 1);
|
|
2529
|
+
if (h1 === undefined)
|
|
2530
|
+
return {
|
|
2531
|
+
diagnostics: [
|
|
2532
|
+
diagnostic({
|
|
2533
|
+
code: "markdown_invalid",
|
|
2534
|
+
message: "Markdown body is missing an updateable h1",
|
|
2535
|
+
}),
|
|
2536
|
+
],
|
|
2537
|
+
};
|
|
2538
|
+
return {
|
|
2539
|
+
start: h1.range.start.offset,
|
|
2540
|
+
headingEnd: h1.range.end.offset,
|
|
2541
|
+
trimmedEnd: trimTrailingWhitespaceOffset(source, h1.range.start.offset, source.length),
|
|
2542
|
+
rawEnd: source.length,
|
|
2543
|
+
level: 1,
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2516
2546
|
const stack = [];
|
|
2517
2547
|
const matches = [];
|
|
2518
2548
|
for (const [index, heading] of headings.entries()) {
|
|
@@ -2527,13 +2557,23 @@ function markdownRevisionSection(source, locator) {
|
|
|
2527
2557
|
matches.push({ index, level: heading.level });
|
|
2528
2558
|
}
|
|
2529
2559
|
const publicLocator = { markdown: [...locator] };
|
|
2530
|
-
if (matches.length
|
|
2560
|
+
if (matches.length === 0)
|
|
2561
|
+
return {
|
|
2562
|
+
diagnostics: [
|
|
2563
|
+
diagnostic({
|
|
2564
|
+
code: "locator_not_found",
|
|
2565
|
+
locator: publicLocator,
|
|
2566
|
+
message: "Markdown heading path was not found",
|
|
2567
|
+
}),
|
|
2568
|
+
],
|
|
2569
|
+
};
|
|
2570
|
+
if (matches.length > 1)
|
|
2531
2571
|
return {
|
|
2532
2572
|
diagnostics: [
|
|
2533
2573
|
diagnostic({
|
|
2534
|
-
code:
|
|
2574
|
+
code: "locator_ambiguous",
|
|
2535
2575
|
locator: publicLocator,
|
|
2536
|
-
message: "Markdown heading path
|
|
2576
|
+
message: "Markdown heading path matches more than one section",
|
|
2537
2577
|
}),
|
|
2538
2578
|
],
|
|
2539
2579
|
};
|
|
@@ -2658,6 +2698,18 @@ function sourcePoint(source, offset) {
|
|
|
2658
2698
|
function diagnostic(value) {
|
|
2659
2699
|
return freeze(value);
|
|
2660
2700
|
}
|
|
2701
|
+
/** Human-readable locator for receipts and failure text; the structured locator stays the source of truth. */
|
|
2702
|
+
export function formatWorldDocumentLocator(locator) {
|
|
2703
|
+
if ("markdown" in locator)
|
|
2704
|
+
return locator.markdown.length === 0
|
|
2705
|
+
? "document body"
|
|
2706
|
+
: locator.markdown.join(" > ");
|
|
2707
|
+
return (locator.yaml.reduce((text, segment) => typeof segment === "number"
|
|
2708
|
+
? `${text}[${segment}]`
|
|
2709
|
+
: text === ""
|
|
2710
|
+
? segment
|
|
2711
|
+
: `${text}.${segment}`, "") || "whole body");
|
|
2712
|
+
}
|
|
2661
2713
|
function revisionDiagnostic(commandIndex, value) {
|
|
2662
2714
|
return freeze({ ...value, commandIndex });
|
|
2663
2715
|
}
|
|
@@ -2801,11 +2853,11 @@ function validYamlLocator(value) {
|
|
|
2801
2853
|
value.yaml.every((segment) => typeof segment === "string" ||
|
|
2802
2854
|
(Number.isSafeInteger(segment) && Number(segment) >= 0)));
|
|
2803
2855
|
}
|
|
2804
|
-
function validMarkdownLocator(value) {
|
|
2856
|
+
function validMarkdownLocator(value, allowBodyRoot = false) {
|
|
2805
2857
|
return (isRecord(value) &&
|
|
2806
2858
|
Object.keys(value).length === 1 &&
|
|
2807
2859
|
Array.isArray(value.markdown) &&
|
|
2808
|
-
value.markdown.length > 0 &&
|
|
2860
|
+
(allowBodyRoot || value.markdown.length > 0) &&
|
|
2809
2861
|
value.markdown.every((segment) => typeof segment === "string" && segment.length > 0));
|
|
2810
2862
|
}
|
|
2811
2863
|
function validMarkdownHeadingTitle(value) {
|