@omgbase/core 0.10.1 → 0.11.1

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 (63) hide show
  1. package/README.md +10 -10
  2. package/dist/src/core/read/document.d.ts +18 -5
  3. package/dist/src/core/read/document.js +21 -7
  4. package/dist/src/core/read/nodes.d.ts +7 -7
  5. package/dist/src/core/read/nodes.js +5 -3
  6. package/dist/src/core/read/reader.d.ts +11 -7
  7. package/dist/src/core/read/reader.js +8 -10
  8. package/dist/src/format/adapter.d.ts +1 -1
  9. package/dist/src/graph/history.d.ts +20 -14
  10. package/dist/src/graph/history.js +12 -12
  11. package/dist/src/graph/inbound-links.d.ts +4 -4
  12. package/dist/src/graph/inbound-links.js +4 -4
  13. package/dist/src/graph/link-health.d.ts +8 -8
  14. package/dist/src/graph/link-health.js +6 -6
  15. package/dist/src/graph/links.d.ts +3 -3
  16. package/dist/src/graph/links.js +4 -4
  17. package/dist/src/index.d.ts +2 -1
  18. package/dist/src/index.js +2 -1
  19. package/dist/src/mcp/arguments.d.ts +27 -0
  20. package/dist/src/mcp/arguments.js +147 -0
  21. package/dist/src/mcp/errors.d.ts +1 -1
  22. package/dist/src/mcp/graph.d.ts +16 -9
  23. package/dist/src/mcp/graph.js +58 -35
  24. package/dist/src/mcp/reference.d.ts +1 -1
  25. package/dist/src/mcp/reference.js +200 -111
  26. package/dist/src/mcp/server.d.ts +9 -1
  27. package/dist/src/mcp/server.js +359 -268
  28. package/dist/src/mutate/apply.d.ts +11 -4
  29. package/dist/src/mutate/apply.js +3 -2
  30. package/dist/src/mutate/docs.js +7 -7
  31. package/dist/src/mutate/macros.d.ts +4 -1
  32. package/dist/src/mutate/macros.js +1 -1
  33. package/dist/src/mutate/ops.js +2 -2
  34. package/dist/src/mutate/opset.d.ts +8 -5
  35. package/dist/src/mutate/plan-update.js +9 -8
  36. package/dist/src/oqx-js/context.d.ts +17 -4
  37. package/dist/src/oqx-js/context.js +268 -131
  38. package/dist/src/oqx-js/planner.d.ts +3 -3
  39. package/dist/src/oqx-js/planner.js +14 -15
  40. package/dist/src/oqx-js/root-row.d.ts +10 -3
  41. package/dist/src/oqx-js/root-row.js +18 -8
  42. package/dist/src/oqx-js/run.d.ts +6 -0
  43. package/dist/src/oqx-js/run.js +72 -25
  44. package/dist/src/oqx-js/sql/scan.js +7 -7
  45. package/dist/src/oqx-js/sql/translate.d.ts +12 -9
  46. package/dist/src/oqx-js/sql/translate.js +183 -75
  47. package/dist/src/oqx-js/store-index.d.ts +1 -1
  48. package/dist/src/oqx-js/store-index.js +28 -26
  49. package/dist/src/search/cel/ast.d.ts +1 -1
  50. package/dist/src/search/cel/compile.d.ts +1 -1
  51. package/dist/src/search/cel/compile.js +43 -70
  52. package/dist/src/search/cel/parser.js +1 -1
  53. package/dist/src/search/resolve.d.ts +2 -0
  54. package/dist/src/search/rrf.js +2 -2
  55. package/dist/src/search/text.d.ts +2 -2
  56. package/dist/src/search/text.js +1 -1
  57. package/dist/src/spec-versions.d.ts +6 -6
  58. package/dist/src/spec-versions.js +5 -5
  59. package/dist/src/surface-paths.d.ts +41 -8
  60. package/dist/src/surface-paths.js +34 -18
  61. package/dist/src/sync/admin.d.ts +2 -1
  62. package/dist/src/sync/admin.js +1 -1
  63. package/package.json +2 -2
@@ -10,16 +10,16 @@
10
10
  // are planned; everything else declines to a full in-memory run. One more
11
11
  // decline keeps the planned path invisible (spec/surface §1 decline (c)): a
12
12
  // pushed conjunct that empties the scan would also hide an OQX eval error a
13
- // residual conjunct raises in memory (`path == "x" && $path == "nope.md"` was
13
+ // residual conjunct raises in memory (`path == "x" && path == "nope.md"` was
14
14
  // `[]` planned, `filter_invalid` in memory), so when any residual conjunct could
15
15
  // raise — a function or method call, a `single` block, a `^`-escaped name, or a
16
16
  // bare reserved docs basename — the WHOLE query runs unplanned.
17
17
  import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
18
18
  import { makeStoreContext, tagRows } from "./context.js";
19
- import { translatePredicate, RESERVED_DOC_BASENAMES } from "./sql/translate.js";
19
+ import { translatePredicate } from "./sql/translate.js";
20
20
  import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
21
21
  import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
22
- // The root collection a query scans, if it is a bare `docs|blocks|nodes|edges`
22
+ // The root collection a query scans, if it is a bare `docs|blocks|nodes|links`
23
23
  // source at the root scope (else null — not a pushable shape; a nested `^docs`
24
24
  // is a block's receiver, never the query's source). run.ts also asks: a bare
25
25
  // root scan's rows are store rows by construction.
@@ -38,12 +38,12 @@ export function rootTarget(source) {
38
38
  * • any `call` node (a function or method: unknown name, bad regex, wrong target…)
39
39
  * • any nested block whose consumer is `single` (raises on 0 or >1 rows)
40
40
  * • any `^`-escaped name: an `outer` reference or a `^name:` lift
41
- * • a bare reserved docs basename (`id path updated_at content_hash body`): as
41
+ * • a bare reserved docs basename (`id path updated_at hash raw`): as
42
42
  * an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
43
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
44
+ * • a bare target name (`docs blocks nodes links`, root-row.ts) read as a
45
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),
46
+ * the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
47
47
  * as an `ident` at any depth below (the rows there are unknown), or as
48
48
  * `<head>.<name>` unless the head is a reach-through whose row has that
49
49
  * relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
@@ -86,6 +86,8 @@ function subMayRaise(s, target) {
86
86
  }
87
87
  if (s.orderBy?.some((o) => exprMayRaise(o.expr, target, false)))
88
88
  return true;
89
+ if (s.frontier && exprMayRaise(s.frontier, target, false))
90
+ return true;
89
91
  if (s.follow && followMayRaise(s.follow, target))
90
92
  return true;
91
93
  if (s.limit && exprMayRaise(s.limit, target, false))
@@ -95,23 +97,18 @@ function subMayRaise(s, target) {
95
97
  return false;
96
98
  }
97
99
  // A destination is a relation Expr read in the frontier row's scope, or a
98
- // destination block (an OpNode) whose receiver and body may each raise.
100
+ // destination block (an OpNode) whose receiver, body, `frontier` and `depth`
101
+ // are its own (oqx 0.20).
99
102
  function followMayRaise(f, target) {
100
- if (f.destinations.some((d) => (d.kind === "op" ? opMayRaise(d, target, false) : exprMayRaise(d, target, false))))
101
- return true;
102
- return [f.where, f.frontier, f.by].some((e) => e !== null && exprMayRaise(e, target, false));
103
+ return f.destinations.some((d) => (d.kind === "op" ? opMayRaise(d, target, false) : exprMayRaise(d, target, false)));
103
104
  }
104
105
  function exprMayRaise(e, target, root) {
105
106
  switch (e.kind) {
106
107
  case "call": return true;
107
108
  case "outer": return true;
108
109
  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);
110
+ return TARGETS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
112
111
  case "member":
113
- if (e.recv.kind === "ident" && e.recv.name === "doc" && RESERVED_DOC_BASENAMES.has(e.name))
114
- return true;
115
112
  if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
116
113
  return true;
117
114
  return exprMayRaise(e.recv, target, root);
@@ -126,6 +123,8 @@ function exprMayRaise(e, target, root) {
126
123
  return exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
127
124
  case "range":
128
125
  return (e.lo !== null && exprMayRaise(e.lo, target, root)) || (e.hi !== null && exprMayRaise(e.hi, target, root));
126
+ // A tuple (oqx 0.20) is never pushed down; it raises iff an element does.
127
+ case "tuple": return e.items.some((x) => exprMayRaise(x, target, root));
129
128
  case "lit":
130
129
  case "binding": return false;
131
130
  }
@@ -1,10 +1,10 @@
1
- export type Target = "docs" | "blocks" | "nodes" | "edges";
1
+ export type Target = "docs" | "blocks" | "nodes" | "links";
2
2
  export declare const TARGETS: ReadonlySet<string>;
3
3
  /** The target names that are a relation of each target's rows (§1.2). */
4
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`). */
5
+ /** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
6
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`";
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
8
  /**
9
9
  * The bare-target error. `depth` is the scope depth the name was read at when
10
10
  * known statically (the carets in the hint count it); `null` when raised at read
@@ -17,5 +17,12 @@ export declare function bareTargetMessage(name: string, depth: number | null): s
17
17
  * bare spelling. `levels` is the caret count written.
18
18
  */
19
19
  export declare function pastRootMessage(name: string, levels: number): string;
20
+ /**
21
+ * The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
22
+ * is not one of the four collections would read an absent root and return an
23
+ * empty result — refused instead, naming the collections; `edges`, the links
24
+ * target's name before 3.0, gets its new name spelled out.
25
+ */
26
+ export declare function unknownTargetMessage(name: string): string;
20
27
  /** 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
28
  export declare function isTargetRelationOf(target: Target | undefined, name: string): boolean;
@@ -1,6 +1,6 @@
1
1
  // The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
2
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
3
+ // enclosing block, or the absolute `0^docs`), `^id` the repository id, and the
4
4
  // root object (`^$it`) exposes the four collections. Two spellings are refused
5
5
  // as `filter_invalid`, with the messages below, in both engines:
6
6
  //
@@ -10,24 +10,24 @@
10
10
  // `checkRootSpellings`);
11
11
  // • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
12
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
13
+ // which has none: `docs` and `links` are no row's relation, so they are
14
14
  // refused statically by the same walk; `blocks` and `nodes` ARE relations
15
15
  // of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
16
16
  // `section.blocks`), so the context raises the same error at read time on a
17
17
  // row that lacks the relation (context.ts). A frontmatter key named after a
18
18
  // target is unreachable by bare name (use `frontmatter.<k>`).
19
- export const TARGETS = new Set(["docs", "blocks", "nodes", "edges"]);
19
+ export const TARGETS = new Set(["docs", "blocks", "nodes", "links"]);
20
20
  /** The target names that are a relation of each target's rows (§1.2). */
21
21
  export const TARGET_RELATIONS = {
22
22
  docs: new Set(["nodes", "blocks"]),
23
23
  blocks: new Set(["nodes"]),
24
24
  nodes: new Set(["blocks"]),
25
- edges: new Set(),
25
+ links: new Set(),
26
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`";
27
+ /** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
28
+ export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes", dst: "docs" };
29
+ const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", links: "links" };
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
31
  /**
32
32
  * The bare-target error. `depth` is the scope depth the name was read at when
33
33
  * known statically (the carets in the hint count it); `null` when raised at read
@@ -48,6 +48,16 @@ export function pastRootMessage(name, levels) {
48
48
  const t = name;
49
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
50
  }
51
+ /**
52
+ * The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
53
+ * is not one of the four collections would read an absent root and return an
54
+ * empty result — refused instead, naming the collections; `edges`, the links
55
+ * target's name before 3.0, gets its new name spelled out.
56
+ */
57
+ export function unknownTargetMessage(name) {
58
+ const was = name === "edges" ? " (`edges` was renamed `links` in surface 3.0)" : "";
59
+ return `\`${name}\` is not a target — the repository's collections are \`docs\`, \`blocks\`, \`nodes\`, \`links\`${was}`;
60
+ }
51
61
  /** 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
62
  export function isTargetRelationOf(target, name) {
53
63
  return target !== undefined && TARGET_RELATIONS[target].has(name);
@@ -6,6 +6,12 @@ export type EmbedQuery = (text: string) => Promise<{
6
6
  model: string;
7
7
  vec: Float32Array;
8
8
  }>;
9
+ /** A query hit (spec/surface §1.4, 3.0): the row's identity under the SAME
10
+ * bare names the expression language reads — `id`, `path` — plus whatever
11
+ * `select` projected. A key copied from a hit is valid in the next `where` with
12
+ * no translation (`where id == "<hit.id>"`); a `select id, path` is simply the
13
+ * envelope, and any other projection whose output key would be `id` or `path`
14
+ * must be aliased (`envelopeCollision`). */
9
15
  export interface OqxHit {
10
16
  id: string;
11
17
  path: string;
@@ -1,12 +1,13 @@
1
1
  // omgbase's OQX entry point, served by `@omgbase/oqx`: parse the source, run it
2
2
  // through the in-memory engine over a store-backed DataContext (context.ts), and
3
- // shape the engine's result into omgbase's OqxResult (lean {id, path, …} hits,
4
- // keyset pagination, count/exists scalars). Same signature + shape as the former
3
+ // shape the engine's result into omgbase's OqxResult (lean {id, path, …} hits
4
+ // — the envelope keys ARE the attributes, spec/surface §1.4 3.0 — keyset
5
+ // pagination, count/exists scalars). Same signature + shape as the former
5
6
  // in-tree compiler, so every caller and the corpus are unchanged.
6
7
  import { parse, PlannedEngine, InMemoryEngine, OqxError, resolveAliases, transform, visit, build, semantics } from "@omgbase/oqx";
7
8
  import { makeStoreContext, rowRef } from "./context.js";
8
9
  import { SQLiteQueryPlanner, rootTarget } from "./planner.js";
9
- import { REPO_REMOVED_MESSAGE, TARGETS, bareTargetMessage, pastRootMessage } from "./root-row.js";
10
+ import { REPO_REMOVED_MESSAGE, TARGETS, bareTargetMessage, pastRootMessage, unknownTargetMessage } from "./root-row.js";
10
11
  import { FilterInvalid } from "../search/cel/parser.js";
11
12
  import { float32ToBlob } from "../core/vec.js";
12
13
  import { encodeCursor as encodeKeyset, decodePathCursor } from "../core/cursor.js";
@@ -24,15 +25,25 @@ const ROW_FNS = new Set([
24
25
  "yaml_path", "json_pointer", "has_edge", "has_anchor", "child_count", "parent_type",
25
26
  ]);
26
27
  // Paths on the surface are the reference form (spec/surface §1 "Paths", 2.0):
27
- // `$path` and `$dst_path` read `/a/b.md`. A string literal a query compares
28
- // with one of them — `$path == "a.md"`, `"a.md" != $dst_path`,
29
- // `$path.startsWith("lab/")` — is rooted first, so both spellings match; a
30
- // property or binding compared with a path is never touched (it is the
31
- // author's value). The planner sees the rewritten tree too, so the pushed SQL
32
- // and the in-memory engine agree by construction.
33
- const PATH_INTRINSICS = new Set(["$path", "$dst_path"]);
28
+ // `path` and the edge field `dst_path` read `/a/b.md`. A string literal a query
29
+ // compares with one of them — `path == "a.md"`, `"a.md" != dst_path`,
30
+ // `path.startsWith("lab/")`, `doc.path == "a.md"` — is rooted first, so both
31
+ // spellings match; a property or binding compared with a path is never touched
32
+ // (it is the author's value), and neither is an authored `path:` key read
33
+ // through a source handle (`data.path`, `frontmatter.path`, `inline.path`): a
34
+ // member read is a path read only through a ROW handle (`doc`, `block`, `dst`,
35
+ // `$it`).
36
+ // The planner sees the rewritten tree too, so the pushed SQL and the in-memory
37
+ // engine agree by construction.
38
+ const PATH_ATTRIBUTES = new Set(["path", "dst_path"]);
39
+ const ROW_HANDLES = new Set(["doc", "block", "dst", "$it"]);
34
40
  function isPathRead(e) {
35
- return (e.kind === "ident" || e.kind === "outer" || e.kind === "member") && PATH_INTRINSICS.has(e.name);
41
+ if (e.kind === "ident" || e.kind === "outer")
42
+ return PATH_ATTRIBUTES.has(e.name);
43
+ if (e.kind === "member" && PATH_ATTRIBUTES.has(e.name)) {
44
+ return (e.recv.kind === "ident" || e.recv.kind === "outer") && ROW_HANDLES.has(e.recv.name);
45
+ }
46
+ return false;
36
47
  }
37
48
  function rootLiteral(e) {
38
49
  return e.kind === "lit" && typeof e.value === "string" ? { ...e, value: referencePath(e.value) } : e;
@@ -50,7 +61,7 @@ export function rootPathLiterals(e) {
50
61
  return e;
51
62
  }
52
63
  // The root row (root-row.ts, spec/surface §1.1, 2.0): `$repo` anywhere, and a
53
- // bare `docs`/`edges` at scope depth ≥ 1 (a property read of a row that has
64
+ // bare `docs`/`links` at scope depth ≥ 1 (a property read of a row that has
54
65
  // none), are refused before evaluation — one `visit` over the parsed tree, so
55
66
  // the error does not depend on which rows the data happens to hold and the
56
67
  // planned and in-memory paths agree trivially. `blocks`/`nodes` are relations of
@@ -59,13 +70,18 @@ export function rootPathLiterals(e) {
59
70
  // top level) would read absent and count 0: refused too, naming the bare
60
71
  // spelling.
61
72
  export function checkRootSpellings(q) {
73
+ // A top-level `from <name>` that is no collection (`from edges`, the links
74
+ // target's name before 3.0) would scan an absent root and come back empty:
75
+ // refused, naming the four collections (root-row.ts).
76
+ if (q.source.kind === "ident" && !TARGETS.has(q.source.name))
77
+ throw new FilterInvalid(unknownTargetMessage(q.source.name), "OQX");
62
78
  visit(q, {
63
79
  enter(node, ctx) {
64
80
  if (node.kind !== "ident" && node.kind !== "outer")
65
81
  return;
66
82
  if (node.name === "$repo")
67
83
  throw new FilterInvalid(REPO_REMOVED_MESSAGE, "OQX");
68
- if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "edges")) {
84
+ if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "links")) {
69
85
  throw new FilterInvalid(bareTargetMessage(node.name, ctx.depth), "OQX");
70
86
  }
71
87
  if (node.kind === "outer" && TARGETS.has(node.name) && node.levels > ctx.depth) {
@@ -110,13 +126,34 @@ export function collectSemanticPhrases(source) {
110
126
  });
111
127
  return [...phrases];
112
128
  }
113
- const ID_ITEM = build.field("__oqx_id", build.ident("$id"));
114
- const PATH_ITEM = build.field("__oqx_path", build.ident("$path"));
129
+ const ID_ITEM = build.field("__oqx_id", build.ident("id"));
130
+ const PATH_ITEM = build.field("__oqx_path", build.ident("path"));
131
+ // Result projections may not overwrite the envelope (spec/surface §1.4, 3.0):
132
+ // a projection whose output key is `id` or `path` is allowed only when it IS
133
+ // the envelope's own selection — the bare attribute of the same name, which
134
+ // collapses onto the envelope key (`select id, path` is simply the envelope) —
135
+ // and is otherwise a loud error asking for an alias: `select data.path` must be
136
+ // `select p: data.path`, `select path: doc.layer` likewise. Checked over the
137
+ // resolved items (an alias is just a name here), before the engine runs, so the
138
+ // error never depends on the rows. A `values` projection returns no hits, so
139
+ // the envelope — and the rule — do not apply.
140
+ const ENVELOPE_KEYS = new Set(["id", "path"]);
141
+ function envelopeCollision(items) {
142
+ for (const item of items) {
143
+ if (!ENVELOPE_KEYS.has(item.name))
144
+ continue;
145
+ if (item.kind === "field" && item.expr.kind === "ident" && item.expr.name === item.name)
146
+ continue;
147
+ const hint = item.name === "path" ? "p: data.path" : "i: data.id";
148
+ return new FilterInvalid(`projection \`${item.name}\` collides with the hit envelope — alias it (\`${hint}\`)`, "OQX");
149
+ }
150
+ return null;
151
+ }
115
152
  // A hit is a store row (spec/surface §1.4, 1.5). The engine's top-level rows
116
153
  // are store rows by construction when the source is a bare root scan and
117
154
  // nothing re-projects them; a `follow` destination (`follow before` over a
118
155
  // frontmatter list of paths), a `from E` re-projection or any other source can
119
- // reach a scalar, which the injected `$id`/`$path` reads would render as the
156
+ // reach a scalar, which the injected `id`/`path` reads would render as the
120
157
  // junk hit `{ id: "undefined", path: "" }`. For those queries the row itself is
121
158
  // projected too (`$it`), and `toHit` fails the query when it is not a row. Only
122
159
  // then: a row clone per hit is free here but not in the Rust port, and a root
@@ -143,7 +180,7 @@ function describeValue(v) {
143
180
  return "an object";
144
181
  }
145
182
  function notAStoreRow(v) {
146
- return new FilterInvalid(`a hit must be a document, block, node or edge row — the query reached ${describeValue(v)}; to follow document references held in a property use refs(<field>)`, "OQX");
183
+ return new FilterInvalid(`a hit must be a document, block, node or link row — the query reached ${describeValue(v)}; to follow document references held in a property use refs(<field>)`, "OQX");
147
184
  }
148
185
  // The reserved key a top-level `values` projection's single item is renamed to,
149
186
  // so it rides through id/path injection, keyset paging, and distinct as an
@@ -155,9 +192,11 @@ const VALUE_KEY = "__oqx_value";
155
192
  // spec/oqx §12), a `values` projection of `$it`/`$self`, a field bound to a
156
193
  // row — and a store row is not a wire shape (column names, `attrs` as a JSON
157
194
  // string, the `__path` join column, Buffers). Walk the projected value and
158
- // render every tagged row as `{ id, path }`; everything else passes through
159
- // untouched (plain records and arrays are descended, scalars kept). Hits are
160
- // records the engine built from the injected id/path items, never rows.
195
+ // render every tagged row as `{ id, path }` — the hit envelope's own keys
196
+ // (3.0), one notation for "a document reference" everywhere a row shape
197
+ // appears in a result; everything else passes through untouched (plain records
198
+ // and arrays are descended, scalars kept). Hits are records the engine built
199
+ // from the injected id/path items, never rows.
161
200
  function renderValue(v) {
162
201
  if (v === null || typeof v !== "object")
163
202
  return v;
@@ -179,11 +218,13 @@ function toHit(row) {
179
218
  const { __oqx_id, __oqx_path, __oqx_self, ...rest } = o;
180
219
  if (SELF_KEY in o && !rowRef(__oqx_self))
181
220
  throw notAStoreRow(__oqx_self);
221
+ // The envelope keys lead; a user projection of `id`/`path` collapses onto
222
+ // them in place (the same value, read by the same attribute).
182
223
  return { id: String(__oqx_id), path: String(__oqx_path ?? ""), ...renderValue(rest) };
183
224
  }
184
225
  // Top-level `select distinct`: dedup hits by their USER projection (every field
185
- // except the injected id/path), keeping the first (so the retained hit carries a
186
- // real id/path). Order-preserving.
226
+ // except the envelope's `id`/`path`), keeping the first (so the retained hit
227
+ // carries a real identity). Order-preserving.
187
228
  function dedupHitsByProjection(hits) {
188
229
  const seen = new Set();
189
230
  const out = [];
@@ -248,6 +289,11 @@ function oqxRunInner(store, repoId, source, opts) {
248
289
  // the final page below and returned as `values` with `hits` empty.
249
290
  const topValues = parsed.values;
250
291
  const userSelect = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
292
+ if (!topValues) {
293
+ const collision = envelopeCollision(userSelect);
294
+ if (collision)
295
+ throw collision;
296
+ }
251
297
  // On the collect path a top-level `limit`/`offset` is ALSO taken out of the
252
298
  // engine query and applied here, after the runner's own distinct — the engine
253
299
  // would otherwise bound the raw rows before dedup (`select distinct type
@@ -302,9 +348,10 @@ export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery)
302
348
  }
303
349
  return oqxRun(store, repoId, source, { ...opts, semanticVectors });
304
350
  }
305
- // The collect page's keyset is (path, id) — the hit's path, so the reference
306
- // form (`/a.md`): the kernel's shared cursor encoding (core/cursor.ts) with a
307
- // two-part tuple. A malformed cursor is CursorInvalid, which the MCP layer maps
351
+ // The collect page's keyset is (path, id) — the hit's `path`, so the reference
352
+ // form (`/a.md`), and its `id`: the kernel's shared cursor encoding
353
+ // (core/cursor.ts) with a two-part tuple of VALUES (no key names ride in it, so
354
+ // a 2.x cursor stays valid on 3.0). A malformed cursor is CursorInvalid, which the MCP layer maps
308
355
  // to filter_invalid; so is a cursor whose path is not rooted — one issued by a
309
356
  // 1.x surface, whose keyset would otherwise sort before every rooted hit and
310
357
  // silently replay the first page (spec/surface §1.4, 2.0).
@@ -9,13 +9,13 @@ export const ALIAS = {
9
9
  docs: { self: "d", doc: "d" },
10
10
  blocks: { self: "b", doc: "d" },
11
11
  nodes: { self: "n", doc: "d" },
12
- edges: { self: "e", doc: "d" },
12
+ links: { self: "e", doc: "d" },
13
13
  };
14
14
  export const FROM = {
15
15
  docs: "docs d",
16
16
  blocks: "blocks b JOIN docs d ON d.doc_id = b.doc_id",
17
17
  nodes: "nodes n JOIN docs d ON d.doc_id = n.doc_id",
18
- edges: "edges e JOIN docs d ON d.doc_id = e.src_doc",
18
+ links: "edges e JOIN docs d ON d.doc_id = e.src_doc",
19
19
  };
20
20
  /** Row columns + the owning-doc path as `__path` (matches the context roots so
21
21
  * produced rows are indistinguishable from a full scan's). */
@@ -23,7 +23,7 @@ export const COLS = {
23
23
  docs: "d.*",
24
24
  blocks: "b.*, d.path AS __path",
25
25
  nodes: "n.*, d.path AS __path",
26
- edges: "e.*, d.path AS __path",
26
+ links: "e.*, d.path AS __path",
27
27
  };
28
28
  /** The joined targets driven FROM the document: the same rows and columns as
29
29
  * `FROM`, with the loop order fixed by `CROSS JOIN` so a predicate on the
@@ -32,13 +32,13 @@ export const COLS = {
32
32
  export const FROM_BY_DOC = {
33
33
  blocks: "docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id",
34
34
  nodes: "docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id",
35
- edges: "docs d CROSS JOIN edges e ON e.src_doc = d.doc_id",
35
+ links: "docs d CROSS JOIN edges e ON e.src_doc = d.doc_id",
36
36
  };
37
37
  export const ORDER = {
38
38
  docs: "d.path, d.doc_id",
39
39
  blocks: "d.path, b.block_id",
40
40
  nodes: "d.path, n.node_id",
41
- edges: "d.path, e.edge_id",
41
+ links: "d.path, e.edge_id",
42
42
  };
43
43
  /** The live-row and repository guards of a target scan. */
44
44
  export function guards(target, repoId) {
@@ -46,7 +46,7 @@ export function guards(target, repoId) {
46
46
  case "docs": return { sql: "d.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
47
47
  case "blocks": return { sql: "b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
48
48
  case "nodes": return { sql: "n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
49
- case "edges": return { sql: "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
49
+ case "links": return { sql: "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
50
50
  }
51
51
  }
52
52
  /** `guards` for a `FROM_BY_DOC` statement: the same tests, with the document's
@@ -56,7 +56,7 @@ export function guardsByDoc(target, repoId) {
56
56
  switch (target) {
57
57
  case "blocks": return { sql: "d.repo_id = ? AND +b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId, repoId] };
58
58
  case "nodes": return { sql: "d.repo_id = ? AND +n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId, repoId] };
59
- case "edges": return { sql: "d.repo_id = ? AND +e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId, repoId] };
59
+ case "links": return { sql: "d.repo_id = ? AND +e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId, repoId] };
60
60
  }
61
61
  }
62
62
  //# sourceMappingURL=scan.js.map
@@ -1,6 +1,6 @@
1
1
  import type { Expr } from "@omgbase/oqx";
2
2
  /** SQL row domain, matching the store's tables. */
3
- export type Target = "docs" | "blocks" | "nodes" | "edges";
3
+ export type Target = "docs" | "blocks" | "nodes" | "links";
4
4
  /** The SQL aliases the compiler assigned to the current scope's row (`self`) and
5
5
  * its owning document (`doc`). On the `docs` target the row IS the document, so
6
6
  * both are the same alias. */
@@ -24,7 +24,7 @@ export interface Frag {
24
24
  * decline (a)):
25
25
  *
26
26
  * text a string literal/binding, a text column or intrinsic, `.lower()`/`.upper()`
27
- * int an integer-typed intrinsic column (blocks `$ordinal`, `$depth`)
27
+ * int an integer-typed column (blocks `ordinal`, `depth`)
28
28
  * num a number literal/binding
29
29
  * bool a boolean literal/binding
30
30
  * null a null (or undefined) literal/binding
@@ -43,12 +43,16 @@ export interface Operand extends Frag {
43
43
  col: string;
44
44
  path: string;
45
45
  };
46
+ /** A property read: `docId` is the SQL expression naming the document whose
47
+ * rows are read (`d.doc_id` for the row's own document, `e.dst_node` for a
48
+ * link's destination). */
46
49
  prop?: {
47
- docAlias: string;
50
+ docId: string;
48
51
  key: string;
52
+ source?: string;
49
53
  };
50
54
  value?: unknown;
51
- /** The storage-form path column behind a `$path` / `doc.$path` read
55
+ /** The storage-form path column behind a `path` / `doc.path` read
52
56
  * (`d.path`): the operand's SQL is the rooted `'/' || <col>`, and an
53
57
  * equality against a rooted text constant is re-shaped onto the bare,
54
58
  * indexed column (`pathEquality`). */
@@ -66,10 +70,9 @@ export interface Operand extends Frag {
66
70
  export declare const NON_PROPERTY_NAMES: Readonly<Record<Target, ReadonlySet<string>>>;
67
71
  /** The rooted path read over a storage path column. */
68
72
  export declare function pathColumn(col: string): Operand;
69
- /** docs intrinsics whose BARE (non-$) form is a loud error in-memory (10 §2) — not
70
- * pushable, so the residual raises it (and the planner keeps the whole query
71
- * unplanned when one is residual: spec/surface §1 decline (c)). */
72
- export declare const RESERVED_DOC_BASENAMES: ReadonlySet<string>;
73
+ /** The identity/bytes/time attributes per kind — the names `attributeSql` owns
74
+ * (the structural columns are handled per target in `translateOperand`). */
75
+ export declare const ROW_ATTRIBUTES: Readonly<Record<Target, ReadonlySet<string>>>;
73
76
  /** Translate an expression used as a VALUE (comparison operand, method receiver,
74
77
  * function argument) to a SQL scalar with its operand kind. Returns null if not
75
78
  * faithfully translatable. */
@@ -100,7 +103,7 @@ export declare function translateValue(e: Expr, ctx: TranslateCtx): Frag | null;
100
103
  * ordered, exactly as in memory.
101
104
  * Every other pair declines: SQLite sees JSON `true` and `1`, `val_bool` and
102
105
  * `val_num` alike (`$ordinal == checked`, `true == 1`), orders every
103
- * integer before every text (`$ordinal < "3"`, `level < "x"`, `$path > 5`),
106
+ * integer before every text (`$ordinal < "3"`, `level < "x"`, `path > 5`),
104
107
  * and two JSON or property reads carry no type at plan time
105
108
  * (`attrs.a == attrs.b`, `era == stages`).
106
109
  *