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.
Files changed (29) hide show
  1. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +14 -1
  2. package/dist/node/runtime/history/HistoryRendering.js +25 -6
  3. package/dist/node/runtime/history/HistoryTools.js +31 -0
  4. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +12 -4
  5. package/dist/node/runtime/play/PlayCallChain.js +10 -21
  6. package/dist/node/runtime/play/PlayDocumentTools.js +72 -43
  7. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +35 -19
  8. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -12
  9. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +0 -67
  10. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +326 -62
  11. package/dist/node/runtime/setting/SettingImprovementSession.js +40 -3
  12. package/dist/node/runtime/world/FileNativeWorldStore.js +13 -3
  13. package/dist/node/runtime/world/WorldDocumentMaintenance.js +9 -2
  14. package/dist/node/runtime/world/WorldDocumentStore.js +441 -389
  15. package/dist/node/runtime/world/WorldLedgerAuthority.js +18 -2
  16. package/dist/node/runtime/world/WorldStateRevision.js +6 -3
  17. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +61 -17
  18. package/dist/node/runtime/world-revision/LegacyWorldRevisionEpochUpgrade.js +46 -0
  19. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +83 -30
  20. package/dist/node/runtime/world-revision/WorldRevisionSession.js +64 -4
  21. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +32 -11
  22. package/dist/node/shared/default-setting-improvement-prompt.js +23 -8
  23. package/dist/node/shared/ordered-author-prompts.js +9 -7
  24. package/dist/node/shared/ordered-play-prompts.js +2 -2
  25. package/dist/web/assets/index-B2kbaOGe.js +1408 -0
  26. package/dist/web/assets/{index-BjxBYYQi.css → index-DH2BVsYd.css} +1 -1
  27. package/dist/web/index.html +2 -2
  28. package/package.json +1 -1
  29. 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
- if (target.entry.codec === "markdown") {
346
- const patched = patchRevisionMarkdown(target.working.file.contents, command.edits, target.entry.descriptor);
347
- if ("diagnostics" in patched)
348
- return this.#revisionFailure(commandIndex, patched.diagnostics.map((problem) => ({
349
- ...problem,
350
- logicalPath: target.entry.file.path,
351
- documentId: target.entry.descriptor.documentId,
352
- })));
353
- target.working.file.contents = patched.contents;
354
- }
355
- else {
356
- let document = parseDocument(target.working.file.contents, {
357
- schema: "core",
358
- uniqueKeys: true,
359
- strict: true,
360
- });
361
- let metadata = {
362
- title: target.entry.descriptor.title,
363
- summary: target.entry.descriptor.summary,
364
- aliases: [...target.entry.descriptor.aliases],
365
- };
366
- for (const edit of command.edits) {
367
- if (!isRecord(edit) || typeof edit.op !== "string")
368
- return this.#revisionFailure(commandIndex, {
369
- code: "query_invalid",
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
- target.working.file.contents = document.toString({
533
- indent: 2,
534
- lineWidth: 0,
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 = target.working.file.path;
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 patchRevisionMarkdown(source, edits, initialMetadata) {
2251
- let contents = source;
2252
- let metadata = {
2253
- title: initialMetadata.title,
2254
- summary: initialMetadata.summary,
2255
- aliases: [...initialMetadata.aliases],
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
- for (const edit of edits) {
2258
- if (!isRecord(edit) || typeof edit.op !== "string")
2259
- return {
2260
- diagnostics: [
2261
- diagnostic({
2262
- code: "query_invalid",
2263
- message: "A Markdown patch edit must be a closed object with op",
2264
- }),
2265
- ],
2266
- };
2267
- if (edit.op === "set_metadata") {
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)
2279
- return {
2280
- diagnostics: [
2281
- diagnostic({
2282
- code: "document_header_invalid",
2283
- message: `set_metadata arguments are invalid: ${resolved.problems.join("; ")}`,
2284
- }),
2285
- ],
2286
- };
2287
- metadata = resolved.metadata;
2288
- const envelope = markdownEnvelope(contents);
2289
- if ("diagnostics" in envelope)
2290
- return envelope;
2291
- envelope.header.setIn(["$document", "title"], metadata.title);
2292
- envelope.header.setIn(["$document", "summary"], metadata.summary);
2293
- envelope.header.setIn(["$document", "aliases"], [...metadata.aliases]);
2294
- const body = contents.slice(envelope.bodyStart);
2295
- const firstHeading = scanMarkdownHeadings(body, 0).headings[0];
2296
- if (firstHeading?.level !== 1)
2297
- return {
2298
- diagnostics: [
2299
- diagnostic({
2300
- code: "markdown_invalid",
2301
- message: "Markdown body is missing an updateable h1",
2302
- }),
2303
- ],
2304
- };
2305
- const nextBody = `${body.slice(0, firstHeading.range.start.offset)}# ${metadata.title}${body.slice(firstHeading.range.end.offset)}`;
2306
- contents = `---\n${envelope.header.toString({ indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${nextBody}`;
2307
- continue;
2308
- }
2309
- if (edit.op === "replace_body" || edit.op === "replace_preamble") {
2310
- if (!hasOnlyKeys(edit, ["op", "markdown"]) ||
2311
- typeof edit.markdown !== "string")
2312
- return {
2313
- diagnostics: [
2314
- diagnostic({
2315
- code: "query_invalid",
2316
- message: "A Markdown body edit requires markdown source",
2317
- }),
2318
- ],
2319
- };
2320
- const envelope = markdownEnvelope(contents);
2321
- if ("diagnostics" in envelope)
2322
- return envelope;
2323
- if (edit.op === "replace_body") {
2324
- contents = `${contents.slice(0, envelope.bodyStart)}${edit.markdown.trimEnd()}\n`;
2325
- continue;
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 headings = scanMarkdownHeadings(contents, envelope.bodyStart).headings;
2328
- const h1 = headings.find(({ level }) => level === 1);
2329
- if (h1 === undefined)
2330
- return {
2331
- diagnostics: [
2332
- diagnostic({
2333
- code: "markdown_invalid",
2334
- message: "Markdown body is missing an updateable h1",
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 (edit.op === "rename_section") {
2382
- if (!hasOnlyKeys(edit, ["op", "locator", "title"]) ||
2383
- !validMarkdownHeadingTitle(edit.title))
2384
- return {
2385
- diagnostics: [
2386
- diagnostic({
2387
- code: "query_invalid",
2388
- locator,
2389
- message: "rename_section requires a valid new heading",
2390
- }),
2391
- ],
2392
- };
2393
- contents = `${contents.slice(0, section.start)}${"#".repeat(section.level)} ${edit.title}${contents.slice(section.headingEnd)}`;
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
- if (!hasOnlyKeys(edit, ["op", "locator", "markdown"]) ||
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
- diagnostics: [
2400
- diagnostic({
2401
- code: "query_invalid",
2402
- locator,
2403
- message: "A Markdown section edit requires a complete Markdown block",
2404
- }),
2405
- ],
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
- if (edit.op === "replace_section") {
2414
- if (first?.range.start.offset !== 0 ||
2415
- first.level !== section.level ||
2416
- first.title.normalize("NFKC") !==
2417
- locator.markdown.at(-1).normalize("NFKC") ||
2418
- escapesSectionSubtree ||
2419
- hasUnclosedFence)
2420
- return {
2421
- diagnostics: [
2422
- diagnostic({
2423
- code: "locator_invalid",
2424
- locator,
2425
- message: "replace_section first heading must preserve the exact original heading path",
2426
- }),
2427
- ],
2428
- };
2429
- contents = `${contents.slice(0, section.start)}${block}${contents.slice(section.trimmedEnd)}`;
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
- else {
2432
- if (first?.range.start.offset !== 0 ||
2433
- first.level !== section.level + 1 ||
2434
- escapesSectionSubtree ||
2435
- hasUnclosedFence)
2436
- return {
2437
- diagnostics: [
2438
- diagnostic({
2439
- code: "locator_invalid",
2440
- locator,
2441
- message: "add_section must add a complete block with a heading one level lower",
2442
- }),
2443
- ],
2444
- };
2445
- contents = `${contents.slice(0, section.trimmedEnd)}\n\n${block}${contents.slice(section.trimmedEnd)}`;
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
- return { contents: contents.trimEnd().concat("\n") };
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 !== 1)
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: matches.length === 0 ? "locator_not_found" : "locator_ambiguous",
2574
+ code: "locator_ambiguous",
2535
2575
  locator: publicLocator,
2536
- message: "Markdown heading path cannot be located uniquely",
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) {