@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 { stringify as stringifyYaml, parse as parseYaml } from "yaml";
2
+ import { ingestFile } from "../core/ingest.js";
3
+ import { findDoc, findDocByRef } from "../core/read/reader.js";
4
+ import { makeReconcilingResolver } from "../sync/reconciling-ingest.js";
5
+ import { newCommit } from "../core/store/writers.js";
6
+ import { ftsDeleteDoc } from "../core/store/fts.js";
7
+ import { adoptPhantoms, rebuildDocEdges } from "../core/store/edges.js";
8
+ import { withWriterLock } from "../sync/writer-lock.js";
9
+ import { inboundLinksTo, retargetLinksInRaw } from "../graph/inbound-links.js";
10
+ import { apply } from "./apply.js";
11
+ import { resolveDocStore } from "./doc-store.js";
12
+ import { MutationError } from "./tree.js";
13
+ function dirOf(path) {
14
+ return path.replace(/[^/]*$/, "");
15
+ }
16
+ function underLock(ctx, fn) {
17
+ return ctx.omgbaseDir ? withWriterLock(ctx.omgbaseDir, fn) : fn();
18
+ }
19
+ function canonical(path) {
20
+ return path.replace(/^\/+/, "").split("\\").join("/");
21
+ }
22
+ // Compose a full document's bytes from optional frontmatter + body markdown.
23
+ function composeFile(markdown, frontmatter) {
24
+ const body = markdown.endsWith("\n") || markdown.length === 0 ? markdown : markdown + "\n";
25
+ if (!frontmatter || Object.keys(frontmatter).length === 0)
26
+ return body;
27
+ const yaml = stringifyYaml(frontmatter).replace(/\n$/, "");
28
+ return `---\n${yaml}\n---\n${body.startsWith("\n") ? body : "\n" + body}`;
29
+ }
30
+ /** docs_create: create a new document from complete file bytes (frontmatter incl.). */
31
+ export function docsCreate(store, ctx, path, markdown, frontmatter) {
32
+ const rel = canonical(path);
33
+ const existing = findDoc(store, { repoId: ctx.repoId, path: rel });
34
+ if (existing)
35
+ throw new MutationError("path_taken", `document already exists at ${rel}`);
36
+ const content = composeFile(markdown, frontmatter);
37
+ const docStore = resolveDocStore(ctx);
38
+ return underLock(ctx, () => {
39
+ if (docStore.exists(rel))
40
+ throw new MutationError("path_taken", `file already exists on disk at ${rel}`);
41
+ docStore.write(rel, content);
42
+ const ts = new Date().toISOString();
43
+ const res = ingestFile(store, ctx.repoId, rel, content, { ts, origin: "import", resolveIds: makeReconcilingResolver(store, ctx.repoId, { ts, path: rel }) });
44
+ if (ctx.omgbaseDir)
45
+ docStore.recordStat(store, ctx.repoId, rel, content);
46
+ return { docId: res.docId, path: rel, committed: true };
47
+ });
48
+ }
49
+ /**
50
+ * docs_move: rename a document to a new path (identity preserved). The edge
51
+ * index follows the path, not the identity: links written against the OLD path
52
+ * now dangle (their open edges are re-pointed at `phantom:<old>`, exactly what
53
+ * re-extracting the source would produce, so `links_stale` sees them), and
54
+ * phantom edges already written against the NEW path are adopted. The result
55
+ * lists the dangling inbound links; `retargetInbound` rewrites them.
56
+ */
57
+ export function docsMove(store, ctx, docRef, toPath, opts = {}) {
58
+ const info = findDocByRef(store, ctx.repoId, docRef);
59
+ if (!info)
60
+ throw new MutationError("doc_missing", `no document ${docRef}`);
61
+ const toRel = canonical(toPath);
62
+ if (findDoc(store, { repoId: ctx.repoId, path: toRel }))
63
+ throw new MutationError("path_taken", `a document already exists at ${toRel}`);
64
+ // Who links here BY PATH, as written — before the path changes.
65
+ const inbound = inboundLinksTo(store, ctx.repoId, info.docId, info.path);
66
+ const docStore = resolveDocStore(ctx);
67
+ const moved = underLock(ctx, () => {
68
+ if (docStore.exists(toRel))
69
+ throw new MutationError("path_taken", `file already exists on disk at ${toRel}`);
70
+ const content = docStore.read(info.path) ?? "";
71
+ if (docStore.exists(info.path))
72
+ docStore.rename(info.path, toRel);
73
+ else
74
+ docStore.write(toRel, content);
75
+ const ts = new Date().toISOString();
76
+ store.write((db) => {
77
+ // Update the document row's path + its open revisions' path pointer, and
78
+ // record an api commit noting the move.
79
+ const commit = newCommit(db, { repoId: ctx.repoId, ts, origin: "api", actor: ctx.actor ?? null, reason: `move ${info.path} -> ${toRel}` });
80
+ db.prepare("UPDATE docs SET path = ? WHERE doc_id = ?").run(toRel, info.docId);
81
+ db.prepare("UPDATE revisions SET path = ? WHERE doc_id = ? AND rev_id = ?").run(toRel, info.docId, info.currentRev);
82
+ void commit;
83
+ // Edge index. Every open edge from ANOTHER doc into this one resolved
84
+ // via the old path, so it dangles now → re-point it at the old path's
85
+ // phantom (in place, mirroring adoptPhantoms). A self-doc edge dangles
86
+ // only when the link names the path (`[x](/old.md#H)`); a pure fragment
87
+ // (`#H`) resolves to the doc regardless of path and is left alone — the
88
+ // inbound scan distinguishes them from the raw.
89
+ const phantom = `phantom:${info.path}`;
90
+ const affected = new Set();
91
+ const others = db.prepare("SELECT DISTINCT src_doc FROM edges WHERE dst_node = ? AND to_commit IS NULL AND src_doc != ?").all(info.docId, info.docId);
92
+ for (const r of others)
93
+ affected.add(r.src_doc);
94
+ db.prepare("UPDATE edges SET dst_node = ? WHERE dst_node = ? AND to_commit IS NULL AND src_doc != ?").run(phantom, info.docId, info.docId);
95
+ const selfEdge = db.prepare("UPDATE edges SET dst_node = ? WHERE dst_node = ? AND to_commit IS NULL AND src_doc = ? AND src_block = ? AND anchor IS ?");
96
+ for (const l of inbound) {
97
+ if (l.doc !== info.docId || l.block === null)
98
+ continue;
99
+ selfEdge.run(phantom, info.docId, info.docId, l.block, l.anchor);
100
+ affected.add(info.docId);
101
+ }
102
+ for (const d of affected)
103
+ rebuildDocEdges(db, d);
104
+ // Links already written against the NEW path resolve to this doc now.
105
+ adoptPhantoms(db, toRel, info.docId);
106
+ });
107
+ if (ctx.omgbaseDir) {
108
+ docStore.clearStat(store, ctx.repoId, info.path); // drop the old path's row
109
+ docStore.recordStat(store, ctx.repoId, toRel, content);
110
+ }
111
+ return { docId: info.docId, path: toRel, committed: true };
112
+ });
113
+ if (!opts.retargetInbound || inbound.length === 0)
114
+ return { ...moved, dangling: inbound, retargeted: null };
115
+ // Rewrite the dangling links block by block (one coalesced update per block,
116
+ // CAS on the block's current hash) and apply as one changeset; the re-ingest
117
+ // re-extracts each source doc, whose links now resolve to the moved doc.
118
+ const byBlock = new Map();
119
+ for (const l of inbound)
120
+ if (l.block !== null && !byBlock.has(l.block))
121
+ byBlock.set(l.block, l);
122
+ // A container's raw includes its children's raw, so a link inside a list item
123
+ // is reported for BOTH the list and the item. Rewrite the deepest block only:
124
+ // updating the container would re-parse (and re-identify) its children, and
125
+ // the child's update alone already changes the container's rendered raw.
126
+ const parentOf = store.db.prepare("SELECT parent_block FROM blocks WHERE block_id = ? AND deleted_commit IS NULL");
127
+ const containers = new Set();
128
+ for (const blockId of byBlock.keys()) {
129
+ let cur = blockId;
130
+ while (cur) {
131
+ const row = parentOf.get(cur);
132
+ cur = row?.parent_block ?? null;
133
+ if (cur && byBlock.has(cur))
134
+ containers.add(cur);
135
+ }
136
+ }
137
+ for (const c of containers)
138
+ byBlock.delete(c);
139
+ const ops = [];
140
+ const blocks = [];
141
+ const docs = new Set();
142
+ const readBlock = store.db.prepare("SELECT bl.raw_hash, b.bytes FROM blocks bl JOIN blobs b ON b.hash = bl.raw_hash WHERE bl.block_id = ? AND bl.deleted_commit IS NULL");
143
+ for (const [blockId, src] of byBlock) {
144
+ const row = readBlock.get(blockId);
145
+ if (!row)
146
+ continue;
147
+ // Match against the directory the link was RESOLVED in (the source's path at
148
+ // extraction time); write relative forms against the source's CURRENT
149
+ // directory — they differ only for links inside the moved doc itself.
150
+ const writeDir = src.doc === info.docId ? dirOf(toRel) : dirOf(src.path);
151
+ const newRaw = retargetLinksInRaw(row.bytes.toString("utf8"), dirOf(src.path), writeDir, info.path, toRel);
152
+ if (newRaw === null)
153
+ continue;
154
+ ops.push({ op: "update", block: blockId, markdown: newRaw, expect: { content_hash: row.raw_hash.toString("hex") } });
155
+ blocks.push(blockId);
156
+ docs.add(src.doc);
157
+ }
158
+ if (ops.length > 0) {
159
+ apply(store, {
160
+ repoId: ctx.repoId,
161
+ ...(ctx.rootPath ? { rootPath: ctx.rootPath } : {}),
162
+ ...(ctx.docStore ? { docStore: ctx.docStore } : {}),
163
+ ...(ctx.omgbaseDir ? { omgbaseDir: ctx.omgbaseDir } : {}),
164
+ ops,
165
+ origin: { actor: ctx.actor ?? "api", reason: `retarget inbound links ${info.path} -> ${toRel}` },
166
+ });
167
+ }
168
+ // Containers whose rewritten child covered them count as rewritten too.
169
+ const rewritten = new Set([...blocks, ...containers]);
170
+ const dangling = inbound.filter((l) => l.block === null || !rewritten.has(l.block));
171
+ return { ...moved, dangling, retargeted: { blocks, docs: [...docs] } };
172
+ }
173
+ /** docs_delete: mark a document deleted and remove its file (resurrection-poolable). */
174
+ export function docsDelete(store, ctx, docRef) {
175
+ const info = findDocByRef(store, ctx.repoId, docRef);
176
+ if (!info)
177
+ throw new MutationError("doc_missing", `no document ${docRef}`);
178
+ const docStore = resolveDocStore(ctx);
179
+ return underLock(ctx, () => {
180
+ const ts = new Date().toISOString();
181
+ store.write((db) => {
182
+ const commit = newCommit(db, { repoId: ctx.repoId, ts, origin: "api", actor: ctx.actor ?? null, reason: `delete ${info.path}` });
183
+ // Tombstone the document and its live blocks; FTS rows drop with the blocks.
184
+ ftsDeleteDoc(db, info.docId);
185
+ db.prepare("UPDATE blocks SET deleted_commit = ? WHERE doc_id = ? AND deleted_commit IS NULL").run(commit.commitId, info.docId);
186
+ db.prepare("UPDATE docs SET deleted_commit = ? WHERE doc_id = ?").run(commit.commitId, info.docId);
187
+ });
188
+ docStore.remove(info.path);
189
+ if (ctx.omgbaseDir)
190
+ docStore.clearStat(store, ctx.repoId, info.path);
191
+ return { docId: info.docId, path: info.path, committed: true };
192
+ });
193
+ }
194
+ /** docs_set_meta: surgical frontmatter patch (set/unset keys), re-ingested. */
195
+ export function docsSetMeta(store, ctx, docRef, patch) {
196
+ const info = findDocByRef(store, ctx.repoId, docRef);
197
+ if (!info)
198
+ throw new MutationError("doc_missing", `no document ${docRef}`);
199
+ const docStore = resolveDocStore(ctx);
200
+ return underLock(ctx, () => {
201
+ const original = docStore.read(info.path) ?? "";
202
+ const { frontmatter, body } = splitFrontmatter(original);
203
+ const merged = { ...frontmatter, ...(patch.set ?? {}) };
204
+ for (const k of patch.unset ?? [])
205
+ delete merged[k];
206
+ const content = composeFile(body, merged);
207
+ docStore.write(info.path, content);
208
+ const ts = new Date().toISOString();
209
+ const res = ingestFile(store, ctx.repoId, info.path, content, { ts, origin: "import", resolveIds: makeReconcilingResolver(store, ctx.repoId, { ts, path: info.path }) });
210
+ if (ctx.omgbaseDir)
211
+ docStore.recordStat(store, ctx.repoId, info.path, content);
212
+ return { docId: res.docId, path: info.path, committed: true };
213
+ });
214
+ }
215
+ // Split a file's bytes into (frontmatter object, body markdown). Missing or
216
+ // malformed frontmatter yields an empty object and the whole content as body.
217
+ function splitFrontmatter(content) {
218
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
219
+ if (!m)
220
+ return { frontmatter: {}, body: content };
221
+ let fm = {};
222
+ try {
223
+ const parsed = parseYaml(m[1]);
224
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
225
+ fm = parsed;
226
+ }
227
+ catch {
228
+ /* malformed → empty */
229
+ }
230
+ return { frontmatter: fm, body: content.slice(m[0].length) };
231
+ }
232
+ //# sourceMappingURL=docs.js.map
@@ -0,0 +1,12 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import type { IdResolver } from "../core/ingest.js";
3
+ import type { MutDoc } from "./tree.js";
4
+ /**
5
+ * An IdResolver that threads the ops' known ids (from the committed MutDoc)
6
+ * rather than reconciling. `priorIds` is the set of block ids that existed in
7
+ * the doc's previous revision — used only to label dispositions (same/edited vs
8
+ * inserted); it does not affect id assignment.
9
+ */
10
+ export declare function makeKnownIdResolver(store: Store, repoId: string, doc: MutDoc, priorIds: Set<string>, opts?: {
11
+ path?: string;
12
+ }): IdResolver;
@@ -0,0 +1,154 @@
1
+ import { mintId } from "../core/ids.js";
2
+ import { extractFromBlock, extractFromFrontmatter, resolveRelativePath } from "../graph/extract.js";
3
+ import { resolveExternal, resolveDocPath } from "../core/store/edges.js";
4
+ import { adapterForPath } from "../format/index.js";
5
+ // Known-id resolver for the INTENT path (04 §2, node-editability fix). When
6
+ // `apply` commits, it already holds the mutated tree with DETERMINISTIC block
7
+ // ids — opUpdate keeps a block's id in place, untouched blocks keep theirs, and
8
+ // new blocks carry the id the op minted. Re-ingesting the rendered bytes through
9
+ // the probabilistic reconciler would DISCARD those ids and re-derive identity
10
+ // from bytes, re-minting on ordinary edits (and cascading to node ids). This
11
+ // resolver instead carries the MutDoc's ids onto the re-parsed blocks
12
+ // positionally — round-trip law guarantees parse(render(tree)) is structurally
13
+ // identical to the op tree — and records intent dispositions (confidence 1.0),
14
+ // never a matcher guess. Edge extraction is still real derivation (reused).
15
+ /** Flatten a MutDoc body to a positional id list mirroring flatten()'s keys. */
16
+ function idByPositionalKey(children) {
17
+ const out = new Map();
18
+ const walk = (list, parentKey) => {
19
+ list.forEach((b, index) => {
20
+ const key = `${parentKey ?? ""}/${index}`;
21
+ out.set(key, b.id);
22
+ if (b.children.length > 0)
23
+ walk(b.children, key);
24
+ });
25
+ };
26
+ walk(children, null);
27
+ return out;
28
+ }
29
+ /** Assign the MutDoc's ids onto the re-parsed tree by positional key; mint only
30
+ * if a position is unexpectedly absent (should not happen under round-trip). */
31
+ function assignFromMut(blocks, byKey) {
32
+ const walk = (list, parentKey) => list.map((b, index) => {
33
+ const key = `${parentKey ?? ""}/${index}`;
34
+ const blockId = byKey.get(key) ?? mintId("b");
35
+ return { blockId, type: b.type, raw: b.raw, trivia: b.trivia, attrs: b.attrs, children: walk(b.children, key) };
36
+ });
37
+ return walk(blocks, null);
38
+ }
39
+ // Rebuild id-assigned blocks as RawBlock-shaped inputs for adapter edge
40
+ // extraction, threading each block's blockId so extractEdges can set src_block.
41
+ function collectRawBlocks(blocks) {
42
+ return blocks.map((b) => ({
43
+ type: b.type,
44
+ span: { start: 0, end: 0 },
45
+ raw: b.raw,
46
+ text: "",
47
+ attrs: b.attrs,
48
+ children: collectRawBlocks(b.children),
49
+ trivia: b.trivia,
50
+ anchors: [],
51
+ outLinks: [],
52
+ blockId: b.blockId,
53
+ }));
54
+ }
55
+ function collectIds(blocks) {
56
+ const out = [];
57
+ const walk = (list) => { for (const b of list) {
58
+ out.push(b.blockId);
59
+ walk(b.children);
60
+ } };
61
+ walk(blocks);
62
+ return out;
63
+ }
64
+ /**
65
+ * An IdResolver that threads the ops' known ids (from the committed MutDoc)
66
+ * rather than reconciling. `priorIds` is the set of block ids that existed in
67
+ * the doc's previous revision — used only to label dispositions (same/edited vs
68
+ * inserted); it does not affect id assignment.
69
+ */
70
+ export function makeKnownIdResolver(store, repoId, doc, priorIds, opts = {}) {
71
+ const adapter = opts.path ? adapterForPath(opts.path) : undefined;
72
+ // Source directory for `./`/`../` link targets — the same rule the observed
73
+ // (reconciling) ingest applies, so an `apply` re-ingest resolves a relative
74
+ // link to the same node instead of minting a `phantom:../x.md` placeholder.
75
+ const docDir = opts.path ? opts.path.replace(/[^/]*$/, "") : "";
76
+ const byKey = idByPositionalKey(doc.children);
77
+ return (rest, docId) => {
78
+ const db = store.db;
79
+ const assigned = assignFromMut(rest, byKey);
80
+ const nowIds = collectIds(assigned);
81
+ const nowSet = new Set(nowIds);
82
+ // Dispositions are INTENT, not inference: a carried id that existed before is
83
+ // `same`/`edited` (confidence 1.0); an id new this commit is `inserted`; a
84
+ // prior id no longer present is `deleted`.
85
+ const dispositions = nowIds.map((id) => ({
86
+ blockId: id,
87
+ kind: priorIds.has(id) ? "edited" : "inserted",
88
+ confidence: 1,
89
+ reason: "api",
90
+ matcherV: null,
91
+ detail: {},
92
+ }));
93
+ const deleted = [...priorIds].filter((id) => !nowSet.has(id));
94
+ const extractEdges = (thisDocId, metadata) => {
95
+ const out = [];
96
+ if (adapter?.extractEdges) {
97
+ const raw = collectRawBlocks(assigned);
98
+ for (const e of adapter.extractEdges(raw, metadata))
99
+ out.push(resolveAdapterEdge(db, repoId, thisDocId, e, docDir));
100
+ }
101
+ else {
102
+ const walk = (blocks) => {
103
+ for (const b of blocks) {
104
+ for (const e of extractFromBlock(b.blockId, b.type, b.raw))
105
+ out.push(resolveEdge(db, repoId, thisDocId, e, docDir));
106
+ if (b.children.length > 0)
107
+ walk(b.children);
108
+ }
109
+ };
110
+ walk(assigned);
111
+ for (const e of extractFromFrontmatter(metadata))
112
+ out.push(resolveEdge(db, repoId, thisDocId, e, docDir));
113
+ }
114
+ return out;
115
+ };
116
+ void docId;
117
+ return { assigned, dispositions, deleted, extractEdges };
118
+ };
119
+ }
120
+ // Edge resolution — mirrors reconciling-ingest's resolvers (block-anchor targets
121
+ // resolve to the document node in v1; the anchor is preserved on the edge).
122
+ function resolveEdge(db, repoId, srcDoc, e, docDir) {
123
+ let dstNode;
124
+ let dstKind = e.dstKind;
125
+ if (e.dstKind === "external") {
126
+ dstNode = resolveExternal(db, repoId, e.target);
127
+ }
128
+ else if (e.target === "") {
129
+ dstNode = srcDoc;
130
+ dstKind = "document";
131
+ }
132
+ else {
133
+ dstNode = resolveDocPath(db, repoId, resolveRelativePath(e.target, docDir)).id;
134
+ dstKind = "document";
135
+ }
136
+ return { srcDoc, srcBlock: e.srcBlock, srcField: e.srcField, predicate: e.predicate, dstKind, dstNode, anchor: e.anchor, provenance: e.provenance };
137
+ }
138
+ function resolveAdapterEdge(db, repoId, srcDoc, e, docDir) {
139
+ let dstNode;
140
+ let dstKind = e.dstKind;
141
+ if (e.dstKind === "external") {
142
+ dstNode = resolveExternal(db, repoId, e.target);
143
+ }
144
+ else if (e.target === "") {
145
+ dstNode = srcDoc;
146
+ dstKind = "document";
147
+ }
148
+ else {
149
+ dstNode = resolveDocPath(db, repoId, resolveRelativePath(e.target, docDir)).id;
150
+ dstKind = "document";
151
+ }
152
+ return { srcDoc, srcBlock: e.srcBlock, srcField: e.srcField, predicate: e.predicate, dstKind, dstNode, anchor: e.anchor, provenance: e.provenance };
153
+ }
154
+ //# sourceMappingURL=known-ids.js.map
@@ -0,0 +1,3 @@
1
+ import type { Database } from "better-sqlite3";
2
+ import type { MutDoc } from "./tree.js";
3
+ export declare function loadMutDoc(db: Database, docId: string): MutDoc | null;
@@ -0,0 +1,66 @@
1
+ export function loadMutDoc(db, docId) {
2
+ const doc = db.prepare("SELECT doc_id, path, format, leading_trivia, frontmatter_trivia, current_rev FROM docs WHERE doc_id = ? AND deleted_commit IS NULL").get(docId);
3
+ if (!doc)
4
+ return null;
5
+ const rows = db
6
+ .prepare(`SELECT block_id, parent_block, ordinal, type, attrs, raw_hash, trivia_hash
7
+ FROM blocks WHERE doc_id = ? AND deleted_commit IS NULL
8
+ ORDER BY parent_block, ordinal`)
9
+ .all(docId);
10
+ const blob = db.prepare("SELECT bytes FROM blobs WHERE hash = ?");
11
+ const nodes = new Map();
12
+ for (const r of rows) {
13
+ const raw = blob.get(r.raw_hash)?.bytes.toString("utf8") ?? "";
14
+ // A NULL trivia_hash means the block's trailing trivia was genuinely empty
15
+ // (e.g. the last block of a file with no trailing newline). Emit "" — never
16
+ // a fabricated separator, which would corrupt the file on the next write.
17
+ // Freshly-inserted blocks carry their own explicit trivia from the op layer,
18
+ // so they are unaffected by this.
19
+ let trivia = "";
20
+ if (r.trivia_hash) {
21
+ const tb = blob.get(r.trivia_hash);
22
+ if (tb)
23
+ trivia = tb.bytes.toString("utf8");
24
+ }
25
+ nodes.set(r.block_id, {
26
+ id: r.block_id,
27
+ type: r.type,
28
+ raw,
29
+ trivia,
30
+ attrs: JSON.parse(r.attrs),
31
+ children: [],
32
+ });
33
+ }
34
+ const roots = [];
35
+ const ordered = [...rows].sort((a, b) => a.ordinal - b.ordinal);
36
+ for (const r of ordered) {
37
+ const node = nodes.get(r.block_id);
38
+ if (r.parent_block && nodes.has(r.parent_block))
39
+ nodes.get(r.parent_block).children.push(node);
40
+ }
41
+ for (const r of ordered) {
42
+ if (!r.parent_block || !nodes.has(r.parent_block))
43
+ roots.push(nodes.get(r.block_id));
44
+ }
45
+ let frontmatterRaw = null;
46
+ if (doc.current_rev) {
47
+ const rev = db.prepare("SELECT frontmatter_blob FROM revisions WHERE rev_id = ?").get(doc.current_rev);
48
+ if (rev?.frontmatter_blob) {
49
+ const fm = blob.get(rev.frontmatter_blob);
50
+ // Append the exact stored separator (docs.frontmatter_trivia) so the
51
+ // write path round-trips byte-for-byte. Ingest always captures it when a
52
+ // frontmatter block exists, so the ?? "" is just a type guard.
53
+ if (fm)
54
+ frontmatterRaw = fm.bytes.toString("utf8") + (doc.frontmatter_trivia ?? "");
55
+ }
56
+ }
57
+ return {
58
+ docId: doc.doc_id,
59
+ path: doc.path,
60
+ format: doc.format,
61
+ leadingTrivia: doc.leading_trivia,
62
+ frontmatterRaw,
63
+ children: roots,
64
+ };
65
+ }
66
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1,32 @@
1
+ import type { RawBlock } from "../core/parse/types.js";
2
+ import { type MutDoc } from "./tree.js";
3
+ import type { PlanOp } from "./opset.js";
4
+ /** A disposition row as this module consumes it (decoupled from reconcile/). */
5
+ export interface LowerDisposition {
6
+ blockId: string;
7
+ kind: string;
8
+ confidence: number | null;
9
+ reason: string | null;
10
+ detail?: Record<string, unknown>;
11
+ }
12
+ export interface LowerResult {
13
+ ops: PlanOp[];
14
+ preserved: number;
15
+ ambiguous: number;
16
+ }
17
+ /**
18
+ * Lower a reconciled whole-document update to a kernel opset (top-level
19
+ * granularity). Preserves identity for every top-level block carried unchanged
20
+ * and correctly realizes add / remove / reorder / content-edit at the top
21
+ * level. A carried container whose content changed is updated as a whole (its
22
+ * children re-minted); recursion to preserve nested identity is a later
23
+ * refinement (verification keeps this honest either way).
24
+ */
25
+ export declare function lowerTopLevel(oldDoc: MutDoc, rest: RawBlock[], assignment: Map<string, string>, dispositions: LowerDisposition[]): LowerResult;
26
+ /**
27
+ * Guaranteed fallback: replace the whole body. Remove every current top-level
28
+ * block, then insert the entire proposed body as one op. Loses all block
29
+ * identity but always reproduces the exact bytes (verification still checks).
30
+ * Used only when no identity-preserving lowering converges.
31
+ */
32
+ export declare function lowerReplace(oldDoc: MutDoc, content: string): LowerResult;