@omgbase/core 0.2.0 → 0.4.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/dist/src/core/clock.d.ts +9 -0
- package/dist/src/core/clock.js +46 -0
- package/dist/src/core/ingest.js +15 -12
- package/dist/src/core/read/document.js +6 -1
- package/dist/src/core/read/nodes.js +5 -1
- package/dist/src/core/store/fts.d.ts +38 -7
- package/dist/src/core/store/fts.js +60 -9
- package/dist/src/core/store/rebuild.js +5 -4
- package/dist/src/core/store/writers.d.ts +6 -0
- package/dist/src/core/store/writers.js +1 -0
- package/dist/src/format/markdown.js +27 -23
- package/dist/src/graph/extract.d.ts +41 -1
- package/dist/src/graph/extract.js +59 -1
- package/dist/src/graph/history.d.ts +33 -1
- package/dist/src/graph/history.js +141 -18
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +4 -1
- package/dist/src/mcp/server.js +23 -12
- package/dist/src/mutate/apply.d.ts +2 -0
- package/dist/src/mutate/apply.js +8 -5
- package/dist/src/mutate/known-ids.js +2 -2
- package/dist/src/mutate/ops.d.ts +3 -3
- package/dist/src/mutate/ops.js +38 -13
- package/dist/src/oqx-js/context.d.ts +11 -0
- package/dist/src/oqx-js/context.js +30 -1
- package/dist/src/oqx-js/planner.d.ts +20 -1
- package/dist/src/oqx-js/planner.js +95 -2
- package/dist/src/oqx-js/run.js +27 -2
- package/dist/src/oqx-js/sql/translate.d.ts +71 -1
- package/dist/src/oqx-js/sql/translate.js +165 -70
- package/dist/src/search/rrf.js +2 -1
- package/dist/src/search/tasks.d.ts +34 -1
- package/dist/src/search/tasks.js +42 -23
- package/dist/src/search/vector.js +19 -18
- package/dist/src/sync/external-source.js +20 -7
- package/dist/src/sync/freshness.d.ts +8 -1
- package/dist/src/sync/freshness.js +23 -3
- package/dist/src/sync/plugin.d.ts +30 -5
- package/dist/src/sync/plugin.js +13 -1
- package/dist/src/sync/reconciling-ingest.js +5 -3
- package/dist/src/sync/watcher.d.ts +2 -0
- package/dist/src/sync/watcher.js +10 -2
- package/package.json +2 -2
package/dist/src/search/rrf.js
CHANGED
|
@@ -50,7 +50,8 @@ export function hybridSearch(store, input) {
|
|
|
50
50
|
const vecRanks = new Map();
|
|
51
51
|
const cosineByBlock = new Map();
|
|
52
52
|
if (input.vector) {
|
|
53
|
-
// First occurrence wins, exactly as for FTS (spec/search §4).
|
|
53
|
+
// First occurrence wins, exactly as for FTS (spec/search §4). Since search
|
|
54
|
+
// 1.1 vectorSearch yields one candidate per block, so this is a no-op guard.
|
|
54
55
|
vectorSearch(store, input.repoId, input.vector.model, input.vector.vec, { limit: 200 }).forEach((h, i) => {
|
|
55
56
|
if (vecRanks.has(h.blockId))
|
|
56
57
|
return;
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
import { type EmbedTask, type DocEmbedTask } from "./embeddings.js";
|
|
3
|
-
/**
|
|
3
|
+
/** The columns a block's current context (spec/search §2.2) is a function of. */
|
|
4
|
+
export interface BlockContextInput {
|
|
5
|
+
doc_id: string;
|
|
6
|
+
path: string;
|
|
7
|
+
ordinal: number;
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a block's *current* embedding context (spec/search §2.2) — the one
|
|
12
|
+
* rule every reader of the `embeddings` cache keys with: the drain (tasks), the
|
|
13
|
+
* pooled document vector, `vector_search` (§3) and `semantic()` (spec/surface
|
|
14
|
+
* §1.3). Built once per repo (or per doc) and reused across blocks: one scan of
|
|
15
|
+
* the frontmatter titles and one of the sections, then a per-block walk of the
|
|
16
|
+
* doc's sections for the heading chain.
|
|
17
|
+
*/
|
|
18
|
+
export interface BlockContextResolver {
|
|
19
|
+
ctx(block: BlockContextInput): string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load the title/section facts the context prefix depends on. `scope` narrows
|
|
23
|
+
* the load to one repo (every doc — the drain and vector search) or one doc
|
|
24
|
+
* (a single block's lookup).
|
|
25
|
+
*/
|
|
26
|
+
export declare function makeBlockContextResolver(store: Store, scope: {
|
|
27
|
+
repoId: string;
|
|
28
|
+
} | {
|
|
29
|
+
docId: string;
|
|
30
|
+
}): BlockContextResolver;
|
|
31
|
+
/**
|
|
32
|
+
* Build EmbedTask[] for every embeddable live block in the repo: the live
|
|
33
|
+
* LEAF blocks (spec/search §2.1, 1.2 — a container's text is its children's
|
|
34
|
+
* text, so embedding it would pool the same words twice) whose text clears the
|
|
35
|
+
* `shouldEmbed` floor.
|
|
36
|
+
*/
|
|
4
37
|
export declare function buildEmbedTasks(store: Store, repoId: string): EmbedTask[];
|
|
5
38
|
/**
|
|
6
39
|
* Build DocEmbedTask[] for every live document in the repo (doc-grain semantic
|
package/dist/src/search/tasks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { reconstructContent } from "../core/read/document.js";
|
|
2
2
|
import { docPropertiesMerged } from "../core/store/properties.js";
|
|
3
|
+
import { LIVE_LEAF_SQL } from "../core/store/fts.js";
|
|
3
4
|
import { contextPrefix, shouldEmbed, estimateTokens } from "./embeddings.js";
|
|
4
5
|
function docTitle(titleByDoc, firstHeadingByDoc, docId, path) {
|
|
5
6
|
const t = titleByDoc.get(docId);
|
|
@@ -7,31 +8,31 @@ function docTitle(titleByDoc, firstHeadingByDoc, docId, path) {
|
|
|
7
8
|
return t;
|
|
8
9
|
return firstHeadingByDoc.get(docId) ?? path;
|
|
9
10
|
}
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
11
|
+
/**
|
|
12
|
+
* Load the title/section facts the context prefix depends on. `scope` narrows
|
|
13
|
+
* the load to one repo (every doc — the drain and vector search) or one doc
|
|
14
|
+
* (a single block's lookup).
|
|
15
|
+
*/
|
|
16
|
+
export function makeBlockContextResolver(store, scope) {
|
|
17
|
+
const byRepo = "repoId" in scope;
|
|
18
|
+
// Frontmatter title per doc (scalar string), for the embed context prefix.
|
|
19
|
+
// One scan of the properties table instead of a per-block metadata parse.
|
|
21
20
|
const titleByDoc = new Map();
|
|
22
21
|
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)) {
|
|
22
|
+
WHERE ${byRepo ? "p.repo_id = ?" : "p.doc_id = ?"} AND p.source = 'frontmatter' AND p.key = 'title'
|
|
23
|
+
AND p.card = 'scalar' AND p.type = 'string' AND p.deleted_commit IS NULL`).all(byRepo ? scope.repoId : scope.docId)) {
|
|
25
24
|
if (r.title)
|
|
26
25
|
titleByDoc.set(r.doc_id, r.title);
|
|
27
26
|
}
|
|
28
|
-
//
|
|
27
|
+
// The sections in scope with their heading text, for chain lookup. Ordered by
|
|
28
|
+
// first_ordinal so the first per doc is the title fallback (§2.2).
|
|
29
29
|
const sections = store.db
|
|
30
30
|
.prepare(`SELECT s.doc_id AS doc_id, hb.text AS heading_text, s.level AS level,
|
|
31
31
|
s.first_ordinal AS first_ordinal, s.last_ordinal AS last_ordinal
|
|
32
32
|
FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
|
|
33
|
-
WHERE hb.repo_id =
|
|
34
|
-
|
|
33
|
+
WHERE ${byRepo ? "hb.repo_id = ?" : "s.doc_id = ?"}
|
|
34
|
+
ORDER BY s.doc_id, s.first_ordinal, s.rowid`)
|
|
35
|
+
.all(byRepo ? scope.repoId : scope.docId);
|
|
35
36
|
const sectionsByDoc = new Map();
|
|
36
37
|
for (const s of sections) {
|
|
37
38
|
(sectionsByDoc.get(s.doc_id) ?? sectionsByDoc.set(s.doc_id, []).get(s.doc_id)).push(s);
|
|
@@ -52,17 +53,35 @@ export function buildEmbedTasks(store, repoId) {
|
|
|
52
53
|
.sort((a, b) => a.level - b.level)
|
|
53
54
|
.map((s) => s.heading_text);
|
|
54
55
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!shouldEmbed(b.text))
|
|
58
|
-
continue;
|
|
59
|
-
const ctx = contextPrefix({
|
|
56
|
+
return {
|
|
57
|
+
ctx: (b) => contextPrefix({
|
|
60
58
|
docTitle: docTitle(titleByDoc, firstHeadingByDoc, b.doc_id, b.path),
|
|
61
59
|
path: b.path,
|
|
62
60
|
headingChain: headingChain(b.doc_id, b.ordinal),
|
|
63
61
|
blockType: b.type,
|
|
64
|
-
})
|
|
65
|
-
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build EmbedTask[] for every embeddable live block in the repo: the live
|
|
67
|
+
* LEAF blocks (spec/search §2.1, 1.2 — a container's text is its children's
|
|
68
|
+
* text, so embedding it would pool the same words twice) whose text clears the
|
|
69
|
+
* `shouldEmbed` floor.
|
|
70
|
+
*/
|
|
71
|
+
export function buildEmbedTasks(store, repoId) {
|
|
72
|
+
const blocks = store.db
|
|
73
|
+
.prepare(`SELECT b.block_id, b.doc_id, d.path AS path,
|
|
74
|
+
b.ordinal, b.type, b.text, b.raw_hash
|
|
75
|
+
FROM blocks b JOIN docs d ON d.doc_id = b.doc_id
|
|
76
|
+
WHERE b.repo_id = ? AND ${LIVE_LEAF_SQL}
|
|
77
|
+
ORDER BY d.path, b.ordinal`)
|
|
78
|
+
.all(repoId);
|
|
79
|
+
const contexts = makeBlockContextResolver(store, { repoId });
|
|
80
|
+
const tasks = [];
|
|
81
|
+
for (const b of blocks) {
|
|
82
|
+
if (!shouldEmbed(b.text))
|
|
83
|
+
continue;
|
|
84
|
+
tasks.push({ blockId: b.block_id, contentHashHex: b.raw_hash.toString("hex"), ctx: contexts.ctx(b), text: b.text });
|
|
66
85
|
}
|
|
67
86
|
return tasks;
|
|
68
87
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ctxHashHex } from "./embeddings.js";
|
|
2
|
+
import { buildEmbedTasks } from "./tasks.js";
|
|
1
3
|
function cosine(a, b) {
|
|
2
4
|
let dot = 0, na = 0, nb = 0;
|
|
3
5
|
const n = Math.min(a.length, b.length);
|
|
@@ -12,25 +14,24 @@ function cosine(a, b) {
|
|
|
12
14
|
}
|
|
13
15
|
export function vectorSearch(store, repoId, model, queryVec, opts = {}) {
|
|
14
16
|
const limit = opts.limit ?? 50;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
// The candidates are exactly the drain's tasks: every live embeddable block
|
|
18
|
+
// with its current (content_hash, ctx). Reusing buildEmbedTasks keeps the
|
|
19
|
+
// context rule in one place; its cost is one walk of the repo's blocks and
|
|
20
|
+
// sections per query (fine at v1 scale, see the ceiling above).
|
|
21
|
+
const tasks = buildEmbedTasks(store, repoId);
|
|
22
|
+
const docPath = store.db.prepare("SELECT doc_id AS docId, path FROM blocks JOIN docs USING (doc_id) WHERE block_id = ?");
|
|
23
|
+
const vecFor = store.db.prepare("SELECT vec FROM embeddings WHERE content_hash = ? AND ctx_hash = ? AND model = ?");
|
|
24
|
+
const scored = [];
|
|
25
|
+
for (const t of tasks) {
|
|
26
|
+
const row = vecFor.get(Buffer.from(t.contentHashHex, "hex"), Buffer.from(ctxHashHex(t.ctx), "hex"), model);
|
|
27
|
+
if (!row)
|
|
28
|
+
continue; // stale (§2.3): no vector until the next drain
|
|
29
|
+
const meta = docPath.get(t.blockId);
|
|
30
|
+
if (!meta)
|
|
31
|
+
continue;
|
|
32
|
+
const v = new Float32Array(row.vec.buffer, row.vec.byteOffset, row.vec.byteLength / 4);
|
|
33
|
+
scored.push({ blockId: t.blockId, docId: meta.docId, path: meta.path, cosine: cosine(queryVec, v) });
|
|
32
34
|
}
|
|
33
|
-
const scored = [...best.values()];
|
|
34
35
|
scored.sort((a, b) => b.cosine - a.cosine || (a.blockId < b.blockId ? -1 : 1));
|
|
35
36
|
return scored.slice(0, limit);
|
|
36
37
|
}
|
|
@@ -17,7 +17,7 @@ export async function createExternalSource(spec) {
|
|
|
17
17
|
}
|
|
18
18
|
const rl = createInterface({ input: child.stdout });
|
|
19
19
|
let watchListener = null;
|
|
20
|
-
const router = messageRouter(rl, (
|
|
20
|
+
const router = messageRouter(rl, (ev) => watchListener?.(ev));
|
|
21
21
|
const died = new Promise((_, reject) => {
|
|
22
22
|
child.once("error", (err) => reject(new Error(`sync adapter '${spec.command}' failed to spawn: ${err.message}`)));
|
|
23
23
|
child.once("exit", (code) => reject(new Error(`sync adapter '${spec.command}' exited early (code ${code ?? "?"})`)));
|
|
@@ -69,10 +69,19 @@ export async function createExternalSource(spec) {
|
|
|
69
69
|
},
|
|
70
70
|
...(caps.watch
|
|
71
71
|
? {
|
|
72
|
-
async watch(
|
|
73
|
-
|
|
72
|
+
async watch(listener) {
|
|
73
|
+
// `ready` resolves on the adapter's {"event":"ready"}; the listener sees
|
|
74
|
+
// it as an event too, in wire order with the batches (spec/sync §5, §8).
|
|
75
|
+
let markReady = () => { };
|
|
76
|
+
const ready = new Promise((resolve) => { markReady = resolve; });
|
|
77
|
+
watchListener = (ev) => {
|
|
78
|
+
if (ev.event === "ready")
|
|
79
|
+
markReady();
|
|
80
|
+
listener(ev);
|
|
81
|
+
};
|
|
74
82
|
await call("watch");
|
|
75
83
|
return {
|
|
84
|
+
ready,
|
|
76
85
|
async stop() {
|
|
77
86
|
watchListener = null;
|
|
78
87
|
try {
|
|
@@ -105,9 +114,9 @@ export async function createExternalSource(spec) {
|
|
|
105
114
|
};
|
|
106
115
|
return source;
|
|
107
116
|
}
|
|
108
|
-
// Demultiplex adapter stdout: {"event"
|
|
109
|
-
// every other line is an id-addressed response buffered for nextResponse().
|
|
110
|
-
function messageRouter(rl,
|
|
117
|
+
// Demultiplex adapter stdout: {"event":…} lines feed the watch listener (ready,
|
|
118
|
+
// batch); every other line is an id-addressed response buffered for nextResponse().
|
|
119
|
+
function messageRouter(rl, onEvent) {
|
|
111
120
|
const buffered = [];
|
|
112
121
|
const waiters = [];
|
|
113
122
|
rl.on("line", (line) => {
|
|
@@ -117,7 +126,11 @@ function messageRouter(rl, onBatch) {
|
|
|
117
126
|
try {
|
|
118
127
|
const peek = JSON.parse(trimmed);
|
|
119
128
|
if (peek.event === "batch") {
|
|
120
|
-
|
|
129
|
+
onEvent({ event: "batch", paths: peek.paths ?? [] });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (peek.event === "ready") {
|
|
133
|
+
onEvent({ event: "ready" });
|
|
121
134
|
return;
|
|
122
135
|
}
|
|
123
136
|
}
|
|
@@ -87,7 +87,14 @@ export interface DiskDrift {
|
|
|
87
87
|
export declare function detectDiskDrift(store: Store, repoId: string, rootPath: string, opts?: {
|
|
88
88
|
fs?: SyncFs;
|
|
89
89
|
}): DiskDrift;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Rebuild `file_stats` from scratch (spec/sync §4.3, since 1.1): delete the repo's rows, then
|
|
92
|
+
* walk the tree and record a row only for a file whose bytes' hash equals its live doc's
|
|
93
|
+
* `file_hash` — the cache is allowed to say "known" only about bytes the store already holds.
|
|
94
|
+
* A file with no live doc, or whose bytes differ from what was ingested, gets no row, so the
|
|
95
|
+
* next sweep still treats it as a candidate and drift still reports it (a cache rebuild must
|
|
96
|
+
* never change what the next sweep does). Returns the number of files walked, recorded or not.
|
|
97
|
+
*/
|
|
91
98
|
export declare function rebuildFileStats(store: Store, repoId: string, rootPath: string, opts?: {
|
|
92
99
|
fs?: SyncFs;
|
|
93
100
|
}): number;
|
|
@@ -147,16 +147,36 @@ export function detectDiskDrift(store, repoId, rootPath, opts = {}) {
|
|
|
147
147
|
deleted += 1;
|
|
148
148
|
return { changed, deleted, untracked };
|
|
149
149
|
}
|
|
150
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Rebuild `file_stats` from scratch (spec/sync §4.3, since 1.1): delete the repo's rows, then
|
|
152
|
+
* walk the tree and record a row only for a file whose bytes' hash equals its live doc's
|
|
153
|
+
* `file_hash` — the cache is allowed to say "known" only about bytes the store already holds.
|
|
154
|
+
* A file with no live doc, or whose bytes differ from what was ingested, gets no row, so the
|
|
155
|
+
* next sweep still treats it as a candidate and drift still reports it (a cache rebuild must
|
|
156
|
+
* never change what the next sweep does). Returns the number of files walked, recorded or not.
|
|
157
|
+
*/
|
|
151
158
|
export function rebuildFileStats(store, repoId, rootPath, opts = {}) {
|
|
152
159
|
const fs = opts.fs ?? nodeFs;
|
|
153
160
|
store.db.prepare("DELETE FROM file_stats WHERE repo_id = ?").run(repoId);
|
|
161
|
+
// The live docs' durable convergence signal, keyed by path (one query).
|
|
162
|
+
const live = new Map();
|
|
163
|
+
for (const row of store.db
|
|
164
|
+
.prepare("SELECT path, file_hash FROM docs WHERE repo_id = ? AND deleted_commit IS NULL AND file_hash IS NOT NULL")
|
|
165
|
+
.all(repoId)) {
|
|
166
|
+
live.set(row.path, row.file_hash);
|
|
167
|
+
}
|
|
154
168
|
const paths = fs.walk(rootPath);
|
|
155
169
|
for (const path of paths) {
|
|
170
|
+
const want = live.get(path);
|
|
171
|
+
if (!want)
|
|
172
|
+
continue; // no live doc: nothing the cache may vouch for
|
|
156
173
|
const bytes = fs.read(rootPath, path);
|
|
157
174
|
if (bytes === null)
|
|
158
|
-
continue;
|
|
159
|
-
|
|
175
|
+
continue; // vanished between the walk and the read
|
|
176
|
+
const hash = sha256(bytes);
|
|
177
|
+
if (!hash.equals(want))
|
|
178
|
+
continue; // a pending edit: leave it visible
|
|
179
|
+
recordStat(store, repoId, path, fs.stat(rootPath, path), hash);
|
|
160
180
|
}
|
|
161
181
|
return paths.length;
|
|
162
182
|
}
|
|
@@ -25,13 +25,38 @@ export interface SourceItem extends SourceEntry {
|
|
|
25
25
|
/** The engine hashes this itself for the authoritative echo/convergence gate. */
|
|
26
26
|
content: string;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* An unsolicited event of a live watch (spec/sync §5): `ready` once — the feed
|
|
30
|
+
* is primed and every change from now on will be reported — then `batch` lines
|
|
31
|
+
* of changed storage keys. An adapter built before sync 1.2 never sends `ready`.
|
|
32
|
+
*/
|
|
33
|
+
export type WatchEvent = {
|
|
34
|
+
event: "ready";
|
|
35
|
+
} | {
|
|
36
|
+
event: "batch";
|
|
37
|
+
paths: string[];
|
|
38
|
+
};
|
|
39
|
+
/** Receives every event of a live watch, in wire order. */
|
|
40
|
+
export type WatchListener = (event: WatchEvent) => void;
|
|
30
41
|
/** A live subscription created by SyncSource.watch. */
|
|
31
42
|
export interface SourceWatch {
|
|
32
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Resolves when the source reported `ready` (spec/sync §5). An in-process
|
|
45
|
+
* source resolves it at once; an adapter that never reports leaves it pending,
|
|
46
|
+
* so a host bounds the wait (`awaitReady`).
|
|
47
|
+
*/
|
|
48
|
+
ready: Promise<void>;
|
|
49
|
+
/** Stop delivering events and release resources. */
|
|
33
50
|
stop(): Promise<void>;
|
|
34
51
|
}
|
|
52
|
+
/** How long a host waits for `ready` before proceeding as if ready (spec/sync §5: unpinned; 30 s). */
|
|
53
|
+
export declare const WATCH_READY_PATIENCE_MS = 30000;
|
|
54
|
+
/**
|
|
55
|
+
* Wait for a watch's `ready` with bounded patience: `true` when it resolved in
|
|
56
|
+
* time, `false` on timeout (the host proceeds as if ready, with a warning — an
|
|
57
|
+
* adapter without `ready` still works, with the old window).
|
|
58
|
+
*/
|
|
59
|
+
export declare function awaitReady(ready: Promise<void>, patienceMs?: number): Promise<boolean>;
|
|
35
60
|
/**
|
|
36
61
|
* The in-engine handle to an external source process. Only capabilities/
|
|
37
62
|
* enumerate/fetch are always meaningful; watch exists when capabilities().watch,
|
|
@@ -43,8 +68,8 @@ export interface SyncSource {
|
|
|
43
68
|
enumerate(): Promise<SourceEntry[]>;
|
|
44
69
|
/** Current state of one member, or null if it left the scope (a delete). */
|
|
45
70
|
fetch(path: string): Promise<SourceItem | null>;
|
|
46
|
-
/** Subscribe to
|
|
47
|
-
watch?(
|
|
71
|
+
/** Subscribe to the change feed (push sources): `ready` once, then debounced `batch` events. */
|
|
72
|
+
watch?(listener: WatchListener): Promise<SourceWatch>;
|
|
48
73
|
/** Persist engine-authored bytes back to the source (writeThrough only). */
|
|
49
74
|
write?(path: string, content: string): Promise<void>;
|
|
50
75
|
/** Remove a member from the source (writeThrough only). */
|
package/dist/src/sync/plugin.js
CHANGED
|
@@ -8,5 +8,17 @@
|
|
|
8
8
|
// reconciliation, commits, convergence, and durable revision/cursor state — so a
|
|
9
9
|
// source only reports membership and transports bytes; it can never write a
|
|
10
10
|
// commit, mint identity, or bypass the convergence check.
|
|
11
|
-
|
|
11
|
+
/** How long a host waits for `ready` before proceeding as if ready (spec/sync §5: unpinned; 30 s). */
|
|
12
|
+
export const WATCH_READY_PATIENCE_MS = 30_000;
|
|
13
|
+
/**
|
|
14
|
+
* Wait for a watch's `ready` with bounded patience: `true` when it resolved in
|
|
15
|
+
* time, `false` on timeout (the host proceeds as if ready, with a warning — an
|
|
16
|
+
* adapter without `ready` still works, with the old window).
|
|
17
|
+
*/
|
|
18
|
+
export function awaitReady(ready, patienceMs = WATCH_READY_PATIENCE_MS) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const timer = setTimeout(() => resolve(false), patienceMs);
|
|
21
|
+
ready.then(() => { clearTimeout(timer); resolve(true); }, () => { clearTimeout(timer); resolve(false); });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
12
24
|
//# sourceMappingURL=plugin.js.map
|
|
@@ -105,7 +105,7 @@ function assignFromMap(blocks, assignment) {
|
|
|
105
105
|
const walk = (list, parentKey) => list.map((b, index) => {
|
|
106
106
|
const key = `${parentKey ?? ""}/${index}`;
|
|
107
107
|
const blockId = assignment.get(key) ?? mintId("b");
|
|
108
|
-
return { blockId, type: b.type, raw: b.raw, trivia: b.trivia, attrs: b.attrs, children: walk(b.children, key) };
|
|
108
|
+
return { blockId, type: b.type, raw: b.raw, trivia: b.trivia, attrs: b.attrs, children: walk(b.children, key), span: b.span };
|
|
109
109
|
});
|
|
110
110
|
return walk(blocks, null);
|
|
111
111
|
}
|
|
@@ -260,11 +260,13 @@ function resolveAdapterEdge(db, repoId, srcDoc, e, docDir) {
|
|
|
260
260
|
provenance: e.provenance,
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
|
-
// Collect RawBlock-shaped objects from the id-assigned tree for adapter edge
|
|
263
|
+
// Collect RawBlock-shaped objects from the id-assigned tree for adapter edge
|
|
264
|
+
// extraction. The parsed span rides along so the adapter can blank a
|
|
265
|
+
// container's children (spec/graph §1 own text).
|
|
264
266
|
function collectRawBlocks(blocks) {
|
|
265
267
|
return blocks.map((b) => ({
|
|
266
268
|
type: b.type,
|
|
267
|
-
span: { start: 0, end: 0 },
|
|
269
|
+
span: b.span ?? { start: 0, end: 0 },
|
|
268
270
|
raw: b.raw,
|
|
269
271
|
text: "",
|
|
270
272
|
attrs: b.attrs,
|
|
@@ -14,6 +14,8 @@ export declare class Watcher {
|
|
|
14
14
|
private sub;
|
|
15
15
|
constructor(store: Store, repoId: string, source: SyncSource, opts?: WatcherOptions);
|
|
16
16
|
start(): Promise<void>;
|
|
17
|
+
/** The live subscription's `ready` (spec/sync §5); throws before `start()`. */
|
|
18
|
+
ready(): Promise<void>;
|
|
17
19
|
private ingest;
|
|
18
20
|
stop(): Promise<void>;
|
|
19
21
|
}
|
package/dist/src/sync/watcher.js
CHANGED
|
@@ -14,11 +14,19 @@ export class Watcher {
|
|
|
14
14
|
async start() {
|
|
15
15
|
if (!this.source.watch)
|
|
16
16
|
throw new Error("source does not support watch()");
|
|
17
|
-
this.sub = await this.source.watch((
|
|
17
|
+
this.sub = await this.source.watch((ev) => {
|
|
18
|
+
if (ev.event !== "batch")
|
|
19
|
+
return; // `ready` is surfaced through ready()
|
|
18
20
|
// A batch arrives from the adapter's stream; reconcile it off the callback.
|
|
19
|
-
void this.ingest(paths).catch((err) => this.opts.onError?.(err));
|
|
21
|
+
void this.ingest(ev.paths).catch((err) => this.opts.onError?.(err));
|
|
20
22
|
});
|
|
21
23
|
}
|
|
24
|
+
/** The live subscription's `ready` (spec/sync §5); throws before `start()`. */
|
|
25
|
+
ready() {
|
|
26
|
+
if (!this.sub)
|
|
27
|
+
throw new Error("watcher not started");
|
|
28
|
+
return this.sub.ready;
|
|
29
|
+
}
|
|
22
30
|
async ingest(paths) {
|
|
23
31
|
if (paths.length === 0)
|
|
24
32
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omgbase/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Open Markdown Graph Base — embedded engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
58
|
-
"@omgbase/oqx": "^0.
|
|
58
|
+
"@omgbase/oqx": "^0.13.0",
|
|
59
59
|
"better-sqlite3": "^13.0.3",
|
|
60
60
|
"mdast-util-from-markdown": "^2.0.2",
|
|
61
61
|
"mdast-util-frontmatter": "^2.0.1",
|