@omgbase/core 0.1.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cli/eval-matcher.d.ts +13 -0
  3. package/dist/src/cli/eval-matcher.js +36 -0
  4. package/dist/src/core/attach.d.ts +5 -0
  5. package/dist/src/core/attach.js +38 -0
  6. package/dist/src/core/cursor.d.ts +11 -0
  7. package/dist/src/core/cursor.js +35 -0
  8. package/dist/src/core/hash.d.ts +23 -0
  9. package/dist/src/core/hash.js +82 -0
  10. package/dist/src/core/ids.d.ts +6 -0
  11. package/dist/src/core/ids.js +26 -0
  12. package/dist/src/core/index.d.ts +1 -0
  13. package/dist/src/core/index.js +2 -0
  14. package/dist/src/core/ingest.d.ts +43 -0
  15. package/dist/src/core/ingest.js +287 -0
  16. package/dist/src/core/order-key.d.ts +8 -0
  17. package/dist/src/core/order-key.js +72 -0
  18. package/dist/src/core/parse/parse.d.ts +3 -0
  19. package/dist/src/core/parse/parse.js +104 -0
  20. package/dist/src/core/parse/render.d.ts +3 -0
  21. package/dist/src/core/parse/render.js +20 -0
  22. package/dist/src/core/parse/tree.d.ts +8 -0
  23. package/dist/src/core/parse/tree.js +43 -0
  24. package/dist/src/core/parse/types.d.ts +52 -0
  25. package/dist/src/core/parse/types.js +9 -0
  26. package/dist/src/core/read/document.d.ts +107 -0
  27. package/dist/src/core/read/document.js +191 -0
  28. package/dist/src/core/read/nodes.d.ts +47 -0
  29. package/dist/src/core/read/nodes.js +102 -0
  30. package/dist/src/core/read/outline.d.ts +12 -0
  31. package/dist/src/core/read/outline.js +76 -0
  32. package/dist/src/core/read/reader.d.ts +125 -0
  33. package/dist/src/core/read/reader.js +206 -0
  34. package/dist/src/core/read/refs.d.ts +15 -0
  35. package/dist/src/core/read/refs.js +51 -0
  36. package/dist/src/core/store/edges.d.ts +26 -0
  37. package/dist/src/core/store/edges.js +101 -0
  38. package/dist/src/core/store/fts.d.ts +10 -0
  39. package/dist/src/core/store/fts.js +22 -0
  40. package/dist/src/core/store/gc.d.ts +18 -0
  41. package/dist/src/core/store/gc.js +69 -0
  42. package/dist/src/core/store/nodes.d.ts +5 -0
  43. package/dist/src/core/store/nodes.js +57 -0
  44. package/dist/src/core/store/properties.d.ts +38 -0
  45. package/dist/src/core/store/properties.js +177 -0
  46. package/dist/src/core/store/rebuild.d.ts +3 -0
  47. package/dist/src/core/store/rebuild.js +29 -0
  48. package/dist/src/core/store/schema.d.ts +9 -0
  49. package/dist/src/core/store/schema.js +386 -0
  50. package/dist/src/core/store/sections.d.ts +2 -0
  51. package/dist/src/core/store/sections.js +29 -0
  52. package/dist/src/core/store/store.d.ts +24 -0
  53. package/dist/src/core/store/store.js +164 -0
  54. package/dist/src/core/store/writers.d.ts +52 -0
  55. package/dist/src/core/store/writers.js +89 -0
  56. package/dist/src/core/vec.d.ts +10 -0
  57. package/dist/src/core/vec.js +33 -0
  58. package/dist/src/format/adapter.d.ts +97 -0
  59. package/dist/src/format/adapter.js +14 -0
  60. package/dist/src/format/index.d.ts +6 -0
  61. package/dist/src/format/index.js +15 -0
  62. package/dist/src/format/json.d.ts +3 -0
  63. package/dist/src/format/json.js +324 -0
  64. package/dist/src/format/markdown.d.ts +5 -0
  65. package/dist/src/format/markdown.js +217 -0
  66. package/dist/src/format/registry.d.ts +6 -0
  67. package/dist/src/format/registry.js +24 -0
  68. package/dist/src/format/yaml.d.ts +5 -0
  69. package/dist/src/format/yaml.js +413 -0
  70. package/dist/src/graph/extract.d.ts +38 -0
  71. package/dist/src/graph/extract.js +242 -0
  72. package/dist/src/graph/history.d.ts +103 -0
  73. package/dist/src/graph/history.js +210 -0
  74. package/dist/src/graph/inbound-links.d.ts +33 -0
  75. package/dist/src/graph/inbound-links.js +130 -0
  76. package/dist/src/graph/link-destinations.d.ts +22 -0
  77. package/dist/src/graph/link-destinations.js +84 -0
  78. package/dist/src/graph/link-health.d.ts +79 -0
  79. package/dist/src/graph/link-health.js +146 -0
  80. package/dist/src/graph/links.d.ts +23 -0
  81. package/dist/src/graph/links.js +42 -0
  82. package/dist/src/index.d.ts +60 -0
  83. package/dist/src/index.js +70 -0
  84. package/dist/src/mcp/errors.d.ts +17 -0
  85. package/dist/src/mcp/errors.js +18 -0
  86. package/dist/src/mcp/graph.d.ts +63 -0
  87. package/dist/src/mcp/graph.js +212 -0
  88. package/dist/src/mcp/reference.d.ts +1 -0
  89. package/dist/src/mcp/reference.js +227 -0
  90. package/dist/src/mcp/server.d.ts +30 -0
  91. package/dist/src/mcp/server.js +1076 -0
  92. package/dist/src/mcp/stdio.d.ts +13 -0
  93. package/dist/src/mcp/stdio.js +24 -0
  94. package/dist/src/migrate/mrplex.d.ts +27 -0
  95. package/dist/src/migrate/mrplex.js +29 -0
  96. package/dist/src/mutate/apply.d.ts +83 -0
  97. package/dist/src/mutate/apply.js +302 -0
  98. package/dist/src/mutate/doc-store.d.ts +49 -0
  99. package/dist/src/mutate/doc-store.js +71 -0
  100. package/dist/src/mutate/docs.d.ts +60 -0
  101. package/dist/src/mutate/docs.js +232 -0
  102. package/dist/src/mutate/known-ids.d.ts +12 -0
  103. package/dist/src/mutate/known-ids.js +154 -0
  104. package/dist/src/mutate/load.d.ts +3 -0
  105. package/dist/src/mutate/load.js +66 -0
  106. package/dist/src/mutate/lower.d.ts +32 -0
  107. package/dist/src/mutate/lower.js +232 -0
  108. package/dist/src/mutate/macros.d.ts +91 -0
  109. package/dist/src/mutate/macros.js +230 -0
  110. package/dist/src/mutate/ops.d.ts +40 -0
  111. package/dist/src/mutate/ops.js +513 -0
  112. package/dist/src/mutate/opset.d.ts +92 -0
  113. package/dist/src/mutate/opset.js +79 -0
  114. package/dist/src/mutate/plan-update.d.ts +51 -0
  115. package/dist/src/mutate/plan-update.js +187 -0
  116. package/dist/src/mutate/tree.d.ts +46 -0
  117. package/dist/src/mutate/tree.js +167 -0
  118. package/dist/src/oqx/run.d.ts +1 -0
  119. package/dist/src/oqx/run.js +6 -0
  120. package/dist/src/oqx-js/context.d.ts +23 -0
  121. package/dist/src/oqx-js/context.js +564 -0
  122. package/dist/src/oqx-js/planner.d.ts +10 -0
  123. package/dist/src/oqx-js/planner.js +91 -0
  124. package/dist/src/oqx-js/run.d.ts +41 -0
  125. package/dist/src/oqx-js/run.js +332 -0
  126. package/dist/src/oqx-js/sql/translate.d.ts +27 -0
  127. package/dist/src/oqx-js/sql/translate.js +290 -0
  128. package/dist/src/reconcile/crossdoc.d.ts +34 -0
  129. package/dist/src/reconcile/crossdoc.js +76 -0
  130. package/dist/src/reconcile/eval/generator.d.ts +21 -0
  131. package/dist/src/reconcile/eval/generator.js +129 -0
  132. package/dist/src/reconcile/eval/metrics.d.ts +32 -0
  133. package/dist/src/reconcile/eval/metrics.js +74 -0
  134. package/dist/src/reconcile/flatten.d.ts +12 -0
  135. package/dist/src/reconcile/flatten.js +37 -0
  136. package/dist/src/reconcile/phase5.d.ts +8 -0
  137. package/dist/src/reconcile/phase5.js +167 -0
  138. package/dist/src/reconcile/phase6a.d.ts +2 -0
  139. package/dist/src/reconcile/phase6a.js +159 -0
  140. package/dist/src/reconcile/phases.d.ts +28 -0
  141. package/dist/src/reconcile/phases.js +0 -0
  142. package/dist/src/reconcile/reconcile.d.ts +21 -0
  143. package/dist/src/reconcile/reconcile.js +115 -0
  144. package/dist/src/reconcile/similarity.d.ts +7 -0
  145. package/dist/src/reconcile/similarity.js +41 -0
  146. package/dist/src/reconcile/types.d.ts +46 -0
  147. package/dist/src/reconcile/types.js +19 -0
  148. package/dist/src/search/cel/ast.d.ts +74 -0
  149. package/dist/src/search/cel/ast.js +3 -0
  150. package/dist/src/search/cel/compile.d.ts +60 -0
  151. package/dist/src/search/cel/compile.js +836 -0
  152. package/dist/src/search/cel/lexer.d.ts +9 -0
  153. package/dist/src/search/cel/lexer.js +139 -0
  154. package/dist/src/search/cel/parser.d.ts +7 -0
  155. package/dist/src/search/cel/parser.js +219 -0
  156. package/dist/src/search/drain.d.ts +39 -0
  157. package/dist/src/search/drain.js +97 -0
  158. package/dist/src/search/embeddings.d.ts +131 -0
  159. package/dist/src/search/embeddings.js +263 -0
  160. package/dist/src/search/external.d.ts +13 -0
  161. package/dist/src/search/external.js +177 -0
  162. package/dist/src/search/fts-query.d.ts +1 -0
  163. package/dist/src/search/fts-query.js +64 -0
  164. package/dist/src/search/provider.d.ts +17 -0
  165. package/dist/src/search/provider.js +13 -0
  166. package/dist/src/search/resolve.d.ts +18 -0
  167. package/dist/src/search/resolve.js +23 -0
  168. package/dist/src/search/rrf.d.ts +36 -0
  169. package/dist/src/search/rrf.js +84 -0
  170. package/dist/src/search/tasks.d.ts +12 -0
  171. package/dist/src/search/tasks.js +131 -0
  172. package/dist/src/search/text.d.ts +16 -0
  173. package/dist/src/search/text.js +21 -0
  174. package/dist/src/search/vector.d.ts +18 -0
  175. package/dist/src/search/vector.js +49 -0
  176. package/dist/src/sync/admin.d.ts +59 -0
  177. package/dist/src/sync/admin.js +53 -0
  178. package/dist/src/sync/attach.d.ts +13 -0
  179. package/dist/src/sync/attach.js +29 -0
  180. package/dist/src/sync/checkpoint.d.ts +21 -0
  181. package/dist/src/sync/checkpoint.js +52 -0
  182. package/dist/src/sync/driver.d.ts +25 -0
  183. package/dist/src/sync/driver.js +80 -0
  184. package/dist/src/sync/external-source.d.ts +14 -0
  185. package/dist/src/sync/external-source.js +137 -0
  186. package/dist/src/sync/freshness.d.ts +42 -0
  187. package/dist/src/sync/freshness.js +152 -0
  188. package/dist/src/sync/fs-util.d.ts +12 -0
  189. package/dist/src/sync/fs-util.js +55 -0
  190. package/dist/src/sync/git-heuristics.d.ts +6 -0
  191. package/dist/src/sync/git-heuristics.js +16 -0
  192. package/dist/src/sync/observe.d.ts +63 -0
  193. package/dist/src/sync/observe.js +84 -0
  194. package/dist/src/sync/plugin.d.ts +54 -0
  195. package/dist/src/sync/plugin.js +12 -0
  196. package/dist/src/sync/reconciling-ingest.d.ts +12 -0
  197. package/dist/src/sync/reconciling-ingest.js +209 -0
  198. package/dist/src/sync/recovery.d.ts +6 -0
  199. package/dist/src/sync/recovery.js +34 -0
  200. package/dist/src/sync/settings.d.ts +18 -0
  201. package/dist/src/sync/settings.js +58 -0
  202. package/dist/src/sync/sources.d.ts +37 -0
  203. package/dist/src/sync/sources.js +75 -0
  204. package/dist/src/sync/tombstone.d.ts +9 -0
  205. package/dist/src/sync/tombstone.js +55 -0
  206. package/dist/src/sync/watch-lease.d.ts +14 -0
  207. package/dist/src/sync/watch-lease.js +91 -0
  208. package/dist/src/sync/watcher.d.ts +19 -0
  209. package/dist/src/sync/watcher.js +34 -0
  210. package/dist/src/sync/workspace.d.ts +34 -0
  211. package/dist/src/sync/workspace.js +120 -0
  212. package/dist/src/sync/writer-lock.d.ts +16 -0
  213. package/dist/src/sync/writer-lock.js +121 -0
  214. package/package.json +80 -0
@@ -0,0 +1,232 @@
1
+ import { rawHashHex } from "./tree.js";
2
+ function collectMutIds(list, out = new Set()) {
3
+ for (const b of list) {
4
+ out.add(b.id);
5
+ collectMutIds(b.children, out);
6
+ }
7
+ return out;
8
+ }
9
+ function collectTargetIds(list, out = new Set()) {
10
+ for (const b of list) {
11
+ out.add(b.id);
12
+ collectTargetIds(b.children, out);
13
+ }
14
+ return out;
15
+ }
16
+ // Build the proposed tree annotated with assigned ids + carry flags, keying
17
+ // exactly as flatten()/assignFromMut() do (parentKey + '/' + index).
18
+ function buildTarget(blocks, assignment, oldIds, parentKey) {
19
+ return blocks.map((b, index) => {
20
+ const key = `${parentKey ?? ""}/${index}`;
21
+ const id = assignment.get(key) ?? key; // key fallback should not happen
22
+ return {
23
+ id,
24
+ key,
25
+ type: b.type,
26
+ raw: b.raw,
27
+ trivia: b.trivia,
28
+ children: buildTarget(b.children, assignment, oldIds, key),
29
+ carried: oldIds.has(id),
30
+ };
31
+ });
32
+ }
33
+ // Longest common subsequence of two id lists (indices into a). Standard DP;
34
+ // used to leave already-ordered survivors in place and move only the rest.
35
+ function lcs(a, b) {
36
+ const n = a.length, m = b.length;
37
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
38
+ for (let i = n - 1; i >= 0; i--) {
39
+ for (let j = m - 1; j >= 0; j--) {
40
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
41
+ }
42
+ }
43
+ const keep = new Set();
44
+ let i = 0, j = 0;
45
+ while (i < n && j < m) {
46
+ if (a[i] === b[j]) {
47
+ keep.add(a[i]);
48
+ i++;
49
+ j++;
50
+ }
51
+ else if (dp[i + 1][j] >= dp[i][j + 1])
52
+ i++;
53
+ else
54
+ j++;
55
+ }
56
+ return keep;
57
+ }
58
+ /**
59
+ * Lower a reconciled whole-document update to a kernel opset (top-level
60
+ * granularity). Preserves identity for every top-level block carried unchanged
61
+ * and correctly realizes add / remove / reorder / content-edit at the top
62
+ * level. A carried container whose content changed is updated as a whole (its
63
+ * children re-minted); recursion to preserve nested identity is a later
64
+ * refinement (verification keeps this honest either way).
65
+ */
66
+ export function lowerTopLevel(oldDoc, rest, assignment, dispositions) {
67
+ const docId = oldDoc.docId;
68
+ const oldIds = collectMutIds(oldDoc.children);
69
+ const target = buildTarget(rest, assignment, oldIds, null);
70
+ const targetIdSet = collectTargetIds(target);
71
+ const dispBy = new Map(dispositions.map((d) => [d.blockId, d]));
72
+ const oldById = new Map();
73
+ {
74
+ const walk = (l) => { for (const b of l) {
75
+ oldById.set(b.id, b);
76
+ walk(b.children);
77
+ } };
78
+ walk(oldDoc.children);
79
+ }
80
+ const oldTopIds = oldDoc.children.map((b) => b.id);
81
+ const oldTopSet = new Set(oldTopIds);
82
+ const ops = [];
83
+ let preserved = 0;
84
+ let ambiguous = 0;
85
+ for (const d of dispositions)
86
+ if (d.detail && Array.isArray(d.detail.nearMisses))
87
+ ambiguous++;
88
+ const meta = (id) => {
89
+ const d = dispBy.get(id);
90
+ return { confidence: d?.confidence ?? null, reason: d?.reason ?? null, ...(d?.detail ? { detail: d.detail } : {}) };
91
+ };
92
+ // 1. Removes: current top-level blocks gone from the proposed TOP LEVEL. An id
93
+ // the reconciler carried into a nested position (a paragraph that became a
94
+ // blockquote's child) is still removed here: the container it now lives in is
95
+ // inserted fresh below, and an insert cannot carry ids, so leaving the old
96
+ // top-level block in place would duplicate its bytes.
97
+ const targetTopIds = new Set(target.map((t) => t.id));
98
+ for (const ob of oldDoc.children) {
99
+ if (!targetTopIds.has(ob.id)) {
100
+ ops.push({
101
+ op: { op: "remove", blocks: [ob.id], expect: { [ob.id]: { content_hash: rawHashHex(ob.raw) } } },
102
+ disposition: "deleted", blocks: [ob.id], confidence: null, reason: "tombstone",
103
+ });
104
+ }
105
+ }
106
+ // 2. Content updates: a carried top-level block whose raw changed. A carried
107
+ // container (list/blockquote) is updated as a whole — its raw carries the
108
+ // children — but `childIds` threads the reconcile-carried identity of the
109
+ // items onto the re-parsed subtree, so a within-container edit/reorder/insert
110
+ // preserves the identity of every item that was NOT itself changed (only
111
+ // genuinely new items mint). Nested identity, at no convergence cost.
112
+ for (const t of target) {
113
+ if (!t.carried)
114
+ continue;
115
+ const ob = oldById.get(t.id);
116
+ if (!ob)
117
+ continue; // carried an id that lives nested in old — handled by move below
118
+ if (rawHashHex(ob.raw) !== rawHashHex(t.raw)) {
119
+ const m = meta(t.id);
120
+ const childIds = t.children.length > 0 ? carriedChildIdMap(t, oldIds) : {};
121
+ ops.push({
122
+ op: { op: "update", block: t.id, markdown: t.raw, expect: { content_hash: rawHashHex(ob.raw) }, ...(Object.keys(childIds).length > 0 ? { childIds } : {}) },
123
+ disposition: "edited", blocks: [t.id], confidence: m.confidence, reason: m.reason, ...(m.detail ? { detail: m.detail } : {}),
124
+ });
125
+ }
126
+ }
127
+ // 3. Placement: realize the proposed top-level order. Leave the LCS of
128
+ // survivors in place; move the rest and insert new blocks, each anchored after
129
+ // the preceding proposed block (a minted-id placeholder for a fresh insert).
130
+ const keptTop = target.filter((t) => t.carried && oldTopSet.has(t.id)).map((t) => t.id);
131
+ const survivorsInOldOrder = oldTopIds.filter((id) => targetIdSet.has(id) && keptTop.includes(id));
132
+ const stable = lcs(survivorsInOldOrder, keptTop);
133
+ let prevRef = null; // null = document start
134
+ const refOf = []; // parallel to target, the id/placeholder to anchor after
135
+ const placed = []; // parallel to target: realized by insert/move (seam-healing ops)
136
+ target.forEach((t, i) => {
137
+ const anchorAt = prevRef === null ? "start" : { after: prevRef };
138
+ placed[i] = !t.carried || !oldTopSet.has(t.id) || !stable.has(t.id);
139
+ if (!t.carried || !oldTopSet.has(t.id)) {
140
+ // Insert new (or a block not previously top-level) at this position.
141
+ const opIndex = ops.length;
142
+ ops.push({
143
+ op: { op: "insert", doc: docId, to: { parent: { doc: true }, at: anchorAt }, markdown: t.raw },
144
+ disposition: dispositionForNew(dispBy.get(t.id)?.kind), blocks: [], confidence: dispBy.get(t.id)?.confidence ?? null, reason: dispBy.get(t.id)?.reason ?? null,
145
+ });
146
+ prevRef = `$${opIndex}.ids[0]`;
147
+ }
148
+ else {
149
+ if (!stable.has(t.id)) {
150
+ const m = meta(t.id);
151
+ ops.push({
152
+ op: { op: "move", blocks: [t.id], to: { parent: { doc: true }, at: anchorAt } },
153
+ disposition: "moved", blocks: [t.id], confidence: m.confidence, reason: m.reason, ...(m.detail ? { detail: m.detail } : {}),
154
+ });
155
+ }
156
+ prevRef = t.id;
157
+ }
158
+ refOf[i] = { ref: prevRef };
159
+ });
160
+ // 4. Trivia: set exact trailing trivia for proposed top-level blocks that are
161
+ // inserted, moved, or whose trivia differs from the current block's — so the
162
+ // committed bytes equal the proposed content exactly. A block ANCHORING an
163
+ // insert/move is retiled too: those ops heal the seam (bump a lone "\n" to a
164
+ // blank line) so hand-authored ops never jam blocks, but the proposed content
165
+ // may legitimately want the tight seam (a blockquote directly under a heading).
166
+ target.forEach((t, i) => {
167
+ const ob = t.carried ? oldById.get(t.id) : undefined;
168
+ const wasTop = ob && oldTopSet.has(t.id);
169
+ const moved = wasTop && !stable.has(t.id);
170
+ const anchorsPlacement = placed[i + 1] === true;
171
+ const needs = !wasTop || moved || anchorsPlacement || (ob && ob.trivia !== t.trivia);
172
+ if (!needs)
173
+ return;
174
+ ops.push({
175
+ op: { op: "update", block: refOf[i].ref, trivia: t.trivia },
176
+ disposition: "retiled", blocks: wasTop ? [t.id] : [], confidence: null, reason: null,
177
+ });
178
+ });
179
+ // preserved = carried top-level blocks with unchanged content AND position.
180
+ for (const t of target) {
181
+ if (t.carried && oldById.get(t.id) && rawHashHex(oldById.get(t.id).raw) === rawHashHex(t.raw) && stable.has(t.id))
182
+ preserved++;
183
+ }
184
+ return { ops, preserved, ambiguous };
185
+ }
186
+ // Positional-key → id map for a container's carried descendants (keys relative
187
+ // to the container's own children, e.g. "/0", "/1", "/0/0"). Only carried ids
188
+ // (present in the current tree) are threaded; genuinely-new positions are
189
+ // omitted so the op stays deterministic and opUpdate mints those fresh.
190
+ function carriedChildIdMap(container, oldIds) {
191
+ const out = {};
192
+ const walk = (children, parentKey) => {
193
+ children.forEach((c, i) => {
194
+ const key = `${parentKey}/${i}`;
195
+ if (oldIds.has(c.id))
196
+ out[key] = c.id;
197
+ if (c.children.length > 0)
198
+ walk(c.children, key);
199
+ });
200
+ };
201
+ walk(container.children, "");
202
+ return out;
203
+ }
204
+ function dispositionForNew(kind) {
205
+ if (kind === "copied_from")
206
+ return "copied_from";
207
+ if (kind === "resurrected")
208
+ return "resurrected";
209
+ if (kind === "split_from")
210
+ return "split_from";
211
+ return "inserted";
212
+ }
213
+ /**
214
+ * Guaranteed fallback: replace the whole body. Remove every current top-level
215
+ * block, then insert the entire proposed body as one op. Loses all block
216
+ * identity but always reproduces the exact bytes (verification still checks).
217
+ * Used only when no identity-preserving lowering converges.
218
+ */
219
+ export function lowerReplace(oldDoc, content) {
220
+ const ops = [];
221
+ for (const ob of oldDoc.children) {
222
+ ops.push({ op: { op: "remove", blocks: [ob.id] }, disposition: "deleted", blocks: [ob.id], confidence: null, reason: "tombstone" });
223
+ }
224
+ ops.push({ op: { op: "insert", doc: oldDoc.docId, to: { parent: { doc: true }, at: "end" }, markdown: bodyOf(content) }, disposition: "bulk_rewrite", blocks: [], confidence: null, reason: null });
225
+ return { ops, preserved: 0, ambiguous: 0 };
226
+ }
227
+ // Strip a leading YAML frontmatter block from raw content for a body-only insert.
228
+ function bodyOf(content) {
229
+ const m = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(content);
230
+ return m ? content.slice(m[0].length).replace(/^\s*\n/, "") : content;
231
+ }
232
+ //# sourceMappingURL=lower.js.map
@@ -0,0 +1,91 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import type { Op } from "./apply.js";
3
+ import type { To } from "./ops.js";
4
+ /** tasks_complete: update(attrs:{checked:true}) per block. */
5
+ export declare function tasksComplete(store: Store, blocks: string[]): Op[];
6
+ /** sections_append: insert markdown at the end of a heading's section. */
7
+ export declare function sectionsAppend(heading: string, markdown: string): Op[];
8
+ /**
9
+ * docs_append: insert markdown at the END of a whole document — the
10
+ * document-root peer of sections_append (which appends within a heading's
11
+ * section). ADDITIVE, not a whole-body replace: it parses `markdown` into new
12
+ * blocks and inserts them as fresh top-level blocks after the document's
13
+ * existing ones, so every existing block keeps its stable `b_` id. Expands to a
14
+ * single insert op targeting the document top level (`{ doc: true }`, `at:
15
+ * "end"`) with an explicit `doc` — the kernel then threads the new blocks in
16
+ * without touching any prior block. The doc must already exist; resolving the
17
+ * ref is the caller's job (a missing doc is doc_missing — creation belongs to
18
+ * docs_create, never here).
19
+ */
20
+ export declare function docsAppend(docId: string, markdown: string): Op[];
21
+ /** sections_rename: update the heading block's markdown to a new title. */
22
+ export declare function sectionsRename(store: Store, heading: string, title: string): Op[];
23
+ /** sections_move: move a heading's whole section range to a new location. */
24
+ export declare function sectionsMove(store: Store, heading: string, to: To): Op[];
25
+ /** lists_insert_item: insert a list item at a position relative to a list/item. */
26
+ export declare function listsInsertItem(anchor: string, at: "start" | "end" | {
27
+ before: string;
28
+ } | {
29
+ after: string;
30
+ }, markdown: string): Op[];
31
+ export interface RetargetHit {
32
+ block: string;
33
+ /** repo-relative path of the block's document */
34
+ path: string;
35
+ oldRaw: string;
36
+ newRaw: string;
37
+ }
38
+ /** node_set: surgically set one editable property of a node (node-editability).
39
+ * Resolves the node → its block + owning doc's format adapter, runs the adapter's
40
+ * registered editor for (kind, prop) against the block's current raw + the node's
41
+ * span, and returns a single kernel `update` op (markdown or attrs) with a CAS
42
+ * pin. The node stays a read-only projection; the write goes through the block.
43
+ * Throws node_not_editable when no editor is registered for (kind, prop). */
44
+ export declare function nodeSet(store: Store, nodeId: string, prop: string, value: string): Op[];
45
+ /** The editable property names registered for a (format, kind), for discovery. */
46
+ export declare function editablePropsFor(format: string, kind: string): string[];
47
+ /** links_retarget: the single-pair form of linksRepair (delegates). */
48
+ export declare function linksRetarget(store: Store, repoId: string, fromTarget: string, toTarget: string, opts?: LinkRepairOptions): LinkRepairPlan;
49
+ /** A single from→to link-destination rewrite. */
50
+ export interface LinkRepair {
51
+ from: string;
52
+ to: string;
53
+ }
54
+ export interface LinkRepairOptions {
55
+ /** scope the SOURCE docs by a path glob (`journal/*`), mirroring linksStale. */
56
+ pathGlob?: string;
57
+ }
58
+ /** Per-pair tally: how many link destinations `from` matched (across all hits). */
59
+ export interface LinkRepairCount extends LinkRepair {
60
+ hits: number;
61
+ }
62
+ export interface LinkRepairPlan {
63
+ /** kernel update ops, one per rewritten TOP-MOST block */
64
+ ops: Op[];
65
+ /** the rewritten blocks (dry-run preview) */
66
+ hits: RetargetHit[];
67
+ /** each input pair with its destination-match count (0 = nothing to fix) */
68
+ pairs: LinkRepairCount[];
69
+ }
70
+ /**
71
+ * links_repair: bulk stale-link repair. Given a batch of {from,to} destination
72
+ * rewrites, produce the kernel update ops plus the hits for dry-run preview.
73
+ * Repairs compose within ONE changeset so an agent can fix many dangling
74
+ * targets (surfaced by linksStale) at once.
75
+ *
76
+ * Scope: this rewrites LINK DESTINATIONS, not substrings. A destination is
77
+ * rewritten only when `from` is the WHOLE destination of a Markdown link /
78
+ * image, a wikilink, or a bare-path inline field (`key:: /path`) — ignoring the
79
+ * leading `/`, and allowing a trailing `#heading`/`^ref` fragment which is
80
+ * re-appended. Prose mentions, inline code, and `code_fence` blocks are never
81
+ * touched, and a `from` that is merely a suffix/prefix of a longer path does
82
+ * not match it. Each destination takes the FIRST matching pair (no chaining).
83
+ * Frontmatter links are not blocks and are out of reach here (use docs_set_meta).
84
+ *
85
+ * Coalescing: a block hit by several pairs gets a single update op (one CAS pin
86
+ * per block). Hits collapse to their TOP-MOST block: a list and its list items
87
+ * both carry the same bytes, and updating the container re-parses (re-mints)
88
+ * its children, so a second op on the child would fail block_missing and sink
89
+ * the whole changeset. Mirrors opRemove's top-most collapse. No FS/network I/O.
90
+ */
91
+ export declare function linksRepair(store: Store, repoId: string, repairs: LinkRepair[], opts?: LinkRepairOptions): LinkRepairPlan;
@@ -0,0 +1,230 @@
1
+ import { adapterForFormat } from "../format/registry.js";
2
+ import { MutationError } from "./tree.js";
3
+ import { globClause } from "../graph/link-health.js";
4
+ import { rewriteLinkDestinations, splitDestination } from "../graph/link-destinations.js";
5
+ // Server-side macros (04 §3). Each expands deterministically to kernel ops —
6
+ // no policy judgment (that belongs in the agent). The expansion is returned so
7
+ // callers see the kernel ops that ran. Macros compose within one changeset.
8
+ function rawHashOfBlock(store, blockId) {
9
+ const row = store.db.prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(blockId);
10
+ return row?.h ?? null;
11
+ }
12
+ function rawOfBlock(store, blockId) {
13
+ const row = store.db.prepare("SELECT b.bytes FROM blocks bl JOIN blobs b ON b.hash = bl.raw_hash WHERE bl.block_id = ? AND bl.deleted_commit IS NULL").get(blockId);
14
+ return row?.bytes.toString("utf8") ?? null;
15
+ }
16
+ /** tasks_complete: update(attrs:{checked:true}) per block. */
17
+ export function tasksComplete(store, blocks) {
18
+ return blocks.map((block) => {
19
+ const hash = rawHashOfBlock(store, block);
20
+ return { op: "update", block, attrs: { checked: true }, ...(hash ? { expect: { content_hash: hash } } : {}) };
21
+ });
22
+ }
23
+ /** sections_append: insert markdown at the end of a heading's section. */
24
+ export function sectionsAppend(heading, markdown) {
25
+ const to = { parent: { heading, scope: "section" }, at: "end" };
26
+ return [{ op: "insert", to, markdown }];
27
+ }
28
+ /**
29
+ * docs_append: insert markdown at the END of a whole document — the
30
+ * document-root peer of sections_append (which appends within a heading's
31
+ * section). ADDITIVE, not a whole-body replace: it parses `markdown` into new
32
+ * blocks and inserts them as fresh top-level blocks after the document's
33
+ * existing ones, so every existing block keeps its stable `b_` id. Expands to a
34
+ * single insert op targeting the document top level (`{ doc: true }`, `at:
35
+ * "end"`) with an explicit `doc` — the kernel then threads the new blocks in
36
+ * without touching any prior block. The doc must already exist; resolving the
37
+ * ref is the caller's job (a missing doc is doc_missing — creation belongs to
38
+ * docs_create, never here).
39
+ */
40
+ export function docsAppend(docId, markdown) {
41
+ const to = { parent: { doc: true }, at: "end" };
42
+ return [{ op: "insert", doc: docId, to, markdown }];
43
+ }
44
+ /** sections_rename: update the heading block's markdown to a new title. */
45
+ export function sectionsRename(store, heading, title) {
46
+ const raw = rawOfBlock(store, heading) ?? "";
47
+ const level = (/^(#{1,6})\s/.exec(raw)?.[1] ?? "#").length;
48
+ const hash = rawHashOfBlock(store, heading);
49
+ const markdown = `${"#".repeat(level)} ${title}`;
50
+ return [{ op: "update", block: heading, markdown, ...(hash ? { expect: { content_hash: hash } } : {}) }];
51
+ }
52
+ /** sections_move: move a heading's whole section range to a new location. */
53
+ export function sectionsMove(store, heading, to) {
54
+ // The section range is the heading + blocks until the next peer/higher
55
+ // heading. We resolve the contiguous top-level run of ids from the store.
56
+ const docRow = store.db.prepare("SELECT doc_id, ordinal, json_extract(attrs,'$.level') level FROM blocks WHERE block_id = ?").get(heading);
57
+ if (!docRow)
58
+ return [];
59
+ const level = docRow.level ?? 1;
60
+ const tops = store.db.prepare("SELECT block_id, ordinal, type, json_extract(attrs,'$.level') level FROM blocks WHERE doc_id = ? AND parent_block IS NULL AND deleted_commit IS NULL ORDER BY ordinal").all(docRow.doc_id);
61
+ const startIdx = tops.findIndex((t) => t.block_id === heading);
62
+ let end = tops.length;
63
+ for (let i = startIdx + 1; i < tops.length; i++) {
64
+ if (tops[i].type === "heading" && (tops[i].level ?? 1) <= level) {
65
+ end = i;
66
+ break;
67
+ }
68
+ }
69
+ const run = tops.slice(startIdx, end).map((t) => t.block_id);
70
+ return [{ op: "move", blocks: run, to }];
71
+ }
72
+ /** lists_insert_item: insert a list item at a position relative to a list/item. */
73
+ export function listsInsertItem(anchor, at, markdown) {
74
+ const item = markdown.trimStart().startsWith("- ") ? markdown : `- ${markdown}`;
75
+ return [{ op: "insert", to: { parent: anchor, at }, markdown: item }];
76
+ }
77
+ /** node_set: surgically set one editable property of a node (node-editability).
78
+ * Resolves the node → its block + owning doc's format adapter, runs the adapter's
79
+ * registered editor for (kind, prop) against the block's current raw + the node's
80
+ * span, and returns a single kernel `update` op (markdown or attrs) with a CAS
81
+ * pin. The node stays a read-only projection; the write goes through the block.
82
+ * Throws node_not_editable when no editor is registered for (kind, prop). */
83
+ export function nodeSet(store, nodeId, prop, value) {
84
+ const node = store.db.prepare(`SELECT n.kind, n.name, n.value, n.attrs, n.span_start, n.span_end, n.block_id,
85
+ d.format AS format
86
+ FROM nodes n JOIN docs d ON d.doc_id = n.doc_id
87
+ WHERE n.node_id = ?`).get(nodeId);
88
+ if (!node)
89
+ throw new MutationError("block_missing", `node ${nodeId} not found`, {});
90
+ if (!node.block_id)
91
+ throw new MutationError("node_not_editable", `node ${nodeId} is not anchored to a block`, {});
92
+ const adapter = adapterForFormat(node.format);
93
+ const editor = adapter?.nodeEditors?.[node.kind]?.[prop];
94
+ if (!editor) {
95
+ throw new MutationError("node_not_editable", `no editor for ${node.kind}.${prop}`, {
96
+ kind: node.kind, prop, editable: editablePropsFor(node.format, node.kind),
97
+ });
98
+ }
99
+ const blockRaw = rawOfBlock(store, node.block_id);
100
+ if (blockRaw === null)
101
+ throw new MutationError("block_missing", `block ${node.block_id} not found`, {});
102
+ const hash = rawHashOfBlock(store, node.block_id);
103
+ const result = editor({
104
+ blockRaw,
105
+ span: node.span_start !== null && node.span_end !== null ? { start: node.span_start, end: node.span_end } : null,
106
+ node: {
107
+ kind: node.kind,
108
+ ...(node.name !== null ? { name: node.name } : {}),
109
+ ...(node.value !== null ? { value: node.value } : {}),
110
+ attrs: JSON.parse(node.attrs || "{}"),
111
+ },
112
+ }, value);
113
+ const expect = hash ? { expect: { content_hash: hash } } : {};
114
+ if ("markdown" in result) {
115
+ return [{ op: "update", block: node.block_id, markdown: result.markdown, ...expect }];
116
+ }
117
+ return [{ op: "update", block: node.block_id, attrs: result.attrs, ...expect }];
118
+ }
119
+ /** The editable property names registered for a (format, kind), for discovery. */
120
+ export function editablePropsFor(format, kind) {
121
+ const editors = adapterForFormat(format)?.nodeEditors?.[kind];
122
+ return editors ? Object.keys(editors) : [];
123
+ }
124
+ /** links_retarget: the single-pair form of linksRepair (delegates). */
125
+ export function linksRetarget(store, repoId, fromTarget, toTarget, opts = {}) {
126
+ return linksRepair(store, repoId, [{ from: fromTarget, to: toTarget }], opts);
127
+ }
128
+ const stripSlash = (s) => s.replace(/^\//, "");
129
+ // Does destination `dest` (as authored) name the same link target as `from`?
130
+ // Whole-destination match first (lets `from` carry its own #fragment), then the
131
+ // path part alone (the fragment rides along). Both sides compare with the
132
+ // leading `/` dropped, so `from` may be given either as authored (`/b.md`) or as
133
+ // links_stale reports `target` (`b.md`). Returns the fragment to re-append.
134
+ function destinationMatch(dest, from) {
135
+ if (stripSlash(dest) === stripSlash(from))
136
+ return { fragment: "" };
137
+ const { path, fragment } = splitDestination(dest);
138
+ if (fragment !== "" && stripSlash(path) === stripSlash(from))
139
+ return { fragment };
140
+ return null;
141
+ }
142
+ /**
143
+ * links_repair: bulk stale-link repair. Given a batch of {from,to} destination
144
+ * rewrites, produce the kernel update ops plus the hits for dry-run preview.
145
+ * Repairs compose within ONE changeset so an agent can fix many dangling
146
+ * targets (surfaced by linksStale) at once.
147
+ *
148
+ * Scope: this rewrites LINK DESTINATIONS, not substrings. A destination is
149
+ * rewritten only when `from` is the WHOLE destination of a Markdown link /
150
+ * image, a wikilink, or a bare-path inline field (`key:: /path`) — ignoring the
151
+ * leading `/`, and allowing a trailing `#heading`/`^ref` fragment which is
152
+ * re-appended. Prose mentions, inline code, and `code_fence` blocks are never
153
+ * touched, and a `from` that is merely a suffix/prefix of a longer path does
154
+ * not match it. Each destination takes the FIRST matching pair (no chaining).
155
+ * Frontmatter links are not blocks and are out of reach here (use docs_set_meta).
156
+ *
157
+ * Coalescing: a block hit by several pairs gets a single update op (one CAS pin
158
+ * per block). Hits collapse to their TOP-MOST block: a list and its list items
159
+ * both carry the same bytes, and updating the container re-parses (re-mints)
160
+ * its children, so a second op on the child would fail block_missing and sink
161
+ * the whole changeset. Mirrors opRemove's top-most collapse. No FS/network I/O.
162
+ */
163
+ export function linksRepair(store, repoId, repairs, opts = {}) {
164
+ const pairs = repairs.map((r) => ({ from: r.from, to: r.to, hits: 0 }));
165
+ const effective = pairs.filter((r) => stripSlash(r.from) !== "" && r.from !== r.to);
166
+ if (effective.length === 0)
167
+ return { ops: [], hits: [], pairs };
168
+ // Candidate blocks: live, not a code fence, whose bytes contain the slash-less
169
+ // form of some `from` (a cheap prefilter; the destination scan below decides).
170
+ const params = [];
171
+ let globSql = "";
172
+ if (opts.pathGlob) {
173
+ const { clause, param } = globClause("d.path", opts.pathGlob);
174
+ globSql = `AND ${clause}`;
175
+ params.push(param);
176
+ }
177
+ const stmt = store.db.prepare(`SELECT bl.block_id, bl.parent_block, d.path, b.bytes
178
+ FROM blocks bl
179
+ JOIN blobs b ON b.hash = bl.raw_hash
180
+ JOIN docs d ON d.doc_id = bl.doc_id
181
+ WHERE bl.repo_id = ? AND bl.deleted_commit IS NULL AND d.deleted_commit IS NULL
182
+ AND bl.type != 'code_fence' AND instr(b.bytes, ?) > 0 ${globSql}`);
183
+ const candidates = new Map();
184
+ for (const key of new Set(effective.map((r) => stripSlash(r.from)))) {
185
+ const rows = stmt.all(repoId, key, ...params);
186
+ for (const row of rows) {
187
+ if (!candidates.has(row.block_id))
188
+ candidates.set(row.block_id, { parent: row.parent_block, path: row.path, raw: row.bytes.toString("utf8") });
189
+ }
190
+ }
191
+ // Rewrite each candidate's destinations; keep the blocks that changed.
192
+ const changed = new Map();
193
+ for (const [blockId, c] of candidates) {
194
+ const tally = effective.map(() => 0);
195
+ const newRaw = rewriteLinkDestinations(c.raw, (dest) => {
196
+ for (let i = 0; i < effective.length; i++) {
197
+ const m = destinationMatch(dest, effective[i].from);
198
+ if (m) {
199
+ tally[i]++;
200
+ return effective[i].to + m.fragment;
201
+ }
202
+ }
203
+ return null;
204
+ });
205
+ if (newRaw !== c.raw)
206
+ changed.set(blockId, { parent: c.parent, path: c.path, oldRaw: c.raw, newRaw, tally });
207
+ }
208
+ // Collapse to top-most blocks: drop a hit whose ancestor is also a hit.
209
+ const parentOf = store.db.prepare("SELECT parent_block FROM blocks WHERE block_id = ? AND deleted_commit IS NULL");
210
+ const hasChangedAncestor = (parent) => {
211
+ for (let p = parent; p !== null;) {
212
+ if (changed.has(p))
213
+ return true;
214
+ p = parentOf.get(p)?.parent_block ?? null;
215
+ }
216
+ return false;
217
+ };
218
+ const ops = [];
219
+ const hits = [];
220
+ for (const [blockId, c] of changed) {
221
+ if (hasChangedAncestor(c.parent))
222
+ continue;
223
+ c.tally.forEach((n, i) => { effective[i].hits += n; });
224
+ const hash = rawHashOfBlock(store, blockId);
225
+ ops.push({ op: "update", block: blockId, markdown: c.newRaw, ...(hash ? { expect: { content_hash: hash } } : {}) });
226
+ hits.push({ block: blockId, path: c.path, oldRaw: c.oldRaw, newRaw: c.newRaw });
227
+ }
228
+ return { ops, hits, pairs };
229
+ }
230
+ //# sourceMappingURL=macros.js.map
@@ -0,0 +1,40 @@
1
+ import { type MutDoc } from "./tree.js";
2
+ export type At = "start" | "end" | {
3
+ before: string;
4
+ } | {
5
+ after: string;
6
+ };
7
+ export interface To {
8
+ parent: string | {
9
+ doc: true;
10
+ } | {
11
+ heading: string;
12
+ scope: "section";
13
+ };
14
+ at: At;
15
+ }
16
+ export interface Expect {
17
+ content_hash?: string;
18
+ parent_children_hash?: string;
19
+ }
20
+ export declare function opInsert(doc: MutDoc, to: To, markdown: string): {
21
+ ids: string[];
22
+ };
23
+ export declare function opUpdate(doc: MutDoc, blockId: string, opIndex: number, markdown?: string, attrs?: Record<string, unknown>, expect?: Expect, trivia?: string, childIds?: Record<string, string>): {
24
+ ids: string[];
25
+ };
26
+ export declare function opMove(doc: MutDoc, blockIds: string[], to: To, opIndex: number): {
27
+ ids: string[];
28
+ };
29
+ export declare function opRemove(doc: MutDoc, blockIds: string[], opIndex: number, expectPer?: Record<string, Expect>): {
30
+ ids: string[];
31
+ removed: string[];
32
+ };
33
+ export declare function opSplit(doc: MutDoc, blockId: string, at: number[], opIndex: number, expect?: Expect): {
34
+ ids: string[];
35
+ };
36
+ export declare function opMerge(doc: MutDoc, blockIds: string[], opIndex: number, separator?: string, expectPer?: Record<string, Expect>): {
37
+ ids: string[];
38
+ mergedInto: string[];
39
+ };
40
+ export declare function checkParentChildrenHash(doc: MutDoc, parentId: string | null, expected: string, opIndex: number): void;