@omgbase/core 0.1.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cli/eval-matcher.d.ts +13 -0
  3. package/dist/src/cli/eval-matcher.js +36 -0
  4. package/dist/src/core/attach.d.ts +5 -0
  5. package/dist/src/core/attach.js +38 -0
  6. package/dist/src/core/cursor.d.ts +11 -0
  7. package/dist/src/core/cursor.js +35 -0
  8. package/dist/src/core/hash.d.ts +23 -0
  9. package/dist/src/core/hash.js +82 -0
  10. package/dist/src/core/ids.d.ts +6 -0
  11. package/dist/src/core/ids.js +26 -0
  12. package/dist/src/core/index.d.ts +1 -0
  13. package/dist/src/core/index.js +2 -0
  14. package/dist/src/core/ingest.d.ts +43 -0
  15. package/dist/src/core/ingest.js +287 -0
  16. package/dist/src/core/order-key.d.ts +8 -0
  17. package/dist/src/core/order-key.js +72 -0
  18. package/dist/src/core/parse/parse.d.ts +3 -0
  19. package/dist/src/core/parse/parse.js +104 -0
  20. package/dist/src/core/parse/render.d.ts +3 -0
  21. package/dist/src/core/parse/render.js +20 -0
  22. package/dist/src/core/parse/tree.d.ts +8 -0
  23. package/dist/src/core/parse/tree.js +43 -0
  24. package/dist/src/core/parse/types.d.ts +52 -0
  25. package/dist/src/core/parse/types.js +9 -0
  26. package/dist/src/core/read/document.d.ts +107 -0
  27. package/dist/src/core/read/document.js +191 -0
  28. package/dist/src/core/read/nodes.d.ts +47 -0
  29. package/dist/src/core/read/nodes.js +102 -0
  30. package/dist/src/core/read/outline.d.ts +12 -0
  31. package/dist/src/core/read/outline.js +76 -0
  32. package/dist/src/core/read/reader.d.ts +125 -0
  33. package/dist/src/core/read/reader.js +206 -0
  34. package/dist/src/core/read/refs.d.ts +15 -0
  35. package/dist/src/core/read/refs.js +51 -0
  36. package/dist/src/core/store/edges.d.ts +26 -0
  37. package/dist/src/core/store/edges.js +101 -0
  38. package/dist/src/core/store/fts.d.ts +10 -0
  39. package/dist/src/core/store/fts.js +22 -0
  40. package/dist/src/core/store/gc.d.ts +18 -0
  41. package/dist/src/core/store/gc.js +69 -0
  42. package/dist/src/core/store/nodes.d.ts +5 -0
  43. package/dist/src/core/store/nodes.js +57 -0
  44. package/dist/src/core/store/properties.d.ts +38 -0
  45. package/dist/src/core/store/properties.js +177 -0
  46. package/dist/src/core/store/rebuild.d.ts +3 -0
  47. package/dist/src/core/store/rebuild.js +29 -0
  48. package/dist/src/core/store/schema.d.ts +9 -0
  49. package/dist/src/core/store/schema.js +386 -0
  50. package/dist/src/core/store/sections.d.ts +2 -0
  51. package/dist/src/core/store/sections.js +29 -0
  52. package/dist/src/core/store/store.d.ts +24 -0
  53. package/dist/src/core/store/store.js +164 -0
  54. package/dist/src/core/store/writers.d.ts +52 -0
  55. package/dist/src/core/store/writers.js +89 -0
  56. package/dist/src/core/vec.d.ts +10 -0
  57. package/dist/src/core/vec.js +33 -0
  58. package/dist/src/format/adapter.d.ts +97 -0
  59. package/dist/src/format/adapter.js +14 -0
  60. package/dist/src/format/index.d.ts +6 -0
  61. package/dist/src/format/index.js +15 -0
  62. package/dist/src/format/json.d.ts +3 -0
  63. package/dist/src/format/json.js +324 -0
  64. package/dist/src/format/markdown.d.ts +5 -0
  65. package/dist/src/format/markdown.js +217 -0
  66. package/dist/src/format/registry.d.ts +6 -0
  67. package/dist/src/format/registry.js +24 -0
  68. package/dist/src/format/yaml.d.ts +5 -0
  69. package/dist/src/format/yaml.js +413 -0
  70. package/dist/src/graph/extract.d.ts +38 -0
  71. package/dist/src/graph/extract.js +242 -0
  72. package/dist/src/graph/history.d.ts +103 -0
  73. package/dist/src/graph/history.js +210 -0
  74. package/dist/src/graph/inbound-links.d.ts +33 -0
  75. package/dist/src/graph/inbound-links.js +130 -0
  76. package/dist/src/graph/link-destinations.d.ts +22 -0
  77. package/dist/src/graph/link-destinations.js +84 -0
  78. package/dist/src/graph/link-health.d.ts +79 -0
  79. package/dist/src/graph/link-health.js +146 -0
  80. package/dist/src/graph/links.d.ts +23 -0
  81. package/dist/src/graph/links.js +42 -0
  82. package/dist/src/index.d.ts +60 -0
  83. package/dist/src/index.js +70 -0
  84. package/dist/src/mcp/errors.d.ts +17 -0
  85. package/dist/src/mcp/errors.js +18 -0
  86. package/dist/src/mcp/graph.d.ts +63 -0
  87. package/dist/src/mcp/graph.js +212 -0
  88. package/dist/src/mcp/reference.d.ts +1 -0
  89. package/dist/src/mcp/reference.js +227 -0
  90. package/dist/src/mcp/server.d.ts +30 -0
  91. package/dist/src/mcp/server.js +1076 -0
  92. package/dist/src/mcp/stdio.d.ts +13 -0
  93. package/dist/src/mcp/stdio.js +24 -0
  94. package/dist/src/migrate/mrplex.d.ts +27 -0
  95. package/dist/src/migrate/mrplex.js +29 -0
  96. package/dist/src/mutate/apply.d.ts +83 -0
  97. package/dist/src/mutate/apply.js +302 -0
  98. package/dist/src/mutate/doc-store.d.ts +49 -0
  99. package/dist/src/mutate/doc-store.js +71 -0
  100. package/dist/src/mutate/docs.d.ts +60 -0
  101. package/dist/src/mutate/docs.js +232 -0
  102. package/dist/src/mutate/known-ids.d.ts +12 -0
  103. package/dist/src/mutate/known-ids.js +154 -0
  104. package/dist/src/mutate/load.d.ts +3 -0
  105. package/dist/src/mutate/load.js +66 -0
  106. package/dist/src/mutate/lower.d.ts +32 -0
  107. package/dist/src/mutate/lower.js +232 -0
  108. package/dist/src/mutate/macros.d.ts +91 -0
  109. package/dist/src/mutate/macros.js +230 -0
  110. package/dist/src/mutate/ops.d.ts +40 -0
  111. package/dist/src/mutate/ops.js +513 -0
  112. package/dist/src/mutate/opset.d.ts +92 -0
  113. package/dist/src/mutate/opset.js +79 -0
  114. package/dist/src/mutate/plan-update.d.ts +51 -0
  115. package/dist/src/mutate/plan-update.js +187 -0
  116. package/dist/src/mutate/tree.d.ts +46 -0
  117. package/dist/src/mutate/tree.js +167 -0
  118. package/dist/src/oqx/run.d.ts +1 -0
  119. package/dist/src/oqx/run.js +6 -0
  120. package/dist/src/oqx-js/context.d.ts +23 -0
  121. package/dist/src/oqx-js/context.js +564 -0
  122. package/dist/src/oqx-js/planner.d.ts +10 -0
  123. package/dist/src/oqx-js/planner.js +91 -0
  124. package/dist/src/oqx-js/run.d.ts +41 -0
  125. package/dist/src/oqx-js/run.js +332 -0
  126. package/dist/src/oqx-js/sql/translate.d.ts +27 -0
  127. package/dist/src/oqx-js/sql/translate.js +290 -0
  128. package/dist/src/reconcile/crossdoc.d.ts +34 -0
  129. package/dist/src/reconcile/crossdoc.js +76 -0
  130. package/dist/src/reconcile/eval/generator.d.ts +21 -0
  131. package/dist/src/reconcile/eval/generator.js +129 -0
  132. package/dist/src/reconcile/eval/metrics.d.ts +32 -0
  133. package/dist/src/reconcile/eval/metrics.js +74 -0
  134. package/dist/src/reconcile/flatten.d.ts +12 -0
  135. package/dist/src/reconcile/flatten.js +37 -0
  136. package/dist/src/reconcile/phase5.d.ts +8 -0
  137. package/dist/src/reconcile/phase5.js +167 -0
  138. package/dist/src/reconcile/phase6a.d.ts +2 -0
  139. package/dist/src/reconcile/phase6a.js +159 -0
  140. package/dist/src/reconcile/phases.d.ts +28 -0
  141. package/dist/src/reconcile/phases.js +0 -0
  142. package/dist/src/reconcile/reconcile.d.ts +21 -0
  143. package/dist/src/reconcile/reconcile.js +115 -0
  144. package/dist/src/reconcile/similarity.d.ts +7 -0
  145. package/dist/src/reconcile/similarity.js +41 -0
  146. package/dist/src/reconcile/types.d.ts +46 -0
  147. package/dist/src/reconcile/types.js +19 -0
  148. package/dist/src/search/cel/ast.d.ts +74 -0
  149. package/dist/src/search/cel/ast.js +3 -0
  150. package/dist/src/search/cel/compile.d.ts +60 -0
  151. package/dist/src/search/cel/compile.js +836 -0
  152. package/dist/src/search/cel/lexer.d.ts +9 -0
  153. package/dist/src/search/cel/lexer.js +139 -0
  154. package/dist/src/search/cel/parser.d.ts +7 -0
  155. package/dist/src/search/cel/parser.js +219 -0
  156. package/dist/src/search/drain.d.ts +39 -0
  157. package/dist/src/search/drain.js +97 -0
  158. package/dist/src/search/embeddings.d.ts +131 -0
  159. package/dist/src/search/embeddings.js +263 -0
  160. package/dist/src/search/external.d.ts +13 -0
  161. package/dist/src/search/external.js +177 -0
  162. package/dist/src/search/fts-query.d.ts +1 -0
  163. package/dist/src/search/fts-query.js +64 -0
  164. package/dist/src/search/provider.d.ts +17 -0
  165. package/dist/src/search/provider.js +13 -0
  166. package/dist/src/search/resolve.d.ts +18 -0
  167. package/dist/src/search/resolve.js +23 -0
  168. package/dist/src/search/rrf.d.ts +36 -0
  169. package/dist/src/search/rrf.js +84 -0
  170. package/dist/src/search/tasks.d.ts +12 -0
  171. package/dist/src/search/tasks.js +131 -0
  172. package/dist/src/search/text.d.ts +16 -0
  173. package/dist/src/search/text.js +21 -0
  174. package/dist/src/search/vector.d.ts +18 -0
  175. package/dist/src/search/vector.js +49 -0
  176. package/dist/src/sync/admin.d.ts +59 -0
  177. package/dist/src/sync/admin.js +53 -0
  178. package/dist/src/sync/attach.d.ts +13 -0
  179. package/dist/src/sync/attach.js +29 -0
  180. package/dist/src/sync/checkpoint.d.ts +21 -0
  181. package/dist/src/sync/checkpoint.js +52 -0
  182. package/dist/src/sync/driver.d.ts +25 -0
  183. package/dist/src/sync/driver.js +80 -0
  184. package/dist/src/sync/external-source.d.ts +14 -0
  185. package/dist/src/sync/external-source.js +137 -0
  186. package/dist/src/sync/freshness.d.ts +42 -0
  187. package/dist/src/sync/freshness.js +152 -0
  188. package/dist/src/sync/fs-util.d.ts +12 -0
  189. package/dist/src/sync/fs-util.js +55 -0
  190. package/dist/src/sync/git-heuristics.d.ts +6 -0
  191. package/dist/src/sync/git-heuristics.js +16 -0
  192. package/dist/src/sync/observe.d.ts +63 -0
  193. package/dist/src/sync/observe.js +84 -0
  194. package/dist/src/sync/plugin.d.ts +54 -0
  195. package/dist/src/sync/plugin.js +12 -0
  196. package/dist/src/sync/reconciling-ingest.d.ts +12 -0
  197. package/dist/src/sync/reconciling-ingest.js +209 -0
  198. package/dist/src/sync/recovery.d.ts +6 -0
  199. package/dist/src/sync/recovery.js +34 -0
  200. package/dist/src/sync/settings.d.ts +18 -0
  201. package/dist/src/sync/settings.js +58 -0
  202. package/dist/src/sync/sources.d.ts +37 -0
  203. package/dist/src/sync/sources.js +75 -0
  204. package/dist/src/sync/tombstone.d.ts +9 -0
  205. package/dist/src/sync/tombstone.js +55 -0
  206. package/dist/src/sync/watch-lease.d.ts +14 -0
  207. package/dist/src/sync/watch-lease.js +91 -0
  208. package/dist/src/sync/watcher.d.ts +19 -0
  209. package/dist/src/sync/watcher.js +34 -0
  210. package/dist/src/sync/workspace.d.ts +34 -0
  211. package/dist/src/sync/workspace.js +120 -0
  212. package/dist/src/sync/writer-lock.d.ts +16 -0
  213. package/dist/src/sync/writer-lock.js +121 -0
  214. package/package.json +80 -0
@@ -0,0 +1,836 @@
1
+ import { FilterInvalid } from "./parser.js";
2
+ import { sanitizeFtsQuery } from "../fts-query.js";
3
+ function resolveOuter(ctx, name) {
4
+ const b = ctx.outer?.(name);
5
+ if (!b) {
6
+ throw new FilterInvalid(`no binding ^${name} one scope outward — a ^ reference reads a name bound in the immediately enclosing query scope`, "OQX ^");
7
+ }
8
+ return b;
9
+ }
10
+ export function defaultCtx(target) {
11
+ if (target === "docs")
12
+ return { self: "d", doc: "d" };
13
+ if (target === "blocks")
14
+ return { self: "b", doc: "d" };
15
+ if (target === "edges")
16
+ return { self: "e", doc: "d" };
17
+ return { self: "n", doc: "d" };
18
+ }
19
+ // Build a JSON1 path literal from parsed identifier segments. Segments come
20
+ // from the lexer's identifier rule ([A-Za-z_][A-Za-z0-9_]*), so they are safe
21
+ // to inline — no user free-text reaches this. Inlining (rather than binding)
22
+ // keeps field expressions parameter-free, so repeating an expression in SQL
23
+ // never desynchronizes the bound-param list.
24
+ function jsonPath(segs) {
25
+ for (const s of segs) {
26
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) {
27
+ throw new FilterInvalid(`invalid field segment '${s}'`, "10 §2");
28
+ }
29
+ }
30
+ return `'$.${segs.join(".")}'`;
31
+ }
32
+ // ---- properties routing (properties-table) -------------------------------
33
+ // Documents-target fields (and doc.<k> reach-through from blocks/nodes) resolve
34
+ // against the indexed `properties` table instead of json_extract over a JSON
35
+ // blob. A field access is one of: a scalar-value expression (comparisons), or
36
+ // an existence/count predicate (membership, size, has, bool-context). All
37
+ // correlate on `<doc>.doc_id` (the correlated docs row, present on every target).
38
+ const PROP_SOURCES = new Set(["frontmatter", "inline", "computed"]);
39
+ // Computed properties surfaced as document intrinsics (12 §4). Engine-derived,
40
+ // stored as source='computed' rows keyed by their $ name. Filterable like any
41
+ // property (scalar ==, list(), membership) but never shadow authored keys.
42
+ const COMPUTED_INTRINSICS = new Set(["$title", "$tags"]);
43
+ // Base names of the document intrinsics ($id, $path, ...) MINUS their `$`. On
44
+ // the docs open namespace (and doc.<k> reach-through), a BARE first segment
45
+ // equal to one of these is almost always a typo for the intrinsic: it would
46
+ // otherwise resolve to an absent frontmatter key and silently match nothing
47
+ // (a misleading empty). We reject it with a hint instead. `title`/`tags` are
48
+ // deliberately EXCLUDED: their $-forms are `computed` intrinsics that do not
49
+ // shadow authored frontmatter, so bare `title`/`tags` remain legitimate key
50
+ // access (see COMPUTED_INTRINSICS and QUERY_SYNTAX). A source-scoped form
51
+ // (frontmatter.path / inline.path) is an explicit request for that property
52
+ // and is likewise not caught — only the bare first-segment case is the trap.
53
+ const RESERVED_INTRINSIC_BASENAMES = new Set([
54
+ "id", "path", "repo", "updated_at", "content_hash", "body",
55
+ ]);
56
+ // Guard the docs open namespace against reserved-intrinsic collisions. `segs`
57
+ // are the property key segments (after any doc./source scope has been peeled).
58
+ // Throws when the FIRST segment shadows an intrinsic base name.
59
+ function guardReservedCollision(segs) {
60
+ if (RESERVED_INTRINSIC_BASENAMES.has(segs[0])) {
61
+ const name = segs[0];
62
+ throw new FilterInvalid(`bare '${name}' reads a frontmatter key; did you mean the intrinsic $${name}? (use frontmatter.${name} to force the property)`, "10 §2");
63
+ }
64
+ }
65
+ // Validate + join identifier segments into a dotted property key. Segments come
66
+ // from the lexer's identifier rule, so inlining is injection-safe (mirrors
67
+ // jsonPath); keeping keys param-free preserves the "fields carry no params"
68
+ // invariant the comparison/membership param ordering relies on.
69
+ function propKey(segs) {
70
+ for (const s of segs) {
71
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) {
72
+ throw new FilterInvalid(`invalid field segment '${s}'`, "10 §2");
73
+ }
74
+ }
75
+ return segs.join(".");
76
+ }
77
+ // Is this field a properties-routed document property? Returns {key, source?}
78
+ // or null (intrinsic, `format`, or a non-doc field). A leading
79
+ // frontmatter./inline./computed. segment (with something after it) is a
80
+ // source scope, not a key segment.
81
+ function propRef(field, target) {
82
+ let segs = field.segments;
83
+ if (target === "docs") {
84
+ // Computed intrinsic ($title, $tags): route to source='computed' rows.
85
+ if (field.intrinsic) {
86
+ if (segs.length === 1 && COMPUTED_INTRINSICS.has(segs[0]))
87
+ return { key: segs[0], source: "computed" };
88
+ return null;
89
+ }
90
+ if (segs[0] === "format")
91
+ return null;
92
+ }
93
+ else {
94
+ if (field.intrinsic)
95
+ return null;
96
+ // blocks / nodes: only doc.<k> reach-through routes to properties.
97
+ if (segs[0] !== "doc")
98
+ return null;
99
+ segs = segs.slice(1);
100
+ // doc.$title reach-through to a computed intrinsic.
101
+ if (segs.length === 1 && COMPUTED_INTRINSICS.has(segs[0]))
102
+ return { key: segs[0], source: "computed" };
103
+ if (segs.length === 0 || segs[0].startsWith("$") || segs[0] === "format")
104
+ return null;
105
+ }
106
+ if (segs.length >= 2 && PROP_SOURCES.has(segs[0])) {
107
+ const source = segs[0];
108
+ return { key: propKey(segs.slice(1)), source };
109
+ }
110
+ // Bare (non-`$`, non-source-scoped) key: reject a first segment that shadows
111
+ // a reserved intrinsic base name (path/id/repo/...) so a typo for `$path`
112
+ // fails loud instead of silently resolving to an absent frontmatter key.
113
+ guardReservedCollision(segs);
114
+ return { key: propKey(segs) };
115
+ }
116
+ function srcClause(source, alias = "p") {
117
+ return source ? ` AND ${alias}.source = '${source}'` : "";
118
+ }
119
+ // Scalar value of a property for comparisons: the scalar-authored value, but
120
+ // ONLY when the key is single-valued in the queried scope. A key resolves to a
121
+ // scalar iff it has exactly one row in scope AND that row is card='scalar'.
122
+ // This makes:
123
+ // - a lone inline `element:: fire` (card='scalar', 1 row) comparable;
124
+ // - a YAML list `tags: [a]` (1 row, card='list') NOT scalar-equal;
125
+ // - a frontmatter+inline collision on a bare key (≥2 rows in the union scope)
126
+ // fall back to list semantics — `job == "farmer"` is false, you must use
127
+ // `"farmer" in list(job)`.
128
+ // The inner COUNT spans ALL cards in scope so multiplicity — not just authored
129
+ // shape — governs scalar eligibility. NULL (⇒ false) when the gate fails.
130
+ function propScalarExpr(ref, ctx) {
131
+ return `(SELECT COALESCE(p.val_text, p.val_num, p.val_bool) FROM properties p
132
+ WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}' AND p.card = 'scalar'${srcClause(ref.source)}
133
+ AND p.deleted_commit IS NULL
134
+ AND (SELECT COUNT(*) FROM properties p2
135
+ WHERE p2.doc_id = ${ctx.doc}.doc_id AND p2.key = '${ref.key}'${srcClause(ref.source, "p2")}
136
+ AND p2.deleted_commit IS NULL) = 1
137
+ LIMIT 1)`;
138
+ }
139
+ // EXISTS over ALL rows for the key (any card) whose value equals ? — the
140
+ // membership primitive. Spans scalar and list rows, so `"canon" in list(layer)`
141
+ // matches a scalar frontmatter value and `"a" in list(tags)` matches a list.
142
+ function propMemberExists(ref, ctx) {
143
+ return `EXISTS (SELECT 1 FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
144
+ AND p.deleted_commit IS NULL AND COALESCE(p.val_text, p.val_num, p.val_bool) = ?)`;
145
+ }
146
+ // COUNT of all rows for the key (size(list(k))): 0 when absent, 1 for a scalar,
147
+ // N for a list — matching json_array_length / scalar=1 / null=0 today.
148
+ function propCountExpr(ref, ctx) {
149
+ return `(SELECT COUNT(*) FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
150
+ AND p.deleted_commit IS NULL)`;
151
+ }
152
+ // Existence of the key in any card (has(k)).
153
+ function propHasExists(ref, ctx) {
154
+ return `EXISTS (SELECT 1 FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
155
+ AND p.deleted_commit IS NULL)`;
156
+ }
157
+ // Truthy in boolean position: present list row, or present scalar with a
158
+ // truthy value (not 0/''/false). Absent ⇒ false; `!k` negates.
159
+ function propTruthyExists(ref, ctx) {
160
+ return `EXISTS (SELECT 1 FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
161
+ AND p.deleted_commit IS NULL
162
+ AND (p.card = 'list' OR COALESCE(p.val_text, p.val_num, p.val_bool) NOT IN (0, '', 'false')))`;
163
+ }
164
+ // Column/JSON accessor for a field on a given target. Returns a SQL scalar
165
+ // expression that is NULL when the field is absent. Never carries params.
166
+ function fieldSql(field, target, ctx) {
167
+ const segs = field.segments;
168
+ const head = segs[0];
169
+ const self = ctx.self;
170
+ const doc = ctx.doc;
171
+ // Recursion intrinsics ($depth/$stop/$leaf/$frontier) resolve to the walk's
172
+ // materialized columns when a follow query's post-walk scope is active. Routed
173
+ // before the per-target intrinsic switch so `$depth` means RECURSION depth here
174
+ // (shadowing block-nesting depth). Only a bare intrinsic (`$stop`, not
175
+ // `$stop.x`) is a recursion column. Param-free, so the field param invariant holds.
176
+ if (field.intrinsic && ctx.recur && segs.length === 1) {
177
+ switch (head) {
178
+ case "$depth": return { expr: ctx.recur.depth };
179
+ case "$stop": return { expr: ctx.recur.stop };
180
+ case "$leaf": return { expr: ctx.recur.leaf };
181
+ case "$frontier": return { expr: ctx.recur.frontier };
182
+ case "$ordinal": return { expr: ctx.recur.ordinal };
183
+ default: break;
184
+ }
185
+ }
186
+ if (field.intrinsic) {
187
+ if (target === "docs") {
188
+ switch (head) {
189
+ case "$id": return { expr: `${self}.doc_id` };
190
+ case "$path": return { expr: `${self}.path` };
191
+ case "$repo": return { expr: `${self}.repo_id` };
192
+ case "$updated_at": return { expr: `(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ${self}.current_rev)` };
193
+ case "$content_hash": return { expr: `lower(hex(${self}.file_hash))` };
194
+ default: {
195
+ // Computed property intrinsic ($title, $tags) → properties row scalar.
196
+ const ref = propRef(field, target);
197
+ if (ref)
198
+ return { expr: propScalarExpr(ref, ctx) };
199
+ throw new FilterInvalid(`unknown intrinsic ${head} on docs`, "10 §2");
200
+ }
201
+ }
202
+ }
203
+ else if (target === "nodes") {
204
+ switch (head) {
205
+ case "$id": return { expr: `${self}.node_id` };
206
+ case "$node_id": return { expr: `${self}.node_id` };
207
+ case "$doc_id": return { expr: `${self}.doc_id` };
208
+ case "$block_id": return { expr: `${self}.block_id` };
209
+ case "$path": return { expr: `${doc}.path` };
210
+ default: throw new FilterInvalid(`unknown intrinsic ${head} on nodes`, "10 §2");
211
+ }
212
+ }
213
+ else if (target === "edges") {
214
+ switch (head) {
215
+ case "$id": return { expr: `${self}.edge_id` };
216
+ case "$src": return { expr: `${self}.src_doc` };
217
+ case "$dst": return { expr: `${self}.dst_node` };
218
+ case "$src_block": return { expr: `${self}.src_block` };
219
+ case "$via": return { expr: `${self}.via_node` };
220
+ case "$from_commit": return { expr: `${self}.from_commit` };
221
+ case "$path": return { expr: `${doc}.path` }; // source document's path
222
+ // Meaningful destination, resolved per dst_kind (cheap correlated lookups,
223
+ // NULL when not applicable): the target document's path, or the external
224
+ // node's URI. Lets `from edges` show real targets, not opaque node ids.
225
+ case "$dst_path": return { expr: `(SELECT dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)` };
226
+ case "$dst_uri": return { expr: `(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = ${self}.dst_node)` };
227
+ default: throw new FilterInvalid(`unknown intrinsic ${head} on edges`, "10 §2");
228
+ }
229
+ }
230
+ else {
231
+ switch (head) {
232
+ case "$id": return { expr: `${self}.block_id` };
233
+ case "$doc": return { expr: `${self}.doc_id` };
234
+ case "$path": return { expr: `${doc}.path` };
235
+ case "$ordinal": return { expr: `${self}.ordinal` };
236
+ case "$depth": return { expr: `${self}.depth` };
237
+ case "$content_hash": return { expr: `lower(hex(${self}.raw_hash))` };
238
+ case "$body": return { expr: `${self}.text` };
239
+ case "$updated_at": return { expr: `(SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ${self}.block_id)` };
240
+ default: throw new FilterInvalid(`unknown intrinsic ${head} on blocks`, "10 §2");
241
+ }
242
+ }
243
+ }
244
+ if (target === "docs") {
245
+ if (head === "format")
246
+ return { expr: `${self}.format` };
247
+ const ref = propRef(field, target);
248
+ if (ref)
249
+ return { expr: propScalarExpr(ref, ctx) };
250
+ return { expr: `json_extract(${self}.metadata, ${jsonPath(segs)})` };
251
+ }
252
+ if (target === "nodes") {
253
+ if (head === "kind")
254
+ return { expr: `${self}.kind` };
255
+ if (head === "name")
256
+ return { expr: `${self}.name` };
257
+ if (head === "value")
258
+ return { expr: `${self}.value` };
259
+ if (head === "attrs") {
260
+ return { expr: `json_extract(${self}.attrs, ${jsonPath(segs.slice(1))})` };
261
+ }
262
+ if (head === "doc") {
263
+ const rest = segs.slice(1);
264
+ if (rest[0]?.startsWith("$")) {
265
+ return fieldSql({ kind: "field", segments: rest, intrinsic: true }, "docs", { self: doc, doc });
266
+ }
267
+ if (rest[0] === "format")
268
+ return { expr: `${doc}.format` };
269
+ const ref = propRef(field, target);
270
+ if (ref)
271
+ return { expr: propScalarExpr(ref, ctx) };
272
+ return { expr: `json_extract(${doc}.metadata, ${jsonPath(rest)})` };
273
+ }
274
+ if (head === "block") {
275
+ const rest = segs.slice(1);
276
+ if (rest[0] === "type")
277
+ return { expr: `(SELECT bb.type FROM blocks bb WHERE bb.block_id = ${self}.block_id)` };
278
+ if (rest[0] === "text")
279
+ return { expr: `(SELECT bb.text FROM blocks bb WHERE bb.block_id = ${self}.block_id)` };
280
+ throw new FilterInvalid(`unknown block field '${rest.join(".")}' on nodes`, "10 §2");
281
+ }
282
+ throw new FilterInvalid(`unknown field '${segs.join(".")}' on nodes`, "10 §2");
283
+ }
284
+ if (target === "edges") {
285
+ if (head === "predicate")
286
+ return { expr: `${self}.predicate` };
287
+ if (head === "provenance")
288
+ return { expr: `${self}.provenance` };
289
+ if (head === "dst_kind")
290
+ return { expr: `${self}.dst_kind` };
291
+ if (head === "anchor")
292
+ return { expr: `${self}.anchor` };
293
+ if (head === "src_field")
294
+ return { expr: `${self}.src_field` };
295
+ if (head === "doc") {
296
+ // reach-through to the SOURCE document (edges join docs on src_doc).
297
+ const rest = segs.slice(1);
298
+ if (rest[0]?.startsWith("$")) {
299
+ return fieldSql({ kind: "field", segments: rest, intrinsic: true }, "docs", { self: doc, doc });
300
+ }
301
+ if (rest[0] === "format")
302
+ return { expr: `${doc}.format` };
303
+ const ref = propRef(field, target);
304
+ if (ref)
305
+ return { expr: propScalarExpr(ref, ctx) };
306
+ return { expr: `json_extract(${doc}.metadata, ${jsonPath(rest)})` };
307
+ }
308
+ throw new FilterInvalid(`unknown field '${segs.join(".")}' on edges`, "10 §2");
309
+ }
310
+ // blocks target
311
+ if (head === "type")
312
+ return { expr: `${self}.type` };
313
+ if (head === "text")
314
+ return { expr: `${self}.text` };
315
+ if (head === "attrs") {
316
+ return { expr: `json_extract(${self}.attrs, ${jsonPath(segs.slice(1))})` };
317
+ }
318
+ if (head === "doc") {
319
+ const rest = segs.slice(1);
320
+ if (rest[0]?.startsWith("$")) {
321
+ return fieldSql({ kind: "field", segments: rest, intrinsic: true }, "docs", { self: doc, doc });
322
+ }
323
+ if (rest[0] === "format")
324
+ return { expr: `${doc}.format` };
325
+ const ref = propRef(field, target);
326
+ if (ref)
327
+ return { expr: propScalarExpr(ref, ctx) };
328
+ return { expr: `json_extract(${doc}.metadata, ${jsonPath(rest)})` };
329
+ }
330
+ throw new FilterInvalid(`unknown field '${segs.join(".")}' on blocks`, "10 §2");
331
+ }
332
+ function literalParam(lit) {
333
+ return lit.value;
334
+ }
335
+ // A scalar-valued operand in a comparison: a field, or a value-returning call
336
+ // (size(list(f)), child_count(), parent_type()). Returns a NULL-when-absent
337
+ // SQL scalar expression + its params.
338
+ function scalarSql(node, target, ctx) {
339
+ if (node.kind === "field")
340
+ return { expr: fieldSql(node, target, ctx).expr, params: [] };
341
+ if (node.kind === "outerref") {
342
+ const b = resolveOuter(ctx, node.name);
343
+ if (!b.scalar) {
344
+ throw new FilterInvalid(`^${node.name} is a collection binding; use it as \`<value> in ^${node.name}\`, not as a scalar`, "OQX ^");
345
+ }
346
+ return b.scalar();
347
+ }
348
+ if (node.kind === "call") {
349
+ switch (node.name) {
350
+ case "semantic":
351
+ return compileSemantic(argString(node.args, 0), target, ctx);
352
+ case "size": {
353
+ const inner = node.args[0];
354
+ if (inner && inner.kind === "call" && inner.name === "list") {
355
+ const lf = argField(inner.args, 0);
356
+ const ref = propRef(lf, target);
357
+ if (ref)
358
+ return { expr: propCountExpr(ref, ctx), params: [] };
359
+ const f = fieldSql(lf, target, ctx);
360
+ return {
361
+ expr: `(CASE WHEN ${f.expr} IS NULL THEN 0
362
+ WHEN json_valid(${f.expr}) AND json_type(${f.expr})='array'
363
+ THEN json_array_length(${f.expr})
364
+ ELSE 1 END)`,
365
+ params: [],
366
+ };
367
+ }
368
+ if (inner && inner.kind === "field") {
369
+ return { expr: `length(${fieldSql(inner, target, ctx).expr})`, params: [] };
370
+ }
371
+ throw new FilterInvalid("size() takes a field or list(field)");
372
+ }
373
+ case "child_count":
374
+ requireBlocks(target, "child_count");
375
+ return { expr: `(SELECT COUNT(*) FROM blocks cb WHERE cb.parent_block = ${ctx.self}.block_id AND cb.deleted_commit IS NULL)`, params: [] };
376
+ case "parent_type":
377
+ requireBlocks(target, "parent_type");
378
+ return { expr: `(SELECT pb.type FROM blocks pb WHERE pb.block_id = ${ctx.self}.parent_block)`, params: [] };
379
+ default:
380
+ throw new FilterInvalid(`function '${node.name}()' is not a comparable value`, "10 §5");
381
+ }
382
+ }
383
+ throw new FilterInvalid("comparisons must be between a field/value and a literal", "10 §3.1");
384
+ }
385
+ function compileComparison(cmp, target, ctx) {
386
+ const rejectList = (n) => {
387
+ if (n.kind === "call" && n.name === "list") {
388
+ throw new FilterInvalid("list() is only valid inside `in`, size(), .all, .exists", "10 §4");
389
+ }
390
+ };
391
+ // Literal-on-one-side: the canonical form, kept byte-identical (a bound `?`).
392
+ if (cmp.right.kind === "literal" || cmp.left.kind === "literal") {
393
+ let scalar;
394
+ let litNode;
395
+ let op = cmp.op;
396
+ if (cmp.right.kind === "literal") {
397
+ scalar = cmp.left;
398
+ litNode = cmp.right;
399
+ }
400
+ else {
401
+ scalar = cmp.right;
402
+ litNode = cmp.left;
403
+ op = flip(op);
404
+ }
405
+ rejectList(scalar);
406
+ const s = scalarSql(scalar, target, ctx);
407
+ // NULL-safe: absent operands yield NULL; `NULL <op> x` is NULL (falsy),
408
+ // exactly the absence=false rule — including `!=` (10 §3.3 note).
409
+ return {
410
+ sql: `(${s.expr} ${SQL_OP[op]} ?) IS 1`,
411
+ params: [...s.params, coerce(litNode)],
412
+ };
413
+ }
414
+ // Neither side is a literal. This is only reachable via a `^name` correlation
415
+ // operand (`attrs.id == ^owner_id`) — the base `query` grammar still requires a
416
+ // literal (field-vs-field stays an error, unchanged behaviour).
417
+ if (cmp.left.kind !== "outerref" && cmp.right.kind !== "outerref") {
418
+ throw new FilterInvalid("one side of a comparison must be a literal", "10 §3.1");
419
+ }
420
+ rejectList(cmp.left);
421
+ rejectList(cmp.right);
422
+ const l = scalarSql(cmp.left, target, ctx);
423
+ const r = scalarSql(cmp.right, target, ctx);
424
+ return {
425
+ sql: `(${l.expr} ${SQL_OP[cmp.op]} ${r.expr}) IS 1`,
426
+ params: [...l.params, ...r.params],
427
+ };
428
+ }
429
+ const SQL_OP = {
430
+ "==": "=", "!=": "<>", "<": "<", "<=": "<=", ">": ">", ">=": ">=",
431
+ };
432
+ function flip(op) {
433
+ switch (op) {
434
+ case "<": return ">";
435
+ case "<=": return ">=";
436
+ case ">": return "<";
437
+ case ">=": return "<=";
438
+ default: return op;
439
+ }
440
+ }
441
+ function coerce(lit) {
442
+ if (lit.type === "bool")
443
+ return lit.value ? 1 : 0;
444
+ return literalParam(lit);
445
+ }
446
+ // A bare field in boolean position coerces to false when absent/false.
447
+ function compileBoolField(field, target, ctx) {
448
+ const ref = propRef(field, target);
449
+ if (ref)
450
+ return { sql: propTruthyExists(ref, ctx), params: [] };
451
+ const f = fieldSql(field, target, ctx);
452
+ // truthy: not null, not 0, not '', not false
453
+ return {
454
+ sql: `(${f.expr} IS NOT NULL AND ${f.expr} NOT IN (0, '', 'false'))`,
455
+ params: [],
456
+ };
457
+ }
458
+ // A value-expression fragment for OQX projection (`select name: <expr>`). Parses
459
+ // a single scalar CEL expression (a field or value-returning call) and returns a
460
+ // NULL-when-absent SQL scalar + params. Comparisons/booleans are not values and
461
+ // are rejected. The alias context is supplied by OQX per scope (defaults to the
462
+ // canonical per-target aliases).
463
+ export function scalarValue(node, target, ctx = defaultCtx(target)) {
464
+ return scalarSql(node, target, ctx);
465
+ }
466
+ export function compile(node, target, ctx = defaultCtx(target)) {
467
+ switch (node.kind) {
468
+ case "or": {
469
+ const l = compile(node.left, target, ctx);
470
+ const r = compile(node.right, target, ctx);
471
+ return { sql: `(${l.sql} OR ${r.sql})`, params: [...l.params, ...r.params] };
472
+ }
473
+ case "and": {
474
+ const l = compile(node.left, target, ctx);
475
+ const r = compile(node.right, target, ctx);
476
+ return { sql: `(${l.sql} AND ${r.sql})`, params: [...l.params, ...r.params] };
477
+ }
478
+ case "not": {
479
+ const inner = compile(node.operand, target, ctx);
480
+ return { sql: `(NOT (${inner.sql}))`, params: inner.params };
481
+ }
482
+ case "comparison":
483
+ return compileComparison(node, target, ctx);
484
+ case "membership":
485
+ return compileMembership(node.value, node.field, target, ctx);
486
+ case "outerMembership":
487
+ return compileOuterMembership(node.value, node.collection, target, ctx);
488
+ case "field":
489
+ return compileBoolField(node, target, ctx);
490
+ case "call":
491
+ return compileCall(node.name, node.args, target, ctx);
492
+ case "method":
493
+ return compileMethod(node, target, ctx);
494
+ default:
495
+ throw new FilterInvalid(`unsupported expression '${node.kind}' in this position`);
496
+ }
497
+ }
498
+ function compileMembership(value, field, target, ctx) {
499
+ // Documents-target (and doc.<k>) properties: membership is an indexed EXISTS
500
+ // over ALL rows for the key (any card) — scalar or list value equals v.
501
+ const ref = propRef(field, target);
502
+ if (ref)
503
+ return { sql: propMemberExists(ref, ctx), params: [coerce(value)] };
504
+ // "v" in list(field): field may be scalar or JSON array. Match either the
505
+ // scalar equals v, or (when the value is a JSON array) the array contains v.
506
+ // Absent ⇒ NULL ⇒ [] ⇒ false. json_each over a non-array/NULL yields no rows,
507
+ // so a single EXISTS covers the array case; the scalar case is a direct `=`.
508
+ const f = fieldSql(field, target, ctx);
509
+ const isArray = `(json_valid(${f.expr}) AND json_type(${f.expr}) = 'array')`;
510
+ const sql = `(
511
+ (NOT ${isArray} AND ${f.expr} = ?)
512
+ OR (${isArray} AND EXISTS (SELECT 1 FROM json_each(${f.expr}) je WHERE je.value = ?))
513
+ )`;
514
+ // param order matches expr occurrences: isArray(1), f=?, isArray(1), value, isArray in EXISTS(1), value
515
+ return { sql, params: [coerce(value), coerce(value)] };
516
+ }
517
+ // `<value> in ^name`: does the one-scope-outward collection (a json array, e.g.
518
+ // a lift) contain the current scope's value? json_each yields no rows over a
519
+ // NULL/non-array, so absence ⇒ false. The collection expression is resolved from
520
+ // the enclosing scope (it references the parent row's aliases, valid here as a
521
+ // correlated subquery); its params precede the value's in statement order.
522
+ function compileOuterMembership(value, ref, target, ctx) {
523
+ const b = resolveOuter(ctx, ref.name);
524
+ if (!b.collection) {
525
+ throw new FilterInvalid(`^${ref.name} is a scalar binding; membership needs a collection (a lift or a parent collect)`, "OQX ^");
526
+ }
527
+ const coll = b.collection();
528
+ let vExpr;
529
+ let vParams;
530
+ if (value.kind === "literal") {
531
+ vExpr = "?";
532
+ vParams = [coerce(value)];
533
+ }
534
+ else {
535
+ const v = scalarSql(value, target, ctx);
536
+ vExpr = v.expr;
537
+ vParams = v.params;
538
+ }
539
+ return {
540
+ sql: `EXISTS (SELECT 1 FROM json_each(${coll.expr}) je WHERE je.value = ${vExpr})`,
541
+ params: [...coll.params, ...vParams],
542
+ };
543
+ }
544
+ function argField(args, i) {
545
+ const a = args[i];
546
+ if (!a || a.kind !== "field")
547
+ throw new FilterInvalid("expected a field argument");
548
+ return a;
549
+ }
550
+ function argString(args, i) {
551
+ const a = args[i];
552
+ if (!a || a.kind !== "literal" || a.type !== "string")
553
+ throw new FilterInvalid("expected a string argument");
554
+ return a.value;
555
+ }
556
+ function compileCall(name, args, target, ctx) {
557
+ switch (name) {
558
+ case "has": {
559
+ const hf = argField(args, 0);
560
+ const ref = propRef(hf, target);
561
+ if (ref)
562
+ return { sql: propHasExists(ref, ctx), params: [] };
563
+ const f = fieldSql(hf, target, ctx);
564
+ return { sql: `(${f.expr} IS NOT NULL)`, params: [] };
565
+ }
566
+ case "size":
567
+ throw new FilterInvalid("size(...) must be compared, e.g. size(list(tags)) > 2", "10 §4");
568
+ case "semantic":
569
+ // semantic(...) yields a cosine SCORE, not a boolean; it must be compared
570
+ // (threshold prune) or projected — e.g. semantic("aurora") > 0.6.
571
+ throw new FilterInvalid('semantic(...) returns a score; compare it, e.g. semantic("x") > 0.6', "OQX semantic");
572
+ // structural functions (blocks target) — 10 §5
573
+ case "under":
574
+ requireBlocks(target, "under");
575
+ return compileUnder(argString(args, 0), ctx);
576
+ case "under_heading":
577
+ requireBlocks(target, "under_heading");
578
+ return compileUnderHeading(argString(args, 0), ctx);
579
+ case "within":
580
+ requireBlocks(target, "within");
581
+ return compileWithin(argString(args, 0), ctx);
582
+ case "text":
583
+ return compileText(argString(args, 0), target, ctx);
584
+ case "has_edge":
585
+ return compileHasEdge(args, target, ctx);
586
+ case "has_anchor":
587
+ requireBlocks(target, "has_anchor");
588
+ return { sql: `EXISTS (SELECT 1 FROM edges e2 WHERE e2.src_block = ${ctx.self}.block_id AND e2.anchor IS NOT NULL)`, params: [] };
589
+ case "parent_type":
590
+ requireBlocks(target, "parent_type");
591
+ // parent_type() returns a value; only valid inside a comparison. Emitted
592
+ // as a scalar subquery via a synthetic field is complex; support the
593
+ // common form parent_type() == "x" by rewriting here is not possible
594
+ // without the comparison context, so expose as a scalar the caller wraps.
595
+ throw new FilterInvalid("parent_type() must be compared, e.g. parent_type() == \"blockquote\" (unsupported standalone)", "10 §5");
596
+ case "child_count":
597
+ requireBlocks(target, "child_count");
598
+ throw new FilterInvalid("child_count() must be compared, e.g. child_count() > 0", "10 §5");
599
+ case "under_kind":
600
+ requireBlocks(target, "under_kind");
601
+ return compileUnderKind(argString(args, 0), args.length >= 2 ? argString(args, 1) : undefined, ctx);
602
+ case "yaml_path":
603
+ requireBlocks(target, "yaml_path");
604
+ return compileYamlPath(argString(args, 0), ctx);
605
+ case "json_pointer":
606
+ requireBlocks(target, "json_pointer");
607
+ return compileJsonPointer(argString(args, 0), ctx);
608
+ case "list":
609
+ throw new FilterInvalid("list() is only valid inside `in`, size(), .all, .exists", "10 §4");
610
+ default:
611
+ throw new FilterInvalid(`unknown function '${name}'`, "10 §3.2");
612
+ }
613
+ }
614
+ function requireBlocks(target, fn) {
615
+ if (target !== "blocks")
616
+ throw new FilterInvalid(`${fn}() is only available on the blocks target`, "10 §5");
617
+ }
618
+ function compileUnder(target, ctx) {
619
+ // subtree membership via ancestor_path prefix, or section range when the
620
+ // target is a heading. v1: ancestor_path prefix on the target block id.
621
+ return {
622
+ sql: `(${ctx.self}.ancestor_path LIKE ? OR ${ctx.self}.block_id = ?)`,
623
+ params: [`%/${target}/%`, target],
624
+ };
625
+ }
626
+ function compileUnderHeading(text, ctx) {
627
+ // Some ancestor section's heading text contains `text` (case-insensitive).
628
+ // Section ranges are in TOP-LEVEL ordinals, so we compare against the block's
629
+ // top-level ancestor's ordinal (the block itself if it is already top-level).
630
+ const top = topOrdinal(ctx.self);
631
+ return {
632
+ sql: `EXISTS (
633
+ SELECT 1 FROM sections s
634
+ JOIN blocks hb ON hb.block_id = s.heading_block
635
+ WHERE s.doc_id = ${ctx.self}.doc_id
636
+ AND lower(hb.text) LIKE '%' || lower(?) || '%'
637
+ AND ${top} >= s.first_ordinal AND ${top} <= s.last_ordinal
638
+ )`,
639
+ params: [text],
640
+ };
641
+ }
642
+ // Ordinal of a block's top-level ancestor. ancestor_path is '/b_x/b_y/…'; the
643
+ // first segment (when present) is the top-level ancestor. If the block is
644
+ // itself top-level (parent_block IS NULL), its own ordinal is the top ordinal.
645
+ // Parameterized by the block alias so it composes with OQX's per-scope aliases
646
+ // and cross-target relation correlations (e.g. section.blocks).
647
+ export function topOrdinal(self) {
648
+ return `(
649
+ CASE WHEN ${self}.parent_block IS NULL THEN ${self}.ordinal
650
+ ELSE (SELECT tb.ordinal FROM blocks tb
651
+ WHERE tb.doc_id = ${self}.doc_id AND tb.parent_block IS NULL
652
+ AND tb.block_id = REPLACE(
653
+ SUBSTR(${self}.ancestor_path, 2, INSTR(SUBSTR(${self}.ancestor_path, 2), '/') - 1), '/', ''))
654
+ END)`;
655
+ }
656
+ function compileWithin(target, ctx) {
657
+ // doc id, exact path, or glob path
658
+ if (target.startsWith("d_")) {
659
+ return { sql: `(${ctx.self}.doc_id = ?)`, params: [target] };
660
+ }
661
+ if (target.includes("*")) {
662
+ const like = target.replace(/[%_]/g, "\\$&").replace(/\*/g, "%");
663
+ return { sql: `(${ctx.doc}.path LIKE ? ESCAPE '\\')`, params: [like] };
664
+ }
665
+ return { sql: `(${ctx.doc}.path = ?)`, params: [target] };
666
+ }
667
+ // text("terms") — a full-text PRUNING predicate (FTS5, the same index the
668
+ // `query` tool's `text:` uses). It is a boolean, not a ranker: it says "this
669
+ // row's text matches", composing with every other predicate (and, notably,
670
+ // inside correlated subqueries / collects). bm25 relevance ordering is a
671
+ // ranking concern handled separately (order by). The literal is sanitized at
672
+ // compile time (fts-query.js); a query that reduces to no searchable token
673
+ // matches nothing (mirrors the `query` tool's empty-text short-circuit). Blocks
674
+ // match their own row; docs match when any of their blocks does; nodes use the
675
+ // node FTS index. Aliased by the scope so it works under OQX's per-scope aliases.
676
+ function compileText(terms, target, ctx) {
677
+ if (target === "edges")
678
+ throw new FilterInvalid("text(...) is not available on the edges target (edges carry no text index)", "10 §5");
679
+ const match = sanitizeFtsQuery(terms);
680
+ if (match === "")
681
+ return { sql: "(1 = 0)", params: [] };
682
+ if (target === "docs") {
683
+ return {
684
+ sql: `${ctx.self}.doc_id IN (SELECT b2.doc_id FROM blocks_fts JOIN blocks b2 ON b2.rowid = blocks_fts.rowid WHERE blocks_fts MATCH ?)`,
685
+ params: [match],
686
+ };
687
+ }
688
+ const ftsTable = target === "nodes" ? "nodes_fts" : "blocks_fts";
689
+ return {
690
+ sql: `${ctx.self}.rowid IN (SELECT rowid FROM ${ftsTable} WHERE ${ftsTable} MATCH ?)`,
691
+ params: [match],
692
+ };
693
+ }
694
+ // semantic("phrase") — a scalar SCORE: cosine similarity of the current row's
695
+ // embedding to the (pre-computed) query vector, via the `cosine` UDF. A SCORE,
696
+ // not a ranker — usable as a threshold prune (`semantic("x") > 0.6`) or a
697
+ // projection (`select s: semantic("x")`); ORDER BY does the actual ranking.
698
+ // docs score their whole-document vector (doc_embeddings), blocks their own
699
+ // block vector (embeddings, keyed by raw_hash); nodes have no embeddings. A row
700
+ // with no stored vector scores NULL (cosine of a missing BLOB) ⇒ absent, so a
701
+ // threshold comparison excludes it (absence = false). The query vector + model
702
+ // come from ctx.semantic (the runner embedded the literal); absent ⇒ loud.
703
+ function compileSemantic(phrase, target, ctx) {
704
+ if (target === "nodes" || target === "edges") {
705
+ throw new FilterInvalid('semantic(...) is available on the docs and blocks targets (nodes/edges have no embeddings)', "OQX semantic");
706
+ }
707
+ const resolved = ctx.semantic?.(phrase);
708
+ if (!resolved) {
709
+ throw new FilterInvalid(`semantic(${JSON.stringify(phrase)}) needs an embedding provider; none is configured for this query`, "OQX semantic");
710
+ }
711
+ const expr = target === "docs"
712
+ ? `(SELECT cosine(de.vec, ?) FROM doc_embeddings de WHERE de.doc_id = ${ctx.self}.doc_id AND de.model = ?)`
713
+ : `(SELECT cosine(e.vec, ?) FROM embeddings e WHERE e.content_hash = ${ctx.self}.raw_hash AND e.model = ? LIMIT 1)`;
714
+ return { expr, params: [resolved.vec, resolved.model] };
715
+ }
716
+ function compileHasEdge(args, target, ctx) {
717
+ const pred = argString(args, 0);
718
+ const srcCol = target === "blocks" ? `e2.src_block = ${ctx.self}.block_id` : `e2.src_doc = ${ctx.doc}.doc_id`;
719
+ if (args.length >= 2) {
720
+ const dst = argString(args, 1);
721
+ return {
722
+ sql: `EXISTS (SELECT 1 FROM edges e2 WHERE ${srcCol} AND e2.predicate = ? AND e2.to_commit IS NULL AND e2.dst_node = ?)`,
723
+ params: [pred, dst],
724
+ };
725
+ }
726
+ return {
727
+ sql: `EXISTS (SELECT 1 FROM edges e2 WHERE ${srcCol} AND e2.predicate = ? AND e2.to_commit IS NULL)`,
728
+ params: [pred],
729
+ };
730
+ }
731
+ function compileMethod(node, target, ctx) {
732
+ const { receiver, name, args } = node;
733
+ if (receiver.kind !== "field") {
734
+ throw new FilterInvalid(`method .${name}() must be called on a field`, "10 §3.2");
735
+ }
736
+ const f = fieldSql(receiver, target, ctx);
737
+ switch (name) {
738
+ case "contains":
739
+ return { sql: `(${f.expr} LIKE '%' || ? || '%')`, params: [argString(args, 0)] };
740
+ case "startsWith":
741
+ return { sql: `(${f.expr} LIKE ? || '%')`, params: [argString(args, 0)] };
742
+ case "endsWith":
743
+ return { sql: `(${f.expr} LIKE '%' || ?)`, params: [argString(args, 0)] };
744
+ case "matches":
745
+ // RE2 not available in SQLite by default → mark for post-filter.
746
+ throw new FilterInvalid("matches() requires post-filter (not yet wired); use contains/startsWith for indexed queries", "10 §8");
747
+ default:
748
+ throw new FilterInvalid(`unknown method .${name}()`, "10 §3.2");
749
+ }
750
+ }
751
+ // under_kind(kind, name?) — blocks that are children/descendants of a block
752
+ // with the given type and optional key/text match. Uses ancestor_path to find
753
+ // the ancestor block, then checks its type and attrs.
754
+ function compileUnderKind(kind, name, ctx) {
755
+ const self = ctx.self;
756
+ if (name) {
757
+ return {
758
+ sql: `EXISTS (
759
+ SELECT 1 FROM blocks ab
760
+ WHERE ab.doc_id = ${self}.doc_id
761
+ AND ${self}.ancestor_path LIKE '%/' || ab.block_id || '/%'
762
+ AND ab.type = ?
763
+ AND (ab.text LIKE '%' || ? || '%' OR json_extract(ab.attrs, '$.key') = ?)
764
+ AND ab.deleted_commit IS NULL
765
+ )`,
766
+ params: [kind, name, name],
767
+ };
768
+ }
769
+ return {
770
+ sql: `EXISTS (
771
+ SELECT 1 FROM blocks ab
772
+ WHERE ab.doc_id = ${self}.doc_id
773
+ AND ${self}.ancestor_path LIKE '%/' || ab.block_id || '/%'
774
+ AND ab.type = ?
775
+ AND ab.deleted_commit IS NULL
776
+ )`,
777
+ params: [kind],
778
+ };
779
+ }
780
+ // yaml_path("database.host") — blocks at a YAML key path. Builds nested
781
+ // EXISTS subqueries walking parent_block upward so each alias is in scope.
782
+ function compileYamlPath(path, ctx) {
783
+ const segments = path.split(".");
784
+ if (segments.length === 0)
785
+ throw new FilterInvalid("yaml_path() requires a non-empty key path");
786
+ if (segments.length === 1) {
787
+ return {
788
+ sql: `(${ctx.self}.type LIKE 'yaml:%' AND json_extract(${ctx.self}.attrs, '$.key') = ? AND ${ctx.self}.deleted_commit IS NULL)`,
789
+ params: [segments[0]],
790
+ };
791
+ }
792
+ return compileKeyPathChain(segments, "yaml", ctx);
793
+ }
794
+ // json_pointer("#/definitions/User") — blocks at a JSON Pointer path.
795
+ function compileJsonPointer(pointer, ctx) {
796
+ const normalized = pointer.replace(/^#?\/?/, "");
797
+ const segments = normalized.split("/").filter(Boolean);
798
+ if (segments.length === 0)
799
+ throw new FilterInvalid("json_pointer() requires a non-empty pointer");
800
+ if (segments.length === 1) {
801
+ return {
802
+ sql: `(${ctx.self}.type LIKE 'json:%' AND json_extract(${ctx.self}.attrs, '$.key') = ? AND ${ctx.self}.deleted_commit IS NULL)`,
803
+ params: [segments[0]],
804
+ };
805
+ }
806
+ return compileKeyPathChain(segments, "json", ctx);
807
+ }
808
+ // Build properly nested EXISTS for a multi-segment key path (yaml or json).
809
+ // segments = ["database", "host"]: the leaf block has key "host", its parent
810
+ // has key "database". Each ancestor check is a nested EXISTS so inner aliases
811
+ // can reference the enclosing scope.
812
+ function compileKeyPathChain(segments, prefix, ctx) {
813
+ const self = ctx.self;
814
+ const leaf = segments[segments.length - 1];
815
+ const ancestors = segments.slice(0, -1);
816
+ const params = [leaf];
817
+ // Build from outermost ancestor (root) to the leaf's immediate parent.
818
+ // The innermost (leaf) condition is on `self` itself; each ancestor wraps
819
+ // its child in a nested EXISTS.
820
+ let sql = `${self}.type LIKE '${prefix}:%' AND json_extract(${self}.attrs, '$.key') = ? AND ${self}.deleted_commit IS NULL`;
821
+ let innerRef = self;
822
+ for (let i = ancestors.length - 1; i >= 0; i--) {
823
+ const alias = `kp${i}`;
824
+ sql = `EXISTS (
825
+ SELECT 1 FROM blocks ${alias}
826
+ WHERE ${alias}.block_id = ${innerRef}.parent_block
827
+ AND ${alias}.type LIKE '${prefix}:%'
828
+ AND json_extract(${alias}.attrs, '$.key') = ?
829
+ AND ${alias}.deleted_commit IS NULL
830
+ ) AND ${sql}`;
831
+ params.unshift(ancestors[i]);
832
+ innerRef = alias;
833
+ }
834
+ return { sql: `(${sql})`, params };
835
+ }
836
+ //# sourceMappingURL=compile.js.map