@omgbase/core 0.11.0 → 0.15.0

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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/src/core/ids.d.ts +4 -0
  3. package/dist/src/core/ids.js +17 -0
  4. package/dist/src/core/ingest.d.ts +39 -3
  5. package/dist/src/core/ingest.js +233 -202
  6. package/dist/src/core/store/schema.d.ts +3 -2
  7. package/dist/src/core/store/schema.js +28 -3
  8. package/dist/src/core/store/store.d.ts +9 -0
  9. package/dist/src/core/store/store.js +11 -0
  10. package/dist/src/graph/history.js +15 -12
  11. package/dist/src/index.d.ts +9 -5
  12. package/dist/src/index.js +14 -7
  13. package/dist/src/mcp/arguments.d.ts +9 -2
  14. package/dist/src/mcp/arguments.js +15 -2
  15. package/dist/src/mcp/errors.d.ts +1 -1
  16. package/dist/src/mcp/errors.js +3 -0
  17. package/dist/src/mcp/reference.d.ts +1 -1
  18. package/dist/src/mcp/reference.js +9 -9
  19. package/dist/src/mcp/server.d.ts +22 -0
  20. package/dist/src/mcp/server.js +263 -186
  21. package/dist/src/mutate/apply.d.ts +93 -12
  22. package/dist/src/mutate/apply.js +203 -113
  23. package/dist/src/mutate/commit.d.ts +150 -0
  24. package/dist/src/mutate/commit.js +349 -0
  25. package/dist/src/mutate/doc-store.d.ts +3 -3
  26. package/dist/src/mutate/doc-store.js +21 -6
  27. package/dist/src/mutate/docs.d.ts +42 -87
  28. package/dist/src/mutate/docs.js +50 -204
  29. package/dist/src/mutate/effects.d.ts +331 -0
  30. package/dist/src/mutate/effects.js +981 -0
  31. package/dist/src/mutate/materialize.d.ts +49 -0
  32. package/dist/src/mutate/materialize.js +174 -0
  33. package/dist/src/mutate/opset.d.ts +27 -2
  34. package/dist/src/mutate/opset.js +12 -5
  35. package/dist/src/mutate/plan-update.d.ts +1 -46
  36. package/dist/src/mutate/plan-update.js +19 -67
  37. package/dist/src/mutate/plan.d.ts +200 -0
  38. package/dist/src/mutate/plan.js +472 -0
  39. package/dist/src/mutate/sugar.d.ts +135 -0
  40. package/dist/src/mutate/sugar.js +167 -0
  41. package/dist/src/mutate/tree.d.ts +4 -2
  42. package/dist/src/oqx-js/run.js +1 -1
  43. package/dist/src/spec-versions.d.ts +5 -5
  44. package/dist/src/spec-versions.js +5 -5
  45. package/dist/src/surface-paths.d.ts +17 -26
  46. package/dist/src/surface-paths.js +27 -22
  47. package/dist/src/sync/checkpoint.d.ts +5 -2
  48. package/dist/src/sync/checkpoint.js +10 -6
  49. package/dist/src/sync/freshness.d.ts +10 -7
  50. package/dist/src/sync/freshness.js +15 -17
  51. package/dist/src/sync/observe.d.ts +4 -1
  52. package/dist/src/sync/observe.js +56 -5
  53. package/dist/src/sync/recovery.d.ts +4 -0
  54. package/dist/src/sync/recovery.js +4 -1
  55. package/dist/src/sync/stat-cache.d.ts +7 -0
  56. package/dist/src/sync/stat-cache.js +22 -0
  57. package/dist/src/sync/tombstone.js +23 -19
  58. package/dist/src/sync/watcher.d.ts +2 -0
  59. package/dist/src/sync/watcher.js +4 -1
  60. package/dist/src/sync/writer-lock.d.ts +15 -0
  61. package/dist/src/sync/writer-lock.js +104 -17
  62. package/package.json +1 -1
@@ -0,0 +1,981 @@
1
+ import { findDoc } from "../core/read/reader.js";
2
+ import { withIdMinter } from "../core/ids.js";
3
+ import { inboundLinksTo } from "../graph/inbound-links.js";
4
+ import { readDocBase } from "./commit.js";
5
+ import { canonicalDocPath, composeFile, parseNewDoc, planInboundRetarget, splitFrontmatter } from "./docs.js";
6
+ import { linksRepair, nodeSet } from "./macros.js";
7
+ import { planUpdate } from "./plan-update.js";
8
+ import { MutationError, rawHashHex, renderDoc } from "./tree.js";
9
+ export const EFFECT_OPS = ["create", "insert", "set", "unset", "move", "remove", "split", "merge", "retarget"];
10
+ /** `$new.<n>`: an identity an earlier effect of the same request creates (§11.1). */
11
+ export const NEW_SYMBOL_RE = /^\$new\.(0|[1-9][0-9]*)$/;
12
+ export function isNewSymbol(s) {
13
+ return NEW_SYMBOL_RE.test(s);
14
+ }
15
+ // ---- validation (§11.1; a malformed effect is `filter_invalid` with `hint: "arguments"`) ----
16
+ function isRecord(v) {
17
+ return typeof v === "object" && v !== null && !Array.isArray(v);
18
+ }
19
+ /** The argument-shape error of the surface, thrown from the planner: `filter_invalid { reason, hint: "arguments" }`. */
20
+ export function effectInvalid(message) {
21
+ return new MutationError("filter_invalid", message, { reason: message, hint: "arguments" });
22
+ }
23
+ function onlyKeys(e, allowed, here) {
24
+ const extra = Object.keys(e).filter((k) => !allowed.includes(k));
25
+ if (extra.length > 0)
26
+ throw effectInvalid(`${here}: unknown key${extra.length > 1 ? "s" : ""} ${extra.map((k) => `\`${k}\``).join(", ")}`);
27
+ }
28
+ function requireString(e, key, here) {
29
+ const v = e[key];
30
+ if (typeof v !== "string")
31
+ throw effectInvalid(`${here}: \`${key}\` must be a string`);
32
+ return v;
33
+ }
34
+ function optionalString(e, key, here) {
35
+ const v = e[key];
36
+ if (v === undefined)
37
+ return undefined;
38
+ if (typeof v !== "string")
39
+ throw effectInvalid(`${here}: \`${key}\` must be a string`);
40
+ return v;
41
+ }
42
+ function validateTarget(v, here, kinds) {
43
+ if (!isRecord(v) || typeof v.kind !== "string")
44
+ throw effectInvalid(`${here}: a target is { kind: ${kinds.map((k) => `"${k}"`).join(" | ")}, … }`);
45
+ if (!kinds.includes(v.kind))
46
+ throw effectInvalid(`${here}: \`kind\` must be one of ${kinds.join(", ")} (got "${v.kind}")`);
47
+ switch (v.kind) {
48
+ case "document": {
49
+ onlyKeys(v, ["kind", "id", "path"], here);
50
+ const id = optionalString(v, "id", here);
51
+ const path = optionalString(v, "path", here);
52
+ if (id === undefined && path === undefined)
53
+ throw effectInvalid(`${here}: a document target needs \`id\` or \`path\``);
54
+ return { kind: "document", ...(id !== undefined ? { id } : {}), ...(path !== undefined ? { path } : {}) };
55
+ }
56
+ case "block":
57
+ onlyKeys(v, ["kind", "id"], here);
58
+ return { kind: "block", id: requireString(v, "id", here) };
59
+ case "node":
60
+ onlyKeys(v, ["kind", "id"], here);
61
+ return { kind: "node", id: requireString(v, "id", here) };
62
+ case "section":
63
+ onlyKeys(v, ["kind", "heading_id"], here);
64
+ return { kind: "section", heading_id: requireString(v, "heading_id", here) };
65
+ }
66
+ }
67
+ function validateTargets(v, here) {
68
+ if (!Array.isArray(v) || v.length === 0)
69
+ throw effectInvalid(`${here}: \`targets\` must be a non-empty array of block targets`);
70
+ return v.map((t, i) => validateTarget(t, `${here}.targets[${i}]`, ["block"]));
71
+ }
72
+ function validateAt(v, here) {
73
+ if (v === undefined)
74
+ return undefined;
75
+ if (v === "start" || v === "end")
76
+ return v;
77
+ if (isRecord(v) && Object.keys(v).length === 1) {
78
+ if (typeof v.before === "string")
79
+ return { before: v.before };
80
+ if (typeof v.after === "string")
81
+ return { after: v.after };
82
+ }
83
+ throw effectInvalid(`${here}: \`at\` must be "start", "end", { before: <block id> } or { after: <block id> }`);
84
+ }
85
+ function validateExpect(v, here) {
86
+ if (v === undefined)
87
+ return undefined;
88
+ if (!isRecord(v))
89
+ throw effectInvalid(`${here}: \`expect\` must be an object`);
90
+ onlyKeys(v, ["content_hash", "parent_children_hash", "revision"], `${here}.expect`);
91
+ const out = {};
92
+ for (const k of ["content_hash", "parent_children_hash", "revision"]) {
93
+ const s = optionalString(v, k, `${here}.expect`);
94
+ if (s !== undefined)
95
+ out[k] = s;
96
+ }
97
+ return out;
98
+ }
99
+ function validateExpectMap(v, here) {
100
+ if (v === undefined)
101
+ return undefined;
102
+ if (!isRecord(v))
103
+ throw effectInvalid(`${here}: \`expect\` must be an object keyed by block id`);
104
+ const out = {};
105
+ for (const [id, x] of Object.entries(v)) {
106
+ if (!isRecord(x))
107
+ throw effectInvalid(`${here}.expect[${JSON.stringify(id)}]: must be an object`);
108
+ onlyKeys(x, ["content_hash", "parent_children_hash"], `${here}.expect[${JSON.stringify(id)}]`);
109
+ const e = {};
110
+ const ch = optionalString(x, "content_hash", `${here}.expect[${JSON.stringify(id)}]`);
111
+ const pch = optionalString(x, "parent_children_hash", `${here}.expect[${JSON.stringify(id)}]`);
112
+ if (ch !== undefined)
113
+ e.content_hash = ch;
114
+ if (pch !== undefined)
115
+ e.parent_children_hash = pch;
116
+ out[id] = e;
117
+ }
118
+ return out;
119
+ }
120
+ /** The shape check of one effect (§11.1). `i` names it in the message (`effects[i]`). */
121
+ export function validateEffect(v, i) {
122
+ const here = `effects[${i}]`;
123
+ if (!isRecord(v) || typeof v.op !== "string")
124
+ throw effectInvalid(`${here}: an effect is an object whose \`op\` is one of ${EFFECT_OPS.join("/")}`);
125
+ if (!EFFECT_OPS.includes(v.op))
126
+ throw effectInvalid(`${here}: \`op\` must be one of ${EFFECT_OPS.join("/")} (got "${v.op}")`);
127
+ switch (v.op) {
128
+ case "create": {
129
+ onlyKeys(v, ["op", "path", "markdown", "frontmatter"], here);
130
+ const path = requireString(v, "path", here);
131
+ const markdown = requireString(v, "markdown", here);
132
+ if (v.frontmatter !== undefined && !isRecord(v.frontmatter))
133
+ throw effectInvalid(`${here}: \`frontmatter\` must be an object`);
134
+ return { op: "create", path, markdown, ...(v.frontmatter !== undefined ? { frontmatter: v.frontmatter } : {}) };
135
+ }
136
+ case "insert": {
137
+ onlyKeys(v, ["op", "into", "at", "markdown", "expect"], here);
138
+ const into = validateTarget(v.into, `${here}.into`, ["document", "section", "block"]);
139
+ const at = validateAt(v.at, here);
140
+ const markdown = requireString(v, "markdown", here);
141
+ const expect = validateExpect(v.expect, here);
142
+ return { op: "insert", into, ...(at !== undefined ? { at } : {}), markdown, ...(expect !== undefined ? { expect } : {}) };
143
+ }
144
+ case "set": {
145
+ onlyKeys(v, ["op", "target", "field", "value", "expect"], here);
146
+ const target = validateTarget(v.target, `${here}.target`, ["document", "block", "node"]);
147
+ const field = requireString(v, "field", here);
148
+ if (!("value" in v))
149
+ throw effectInvalid(`${here}: \`value\` is required`);
150
+ checkField(target.kind, field, here, "set");
151
+ const expect = validateExpect(v.expect, here);
152
+ return { op: "set", target, field, value: v.value, ...(expect !== undefined ? { expect } : {}) };
153
+ }
154
+ case "unset": {
155
+ onlyKeys(v, ["op", "target", "field", "expect"], here);
156
+ const target = validateTarget(v.target, `${here}.target`, ["document"]);
157
+ const field = requireString(v, "field", here);
158
+ checkField(target.kind, field, here, "unset");
159
+ const expect = validateExpect(v.expect, here);
160
+ return { op: "unset", target, field, ...(expect !== undefined ? { expect } : {}) };
161
+ }
162
+ case "move": {
163
+ if ("targets" in v) {
164
+ onlyKeys(v, ["op", "targets", "into", "at", "expect"], here);
165
+ const targets = validateTargets(v.targets, here);
166
+ const into = validateTarget(v.into, `${here}.into`, ["document", "section", "block"]);
167
+ const at = validateAt(v.at, here);
168
+ const expect = validateExpect(v.expect, here);
169
+ return { op: "move", targets, into, ...(at !== undefined ? { at } : {}), ...(expect !== undefined ? { expect } : {}) };
170
+ }
171
+ onlyKeys(v, ["op", "target", "to", "retarget_inbound"], here);
172
+ const target = validateTarget(v.target, `${here}.target`, ["document"]);
173
+ const to = requireString(v, "to", here);
174
+ if (v.retarget_inbound !== undefined && typeof v.retarget_inbound !== "boolean")
175
+ throw effectInvalid(`${here}: \`retarget_inbound\` must be a boolean`);
176
+ return { op: "move", target, to, ...(v.retarget_inbound !== undefined ? { retarget_inbound: v.retarget_inbound } : {}) };
177
+ }
178
+ case "remove": {
179
+ if ("targets" in v) {
180
+ onlyKeys(v, ["op", "targets", "expect"], here);
181
+ const targets = validateTargets(v.targets, here);
182
+ const expect = validateExpectMap(v.expect, here);
183
+ return { op: "remove", targets, ...(expect !== undefined ? { expect } : {}) };
184
+ }
185
+ onlyKeys(v, ["op", "target"], here);
186
+ return { op: "remove", target: validateTarget(v.target, `${here}.target`, ["document"]) };
187
+ }
188
+ case "split": {
189
+ onlyKeys(v, ["op", "target", "at", "expect"], here);
190
+ const target = validateTarget(v.target, `${here}.target`, ["block"]);
191
+ if (!Array.isArray(v.at) || v.at.some((n) => typeof n !== "number" || !Number.isInteger(n) || n < 0))
192
+ throw effectInvalid(`${here}: \`at\` must be an array of byte offsets`);
193
+ const expect = validateExpect(v.expect, here);
194
+ return { op: "split", target, at: v.at, ...(expect !== undefined ? { expect } : {}) };
195
+ }
196
+ case "merge": {
197
+ onlyKeys(v, ["op", "targets", "separator", "expect"], here);
198
+ const targets = validateTargets(v.targets, here);
199
+ const separator = optionalString(v, "separator", here);
200
+ const expect = validateExpectMap(v.expect, here);
201
+ return { op: "merge", targets, ...(separator !== undefined ? { separator } : {}), ...(expect !== undefined ? { expect } : {}) };
202
+ }
203
+ case "retarget": {
204
+ onlyKeys(v, ["op", "repairs", "path_glob"], here);
205
+ if (!Array.isArray(v.repairs))
206
+ throw effectInvalid(`${here}: \`repairs\` must be an array of { from, to }`);
207
+ const repairs = v.repairs.map((r, j) => {
208
+ if (!isRecord(r))
209
+ throw effectInvalid(`${here}.repairs[${j}]: must be { from, to }`);
210
+ onlyKeys(r, ["from", "to"], `${here}.repairs[${j}]`);
211
+ return { from: requireString(r, "from", `${here}.repairs[${j}]`), to: requireString(r, "to", `${here}.repairs[${j}]`) };
212
+ });
213
+ const path_glob = optionalString(v, "path_glob", here);
214
+ return { op: "retarget", repairs, ...(path_glob !== undefined ? { path_glob } : {}) };
215
+ }
216
+ }
217
+ }
218
+ /** The whole list; a non-array or an empty list is a shape error too. */
219
+ export function validateEffects(v) {
220
+ if (!Array.isArray(v))
221
+ throw effectInvalid("`effects` must be an array of effects");
222
+ if (v.length === 0)
223
+ throw effectInvalid("`effects` must name at least one effect");
224
+ return v.map((e, i) => validateEffect(e, i));
225
+ }
226
+ // §11.2: the `field` spellings per target kind.
227
+ export const FRONTMATTER_FIELD = /^frontmatter\.(.+)$/;
228
+ function checkField(kind, field, here, op) {
229
+ if (kind === "document") {
230
+ if (FRONTMATTER_FIELD.test(field) || (op === "set" && field === "content"))
231
+ return;
232
+ throw effectInvalid(`${here}: a document's \`field\` is \`frontmatter.<key>\`${op === "set" ? " or `content`" : ""} (got "${field}")`);
233
+ }
234
+ if (kind === "block") {
235
+ if (field === "markdown" || field === "attrs.checked")
236
+ return;
237
+ throw effectInvalid(`${here}: a block's \`field\` is \`markdown\` or \`attrs.checked\` (got "${field}")`);
238
+ }
239
+ // node: the adapter's editable props (`node_not_editable` names them when unknown)
240
+ if (field === "" || field.includes("."))
241
+ throw effectInvalid(`${here}: a node's \`field\` is one of its editable properties (got "${field}")`);
242
+ }
243
+ export function conflictingEffects(effects, focus) {
244
+ const what = focus.field !== undefined ? `${focus.target.kind} ${focus.target.id ?? focus.target.path} ${focus.field}` : `${focus.target.kind} ${focus.target.id ?? focus.target.path}`;
245
+ return new MutationError("conflicting_effects", `effects ${effects[0]} and ${effects[1]} contradict each other on ${what}`, { effects, focus, retriable: false });
246
+ }
247
+ /** The focus bookkeeping over an ordered effect list (§11.3). */
248
+ export class ConflictTracker {
249
+ canonical;
250
+ scalar = new Map();
251
+ retiredDocs = new Map();
252
+ retiredBlocks = new Map();
253
+ retiredPaths = new Map();
254
+ touchedDocs = new Map();
255
+ diagnostics = [];
256
+ constructor(canonical) {
257
+ this.canonical = canonical;
258
+ }
259
+ /** Check effect `i`; returns `false` when it is an identical duplicate to drop. Throws `conflicting_effects`. */
260
+ check(i, info, describe) {
261
+ for (const d of info.docs) {
262
+ const by = this.retiredDocs.get(d);
263
+ if (by !== undefined)
264
+ throw conflictingEffects([by, i], { kind: "identity", target: { kind: "document", id: d } });
265
+ }
266
+ for (const b of info.blocks) {
267
+ const by = this.retiredBlocks.get(b);
268
+ if (by !== undefined)
269
+ throw conflictingEffects([by, i], { kind: "identity", target: { kind: "block", id: b } });
270
+ }
271
+ for (const p of info.paths) {
272
+ const by = this.retiredPaths.get(p);
273
+ if (by !== undefined)
274
+ throw conflictingEffects([by, i], { kind: "identity", target: { kind: "path", path: p } });
275
+ }
276
+ if (info.wholeDoc !== undefined) {
277
+ const by = this.touchedDocs.get(info.wholeDoc);
278
+ if (by !== undefined)
279
+ throw conflictingEffects([by, i], { kind: "identity", target: { kind: "document", id: info.wholeDoc } });
280
+ }
281
+ if (info.scalar) {
282
+ const prev = this.scalar.get(info.scalar.key);
283
+ if (prev) {
284
+ const identical = prev.op === info.scalar.op && (info.scalar.op === "unset" || this.canonical(prev.value) === this.canonical(info.scalar.value));
285
+ if (!identical)
286
+ throw conflictingEffects([prev.index, i], { kind: "scalar", target: info.scalar.target, field: info.scalar.field });
287
+ this.diagnostics.push(`effect ${i} (${describe}) duplicates effect ${prev.index}; dropped`);
288
+ return false;
289
+ }
290
+ }
291
+ for (const s of info.scalars ?? []) {
292
+ const prev = this.scalar.get(s.key);
293
+ if (prev)
294
+ throw conflictingEffects([prev.index, i], { kind: "scalar", target: s.target, field: s.field });
295
+ }
296
+ // Record.
297
+ if (info.scalar)
298
+ this.scalar.set(info.scalar.key, { index: i, op: info.scalar.op, value: info.scalar.value, target: info.scalar.target, field: info.scalar.field });
299
+ for (const s of info.scalars ?? [])
300
+ this.scalar.set(s.key, { index: i, op: "set", value: undefined, target: s.target, field: s.field });
301
+ for (const d of info.docs)
302
+ if (!this.touchedDocs.has(d))
303
+ this.touchedDocs.set(d, i);
304
+ for (const d of info.retiresDocs ?? [])
305
+ this.retiredDocs.set(d, i);
306
+ for (const b of info.retiresBlocks ?? [])
307
+ this.retiredBlocks.set(b, i);
308
+ for (const p of info.retiresPaths ?? [])
309
+ this.retiredPaths.set(p, i);
310
+ return true;
311
+ }
312
+ }
313
+ // ---- the lowering context ---------------------------------------------------------
314
+ /** `$new.n` symbols in allocation order (§11.4 `new_ids`). */
315
+ export class SymbolTable {
316
+ entries = [];
317
+ alloc(kind) {
318
+ const symbol = `$new.${this.entries.length}`;
319
+ this.entries.push({ symbol, kind });
320
+ return symbol;
321
+ }
322
+ kindOf(symbol) {
323
+ return this.entries.find((e) => e.symbol === symbol)?.kind;
324
+ }
325
+ }
326
+ // ---- resolution --------------------------------------------------------------------
327
+ function docMissing(ref) {
328
+ return new MutationError("doc_missing", `doc ${ref} not found`, { doc: ref });
329
+ }
330
+ /** Resolve a document target: an existing document's `{ id, path }`, or a `$new.n` document this plan creates. */
331
+ export function resolveDocumentTarget(ctx, t) {
332
+ if (t.id !== undefined && isNewSymbol(t.id)) {
333
+ const created = ctx.creates.find((c) => c.symbol === t.id);
334
+ if (!created)
335
+ throw symbolMissing(ctx, t.id, "document");
336
+ if (t.path !== undefined && canonicalDocPath(t.path) !== created.path)
337
+ throw effectInvalid(`id ${JSON.stringify(t.id)} and path ${JSON.stringify(t.path)} name different documents`);
338
+ return { kind: "document", id: t.id, path: created.path };
339
+ }
340
+ const byId = t.id !== undefined ? findDoc(ctx.store, { docId: t.id }) : null;
341
+ if (t.id !== undefined && (!byId || byId.repoId !== ctx.repoId))
342
+ throw docMissing(t.id);
343
+ const byPath = t.path !== undefined ? findDoc(ctx.store, { repoId: ctx.repoId, path: t.path }) : null;
344
+ if (t.path !== undefined && !byPath)
345
+ throw docMissing(t.path);
346
+ if (byId && byPath && byId.docId !== byPath.docId)
347
+ throw effectInvalid(`id ${JSON.stringify(t.id)} and path ${JSON.stringify(t.path)} name different documents`);
348
+ const info = (byId ?? byPath);
349
+ return { kind: "document", id: info.docId, path: info.path };
350
+ }
351
+ function symbolMissing(ctx, symbol, wanted) {
352
+ const kind = ctx.symbols.kindOf(symbol);
353
+ if (kind === undefined)
354
+ return new MutationError("target_missing", `${symbol} is not created by an earlier effect`, { symbol });
355
+ return new MutationError("target_missing", `${symbol} names a ${kind}, not a ${wanted}`, { symbol });
356
+ }
357
+ /** A block id as the kernel must see it: a `$new.n` an op minted becomes the placeholder of that op;
358
+ * a `$new.n` of a created document's tree (3.0) stays the symbol — it IS the tree's id at plan
359
+ * time, and `execute` aliases it to the id its replay mints (§11.7). */
360
+ function kernelBlockId(ctx, id) {
361
+ if (!isNewSymbol(id))
362
+ return id;
363
+ const placeholder = ctx.blockSymbols.get(id);
364
+ if (placeholder !== undefined)
365
+ return placeholder;
366
+ if (locateLoaded(ctx, id))
367
+ return id;
368
+ throw symbolMissing(ctx, id, "block");
369
+ }
370
+ function kernelAt(ctx, at) {
371
+ if (at === "start" || at === "end")
372
+ return at;
373
+ if ("before" in at)
374
+ return { before: kernelBlockId(ctx, at.before) };
375
+ return { after: kernelBlockId(ctx, at.after) };
376
+ }
377
+ /** A block in the planner's trees (a `$new.n` is only there) — never loads a document. */
378
+ function locateLoaded(ctx, id) {
379
+ const f = ctx.planner.locateLoaded(id);
380
+ return f ? { docId: f.docId, block: f.block } : null;
381
+ }
382
+ /** The live row of a block: its document and raw hash; `$new.n` from the tree. */
383
+ function blockInfo(ctx, id) {
384
+ const inTree = locateLoaded(ctx, id);
385
+ if (inTree)
386
+ return { docId: inTree.docId, hash: rawHashHex(inTree.block.raw), block: inTree.block };
387
+ if (isNewSymbol(id))
388
+ throw symbolMissing(ctx, id, "block");
389
+ const row = ctx.store.db
390
+ .prepare("SELECT doc_id, lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
391
+ .get(id);
392
+ if (!row)
393
+ throw new MutationError("block_missing", `block ${id} not found`, { block: id });
394
+ return { docId: row.doc_id, hash: row.h, block: null };
395
+ }
396
+ /** Every descendant id of a block: the planner's tree when loaded, else the store's live rows. */
397
+ function descendantsOf(ctx, id) {
398
+ const inTree = locateLoaded(ctx, id);
399
+ if (inTree) {
400
+ const out = [];
401
+ const walk = (b) => {
402
+ for (const c of b.children) {
403
+ out.push(c.id);
404
+ walk(c);
405
+ }
406
+ };
407
+ walk(inTree.block);
408
+ return out;
409
+ }
410
+ if (isNewSymbol(id))
411
+ return [];
412
+ const rows = ctx.store.db
413
+ .prepare(`WITH RECURSIVE sub(block_id) AS (
414
+ SELECT block_id FROM blocks WHERE parent_block = ? AND deleted_commit IS NULL
415
+ UNION ALL
416
+ SELECT b.block_id FROM blocks b JOIN sub ON b.parent_block = sub.block_id WHERE b.deleted_commit IS NULL
417
+ ) SELECT block_id FROM sub`)
418
+ .all(id);
419
+ return rows.map((r) => r.block_id);
420
+ }
421
+ /** The document a placement target lives in (loading nothing; `parent_missing` for an unknown heading/block). */
422
+ function targetDoc(ctx, t) {
423
+ switch (t.kind) {
424
+ case "document":
425
+ return t.id;
426
+ case "section": {
427
+ const inTree = locateLoaded(ctx, t.heading_id);
428
+ if (inTree)
429
+ return inTree.docId;
430
+ const row = ctx.store.db.prepare("SELECT doc_id FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(t.heading_id);
431
+ if (!row)
432
+ throw new MutationError("parent_missing", `parent ${t.heading_id} not found`);
433
+ return row.doc_id;
434
+ }
435
+ case "block": {
436
+ const inTree = locateLoaded(ctx, t.id);
437
+ if (inTree)
438
+ return inTree.docId;
439
+ if (isNewSymbol(t.id))
440
+ throw symbolMissing(ctx, t.id, "block");
441
+ const row = ctx.store.db.prepare("SELECT doc_id FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(t.id);
442
+ if (!row)
443
+ throw new MutationError("parent_missing", `parent ${t.id} not found`);
444
+ return row.doc_id;
445
+ }
446
+ case "node":
447
+ return nodeBlock(ctx, t.id).docId;
448
+ }
449
+ }
450
+ function nodeBlock(ctx, nodeId) {
451
+ const row = ctx.store.db.prepare("SELECT doc_id, block_id FROM nodes WHERE node_id = ?").get(nodeId);
452
+ if (!row)
453
+ throw new MutationError("block_missing", `node ${nodeId} not found`, {});
454
+ if (!row.block_id)
455
+ throw new MutationError("node_not_editable", `node ${nodeId} is not anchored to a block`, {});
456
+ return { docId: row.doc_id, blockId: row.block_id };
457
+ }
458
+ function placement(ctx, into, at) {
459
+ const kernel = kernelAt(ctx, at);
460
+ switch (into.kind) {
461
+ case "document":
462
+ return { parent: { doc: true }, at: kernel };
463
+ case "section":
464
+ return { parent: { heading: kernelBlockId(ctx, into.heading_id), scope: "section" }, at: kernel };
465
+ case "block":
466
+ return { parent: kernelBlockId(ctx, into.id), at: kernel };
467
+ case "node":
468
+ throw effectInvalid("a node is not a placement target");
469
+ }
470
+ }
471
+ /** `expect.revision` against the document's current revision (§11.2): a mismatch is `stale_plan`. */
472
+ function checkRevision(ctx, docId, expect) {
473
+ if (expect?.revision === undefined)
474
+ return;
475
+ if (isNewSymbol(docId))
476
+ throw effectInvalid(`expect.revision cannot name a document this plan creates (${docId})`);
477
+ const base = readDocBase(ctx.store.db, docId);
478
+ if (!base)
479
+ throw docMissing(docId);
480
+ if (base.currentRev !== expect.revision) {
481
+ throw new MutationError("stale_plan", `document ${docId} is at revision ${base.currentRev ?? "null"}, not ${expect.revision}; re-plan`, {
482
+ doc: docId,
483
+ expected_revision: expect.revision,
484
+ current: { revision: base.currentRev, hash: base.fileHash },
485
+ retriable: true,
486
+ });
487
+ }
488
+ }
489
+ /** Pin a block's content hash: the caller's `expect.content_hash` when given, else the live row's (the sugar tools' rule). */
490
+ function pin(ctx, id, given) {
491
+ const hash = given ?? blockInfo(ctx, id).hash;
492
+ if (!isNewSymbol(id) && !ctx.pins.has(id))
493
+ ctx.pins.set(id, hash);
494
+ return hash;
495
+ }
496
+ function pushOp(ctx, op) {
497
+ ctx.ops.push(op);
498
+ const before = ctx.planner.results.length;
499
+ ctx.planner.push(op);
500
+ // Any `$new.n` this op minted is referenceable through the op's placeholder.
501
+ const result = ctx.planner.results[before];
502
+ result.ids.forEach((id, j) => {
503
+ if (isNewSymbol(id) && !ctx.blockSymbols.has(id))
504
+ ctx.blockSymbols.set(id, `$${before}.ids[${j}]`);
505
+ });
506
+ }
507
+ function kernelExpect(e, contentHash) {
508
+ const out = {};
509
+ if (contentHash !== undefined)
510
+ out.content_hash = contentHash;
511
+ if (e?.parent_children_hash !== undefined)
512
+ out.parent_children_hash = e.parent_children_hash;
513
+ return Object.keys(out).length > 0 ? out : undefined;
514
+ }
515
+ function recordParentPin(ctx, doc, into, expect) {
516
+ if (expect?.parent_children_hash === undefined)
517
+ return;
518
+ const parent = into.kind === "block" ? into.id : null;
519
+ ctx.parents.push({ doc, parent, parent_children_hash: expect.parent_children_hash });
520
+ }
521
+ // ---- lowering (§11.3, the table) -----------------------------------------------------
522
+ function describe(e) {
523
+ switch (e.op) {
524
+ case "create": return `create ${e.path}`;
525
+ case "insert": return `insert into ${targetName(e.into)}`;
526
+ case "set": return `set ${e.field} on ${targetName(e.target)}`;
527
+ case "unset": return `unset ${e.field} on ${targetName(e.target)}`;
528
+ case "move": return "targets" in e ? `move ${e.targets.map(targetName).join(",")}` : `move ${targetName(e.target)} -> ${e.to}`;
529
+ case "remove": return "targets" in e ? `remove ${e.targets.map(targetName).join(",")}` : `remove ${targetName(e.target)}`;
530
+ case "split": return `split ${targetName(e.target)}`;
531
+ case "merge": return `merge ${e.targets.map(targetName).join(",")}`;
532
+ case "retarget": return `retarget ${e.repairs.map((r) => `${r.from}->${r.to}`).join(",")}`;
533
+ }
534
+ }
535
+ function targetName(t) {
536
+ return t.kind === "section" ? `section ${t.heading_id}` : `${t.kind} ${t.id}`;
537
+ }
538
+ /** Resolve one validated effect's targets (§11.4): documents to `{ id, path }`, defaults made explicit. */
539
+ export function resolveEffect(ctx, e) {
540
+ const target = (t) => (t.kind === "document" ? resolveDocumentTarget(ctx, t) : t);
541
+ switch (e.op) {
542
+ case "create":
543
+ return { op: "create", path: canonicalDocPath(e.path), markdown: e.markdown, ...(e.frontmatter !== undefined ? { frontmatter: e.frontmatter } : {}) };
544
+ case "insert":
545
+ return { op: "insert", into: target(e.into), at: e.at ?? "end", markdown: e.markdown, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
546
+ case "set":
547
+ return { op: "set", target: target(e.target), field: e.field, value: e.value, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
548
+ case "unset":
549
+ return { op: "unset", target: target(e.target), field: e.field, ...(e.expect !== undefined ? { expect: e.expect } : {}) };
550
+ case "move":
551
+ if ("targets" in e)
552
+ return { op: "move", targets: e.targets.map(target), into: target(e.into), at: e.at ?? "end", ...(e.expect !== undefined ? { expect: e.expect } : {}) };
553
+ return { op: "move", target: target(e.target), to: canonicalDocPath(e.to), retarget_inbound: e.retarget_inbound !== false };
554
+ case "remove":
555
+ if ("targets" in e)
556
+ return { op: "remove", targets: e.targets.map(target), ...(e.expect !== undefined ? { expect: e.expect } : {}) };
557
+ return { op: "remove", target: target(e.target) };
558
+ case "split":
559
+ return { op: "split", target: target(e.target), at: [...e.at], ...(e.expect !== undefined ? { expect: e.expect } : {}) };
560
+ case "merge":
561
+ return { op: "merge", targets: e.targets.map(target), separator: e.separator ?? " ", ...(e.expect !== undefined ? { expect: e.expect } : {}) };
562
+ case "retarget":
563
+ return { op: "retarget", repairs: e.repairs.map((r) => ({ from: r.from, to: r.to })), ...(e.path_glob !== undefined ? { path_glob: e.path_glob } : {}) };
564
+ }
565
+ }
566
+ function docTarget(t) {
567
+ if (t.kind !== "document")
568
+ throw effectInvalid(`expected a document target, got ${t.kind}`);
569
+ return t;
570
+ }
571
+ function blockTarget(t) {
572
+ if (t.kind !== "block")
573
+ throw effectInvalid(`expected a block target, got ${t.kind}`);
574
+ return t;
575
+ }
576
+ function nodeTarget(t) {
577
+ if (t.kind !== "node")
578
+ throw effectInvalid(`expected a node target, got ${t.kind}`);
579
+ return t;
580
+ }
581
+ /** The current frontmatter patch base of a loaded document: `(frontmatter, body)` of its render. */
582
+ function frontmatterOf(ctx, docId) {
583
+ const d = ctx.planner.ensureDoc(docId);
584
+ const { frontmatter, body } = splitFrontmatter(renderDoc(d));
585
+ return { d, frontmatter, body };
586
+ }
587
+ /**
588
+ * Patch a loaded document's frontmatter: compose the whole file as
589
+ * `docs_set_meta` does (`composeFile(body, merged)`, §6), then set the file
590
+ * head so the known-id ingest renders exactly those bytes — the body blocks
591
+ * keep their ids and bytes (a file without a trailing newline gains one on its
592
+ * last block's trivia, as `composeFile` would add it).
593
+ */
594
+ function patchFrontmatter(ctx, docId, merged, body) {
595
+ const d = ctx.planner.ensureDoc(docId);
596
+ const after = composeFile(body, merged);
597
+ let blocks = d.children.map((b) => renderBlockExact(d, b)).join("");
598
+ if (!after.endsWith(blocks)) {
599
+ const last = d.children[d.children.length - 1];
600
+ if (last && after.endsWith(blocks + "\n")) {
601
+ last.trivia += "\n";
602
+ blocks += "\n";
603
+ }
604
+ else {
605
+ throw new MutationError("plan_not_convergent", `the frontmatter patch of ${docId} does not preserve the body bytes`, { doc: docId });
606
+ }
607
+ }
608
+ const head = after.slice(0, after.length - blocks.length);
609
+ ctx.planner.setHead(docId, head);
610
+ ctx.heads.set(docId, head);
611
+ }
612
+ // A top-level block as `renderDoc` emits it (its subtree may be dirty from an earlier op).
613
+ function renderBlockExact(d, b) {
614
+ const solo = { ...d, leadingTrivia: "", frontmatterRaw: null, children: [b] };
615
+ return renderDoc(solo);
616
+ }
617
+ /** Shift a kernel op's `$n.ids[i]` placeholders by `offset` (an opset planned alone, spliced into a longer op list). */
618
+ export function shiftPlaceholders(op, offset) {
619
+ const s = (v) => {
620
+ const m = /^\$(\d+)\.ids\[(\d+)\]$/.exec(v);
621
+ return m ? `$${Number(m[1]) + offset}.ids[${m[2]}]` : v;
622
+ };
623
+ const to = (t) => ({
624
+ parent: typeof t.parent === "string" ? s(t.parent) : t.parent,
625
+ at: typeof t.at === "string" ? t.at : "before" in t.at ? { before: s(t.at.before) } : { after: s(t.at.after) },
626
+ });
627
+ switch (op.op) {
628
+ case "insert": return { ...op, to: to(op.to) };
629
+ case "update": return { ...op, block: s(op.block) };
630
+ case "move": return { ...op, blocks: op.blocks.map(s), to: to(op.to) };
631
+ case "remove": return { ...op, blocks: op.blocks.map(s) };
632
+ case "split": return { ...op, block: s(op.block) };
633
+ case "merge": return { ...op, blocks: op.blocks.map(s) };
634
+ }
635
+ }
636
+ /**
637
+ * Lower effect `i` (already validated): resolve its targets, classify its
638
+ * focus (throwing `conflicting_effects`, or dropping an identical duplicate
639
+ * with a diagnostic), then push its kernel ops / document drafts. Returns the
640
+ * resolved effect, or null when it was dropped.
641
+ */
642
+ export function lowerEffect(ctx, i, effect) {
643
+ const e = resolveEffect(ctx, effect);
644
+ const info = focusOf(ctx, e);
645
+ if (!ctx.tracker.check(i, info, describe(e)))
646
+ return null;
647
+ ctx.resolved.push(e);
648
+ applyLowering(ctx, i, e);
649
+ return e;
650
+ }
651
+ // The addressed/retired sets and the scalar key of an effect (§11.3 table).
652
+ function focusOf(ctx, e) {
653
+ const blocksOf = (ids) => {
654
+ const docs = new Set();
655
+ for (const id of ids)
656
+ docs.add(blockInfo(ctx, id).docId);
657
+ return { docs: [...docs], blocks: ids };
658
+ };
659
+ switch (e.op) {
660
+ case "create":
661
+ return { docs: [], blocks: [], paths: [e.path] };
662
+ case "insert": {
663
+ const doc = targetDoc(ctx, e.into);
664
+ const blocks = [...(e.into.kind === "block" ? [e.into.id] : e.into.kind === "section" ? [e.into.heading_id] : []), ...anchorIds(e.at)];
665
+ return { docs: [doc], blocks, paths: [] };
666
+ }
667
+ case "set":
668
+ case "unset": {
669
+ if (e.target.kind === "document") {
670
+ const field = e.field === "content" ? "content" : e.field;
671
+ return {
672
+ scalar: { key: `document:${e.target.id}:${field}`, target: { kind: "document", id: e.target.id }, field, op: e.op, value: e.op === "set" ? e.value : undefined },
673
+ docs: [e.target.id], blocks: [], paths: [],
674
+ ...(field === "content" ? { wholeDoc: e.target.id, retiresDocs: [e.target.id], retiresBlocks: allBlocksOf(ctx, e.target.id) } : {}),
675
+ };
676
+ }
677
+ if (e.target.kind === "block") {
678
+ const { docId } = blockInfo(ctx, e.target.id);
679
+ return {
680
+ scalar: { key: `block:${e.target.id}:${e.field}`, target: { kind: "block", id: e.target.id }, field: e.field, op: e.op, value: e.op === "set" ? e.value : undefined },
681
+ docs: [docId], blocks: [e.target.id], paths: [],
682
+ ...(e.field === "markdown" ? { retiresBlocks: descendantsOf(ctx, e.target.id) } : {}),
683
+ };
684
+ }
685
+ const node = nodeTarget(e.target);
686
+ const { docId, blockId } = nodeBlock(ctx, node.id);
687
+ return {
688
+ scalar: { key: `node:${node.id}:${e.field}`, target: { kind: "node", id: node.id }, field: e.field, op: e.op, value: e.op === "set" ? e.value : undefined },
689
+ docs: [docId], blocks: [blockId], paths: [],
690
+ };
691
+ }
692
+ case "move": {
693
+ if ("targets" in e) {
694
+ const ids = e.targets.map((t) => blockTarget(t).id);
695
+ const { docs } = blocksOf(ids);
696
+ const dst = targetDoc(ctx, e.into);
697
+ const blocks = [...ids, ...(e.into.kind === "block" ? [e.into.id] : e.into.kind === "section" ? [e.into.heading_id] : []), ...anchorIds(e.at)];
698
+ return { docs: [...new Set([...docs, dst])], blocks, paths: [] };
699
+ }
700
+ const t = docTarget(e.target);
701
+ return {
702
+ scalar: { key: `document:${t.id}:path`, target: { kind: "document", id: t.id }, field: "path", op: "set", value: e.to },
703
+ docs: [t.id], blocks: [], paths: [e.to], retiresPaths: [t.path],
704
+ };
705
+ }
706
+ case "remove": {
707
+ if ("targets" in e) {
708
+ const ids = e.targets.map((t) => blockTarget(t).id);
709
+ const { docs } = blocksOf(ids);
710
+ return { docs, blocks: ids, paths: [], retiresBlocks: ids.flatMap((id) => [id, ...descendantsOf(ctx, id)]) };
711
+ }
712
+ const t = docTarget(e.target);
713
+ return { docs: [t.id], blocks: [], paths: [], retiresDocs: [t.id], retiresBlocks: allBlocksOf(ctx, t.id) };
714
+ }
715
+ case "split": {
716
+ const id = blockTarget(e.target).id;
717
+ return { ...blocksOf([id]), paths: [] };
718
+ }
719
+ case "merge": {
720
+ const ids = e.targets.map((t) => blockTarget(t).id);
721
+ return { ...blocksOf(ids), paths: [], retiresBlocks: ids.slice(1).flatMap((id) => [id, ...descendantsOf(ctx, id)]) };
722
+ }
723
+ case "retarget": {
724
+ const plan = linksRepair(ctx.store, ctx.repoId, e.repairs, e.path_glob !== undefined ? { pathGlob: e.path_glob } : {});
725
+ const ids = plan.ops.map((o) => o.block);
726
+ return {
727
+ scalars: ids.map((id) => ({ key: `block:${id}:markdown`, target: { kind: "block", id }, field: "markdown" })),
728
+ ...blocksOf(ids), paths: [],
729
+ };
730
+ }
731
+ }
732
+ }
733
+ function anchorIds(at) {
734
+ if (at === "start" || at === "end")
735
+ return [];
736
+ return ["before" in at ? at.before : at.after];
737
+ }
738
+ function allBlocksOf(ctx, docId) {
739
+ if (isNewSymbol(docId))
740
+ return [];
741
+ return ctx.store.db.prepare("SELECT block_id FROM blocks WHERE doc_id = ? AND deleted_commit IS NULL").all(docId).map((r) => r.block_id);
742
+ }
743
+ // The lowering table (§11.3), one resolved effect → kernel ops / document entries. `i` is the effect's index.
744
+ function applyLowering(ctx, i, e) {
745
+ switch (e.op) {
746
+ case "create": {
747
+ if (findDoc(ctx.store, { repoId: ctx.repoId, path: e.path }))
748
+ throw new MutationError("path_taken", `document already exists at ${e.path}`, { path: e.path });
749
+ if (ctx.absentPaths.includes(e.path))
750
+ throw new MutationError("path_taken", `an earlier effect already creates or moves a document to ${e.path}`, { path: e.path });
751
+ if (ctx.docStore.exists(e.path))
752
+ throw new MutationError("path_taken", `file already exists on disk at ${e.path}`, { path: e.path });
753
+ // 3.0: the document symbol, then its tree parsed under the symbolic minter
754
+ // — its blocks are the next `$new.k` in pre-order — and adopted as a loaded
755
+ // document, so every later effect addresses it and its blocks like any other.
756
+ const symbol = ctx.symbols.alloc("document");
757
+ const source = composeFile(e.markdown, e.frontmatter);
758
+ ctx.planner.adoptNew(symbol, parseNewDoc(symbol, e.path, source));
759
+ ctx.creates.push({ symbol, path: e.path, source, before_op: ctx.ops.length });
760
+ ctx.absentPaths.push(e.path);
761
+ return;
762
+ }
763
+ case "insert": {
764
+ const doc = targetDoc(ctx, e.into);
765
+ checkRevision(ctx, doc, e.expect);
766
+ recordParentPin(ctx, doc, e.into, e.expect);
767
+ const to = placement(ctx, e.into, e.at);
768
+ const expect = kernelExpect(e.expect);
769
+ pushOp(ctx, { op: "insert", ...(e.into.kind === "document" ? { doc } : {}), to, markdown: e.markdown, ...(expect !== undefined ? { expect } : {}) });
770
+ return;
771
+ }
772
+ case "set":
773
+ case "unset": {
774
+ if (e.target.kind === "document") {
775
+ const docId = e.target.id;
776
+ checkRevision(ctx, docId, e.expect);
777
+ if (e.field === "content") {
778
+ if (e.op !== "set" || typeof e.value !== "string")
779
+ throw effectInvalid("`content` takes a string value: the complete proposed document");
780
+ if (isNewSymbol(docId))
781
+ throw new MutationError("target_missing", `${docId} is a document this plan creates; its content is the create's markdown`, { symbol: docId });
782
+ lowerContent(ctx, docId, e.value);
783
+ return;
784
+ }
785
+ // `frontmatter.<key>` — on a created document too (3.0): its tree is loaded, the patch is a head like any other.
786
+ const key = FRONTMATTER_FIELD.exec(e.field)[1];
787
+ const { frontmatter, body } = frontmatterOf(ctx, docId);
788
+ const merged = { ...frontmatter };
789
+ if (e.op === "set")
790
+ merged[key] = e.value;
791
+ else
792
+ delete merged[key];
793
+ patchFrontmatter(ctx, docId, merged, body);
794
+ return;
795
+ }
796
+ if (e.op !== "set")
797
+ throw effectInvalid("`unset` takes a document target");
798
+ if (e.target.kind === "block") {
799
+ const id = e.target.id;
800
+ const { docId } = blockInfo(ctx, id);
801
+ checkRevision(ctx, docId, e.expect);
802
+ const hash = pin(ctx, id, e.expect?.content_hash);
803
+ if (e.field === "markdown") {
804
+ if (typeof e.value !== "string")
805
+ throw effectInvalid("`markdown` takes a string value");
806
+ pushOp(ctx, { op: "update", block: kernelBlockId(ctx, id), markdown: e.value, expect: kernelExpect(e.expect, hash) });
807
+ }
808
+ else {
809
+ if (typeof e.value !== "boolean")
810
+ throw effectInvalid("`attrs.checked` takes a boolean value");
811
+ pushOp(ctx, { op: "update", block: kernelBlockId(ctx, id), attrs: { checked: e.value }, expect: kernelExpect(e.expect, hash) });
812
+ }
813
+ return;
814
+ }
815
+ // node: the adapter's editor → one update (macros.ts nodeSet)
816
+ const node = nodeTarget(e.target);
817
+ const { docId, blockId } = nodeBlock(ctx, node.id);
818
+ checkRevision(ctx, docId, e.expect);
819
+ const ops = nodeSet(ctx.store, node.id, e.field, typeof e.value === "string" ? e.value : ctx.canonical(e.value));
820
+ for (const op of ops) {
821
+ const u = op;
822
+ const hash = pin(ctx, blockId, e.expect?.content_hash ?? u.expect?.content_hash);
823
+ pushOp(ctx, { ...u, expect: { ...(u.expect ?? {}), content_hash: hash } });
824
+ }
825
+ return;
826
+ }
827
+ case "move": {
828
+ if ("targets" in e) {
829
+ const ids = e.targets.map((t) => blockTarget(t).id);
830
+ const dst = targetDoc(ctx, e.into);
831
+ checkRevision(ctx, dst, e.expect);
832
+ recordParentPin(ctx, dst, e.into, e.expect);
833
+ const to = placement(ctx, e.into, e.at);
834
+ const expect = kernelExpect(e.expect);
835
+ // `doc` names the destination when `into` is a document (§2.3, 3.0): a
836
+ // top-level start/end would otherwise mean the SOURCE document's.
837
+ pushOp(ctx, { op: "move", blocks: ids.map((id) => kernelBlockId(ctx, id)), to, ...(e.into.kind === "document" ? { doc: dst } : {}), ...(expect !== undefined ? { expect } : {}) });
838
+ return;
839
+ }
840
+ const t = docTarget(e.target);
841
+ if (isNewSymbol(t.id))
842
+ throw new MutationError("target_missing", `${t.id} is a document this plan creates; it cannot be moved in the same plan`, { symbol: t.id });
843
+ const base = readDocBase(ctx.store.db, t.id);
844
+ if (!base)
845
+ throw docMissing(t.id);
846
+ if (findDoc(ctx.store, { repoId: ctx.repoId, path: e.to }))
847
+ throw new MutationError("path_taken", `a document already exists at ${e.to}`, { path: e.to });
848
+ if (ctx.absentPaths.includes(e.to))
849
+ throw new MutationError("path_taken", `an earlier effect already creates or moves a document to ${e.to}`, { path: e.to });
850
+ if (ctx.docStore.exists(e.to))
851
+ throw new MutationError("path_taken", `file already exists on disk at ${e.to}`, { path: e.to });
852
+ ctx.absentPaths.push(e.to);
853
+ if (!ctx.docBases.has(t.id))
854
+ ctx.docBases.set(t.id, base);
855
+ const inbound = inboundLinksTo(ctx.store, ctx.repoId, t.id, t.path);
856
+ let retargeted = null;
857
+ if (e.retarget_inbound && inbound.length > 0) {
858
+ const plan = planInboundRetarget(ctx.store, t.id, t.path, e.to, inbound);
859
+ for (const op of plan.ops) {
860
+ const u = op;
861
+ pin(ctx, u.block, u.expect?.content_hash);
862
+ pushOp(ctx, u);
863
+ }
864
+ retargeted = { blocks: plan.blocks, docs: plan.docs };
865
+ }
866
+ ctx.moves.push({ id: t.id, from: t.path, to: e.to, currentRev: base.currentRev, inbound, retargeted });
867
+ return;
868
+ }
869
+ case "remove": {
870
+ if ("targets" in e) {
871
+ const ids = e.targets.map((t) => blockTarget(t).id);
872
+ const expect = {};
873
+ for (const id of ids) {
874
+ const given = e.expect?.[id];
875
+ const hash = pin(ctx, id, given?.content_hash);
876
+ expect[kernelBlockId(ctx, id)] = { ...(given ?? {}), content_hash: hash };
877
+ }
878
+ pushOp(ctx, { op: "remove", blocks: ids.map((id) => kernelBlockId(ctx, id)), expect });
879
+ return;
880
+ }
881
+ const t = docTarget(e.target);
882
+ if (isNewSymbol(t.id))
883
+ throw new MutationError("target_missing", `${t.id} is a document this plan creates; it cannot be removed in the same plan`, { symbol: t.id });
884
+ const base = readDocBase(ctx.store.db, t.id);
885
+ if (!base)
886
+ throw docMissing(t.id);
887
+ if (!ctx.docBases.has(t.id))
888
+ ctx.docBases.set(t.id, base);
889
+ ctx.tombstones.push({ id: t.id, path: t.path, currentRev: base.currentRev });
890
+ return;
891
+ }
892
+ case "split": {
893
+ const id = blockTarget(e.target).id;
894
+ const { docId } = blockInfo(ctx, id);
895
+ checkRevision(ctx, docId, e.expect);
896
+ const hash = pin(ctx, id, e.expect?.content_hash);
897
+ pushOp(ctx, { op: "split", block: kernelBlockId(ctx, id), at: [...e.at], expect: kernelExpect(e.expect, hash) });
898
+ return;
899
+ }
900
+ case "merge": {
901
+ const ids = e.targets.map((t) => blockTarget(t).id);
902
+ const expect = {};
903
+ for (const id of ids) {
904
+ const given = e.expect?.[id];
905
+ const hash = pin(ctx, id, given?.content_hash);
906
+ expect[kernelBlockId(ctx, id)] = { ...(given ?? {}), content_hash: hash };
907
+ }
908
+ pushOp(ctx, { op: "merge", blocks: ids.map((id) => kernelBlockId(ctx, id)), separator: e.separator, expect });
909
+ return;
910
+ }
911
+ case "retarget": {
912
+ const plan = linksRepair(ctx.store, ctx.repoId, e.repairs, e.path_glob !== undefined ? { pathGlob: e.path_glob } : {});
913
+ ctx.retargets.push({ effect_index: i, hits: plan.hits, pairs: plan.pairs });
914
+ for (const op of plan.ops) {
915
+ const u = op;
916
+ pin(ctx, u.block, u.expect?.content_hash);
917
+ pushOp(ctx, u);
918
+ }
919
+ return;
920
+ }
921
+ }
922
+ }
923
+ /**
924
+ * The symbolic minter (§11.4): `b` and `d` mints yield `$new.0, $new.1, …`
925
+ * from `symbols`, so a plan consumes no real id and the kernel (id-agnostic
926
+ * for rendering, ADR-002) renders exact bytes. Planning mints nothing else —
927
+ * `c`/`r`/`x`/`e` are minted inside the commit transaction.
928
+ */
929
+ export function symbolicMinter(symbols) {
930
+ return (prefix) => {
931
+ if (prefix === "b")
932
+ return symbols.alloc("block");
933
+ if (prefix === "d")
934
+ return symbols.alloc("document");
935
+ throw new Error(`the effect planner mints only b/d ids, not ${prefix}`);
936
+ };
937
+ }
938
+ /** `set document.content`: the whole-document planner's opset, spliced in (placeholders shifted), its frontmatter as a file head. */
939
+ function lowerContent(ctx, docId, content) {
940
+ // The whole-document planner verifies its candidates by dry-run applies,
941
+ // which mint: give them a throwaway symbol table so the plan's own `$new.n`
942
+ // sequence stays the one `execute`'s replay reproduces (the opset's ops carry
943
+ // no minted ids — an insert carries markdown — so nothing leaks).
944
+ const opset = withIdMinter(symbolicMinter(new SymbolTable()), () => planUpdate(ctx.store, ctx.repoId, "", docId, content));
945
+ if (!opset.converges) {
946
+ const why = opset.diagnostics.length > 0 ? ` — ${opset.diagnostics[opset.diagnostics.length - 1]}` : "";
947
+ throw new MutationError("plan_not_convergent", `the proposed content of ${docId} cannot be reproduced by the planner; re-plan${why}`, { doc: docId, diagnostics: opset.diagnostics });
948
+ }
949
+ // The identity consequences, surfaced as `preview.identity[doc]` (3.0; the
950
+ // 3.x `docs_update` result): each planned op by its index in the plan's ops.
951
+ const offset = ctx.ops.length;
952
+ ctx.identity.set(docId, {
953
+ matcher_v: opset.matcherV,
954
+ summary: opset.summary,
955
+ ops: opset.ops.map((p, k) => ({ op_index: offset + k, disposition: p.disposition, blocks: p.blocks, confidence: p.confidence, reason: p.reason, ...(p.detail !== undefined ? { detail: p.detail } : {}) })),
956
+ diagnostics: opset.diagnostics,
957
+ });
958
+ if (opset.ops.length === 0 && !opset.frontmatter) {
959
+ // The content IS the document (the zero-effects rule, §11.7): nothing is
960
+ // loaded, so nothing is ingested and `execute` records no commit — the
961
+ // document's triple is still a precondition (the opset's), so the plan is
962
+ // `stale_plan` once the document moves on.
963
+ const base = readDocBase(ctx.store.db, docId);
964
+ if (!base)
965
+ throw docMissing(docId);
966
+ if (!ctx.docBases.has(docId))
967
+ ctx.docBases.set(docId, base);
968
+ return;
969
+ }
970
+ const d = ctx.planner.ensureDoc(docId);
971
+ if (opset.frontmatter) {
972
+ // The whole-document planner's frontmatter keeps the leading trivia in
973
+ // front (its legacy `set_frontmatter`); as a file head that is `leading + raw`.
974
+ const head = d.leadingTrivia + (opset.frontmatter.raw ?? "");
975
+ ctx.planner.setHead(docId, head);
976
+ ctx.heads.set(docId, head);
977
+ }
978
+ for (const p of opset.ops)
979
+ pushOp(ctx, shiftPlaceholders(p.op, offset));
980
+ }
981
+ //# sourceMappingURL=effects.js.map