@omgbase/core 0.11.1 → 0.19.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 (72) 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/size.d.ts +76 -0
  9. package/dist/src/core/store/size.js +223 -0
  10. package/dist/src/core/store/store.d.ts +9 -0
  11. package/dist/src/core/store/store.js +11 -0
  12. package/dist/src/graph/history.js +15 -12
  13. package/dist/src/index.d.ts +12 -5
  14. package/dist/src/index.js +18 -7
  15. package/dist/src/mcp/arguments.d.ts +9 -2
  16. package/dist/src/mcp/arguments.js +15 -2
  17. package/dist/src/mcp/errors.d.ts +1 -1
  18. package/dist/src/mcp/errors.js +3 -0
  19. package/dist/src/mcp/reference.d.ts +1 -1
  20. package/dist/src/mcp/reference.js +27 -6
  21. package/dist/src/mcp/server.d.ts +56 -1
  22. package/dist/src/mcp/server.js +365 -203
  23. package/dist/src/mutate/apply.d.ts +93 -12
  24. package/dist/src/mutate/apply.js +203 -113
  25. package/dist/src/mutate/canonical.d.ts +27 -0
  26. package/dist/src/mutate/canonical.js +138 -0
  27. package/dist/src/mutate/commit.d.ts +150 -0
  28. package/dist/src/mutate/commit.js +349 -0
  29. package/dist/src/mutate/doc-store.d.ts +3 -3
  30. package/dist/src/mutate/doc-store.js +21 -6
  31. package/dist/src/mutate/docs.d.ts +42 -87
  32. package/dist/src/mutate/docs.js +50 -204
  33. package/dist/src/mutate/effects.d.ts +363 -0
  34. package/dist/src/mutate/effects.js +1161 -0
  35. package/dist/src/mutate/materialize.d.ts +49 -0
  36. package/dist/src/mutate/materialize.js +174 -0
  37. package/dist/src/mutate/opset.d.ts +27 -2
  38. package/dist/src/mutate/opset.js +12 -5
  39. package/dist/src/mutate/plan-update.d.ts +1 -46
  40. package/dist/src/mutate/plan-update.js +19 -67
  41. package/dist/src/mutate/plan.d.ts +210 -0
  42. package/dist/src/mutate/plan.js +512 -0
  43. package/dist/src/mutate/select.d.ts +89 -0
  44. package/dist/src/mutate/select.js +879 -0
  45. package/dist/src/mutate/sugar.d.ts +135 -0
  46. package/dist/src/mutate/sugar.js +167 -0
  47. package/dist/src/mutate/tree.d.ts +4 -2
  48. package/dist/src/oqx-js/planner.js +4 -0
  49. package/dist/src/oqx-js/run.d.ts +28 -1
  50. package/dist/src/oqx-js/run.js +85 -22
  51. package/dist/src/spec-versions.d.ts +6 -6
  52. package/dist/src/spec-versions.js +5 -5
  53. package/dist/src/surface-paths.d.ts +17 -26
  54. package/dist/src/surface-paths.js +27 -22
  55. package/dist/src/sync/checkpoint.d.ts +5 -2
  56. package/dist/src/sync/checkpoint.js +10 -6
  57. package/dist/src/sync/freshness.d.ts +10 -7
  58. package/dist/src/sync/freshness.js +15 -17
  59. package/dist/src/sync/observe.d.ts +4 -1
  60. package/dist/src/sync/observe.js +56 -5
  61. package/dist/src/sync/recovery.d.ts +4 -0
  62. package/dist/src/sync/recovery.js +4 -1
  63. package/dist/src/sync/stat-cache.d.ts +7 -0
  64. package/dist/src/sync/stat-cache.js +22 -0
  65. package/dist/src/sync/tombstone.js +23 -19
  66. package/dist/src/sync/watcher.d.ts +2 -0
  67. package/dist/src/sync/watcher.js +4 -1
  68. package/dist/src/sync/writer-lock.d.ts +15 -0
  69. package/dist/src/sync/writer-lock.js +104 -17
  70. package/dist/src/version.d.ts +7 -2
  71. package/dist/src/version.js +5 -2
  72. package/package.json +2 -2
@@ -0,0 +1,512 @@
1
+ import { sha256 } from "../core/hash.js";
2
+ import { observeMints, withIdMinter } from "../core/ids.js";
3
+ import { findDoc } from "../core/read/reader.js";
4
+ import { inboundLinksTo } from "../graph/inbound-links.js";
5
+ import { withWriterLock } from "../sync/writer-lock.js";
6
+ import { ChangesetPlanner } from "./apply.js";
7
+ import { commitChangeset, readDocBase } from "./commit.js";
8
+ import { resolveDocStore } from "./doc-store.js";
9
+ import { parseNewDoc } from "./docs.js";
10
+ import { ConflictTracker, SymbolTable, isNewSymbol, lowerEffect, symbolicMinter, effectInvalid, } from "./effects.js";
11
+ import { compilePlain, compileSelection, selectionInvalid } from "./select.js";
12
+ import { MutationError } from "./tree.js";
13
+ import { canonicalJson, fixRequest, rootOfId, narrowedTo } from "./canonical.js";
14
+ // The Plan (spec/mutate §11.4–§11.7, 2.3): the frozen, executable, digest-named
15
+ // result of lowering an ordered effect list. `plan` writes nothing; `execute`
16
+ // commits exactly a plan — its preconditions re-validated inside the
17
+ // transaction by `commit.ts` — and `apply({ effects })` is plan + execute under
18
+ // one writer lock and one transaction. New identities are symbolic (`$new.n`,
19
+ // minted at execute, mapped in the receipt); a plan's identity is the sha256 of
20
+ // the canonical JSON of its executable core. Since 3.1 (§11.8) a request may
21
+ // carry an OQX `select` whose hits, bound to the `effects` as templates, ARE
22
+ // the effects (`select.ts`); the frozen `selection` is the core's seventh member.
23
+ export const PLAN_VERSION = 1;
24
+ /** The lowering + conflict rules version (§11.4; the `matcher_v` precedent). */
25
+ export const PLANNER_V = "p3";
26
+ // ---- canonical JSON and the digest (§11.5) ----------------------------------------------
27
+ export { canonicalJson } from "./canonical.js";
28
+ /** The executable core of a plan — the six digested keys (seven with `selection`), nothing else. */
29
+ export function planCore(p) {
30
+ return {
31
+ version: p.version, planner_v: p.planner_v, effects: p.effects, opset: p.opset, preconditions: p.preconditions, new_ids: p.new_ids,
32
+ ...(p.selection !== undefined ? { selection: p.selection } : {}),
33
+ };
34
+ }
35
+ /** `plan_id = hex(sha256(canonical(core)))` (§11.5). */
36
+ export function planDigest(core) {
37
+ return sha256(canonicalJson(planCore(core))).toString("hex");
38
+ }
39
+ /** The kernel op as the plan carries it (`childIds` → `child_ids`). */
40
+ export function wireOp(o) {
41
+ if (o.op === "update") {
42
+ const { childIds, ...rest } = o;
43
+ return { ...rest, ...(childIds !== undefined ? { child_ids: childIds } : {}) };
44
+ }
45
+ return o;
46
+ }
47
+ /** A plan's op back to the kernel's spelling. */
48
+ export function kernelOp(o) {
49
+ if (o.op === "update") {
50
+ const { child_ids, ...rest } = o;
51
+ return { ...rest, ...(child_ids !== undefined ? { childIds: child_ids } : {}) };
52
+ }
53
+ return o;
54
+ }
55
+ /**
56
+ * Lower an ordered effect list into a frozen Plan (§11.6): under the symbolic
57
+ * minter, resolve and lower each effect in order (conflicts by focus kind,
58
+ * identical duplicates dropped with a diagnostic), render the touched documents,
59
+ * freeze the preconditions, digest the core. Reads only; mints no real id.
60
+ */
61
+ export function planEffects(store, req) {
62
+ // A selector request (§11.8): the templates compile into the concrete effects
63
+ // — hits × templates — and the frozen `selection` joins the core; an empty
64
+ // selection is an empty plan (the non-empty rule of §11.1 is the templates').
65
+ let effects;
66
+ let selection;
67
+ let templates;
68
+ let origins = [];
69
+ if (req.select !== undefined || req.expect !== undefined) {
70
+ if (req.select === undefined)
71
+ throw selectionInvalid("`expect` bounds a selection — pass `select` with it");
72
+ const compiled = compileSelection(store, req.repoId, req.select, req.expect, req.effects);
73
+ effects = compiled.effects;
74
+ selection = compiled.selection;
75
+ origins = compiled.origins;
76
+ }
77
+ else {
78
+ // (3.2) the `-> ` sugar and expressions over each effect's own target row; without any, the shape check alone.
79
+ const compiled = compilePlain(store, req.repoId, req.effects);
80
+ effects = compiled.effects;
81
+ templates = compiled.templates;
82
+ origins = compiled.origins;
83
+ }
84
+ const docStore = resolveDocStore(req);
85
+ const symbols = new SymbolTable();
86
+ return withIdMinter(symbolicMinter(symbols), () => {
87
+ const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
88
+ const ctx = {
89
+ store, repoId: req.repoId, docStore, planner, symbols,
90
+ blockSymbols: new Map(), creates: [], identity: new Map(), retargets: [], moves: [], tombstones: [], heads: new Map(), ops: [],
91
+ pins: new Map(), parents: [], absentPaths: [], docBases: new Map(),
92
+ tracker: new ConflictTracker(canonicalJson), canonical: canonicalJson, resolved: [], summary: [],
93
+ };
94
+ effects.forEach((e, i) => {
95
+ try {
96
+ lowerEffect(ctx, i, e);
97
+ }
98
+ catch (err) {
99
+ // Every failure names the effect it arose from (the kernel's own data — `op_index`, `block` — stays);
100
+ // an expanded effect's names its template and the hit it was bound to as well (§11.8).
101
+ if (err instanceof MutationError) {
102
+ if (err.data.effect_index === undefined)
103
+ err.data = { ...err.data, effect_index: i };
104
+ const origin = origins[i];
105
+ if (origin !== undefined && err.data.row === undefined) {
106
+ err.data = { ...err.data, ...(origin.template_index !== undefined ? { template_index: origin.template_index } : {}), row: origin.row };
107
+ }
108
+ // (3.3) the corrected example, built from the request as given (§8 "fix").
109
+ const fix = planningFix(err, i, req, origins, symbols);
110
+ if (fix !== undefined && err.data.fix === undefined)
111
+ err.data = { ...err.data, fix };
112
+ }
113
+ throw err;
114
+ }
115
+ });
116
+ const changeset = planner.finish();
117
+ const opset = {
118
+ creates: ctx.creates,
119
+ moves: ctx.moves.map((m) => ({ id: m.id, from: m.from, to: m.to })),
120
+ tombstones: ctx.tombstones.map((t) => ({ id: t.id, path: t.path })),
121
+ set_frontmatter: [...ctx.heads].map(([doc, raw]) => ({ doc, raw })),
122
+ ops: ctx.ops.map(wireOp),
123
+ };
124
+ const docs = changeset.preconditions.map((p) => ({ id: p.id, path: p.path, base_revision: p.currentRev, hash: p.fileHash }));
125
+ for (const [id, base] of ctx.docBases) {
126
+ if (!docs.some((d) => d.id === id))
127
+ docs.push({ id, path: base.path, base_revision: base.currentRev, hash: base.fileHash });
128
+ }
129
+ const preconditions = {
130
+ docs,
131
+ blocks: [...ctx.pins].map(([id, content_hash]) => ({ id, content_hash })),
132
+ parents: ctx.parents,
133
+ absent_paths: ctx.absentPaths,
134
+ };
135
+ const core = { version: PLAN_VERSION, planner_v: PLANNER_V, effects: ctx.resolved, opset, preconditions, new_ids: symbols.entries, ...(selection !== undefined ? { selection } : {}) };
136
+ const plan_id = planDigest(core);
137
+ // The preview (not digested): the kernel results, every revision the commit
138
+ // would record — a created document's under its symbol, from its adopted
139
+ // tree like any loaded document's — and the per-file diffs: a create
140
+ // `"" → bytes`, a move two entries (its retargeted bytes land at the new
141
+ // path), a removal `bytes → ""`.
142
+ const diffs = { ...changeset.diffs };
143
+ const revisions = [...changeset.revisions];
144
+ for (const m of ctx.moves) {
145
+ const bytes = docStore.read(m.from) ?? "";
146
+ const own = diffs[m.from];
147
+ delete diffs[m.from];
148
+ diffs[m.from] = { before: bytes, after: "" };
149
+ diffs[m.to] = { before: "", after: own?.after ?? bytes };
150
+ }
151
+ for (const t of ctx.tombstones)
152
+ diffs[t.path] = { before: docStore.read(t.path) ?? "", after: "" };
153
+ for (const r of revisions) {
154
+ const moved = ctx.moves.find((m) => m.id === r.id);
155
+ if (moved)
156
+ r.path = moved.to;
157
+ }
158
+ const preview = { results: changeset.results, revisions, diffs, summary: ctx.summary };
159
+ if (ctx.identity.size > 0)
160
+ preview.identity = Object.fromEntries(ctx.identity);
161
+ if (ctx.retargets.length > 0)
162
+ preview.retarget = ctx.retargets;
163
+ return { ...core, plan_id, diagnostics: ctx.tracker.diagnostics, ...(templates !== undefined ? { templates } : {}), preview };
164
+ });
165
+ }
166
+ // ---- the corrected examples of planning errors (§8 "fix", 3.3) ------------------------------
167
+ /** The request as the caller gave it, for a `fix`: `select`/`expect` only when present. */
168
+ function requestOf(req) {
169
+ return { effects: req.effects, ...(req.select !== undefined ? { select: req.select } : {}), ...(req.expect !== undefined ? { expect: req.expect } : {}) };
170
+ }
171
+ /**
172
+ * `conflicting_effects`: keep the later effect — the request minus the earlier
173
+ * one; under a selector, a cross-hit collision narrows the selector to the
174
+ * later hit (`from <root> where id == …`, `expect.max: 1`) and a same-hit
175
+ * collision drops the earlier template. `stale_plan` at plan time (an
176
+ * `expect.revision` that no longer holds): the same request with the current
177
+ * revision in that guard. `target_missing { symbol }` for a `$new.n` no earlier
178
+ * effect created: the next `create` of a plain request moved before the effect
179
+ * that uses it, the symbol renamed to the one that create will allocate.
180
+ */
181
+ function planningFix(err, i, req, origins, symbols) {
182
+ const effects = Array.isArray(req.effects) ? req.effects : null;
183
+ if (effects === null)
184
+ return undefined;
185
+ const selector = req.select !== undefined;
186
+ if (err.code === "conflicting_effects" && Array.isArray(err.data.effects)) {
187
+ const [a, b] = err.data.effects;
188
+ if (!selector)
189
+ return fixRequest({ effects: effects.filter((_, k) => k !== a) });
190
+ const oa = origins[a];
191
+ const ob = origins[b];
192
+ if (oa === undefined || ob === undefined || oa.template_index === undefined)
193
+ return undefined;
194
+ if (oa.row.id !== ob.row.id)
195
+ return fixRequest({ select: narrowedTo(rootOfId(ob.row.id), ob.row.id), effects, expect: { max: 1 } });
196
+ return fixRequest({ ...requestOf(req), effects: effects.filter((_, k) => k !== oa.template_index) });
197
+ }
198
+ if (err.code === "stale_plan" && isRecord(err.data.current) && typeof err.data.current.revision === "string") {
199
+ const t = selector ? origins[i]?.template_index : i;
200
+ if (t === undefined || !isRecord(effects[t]))
201
+ return undefined;
202
+ const e = effects[t];
203
+ const expect = { ...(isRecord(e.expect) ? e.expect : {}), revision: err.data.current.revision };
204
+ return fixRequest({ ...requestOf(req), effects: effects.map((x, k) => (k === t ? { ...e, expect } : x)) });
205
+ }
206
+ if (err.code === "target_missing" && typeof err.data.symbol === "string" && !selector && symbols.kindOf(err.data.symbol) === undefined) {
207
+ const c = effects.findIndex((e, k) => k > i && isRecord(e) && e.op === "create");
208
+ if (c < 0)
209
+ return undefined;
210
+ const symbol = `$new.${symbols.entries.length}`;
211
+ const reordered = effects.filter((_, k) => k !== c);
212
+ reordered.splice(i, 0, effects[c]);
213
+ return fixRequest({ effects: reordered.map((e, k) => (k === i + 1 ? substituteSymbols(e, { [err.data.symbol]: symbol }) : e)) });
214
+ }
215
+ return undefined;
216
+ }
217
+ /** The `fix` of a plan that cannot be executed as it stands (`stale_plan`, `plan_mismatch`): re-plan and commit in one call — `apply` over the plan's selector form when it has one, else over its concrete effects (none when the plan has no effects: `apply` takes at least one). */
218
+ function replanFix(plan) {
219
+ if (plan.selection !== undefined) {
220
+ const sel = plan.selection;
221
+ return fixRequest({ select: sel.query, effects: sel.templates, expect: sel.expect ?? { max: sel.cardinality } });
222
+ }
223
+ return Array.isArray(plan.effects) && plan.effects.length > 0 ? fixRequest({ effects: plan.effects }) : undefined;
224
+ }
225
+ function isRecord(v) {
226
+ return typeof v === "object" && v !== null && !Array.isArray(v);
227
+ }
228
+ /**
229
+ * The plan form's admission (§11.7): `version` first — anything but 1 is
230
+ * `unsupported_plan_version { version, supported: [1] }` — then the core's keys
231
+ * must be present (`filter_invalid`), then the recomputed digest must equal the
232
+ * embedded `plan_id` (`plan_mismatch { expected, actual }`). `preview` and
233
+ * `diagnostics` are ignored.
234
+ */
235
+ export function admitPlan(input) {
236
+ if (!isRecord(input))
237
+ throw effectInvalid("`plan` must be an object");
238
+ if (input.version !== PLAN_VERSION) {
239
+ throw new MutationError("unsupported_plan_version", `plan version ${JSON.stringify(input.version)} is not supported (supported: ${PLAN_VERSION})`, { version: input.version ?? null, supported: [PLAN_VERSION] });
240
+ }
241
+ for (const k of ["planner_v", "effects", "opset", "preconditions", "new_ids", "plan_id"]) {
242
+ if (!(k in input))
243
+ throw effectInvalid(`\`plan.${k}\` is required`);
244
+ }
245
+ if (typeof input.planner_v !== "string")
246
+ throw effectInvalid("`plan.planner_v` must be a string");
247
+ if (!Array.isArray(input.effects))
248
+ throw effectInvalid("`plan.effects` must be an array");
249
+ if (!isRecord(input.opset))
250
+ throw effectInvalid("`plan.opset` must be an object");
251
+ for (const k of ["creates", "moves", "tombstones", "set_frontmatter", "ops"]) {
252
+ if (!Array.isArray(input.opset[k]))
253
+ throw effectInvalid(`\`plan.opset.${k}\` must be an array`);
254
+ }
255
+ for (const c of input.opset.creates) {
256
+ if (!isRecord(c) || typeof c.symbol !== "string" || typeof c.path !== "string" || typeof c.source !== "string" || typeof c.before_op !== "number") {
257
+ throw effectInvalid("`plan.opset.creates[]` entries are { symbol, path, source, before_op }");
258
+ }
259
+ }
260
+ if (!isRecord(input.preconditions))
261
+ throw effectInvalid("`plan.preconditions` must be an object");
262
+ for (const k of ["docs", "blocks", "parents", "absent_paths"]) {
263
+ if (!Array.isArray(input.preconditions[k]))
264
+ throw effectInvalid(`\`plan.preconditions.${k}\` must be an array`);
265
+ }
266
+ if (!Array.isArray(input.new_ids))
267
+ throw effectInvalid("`plan.new_ids` must be an array");
268
+ if (input.selection !== undefined) {
269
+ // (3.1) the optional seventh member: its shape, so the digest below covers what it says.
270
+ const sel = input.selection;
271
+ if (!isRecord(sel) || typeof sel.query !== "string" || !Array.isArray(sel.templates) || typeof sel.cardinality !== "number" || !Array.isArray(sel.identities)) {
272
+ throw effectInvalid("`plan.selection` is { query, expect?, templates, cardinality, identities }");
273
+ }
274
+ if (sel.expect !== undefined && (!isRecord(sel.expect) || typeof sel.expect.max !== "number"))
275
+ throw effectInvalid("`plan.selection.expect` is { max }");
276
+ }
277
+ if (typeof input.plan_id !== "string")
278
+ throw effectInvalid("`plan.plan_id` must be a string");
279
+ // (3.2) the written effects ride outside the digest, like `diagnostics`; their shape is all that is checked.
280
+ if (input.templates !== undefined && !Array.isArray(input.templates))
281
+ throw effectInvalid("`plan.templates` must be an array");
282
+ const plan = input;
283
+ const actual = planDigest(plan);
284
+ if (actual !== plan.plan_id) {
285
+ throw new MutationError("plan_mismatch", "the plan's digest does not match its plan_id; it was edited or produced by a different planner — re-plan", { expected: plan.plan_id, actual, fix: replanFix(plan) });
286
+ }
287
+ return plan;
288
+ }
289
+ /** `execute { plan }` (§11.7): admit, then commit exactly that plan. */
290
+ export function executePlan(store, req) {
291
+ const plan = admitPlan(req.plan);
292
+ const docStore = resolveDocStore(req);
293
+ return underLock(req, () => executeAdmitted(store, req, plan, docStore));
294
+ }
295
+ /** The selector half of a request, threaded to the planner as given (absent keys stay absent). */
296
+ function selectorOf(req) {
297
+ return { ...(req.select !== undefined ? { select: req.select } : {}), ...(req.expect !== undefined ? { expect: req.expect } : {}) };
298
+ }
299
+ /** `execute { plan_id, effects, select?, expect? }` (§11.7): re-plan (with the selector when one is given, §11.8); the digest must equal `plan_id`, else `plan_mismatch`. */
300
+ export function executeByDigest(store, req) {
301
+ const docStore = resolveDocStore(req);
302
+ return underLock(req, () => {
303
+ const plan = planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects, ...selectorOf(req) });
304
+ if (plan.plan_id !== req.planId) {
305
+ throw new MutationError("plan_mismatch", "the effects or the documents they target changed since the plan was made; re-plan", { expected: req.planId, actual: plan.plan_id, fix: fixRequest(requestOf(req)) });
306
+ }
307
+ return executeAdmitted(store, req, plan, docStore);
308
+ });
309
+ }
310
+ /**
311
+ * `apply { effects }` with both halves kept (the sugar's seam — `sugar.ts`,
312
+ * the `omg` verbs, the whole-document update): the Plan always, and the
313
+ * Receipt when it committed (`dryRun` plans only — the CLI's `--dry-run`,
314
+ * spec/cli §3.6). Plan + execute run under one lock and one transaction,
315
+ * exactly as `applyEffects`.
316
+ */
317
+ export function applyEffectsDetailed(store, req) {
318
+ const docStore = resolveDocStore(req);
319
+ const planIt = () => planEffects(store, { repoId: req.repoId, docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}), effects: req.effects, ...selectorOf(req) });
320
+ if (req.dryRun)
321
+ return { plan: planIt() };
322
+ // One-shot `apply` with a selector must bound it (§11.8): the caller never saw
323
+ // the hits. The fix (3.3) is the same call bounded at the selection's actual
324
+ // size — the selector compiles (reads only) to count it; a selection the
325
+ // compile itself refuses reports its own error and fix.
326
+ if (req.select !== undefined && req.expect === undefined) {
327
+ const { selection } = compileSelection(store, req.repoId, req.select, undefined, req.effects);
328
+ throw selectionInvalid("`expect.max` is required on `apply` with `select` — bound the selection, or preview it with `plan`", { fix: fixRequest({ select: req.select, effects: req.effects, expect: { max: selection.cardinality } }) });
329
+ }
330
+ return underLock(req, () => {
331
+ const plan = planIt();
332
+ return { plan, receipt: executeAdmitted(store, req, plan, docStore) };
333
+ });
334
+ }
335
+ /** `apply { effects, select?, expect? }` (§11.7): plan + execute under one lock and one transaction; `dryRun` returns the Plan. */
336
+ export function applyEffects(store, req) {
337
+ const { plan, receipt } = applyEffectsDetailed(store, req);
338
+ return receipt ?? plan;
339
+ }
340
+ // ---- symbols on the wire ----------------------------------------------------------------
341
+ /** Every string that is a key of `ids` replaced by its value, recursively (arrays and plain objects) — a plan's `$new.n` realized from a receipt's `new_ids`. */
342
+ export function substituteSymbols(value, ids) {
343
+ if (typeof value === "string")
344
+ return (Object.hasOwn(ids, value) ? ids[value] : value);
345
+ if (Array.isArray(value))
346
+ return value.map((x) => substituteSymbols(x, ids));
347
+ if (typeof value === "object" && value !== null) {
348
+ const out = {};
349
+ for (const [k, v] of Object.entries(value))
350
+ out[k] = substituteSymbols(v, ids);
351
+ return out;
352
+ }
353
+ return value;
354
+ }
355
+ /**
356
+ * A planner error as the sugar reports it (`sugar.ts` `applySugar`): the
357
+ * `effect_index` the planner adds (§11.6) is dropped — the tool or verb built
358
+ * the effects itself, so the index names nothing the caller sent and the
359
+ * kernel's data (`op_index`, `block`, `current`, …) stays as pinned.
360
+ */
361
+ export function stripEffectIndex(err) {
362
+ if (err instanceof MutationError && Object.hasOwn(err.data, "effect_index")) {
363
+ const { effect_index: _dropped, ...rest } = err.data;
364
+ err.data = rest;
365
+ }
366
+ return err;
367
+ }
368
+ function underLock(req, fn) {
369
+ return req.omgbaseDir ? withWriterLock(req.omgbaseDir, fn) : fn();
370
+ }
371
+ /**
372
+ * Commit an admitted plan: the preconditions pre-checked (`stale_plan` /
373
+ * `doc_missing` / `path_taken`, outside the transaction, so a stale plan fails
374
+ * before any id is minted), the opset replayed under the production minter
375
+ * (`b` ids minted here, before the transaction — as every request's are; a
376
+ * created document is re-adopted from its bytes at its `before_op` position,
377
+ * so the replay mints in the plan's order and the i-th `b` IS the i-th block
378
+ * symbol — the alias the `$new.k` in the ops resolve through), the internal
379
+ * effects built in order — moves, the rendered documents (created ones as
380
+ * `ingest_known { base: null }`; a moved document's revision lands at its new
381
+ * path), tombstones — and `commitChangeset` re-validates every triple inside
382
+ * `BEGIN IMMEDIATE`. The commit row's `ops` is the intent record
383
+ * `canonical({ v: 1, plan_id, effects, selection?, templates? })` (§11.8: the
384
+ * selector, its templates and the hits it bound ride along when the plan had
385
+ * one; a plain request's written effects when one carried an expression, 3.2).
386
+ */
387
+ function executeAdmitted(store, req, plan, docStore) {
388
+ for (const d of plan.preconditions.docs) {
389
+ const row = readDocBase(store.db, d.id);
390
+ if (!row)
391
+ throw new MutationError("doc_missing", `doc ${d.id} no longer exists`, { doc: d.id });
392
+ if (row.path !== d.path || row.currentRev !== d.base_revision || row.fileHash !== d.hash) {
393
+ throw new MutationError("stale_plan", `document ${d.id} changed since the plan was made; re-plan`, {
394
+ doc: d.id,
395
+ expected_revision: d.base_revision,
396
+ current: { revision: row.currentRev, hash: row.fileHash },
397
+ retriable: true,
398
+ fix: replanFix(plan),
399
+ });
400
+ }
401
+ }
402
+ for (const path of plan.preconditions.absent_paths) {
403
+ if (findDoc(store, { repoId: req.repoId, path }))
404
+ throw new MutationError("path_taken", `document already exists at ${path}`, { path });
405
+ if (docStore.exists(path))
406
+ throw new MutationError("path_taken", `file already exists on disk at ${path}`, { path });
407
+ }
408
+ const minted = { b: [], d: [] };
409
+ const ts = req.ts ?? new Date().toISOString();
410
+ const head = {
411
+ origin: "api",
412
+ actor: req.origin.actor,
413
+ reason: req.origin.reason ?? null,
414
+ ops: canonicalJson({
415
+ v: 1, plan_id: plan.plan_id, effects: plan.effects,
416
+ ...(plan.selection !== undefined ? { selection: plan.selection } : {}),
417
+ ...(plan.templates !== undefined ? { templates: plan.templates } : {}),
418
+ }),
419
+ };
420
+ const stat = req.omgbaseDir !== undefined;
421
+ const moved = new Map(plan.opset.moves.map((m) => [m.id, m]));
422
+ const blockSymbols = plan.new_ids.filter((s) => s.kind === "block").map((s) => s.symbol);
423
+ const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
424
+ const { effects, outcome } = observeMints((prefix, id) => {
425
+ if (prefix === "b") {
426
+ // The i-th `b` the replay mints is the i-th block symbol (§11.7): alias it
427
+ // so an op naming a created document's block by its symbol resolves.
428
+ const symbol = blockSymbols[minted.b.length];
429
+ minted.b.push(id);
430
+ if (symbol !== undefined)
431
+ planner.alias(symbol, id);
432
+ }
433
+ else if (prefix === "d")
434
+ minted.d.push(id);
435
+ }, () => {
436
+ const heads = plan.opset.set_frontmatter;
437
+ for (const h of heads)
438
+ if (!isNewSymbol(h.doc))
439
+ planner.setHead(h.doc, h.raw);
440
+ // Re-adopt each created document where the plan did (`before_op`), its
441
+ // head applied once its tree is loaded; the ops between in order.
442
+ const creates = plan.opset.creates;
443
+ let ci = 0;
444
+ const adoptUpTo = (opIndex) => {
445
+ while (ci < creates.length && creates[ci].before_op <= opIndex) {
446
+ const c = creates[ci++];
447
+ planner.adoptNew(c.symbol, parseNewDoc(c.symbol, c.path, c.source));
448
+ for (const h of heads)
449
+ if (h.doc === c.symbol)
450
+ planner.setHead(c.symbol, h.raw);
451
+ }
452
+ };
453
+ plan.opset.ops.forEach((op, i) => {
454
+ adoptUpTo(i);
455
+ planner.push(kernelOp(op));
456
+ });
457
+ adoptUpTo(plan.opset.ops.length);
458
+ const changeset = planner.finish();
459
+ const effects = [];
460
+ for (const m of plan.opset.moves) {
461
+ const bytes = docStore.read(m.from) ?? "";
462
+ const rev = readDocBase(store.db, m.id)?.currentRev ?? null;
463
+ effects.push({ kind: "move_path", docId: m.id, from: m.from, to: m.to, currentRev: rev, inbound: inboundLinksTo(store, req.repoId, m.id, m.from), bytes });
464
+ if (stat)
465
+ effects.push({ kind: "clear_stat", path: m.from }, { kind: "record_stat", path: m.to, bytes });
466
+ }
467
+ for (const e of changeset.effects) {
468
+ // The moved document is loaded under its OLD path (the row changes
469
+ // inside the transaction); its rewritten bytes belong at the new path.
470
+ if (e.kind === "ingest_known" && moved.has(e.docId))
471
+ effects.push({ ...e, path: moved.get(e.docId).to });
472
+ else if (e.kind === "record_stat") {
473
+ const m = plan.opset.moves.find((x) => x.from === e.path);
474
+ effects.push(m ? { ...e, path: m.to } : e);
475
+ }
476
+ else
477
+ effects.push(e);
478
+ }
479
+ for (const t of plan.opset.tombstones) {
480
+ const rev = readDocBase(store.db, t.id)?.currentRev ?? null;
481
+ effects.push({ kind: "tombstone", docId: t.id, path: t.path, currentRev: rev, pool: false });
482
+ if (stat)
483
+ effects.push({ kind: "clear_stat", path: t.path });
484
+ }
485
+ const outcome = commitChangeset(store, req.repoId, head, effects, ts, { docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}) });
486
+ return { effects, outcome };
487
+ });
488
+ // The receipt's revisions, in effect order; the ingest results pair with the
489
+ // ingest effects in order (a created document's id is the one its ingest minted).
490
+ const revisions = [];
491
+ let j = 0;
492
+ for (const e of effects) {
493
+ if (e.kind === "ingest_reconciled")
494
+ revisions.push({ id: outcome.ingests[j++].docId, path: e.path, hash: sha256(e.source).toString("hex") });
495
+ else if (e.kind === "ingest_known")
496
+ revisions.push({ id: outcome.ingests[j++].docId, path: e.path, hash: sha256(e.rendered).toString("hex") });
497
+ }
498
+ // `$new.n` → minted id: the i-th block symbol is the i-th `b` the replay
499
+ // minted (all before the transaction, in the plan's order — the aliases
500
+ // above), the i-th document symbol the i-th `d` the created documents'
501
+ // ingests minted inside it (load order = effect order).
502
+ const newIds = {};
503
+ let bi = 0;
504
+ let di = 0;
505
+ for (const s of plan.new_ids) {
506
+ const id = s.kind === "block" ? minted.b[bi++] : minted.d[di++];
507
+ if (id !== undefined)
508
+ newIds[s.symbol] = id;
509
+ }
510
+ return { plan_id: plan.plan_id, commit: outcome.commitId, revisions, new_ids: newIds, committed: true, summary: plan.preview?.summary ?? [] };
511
+ }
512
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1,89 @@
1
+ import { type Query } from "@omgbase/oqx";
2
+ import type { Store } from "../core/store/store.js";
3
+ import { type Effect } from "./effects.js";
4
+ import { MutationError } from "./tree.js";
5
+ import { type FixRequest } from "./canonical.js";
6
+ /** The implicit bound on a selection without `expect.max` (hits beyond it are `selection_too_large`). */
7
+ export declare const SELECTION_CEILING = 1000;
8
+ export interface SelectionExpect {
9
+ max: number;
10
+ }
11
+ /** The seventh digested member of a plan's core (§11.4): the selector and its templates AS GIVEN, the hits it bound. */
12
+ export interface Selection {
13
+ query: string;
14
+ expect?: SelectionExpect;
15
+ templates: unknown[];
16
+ cardinality: number;
17
+ /** the hit ids, result order */
18
+ identities: string[];
19
+ }
20
+ /** Where an expanded effect came from (every error from one carries both, §11.8); a plain effect's
21
+ * expressions (3.2) have a row but no template. */
22
+ export interface EffectOrigin {
23
+ template_index?: number;
24
+ row: {
25
+ id: string;
26
+ path: string;
27
+ };
28
+ }
29
+ export interface CompiledSelection {
30
+ /** the concrete, validated effects: hits × templates */
31
+ effects: Effect[];
32
+ selection: Selection;
33
+ /** expanded index → its origin */
34
+ origins: EffectOrigin[];
35
+ }
36
+ /** (3.2) A plain request compiled: the concrete effects and, when any effect carried an expression, the list as written. */
37
+ export interface CompiledPlain {
38
+ effects: Effect[];
39
+ /** the effects AS WRITTEN — present only when at least one slot was an expression (the intent record's `templates`) */
40
+ templates?: unknown[];
41
+ /** effect index → the row its expressions were read from (sparse; literal effects have none) */
42
+ origins: (EffectOrigin | undefined)[];
43
+ }
44
+ /** The admission pass's refusal: `filter_invalid { reason, hint: "selection" }`. */
45
+ export declare function selectionInvalid(message: string, data?: Record<string, unknown>): MutationError;
46
+ /**
47
+ * `"-> <expr>"` → `{ oqx: "<expr>" }` — or, when the expression is a bare
48
+ * string literal or a template literal without interpolation, the literal
49
+ * itself (constant folding: `-> "x"` and `"x"` plan identically, and
50
+ * `-> "-> note"` is how a literal that begins with the leader is spelled).
51
+ * A parse error is `filter_invalid { hint: "selection" }` with its offset
52
+ * counted from the start of the written string (past the leader).
53
+ */
54
+ export declare function desugarLeader(v: string, here: string): unknown;
55
+ /**
56
+ * Desugar one effect or template as written (§11.8 "Sugar"), before the shape
57
+ * check and the admission: in every slot that admits an expression — the
58
+ * strings `markdown`, `to`, `path`, `separator`, `at.before` / `at.after`,
59
+ * the JSON `value`, each top-level `create.frontmatter` value, a target's `id`
60
+ * / `path`, and a whole target (`into`, `target`, `targets[k]`) — a string
61
+ * beginning `-> ` becomes `{ oqx }` (or folds to its literal); a `-> ` string
62
+ * anywhere else (`expect`, `field`, `op`, a section's `heading_id`, …) is
63
+ * refused. Every other string is literal data, with no escaping and no `${`
64
+ * scanning; a string nested inside a `value` or a frontmatter value is data
65
+ * (the slot is the top-level value, as for `{ oqx }`). Shape problems are the
66
+ * shape check's.
67
+ */
68
+ export declare function desugarEffect(raw: unknown, index: number, request?: FixRequest): unknown;
69
+ /**
70
+ * Compile `{ select, expect?, effects: templates }` into the concrete effects
71
+ * (§11.8): admit statically, rewrite, run once over one snapshot with
72
+ * `limit = bound + 1`, refuse a selection beyond the bound, bind every hit to
73
+ * every template. Reads only.
74
+ */
75
+ export declare function compileSelection(store: Store, repoId: string, select: unknown, expectRaw: unknown, templatesRaw: unknown): CompiledSelection;
76
+ /** The query a selector request compiles to (admission + rewrite, before any row is read) — `print` it to see what runs. */
77
+ export declare function selectionQuery(select: unknown, templates: unknown): Query;
78
+ /**
79
+ * Compile a plain `effects` list (no selector): desugar the `-> ` leaders,
80
+ * then — when nothing is an expression — the shape check as before (3.0
81
+ * byte-identical). Otherwise each effect with expressions is admitted as a
82
+ * template over its own primary target's row: the effects are grouped by that
83
+ * row's identity, one synthetic query per identity — `from <root> where id ==
84
+ * "<id>"` with the slot items appended, named `__omg_sel_<effectIndex>_<slot>`
85
+ * — runs, and the row binds the slots exactly as a selector's hit would. The
86
+ * concrete effects feed the lowering unchanged; `templates` is the list as
87
+ * written, for the intent record.
88
+ */
89
+ export declare function compilePlain(store: Store, repoId: string, raw: unknown): CompiledPlain;