@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,84 @@
1
+ import { docPropertiesMerged } from "../core/store/properties.js";
2
+ import { textSearch } from "./text.js";
3
+ import { vectorSearch } from "./vector.js";
4
+ // Hybrid retrieval & ranking (05 §5). Reciprocal-rank fusion of the FTS and
5
+ // vector rankers, then explainable multiplicative boosts. Every hit carries its
6
+ // evidence (ranks, cosine, boost breakdown). No learned ranker (ADR-009).
7
+ const RRF_K = 60;
8
+ const LAYER_BOOST = { canon: 1.3, working: 1.15, proposed: 1.0, draft: 0.85 };
9
+ function computeBoosts(store, docId, blockId, terms) {
10
+ const boosts = {};
11
+ const doc = store.db.prepare("SELECT path FROM docs WHERE doc_id = ?").get(docId);
12
+ if (!doc)
13
+ return boosts;
14
+ const fm = docPropertiesMerged(store.db, docId);
15
+ const lowerTerms = terms.map((t) => t.toLowerCase()).filter(Boolean);
16
+ // title match (frontmatter title or first heading text)
17
+ const title = String(fm.title ?? "").toLowerCase();
18
+ if (title && lowerTerms.some((t) => title.includes(t)))
19
+ boosts.title = 1.25;
20
+ // heading-chain match: any ancestor section heading contains a term
21
+ const headings = store.db.prepare(`SELECT hb.text FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
22
+ JOIN blocks b ON b.doc_id = s.doc_id AND b.ordinal BETWEEN s.first_ordinal AND s.last_ordinal
23
+ WHERE b.block_id = ?`).all(blockId);
24
+ if (headings.some((h) => lowerTerms.some((t) => h.text.toLowerCase().includes(t))))
25
+ boosts.heading = 1.15;
26
+ // path segment match
27
+ if (lowerTerms.some((t) => doc.path.toLowerCase().includes(t)))
28
+ boosts.path = 1.1;
29
+ // epistemic layer
30
+ const layer = String(fm.layer ?? "");
31
+ const layerBoost = LAYER_BOOST[layer];
32
+ if (layerBoost !== undefined && layerBoost !== 1.0)
33
+ boosts.layer = layerBoost;
34
+ return boosts;
35
+ }
36
+ function applyBoosts(base, b) {
37
+ return base * (b.title ?? 1) * (b.heading ?? 1) * (b.path ?? 1) * (b.layer ?? 1) * (b.recency ?? 1);
38
+ }
39
+ /** Hybrid search: RRF-fuse FTS + vector rankings, then multiplicative boosts. */
40
+ export function hybridSearch(store, input) {
41
+ const limit = input.limit ?? 50;
42
+ const terms = input.terms ?? (input.text ? input.text.split(/\s+/).filter(Boolean) : []);
43
+ const ftsRanks = new Map();
44
+ if (input.text) {
45
+ textSearch(store, input.repoId, input.text, { limit: 200 }).hits.forEach((h, i) => {
46
+ if (!ftsRanks.has(h.blockId))
47
+ ftsRanks.set(h.blockId, i + 1);
48
+ });
49
+ }
50
+ const vecRanks = new Map();
51
+ const cosineByBlock = new Map();
52
+ if (input.vector) {
53
+ vectorSearch(store, input.repoId, input.vector.model, input.vector.vec, { limit: 200 }).forEach((h, i) => {
54
+ vecRanks.set(h.blockId, i + 1);
55
+ cosineByBlock.set(h.blockId, h.cosine);
56
+ });
57
+ }
58
+ // Union of candidate block ids.
59
+ const candidates = new Set([...ftsRanks.keys(), ...vecRanks.keys()]);
60
+ const meta = store.db.prepare("SELECT doc_id, (SELECT path FROM docs WHERE doc_id = blocks.doc_id) AS path FROM blocks WHERE block_id = ?");
61
+ const hits = [];
62
+ for (const blockId of candidates) {
63
+ const fr = ftsRanks.get(blockId);
64
+ const vr = vecRanks.get(blockId);
65
+ const rrf = (fr ? 1 / (RRF_K + fr) : 0) + (vr ? 1 / (RRF_K + vr) : 0);
66
+ const m = meta.get(blockId);
67
+ if (!m)
68
+ continue;
69
+ const boosts = computeBoosts(store, m.doc_id, blockId, terms);
70
+ const evidence = { rrf, boosts };
71
+ if (fr !== undefined)
72
+ evidence.ftsRank = fr;
73
+ if (vr !== undefined) {
74
+ evidence.vectorRank = vr;
75
+ const cos = cosineByBlock.get(blockId);
76
+ if (cos !== undefined)
77
+ evidence.cosine = cos;
78
+ }
79
+ hits.push({ blockId, docId: m.doc_id, path: m.path, score: applyBoosts(rrf, boosts), evidence });
80
+ }
81
+ hits.sort((a, b) => b.score - a.score || (a.blockId < b.blockId ? -1 : 1));
82
+ return hits.slice(0, limit);
83
+ }
84
+ //# sourceMappingURL=rrf.js.map
@@ -0,0 +1,12 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import { type EmbedTask, type DocEmbedTask } from "./embeddings.js";
3
+ /** Build EmbedTask[] for every embeddable live block in the repo. */
4
+ export declare function buildEmbedTasks(store: Store, repoId: string): EmbedTask[];
5
+ /**
6
+ * Build DocEmbedTask[] for every live document in the repo (doc-grain semantic
7
+ * retrieval). Each task carries the whole-document embed input (header +
8
+ * reconstructed body) and the doc's block references (for the pooled fallback,
9
+ * reusing block vectors already keyed in the embeddings cache with the SAME ctx
10
+ * as buildEmbedTasks). Docs with no reconstructable content are skipped.
11
+ */
12
+ export declare function buildDocEmbedTasks(store: Store, repoId: string): DocEmbedTask[];
@@ -0,0 +1,131 @@
1
+ import { reconstructContent } from "../core/read/document.js";
2
+ import { docPropertiesMerged } from "../core/store/properties.js";
3
+ import { contextPrefix, shouldEmbed, estimateTokens } from "./embeddings.js";
4
+ function docTitle(titleByDoc, firstHeadingByDoc, docId, path) {
5
+ const t = titleByDoc.get(docId);
6
+ if (t && t.trim())
7
+ return t;
8
+ return firstHeadingByDoc.get(docId) ?? path;
9
+ }
10
+ /** Build EmbedTask[] for every embeddable live block in the repo. */
11
+ export function buildEmbedTasks(store, repoId) {
12
+ const blocks = store.db
13
+ .prepare(`SELECT b.block_id, b.doc_id, d.path AS path,
14
+ b.ordinal, b.type, b.text, b.raw_hash
15
+ FROM blocks b JOIN docs d ON d.doc_id = b.doc_id
16
+ WHERE b.repo_id = ? AND b.deleted_commit IS NULL
17
+ ORDER BY d.path, b.ordinal`)
18
+ .all(repoId);
19
+ // Frontmatter title per doc (scalar), for the embed context prefix. One scan
20
+ // of the properties table instead of a per-block metadata parse.
21
+ const titleByDoc = new Map();
22
+ for (const r of store.db.prepare(`SELECT p.doc_id AS doc_id, p.val_text AS title FROM properties p
23
+ WHERE p.repo_id = ? AND p.source = 'frontmatter' AND p.key = 'title'
24
+ AND p.card = 'scalar' AND p.type = 'string' AND p.deleted_commit IS NULL`).all(repoId)) {
25
+ if (r.title)
26
+ titleByDoc.set(r.doc_id, r.title);
27
+ }
28
+ // All sections in the repo, with their heading text, for chain lookup.
29
+ const sections = store.db
30
+ .prepare(`SELECT s.doc_id AS doc_id, hb.text AS heading_text, s.level AS level,
31
+ s.first_ordinal AS first_ordinal, s.last_ordinal AS last_ordinal
32
+ FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
33
+ WHERE hb.repo_id = ?`)
34
+ .all(repoId);
35
+ const sectionsByDoc = new Map();
36
+ for (const s of sections) {
37
+ (sectionsByDoc.get(s.doc_id) ?? sectionsByDoc.set(s.doc_id, []).get(s.doc_id)).push(s);
38
+ }
39
+ // First heading text per doc, as a title fallback.
40
+ const firstHeadingByDoc = new Map();
41
+ for (const s of sections) {
42
+ if (!firstHeadingByDoc.has(s.doc_id))
43
+ firstHeadingByDoc.set(s.doc_id, s.heading_text);
44
+ }
45
+ const headingChain = (docId, ordinal) => {
46
+ const docSections = sectionsByDoc.get(docId);
47
+ if (!docSections)
48
+ return [];
49
+ // Sections containing this ordinal, shallowest (lowest level) first.
50
+ return docSections
51
+ .filter((s) => ordinal >= s.first_ordinal && ordinal <= s.last_ordinal)
52
+ .sort((a, b) => a.level - b.level)
53
+ .map((s) => s.heading_text);
54
+ };
55
+ const tasks = [];
56
+ for (const b of blocks) {
57
+ if (!shouldEmbed(b.text))
58
+ continue;
59
+ const ctx = contextPrefix({
60
+ docTitle: docTitle(titleByDoc, firstHeadingByDoc, b.doc_id, b.path),
61
+ path: b.path,
62
+ headingChain: headingChain(b.doc_id, b.ordinal),
63
+ blockType: b.type,
64
+ });
65
+ tasks.push({ blockId: b.block_id, contentHashHex: b.raw_hash.toString("hex"), ctx, text: b.text });
66
+ }
67
+ return tasks;
68
+ }
69
+ // The doc-embedding header: a lightweight identifying line prepended to the
70
+ // body so schema-y atomic notes (a two-line canon definition) embed with their
71
+ // title + type/layer context rather than as near-empty bodies. Kept minimal and
72
+ // consistent with the block embedder's context prefix shape ("title · path ·
73
+ // …") so both grains embed in a comparable space.
74
+ function docHeader(title, path, props) {
75
+ const bits = [title, path];
76
+ const type = props["type"];
77
+ if (typeof type === "string" && type.trim())
78
+ bits.push(`type: ${type}`);
79
+ const layer = props["layer"];
80
+ if (typeof layer === "string" && layer.trim())
81
+ bits.push(`layer: ${layer}`);
82
+ return bits.join(" · ");
83
+ }
84
+ /**
85
+ * Build DocEmbedTask[] for every live document in the repo (doc-grain semantic
86
+ * retrieval). Each task carries the whole-document embed input (header +
87
+ * reconstructed body) and the doc's block references (for the pooled fallback,
88
+ * reusing block vectors already keyed in the embeddings cache with the SAME ctx
89
+ * as buildEmbedTasks). Docs with no reconstructable content are skipped.
90
+ */
91
+ export function buildDocEmbedTasks(store, repoId) {
92
+ const blockTasks = buildEmbedTasks(store, repoId);
93
+ const blocksByDoc = new Map();
94
+ // Map block tasks back to their owning doc via a block_id → doc_id lookup, so
95
+ // the pooled fallback references exactly the vectors the block embedder cached.
96
+ const docIdByBlock = new Map();
97
+ for (const r of store.db.prepare(`SELECT block_id, doc_id FROM blocks WHERE repo_id = ? AND deleted_commit IS NULL`).all(repoId)) {
98
+ docIdByBlock.set(r.block_id, r.doc_id);
99
+ }
100
+ for (const t of blockTasks) {
101
+ const docId = docIdByBlock.get(t.blockId);
102
+ if (!docId)
103
+ continue;
104
+ (blocksByDoc.get(docId) ?? blocksByDoc.set(docId, []).get(docId)).push({
105
+ contentHashHex: t.contentHashHex,
106
+ ctx: t.ctx,
107
+ tokens: estimateTokens(t.text),
108
+ });
109
+ }
110
+ const docs = store.db
111
+ .prepare(`SELECT doc_id, path FROM docs WHERE repo_id = ? AND deleted_commit IS NULL ORDER BY path`)
112
+ .all(repoId);
113
+ const tasks = [];
114
+ for (const d of docs) {
115
+ const body = reconstructContent(store.db, d.doc_id);
116
+ if (body === null)
117
+ continue;
118
+ const props = docPropertiesMerged(store.db, d.doc_id);
119
+ const title = (typeof props["$title"] === "string" && props["$title"].trim())
120
+ ? props["$title"]
121
+ : (typeof props["title"] === "string" && props["title"].trim())
122
+ ? props["title"]
123
+ : d.path;
124
+ const input = `${docHeader(title, d.path, props)}\n${body}`;
125
+ if (input.trim().length === 0)
126
+ continue;
127
+ tasks.push({ docId: d.doc_id, input, blocks: blocksByDoc.get(d.doc_id) ?? [] });
128
+ }
129
+ return tasks;
130
+ }
131
+ //# sourceMappingURL=tasks.js.map
@@ -0,0 +1,16 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ export interface TextHit {
3
+ blockId: string;
4
+ docId: string;
5
+ path: string;
6
+ type: string;
7
+ text: string;
8
+ score: number;
9
+ }
10
+ export interface TextSearchResult {
11
+ hits: TextHit[];
12
+ truncated: boolean;
13
+ }
14
+ export declare function textSearch(store: Store, repoId: string, queryStr: string, opts?: {
15
+ limit?: number;
16
+ }): TextSearchResult;
@@ -0,0 +1,21 @@
1
+ import { sanitizeFtsQuery } from "./fts-query.js";
2
+ export function textSearch(store, repoId, queryStr, opts = {}) {
3
+ const limit = opts.limit ?? 50;
4
+ const match = sanitizeFtsQuery(queryStr);
5
+ if (match === "")
6
+ return { hits: [], truncated: false };
7
+ const rows = store.db
8
+ .prepare(`SELECT b.block_id AS blockId, b.doc_id AS docId, d.path AS path, b.type AS type,
9
+ b.text AS text, bm25(blocks_fts) AS score
10
+ FROM blocks_fts
11
+ JOIN blocks b ON b.rowid = blocks_fts.rowid
12
+ JOIN docs d ON d.doc_id = b.doc_id
13
+ WHERE blocks_fts MATCH ? AND b.repo_id = ? AND b.deleted_commit IS NULL
14
+ ORDER BY score
15
+ LIMIT ?`)
16
+ .all(match, repoId, limit + 1);
17
+ const truncated = rows.length > limit;
18
+ const hits = rows.slice(0, limit).map((r) => ({ ...r, score: -r.score }));
19
+ return { hits, truncated };
20
+ }
21
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1,18 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ export interface VectorHit {
3
+ blockId: string;
4
+ docId: string;
5
+ path: string;
6
+ cosine: number;
7
+ }
8
+ export interface DocVectorHit {
9
+ docId: string;
10
+ path: string;
11
+ cosine: number;
12
+ }
13
+ export declare function vectorSearch(store: Store, repoId: string, model: string, queryVec: Float32Array, opts?: {
14
+ limit?: number;
15
+ }): VectorHit[];
16
+ export declare function docVectorSearch(store: Store, repoId: string, model: string, queryVec: Float32Array, opts?: {
17
+ limit?: number;
18
+ }): DocVectorHit[];
@@ -0,0 +1,49 @@
1
+ function cosine(a, b) {
2
+ let dot = 0, na = 0, nb = 0;
3
+ const n = Math.min(a.length, b.length);
4
+ for (let i = 0; i < n; i++) {
5
+ dot += a[i] * b[i];
6
+ na += a[i] * a[i];
7
+ nb += b[i] * b[i];
8
+ }
9
+ if (na === 0 || nb === 0)
10
+ return 0;
11
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
12
+ }
13
+ export function vectorSearch(store, repoId, model, queryVec, opts = {}) {
14
+ const limit = opts.limit ?? 50;
15
+ const rows = store.db
16
+ .prepare(`SELECT b.block_id AS blockId, b.doc_id AS docId, d.path AS path, e.vec AS vec
17
+ FROM embeddings e
18
+ JOIN blocks b ON b.raw_hash = e.content_hash AND b.deleted_commit IS NULL
19
+ JOIN docs d ON d.doc_id = b.doc_id
20
+ WHERE b.repo_id = ? AND e.model = ?`)
21
+ .all(repoId, model);
22
+ const scored = rows.map((r) => {
23
+ const v = new Float32Array(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength / 4);
24
+ return { blockId: r.blockId, docId: r.docId, path: r.path, cosine: cosine(queryVec, v) };
25
+ });
26
+ scored.sort((a, b) => b.cosine - a.cosine || (a.blockId < b.blockId ? -1 : 1));
27
+ return scored.slice(0, limit);
28
+ }
29
+ // Doc-grain vector search (doc semantic retrieval). Brute-force cosine over the
30
+ // doc_embeddings cache joined to live docs — one vector per document, so results
31
+ // are ranked whole documents (mrplex-parity), never multiple blocks of the same
32
+ // file. Keyed by doc_id (not content_hash), so we join doc_embeddings.doc_id =
33
+ // docs.doc_id and keep only rows for the requested model.
34
+ export function docVectorSearch(store, repoId, model, queryVec, opts = {}) {
35
+ const limit = opts.limit ?? 50;
36
+ const rows = store.db
37
+ .prepare(`SELECT d.doc_id AS docId, d.path AS path, e.vec AS vec
38
+ FROM doc_embeddings e
39
+ JOIN docs d ON d.doc_id = e.doc_id AND d.deleted_commit IS NULL
40
+ WHERE d.repo_id = ? AND e.model = ?`)
41
+ .all(repoId, model);
42
+ const scored = rows.map((r) => {
43
+ const v = new Float32Array(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength / 4);
44
+ return { docId: r.docId, path: r.path, cosine: cosine(queryVec, v) };
45
+ });
46
+ scored.sort((a, b) => b.cosine - a.cosine || (a.docId < b.docId ? -1 : 1));
47
+ return scored.slice(0, limit);
48
+ }
49
+ //# sourceMappingURL=vector.js.map
@@ -0,0 +1,59 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import { type DiskDrift } from "./freshness.js";
3
+ /**
4
+ * On-disk agreement for a repo. Present only when a rootPath was supplied so a
5
+ * READ-ONLY disk-drift scan could run. When absent, disk agreement is UNKNOWN
6
+ * — callers must NOT treat that as "converged" (see syncStatus).
7
+ */
8
+ export interface DiskStatus extends DiskDrift {
9
+ /** true iff a disk scan actually ran (rootPath supplied). */
10
+ checked: boolean;
11
+ }
12
+ export interface RepoStatus {
13
+ repoId: string;
14
+ slug: string;
15
+ rootPath: string;
16
+ docs: number;
17
+ blocks: number;
18
+ commits: number;
19
+ openEdges: number;
20
+ /** docs whose file_hash != current revision rendered_hash (unconverged) */
21
+ unconverged: number;
22
+ /**
23
+ * On-disk drift vs the DB, from a READ-ONLY scan. `checked` is false when no
24
+ * rootPath was available (disk agreement UNVERIFIED); drift counts are 0 in
25
+ * that case but MUST NOT be read as "no drift".
26
+ */
27
+ disk: DiskStatus;
28
+ }
29
+ /**
30
+ * @param rootPath optional working-tree root. When provided, a READ-ONLY
31
+ * disk-drift scan runs and populates `disk`. When omitted, `disk.checked` is
32
+ * false and disk agreement is unverified (do not infer convergence from it).
33
+ */
34
+ export declare function reposStatus(store: Store, repoId: string, rootPath?: string): RepoStatus;
35
+ export interface SyncStatus {
36
+ lastCommitSeq: number;
37
+ lastCheckpoint: string | null;
38
+ /**
39
+ * true ONLY when the DB-internal convergence holds (unconverged === 0) AND a
40
+ * disk scan ran (diskChecked) AND found no drift. When the disk scan could
41
+ * not run, this is false even if the DB signal is clean — we never show green
42
+ * on knowledge we can't confirm is fresh. See `diskChecked` to distinguish
43
+ * "verified stale" from "unverified".
44
+ */
45
+ convergent: boolean;
46
+ /** whether a disk-drift scan actually ran (rootPath available). */
47
+ diskChecked: boolean;
48
+ /** on-disk drift counts from the READ-ONLY scan (all 0 when !diskChecked). */
49
+ disk: DiskStatus;
50
+ }
51
+ /**
52
+ * @param rootPath optional working-tree root; forwarded to reposStatus so the
53
+ * disk-drift scan can run. Without it, `convergent` is false and
54
+ * `diskChecked` is false: disk freshness is UNVERIFIED, which we treat as
55
+ * non-convergent rather than optimistically green. Reporting the DB-only
56
+ * signal as convergent could hide a file deleted/edited on disk but not yet
57
+ * re-ingested — the exact stale-under-green-light failure this guards against.
58
+ */
59
+ export declare function syncStatus(store: Store, repoId: string, rootPath?: string): SyncStatus;
@@ -0,0 +1,53 @@
1
+ import { detectDiskDrift } from "./freshness.js";
2
+ /**
3
+ * @param rootPath optional working-tree root. When provided, a READ-ONLY
4
+ * disk-drift scan runs and populates `disk`. When omitted, `disk.checked` is
5
+ * false and disk agreement is unverified (do not infer convergence from it).
6
+ */
7
+ export function reposStatus(store, repoId, rootPath) {
8
+ const repo = store.db.prepare("SELECT slug FROM repos WHERE repo_id = ?").get(repoId);
9
+ const count = (sql) => store.db.prepare(sql).get(repoId).c;
10
+ const unconverged = store.db.prepare(`SELECT count(*) c FROM docs d JOIN revisions r ON r.rev_id = d.current_rev
11
+ WHERE d.repo_id = ? AND d.deleted_commit IS NULL AND d.file_hash IS NOT r.rendered_hash`).get(repoId).c;
12
+ // Only scan when the caller explicitly supplies a rootPath. We deliberately
13
+ // do NOT fall back to the repo's stored root_path: that column is a stale,
14
+ // filesystem-only hint (NULL for sourceless repos) and using it would blur
15
+ // the "the caller has a live working tree to verify against" signal that
16
+ // `checked` is meant to convey.
17
+ const disk = rootPath
18
+ ? { ...detectDiskDrift(store, repoId, rootPath), checked: true }
19
+ : { changed: 0, deleted: 0, untracked: 0, checked: false };
20
+ return {
21
+ repoId,
22
+ slug: repo?.slug ?? "",
23
+ rootPath: rootPath ?? "",
24
+ docs: count("SELECT count(*) c FROM docs WHERE repo_id = ? AND deleted_commit IS NULL"),
25
+ blocks: count("SELECT count(*) c FROM blocks WHERE repo_id = ? AND deleted_commit IS NULL"),
26
+ commits: count("SELECT count(*) c FROM commits WHERE repo_id = ?"),
27
+ openEdges: count("SELECT count(*) c FROM edges WHERE repo_id = ? AND to_commit IS NULL"),
28
+ unconverged,
29
+ disk,
30
+ };
31
+ }
32
+ /**
33
+ * @param rootPath optional working-tree root; forwarded to reposStatus so the
34
+ * disk-drift scan can run. Without it, `convergent` is false and
35
+ * `diskChecked` is false: disk freshness is UNVERIFIED, which we treat as
36
+ * non-convergent rather than optimistically green. Reporting the DB-only
37
+ * signal as convergent could hide a file deleted/edited on disk but not yet
38
+ * re-ingested — the exact stale-under-green-light failure this guards against.
39
+ */
40
+ export function syncStatus(store, repoId, rootPath) {
41
+ const lastCommit = store.db.prepare("SELECT MAX(seq) s FROM commits WHERE repo_id = ?").get(repoId);
42
+ const lastCp = store.db.prepare("SELECT id FROM checkpoints WHERE repo_id = ? ORDER BY ts DESC LIMIT 1").get(repoId);
43
+ const status = reposStatus(store, repoId, rootPath);
44
+ const noDrift = status.disk.changed === 0 && status.disk.deleted === 0 && status.disk.untracked === 0;
45
+ return {
46
+ lastCommitSeq: lastCommit.s ?? 0,
47
+ lastCheckpoint: lastCp?.id ?? null,
48
+ convergent: status.unconverged === 0 && status.disk.checked && noDrift,
49
+ diskChecked: status.disk.checked,
50
+ disk: status.disk,
51
+ };
52
+ }
53
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1,13 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ export interface IngestDirResult {
3
+ repoId: string;
4
+ fileCount: number;
5
+ allConverged: boolean;
6
+ }
7
+ /**
8
+ * Ingest a directory as a repo: create/reuse the repo (registering its fs
9
+ * source) + ingest all Markdown, reconciling identity and extracting edges.
10
+ * `files` (repo-relative paths) may be supplied by a caller that already walked
11
+ * the tree, to avoid walking twice; omit it to walk here.
12
+ */
13
+ export declare function ingestDirectory(store: Store, slug: string, rootPath: string, files?: string[]): IngestDirResult;
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ensureRepo } from "../core/attach.js";
4
+ import { ingestFile } from "../core/ingest.js";
5
+ import { makeReconcilingResolver } from "./reconciling-ingest.js";
6
+ import { walkMarkdown } from "./fs-util.js";
7
+ /**
8
+ * Ingest a directory as a repo: create/reuse the repo (registering its fs
9
+ * source) + ingest all Markdown, reconciling identity and extracting edges.
10
+ * `files` (repo-relative paths) may be supplied by a caller that already walked
11
+ * the tree, to avoid walking twice; omit it to walk here.
12
+ */
13
+ export function ingestDirectory(store, slug, rootPath, files) {
14
+ const repoId = ensureRepo(store, slug, rootPath);
15
+ const walked = files ?? walkMarkdown(rootPath);
16
+ const ts = new Date().toISOString();
17
+ let allConverged = true;
18
+ for (const rel of walked) {
19
+ const content = readFileSync(join(rootPath, rel), "utf8");
20
+ const res = ingestFile(store, repoId, rel, content, {
21
+ ts,
22
+ resolveIds: makeReconcilingResolver(store, repoId, { ts, path: rel }),
23
+ });
24
+ if (!res.converged)
25
+ allConverged = false;
26
+ }
27
+ return { repoId, fileCount: walked.length, allConverged };
28
+ }
29
+ //# sourceMappingURL=attach.js.map
@@ -0,0 +1,21 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ export interface FileChange {
3
+ /** repo-relative canonical path */
4
+ path: string;
5
+ }
6
+ export interface CheckpointResult {
7
+ checkpointId: string;
8
+ ingested: string[];
9
+ suppressed: string[];
10
+ deleted: string[];
11
+ conflicted: string[];
12
+ }
13
+ /**
14
+ * Process a batch of changed paths against a repo as one checkpoint. On-disk
15
+ * bytes are read directly; a hash equal to the stored file_hash is an engine
16
+ * echo (no commit). Otherwise ingest as an observed commit.
17
+ */
18
+ export declare function processCheckpoint(store: Store, repoId: string, rootPath: string, changes: FileChange[], opts?: {
19
+ ts?: string;
20
+ gitHead?: string | null;
21
+ }): CheckpointResult;
@@ -0,0 +1,52 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { mintId } from "../core/ids.js";
4
+ import { observeOne } from "./observe.js";
5
+ import { sweepResurrectionPool } from "../core/store/gc.js";
6
+ import { tombstoneObservedDeletion } from "./tombstone.js";
7
+ /**
8
+ * Process a batch of changed paths against a repo as one checkpoint. On-disk
9
+ * bytes are read directly; a hash equal to the stored file_hash is an engine
10
+ * echo (no commit). Otherwise ingest as an observed commit.
11
+ */
12
+ export function processCheckpoint(store, repoId, rootPath, changes, opts = {}) {
13
+ const ts = opts.ts ?? new Date().toISOString();
14
+ const checkpointId = mintId("cp");
15
+ const ingested = [];
16
+ const suppressed = [];
17
+ const deleted = [];
18
+ const conflicted = [];
19
+ const fileEntries = [];
20
+ for (const change of changes) {
21
+ const abs = join(rootPath, change.path);
22
+ if (!existsSync(abs)) {
23
+ // File gone from disk: tombstone the live doc (drop FTS, tombstone blocks +
24
+ // doc, pool blocks for resurrection) so it stops being served as a ghost.
25
+ const existing = store.db
26
+ .prepare("SELECT doc_id, file_hash FROM docs WHERE repo_id = ? AND path = ? AND deleted_commit IS NULL")
27
+ .get(repoId, change.path);
28
+ if (existing) {
29
+ tombstoneObservedDeletion(store, repoId, existing.doc_id, ts);
30
+ deleted.push(change.path);
31
+ }
32
+ fileEntries.push([change.path, existing?.file_hash?.toString("hex") ?? null, null]);
33
+ continue;
34
+ }
35
+ // Reconcile the disk bytes through the shared observe primitive (echo gate,
36
+ // identity threading, conflict-marker flagging — the one implementation).
37
+ const r = observeOne(store, repoId, change.path, readFileSync(abs, "utf8"), ts);
38
+ if (r.echo)
39
+ suppressed.push(change.path);
40
+ else if (r.conflicted)
41
+ conflicted.push(change.path);
42
+ else
43
+ ingested.push(change.path);
44
+ fileEntries.push([change.path, r.oldHashHex, r.newHashHex]);
45
+ }
46
+ store.db
47
+ .prepare("INSERT INTO checkpoints (id, repo_id, ts, files, git_head) VALUES (?, ?, ?, ?, ?)")
48
+ .run(checkpointId, repoId, ts, JSON.stringify(fileEntries), opts.gitHead ?? null);
49
+ sweepResurrectionPool(store, ts);
50
+ return { checkpointId, ingested, suppressed, deleted, conflicted };
51
+ }
52
+ //# sourceMappingURL=checkpoint.js.map
@@ -0,0 +1,25 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import type { SyncSource } from "./plugin.js";
3
+ import type { FileChange, CheckpointResult } from "./checkpoint.js";
4
+ export type { FileChange, CheckpointResult };
5
+ /**
6
+ * Reconcile a batch of changed members against a repo as one checkpoint.
7
+ * `source.fetch(path)` supplies current bytes (or null = left the scope). The
8
+ * engine hashes the bytes itself: a hash equal to the stored file_hash is an
9
+ * echo (no commit); otherwise the member is ingested as an observed commit.
10
+ */
11
+ export declare function reconcileChanges(store: Store, repoId: string, source: SyncSource, changes: FileChange[], opts?: {
12
+ ts?: string;
13
+ gitHead?: string | null;
14
+ }): Promise<CheckpointResult>;
15
+ export interface AttachResult {
16
+ repoId: string;
17
+ fileCount: number;
18
+ allConverged: boolean;
19
+ }
20
+ /**
21
+ * Attach a source scope as a repo: create/reuse the repo row, then ingest every
22
+ * member the source enumerates (identity-inferred sources thread the reconciling
23
+ * resolver so edges + identity are established on the initial walk).
24
+ */
25
+ export declare function attachSource(store: Store, slug: string, rootPath: string | null, source: SyncSource): Promise<AttachResult>;