@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
@@ -1,6 +1,8 @@
1
1
  import type { Store } from "../core/store/store.js";
2
+ import { type MutDoc, type MutBlock } from "./tree.js";
2
3
  import { type To, type Expect } from "./ops.js";
3
4
  import { type DocStore } from "./doc-store.js";
5
+ import { type DocBase, type Effect } from "./commit.js";
4
6
  export type Op = {
5
7
  op: "insert";
6
8
  doc?: string;
@@ -19,6 +21,7 @@ export type Op = {
19
21
  op: "move";
20
22
  blocks: string[];
21
23
  to: To;
24
+ doc?: string;
22
25
  expect?: Expect;
23
26
  } | {
24
27
  op: "remove";
@@ -54,18 +57,6 @@ export interface ApplyRequest {
54
57
  * contexts (tests) where the in-process serialization of Store.write suffices.
55
58
  */
56
59
  omgbaseDir?: string;
57
- /**
58
- * Document-level frontmatter overrides (doc id → new frontmatter raw incl.
59
- * fences + trailing separator, or null to drop it). Frontmatter is a
60
- * document-level materialization unit (rendered before the block tree), not a
61
- * block the six ops address; the whole-document update planner sets it here
62
- * when the proposed content changes the frontmatter. The doc is force-loaded
63
- * so a frontmatter-only change still commits.
64
- */
65
- setFrontmatter?: {
66
- doc: string;
67
- raw: string | null;
68
- }[];
69
60
  /**
70
61
  * Commit timestamp (RFC 3339 UTC, spec/store §2.4) stamped on every commit this
71
62
  * request records — the `api` commits and any `observed` commit a file-CAS
@@ -96,4 +87,94 @@ export interface ApplyResult {
96
87
  }>;
97
88
  committed: boolean;
98
89
  }
90
+ /** A planned changeset: the op results, the per-document revisions and diffs
91
+ * the dry run returns, and the effects the commit runs (spec/mutate §4
92
+ * steps 1–4; `commit.ts` is step 5). Nothing has been written. */
93
+ export interface ChangesetPlan {
94
+ results: OpResult[];
95
+ revisions: ApplyRevision[];
96
+ diffs: Record<string, {
97
+ before: string;
98
+ after: string;
99
+ }>;
100
+ /** Per loaded document in load order: its `ingest_known`, then (in a
101
+ * workspace) its `record_stat`. */
102
+ effects: Effect[];
103
+ /** Per loaded document in load order: the row triple it was loaded against
104
+ * (spec/mutate §4 step 5, 2.2) — what the commit re-validates and what a
105
+ * frozen plan will carry. Each `ingest_known` effect carries its own as
106
+ * `base`. */
107
+ preconditions: ({
108
+ id: string;
109
+ } & DocBase)[];
110
+ }
111
+ /** spec/mutate §4 steps 1–4: resolve, load, apply the ops in memory, render. */
112
+ export declare function planChangeset(store: Store, req: Omit<ApplyRequest, "dryRun" | "ts">): ChangesetPlan;
113
+ /**
114
+ * The op loop of spec/mutate §4 steps 1–4 as an incremental planner: `push`
115
+ * applies one op to the in-memory trees (loading a document at first touch and
116
+ * remembering its pre-mutation render, live ids and row triple), `finish`
117
+ * renders every loaded document into the `ChangesetPlan`. `planChangeset` is
118
+ * `push` over a whole op list; the effect planner (`plan.ts`, 2.3) pushes an
119
+ * effect's lowered ops one effect at a time so it can read the ids each op
120
+ * minted (`results`) before lowering the next effect, and it sets a document's
121
+ * file head (`setHead`) for a frontmatter effect.
122
+ */
123
+ export declare class ChangesetPlanner {
124
+ private readonly store;
125
+ private readonly repoId;
126
+ private readonly omgbaseDir?;
127
+ /** Per op, in push order: the ids each op reported (`"$n.ids[i]"` placeholders resolve against these). */
128
+ readonly results: OpResult[];
129
+ /** `$new.n` → the id minted for it at execute (spec/mutate §11.7, 3.0): a block of a document
130
+ * this request creates is named by its symbol in the plan's ops, and resolved here. Empty at
131
+ * plan time, when the symbols ARE the tree's ids. */
132
+ private readonly aliases;
133
+ private readonly loaded;
134
+ private readonly before;
135
+ private readonly priorIds;
136
+ private readonly bases;
137
+ constructor(store: Store, repoId: string, omgbaseDir?: string | undefined);
138
+ /** The loaded document (loading it at first touch); unknown or tombstoned → `doc_missing`. */
139
+ ensureDoc(docId: string): MutDoc;
140
+ /**
141
+ * spec/mutate §11.3 (3.0): register a document this request CREATES — parsed
142
+ * from its composed bytes under the current minter — as a loaded document
143
+ * with no row behind it: no triple (`baseOf` is undefined, it is absent from
144
+ * `preconditions`), an empty `before` render, no prior ids (every block is
145
+ * `inserted / api`). `symbol` is its document symbol (`$new.n`), the id every
146
+ * later op addresses it by; its commit effect is an `ingest_known` with
147
+ * `base: null`, guarded by `path_taken` instead of the triple.
148
+ */
149
+ adoptNew(symbol: string, doc: MutDoc): void;
150
+ /** Name a minted id for a `$new.n` symbol the plan's ops use (execute's replay). */
151
+ alias(symbol: string, id: string): void;
152
+ /** An op's string field: an alias (`$new.n` at execute), a kernel placeholder, or the id itself. */
153
+ private resolveId;
154
+ private resolveTo;
155
+ /** A per-block `expect` map re-keyed by the resolved ids (its keys may be placeholders or symbols). */
156
+ private resolveExpectMap;
157
+ /** `insert.doc` / `move.doc`: a document this request created (its symbol) passes through; else `resolveDocRef`. */
158
+ private resolveDocArg;
159
+ /** Is the document loaded (touched by an earlier op or head change)? */
160
+ isLoaded(docId: string): boolean;
161
+ /** The loaded document's row triple (after `ensureDoc`). */
162
+ baseOf(docId: string): DocBase | undefined;
163
+ /** Resolve a document reference (an id passes through, a path resolves; else `doc_missing`). */
164
+ resolveDoc(ref: string): string;
165
+ /** Locate a block in the LOADED documents only (an id an earlier op minted included); never loads one. */
166
+ locateLoaded(blockId: string): {
167
+ docId: string;
168
+ block: MutBlock;
169
+ siblings: MutBlock[];
170
+ index: number;
171
+ } | null;
172
+ /** spec/mutate §11.3 (2.3): replace everything before the first block — the
173
+ * leading trivia AND the frontmatter — with `raw` (the file head). */
174
+ setHead(docId: string, raw: string): void;
175
+ /** Apply one op (spec/mutate §2); `op_index` is its position in `results`. */
176
+ push(rawOp: Op): OpResult;
177
+ /** Step 4: render every loaded document; the plan's effects, revisions, diffs and preconditions in load order. */
178
+ finish(): ChangesetPlan;
179
+ }
99
180
  export declare function apply(store: Store, req: ApplyRequest): ApplyResult;
@@ -1,14 +1,12 @@
1
1
  import { sha256 } from "../core/hash.js";
2
2
  import { isValidId } from "../core/ids.js";
3
3
  import { findDocByRef } from "../core/read/reader.js";
4
- import { ingestFile } from "../core/ingest.js";
5
- import { makeReconcilingResolver } from "../sync/reconciling-ingest.js";
6
- import { makeKnownIdResolver } from "./known-ids.js";
7
4
  import { loadMutDoc } from "./load.js";
8
5
  import { renderDoc, markContainerDirty, MutationError } from "./tree.js";
9
6
  import { opInsert, opUpdate, opMove, opRemove, opSplit, opMerge, healTopLevelSeams, checkParentChildrenHash } from "./ops.js";
10
7
  import { withWriterLock } from "../sync/writer-lock.js";
11
8
  import { resolveDocStore } from "./doc-store.js";
9
+ import { commitChangeset, readDocBase } from "./commit.js";
12
10
  import { isContainerBlock } from "../format/index.js";
13
11
  // Resolve "$n.ids[i]" placeholders in a string against prior op results.
14
12
  function resolvePlaceholder(value, results) {
@@ -22,17 +20,6 @@ function resolvePlaceholder(value, results) {
22
20
  throw new MutationError("target_missing", `placeholder ${value} did not resolve`);
23
21
  return id;
24
22
  }
25
- function resolveTo(to, results) {
26
- const parent = typeof to.parent === "string" ? resolvePlaceholder(to.parent, results) : to.parent;
27
- let at = to.at;
28
- if (typeof at === "object") {
29
- if ("before" in at)
30
- at = { before: resolvePlaceholder(at.before, results) };
31
- else if ("after" in at)
32
- at = { after: resolvePlaceholder(at.after, results) };
33
- }
34
- return { parent, at };
35
- }
36
23
  // Resolve an insert op's `doc` field, which may be a minted doc id (d_...) or a
37
24
  // document path — the same id-or-path symmetry the read/graph surfaces expose
38
25
  // (shared via findDocByRef; see core/read/reader.ts). A valid d_ id passes
@@ -53,156 +40,259 @@ function docIdForBlock(store, repoId, blockId) {
53
40
  const row = store.db.prepare("SELECT doc_id FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(blockId);
54
41
  return row?.doc_id ?? null;
55
42
  }
56
- export function apply(store, req) {
57
- const { store: _s } = { store };
58
- void _s;
59
- // Load all docs that ops touch into memory; apply ops in order.
60
- const loaded = new Map();
61
- const before = new Map();
43
+ /** spec/mutate §4 steps 1–4: resolve, load, apply the ops in memory, render. */
44
+ export function planChangeset(store, req) {
45
+ // The kernel request (spec/mutate §4, 3.0): the ops alone — a document's file
46
+ // head (frontmatter) is the effect planner's `setHead`, never a request field.
47
+ const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
48
+ for (const op of req.ops)
49
+ planner.push(op);
50
+ return planner.finish();
51
+ }
52
+ /**
53
+ * The op loop of spec/mutate §4 steps 1–4 as an incremental planner: `push`
54
+ * applies one op to the in-memory trees (loading a document at first touch and
55
+ * remembering its pre-mutation render, live ids and row triple), `finish`
56
+ * renders every loaded document into the `ChangesetPlan`. `planChangeset` is
57
+ * `push` over a whole op list; the effect planner (`plan.ts`, 2.3) pushes an
58
+ * effect's lowered ops one effect at a time so it can read the ids each op
59
+ * minted (`results`) before lowering the next effect, and it sets a document's
60
+ * file head (`setHead`) for a frontmatter effect.
61
+ */
62
+ export class ChangesetPlanner {
63
+ store;
64
+ repoId;
65
+ omgbaseDir;
66
+ /** Per op, in push order: the ids each op reported (`"$n.ids[i]"` placeholders resolve against these). */
67
+ results = [];
68
+ /** `$new.n` → the id minted for it at execute (spec/mutate §11.7, 3.0): a block of a document
69
+ * this request creates is named by its symbol in the plan's ops, and resolved here. Empty at
70
+ * plan time, when the symbols ARE the tree's ids. */
71
+ aliases = new Map();
72
+ loaded = new Map();
73
+ before = new Map();
62
74
  // Block ids present in each doc BEFORE any op — used to label intent
63
75
  // dispositions (edited vs inserted) on the known-id commit path.
64
- const priorIds = new Map();
65
- const results = [];
66
- const ensureDoc = (docId) => {
67
- let d = loaded.get(docId);
76
+ priorIds = new Map();
77
+ // The row triple `(path, current_rev, file_hash)` each document is loaded
78
+ // against — the commit's precondition (spec/mutate §4 step 5, 2.2).
79
+ bases = new Map();
80
+ constructor(store, repoId, omgbaseDir) {
81
+ this.store = store;
82
+ this.repoId = repoId;
83
+ this.omgbaseDir = omgbaseDir;
84
+ }
85
+ /** The loaded document (loading it at first touch); unknown or tombstoned → `doc_missing`. */
86
+ ensureDoc(docId) {
87
+ let d = this.loaded.get(docId);
68
88
  if (!d) {
69
- const md = loadMutDoc(store.db, docId);
70
- if (!md)
89
+ const base = readDocBase(this.store.db, docId);
90
+ const md = base ? loadMutDoc(this.store.db, docId) : null;
91
+ if (!base || !md)
71
92
  throw new MutationError("doc_missing", `doc ${docId} not found`);
72
93
  d = md;
73
- loaded.set(docId, d);
74
- before.set(docId, renderDoc(d));
75
- const ids = store.db.prepare("SELECT block_id FROM blocks WHERE doc_id = ? AND deleted_commit IS NULL").all(docId);
76
- priorIds.set(docId, new Set(ids.map((r) => r.block_id)));
94
+ this.loaded.set(docId, d);
95
+ this.bases.set(docId, base);
96
+ this.before.set(docId, renderDoc(d));
97
+ const ids = this.store.db.prepare("SELECT block_id FROM blocks WHERE doc_id = ? AND deleted_commit IS NULL").all(docId);
98
+ this.priorIds.set(docId, new Set(ids.map((r) => r.block_id)));
77
99
  }
78
100
  return d;
79
- };
80
- // Document-level frontmatter overrides, before the op loop (so a change with
81
- // no body ops still loads + commits the doc). Applied to the loaded MutDoc;
82
- // renderDoc emits frontmatterRaw verbatim and the commit re-ingest re-derives
83
- // properties/edges from the rendered bytes.
84
- for (const fm of req.setFrontmatter ?? []) {
85
- // ensureDoc captures `before` (pre-mutation render) at load time.
86
- ensureDoc(fm.doc).frontmatterRaw = fm.raw;
87
101
  }
88
- req.ops.forEach((rawOp, i) => {
102
+ /**
103
+ * spec/mutate §11.3 (3.0): register a document this request CREATES — parsed
104
+ * from its composed bytes under the current minter — as a loaded document
105
+ * with no row behind it: no triple (`baseOf` is undefined, it is absent from
106
+ * `preconditions`), an empty `before` render, no prior ids (every block is
107
+ * `inserted / api`). `symbol` is its document symbol (`$new.n`), the id every
108
+ * later op addresses it by; its commit effect is an `ingest_known` with
109
+ * `base: null`, guarded by `path_taken` instead of the triple.
110
+ */
111
+ adoptNew(symbol, doc) {
112
+ if (this.loaded.has(symbol))
113
+ throw new Error(`adoptNew: ${symbol} is already loaded`);
114
+ this.loaded.set(symbol, doc);
115
+ this.before.set(symbol, "");
116
+ this.priorIds.set(symbol, new Set());
117
+ }
118
+ /** Name a minted id for a `$new.n` symbol the plan's ops use (execute's replay). */
119
+ alias(symbol, id) {
120
+ this.aliases.set(symbol, id);
121
+ }
122
+ /** An op's string field: an alias (`$new.n` at execute), a kernel placeholder, or the id itself. */
123
+ resolveId(value) {
124
+ return this.aliases.get(value) ?? resolvePlaceholder(value, this.results);
125
+ }
126
+ resolveTo(to) {
127
+ const parent = typeof to.parent === "string"
128
+ ? this.resolveId(to.parent)
129
+ : typeof to.parent === "object" && to.parent !== null && "heading" in to.parent
130
+ ? { ...to.parent, heading: this.resolveId(to.parent.heading) }
131
+ : to.parent;
132
+ let at = to.at;
133
+ if (typeof at === "object") {
134
+ if ("before" in at)
135
+ at = { before: this.resolveId(at.before) };
136
+ else if ("after" in at)
137
+ at = { after: this.resolveId(at.after) };
138
+ }
139
+ return { parent, at };
140
+ }
141
+ /** A per-block `expect` map re-keyed by the resolved ids (its keys may be placeholders or symbols). */
142
+ resolveExpectMap(expect) {
143
+ if (!expect)
144
+ return undefined;
145
+ const out = {};
146
+ for (const [k, v] of Object.entries(expect))
147
+ out[this.resolveId(k)] = v;
148
+ return out;
149
+ }
150
+ /** `insert.doc` / `move.doc`: a document this request created (its symbol) passes through; else `resolveDocRef`. */
151
+ resolveDocArg(ref) {
152
+ return this.loaded.has(ref) ? ref : resolveDocRef(this.store, this.repoId, ref);
153
+ }
154
+ /** Is the document loaded (touched by an earlier op or head change)? */
155
+ isLoaded(docId) {
156
+ return this.loaded.has(docId);
157
+ }
158
+ /** The loaded document's row triple (after `ensureDoc`). */
159
+ baseOf(docId) {
160
+ return this.bases.get(docId);
161
+ }
162
+ /** Resolve a document reference (an id passes through, a path resolves; else `doc_missing`). */
163
+ resolveDoc(ref) {
164
+ return resolveDocRef(this.store, this.repoId, ref);
165
+ }
166
+ /** Locate a block in the LOADED documents only (an id an earlier op minted included); never loads one. */
167
+ locateLoaded(blockId) {
168
+ for (const [docId, d] of this.loaded) {
169
+ const f = locateMut(d, blockId);
170
+ if (f)
171
+ return { docId, ...f };
172
+ }
173
+ return null;
174
+ }
175
+ /** spec/mutate §11.3 (2.3): replace everything before the first block — the
176
+ * leading trivia AND the frontmatter — with `raw` (the file head). */
177
+ setHead(docId, raw) {
178
+ const d = this.ensureDoc(docId);
179
+ d.leadingTrivia = "";
180
+ d.frontmatterRaw = raw;
181
+ }
182
+ /** Apply one op (spec/mutate §2); `op_index` is its position in `results`. */
183
+ push(rawOp) {
184
+ const { store, repoId, loaded, results } = this;
185
+ void results;
186
+ const i = results.length;
187
+ const ensureDoc = (docId) => this.ensureDoc(docId);
188
+ let result;
89
189
  switch (rawOp.op) {
90
190
  case "insert": {
91
- const to = resolveTo(rawOp.to, results);
191
+ const to = this.resolveTo(rawOp.to);
92
192
  const docId = rawOp.doc !== undefined
93
- ? resolveDocRef(store, req.repoId, rawOp.doc)
94
- : parentDoc(store, req.repoId, to, loaded);
193
+ ? this.resolveDocArg(rawOp.doc)
194
+ : parentDoc(store, repoId, to, loaded);
95
195
  const d = ensureDoc(docId);
96
- results.push(opInsert(d, to, rawOp.markdown, rawOp.expect, i));
196
+ result = opInsert(d, to, rawOp.markdown, rawOp.expect, i);
97
197
  break;
98
198
  }
99
199
  case "update": {
100
- const block = resolvePlaceholder(rawOp.block, results);
101
- const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store, req.repoId, block);
200
+ const block = this.resolveId(rawOp.block);
201
+ const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store, repoId, block);
102
202
  if (!docId)
103
203
  throw new MutationError("block_missing", `block ${block} not found`, { op_index: i });
104
- results.push(opUpdate(ensureDoc(docId), block, i, rawOp.markdown, rawOp.attrs, rawOp.expect, rawOp.trivia, rawOp.childIds));
204
+ result = opUpdate(ensureDoc(docId), block, i, rawOp.markdown, rawOp.attrs, rawOp.expect, rawOp.trivia, rawOp.childIds);
105
205
  break;
106
206
  }
107
207
  case "move": {
108
- const blocks = rawOp.blocks.map((b) => resolvePlaceholder(b, results));
109
- const to = resolveTo(rawOp.to, results);
110
- const srcDocId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, req.repoId, blocks[0]);
208
+ const blocks = rawOp.blocks.map((b) => this.resolveId(b));
209
+ const to = this.resolveTo(rawOp.to);
210
+ const srcDocId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, repoId, blocks[0]);
111
211
  if (!srcDocId)
112
212
  throw new MutationError("block_missing", `block ${blocks[0]} not found`, { op_index: i });
113
- // {doc:true} with a non-anchor `at` (start/end) means the source doc's
114
- // top level; otherwise infer the destination doc from the target.
115
- const topLevelSameDoc = typeof to.parent === "object" && "doc" in to.parent && typeof to.at === "string";
116
- const dstDocId = topLevelSameDoc ? srcDocId : parentDoc(store, req.repoId, to, loaded);
213
+ // {doc:true} with a non-anchor `at` (start/end) means the top level of
214
+ // `doc` when given (spec/mutate §2.3, 3.0: the destination document of a
215
+ // cross-document move to a top-level start/end), else of the source
216
+ // doc; an anchored `at` infers the destination doc from the anchor.
217
+ const topLevel = typeof to.parent === "object" && "doc" in to.parent && typeof to.at === "string";
218
+ const dstDocId = topLevel ? (rawOp.doc !== undefined ? this.resolveDocArg(rawOp.doc) : srcDocId) : parentDoc(store, repoId, to, loaded);
117
219
  if (dstDocId === srcDocId) {
118
- results.push(opMove(ensureDoc(srcDocId), blocks, to, i, rawOp.expect));
220
+ result = opMove(ensureDoc(srcDocId), blocks, to, i, rawOp.expect);
119
221
  }
120
222
  else {
121
223
  // Cross-document move (04 §1): extract from source, insert into target.
122
- results.push(crossDocMove(ensureDoc(srcDocId), ensureDoc(dstDocId), blocks, to, i, rawOp.expect));
224
+ result = crossDocMove(ensureDoc(srcDocId), ensureDoc(dstDocId), blocks, to, i, rawOp.expect);
123
225
  }
124
226
  break;
125
227
  }
126
228
  case "remove": {
127
- const blocks = rawOp.blocks.map((b) => resolvePlaceholder(b, results));
128
- const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, req.repoId, blocks[0]);
229
+ const blocks = rawOp.blocks.map((b) => this.resolveId(b));
230
+ const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, repoId, blocks[0]);
129
231
  if (!docId)
130
232
  throw new MutationError("block_missing", `block ${blocks[0]} not found`, { op_index: i });
131
- results.push(opRemove(ensureDoc(docId), blocks, i, rawOp.expect));
233
+ result = opRemove(ensureDoc(docId), blocks, i, this.resolveExpectMap(rawOp.expect));
132
234
  break;
133
235
  }
134
236
  case "split": {
135
- const block = resolvePlaceholder(rawOp.block, results);
136
- const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store, req.repoId, block);
237
+ const block = this.resolveId(rawOp.block);
238
+ const docId = docIdForBlockLoaded(loaded, block) ?? docIdForBlock(store, repoId, block);
137
239
  if (!docId)
138
240
  throw new MutationError("block_missing", `block ${block} not found`, { op_index: i });
139
- results.push(opSplit(ensureDoc(docId), block, rawOp.at, i, rawOp.expect));
241
+ result = opSplit(ensureDoc(docId), block, rawOp.at, i, rawOp.expect);
140
242
  break;
141
243
  }
142
244
  case "merge": {
143
- const blocks = rawOp.blocks.map((b) => resolvePlaceholder(b, results));
144
- const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, req.repoId, blocks[0]);
245
+ const blocks = rawOp.blocks.map((b) => this.resolveId(b));
246
+ const docId = docIdForBlockLoaded(loaded, blocks[0]) ?? docIdForBlock(store, repoId, blocks[0]);
145
247
  if (!docId)
146
248
  throw new MutationError("block_missing", `block ${blocks[0]} not found`, { op_index: i });
147
- results.push(opMerge(ensureDoc(docId), blocks, i, rawOp.separator, rawOp.expect));
249
+ result = opMerge(ensureDoc(docId), blocks, i, rawOp.separator, this.resolveExpectMap(rawOp.expect));
148
250
  break;
149
251
  }
150
252
  }
151
- });
152
- const revisions = [];
153
- const diffs = {};
154
- for (const [docId, d] of loaded) {
155
- const after = renderDoc(d);
156
- diffs[d.path] = { before: before.get(docId) ?? "", after };
157
- revisions.push({ id: docId, path: d.path, hash: sha256(after).toString("hex") });
253
+ results.push(result);
254
+ return result;
255
+ }
256
+ /** Step 4: render every loaded document; the plan's effects, revisions, diffs and preconditions in load order. */
257
+ finish() {
258
+ const revisions = [];
259
+ const diffs = {};
260
+ const effects = [];
261
+ const preconditions = [];
262
+ for (const [docId, d] of this.loaded) {
263
+ const after = renderDoc(d);
264
+ // An adopted document (`adoptNew`) has no triple: it is absent from the
265
+ // preconditions and its ingest is guarded by `path_taken` (`base: null`).
266
+ const base = this.bases.get(docId) ?? null;
267
+ diffs[d.path] = { before: this.before.get(docId) ?? "", after };
268
+ revisions.push({ id: docId, path: d.path, hash: sha256(after).toString("hex") });
269
+ if (base)
270
+ preconditions.push({ id: docId, ...base });
271
+ effects.push({ kind: "ingest_known", docId, path: d.path, rendered: after, mutDoc: d, priorIds: this.priorIds.get(docId) ?? new Set(), base });
272
+ // Keep the freshness cache warm so this engine write isn't re-hashed by a
273
+ // later sweep (echo suppression already covers correctness; this avoids work).
274
+ if (this.omgbaseDir)
275
+ effects.push({ kind: "record_stat", path: d.path, bytes: after });
276
+ }
277
+ return { results: this.results, revisions, diffs, effects, preconditions };
158
278
  }
279
+ }
280
+ export function apply(store, req) {
281
+ const { results, revisions, diffs, effects } = planChangeset(store, req);
159
282
  if (req.dryRun) {
160
283
  return { results, revisions, diffs, committed: false };
161
284
  }
162
- // Write each touched file with the file-CAS + atomic-write protocol (04 §6),
163
- // then re-ingest the rendered bytes as an api-origin commit so identity
164
- // threads (the ops already assigned ids; reconciliation confirms carries).
165
- // Steps 2–7 run under the cross-process writer lock when a workspace dir is
166
- // supplied (11 §3.2); otherwise the Store's in-process serialization suffices.
285
+ // Commit (spec/mutate §4 step 5): one transaction validating every touched
286
+ // document, one `api` commit and a revision per document plus the file
287
+ // intent, then the rendered files materialized — under the cross-process
288
+ // writer lock when a workspace dir is supplied (spec/sync §7); otherwise the
289
+ // Store's in-process serialization suffices.
167
290
  const docStore = resolveDocStore(req);
291
+ const ts = req.ts ?? new Date().toISOString();
292
+ const head = { origin: "api", actor: req.origin.actor, reason: req.origin.reason ?? null };
293
+ const targets = { docStore, ...(req.omgbaseDir ? { omgbaseDir: req.omgbaseDir } : {}) };
168
294
  const commitPhase = () => {
169
- const ts = req.ts ?? new Date().toISOString();
170
- for (const [docId, d] of loaded) {
171
- const rendered = renderDoc(d);
172
- // File-CAS: on-disk bytes must equal the revision we computed against.
173
- const current = store.db.prepare("SELECT file_hash FROM docs WHERE doc_id = ?").get(docId);
174
- const onDiskContent = docStore.read(d.path);
175
- if (onDiskContent !== null && current?.file_hash) {
176
- const onDisk = sha256(onDiskContent);
177
- if (!onDisk.equals(current.file_hash)) {
178
- // A human edit landed first: ingest it, then the caller must retry.
179
- ingestFile(store, req.repoId, d.path, onDiskContent, { ts, resolveIds: makeReconcilingResolver(store, req.repoId, { ts, path: d.path }) });
180
- if (req.omgbaseDir)
181
- docStore.recordStat(store, req.repoId, d.path, onDiskContent);
182
- throw new MutationError("sync_conflict", `file ${d.path} changed on disk; re-ingested — retry`, { retriable: true });
183
- }
184
- }
185
- // Atomic write through the DocStore (no-op for a headless NullDocStore).
186
- docStore.write(d.path, rendered);
187
- // Commit (INTENT path): persist the ops' tree with its KNOWN block ids —
188
- // never re-derive identity from the rendered bytes. The mutated MutDoc `d`
189
- // already carries deterministic ids (opUpdate keeps a block's id in place,
190
- // untouched blocks keep theirs, new blocks carry the op-minted id), so the
191
- // known-id resolver threads them onto the re-parsed tree and records intent
192
- // dispositions (confidence 1.0). Re-reconciling here would gamble block —
193
- // and thus node — identity on the probabilistic matcher (node-editability).
194
- ingestFile(store, req.repoId, d.path, rendered, {
195
- ts,
196
- origin: "api",
197
- actor: req.origin.actor,
198
- reason: req.origin.reason ?? null,
199
- resolveIds: makeKnownIdResolver(store, req.repoId, d, priorIds.get(docId) ?? new Set(), { path: d.path }),
200
- });
201
- // Keep the freshness cache warm so this engine write isn't re-hashed by a
202
- // later sweep (echo suppression already covers correctness; this avoids work).
203
- if (req.omgbaseDir)
204
- docStore.recordStat(store, req.repoId, d.path, rendered);
205
- }
295
+ commitChangeset(store, req.repoId, head, effects, ts, targets);
206
296
  };
207
297
  if (req.omgbaseDir)
208
298
  withWriterLock(req.omgbaseDir, commitPhase);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Canonical JSON (§11.5): no whitespace; object keys sorted by their UTF-8
3
+ * bytes; members whose value is `undefined` omitted (an optional key absent is
4
+ * absent, never `null`; a JSON `null` value stays `null`); strings escaped only
5
+ * as JSON requires — `"`, `\`, and U+0000–U+001F (`\b \f \n \r \t`, else
6
+ * `\u00xx` lower-case) — everything else literal UTF-8; a number that is
7
+ * integral with |n| < 2^53 printed as a plain integer (`-0` as `0`), any other
8
+ * finite number by ECMAScript `Number::toString` (shortest round-trip digits);
9
+ * `true`/`false`/`null`.
10
+ */
11
+ export declare function canonicalJson(v: unknown): string;
12
+ /** A `plan` / `apply` / `execute` request as the caller gave it: `select`, `effects`, `expect` (each only when present). */
13
+ export interface FixRequest {
14
+ select?: unknown;
15
+ effects: unknown;
16
+ expect?: unknown;
17
+ }
18
+ /** The `fix` for a request to the SAME tool: the corrected arguments, canonical JSON. */
19
+ export declare function fixRequest(req: FixRequest): string;
20
+ /** The `fix` naming another tool: `<tool> <canonical args>`. */
21
+ export declare function fixTool(tool: string, args: unknown): string;
22
+ /** The selector that names exactly one row: the request narrowed to `from <root> where id == "<id>"` with `expect.max: 1` (the fix of a cross-hit collision or an ambiguous target under a selector). */
23
+ export declare function narrowedTo(root: "docs" | "blocks" | "nodes", id: string): string;
24
+ /** The root a row id belongs to, by its prefix (`d_` / `b_` / `n_`). */
25
+ export declare function rootOfId(id: string): "docs" | "blocks" | "nodes";
26
+ /** `value` with the member at `path` (keys and array indices) replaced; a copy, the input untouched. */
27
+ export declare function withMember(value: unknown, path: readonly string[], replacement: unknown): unknown;