@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.
- package/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- 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>;
|