@omgbase/core 0.7.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/src/core/cursor.d.ts +13 -1
  2. package/dist/src/core/cursor.js +21 -2
  3. package/dist/src/core/paths.d.ts +10 -0
  4. package/dist/src/core/paths.js +34 -0
  5. package/dist/src/core/read/document.d.ts +1 -0
  6. package/dist/src/core/read/document.js +3 -2
  7. package/dist/src/core/read/reader.d.ts +6 -3
  8. package/dist/src/core/read/reader.js +31 -16
  9. package/dist/src/graph/history.js +16 -10
  10. package/dist/src/graph/link-health.d.ts +2 -1
  11. package/dist/src/graph/link-health.js +13 -7
  12. package/dist/src/index.d.ts +3 -1
  13. package/dist/src/index.js +3 -1
  14. package/dist/src/mcp/errors.js +1 -1
  15. package/dist/src/mcp/graph.js +50 -15
  16. package/dist/src/mcp/reference.d.ts +1 -1
  17. package/dist/src/mcp/reference.js +102 -33
  18. package/dist/src/mcp/server.js +32 -28
  19. package/dist/src/oqx-js/context.d.ts +2 -1
  20. package/dist/src/oqx-js/context.js +181 -49
  21. package/dist/src/oqx-js/planner.d.ts +8 -1
  22. package/dist/src/oqx-js/planner.js +22 -42
  23. package/dist/src/oqx-js/root-row.d.ts +21 -0
  24. package/dist/src/oqx-js/root-row.js +55 -0
  25. package/dist/src/oqx-js/run.d.ts +3 -1
  26. package/dist/src/oqx-js/run.js +128 -164
  27. package/dist/src/oqx-js/sql/scan.d.ts +27 -0
  28. package/dist/src/oqx-js/sql/scan.js +62 -0
  29. package/dist/src/oqx-js/sql/translate.d.ts +7 -0
  30. package/dist/src/oqx-js/sql/translate.js +42 -11
  31. package/dist/src/oqx-js/store-index.d.ts +20 -0
  32. package/dist/src/oqx-js/store-index.js +193 -0
  33. package/dist/src/spec-versions.d.ts +3 -3
  34. package/dist/src/spec-versions.js +2 -2
  35. package/dist/src/surface-paths.d.ts +33 -0
  36. package/dist/src/surface-paths.js +75 -0
  37. package/package.json +2 -2
@@ -12,15 +12,19 @@
12
12
  // the corpus and the differential baseline. The tier-3 SQLite planner (planner.ts)
13
13
  // pushes the hot predicates down; both must agree (conformance suite).
14
14
  import { semantics } from "@omgbase/oqx";
15
+ import { storeIndexFor } from "./store-index.js";
16
+ import { TARGETS, bareTargetMessage } from "./root-row.js";
17
+ import { COLS, FROM, ORDER, guards } from "./sql/scan.js";
15
18
  import { detectRange } from "../core/store/properties.js";
16
19
  import { docsRead } from "../core/read/document.js";
20
+ import { referencePath, storagePath } from "../core/paths.js";
17
21
  import { FilterInvalid } from "../search/cel/parser.js";
18
22
  import { sanitizeFtsQuery } from "../search/fts-query.js";
19
23
  import { cosineBytes } from "../core/vec.js";
20
24
  import { ctxHashHex } from "../search/embeddings.js";
21
25
  import { makeBlockContextResolver } from "../search/tasks.js";
22
26
  const TARGET = Symbol("oqx.target");
23
- const REPO_ROOT = Symbol("oqx.repoRoot");
27
+ const ROOT_ROW = Symbol("oqx.rootRow");
24
28
  const PROP_SOURCE = Symbol("oqx.propSource");
25
29
  function tag(row, t) {
26
30
  if (!row)
@@ -34,9 +38,9 @@ function tagAll(rows, t) {
34
38
  return rows;
35
39
  }
36
40
  // docs intrinsics whose BARE (non-$) form is almost always a typo (10 §2); a bare
37
- // read of one is a loud error, matching the CEL guard.
38
- // (The root-scan receiver is the `$repo` intrinsic — `$repo.docs`/`$repo.nodes`/… —
39
- // so a bare `repo` is just an ordinary frontmatter key.)
41
+ // read of one is a loud error, matching the CEL guard. (A bare target name —
42
+ // `docs`, `edges`, … — is a loud error of its own, root-row.ts; `repo` is just an
43
+ // ordinary frontmatter key.)
40
44
  const RESERVED_DOC_BASENAMES = new Set(["id", "path", "updated_at", "content_hash", "body"]);
41
45
  export function makeStoreContext(store, repoId, opts = {}) {
42
46
  const db = store.db;
@@ -49,20 +53,73 @@ export function makeStoreContext(store, repoId, opts = {}) {
49
53
  return undefined;
50
54
  return Object.values(r)[0];
51
55
  };
52
- // ---- root collections (ordered for a stable (path, id) default) -----------
53
- const docsRoot = () => tagAll(all(`SELECT * FROM docs WHERE repo_id = ? AND deleted_commit IS NULL ORDER BY path, doc_id`, repoId), "docs");
54
- const blocksRoot = () => tagAll(all(`SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id
55
- WHERE b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
56
- ORDER BY d.path, b.block_id`, repoId), "blocks");
57
- const nodesRoot = () => tagAll(all(`SELECT n.*, d.path AS __path FROM nodes n JOIN docs d ON d.doc_id = n.doc_id
58
- WHERE n.repo_id = ? AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id`, repoId), "nodes");
59
- const edgesRoot = () => tagAll(all(`SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
60
- WHERE e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL
61
- ORDER BY d.path, e.edge_id`, repoId), "edges");
62
- const rootFns = { docs: docsRoot, blocks: blocksRoot, nodes: nodesRoot, edges: edgesRoot };
63
- // The repository handle behind the `$repo` intrinsic: `$repo.<target>` is the
64
- // explicit root scan (rootFns), `$repo.$id` the repository id.
65
- const repoRoot = { [REPO_ROOT]: true };
56
+ // ---- root collections (the shared scan shape of sql/scan.ts: live rows of
57
+ // this repo, ordered for a stable (path, id) default) -----------------------
58
+ const scan = (t) => {
59
+ const g = guards(t, repoId);
60
+ return tagAll(all(`SELECT ${COLS[t]} FROM ${FROM[t]} WHERE ${g.sql} ORDER BY ${ORDER[t]}`, ...g.params), t);
61
+ };
62
+ // A root scan is handed out LAZILY: an array (a `Proxy` over one — `Array.isArray`,
63
+ // iteration, indexing, `length`, rendering all see a plain array of tagged
64
+ // rows) that runs its SELECT on first use. The engine's optimizer asks
65
+ // `indexFor` about a nested block's receiver BEFORE reading it, so a
66
+ // correlated probe on `^docs` / `^edges` / … is answered from the
67
+ // store's indexes (store-index.ts) and the scan never runs; anything else
68
+ // (a top-level `from docs`, a `count { }`, a non-indexable path) reads it
69
+ // whole exactly as before. One handle per target per context: a context lives
70
+ // for one run, during which the store does not change, so `^docs` read
71
+ // from every outer row is one SELECT, not one per row — and the engine, which
72
+ // keys collections by identity, sees the same collection each time.
73
+ // `rootScans` recognizes the handles for `indexFor`. Because the handle IS an
74
+ // array, this context needs no `DataContext.materialize` (the Rust port's
75
+ // marker value does); `spec/surface` `lazy-root-*` pins what the language
76
+ // sees either way.
77
+ const rootScans = new WeakMap();
78
+ const handles = new Map();
79
+ const lazyRoot = (t) => {
80
+ let h = handles.get(t);
81
+ if (!h)
82
+ handles.set(t, (h = makeLazyRoot(t)));
83
+ return h;
84
+ };
85
+ const makeLazyRoot = (t) => {
86
+ const backing = [];
87
+ let filled = false;
88
+ const fill = () => {
89
+ if (!filled) {
90
+ filled = true;
91
+ for (const r of scan(t))
92
+ backing.push(r);
93
+ }
94
+ return backing;
95
+ };
96
+ const proxy = new Proxy(backing, {
97
+ get(target, prop, receiver) { fill(); return Reflect.get(target, prop, receiver); },
98
+ has(target, prop) { fill(); return Reflect.has(target, prop); },
99
+ ownKeys(target) { fill(); return Reflect.ownKeys(target); },
100
+ getOwnPropertyDescriptor(target, prop) { fill(); return Reflect.getOwnPropertyDescriptor(target, prop); },
101
+ set(target, prop, value, receiver) { fill(); return Reflect.set(target, prop, value, receiver); },
102
+ defineProperty(target, prop, desc) { fill(); return Reflect.defineProperty(target, prop, desc); },
103
+ deleteProperty(target, prop) { fill(); return Reflect.deleteProperty(target, prop); },
104
+ });
105
+ rootScans.set(proxy, { target: t, rows: fill });
106
+ return proxy;
107
+ };
108
+ const rootFns = {
109
+ docs: () => lazyRoot("docs"), blocks: () => lazyRoot("blocks"), nodes: () => lazyRoot("nodes"), edges: () => lazyRoot("edges"),
110
+ };
111
+ // The root row (spec/surface §1.1, 2.0; spec/oqx 0.18 `rootObject`): the
112
+ // repository, reached as `^$it` from a top-level row (`0^$it` from any depth).
113
+ // Its own enumerable keys are the four collections — lazy getters, so
114
+ // `entries(^$it)` names them without running a scan and `^$it.docs` is the
115
+ // same handle as `^docs` — and `get` answers `$id` with the repository id (an
116
+ // intrinsic, not an entry). Tagged so `get` recognizes it.
117
+ const rootRow = Object.defineProperties({ [ROOT_ROW]: true }, {
118
+ docs: { enumerable: true, get: () => lazyRoot("docs") },
119
+ blocks: { enumerable: true, get: () => lazyRoot("blocks") },
120
+ nodes: { enumerable: true, get: () => lazyRoot("nodes") },
121
+ edges: { enumerable: true, get: () => lazyRoot("edges") },
122
+ });
66
123
  // ---- attrs / property decoding --------------------------------------------
67
124
  const parseJson = (v) => {
68
125
  if (typeof v !== "string")
@@ -270,6 +327,10 @@ export function makeStoreContext(store, repoId, opts = {}) {
270
327
  ? tag(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), "blocks")
271
328
  : undefined;
272
329
  // ---- intrinsics ($-fields) ------------------------------------------------
330
+ // `$path` / `$dst_path` are the REFERENCE form (`/a/b.md`, spec/surface §1
331
+ // "Paths", 2.0): the rows carry the storage form (`path`, the `__path` join
332
+ // column) and the intrinsic roots it on read, so nothing below the binding
333
+ // changes. run.ts roots the string literals a query compares them with.
273
334
  const intrinsic = (row, t, name) => {
274
335
  // `$self` = the current row: run.ts rewrites row-scoped domain functions
275
336
  // (text/semantic/under_*/…) to `$self.fn(…)` so they arrive via callMethod
@@ -279,7 +340,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
279
340
  if (t === "docs")
280
341
  switch (name) {
281
342
  case "$id": return row.doc_id;
282
- case "$path": return row.path;
343
+ case "$path": return referencePath(String(row.path));
283
344
  case "$content_hash": return row.file_hash == null ? null : hex(row.file_hash);
284
345
  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;
285
346
  case "$body": return docsRead(store, String(row.doc_id))?.content ?? null;
@@ -290,7 +351,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
290
351
  switch (name) {
291
352
  case "$id": return row.block_id;
292
353
  case "$doc": return row.doc_id;
293
- case "$path": return row.__path;
354
+ case "$path": return referencePath(String(row.__path));
294
355
  case "$ordinal": return row.ordinal;
295
356
  case "$depth": return row.depth;
296
357
  case "$body": return row.text;
@@ -303,7 +364,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
303
364
  case "$node_id": return row.node_id;
304
365
  case "$doc_id": return row.doc_id;
305
366
  case "$block_id": return row.block_id;
306
- case "$path": return row.__path;
367
+ case "$path": return referencePath(String(row.__path));
307
368
  }
308
369
  if (t === "edges")
309
370
  switch (name) {
@@ -313,8 +374,11 @@ export function makeStoreContext(store, repoId, opts = {}) {
313
374
  case "$src_block": return row.src_block;
314
375
  case "$via": return row.via_node;
315
376
  case "$from_commit": return row.from_commit;
316
- case "$path": return row.__path;
317
- case "$dst_path": return scalar(`SELECT path FROM docs WHERE doc_id = ?`, row.dst_node) ?? null;
377
+ case "$path": return referencePath(String(row.__path));
378
+ case "$dst_path": {
379
+ const p = scalar(`SELECT path FROM docs WHERE doc_id = ?`, row.dst_node);
380
+ return p == null ? null : referencePath(String(p));
381
+ }
318
382
  case "$dst_uri": return scalar(`SELECT uri FROM external_nodes WHERE node_id = ?`, row.dst_node) ?? null;
319
383
  }
320
384
  return undefined;
@@ -323,14 +387,9 @@ export function makeStoreContext(store, repoId, opts = {}) {
323
387
  const getFrom = (row, key) => {
324
388
  if (row == null)
325
389
  return undefined;
326
- // `$repo` is an intrinsic of EVERY scope — the root and any row, store-backed
327
- // or plain. Bare names resolve against the current row only (oqx ≥ 0.7: no
328
- // scope climbing), so a correlated subquery at any depth reaches the
329
- // repository root through this local intrinsic, never by falling through
330
- // to an enclosing scope.
331
- if (key === "$repo")
332
- return repoRoot;
333
- if (row[REPO_ROOT]) {
390
+ // The root row: `^$it.$id` is the repository id, `^$it.<target>` the lazy
391
+ // root scan (the same handle `^<target>` / `root(name)` hands out).
392
+ if (row[ROOT_ROW]) {
334
393
  if (key === "$id")
335
394
  return repoId;
336
395
  return rootFns[key] ? rootFns[key]() : undefined;
@@ -363,6 +422,13 @@ export function makeStoreContext(store, repoId, opts = {}) {
363
422
  const rels = rel[t];
364
423
  if (rels[key])
365
424
  return rels[key](r);
425
+ // A bare target name that is not one of this row's relations reads a
426
+ // property the row has none of (spec/surface §1.1, 2.0): loud, naming the
427
+ // root-row spelling. `docs`/`edges` never get here (run.ts refuses them
428
+ // statically); `blocks` on a block or edge row and `nodes` on a node or
429
+ // edge row do.
430
+ if (TARGETS.has(key))
431
+ throw new FilterInvalid(bareTargetMessage(key, null), "OQX");
366
432
  // per-target scalar fields.
367
433
  if (t === "docs") {
368
434
  if (key === "format")
@@ -505,14 +571,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
505
571
  return !!one(`SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
506
572
  WHERE s.doc_id = ? AND lower(hb.text) LIKE '%' || lower(?) || '%' AND s.first_ordinal <= ? AND s.last_ordinal >= ? LIMIT 1`, r.doc_id, text, top, top);
507
573
  };
574
+ // `within(t)`: a doc id, else a path in either form (`/texts/*`, `texts/*`,
575
+ // `texts/x.md`) matched against the storage path.
508
576
  const within = (r, target) => {
509
577
  if (target.startsWith("d_"))
510
578
  return r.doc_id === target;
511
- if (target.includes("*")) {
512
- const like = target.replace(/[%_]/g, "\\$&").replace(/\*/g, "%");
579
+ const path = storagePath(target);
580
+ if (path.includes("*")) {
581
+ const like = path.replace(/[%_]/g, "\\$&").replace(/\*/g, "%");
513
582
  return !!one(`SELECT 1 WHERE ? LIKE ? ESCAPE '\\'`, r.__path, like);
514
583
  }
515
- return r.__path === target;
584
+ return r.__path === path;
516
585
  };
517
586
  const underKind = (r, kind, name) => {
518
587
  const ap = String(r.ancestor_path ?? "").split("/").filter(Boolean);
@@ -531,14 +600,67 @@ export function makeStoreContext(store, repoId, opts = {}) {
531
600
  return false;
532
601
  return jattr(r, "key") === leaf || jattr(r, "key") === key;
533
602
  };
603
+ // ---- refs(x): document references held in a property (spec/surface §1.3, 1.5)
604
+ // `x` is a string, a list, or absent; every string element that names a live
605
+ // document of this repo — a doc id (`d_…`), a repo-root-absolute path
606
+ // (`/a/b.md`) or a bare repo-relative path (`a/b.md`) — resolves to that
607
+ // document's row; anything else (a dangling reference, a non-string element)
608
+ // is dropped. Order preserved, duplicates kept. Each element is one indexed
609
+ // lookup under the root scan's guards and columns (`sql/scan.ts`), so the rows
610
+ // are indistinguishable from scanned ones and the docs root is never read.
611
+ const refDoc = (ref) => {
612
+ const g = guards("docs", repoId);
613
+ const path = storagePath(ref);
614
+ const byPath = one(`SELECT ${COLS.docs} FROM ${FROM.docs} WHERE ${g.sql} AND d.path = ?`, ...g.params, path);
615
+ if (byPath)
616
+ return tag(byPath, "docs");
617
+ if (!ref.startsWith("d_"))
618
+ return undefined;
619
+ return tag(one(`SELECT ${COLS.docs} FROM ${FROM.docs} WHERE ${g.sql} AND d.doc_id = ?`, ...g.params, ref), "docs");
620
+ };
621
+ const refs = (x) => {
622
+ const items = x == null ? [] : Array.isArray(x) ? x : [x];
623
+ const out = [];
624
+ for (const item of items) {
625
+ if (typeof item !== "string")
626
+ continue;
627
+ const row = refDoc(item);
628
+ if (row)
629
+ out.push(row);
630
+ }
631
+ return out;
632
+ };
633
+ // Store indexes by (target, path) — one object, one set of prepared statements,
634
+ // for the whole run (`null`: the path is not indexable).
635
+ const indexes = new Map();
636
+ const identity = (row) => {
637
+ const t = row?.[TARGET];
638
+ if (t === "docs")
639
+ return row.doc_id;
640
+ if (t === "blocks")
641
+ return row.block_id;
642
+ if (t === "nodes")
643
+ return row.node_id;
644
+ if (t === "edges")
645
+ return row.edge_id;
646
+ return row;
647
+ };
534
648
  return {
649
+ // The root scope's names (spec/oqx §2 step 5 at the root): the four lazy
650
+ // scans and `$id`, the repository id — so `^docs` / `0^docs` from any row is
651
+ // the scan and `^$id` / `0^$id` the id.
535
652
  root(name) {
536
653
  if (opts.rowsRoot && name === opts.rowsRoot.name)
537
654
  return opts.rowsRoot.rows;
538
- if (name === "$repo")
539
- return repoRoot;
655
+ if (name === "$id")
656
+ return repoId;
540
657
  return rootFns[name] ? rootFns[name]() : undefined;
541
658
  },
659
+ // The root scope's row (oqx 0.18): the repository, so `^$it` from a
660
+ // top-level row is the root row and `^$it.docs` the docs scan.
661
+ rootObject() {
662
+ return rootRow;
663
+ },
542
664
  get: getFrom,
543
665
  toRows(value) {
544
666
  if (value == null)
@@ -551,17 +673,24 @@ export function makeStoreContext(store, repoId, opts = {}) {
551
673
  return value;
552
674
  return [value];
553
675
  },
554
- identity(row) {
555
- const t = row?.[TARGET];
556
- if (t === "docs")
557
- return row.doc_id;
558
- if (t === "blocks")
559
- return row.block_id;
560
- if (t === "nodes")
561
- return row.node_id;
562
- if (t === "edges")
563
- return row.edge_id;
564
- return row;
676
+ identity,
677
+ // A store-backed equality index for a ROOT SCAN handle on a column or
678
+ // property SQLite indexes (store-index.ts); `undefined` for any other
679
+ // collection (a relation's rows, a lifted array) or path, so the engine
680
+ // builds its own over the materialized rows.
681
+ indexFor(collection, path) {
682
+ if (collection === null || typeof collection !== "object")
683
+ return undefined;
684
+ const root = rootScans.get(collection);
685
+ if (!root)
686
+ return undefined;
687
+ const key = `${root.target}\u0000${JSON.stringify(path)}`;
688
+ let index = indexes.get(key);
689
+ if (index === undefined) {
690
+ index = storeIndexFor(root.target, path, { db, repoId, tag: (rows, t) => tagAll(rows, t), get: getFrom, rows: root.rows, identity }) ?? null;
691
+ indexes.set(key, index);
692
+ }
693
+ return index ?? undefined;
565
694
  },
566
695
  // Free functions: oqx-js builtins (list/size/has) plus `range(s)`, which
567
696
  // coerces a string value to a range so `<point> in range(prop)` tests
@@ -577,6 +706,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
577
706
  const p = args[0];
578
707
  return { handled: true, value: docPropEntries(p.docId, p.source) };
579
708
  }
709
+ if (name === "refs")
710
+ return { handled: true, value: refs(args[0]) };
580
711
  if (name === "range") {
581
712
  const s = args[0];
582
713
  if (semantics.isRange(s))
@@ -613,7 +744,8 @@ export function tagRows(rows, target) {
613
744
  /**
614
745
  * A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
615
746
  * 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.
747
+ * path in the reference form (`/a.md`, §1 "Paths"), the same two keys every hit
748
+ * carries — never the store row itself.
617
749
  * Returns undefined for anything that is not a tagged row (run.ts leaves those
618
750
  * alone).
619
751
  */
@@ -624,7 +756,7 @@ export function rowRef(value) {
624
756
  const r = value;
625
757
  const id = t === "docs" ? r.doc_id : t === "blocks" ? r.block_id : t === "nodes" ? r.node_id : r.edge_id;
626
758
  const path = t === "docs" ? r.path : r.__path;
627
- return { id: String(id), path: String(path ?? "") };
759
+ return { id: String(id), path: path == null ? "" : referencePath(String(path)) };
628
760
  }
629
761
  export { TARGET };
630
762
  //# sourceMappingURL=context.js.map
@@ -1,7 +1,8 @@
1
- import type { Query, Where, QueryPlanner, Plan } from "@omgbase/oqx";
1
+ import type { Query, Expr, Where, QueryPlanner, Plan } from "@omgbase/oqx";
2
2
  import type { Store } from "../core/store/store.js";
3
3
  import { type StoreContextOptions } from "./context.js";
4
4
  import { type Target } from "./sql/translate.js";
5
+ export declare function rootTarget(source: Expr): Target | null;
5
6
  /**
6
7
  * True when evaluating `where` in memory could raise an OQX error the pushed
7
8
  * conjuncts might hide by emptying the scan. The walk is generic over the whole
@@ -14,6 +15,12 @@ import { type Target } from "./sql/translate.js";
14
15
  * • a bare reserved docs basename (`id path updated_at content_hash body`): as
15
16
  * an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
16
17
  * on any target at any depth (the reach-through row is a doc)
18
+ * • a bare target name (`docs blocks nodes edges`, root-row.ts) read as a
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 edges),
21
+ * as an `ident` at any depth below (the rows there are unknown), or as
22
+ * `<head>.<name>` unless the head is a reach-through whose row has that
23
+ * relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
17
24
  *
18
25
  * A residual made only of comparisons, logical operators, `in`/ranges, `!`,
19
26
  * literals, bindings and plain reads (including exists/none/count/collect/first
@@ -17,49 +17,15 @@
17
17
  import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
18
18
  import { makeStoreContext, tagRows } from "./context.js";
19
19
  import { translatePredicate, RESERVED_DOC_BASENAMES } from "./sql/translate.js";
20
- const ALIAS = {
21
- docs: { self: "d", doc: "d" },
22
- blocks: { self: "b", doc: "d" },
23
- nodes: { self: "n", doc: "d" },
24
- edges: { self: "e", doc: "d" },
25
- };
26
- const FROM = {
27
- docs: "docs d",
28
- blocks: "blocks b JOIN docs d ON d.doc_id = b.doc_id",
29
- nodes: "nodes n JOIN docs d ON d.doc_id = n.doc_id",
30
- edges: "edges e JOIN docs d ON d.doc_id = e.src_doc",
31
- };
32
- // Row columns + the owning-doc path as __path (matches the context roots so
33
- // produced rows are indistinguishable from a full scan's).
34
- const COLS = {
35
- docs: "d.*",
36
- blocks: "b.*, d.path AS __path",
37
- nodes: "n.*, d.path AS __path",
38
- edges: "e.*, d.path AS __path",
39
- };
40
- const ORDER = {
41
- docs: "d.path, d.doc_id",
42
- blocks: "d.path, b.block_id",
43
- nodes: "d.path, n.node_id",
44
- edges: "d.path, e.edge_id",
45
- };
46
- function guards(target, repoId) {
47
- switch (target) {
48
- case "docs": return { sql: "d.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
49
- case "blocks": return { sql: "b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
50
- case "nodes": return { sql: "n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
51
- case "edges": return { sql: "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
52
- }
53
- }
20
+ import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
21
+ import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
54
22
  // The root collection a query scans, if it is a bare `docs|blocks|nodes|edges`
55
- // or `$repo.<target>` source (else null — not a pushable shape).
56
- function rootTarget(source) {
57
- const TARGETS = new Set(["docs", "blocks", "nodes", "edges"]);
23
+ // source at the root scope (else null — not a pushable shape; a nested `^docs`
24
+ // is a block's receiver, never the query's source). run.ts also asks: a bare
25
+ // root scan's rows are store rows by construction.
26
+ export function rootTarget(source) {
58
27
  if (source.kind === "ident" && TARGETS.has(source.name))
59
28
  return source.name;
60
- if (source.kind === "member" && source.recv.kind === "ident" && source.recv.name === "$repo" && TARGETS.has(source.name)) {
61
- return source.name;
62
- }
63
29
  return null;
64
30
  }
65
31
  // ---- the residual walk (decline (c)) -----------------------------------------
@@ -75,6 +41,12 @@ function rootTarget(source) {
75
41
  * • a bare reserved docs basename (`id path updated_at content_hash body`): as
76
42
  * an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
77
43
  * on any target at any depth (the reach-through row is a doc)
44
+ * • a bare target name (`docs blocks nodes edges`, root-row.ts) read as a
45
+ * property: as an `ident` at the ROOT scope when it is not a relation of
46
+ * the target's rows (`blocks` on blocks, `nodes` on nodes, any on edges),
47
+ * as an `ident` at any depth below (the rows there are unknown), or as
48
+ * `<head>.<name>` unless the head is a reach-through whose row has that
49
+ * relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
78
50
  *
79
51
  * A residual made only of comparisons, logical operators, `in`/ranges, `!`,
80
52
  * literals, bindings and plain reads (including exists/none/count/collect/first
@@ -133,13 +105,21 @@ function exprMayRaise(e, target, root) {
133
105
  switch (e.kind) {
134
106
  case "call": return true;
135
107
  case "outer": return true;
136
- case "ident": return root && target === "docs" && RESERVED_DOC_BASENAMES.has(e.name);
108
+ case "ident":
109
+ if (TARGETS.has(e.name))
110
+ return !root || !isTargetRelationOf(target, e.name);
111
+ return root && target === "docs" && RESERVED_DOC_BASENAMES.has(e.name);
137
112
  case "member":
138
113
  if (e.recv.kind === "ident" && e.recv.name === "doc" && RESERVED_DOC_BASENAMES.has(e.name))
139
114
  return true;
115
+ if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
116
+ return true;
140
117
  return exprMayRaise(e.recv, target, root);
141
- case "index": return exprMayRaise(e.recv, target, root) || exprMayRaise(e.index, target, root);
142
118
  case "unary": return exprMayRaise(e.expr, target, root);
119
+ // `x!` raises when absent; a value-position directive may `single`-fail or
120
+ // read anything its block reads.
121
+ case "required": return true;
122
+ case "op": return opMayRaise(e, target, root);
143
123
  case "binary":
144
124
  case "logical":
145
125
  case "in":
@@ -0,0 +1,21 @@
1
+ export type Target = "docs" | "blocks" | "nodes" | "edges";
2
+ export declare const TARGETS: ReadonlySet<string>;
3
+ /** The target names that are a relation of each target's rows (§1.2). */
4
+ export declare const TARGET_RELATIONS: Readonly<Record<Target, ReadonlySet<string>>>;
5
+ /** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`). */
6
+ export declare const REACH_THROUGH: Readonly<Record<string, Target>>;
7
+ export declare const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 \u2014 reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^$id` / `0^$id`";
8
+ /**
9
+ * The bare-target error. `depth` is the scope depth the name was read at when
10
+ * known statically (the carets in the hint count it); `null` when raised at read
11
+ * time, where the hint spells out the rule instead.
12
+ */
13
+ export declare function bareTargetMessage(name: string, depth: number | null): string;
14
+ /**
15
+ * The past-the-root error: `^docs` at the root scope (more carets than there
16
+ * are enclosing scopes) would read absent and count 0 — refused, naming the
17
+ * bare spelling. `levels` is the caret count written.
18
+ */
19
+ export declare function pastRootMessage(name: string, levels: number): string;
20
+ /** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
21
+ export declare function isTargetRelationOf(target: Target | undefined, name: string): boolean;
@@ -0,0 +1,55 @@
1
+ // The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
2
+ // root row: from a top-level row `^docs` reaches the documents (one caret per
3
+ // enclosing block, or the absolute `0^docs`), `^$id` the repository id, and the
4
+ // root object (`^$it`) exposes the four collections. Two spellings are refused
5
+ // as `filter_invalid`, with the messages below, in both engines:
6
+ //
7
+ // • `$repo` (removed in 2.0) anywhere — bare, as a member head (`$repo.docs`),
8
+ // `^$repo`, `0^$repo` — statically, before any row is read, so the error is
9
+ // deterministic whether or not a row would have reached it (run.ts
10
+ // `checkRootSpellings`);
11
+ // • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
12
+ // row's clauses), where a bare name reads a property of the current row,
13
+ // which has none: `docs` and `edges` are no row's relation, so they are
14
+ // refused statically by the same walk; `blocks` and `nodes` ARE relations
15
+ // of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
16
+ // `section.blocks`), so the context raises the same error at read time on a
17
+ // row that lacks the relation (context.ts). A frontmatter key named after a
18
+ // target is unreachable by bare name (use `frontmatter.<k>`).
19
+ export const TARGETS = new Set(["docs", "blocks", "nodes", "edges"]);
20
+ /** The target names that are a relation of each target's rows (§1.2). */
21
+ export const TARGET_RELATIONS = {
22
+ docs: new Set(["nodes", "blocks"]),
23
+ blocks: new Set(["nodes"]),
24
+ nodes: new Set(["blocks"]),
25
+ edges: new Set(),
26
+ };
27
+ /** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`). */
28
+ export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes" };
29
+ const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", edges: "edges" };
30
+ export const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^$id` / `0^$id`";
31
+ /**
32
+ * The bare-target error. `depth` is the scope depth the name was read at when
33
+ * known statically (the carets in the hint count it); `null` when raised at read
34
+ * time, where the hint spells out the rule instead.
35
+ */
36
+ export function bareTargetMessage(name, depth) {
37
+ const t = name;
38
+ const carets = "^".repeat(depth ?? 1);
39
+ const how = depth === null ? `; one caret per enclosing block, or the absolute \`0^${name}\`` : "";
40
+ return `\`${name}\` inside a block reads a property of the current row, which has none — did you mean \`${carets}${name}\` (the repository's ${NOUN[t]}${how})?`;
41
+ }
42
+ /**
43
+ * The past-the-root error: `^docs` at the root scope (more carets than there
44
+ * are enclosing scopes) would read absent and count 0 — refused, naming the
45
+ * bare spelling. `levels` is the caret count written.
46
+ */
47
+ export function pastRootMessage(name, levels) {
48
+ const t = name;
49
+ return `\`${"^".repeat(levels)}${name}\` reaches past the root — there is no enclosing row at this depth; at the top level the repository's ${NOUN[t]} are the bare \`${name}\` (\`${name} count { … }\`, \`from ${name}\`, \`entries(${name})\`)`;
50
+ }
51
+ /** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
52
+ export function isTargetRelationOf(target, name) {
53
+ return target !== undefined && TARGET_RELATIONS[target].has(name);
54
+ }
55
+ //# sourceMappingURL=root-row.js.map
@@ -1,4 +1,4 @@
1
- import type { Query } from "@omgbase/oqx";
1
+ import type { Query, Expr } from "@omgbase/oqx";
2
2
  import type { Store } from "../core/store/store.js";
3
3
  import type { SemanticVec } from "../search/cel/compile.js";
4
4
  export type OqxConsumer = "collect" | "count" | "exists" | "none" | "first" | "single";
@@ -34,6 +34,8 @@ export interface OqxOptions {
34
34
  * the differential conformance suite to prove planned == in-memory. */
35
35
  plan?: boolean;
36
36
  }
37
+ export declare function rootPathLiterals(e: Expr): Expr;
38
+ export declare function checkRootSpellings(q: Query): void;
37
39
  export declare function collectSemanticPhrases(source: string): string[];
38
40
  export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
39
41
  export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;