@omgbase/core 0.10.0 → 0.11.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/README.md +10 -10
- package/dist/src/core/read/document.d.ts +18 -5
- package/dist/src/core/read/document.js +21 -7
- package/dist/src/core/read/nodes.d.ts +7 -7
- package/dist/src/core/read/nodes.js +5 -3
- package/dist/src/core/read/reader.d.ts +11 -7
- package/dist/src/core/read/reader.js +8 -10
- package/dist/src/format/adapter.d.ts +1 -1
- package/dist/src/graph/history.d.ts +20 -14
- package/dist/src/graph/history.js +12 -12
- package/dist/src/graph/inbound-links.d.ts +4 -4
- package/dist/src/graph/inbound-links.js +4 -4
- package/dist/src/graph/link-health.d.ts +8 -8
- package/dist/src/graph/link-health.js +6 -6
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/mcp/arguments.d.ts +27 -0
- package/dist/src/mcp/arguments.js +147 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +16 -9
- package/dist/src/mcp/graph.js +58 -35
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +215 -121
- package/dist/src/mcp/server.d.ts +9 -1
- package/dist/src/mcp/server.js +359 -268
- package/dist/src/mutate/apply.d.ts +11 -4
- package/dist/src/mutate/apply.js +3 -2
- package/dist/src/mutate/docs.js +7 -7
- package/dist/src/mutate/macros.d.ts +4 -1
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +8 -5
- package/dist/src/mutate/plan-update.js +9 -8
- package/dist/src/oqx-js/context.d.ts +17 -4
- package/dist/src/oqx-js/context.js +268 -131
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +14 -15
- package/dist/src/oqx-js/root-row.d.ts +10 -3
- package/dist/src/oqx-js/root-row.js +18 -8
- package/dist/src/oqx-js/run.d.ts +6 -0
- package/dist/src/oqx-js/run.js +71 -24
- package/dist/src/oqx-js/sql/scan.js +7 -7
- package/dist/src/oqx-js/sql/translate.d.ts +12 -9
- package/dist/src/oqx-js/sql/translate.js +183 -75
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -26
- package/dist/src/search/cel/ast.d.ts +1 -1
- package/dist/src/search/cel/compile.d.ts +1 -1
- package/dist/src/search/cel/compile.js +43 -70
- package/dist/src/search/cel/parser.js +1 -1
- package/dist/src/search/resolve.d.ts +2 -0
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +2 -2
- package/dist/src/search/text.js +1 -1
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +41 -8
- package/dist/src/surface-paths.js +34 -18
- package/dist/src/sync/admin.d.ts +2 -1
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
|
@@ -37,11 +37,44 @@ function tagAll(rows, t) {
|
|
|
37
37
|
r[TARGET] = t;
|
|
38
38
|
return rows;
|
|
39
39
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
40
|
+
// THE NAME RULE (spec/surface §1.2, 3.0). `$` belongs to OQX: `$it`, `$key`,
|
|
41
|
+
// `$depth`, `$stop`, `$leaf`, `$frontier`, `$ordinal` are values the LANGUAGE
|
|
42
|
+
// injects into a scope; omgbase adds no `$` names. Every engine attribute is a
|
|
43
|
+
// bare snake_case name that WINS on collision — the convention the relations
|
|
44
|
+
// (`children`, `blocks`, `out_links`, `doc`, `frontmatter`, …) always followed:
|
|
45
|
+
// a bare name that is an engine attribute reads the engine's value, never the
|
|
46
|
+
// author's. The attributes: every row's `id`/`path`; docs' and blocks' `raw`/
|
|
47
|
+
// `hash`/`updated_at`; docs' `title` (the computed title) and `tags` (authored
|
|
48
|
+
// tags ∪ body hashtags) and `format`; a foreign key named for what it is
|
|
49
|
+
// (`doc_id`, `block_id`, `via_id`, `dst_id`, `dst_path`, `uri`), a column its
|
|
50
|
+
// column (`type`, `text`, `depth`, `ordinal`, `kind`, `predicate`, `field`).
|
|
51
|
+
// User data is bare too, read by any non-colliding key; where it collides it is
|
|
52
|
+
// reached through a source handle — `data` (the merged authored view:
|
|
53
|
+
// frontmatter + inline by the property rule, exactly the read a bare
|
|
54
|
+
// non-colliding key performs), `frontmatter`, `inline` — so `data.path` is the
|
|
55
|
+
// author's `path:` key, `data.tags` the authored list alone, `data.title` the
|
|
56
|
+
// authored title. There is no loud-error set: a bare `id`/`path`/`updated_at`/
|
|
57
|
+
// `hash`/`raw` is simply the engine's attribute (before this batch of 3.0 the
|
|
58
|
+
// attributes were `$`-prefixed and the bare spelling was an error, §9). A bare
|
|
59
|
+
// target name — `docs`, `links`, … — is a loud error of its own (root-row.ts).
|
|
60
|
+
const DOC_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "title", "tags", "format"]);
|
|
61
|
+
const BLOCK_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "doc_id", "type", "text", "attrs", "depth", "ordinal"]);
|
|
62
|
+
const NODE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs"]);
|
|
63
|
+
const EDGE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "via_id", "dst_id", "dst_path", "uri", "field", "dst_kind", "predicate", "provenance", "anchor"]);
|
|
64
|
+
/** The engine attributes per kind (spec/surface §1.2 table). Exported so the
|
|
65
|
+
* translator's tests pin the two against each other. */
|
|
66
|
+
export const ATTRIBUTES = {
|
|
67
|
+
docs: DOC_ATTRIBUTES, blocks: BLOCK_ATTRIBUTES, nodes: NODE_ATTRIBUTES, links: EDGE_ATTRIBUTES,
|
|
68
|
+
};
|
|
69
|
+
/** The source handles of a document (§1.2): `data` is the merged authored view
|
|
70
|
+
* (frontmatter + inline), the other two one source each. */
|
|
71
|
+
export const SOURCE_HANDLES = new Set(["data", "frontmatter", "inline"]);
|
|
72
|
+
/** The value of a statically SINGLE relation that has no row — `dst` on an
|
|
73
|
+
* external or dangling link, `block` on a node without one (oqx 0.20, SEMANTICS
|
|
74
|
+
* §3): an object the engine treats as one row with no rows, so `dst { title }`
|
|
75
|
+
* is absent rather than `[]`, while as a VALUE it is absent (`materialize`),
|
|
76
|
+
* so `dst == null`, `has(dst)` and `dst.title` read as before. */
|
|
77
|
+
export const ABSENT_SINGLE = Object.freeze({});
|
|
45
78
|
export function makeStoreContext(store, repoId, opts = {}) {
|
|
46
79
|
const db = store.db;
|
|
47
80
|
const sv = opts.semanticVectors;
|
|
@@ -63,7 +96,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
63
96
|
// iteration, indexing, `length`, rendering all see a plain array of tagged
|
|
64
97
|
// rows) that runs its SELECT on first use. The engine's optimizer asks
|
|
65
98
|
// `indexFor` about a nested block's receiver BEFORE reading it, so a
|
|
66
|
-
// correlated probe on `^docs` / `^
|
|
99
|
+
// correlated probe on `^docs` / `^links` / … is answered from the
|
|
67
100
|
// store's indexes (store-index.ts) and the scan never runs; anything else
|
|
68
101
|
// (a top-level `from docs`, a `count { }`, a non-indexable path) reads it
|
|
69
102
|
// whole exactly as before. One handle per target per context: a context lives
|
|
@@ -106,19 +139,19 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
106
139
|
return proxy;
|
|
107
140
|
};
|
|
108
141
|
const rootFns = {
|
|
109
|
-
docs: () => lazyRoot("docs"), blocks: () => lazyRoot("blocks"), nodes: () => lazyRoot("nodes"),
|
|
142
|
+
docs: () => lazyRoot("docs"), blocks: () => lazyRoot("blocks"), nodes: () => lazyRoot("nodes"), links: () => lazyRoot("links"),
|
|
110
143
|
};
|
|
111
144
|
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18 `rootObject`): the
|
|
112
145
|
// repository, reached as `^$it` from a top-level row (`0^$it` from any depth).
|
|
113
146
|
// Its own enumerable keys are the four collections — lazy getters, so
|
|
114
147
|
// `entries(^$it)` names them without running a scan and `^$it.docs` is the
|
|
115
|
-
// same handle as `^docs` — and `get` answers
|
|
116
|
-
//
|
|
148
|
+
// same handle as `^docs` — and `get` answers `id` with the repository id (an
|
|
149
|
+
// attribute, not an entry). Tagged so `get` recognizes it.
|
|
117
150
|
const rootRow = Object.defineProperties({ [ROOT_ROW]: true }, {
|
|
118
151
|
docs: { enumerable: true, get: () => lazyRoot("docs") },
|
|
119
152
|
blocks: { enumerable: true, get: () => lazyRoot("blocks") },
|
|
120
153
|
nodes: { enumerable: true, get: () => lazyRoot("nodes") },
|
|
121
|
-
|
|
154
|
+
links: { enumerable: true, get: () => lazyRoot("links") },
|
|
122
155
|
});
|
|
123
156
|
// ---- attrs / property decoding --------------------------------------------
|
|
124
157
|
const parseJson = (v) => {
|
|
@@ -165,14 +198,18 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
165
198
|
return decodeProp(rows[0]);
|
|
166
199
|
return rows.map(decodeProp);
|
|
167
200
|
};
|
|
168
|
-
// All of a doc's properties
|
|
169
|
-
// entries
|
|
201
|
+
// All of a doc's authored properties as OQX entries, for `entries(frontmatter)`
|
|
202
|
+
// / `entries(inline)` / `entries(data)`: one entry per TOP-LEVEL key, valued by
|
|
170
203
|
// the same scalar-vs-list rule as a bare read (`docProp`), with flattened dotted
|
|
171
|
-
// keys (`logging.level`) folded back into one nested entry.
|
|
172
|
-
//
|
|
173
|
-
//
|
|
204
|
+
// keys (`logging.level`) folded back into one nested entry. `data` (source
|
|
205
|
+
// null) is frontmatter ∪ inline — the keys deduped, each valued by the merged
|
|
206
|
+
// read. Keys come in the properties table's deterministic order — by key —
|
|
207
|
+
// because authored key position is not indexed (`ord` is the position WITHIN
|
|
208
|
+
// a list-valued key).
|
|
174
209
|
const docPropEntries = (docId, source) => {
|
|
175
|
-
const keys =
|
|
210
|
+
const keys = (source
|
|
211
|
+
? all(`SELECT DISTINCT key FROM properties WHERE doc_id = ? AND source = ? AND deleted_commit IS NULL ORDER BY key`, docId, source)
|
|
212
|
+
: all(`SELECT DISTINCT key FROM properties WHERE doc_id = ? AND source IN ('frontmatter', 'inline') AND deleted_commit IS NULL ORDER BY key`, docId)).map((r) => String(r.key));
|
|
176
213
|
const tops = [];
|
|
177
214
|
const seen = new Set();
|
|
178
215
|
for (const k of keys) {
|
|
@@ -182,7 +219,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
182
219
|
tops.push(top);
|
|
183
220
|
}
|
|
184
221
|
}
|
|
185
|
-
return tops.map((t) => semantics.makeEntry(t, docProp(docId, t, source)));
|
|
222
|
+
return tops.map((t) => semantics.makeEntry(t, docProp(docId, t, source ?? undefined)));
|
|
186
223
|
};
|
|
187
224
|
// Reconstruct a nested object from flattened dotted keys under `prefix.`
|
|
188
225
|
// (undefined when there are none). Leaves are decoded property values.
|
|
@@ -261,18 +298,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
261
298
|
docs: {
|
|
262
299
|
nodes: (r) => docNodesInOrder(r.doc_id, r.path),
|
|
263
300
|
blocks: (r) => tagAll(docBlocksPreorder(r.doc_id, r.path).map((x) => x.row), "blocks"),
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
WHERE e.dst_node = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.doc_id), "edges"),
|
|
301
|
+
// The linked / linking DOCUMENTS are reached through the link rows (3.0
|
|
302
|
+
// follow-up, §9: the `out` / `in` relations are gone): `out_links collect
|
|
303
|
+
// distinct { from dst }` and `in_links collect distinct { from doc }`.
|
|
304
|
+
out_links: (r) => tagAll(all(`SELECT e.*, ? AS __path FROM edges e WHERE e.src_doc = ? AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.path, r.doc_id), "links"),
|
|
305
|
+
in_links: (r) => tagAll(all(`SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
|
|
306
|
+
WHERE e.dst_node = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.doc_id), "links"),
|
|
271
307
|
},
|
|
272
308
|
blocks: {
|
|
273
309
|
children: (r) => tagAll(all(`SELECT b.*, ? AS __path FROM blocks b WHERE b.parent_block = ? AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id`, r.__path, r.block_id), "blocks"),
|
|
274
310
|
nodes: (r) => tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.block_id = ? ORDER BY n.span_start, n.node_id`, r.__path, r.block_id), "nodes"),
|
|
275
|
-
|
|
311
|
+
out_links: (r) => tagAll(all(`SELECT e.*, ? AS __path FROM edges e WHERE e.src_block = ? AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.__path, r.block_id), "links"),
|
|
276
312
|
// enclosing section node(s): md:section whose range contains this block's top ordinal.
|
|
277
313
|
section: (r) => {
|
|
278
314
|
const top = topOrdinal(r);
|
|
@@ -314,89 +350,193 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
314
350
|
ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id`, r.__path, r.doc_id, lvl, f, l, lvl), "nodes");
|
|
315
351
|
},
|
|
316
352
|
},
|
|
317
|
-
|
|
353
|
+
links: {},
|
|
318
354
|
};
|
|
319
355
|
const jattr = (row, k) => {
|
|
320
356
|
const a = row.attrs;
|
|
321
357
|
const o = typeof a === "string" ? parseJson(a) : a;
|
|
322
358
|
return o && typeof o === "object" ? o[k] : undefined;
|
|
323
359
|
};
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
360
|
+
// Owning-entity reach-through (single row) with FOREIGN-KEY ELIMINATION
|
|
361
|
+
// (spec/surface §1, 3.0): the owner is handed out as a LAZY row whose identity
|
|
362
|
+
// columns are already known from the child — a block's/node's `doc_id` and
|
|
363
|
+
// `__path` name its document, an edge's `src_doc` and `__path` its source doc,
|
|
364
|
+
// a node's `block_id` (+ `doc_id`, `__path`) its block — so `doc.id` and
|
|
365
|
+
// `doc.path` (and `block.id`/`block.path`/`block.doc_id` on a node) read
|
|
366
|
+
// those columns and run NO statement, exactly like the bare `doc_id`; the full
|
|
367
|
+
// owner row is loaded on the first read of anything else (`doc.layer`,
|
|
368
|
+
// `block.text`). The translator mirrors this in SQL (`doc.id` → the FK
|
|
369
|
+
// column). A Proxy over the backing object so the engine sees a plain tagged
|
|
370
|
+
// row either way (indexing, rendering, `rowRef`).
|
|
371
|
+
const lazyOwner = (known, t, load) => {
|
|
372
|
+
const backing = tag({ ...known }, t);
|
|
373
|
+
const knownKeys = new Set(Object.keys(known));
|
|
374
|
+
let filled = false;
|
|
375
|
+
const fill = () => {
|
|
376
|
+
if (filled)
|
|
377
|
+
return;
|
|
378
|
+
filled = true;
|
|
379
|
+
const full = load();
|
|
380
|
+
if (full)
|
|
381
|
+
Object.assign(backing, full);
|
|
382
|
+
};
|
|
383
|
+
// Only an unknown STRING key is a column read worth the load: symbols (the
|
|
384
|
+
// target tag, the context's own row markers, `Symbol.iterator` probes) are
|
|
385
|
+
// never store columns.
|
|
386
|
+
const loads = (prop) => typeof prop === "string" && !knownKeys.has(prop);
|
|
387
|
+
const proxy = new Proxy(backing, {
|
|
388
|
+
get(target, prop, receiver) {
|
|
389
|
+
if (loads(prop))
|
|
390
|
+
fill();
|
|
391
|
+
return Reflect.get(target, prop, receiver);
|
|
392
|
+
},
|
|
393
|
+
has(target, prop) { if (loads(prop))
|
|
394
|
+
fill(); return Reflect.has(target, prop); },
|
|
395
|
+
ownKeys(target) { fill(); return Reflect.ownKeys(target); },
|
|
396
|
+
getOwnPropertyDescriptor(target, prop) { if (loads(prop))
|
|
397
|
+
fill(); return Reflect.getOwnPropertyDescriptor(target, prop); },
|
|
398
|
+
});
|
|
399
|
+
return proxy;
|
|
400
|
+
};
|
|
401
|
+
const owningDoc = (row) => {
|
|
402
|
+
const id = row.doc_id ?? row.src_doc;
|
|
403
|
+
if (id == null)
|
|
404
|
+
return undefined;
|
|
405
|
+
const path = row.__path;
|
|
406
|
+
if (typeof path !== "string")
|
|
407
|
+
return tag(one(`SELECT * FROM docs WHERE doc_id = ?`, id), "docs");
|
|
408
|
+
return lazyOwner({ doc_id: id, path }, "docs", () => one(`SELECT * FROM docs WHERE doc_id = ?`, id));
|
|
409
|
+
};
|
|
410
|
+
// `dst` on a link row (spec/surface §1.2, 3.0 follow-up): the destination
|
|
411
|
+
// DOCUMENT row when the link points at one (`dst_kind == "document"` and the
|
|
412
|
+
// document exists — a phantom target has the kind and no row), read by
|
|
413
|
+
// `dst_id` exactly as `dst_path` is; absent for a `block` or `external`
|
|
414
|
+
// destination (never reached into). `doc` stays the source document.
|
|
415
|
+
const destinationDoc = (row) => {
|
|
416
|
+
if (row.dst_kind !== "document")
|
|
417
|
+
return undefined;
|
|
418
|
+
return tag(one(`SELECT * FROM docs WHERE doc_id = ?`, row.dst_node), "docs");
|
|
419
|
+
};
|
|
420
|
+
const owningBlock = (row) => {
|
|
421
|
+
if (!row.block_id)
|
|
422
|
+
return undefined;
|
|
423
|
+
const load = () => one(`SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?`, row.block_id);
|
|
424
|
+
if (typeof row.__path !== "string")
|
|
425
|
+
return tag(load(), "blocks");
|
|
426
|
+
return lazyOwner({ block_id: row.block_id, doc_id: row.doc_id, __path: row.__path }, "blocks", load);
|
|
427
|
+
};
|
|
428
|
+
// ---- engine attributes -----------------------------------------------------
|
|
429
|
+
// Bare, snake_case, winning on collision (see the name rule above; spec/surface
|
|
430
|
+
// §1.2, 3.0). `path` (and the edge field `dst_path`) is the REFERENCE form
|
|
431
|
+
// (`/a/b.md`, §1 "Paths", 2.0): the rows carry the storage form (`path`, the
|
|
432
|
+
// `__path` join column) and the read roots it, so nothing below the binding
|
|
333
433
|
// changes. run.ts roots the string literals a query compares them with.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// with the row as receiver (a free function sees no row).
|
|
338
|
-
if (name === "$self")
|
|
339
|
-
return row;
|
|
434
|
+
// `undefined` means "not an attribute" — the caller falls through to the
|
|
435
|
+
// handles, relations and the author's data.
|
|
436
|
+
const attribute = (row, t, name) => {
|
|
340
437
|
if (t === "docs")
|
|
341
438
|
switch (name) {
|
|
342
|
-
case "
|
|
343
|
-
case "
|
|
344
|
-
case "
|
|
345
|
-
case "
|
|
346
|
-
|
|
347
|
-
case "
|
|
348
|
-
|
|
439
|
+
case "id": return row.doc_id;
|
|
440
|
+
case "path": return referencePath(String(row.path));
|
|
441
|
+
case "hash": return row.file_hash == null ? null : hex(row.file_hash);
|
|
442
|
+
case "updated_at": return scalar(`SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?`, row.current_rev) ?? null;
|
|
443
|
+
// `raw`: the exact file bytes (spec/store §6.1, frontmatter included).
|
|
444
|
+
case "raw": return docsRead(store, String(row.doc_id))?.raw ?? null;
|
|
445
|
+
// `title`: the computed title (spec/properties §3.3 — the authored
|
|
446
|
+
// title, else the first H1, else absent → null). `data.title` is the
|
|
447
|
+
// authored one alone.
|
|
448
|
+
case "title": return docProp(String(row.doc_id), "$title", "computed") ?? null;
|
|
449
|
+
// `tags`: the union of the authored tags (frontmatter + inline, by the
|
|
450
|
+
// property rule) and the body `#hashtags` (the computed `$tags` row,
|
|
451
|
+
// spec/properties §3.3), deduped, authored first in authored order, then
|
|
452
|
+
// the hashtags in document order; null when both are absent.
|
|
453
|
+
// `data.tags` is the authored list alone.
|
|
454
|
+
case "tags": return docTags(String(row.doc_id));
|
|
455
|
+
case "format": return row.format;
|
|
349
456
|
}
|
|
350
457
|
if (t === "blocks")
|
|
351
458
|
switch (name) {
|
|
352
|
-
case "
|
|
353
|
-
case "
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
case "
|
|
357
|
-
case "
|
|
358
|
-
case "
|
|
359
|
-
case "
|
|
459
|
+
case "id": return row.block_id;
|
|
460
|
+
case "path": return referencePath(String(row.__path));
|
|
461
|
+
// `raw`: the block's raw source bytes — the blob its `raw_hash` names (a
|
|
462
|
+
// container's raw holds its children verbatim); `text` is the normalized text.
|
|
463
|
+
case "raw": return row.raw_hash == null ? null : scalar(`SELECT bytes FROM blobs WHERE hash = ?`, row.raw_hash)?.toString("utf8") ?? null;
|
|
464
|
+
case "hash": return row.raw_hash == null ? null : hex(row.raw_hash);
|
|
465
|
+
case "updated_at": return scalar(`SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?`, row.block_id) ?? null;
|
|
466
|
+
case "doc_id": return row.doc_id;
|
|
467
|
+
case "type": return row.type;
|
|
468
|
+
case "text": return row.text;
|
|
469
|
+
case "attrs": return parseJson(row.attrs);
|
|
470
|
+
case "ordinal": return row.ordinal;
|
|
471
|
+
case "depth": return row.depth;
|
|
360
472
|
}
|
|
361
473
|
if (t === "nodes")
|
|
362
474
|
switch (name) {
|
|
363
|
-
case "
|
|
364
|
-
case "
|
|
365
|
-
case "
|
|
366
|
-
case "
|
|
367
|
-
case "
|
|
475
|
+
case "id": return row.node_id;
|
|
476
|
+
case "path": return referencePath(String(row.__path));
|
|
477
|
+
case "doc_id": return row.doc_id;
|
|
478
|
+
case "block_id": return row.block_id ?? null;
|
|
479
|
+
case "kind": return row.kind;
|
|
480
|
+
case "name": return row.name;
|
|
481
|
+
case "value": return row.value;
|
|
482
|
+
case "attrs": return parseJson(row.attrs);
|
|
368
483
|
}
|
|
369
|
-
if (t === "
|
|
484
|
+
if (t === "links")
|
|
370
485
|
switch (name) {
|
|
371
|
-
|
|
372
|
-
case "
|
|
373
|
-
case "
|
|
374
|
-
case "
|
|
375
|
-
case "
|
|
376
|
-
case "
|
|
377
|
-
case "
|
|
378
|
-
case "
|
|
486
|
+
// the store's columns under the surface's names (spec/surface §1.2).
|
|
487
|
+
case "id": return row.edge_id;
|
|
488
|
+
case "path": return referencePath(String(row.__path));
|
|
489
|
+
case "doc_id": return row.src_doc;
|
|
490
|
+
case "block_id": return row.src_block ?? null;
|
|
491
|
+
case "via_id": return row.via_node ?? null;
|
|
492
|
+
case "dst_id": return row.dst_node;
|
|
493
|
+
case "dst_path": {
|
|
379
494
|
const p = scalar(`SELECT path FROM docs WHERE doc_id = ?`, row.dst_node);
|
|
380
495
|
return p == null ? null : referencePath(String(p));
|
|
381
496
|
}
|
|
382
|
-
case "
|
|
497
|
+
case "uri": return scalar(`SELECT uri FROM external_nodes WHERE node_id = ?`, row.dst_node) ?? null;
|
|
498
|
+
case "field": return row.src_field ?? null;
|
|
499
|
+
case "predicate":
|
|
500
|
+
case "provenance":
|
|
501
|
+
case "dst_kind":
|
|
502
|
+
case "anchor": return row[name];
|
|
383
503
|
}
|
|
384
504
|
return undefined;
|
|
385
505
|
};
|
|
506
|
+
// `tags` on a document: authored ∪ hashtags, each list as the property rule
|
|
507
|
+
// reads it (a scalar-authored `tags: x` is one element), deduped by value
|
|
508
|
+
// (canonical JSON, so both engines agree on non-string elements).
|
|
509
|
+
const docTags = (docId) => {
|
|
510
|
+
const asList = (v) => (v === undefined || v === null ? [] : Array.isArray(v) ? v : [v]);
|
|
511
|
+
const authored = asList(docProp(docId, "tags"));
|
|
512
|
+
const hashtags = asList(docProp(docId, "$tags", "computed"));
|
|
513
|
+
if (authored.length === 0 && hashtags.length === 0)
|
|
514
|
+
return null;
|
|
515
|
+
const seen = new Set();
|
|
516
|
+
const out = [];
|
|
517
|
+
for (const v of [...authored, ...hashtags]) {
|
|
518
|
+
const k = JSON.stringify(v) ?? "undefined";
|
|
519
|
+
if (seen.has(k))
|
|
520
|
+
continue;
|
|
521
|
+
seen.add(k);
|
|
522
|
+
out.push(v);
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
};
|
|
386
526
|
// ---- get ------------------------------------------------------------------
|
|
387
527
|
const getFrom = (row, key) => {
|
|
388
528
|
if (row == null)
|
|
389
529
|
return undefined;
|
|
390
|
-
// The root row: `^$it
|
|
530
|
+
// The root row: `^$it.id` is the repository id, `^$it.<target>` the lazy
|
|
391
531
|
// root scan (the same handle `^<target>` / `root(name)` hands out).
|
|
392
532
|
if (row[ROOT_ROW]) {
|
|
393
|
-
if (key === "
|
|
533
|
+
if (key === "id")
|
|
394
534
|
return repoId;
|
|
395
535
|
return rootFns[key] ? rootFns[key]() : undefined;
|
|
396
536
|
}
|
|
397
537
|
if (row[PROP_SOURCE]) {
|
|
398
538
|
const p = row;
|
|
399
|
-
return docProp(p.docId, key, p.source);
|
|
539
|
+
return docProp(p.docId, key, p.source ?? undefined);
|
|
400
540
|
}
|
|
401
541
|
const t = row[TARGET];
|
|
402
542
|
if (!t) {
|
|
@@ -404,8 +544,16 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
404
544
|
return row[key];
|
|
405
545
|
}
|
|
406
546
|
const r = row;
|
|
407
|
-
|
|
408
|
-
|
|
547
|
+
// `$self` = the current row: run.ts rewrites row-scoped domain functions
|
|
548
|
+
// (text/semantic/under_*/…) to `$self.fn(…)` so they arrive via callMethod
|
|
549
|
+
// with the row as receiver (a free function sees no row). Any other `$`
|
|
550
|
+
// name is OQX's (resolved by the engine before the row) or unknown — never
|
|
551
|
+
// an omgbase attribute.
|
|
552
|
+
if (key === "$self")
|
|
553
|
+
return r;
|
|
554
|
+
// engine attributes win on collision (the name rule above).
|
|
555
|
+
if (ATTRIBUTES[t].has(key))
|
|
556
|
+
return attribute(r, t, key);
|
|
409
557
|
// self-alias namespaces: `doc.x` (docs), `block.x` (blocks), `section.x` (nodes).
|
|
410
558
|
if (t === "docs" && key === "doc")
|
|
411
559
|
return r;
|
|
@@ -413,62 +561,40 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
413
561
|
return r;
|
|
414
562
|
if (t === "nodes" && key === "section")
|
|
415
563
|
return r;
|
|
416
|
-
// reach-through to owning entities
|
|
564
|
+
// reach-through to owning entities, and from a link row to its destination —
|
|
565
|
+
// statically single relations: absent is the single-with-no-row marker.
|
|
417
566
|
if (t !== "docs" && key === "doc")
|
|
418
|
-
return owningDoc(r);
|
|
567
|
+
return owningDoc(r) ?? ABSENT_SINGLE;
|
|
419
568
|
if (t === "nodes" && key === "block")
|
|
420
|
-
return owningBlock(r);
|
|
569
|
+
return owningBlock(r) ?? ABSENT_SINGLE;
|
|
570
|
+
if (t === "links" && key === "dst")
|
|
571
|
+
return destinationDoc(r) ?? ABSENT_SINGLE;
|
|
421
572
|
// relations.
|
|
422
573
|
const rels = rel[t];
|
|
423
574
|
if (rels[key])
|
|
424
575
|
return rels[key](r);
|
|
425
576
|
// A bare target name that is not one of this row's relations reads a
|
|
426
577
|
// property the row has none of (spec/surface §1.1, 2.0): loud, naming the
|
|
427
|
-
// root-row spelling. `docs`/`
|
|
578
|
+
// root-row spelling. `docs`/`links` never get here (run.ts refuses them
|
|
428
579
|
// statically); `blocks` on a block or edge row and `nodes` on a node or
|
|
429
580
|
// edge row do.
|
|
430
581
|
if (TARGETS.has(key))
|
|
431
582
|
throw new FilterInvalid(bareTargetMessage(key, null), "OQX");
|
|
432
|
-
//
|
|
583
|
+
// the author's data.
|
|
433
584
|
if (t === "docs") {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
throw new FilterInvalid(`bare '${key}' reads a frontmatter key; did you mean the intrinsic $${key}? (use frontmatter.${key} to force the property)`, "10 §2");
|
|
440
|
-
}
|
|
585
|
+
// The source handles: `data` is the merged authored view (frontmatter +
|
|
586
|
+
// inline), the other two one source each; `data.<k>` reaches an authored
|
|
587
|
+
// key whose bare name is an engine attribute (`data.path`, `data.tags`).
|
|
588
|
+
if (SOURCE_HANDLES.has(key))
|
|
589
|
+
return { [PROP_SOURCE]: true, docId: String(r.doc_id), source: key === "data" ? null : key };
|
|
441
590
|
return docProp(String(r.doc_id), key);
|
|
442
591
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (key === "attrs")
|
|
449
|
-
return parseJson(r.attrs);
|
|
450
|
-
// A bare non-structural identifier flattens into attrs: `checked` reads
|
|
451
|
-
// `attrs.checked`. Structural fields above win on collision; an absent key
|
|
452
|
-
// is undefined (silently false in a predicate), matching a doc's missing
|
|
453
|
-
// frontmatter key. The `attrs.<k>` form still works unchanged.
|
|
592
|
+
// blocks / nodes: a bare non-structural identifier flattens into attrs:
|
|
593
|
+
// `checked` reads `attrs.checked`. The attributes above win on collision; an
|
|
594
|
+
// absent key is undefined (silently false in a predicate), matching a doc's
|
|
595
|
+
// missing frontmatter key. The `attrs.<k>` form still works unchanged.
|
|
596
|
+
if (t === "blocks" || t === "nodes")
|
|
454
597
|
return jattr(r, key);
|
|
455
|
-
}
|
|
456
|
-
if (t === "nodes") {
|
|
457
|
-
if (key === "kind")
|
|
458
|
-
return r.kind;
|
|
459
|
-
if (key === "name")
|
|
460
|
-
return r.name;
|
|
461
|
-
if (key === "value")
|
|
462
|
-
return r.value;
|
|
463
|
-
if (key === "attrs")
|
|
464
|
-
return parseJson(r.attrs);
|
|
465
|
-
// Bare non-structural identifier flattens into attrs (see blocks above):
|
|
466
|
-
// `checked` reads `attrs.checked` on an `md:task`, `level` on an `md:section`.
|
|
467
|
-
return jattr(r, key);
|
|
468
|
-
}
|
|
469
|
-
// edges
|
|
470
|
-
if (key === "predicate" || key === "provenance" || key === "dst_kind" || key === "anchor" || key === "src_field")
|
|
471
|
-
return r[key];
|
|
472
598
|
return undefined;
|
|
473
599
|
};
|
|
474
600
|
// ---- domain functions (row-scoped, via callMethod on $self) ---------------
|
|
@@ -526,8 +652,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
526
652
|
}
|
|
527
653
|
};
|
|
528
654
|
const textMatch = (t, r, terms) => {
|
|
529
|
-
if (t === "
|
|
530
|
-
throw new FilterInvalid("text(...) is not available on the
|
|
655
|
+
if (t === "links")
|
|
656
|
+
throw new FilterInvalid("text(...) is not available on the links target", "10 §5");
|
|
531
657
|
const match = sanitizeFtsQuery(terms);
|
|
532
658
|
if (match === "")
|
|
533
659
|
return false;
|
|
@@ -550,7 +676,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
550
676
|
return one(`SELECT vec FROM embeddings WHERE content_hash = ? AND ctx_hash = ? AND model = ?`, r.raw_hash, Buffer.from(ctxHashHex(ctx), "hex"), model);
|
|
551
677
|
};
|
|
552
678
|
const semanticScore = (t, r, phrase) => {
|
|
553
|
-
if (t === "nodes" || t === "
|
|
679
|
+
if (t === "nodes" || t === "links")
|
|
554
680
|
throw new FilterInvalid("semantic(...) is available on the docs and blocks targets", "OQX semantic");
|
|
555
681
|
const resolved = sv?.get(phrase);
|
|
556
682
|
if (!resolved)
|
|
@@ -633,6 +759,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
633
759
|
// Store indexes by (target, path) — one object, one set of prepared statements,
|
|
634
760
|
// for the whole run (`null`: the path is not indexable).
|
|
635
761
|
const indexes = new Map();
|
|
762
|
+
// A row's identity — `$key` (spec/oqx §16, 0.20): its id column for every
|
|
763
|
+
// kind; anything else (a property bag, a lifted value) is known structurally.
|
|
636
764
|
const identity = (row) => {
|
|
637
765
|
const t = row?.[TARGET];
|
|
638
766
|
if (t === "docs")
|
|
@@ -641,18 +769,18 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
641
769
|
return row.block_id;
|
|
642
770
|
if (t === "nodes")
|
|
643
771
|
return row.node_id;
|
|
644
|
-
if (t === "
|
|
772
|
+
if (t === "links")
|
|
645
773
|
return row.edge_id;
|
|
646
774
|
return row;
|
|
647
775
|
};
|
|
648
776
|
return {
|
|
649
777
|
// The root scope's names (spec/oqx §2 step 5 at the root): the four lazy
|
|
650
|
-
// scans and
|
|
651
|
-
// the scan and
|
|
778
|
+
// scans and `id`, the repository id — so `^docs` / `0^docs` from any row is
|
|
779
|
+
// the scan and `^id` / `0^id` the id.
|
|
652
780
|
root(name) {
|
|
653
781
|
if (opts.rowsRoot && name === opts.rowsRoot.name)
|
|
654
782
|
return opts.rowsRoot.rows;
|
|
655
|
-
if (name === "
|
|
783
|
+
if (name === "id")
|
|
656
784
|
return repoId;
|
|
657
785
|
return rootFns[name] ? rootFns[name]() : undefined;
|
|
658
786
|
},
|
|
@@ -662,8 +790,11 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
662
790
|
return rootRow;
|
|
663
791
|
},
|
|
664
792
|
get: getFrom,
|
|
793
|
+
// Row position (spec/oqx §3): a tagged row is ONE row (single — the engine's
|
|
794
|
+
// default for a non-iterable object), a lazy scan (an iterable Proxy) a
|
|
795
|
+
// collection, the absent-single marker one row with none.
|
|
665
796
|
toRows(value) {
|
|
666
|
-
if (value == null)
|
|
797
|
+
if (value == null || value === ABSENT_SINGLE)
|
|
667
798
|
return [];
|
|
668
799
|
if (Array.isArray(value))
|
|
669
800
|
return value;
|
|
@@ -673,7 +804,11 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
673
804
|
return value;
|
|
674
805
|
return [value];
|
|
675
806
|
},
|
|
676
|
-
|
|
807
|
+
// Value position: the absent-single marker is absent.
|
|
808
|
+
materialize(value) {
|
|
809
|
+
return value === ABSENT_SINGLE ? undefined : value;
|
|
810
|
+
},
|
|
811
|
+
identityOf: identity,
|
|
677
812
|
// A store-backed equality index for a ROOT SCAN handle on a column or
|
|
678
813
|
// property SQLite indexes (store-index.ts); `undefined` for any other
|
|
679
814
|
// collection (a relation's rows, a lifted array) or path, so the engine
|
|
@@ -699,9 +834,10 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
699
834
|
// pushdown can serve the property's autopromoted/cached bounds — see
|
|
700
835
|
// detectRange + val_json in store/properties.ts.
|
|
701
836
|
callFunction(name, args) {
|
|
702
|
-
// `entries(frontmatter)` / `entries(inline)`: the source
|
|
703
|
-
// PropSourceRef, not a plain object), so materialize its
|
|
704
|
-
// entries here; any other argument falls through to the
|
|
837
|
+
// `entries(data)` / `entries(frontmatter)` / `entries(inline)`: the source
|
|
838
|
+
// handle is lazy (a PropSourceRef, not a plain object), so materialize its
|
|
839
|
+
// property bag as entries here; any other argument falls through to the
|
|
840
|
+
// oqx builtin.
|
|
705
841
|
if (name === "entries" && args[0]?.[PROP_SOURCE]) {
|
|
706
842
|
const p = args[0];
|
|
707
843
|
return { handled: true, value: docPropEntries(p.docId, p.source) };
|
|
@@ -743,9 +879,10 @@ export function tagRows(rows, target) {
|
|
|
743
879
|
}
|
|
744
880
|
/**
|
|
745
881
|
* A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
|
|
746
|
-
* values, 1.2) is rendered as `{ id, path }` — its id column and its
|
|
747
|
-
* path in the reference form (`/a.md`, §1 "Paths"), the same two keys
|
|
748
|
-
* carries — never the store
|
|
882
|
+
* values, 1.2) is rendered as `{ id, path }` (3.0) — its id column and its
|
|
883
|
+
* document's path in the reference form (`/a.md`, §1 "Paths"), the same two keys
|
|
884
|
+
* every hit carries, spelled as the attributes that read them — never the store
|
|
885
|
+
* row itself.
|
|
749
886
|
* Returns undefined for anything that is not a tagged row (run.ts leaves those
|
|
750
887
|
* alone).
|
|
751
888
|
*/
|
|
@@ -12,12 +12,12 @@ export declare function rootTarget(source: Expr): Target | null;
|
|
|
12
12
|
* • any `call` node (a function or method: unknown name, bad regex, wrong target…)
|
|
13
13
|
* • any nested block whose consumer is `single` (raises on 0 or >1 rows)
|
|
14
14
|
* • any `^`-escaped name: an `outer` reference or a `^name:` lift
|
|
15
|
-
* • a bare reserved docs basename (`id path updated_at
|
|
15
|
+
* • a bare reserved docs basename (`id path updated_at hash raw`): as
|
|
16
16
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
17
17
|
* on any target at any depth (the reach-through row is a doc)
|
|
18
|
-
* • a bare target name (`docs blocks nodes
|
|
18
|
+
* • a bare target name (`docs blocks nodes links`, root-row.ts) read as a
|
|
19
19
|
* property: as an `ident` at the ROOT scope when it is not a relation of
|
|
20
|
-
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on
|
|
20
|
+
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
|
|
21
21
|
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
22
22
|
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
23
23
|
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|