@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
|
@@ -72,26 +72,149 @@ function blocksAtRevision(store, docId, revId) {
|
|
|
72
72
|
walk(rev.root_tree.toString("hex"));
|
|
73
73
|
return out;
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
75
|
+
/** `diff_unified` (spec/surface §3): a unified diff of two revisions' rendered
|
|
76
|
+
* texts — each revision's live raws joined by `\n` — via {@link unifiedDiff}. */
|
|
76
77
|
export function diffUnified(store, docId, fromRev, toRev) {
|
|
77
|
-
const rendered = (rev) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
const rendered = (rev) => [...blocksAtRevision(store, docId, rev).values()].join("\n");
|
|
79
|
+
return unifiedDiff(rendered(fromRev), rendered(toRev));
|
|
80
|
+
}
|
|
81
|
+
// ---- unified diff (spec/surface §3) ------------------------------------------
|
|
82
|
+
// A line-grain unified diff with a deterministic Myers script, so both engines
|
|
83
|
+
// (this reference and crates/omgbase-surface/src/history.rs) produce the same
|
|
84
|
+
// bytes for the same two texts. Kept pure so it can be pinned by unit tests.
|
|
85
|
+
/** How each text becomes lines: `split("\n")` exactly — no trimming, no
|
|
86
|
+
* dropping of a trailing empty element (a raw ending in `\n` yields one) — with
|
|
87
|
+
* a single special case: the empty text has *no* lines (an empty file is zero
|
|
88
|
+
* lines, not one empty line), so a diff from/to nothing is `@@ -0,0 +1,n @@`. */
|
|
89
|
+
export function diffLines(text) {
|
|
90
|
+
return text === "" ? [] : text.split("\n");
|
|
91
|
+
}
|
|
92
|
+
const DIFF_CONTEXT = 3;
|
|
93
|
+
/**
|
|
94
|
+
* Myers' O(ND) shortest edit script (forward, with a per-`d` trace for the
|
|
95
|
+
* backtrack). `V[k]` is the furthest x on diagonal `k = x - y` reachable with
|
|
96
|
+
* `d` edits. The canonical tie rule, identical in both engines: at each step
|
|
97
|
+
* take the diagonal from `k+1` (moving down — an insertion of `b[y]`) when
|
|
98
|
+
* `k == -d || (k != d && V[k-1] < V[k+1])`, else from `k-1` (moving right —
|
|
99
|
+
* a deletion of `a[x]`). On a tie (`V[k-1] == V[k+1]`) that is the deletion.
|
|
100
|
+
*/
|
|
101
|
+
export function myersScript(a, b) {
|
|
102
|
+
const n = a.length;
|
|
103
|
+
const m = b.length;
|
|
104
|
+
const max = n + m;
|
|
105
|
+
// V is indexed by k ∈ [-max-1, max+1]; `off` maps it onto a plain array.
|
|
106
|
+
const off = max + 1;
|
|
107
|
+
const v = new Array(2 * max + 3).fill(0);
|
|
108
|
+
const trace = [];
|
|
109
|
+
let found = false;
|
|
110
|
+
for (let d = 0; d <= max && !found; d++) {
|
|
111
|
+
trace.push(v.slice());
|
|
112
|
+
for (let k = -d; k <= d; k += 2) {
|
|
113
|
+
let x;
|
|
114
|
+
if (k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]))
|
|
115
|
+
x = v[off + k + 1];
|
|
116
|
+
else
|
|
117
|
+
x = v[off + k - 1] + 1;
|
|
118
|
+
let y = x - k;
|
|
119
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
120
|
+
x++;
|
|
121
|
+
y++;
|
|
122
|
+
}
|
|
123
|
+
v[off + k] = x;
|
|
124
|
+
if (x >= n && y >= m) {
|
|
125
|
+
found = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Backtrack from (n, m) through the trace, emitting ops newest-first.
|
|
131
|
+
const ops = [];
|
|
132
|
+
let x = n;
|
|
133
|
+
let y = m;
|
|
134
|
+
for (let d = trace.length - 1; d >= 0; d--) {
|
|
135
|
+
const vd = trace[d];
|
|
136
|
+
const k = x - y;
|
|
137
|
+
const prevK = k === -d || (k !== d && vd[off + k - 1] < vd[off + k + 1]) ? k + 1 : k - 1;
|
|
138
|
+
const prevX = vd[off + prevK];
|
|
139
|
+
const prevY = prevX - prevK;
|
|
140
|
+
while (x > prevX && y > prevY) {
|
|
141
|
+
x--;
|
|
142
|
+
y--;
|
|
143
|
+
ops.push({ kind: "keep", line: a[x] });
|
|
144
|
+
}
|
|
145
|
+
if (d > 0) {
|
|
146
|
+
if (x === prevX)
|
|
147
|
+
ops.push({ kind: "insert", line: b[prevY] });
|
|
148
|
+
else
|
|
149
|
+
ops.push({ kind: "delete", line: a[prevX] });
|
|
150
|
+
}
|
|
151
|
+
x = prevX;
|
|
152
|
+
y = prevY;
|
|
153
|
+
}
|
|
154
|
+
ops.reverse();
|
|
155
|
+
return ops;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The unified diff of two texts (spec/surface §3): hunks of `DIFF_CONTEXT` (3)
|
|
159
|
+
* lines of context; a change group extends to include the next change when
|
|
160
|
+
* fewer than `2 * DIFF_CONTEXT + 1` unchanged lines separate them (the two
|
|
161
|
+
* contexts touch or overlap). Each hunk is `@@ -a,b +c,d @@` (1-based start
|
|
162
|
+
* and length; a length of 1 is written as the start alone; a length of 0 as
|
|
163
|
+
* `a,0` with `a` the line before the insertion point, `0` at the very top)
|
|
164
|
+
* followed by its lines prefixed `-`, `+` or a space with nothing after the
|
|
165
|
+
* sign; hunks joined by `\n`; no file header; identical texts → `""`.
|
|
166
|
+
*/
|
|
167
|
+
export function unifiedDiff(oldText, newText) {
|
|
168
|
+
const ops = myersScript(diffLines(oldText), diffLines(newText));
|
|
169
|
+
// Old/new line counts consumed before each op (0-based positions).
|
|
170
|
+
const oldPos = new Array(ops.length + 1);
|
|
171
|
+
const newPos = new Array(ops.length + 1);
|
|
172
|
+
let o = 0;
|
|
173
|
+
let nn = 0;
|
|
174
|
+
for (let i = 0; i < ops.length; i++) {
|
|
175
|
+
oldPos[i] = o;
|
|
176
|
+
newPos[i] = nn;
|
|
177
|
+
if (ops[i].kind !== "insert")
|
|
178
|
+
o++;
|
|
179
|
+
if (ops[i].kind !== "delete")
|
|
180
|
+
nn++;
|
|
181
|
+
}
|
|
182
|
+
oldPos[ops.length] = o;
|
|
183
|
+
newPos[ops.length] = nn;
|
|
184
|
+
const changes = [];
|
|
185
|
+
for (let i = 0; i < ops.length; i++)
|
|
186
|
+
if (ops[i].kind !== "keep")
|
|
187
|
+
changes.push(i);
|
|
188
|
+
if (changes.length === 0)
|
|
189
|
+
return "";
|
|
190
|
+
const hunks = [];
|
|
191
|
+
let g = 0;
|
|
192
|
+
while (g < changes.length) {
|
|
193
|
+
const first = changes[g];
|
|
194
|
+
let last = first;
|
|
195
|
+
// Merge rule: the next change joins this hunk iff the unchanged lines
|
|
196
|
+
// between them number at most 2 * DIFF_CONTEXT.
|
|
197
|
+
while (g + 1 < changes.length && changes[g + 1] - last - 1 <= 2 * DIFF_CONTEXT) {
|
|
198
|
+
g++;
|
|
199
|
+
last = changes[g];
|
|
200
|
+
}
|
|
201
|
+
g++;
|
|
202
|
+
const start = Math.max(0, first - DIFF_CONTEXT);
|
|
203
|
+
const end = Math.min(ops.length - 1, last + DIFF_CONTEXT);
|
|
204
|
+
const oldLen = oldPos[end + 1] - oldPos[start];
|
|
205
|
+
const newLen = newPos[end + 1] - newPos[start];
|
|
206
|
+
const range = (pos, len) => {
|
|
207
|
+
const startLine = len === 0 ? pos : pos + 1;
|
|
208
|
+
return len === 1 ? `${startLine}` : `${startLine},${len}`;
|
|
209
|
+
};
|
|
210
|
+
const lines = [`@@ -${range(oldPos[start], oldLen)} +${range(newPos[start], newLen)} @@`];
|
|
211
|
+
for (let i = start; i <= end; i++) {
|
|
212
|
+
const op = ops[i];
|
|
213
|
+
lines.push((op.kind === "keep" ? " " : op.kind === "delete" ? "-" : "+") + op.line);
|
|
214
|
+
}
|
|
215
|
+
hunks.push(lines.join("\n"));
|
|
93
216
|
}
|
|
94
|
-
return
|
|
217
|
+
return hunks.join("\n");
|
|
95
218
|
}
|
|
96
219
|
/** changes_since: commit digests after a cursor (repo commit seq) — the change
|
|
97
220
|
* feed (06 §3). `seq` is a dense per-REPO total order, so a cursor is only
|
package/dist/src/index.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export { ingestDirectory, type IngestDirResult } from "./sync/attach.js";
|
|
|
19
19
|
export { ensureRepo } from "./core/attach.js";
|
|
20
20
|
export { walkMarkdown, walkMarkdownAsync, nodeFs, type SyncFs, type FileStat } from "./sync/fs-util.js";
|
|
21
21
|
export { Watcher, type WatcherOptions } from "./sync/watcher.js";
|
|
22
|
-
export type { SyncSource, SourceCapabilities, SourceIdentity, SourceEntry, SourceItem, SourceWatch, WatchListener, } from "./sync/plugin.js";
|
|
22
|
+
export type { SyncSource, SourceCapabilities, SourceIdentity, SourceEntry, SourceItem, SourceWatch, WatchEvent, WatchListener, } from "./sync/plugin.js";
|
|
23
|
+
export { awaitReady, WATCH_READY_PATIENCE_MS } from "./sync/plugin.js";
|
|
23
24
|
export { createExternalSource, type ExternalSourceSpec } from "./sync/external-source.js";
|
|
24
25
|
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, type AdapterRow, type SourceRow, } from "./sync/sources.js";
|
|
25
26
|
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
@@ -43,12 +44,13 @@ export { embeddingSettings, type EmbeddingSettings } from "./search/provider.js"
|
|
|
43
44
|
export { createExternalProvider, type ExternalProvider } from "./search/external.js";
|
|
44
45
|
export { hybridSearch, type HybridHit, type HybridInput } from "./search/rrf.js";
|
|
45
46
|
export { vectorSearch, docVectorSearch, type VectorHit, type DocVectorHit } from "./search/vector.js";
|
|
46
|
-
export { historyNode, diffUnified, changesSince, type NodeChange, type CommitDigest } from "./graph/history.js";
|
|
47
|
+
export { historyNode, diffUnified, unifiedDiff, changesSince, type NodeChange, type CommitDigest } from "./graph/history.js";
|
|
47
48
|
export { docLinks, type LinksResult, type LinkEdge, type LinksOptions } from "./graph/links.js";
|
|
48
49
|
export { FilterInvalid } from "./search/cel/parser.js";
|
|
49
50
|
export { EngineError, type ErrorCode } from "./mcp/errors.js";
|
|
50
51
|
export { MutationError } from "./mutate/tree.js";
|
|
51
52
|
export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter, type IdMinter, type IdPrefix } from "./core/ids.js";
|
|
53
|
+
export { pinClock, withClock } from "./core/clock.js";
|
|
52
54
|
export { sha256 } from "./core/hash.js";
|
|
53
55
|
export { ingestFile, type IngestResult } from "./core/ingest.js";
|
|
54
56
|
export { apply, type ApplyRequest, type ApplyResult, type Op } from "./mutate/apply.js";
|
package/dist/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { ingestDirectory } from "./sync/attach.js";
|
|
|
20
20
|
export { ensureRepo } from "./core/attach.js";
|
|
21
21
|
export { walkMarkdown, walkMarkdownAsync, nodeFs } from "./sync/fs-util.js";
|
|
22
22
|
export { Watcher } from "./sync/watcher.js";
|
|
23
|
+
export { awaitReady, WATCH_READY_PATIENCE_MS } from "./sync/plugin.js";
|
|
23
24
|
export { createExternalSource } from "./sync/external-source.js";
|
|
24
25
|
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
|
|
25
26
|
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
@@ -48,12 +49,14 @@ export { embeddingSettings } from "./search/provider.js";
|
|
|
48
49
|
export { createExternalProvider } from "./search/external.js";
|
|
49
50
|
export { hybridSearch } from "./search/rrf.js";
|
|
50
51
|
export { vectorSearch, docVectorSearch } from "./search/vector.js";
|
|
51
|
-
export { historyNode, diffUnified, changesSince } from "./graph/history.js";
|
|
52
|
+
export { historyNode, diffUnified, unifiedDiff, changesSince } from "./graph/history.js";
|
|
52
53
|
export { docLinks } from "./graph/links.js";
|
|
53
54
|
export { FilterInvalid } from "./search/cel/parser.js";
|
|
54
55
|
export { EngineError } from "./mcp/errors.js";
|
|
55
56
|
export { MutationError } from "./mutate/tree.js";
|
|
56
57
|
export { isValidId, prefixOf, mintId, setIdMinter, withIdMinter, sequentialMinter } from "./core/ids.js";
|
|
58
|
+
// The clock seam (spec/surface §7.1): `omg mcp` pins the process clock under OMGBASE_SPEC_CLOCK.
|
|
59
|
+
export { pinClock, withClock } from "./core/clock.js";
|
|
57
60
|
export { sha256 } from "./core/hash.js";
|
|
58
61
|
export { ingestFile } from "./core/ingest.js";
|
|
59
62
|
// Mutation surface consumed by CLI write commands (11 §5.6).
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -45,14 +45,23 @@ const toSchema = z.object({
|
|
|
45
45
|
]),
|
|
46
46
|
at: atSchema,
|
|
47
47
|
});
|
|
48
|
+
// blocks_insert / blocks_move carry the op-level expectation of spec/mutate §1.2:
|
|
49
|
+
// only `parent_children_hash` (the destination parent's live direct child ids
|
|
50
|
+
// joined by "," and sha256-hexed) has a meaning there — there is no target block
|
|
51
|
+
// whose content_hash could be checked — so the schema names that one key alone
|
|
52
|
+
// (an accidental `content_hash` is dropped, not an error; the kernel would
|
|
53
|
+
// ignore it anyway).
|
|
54
|
+
const parentExpectSchema = z.object({
|
|
55
|
+
parent_children_hash: z.string().optional(),
|
|
56
|
+
});
|
|
48
57
|
const expectSchema = z.object({
|
|
49
58
|
content_hash: z.string().optional(),
|
|
50
59
|
parent_children_hash: z.string().optional(),
|
|
51
60
|
});
|
|
52
61
|
const opSchema = z.discriminatedUnion("op", [
|
|
53
|
-
z.object({ op: z.literal("insert"), doc: z.string().optional(), to: toSchema, markdown: z.string() }),
|
|
62
|
+
z.object({ op: z.literal("insert"), doc: z.string().optional(), to: toSchema, markdown: z.string(), expect: expectSchema.optional() }),
|
|
54
63
|
z.object({ op: z.literal("update"), block: z.string(), markdown: z.string().optional(), attrs: z.record(z.string(), z.unknown()).optional(), expect: expectSchema.optional() }),
|
|
55
|
-
z.object({ op: z.literal("move"), blocks: z.array(z.string()), to: toSchema }),
|
|
64
|
+
z.object({ op: z.literal("move"), blocks: z.array(z.string()), to: toSchema, expect: expectSchema.optional() }),
|
|
56
65
|
z.object({ op: z.literal("remove"), blocks: z.array(z.string()), expect: z.record(z.string(), expectSchema).optional() }),
|
|
57
66
|
z.object({ op: z.literal("split"), block: z.string(), at: z.array(z.number().int()), expect: expectSchema.optional() }),
|
|
58
67
|
z.object({ op: z.literal("merge"), blocks: z.array(z.string()), separator: z.string().optional(), expect: z.record(z.string(), expectSchema).optional() }),
|
|
@@ -279,7 +288,7 @@ export function buildServer(ctx) {
|
|
|
279
288
|
}
|
|
280
289
|
});
|
|
281
290
|
server.registerTool("docs_get_many", {
|
|
282
|
-
description: "Batch whole-document read — the hydrate half of query→hydrate. The plural of docs_read: pass `docs`, a list of refs (each a doc id OR a path, same id-or-path symmetry docs_read accepts in its `doc` field), and get back one full read per ref. Returns `{ items, errors, truncated }`: each found doc is a full docs_read projection (`content` = complete file bytes verbatim, `properties` grouped by source, plus `path`/`docId`/`rev`, and — with include_ids:true — the document's ordered block `ids`, a `hashes` {block id → content hash} map for raw-`apply` CAS pinning, and a `parents` {block id → parent block id | null} map so top-level vs nested blocks are distinguishable without an outline read); a ref that resolves to no live document lands in `errors` as {ref, error:\"doc_not_found\"} WITHOUT failing the call, so one bad ref never sinks the batch. Duplicate refs collapse first-seen (a repeated ref yields a single item). Capped at " + MANY_DOCS_CAP + " refs per call; excess refs are dropped and `truncated` is set. Pass budget_tokens to cap total hydrated size — the batch stops early and flags `truncated` when the next doc would exceed it. Use this after query/text_search/resolve to pull N whole docs in ONE round-trip instead of N serial docs_read calls; for a single doc use docs_read, and for lean structure-only orientation use docs_outline.",
|
|
291
|
+
description: "Batch whole-document read — the hydrate half of query→hydrate. The plural of docs_read: pass `docs`, a list of refs (each a doc id OR a path, same id-or-path symmetry docs_read accepts in its `doc` field), and get back one full read per ref. Returns `{ items, errors, truncated }`: each found doc is a full docs_read projection (`content` = complete file bytes verbatim, `properties` grouped by source, plus `path`/`docId`/`rev`, and — with include_ids:true — the document's ordered block `ids`, a `hashes` {block id → content hash} map for raw-`apply` CAS pinning, and a `parents` {block id → parent block id | null} map so top-level vs nested blocks are distinguishable without an outline read); a ref that resolves to no live document lands in `errors` as {ref, error:\"doc_not_found\"} WITHOUT failing the call, so one bad ref never sinks the batch. Duplicate refs collapse first-seen (a repeated ref yields a single item). Capped at " + MANY_DOCS_CAP + " refs per call; excess refs are dropped and `truncated` is set. Pass budget_tokens to cap total hydrated size — the batch stops early and flags `truncated` when the next doc would exceed it; the FIRST doc always ships even when it alone exceeds the budget (the same at-least-one-row contract as docs_list/docs_tree, so a budgeted caller always makes progress). Use this after query/text_search/resolve to pull N whole docs in ONE round-trip instead of N serial docs_read calls; for a single doc use docs_read, and for lean structure-only orientation use docs_outline.",
|
|
283
292
|
inputSchema: {
|
|
284
293
|
docs: z.array(z.string()),
|
|
285
294
|
include_ids: z.boolean().optional(),
|
|
@@ -322,7 +331,7 @@ export function buildServer(ctx) {
|
|
|
322
331
|
}
|
|
323
332
|
});
|
|
324
333
|
server.registerTool("nodes_get_many", {
|
|
325
|
-
description: "Fetch up to 100 blocks by id (in request order) with budget truncation. Block ids are globally unique, so `ids` may span ANY number of documents — each id is resolved to its owning doc server-side; `doc`/`path` is an optional SCOPE (ids from other docs then count as unresolved), not a requirement. Result carries `nodes`, `truncated` (cap or `budget_tokens` hit), and `unresolved` — the requested ids that name no live block (never silently dropped).",
|
|
334
|
+
description: "Fetch up to 100 blocks by id (in request order) with budget truncation. Block ids are globally unique, so `ids` may span ANY number of documents — each id is resolved to its owning doc server-side; `doc`/`path` is an optional SCOPE (ids from other docs then count as unresolved), not a requirement. Result carries `nodes`, `truncated` (cap or `budget_tokens` hit — the first resolved node always ships even when it alone exceeds the budget, so a budgeted caller always makes progress), and `unresolved` — the requested ids that name no live block (never silently dropped).",
|
|
326
335
|
inputSchema: {
|
|
327
336
|
doc: z.string().optional(),
|
|
328
337
|
path: z.string().optional(),
|
|
@@ -540,16 +549,18 @@ export function buildServer(ctx) {
|
|
|
540
549
|
// local: the resolution that used to live in the CLI now lives next to the
|
|
541
550
|
// store, so a remote client needs no local database.
|
|
542
551
|
server.registerTool("blocks_insert", {
|
|
543
|
-
description: "Insert new block(s) parsed from `markdown` under a parent, at a position. `to` names the parent: a BLOCK ref (id or locator) nests the new blocks under that block, or a DOCUMENT ref (doc id or path) places them at the document's top level — so appending a new section to a document is `{ to: \"<path>\", markdown: \"## New\\n\\n…\" }` (the positional generalization of docs_append). `at` places among the parent's children: \"end\" (default) / \"start\" / {before|after: <block ref>}. Expands to one insert op through the kernel writer.",
|
|
544
|
-
inputSchema: { to: z.string(), markdown: z.string(), at: atSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
552
|
+
description: "Insert new block(s) parsed from `markdown` under a parent, at a position. `to` names the parent: a BLOCK ref (id or locator) nests the new blocks under that block, or a DOCUMENT ref (doc id or path) places them at the document's top level — so appending a new section to a document is `{ to: \"<path>\", markdown: \"## New\\n\\n…\" }` (the positional generalization of docs_append). `at` places among the parent's children: \"end\" (default) / \"start\" / {before|after: <block ref>}. Optional `expect.parent_children_hash` is the destination-parent CAS: the parent's CURRENT direct child ids (the document's top-level ids for a document `to`) joined by \",\" and sha256-hexed — compute it from docs_read include_ids (`ids` filtered by `parents`) — and the insert fails `stale_expectation` (with `data.current.parent_children_hash`) if the siblings changed under you; `content_hash` has no meaning here (there is no target block) and is not accepted. Expands to one insert op through the kernel writer.",
|
|
553
|
+
inputSchema: { to: z.string(), markdown: z.string(), at: atSchema.optional(), expect: parentExpectSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
545
554
|
}, async (args) => {
|
|
546
555
|
try {
|
|
547
556
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
548
557
|
const root = requireRoot(rootPath);
|
|
549
558
|
const { parent, docId } = resolveParentRef(repoId, args.to);
|
|
550
559
|
const to = { parent, at: resolveAt(repoId, args.at) };
|
|
551
|
-
// A document parent carries no block to infer the doc from; pin it.
|
|
552
|
-
|
|
560
|
+
// A document parent carries no block to infer the doc from; pin it. The
|
|
561
|
+
// caller's expect rides through as the op-level expectation (spec/mutate
|
|
562
|
+
// §1.2: checked against the destination parent before the op mutates).
|
|
563
|
+
const ops = [{ op: "insert", ...(typeof parent === "object" ? { doc: docId } : {}), to, markdown: args.markdown, ...(args.expect ? { expect: args.expect } : {}) }];
|
|
553
564
|
return applyOps(repoId, root, ops, "blocks_insert", args.dry_run);
|
|
554
565
|
}
|
|
555
566
|
catch (e) {
|
|
@@ -583,8 +594,8 @@ export function buildServer(ctx) {
|
|
|
583
594
|
}
|
|
584
595
|
});
|
|
585
596
|
server.registerTool("blocks_move", {
|
|
586
|
-
description: "Move block(s) under a new parent at a position. `blocks` are block refs; `to` (the parent) is a block ref, or the blocks' OWN document (id or path) to move them to its top level — moving to ANOTHER document's root is not expressible (target_missing): anchor on a block in that document with `at` {before|after} instead. `at` is \"end\"/\"start\"/{before|after: <ref>}. One move op through the kernel writer.",
|
|
587
|
-
inputSchema: { blocks: z.array(z.string()), to: z.string(), at: atSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
597
|
+
description: "Move block(s) under a new parent at a position. `blocks` are block refs; `to` (the parent) is a block ref, or the blocks' OWN document (id or path) to move them to its top level — moving to ANOTHER document's root is not expressible (target_missing): anchor on a block in that document with `at` {before|after} instead. `at` is \"end\"/\"start\"/{before|after: <ref>}. Optional `expect.parent_children_hash` is the destination-parent CAS (as blocks_insert: the destination parent's CURRENT direct child ids joined by \",\", sha256 hex), checked once for the whole run before anything moves — `stale_expectation` with `data.current.parent_children_hash` if the destination's children changed; `content_hash` is meaningless here and not accepted. One move op through the kernel writer.",
|
|
598
|
+
inputSchema: { blocks: z.array(z.string()), to: z.string(), at: atSchema.optional(), expect: parentExpectSchema.optional(), dry_run: z.boolean().optional(), ...REPO_ARG },
|
|
588
599
|
}, async (args) => {
|
|
589
600
|
try {
|
|
590
601
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
@@ -597,7 +608,7 @@ export function buildServer(ctx) {
|
|
|
597
608
|
throw new EngineError("target_missing", `blocks_move cannot target another document's root (${args.to}); anchor on a block in that document with at.before/at.after`, { data: { to: args.to } });
|
|
598
609
|
}
|
|
599
610
|
const to = { parent, at: resolveAt(repoId, args.at) };
|
|
600
|
-
const ops = [{ op: "move", blocks, to }];
|
|
611
|
+
const ops = [{ op: "move", blocks, to, ...(args.expect ? { expect: args.expect } : {}) }];
|
|
601
612
|
return applyOps(repoId, root, ops, "blocks_move", args.dry_run);
|
|
602
613
|
}
|
|
603
614
|
catch (e) {
|
|
@@ -939,7 +950,7 @@ export function buildServer(ctx) {
|
|
|
939
950
|
}
|
|
940
951
|
});
|
|
941
952
|
server.registerTool("diff_unified", {
|
|
942
|
-
description: "
|
|
953
|
+
description: "Unified diff between two revisions of a document (the `omg diff` rendering): returns `{ doc, path, from, to, diff }` where `diff` is a unified diff of the two rendered texts (Myers shortest edit script; `@@ -a,b +c,d @@` hunks with 3 lines of context; lines prefixed `-`, `+` or a space; no file header; identical texts → \"\"). `from_rev`/`to_rev` default to the previous and current revisions ('what did the last commit change here'). Contrast `diff`, which returns block-grain added/removed/changed entries.",
|
|
943
954
|
inputSchema: { doc: z.string(), from_rev: z.string().optional(), to_rev: z.string().optional(), ...REPO_ARG },
|
|
944
955
|
}, async (args) => {
|
|
945
956
|
try {
|
|
@@ -6,6 +6,7 @@ export type Op = {
|
|
|
6
6
|
doc?: string;
|
|
7
7
|
to: To;
|
|
8
8
|
markdown: string;
|
|
9
|
+
expect?: Expect;
|
|
9
10
|
} | {
|
|
10
11
|
op: "update";
|
|
11
12
|
block: string;
|
|
@@ -18,6 +19,7 @@ export type Op = {
|
|
|
18
19
|
op: "move";
|
|
19
20
|
blocks: string[];
|
|
20
21
|
to: To;
|
|
22
|
+
expect?: Expect;
|
|
21
23
|
} | {
|
|
22
24
|
op: "remove";
|
|
23
25
|
blocks: string[];
|
package/dist/src/mutate/apply.js
CHANGED
|
@@ -6,7 +6,7 @@ import { makeReconcilingResolver } from "../sync/reconciling-ingest.js";
|
|
|
6
6
|
import { makeKnownIdResolver } from "./known-ids.js";
|
|
7
7
|
import { loadMutDoc } from "./load.js";
|
|
8
8
|
import { renderDoc, markContainerDirty, MutationError } from "./tree.js";
|
|
9
|
-
import { opInsert, opUpdate, opMove, opRemove, opSplit, opMerge, healTopLevelSeams } from "./ops.js";
|
|
9
|
+
import { opInsert, opUpdate, opMove, opRemove, opSplit, opMerge, healTopLevelSeams, checkParentChildrenHash } from "./ops.js";
|
|
10
10
|
import { withWriterLock } from "../sync/writer-lock.js";
|
|
11
11
|
import { resolveDocStore } from "./doc-store.js";
|
|
12
12
|
// Resolve "$n.ids[i]" placeholders in a string against prior op results.
|
|
@@ -92,7 +92,7 @@ export function apply(store, req) {
|
|
|
92
92
|
? resolveDocRef(store, req.repoId, rawOp.doc)
|
|
93
93
|
: parentDoc(store, req.repoId, to, loaded);
|
|
94
94
|
const d = ensureDoc(docId);
|
|
95
|
-
results.push(opInsert(d, to, rawOp.markdown));
|
|
95
|
+
results.push(opInsert(d, to, rawOp.markdown, rawOp.expect, i));
|
|
96
96
|
break;
|
|
97
97
|
}
|
|
98
98
|
case "update": {
|
|
@@ -114,11 +114,11 @@ export function apply(store, req) {
|
|
|
114
114
|
const topLevelSameDoc = typeof to.parent === "object" && "doc" in to.parent && typeof to.at === "string";
|
|
115
115
|
const dstDocId = topLevelSameDoc ? srcDocId : parentDoc(store, req.repoId, to, loaded);
|
|
116
116
|
if (dstDocId === srcDocId) {
|
|
117
|
-
results.push(opMove(ensureDoc(srcDocId), blocks, to, i));
|
|
117
|
+
results.push(opMove(ensureDoc(srcDocId), blocks, to, i, rawOp.expect));
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
120
120
|
// Cross-document move (04 §1): extract from source, insert into target.
|
|
121
|
-
results.push(crossDocMove(ensureDoc(srcDocId), ensureDoc(dstDocId), blocks, to, i));
|
|
121
|
+
results.push(crossDocMove(ensureDoc(srcDocId), ensureDoc(dstDocId), blocks, to, i, rawOp.expect));
|
|
122
122
|
}
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
@@ -239,7 +239,7 @@ function throwMissing(id) {
|
|
|
239
239
|
throw new MutationError("parent_missing", `parent ${id} not found`);
|
|
240
240
|
}
|
|
241
241
|
// Cross-document move: extract the contiguous run from src, insert into dst.
|
|
242
|
-
function crossDocMove(src, dst, blockIds, to, opIndex) {
|
|
242
|
+
function crossDocMove(src, dst, blockIds, to, opIndex, expect) {
|
|
243
243
|
// Extract from src (must be a contiguous top-level or same-parent run).
|
|
244
244
|
const moving = [];
|
|
245
245
|
for (const id of blockIds) {
|
|
@@ -248,6 +248,9 @@ function crossDocMove(src, dst, blockIds, to, opIndex) {
|
|
|
248
248
|
throw new MutationError("block_missing", `block ${id} not found in source doc`, { op_index: opIndex });
|
|
249
249
|
moving.push(found.block);
|
|
250
250
|
}
|
|
251
|
+
// §1.2: the destination parent's order CAS (in `dst`), before either tree moves.
|
|
252
|
+
if (expect?.parent_children_hash !== undefined)
|
|
253
|
+
checkParentChildrenHash(resolveDstTarget(dst, to).siblings, expect, opIndex);
|
|
251
254
|
// remove from their source lists (marking each emptied owner dirty)
|
|
252
255
|
for (const id of blockIds) {
|
|
253
256
|
const f = locateMut(src, id);
|
|
@@ -32,7 +32,7 @@ function assignFromMut(blocks, byKey) {
|
|
|
32
32
|
const walk = (list, parentKey) => list.map((b, index) => {
|
|
33
33
|
const key = `${parentKey ?? ""}/${index}`;
|
|
34
34
|
const blockId = byKey.get(key) ?? mintId("b");
|
|
35
|
-
return { blockId, type: b.type, raw: b.raw, trivia: b.trivia, attrs: b.attrs, children: walk(b.children, key) };
|
|
35
|
+
return { blockId, type: b.type, raw: b.raw, trivia: b.trivia, attrs: b.attrs, children: walk(b.children, key), span: b.span };
|
|
36
36
|
});
|
|
37
37
|
return walk(blocks, null);
|
|
38
38
|
}
|
|
@@ -41,7 +41,7 @@ function assignFromMut(blocks, byKey) {
|
|
|
41
41
|
function collectRawBlocks(blocks) {
|
|
42
42
|
return blocks.map((b) => ({
|
|
43
43
|
type: b.type,
|
|
44
|
-
span: { start: 0, end: 0 },
|
|
44
|
+
span: b.span ?? { start: 0, end: 0 },
|
|
45
45
|
raw: b.raw,
|
|
46
46
|
text: "",
|
|
47
47
|
attrs: b.attrs,
|
package/dist/src/mutate/ops.d.ts
CHANGED
|
@@ -17,13 +17,14 @@ export interface Expect {
|
|
|
17
17
|
content_hash?: string;
|
|
18
18
|
parent_children_hash?: string;
|
|
19
19
|
}
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function checkParentChildrenHash(siblings: MutBlock[], expect: Expect | undefined, opIndex: number): void;
|
|
21
|
+
export declare function opInsert(doc: MutDoc, to: To, markdown: string, expect?: Expect, opIndex?: number): {
|
|
21
22
|
ids: string[];
|
|
22
23
|
};
|
|
23
24
|
export declare function opUpdate(doc: MutDoc, blockId: string, opIndex: number, markdown?: string, attrs?: Record<string, unknown>, expect?: Expect, trivia?: string, childIds?: Record<string, string>): {
|
|
24
25
|
ids: string[];
|
|
25
26
|
};
|
|
26
|
-
export declare function opMove(doc: MutDoc, blockIds: string[], to: To, opIndex: number): {
|
|
27
|
+
export declare function opMove(doc: MutDoc, blockIds: string[], to: To, opIndex: number, expect?: Expect): {
|
|
27
28
|
ids: string[];
|
|
28
29
|
};
|
|
29
30
|
export declare function healTopLevelSeams(children: MutBlock[], format: string): void;
|
|
@@ -38,4 +39,3 @@ export declare function opMerge(doc: MutDoc, blockIds: string[], opIndex: number
|
|
|
38
39
|
ids: string[];
|
|
39
40
|
mergedInto: string[];
|
|
40
41
|
};
|
|
41
|
-
export declare function checkParentChildrenHash(doc: MutDoc, parentId: string | null, expected: string, opIndex: number): void;
|
package/dist/src/mutate/ops.js
CHANGED
|
@@ -113,6 +113,20 @@ function checkContentHash(block, expect, opIndex) {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
// §1.2 `check_parent_children_hash`: when the expectation carries a
|
|
117
|
+
// `parent_children_hash`, the LIVE direct children of the sibling list it names
|
|
118
|
+
// (the document top level for `doc.children`) must hash to it. Which list is the
|
|
119
|
+
// op's business: update/split/remove/merge pass the block's current sibling list,
|
|
120
|
+
// insert/move the destination's (resolved as the op resolves `to`). Always after
|
|
121
|
+
// the content CAS and before the tree is touched, so a stale order never lands.
|
|
122
|
+
export function checkParentChildrenHash(siblings, expect, opIndex) {
|
|
123
|
+
if (expect?.parent_children_hash === undefined)
|
|
124
|
+
return;
|
|
125
|
+
const current = parentChildrenHash(siblings);
|
|
126
|
+
if (current !== expect.parent_children_hash) {
|
|
127
|
+
throw new MutationError("stale_expectation", "parent_children_hash mismatch", { op_index: opIndex, current: { parent_children_hash: current }, retriable: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
116
130
|
// Assign known ids onto a freshly-parsed subtree by positional key (parentKey +
|
|
117
131
|
// '/' + index), the same convention flatten()/known-ids use. Keys absent from
|
|
118
132
|
// the map leave the block's minted id in place (a genuinely new child).
|
|
@@ -140,8 +154,11 @@ function markSubtreeClean(b) {
|
|
|
140
154
|
markSubtreeClean(c);
|
|
141
155
|
}
|
|
142
156
|
// ---- the six ops ------------------------------------------------------------
|
|
143
|
-
export function opInsert(doc, to, markdown) {
|
|
157
|
+
export function opInsert(doc, to, markdown, expect, opIndex = 0) {
|
|
144
158
|
const { siblings, index } = resolveTarget(doc, to);
|
|
159
|
+
// §1.2: the destination parent's order CAS — `siblings` IS the resolved parent
|
|
160
|
+
// list (a section scope or a `{ doc: true }` anchor both name the top level).
|
|
161
|
+
checkParentChildrenHash(siblings, expect, opIndex);
|
|
145
162
|
let blocks = parseContentToBlocks(markdown, doc.format);
|
|
146
163
|
// Inserting into a list: the content must land as list ITEMS, not a nested
|
|
147
164
|
// list. Markdown `- x` parses to a `list` wrapping a `list_item`; unwrap it to
|
|
@@ -204,6 +221,8 @@ export function opUpdate(doc, blockId, opIndex, markdown, attrs, expect, trivia,
|
|
|
204
221
|
// juggling post-op content hashes across the changeset.
|
|
205
222
|
if (markdown !== undefined || expect?.content_hash !== undefined)
|
|
206
223
|
checkContentHash(found.block, expect, opIndex);
|
|
224
|
+
// §1.2: the block's CURRENT parent, after the content CAS.
|
|
225
|
+
checkParentChildrenHash(found.siblings, expect, opIndex);
|
|
207
226
|
// Blocks minted from MULTI-block content: the target takes the first parsed
|
|
208
227
|
// block (keeping its id); the rest become fresh siblings right after it.
|
|
209
228
|
const extra = [];
|
|
@@ -309,7 +328,7 @@ export function opUpdate(doc, blockId, opIndex, markdown, attrs, expect, trivia,
|
|
|
309
328
|
}
|
|
310
329
|
return { ids: [blockId, ...extra.map((b) => b.id)] };
|
|
311
330
|
}
|
|
312
|
-
export function opMove(doc, blockIds, to, opIndex) {
|
|
331
|
+
export function opMove(doc, blockIds, to, opIndex, expect) {
|
|
313
332
|
if (blockIds.length === 0)
|
|
314
333
|
throw new MutationError("not_contiguous", "move requires ≥1 block");
|
|
315
334
|
// Locate all; assert contiguous siblings.
|
|
@@ -333,6 +352,12 @@ export function opMove(doc, blockIds, to, opIndex) {
|
|
|
333
352
|
if (typeof to.parent === "string" && movedIds.has(to.parent)) {
|
|
334
353
|
throw new MutationError("cycle_move", "target is inside the moved subtree", { op_index: opIndex });
|
|
335
354
|
}
|
|
355
|
+
// §1.2: the DESTINATION parent's order CAS, resolved on the pre-removal tree
|
|
356
|
+
// (before the op mutates anything) and checked once for the whole run. When
|
|
357
|
+
// source and destination are one list this is the "same siblings reordered
|
|
358
|
+
// under me" guard: the hash covers the moved blocks' current positions too.
|
|
359
|
+
if (expect?.parent_children_hash !== undefined)
|
|
360
|
+
checkParentChildrenHash(resolveTarget(doc, to).siblings, expect, opIndex);
|
|
336
361
|
// Extract (preserve order) then re-insert at the target.
|
|
337
362
|
const moving = located.map((l) => l.block);
|
|
338
363
|
for (const m of moving) {
|
|
@@ -395,8 +420,11 @@ export function opRemove(doc, blockIds, opIndex, expectPer) {
|
|
|
395
420
|
hint: "the id is not a live block of the targeted document — it may have been removed by an earlier op in this changeset (removing a block removes its whole subtree), or never existed",
|
|
396
421
|
});
|
|
397
422
|
}
|
|
398
|
-
|
|
399
|
-
|
|
423
|
+
const e = expectPer?.[id];
|
|
424
|
+
if (e) {
|
|
425
|
+
checkContentHash(found.block, e, opIndex);
|
|
426
|
+
checkParentChildrenHash(found.siblings, e, opIndex); // §1.2: its current parent
|
|
427
|
+
}
|
|
400
428
|
if (!hasAncestorIn(doc, found.siblings, set))
|
|
401
429
|
tops.push(id);
|
|
402
430
|
}
|
|
@@ -454,6 +482,7 @@ export function opSplit(doc, blockId, at, opIndex, expect) {
|
|
|
454
482
|
if (!found)
|
|
455
483
|
throw new MutationError("block_missing", `block ${blockId} not found`, { op_index: opIndex, block: blockId });
|
|
456
484
|
checkContentHash(found.block, expect, opIndex);
|
|
485
|
+
checkParentChildrenHash(found.siblings, expect, opIndex); // §1.2: its current parent
|
|
457
486
|
const raw = found.block.raw;
|
|
458
487
|
// `at` are UTF-8 byte offsets into the raw (spec/mutate §2.5) — the same
|
|
459
488
|
// language-neutral unit every persisted span uses. A JavaScript string slices
|
|
@@ -509,8 +538,11 @@ export function opMerge(doc, blockIds, opIndex, separator = " ", expectPer) {
|
|
|
509
538
|
throw new MutationError("not_contiguous", "merge blocks must share a parent", { op_index: opIndex });
|
|
510
539
|
if (located[i].block.type !== type)
|
|
511
540
|
throw new MutationError("type_mismatch", "merge blocks must share a type", { op_index: opIndex });
|
|
512
|
-
|
|
513
|
-
|
|
541
|
+
const e = expectPer?.[blockIds[i]];
|
|
542
|
+
if (e) {
|
|
543
|
+
checkContentHash(located[i].block, e, opIndex);
|
|
544
|
+
checkParentChildrenHash(located[i].siblings, e, opIndex); // §1.2: its current parent
|
|
545
|
+
}
|
|
514
546
|
}
|
|
515
547
|
for (let i = 1; i < indices.length; i++)
|
|
516
548
|
if (indices[i] !== indices[i - 1] + 1)
|
|
@@ -525,11 +557,4 @@ export function opMerge(doc, blockIds, opIndex, separator = " ", expectPer) {
|
|
|
525
557
|
siblings.splice(indices[i], 1);
|
|
526
558
|
return { ids: [first.id], mergedInto };
|
|
527
559
|
}
|
|
528
|
-
export function checkParentChildrenHash(doc, parentId, expected, opIndex) {
|
|
529
|
-
const list = parentId ? locate(doc, parentId)?.block.children ?? doc.children : doc.children;
|
|
530
|
-
const current = parentChildrenHash(list);
|
|
531
|
-
if (current !== expected) {
|
|
532
|
-
throw new MutationError("stale_expectation", "parent_children_hash mismatch", { op_index: opIndex, current: { parent_children_hash: current }, retriable: true });
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
560
|
//# sourceMappingURL=ops.js.map
|
|
@@ -20,4 +20,15 @@ export declare function makeStoreContext(store: Store, repoId: string, opts?: St
|
|
|
20
20
|
/** Tag SQL rows with their target so the store context resolves them (used by the
|
|
21
21
|
* tier-3 planner to hand produced rows back for residual evaluation). */
|
|
22
22
|
export declare function tagRows(rows: Record<string, unknown>[], target: Target): unknown[];
|
|
23
|
+
/**
|
|
24
|
+
* A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
|
|
25
|
+
* values, 1.2) is rendered as `{ id, path }` — its id column and its document's
|
|
26
|
+
* path, the same two keys every hit carries — never the store row itself.
|
|
27
|
+
* Returns undefined for anything that is not a tagged row (run.ts leaves those
|
|
28
|
+
* alone).
|
|
29
|
+
*/
|
|
30
|
+
export declare function rowRef(value: unknown): {
|
|
31
|
+
id: string;
|
|
32
|
+
path: string;
|
|
33
|
+
} | undefined;
|
|
23
34
|
export { TARGET };
|
|
@@ -17,6 +17,8 @@ import { docsRead } from "../core/read/document.js";
|
|
|
17
17
|
import { FilterInvalid } from "../search/cel/parser.js";
|
|
18
18
|
import { sanitizeFtsQuery } from "../search/fts-query.js";
|
|
19
19
|
import { cosineBytes } from "../core/vec.js";
|
|
20
|
+
import { ctxHashHex } from "../search/embeddings.js";
|
|
21
|
+
import { makeBlockContextResolver } from "../search/tasks.js";
|
|
20
22
|
const TARGET = Symbol("oqx.target");
|
|
21
23
|
const REPO_ROOT = Symbol("oqx.repoRoot");
|
|
22
24
|
const PROP_SOURCE = Symbol("oqx.propSource");
|
|
@@ -470,6 +472,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
470
472
|
// blocks
|
|
471
473
|
return !!one(`SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?) AND blocks_fts MATCH ?`, r.block_id, match);
|
|
472
474
|
};
|
|
475
|
+
// A block's vector is the cache row for its CURRENT (raw_hash, ctx_hash)
|
|
476
|
+
// (spec/surface §1.3, spec/search §3 — since search 1.1; before, the first row
|
|
477
|
+
// for the raw_hash whatever its context). The context resolver is built once
|
|
478
|
+
// per query (one scan of titles and sections), then a per-block chain walk.
|
|
479
|
+
let blockContexts;
|
|
480
|
+
const blockVector = (r, model) => {
|
|
481
|
+
blockContexts ??= makeBlockContextResolver(store, { repoId });
|
|
482
|
+
const path = typeof r.__path === "string" ? r.__path : String(scalar(`SELECT path FROM docs WHERE doc_id = ?`, r.doc_id) ?? "");
|
|
483
|
+
const ctx = blockContexts.ctx({ doc_id: String(r.doc_id), path, ordinal: Number(r.ordinal), type: String(r.type) });
|
|
484
|
+
return one(`SELECT vec FROM embeddings WHERE content_hash = ? AND ctx_hash = ? AND model = ?`, r.raw_hash, Buffer.from(ctxHashHex(ctx), "hex"), model);
|
|
485
|
+
};
|
|
473
486
|
const semanticScore = (t, r, phrase) => {
|
|
474
487
|
if (t === "nodes" || t === "edges")
|
|
475
488
|
throw new FilterInvalid("semantic(...) is available on the docs and blocks targets", "OQX semantic");
|
|
@@ -478,7 +491,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
478
491
|
throw new FilterInvalid(`semantic(${JSON.stringify(phrase)}) needs an embedding provider; none is configured for this query`, "OQX semantic");
|
|
479
492
|
const row = t === "docs"
|
|
480
493
|
? one(`SELECT vec FROM doc_embeddings WHERE doc_id = ? AND model = ?`, r.doc_id, resolved.model)
|
|
481
|
-
:
|
|
494
|
+
: blockVector(r, resolved.model);
|
|
482
495
|
if (!row)
|
|
483
496
|
return null;
|
|
484
497
|
return cosineBytes(row.vec, resolved.vec);
|
|
@@ -597,5 +610,21 @@ function hex(b) {
|
|
|
597
610
|
export function tagRows(rows, target) {
|
|
598
611
|
return tagAll(rows, target);
|
|
599
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
|
|
615
|
+
* values, 1.2) is rendered as `{ id, path }` — its id column and its document's
|
|
616
|
+
* path, the same two keys every hit carries — never the store row itself.
|
|
617
|
+
* Returns undefined for anything that is not a tagged row (run.ts leaves those
|
|
618
|
+
* alone).
|
|
619
|
+
*/
|
|
620
|
+
export function rowRef(value) {
|
|
621
|
+
const t = value?.[TARGET];
|
|
622
|
+
if (!t)
|
|
623
|
+
return undefined;
|
|
624
|
+
const r = value;
|
|
625
|
+
const id = t === "docs" ? r.doc_id : t === "blocks" ? r.block_id : t === "nodes" ? r.node_id : r.edge_id;
|
|
626
|
+
const path = t === "docs" ? r.path : r.__path;
|
|
627
|
+
return { id: String(id), path: String(path ?? "") };
|
|
628
|
+
}
|
|
600
629
|
export { TARGET };
|
|
601
630
|
//# sourceMappingURL=context.js.map
|