mrplex 0.0.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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/dist/cli/config.d.ts +28 -0
  4. package/dist/cli/config.js +48 -0
  5. package/dist/cli/config.js.map +1 -0
  6. package/dist/cli/exit-codes.d.ts +13 -0
  7. package/dist/cli/exit-codes.js +39 -0
  8. package/dist/cli/exit-codes.js.map +1 -0
  9. package/dist/cli/format.d.ts +22 -0
  10. package/dist/cli/format.js +148 -0
  11. package/dist/cli/format.js.map +1 -0
  12. package/dist/cli/main.d.ts +16 -0
  13. package/dist/cli/main.js +1518 -0
  14. package/dist/cli/main.js.map +1 -0
  15. package/dist/client/kernel-client.d.ts +97 -0
  16. package/dist/client/kernel-client.js +14 -0
  17. package/dist/client/kernel-client.js.map +1 -0
  18. package/dist/client/local.d.ts +26 -0
  19. package/dist/client/local.js +103 -0
  20. package/dist/client/local.js.map +1 -0
  21. package/dist/client/remote-mcp.d.ts +35 -0
  22. package/dist/client/remote-mcp.js +174 -0
  23. package/dist/client/remote-mcp.js.map +1 -0
  24. package/dist/embed/backfill.d.ts +24 -0
  25. package/dist/embed/backfill.js +33 -0
  26. package/dist/embed/backfill.js.map +1 -0
  27. package/dist/embed/chunker.d.ts +31 -0
  28. package/dist/embed/chunker.js +121 -0
  29. package/dist/embed/chunker.js.map +1 -0
  30. package/dist/embed/cmd-hook.d.ts +21 -0
  31. package/dist/embed/cmd-hook.js +100 -0
  32. package/dist/embed/cmd-hook.js.map +1 -0
  33. package/dist/embed/config.d.ts +61 -0
  34. package/dist/embed/config.js +109 -0
  35. package/dist/embed/config.js.map +1 -0
  36. package/dist/embed/hook.d.ts +38 -0
  37. package/dist/embed/hook.js +56 -0
  38. package/dist/embed/hook.js.map +1 -0
  39. package/dist/embed/http-hook.d.ts +13 -0
  40. package/dist/embed/http-hook.js +39 -0
  41. package/dist/embed/http-hook.js.map +1 -0
  42. package/dist/embed/worker.d.ts +62 -0
  43. package/dist/embed/worker.js +228 -0
  44. package/dist/embed/worker.js.map +1 -0
  45. package/dist/fixtures/hwe/characters/albion-ever.md +12 -0
  46. package/dist/fixtures/hwe/characters/cent.md +11 -0
  47. package/dist/fixtures/hwe/concepts/cent-demiurge-resonance.md +10 -0
  48. package/dist/fixtures/hwe/concepts/phase-looper.md +10 -0
  49. package/dist/fixtures/hwe/entities/demiurge.md +11 -0
  50. package/dist/fixtures/hwe/entities/monad.md +11 -0
  51. package/dist/fixtures/hwe/moc/globocorp-network.md +19 -0
  52. package/dist/fixtures/hwe/organizations/globocorp.md +11 -0
  53. package/dist/fixtures/hwe/readme.md +24 -0
  54. package/dist/fixtures/starship/crew/aria-okonkwo.md +14 -0
  55. package/dist/fixtures/starship/crew/bexley-orr.md +14 -0
  56. package/dist/fixtures/starship/crew/dax-thorne.md +14 -0
  57. package/dist/fixtures/starship/crew/isolde-marsh.md +16 -0
  58. package/dist/fixtures/starship/crew/kestrel-vance.md +15 -0
  59. package/dist/fixtures/starship/crew/quill-vasquez.md +14 -0
  60. package/dist/fixtures/starship/crew/soren-halloway.md +16 -0
  61. package/dist/fixtures/starship/encounters/the-drift-choir.md +14 -0
  62. package/dist/fixtures/starship/encounters/the-hollow-signal.md +16 -0
  63. package/dist/fixtures/starship/encounters/the-wandering-buoy.md +14 -0
  64. package/dist/fixtures/starship/equipment/coolant-loop-b.md +13 -0
  65. package/dist/fixtures/starship/equipment/emergency-ration-bars.md +12 -0
  66. package/dist/fixtures/starship/equipment/long-range-sensors.md +13 -0
  67. package/dist/fixtures/starship/equipment/plasma-manifold-3.md +15 -0
  68. package/dist/fixtures/starship/equipment/shuttle-corvid.md +13 -0
  69. package/dist/fixtures/starship/logs/marsh-4419-1.md +13 -0
  70. package/dist/fixtures/starship/logs/okonkwo-4415-1.md +13 -0
  71. package/dist/fixtures/starship/logs/orr-4413-1.md +13 -0
  72. package/dist/fixtures/starship/logs/orr-4420-1.md +12 -0
  73. package/dist/fixtures/starship/logs/thorne-4413-2.md +13 -0
  74. package/dist/fixtures/starship/logs/vance-4419-2.md +13 -0
  75. package/dist/fixtures/starship/misc/the-galley-ficus.md +11 -0
  76. package/dist/fixtures/starship/missions/the-cinder-run.md +17 -0
  77. package/dist/fixtures/starship/missions/the-drift-choir.md +17 -0
  78. package/dist/fixtures/starship/missions/the-hollow-signal.md +17 -0
  79. package/dist/fixtures/starship/missions/the-silent-beacon.md +17 -0
  80. package/dist/fixtures/starship/moc/crew.md +19 -0
  81. package/dist/fixtures/starship/moc/encounters.md +14 -0
  82. package/dist/fixtures/starship/moc/missions.md +15 -0
  83. package/dist/fixtures/starship/readme.md +60 -0
  84. package/dist/index.d.ts +1 -0
  85. package/dist/index.js +3 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/kernel/auth/glob.d.ts +46 -0
  88. package/dist/kernel/auth/glob.js +112 -0
  89. package/dist/kernel/auth/glob.js.map +1 -0
  90. package/dist/kernel/auth/scope.d.ts +53 -0
  91. package/dist/kernel/auth/scope.js +83 -0
  92. package/dist/kernel/auth/scope.js.map +1 -0
  93. package/dist/kernel/casefold.d.ts +37 -0
  94. package/dist/kernel/casefold.js +44 -0
  95. package/dist/kernel/casefold.js.map +1 -0
  96. package/dist/kernel/constants.d.ts +21 -0
  97. package/dist/kernel/constants.js +22 -0
  98. package/dist/kernel/constants.js.map +1 -0
  99. package/dist/kernel/context.d.ts +70 -0
  100. package/dist/kernel/context.js +114 -0
  101. package/dist/kernel/context.js.map +1 -0
  102. package/dist/kernel/deletion.d.ts +50 -0
  103. package/dist/kernel/deletion.js +83 -0
  104. package/dist/kernel/deletion.js.map +1 -0
  105. package/dist/kernel/diff.d.ts +36 -0
  106. package/dist/kernel/diff.js +61 -0
  107. package/dist/kernel/diff.js.map +1 -0
  108. package/dist/kernel/errors.d.ts +37 -0
  109. package/dist/kernel/errors.js +55 -0
  110. package/dist/kernel/errors.js.map +1 -0
  111. package/dist/kernel/frontmatter-input.d.ts +32 -0
  112. package/dist/kernel/frontmatter-input.js +85 -0
  113. package/dist/kernel/frontmatter-input.js.map +1 -0
  114. package/dist/kernel/graph.d.ts +34 -0
  115. package/dist/kernel/graph.js +559 -0
  116. package/dist/kernel/graph.js.map +1 -0
  117. package/dist/kernel/history.d.ts +74 -0
  118. package/dist/kernel/history.js +143 -0
  119. package/dist/kernel/history.js.map +1 -0
  120. package/dist/kernel/kernel.d.ts +162 -0
  121. package/dist/kernel/kernel.js +675 -0
  122. package/dist/kernel/kernel.js.map +1 -0
  123. package/dist/kernel/path-config.d.ts +101 -0
  124. package/dist/kernel/path-config.js +171 -0
  125. package/dist/kernel/path-config.js.map +1 -0
  126. package/dist/kernel/query/ast.d.ts +24 -0
  127. package/dist/kernel/query/ast.js +52 -0
  128. package/dist/kernel/query/ast.js.map +1 -0
  129. package/dist/kernel/query/cel-parse.d.ts +37 -0
  130. package/dist/kernel/query/cel-parse.js +88 -0
  131. package/dist/kernel/query/cel-parse.js.map +1 -0
  132. package/dist/kernel/query/degrees.d.ts +31 -0
  133. package/dist/kernel/query/degrees.js +135 -0
  134. package/dist/kernel/query/degrees.js.map +1 -0
  135. package/dist/kernel/query/graph-ast.d.ts +51 -0
  136. package/dist/kernel/query/graph-ast.js +139 -0
  137. package/dist/kernel/query/graph-ast.js.map +1 -0
  138. package/dist/kernel/query/query.d.ts +56 -0
  139. package/dist/kernel/query/query.js +276 -0
  140. package/dist/kernel/query/query.js.map +1 -0
  141. package/dist/kernel/validation.d.ts +64 -0
  142. package/dist/kernel/validation.js +183 -0
  143. package/dist/kernel/validation.js.map +1 -0
  144. package/dist/kernel/version-id.d.ts +10 -0
  145. package/dist/kernel/version-id.js +21 -0
  146. package/dist/kernel/version-id.js.map +1 -0
  147. package/dist/kernel/wire.d.ts +148 -0
  148. package/dist/kernel/wire.js +6 -0
  149. package/dist/kernel/wire.js.map +1 -0
  150. package/dist/links/backfill.d.ts +28 -0
  151. package/dist/links/backfill.js +33 -0
  152. package/dist/links/backfill.js.map +1 -0
  153. package/dist/links/extract.d.ts +29 -0
  154. package/dist/links/extract.js +233 -0
  155. package/dist/links/extract.js.map +1 -0
  156. package/dist/links/link-config.d.ts +70 -0
  157. package/dist/links/link-config.js +107 -0
  158. package/dist/links/link-config.js.map +1 -0
  159. package/dist/links/maintain.d.ts +37 -0
  160. package/dist/links/maintain.js +91 -0
  161. package/dist/links/maintain.js.map +1 -0
  162. package/dist/links/repair.d.ts +46 -0
  163. package/dist/links/repair.js +96 -0
  164. package/dist/links/repair.js.map +1 -0
  165. package/dist/links/resolve.d.ts +41 -0
  166. package/dist/links/resolve.js +121 -0
  167. package/dist/links/resolve.js.map +1 -0
  168. package/dist/links/stale.d.ts +32 -0
  169. package/dist/links/stale.js +73 -0
  170. package/dist/links/stale.js.map +1 -0
  171. package/dist/markdown/content-hash.d.ts +34 -0
  172. package/dist/markdown/content-hash.js +46 -0
  173. package/dist/markdown/content-hash.js.map +1 -0
  174. package/dist/markdown/frontmatter.d.ts +69 -0
  175. package/dist/markdown/frontmatter.js +147 -0
  176. package/dist/markdown/frontmatter.js.map +1 -0
  177. package/dist/markdown/hash-backfill.d.ts +22 -0
  178. package/dist/markdown/hash-backfill.js +43 -0
  179. package/dist/markdown/hash-backfill.js.map +1 -0
  180. package/dist/mcp/query-syntax.d.ts +20 -0
  181. package/dist/mcp/query-syntax.js +194 -0
  182. package/dist/mcp/query-syntax.js.map +1 -0
  183. package/dist/mcp/render.d.ts +31 -0
  184. package/dist/mcp/render.js +110 -0
  185. package/dist/mcp/render.js.map +1 -0
  186. package/dist/mcp/server.d.ts +60 -0
  187. package/dist/mcp/server.js +170 -0
  188. package/dist/mcp/server.js.map +1 -0
  189. package/dist/mcp/tools.d.ts +93 -0
  190. package/dist/mcp/tools.js +1218 -0
  191. package/dist/mcp/tools.js.map +1 -0
  192. package/dist/rest/conditional.d.ts +37 -0
  193. package/dist/rest/conditional.js +58 -0
  194. package/dist/rest/conditional.js.map +1 -0
  195. package/dist/rest/negotiate.d.ts +33 -0
  196. package/dist/rest/negotiate.js +54 -0
  197. package/dist/rest/negotiate.js.map +1 -0
  198. package/dist/rest/routes.d.ts +37 -0
  199. package/dist/rest/routes.js +744 -0
  200. package/dist/rest/routes.js.map +1 -0
  201. package/dist/seed/fixture-seed.d.ts +35 -0
  202. package/dist/seed/fixture-seed.js +75 -0
  203. package/dist/seed/fixture-seed.js.map +1 -0
  204. package/dist/seed/starship-config.d.ts +3 -0
  205. package/dist/seed/starship-config.js +5 -0
  206. package/dist/seed/starship-config.js.map +1 -0
  207. package/dist/server/headers.d.ts +36 -0
  208. package/dist/server/headers.js +30 -0
  209. package/dist/server/headers.js.map +1 -0
  210. package/dist/server/http-error.d.ts +53 -0
  211. package/dist/server/http-error.js +104 -0
  212. package/dist/server/http-error.js.map +1 -0
  213. package/dist/server/serve.d.ts +56 -0
  214. package/dist/server/serve.js +135 -0
  215. package/dist/server/serve.js.map +1 -0
  216. package/dist/shell/audit.d.ts +32 -0
  217. package/dist/shell/audit.js +28 -0
  218. package/dist/shell/audit.js.map +1 -0
  219. package/dist/shell/guard.d.ts +50 -0
  220. package/dist/shell/guard.js +176 -0
  221. package/dist/shell/guard.js.map +1 -0
  222. package/dist/shell/keys.d.ts +36 -0
  223. package/dist/shell/keys.js +64 -0
  224. package/dist/shell/keys.js.map +1 -0
  225. package/dist/shell/login.d.ts +56 -0
  226. package/dist/shell/login.js +130 -0
  227. package/dist/shell/login.js.map +1 -0
  228. package/dist/shell/oidc.d.ts +62 -0
  229. package/dist/shell/oidc.js +102 -0
  230. package/dist/shell/oidc.js.map +1 -0
  231. package/dist/shell/policy.d.ts +100 -0
  232. package/dist/shell/policy.js +232 -0
  233. package/dist/shell/policy.js.map +1 -0
  234. package/dist/shell/proxy-policy.d.ts +34 -0
  235. package/dist/shell/proxy-policy.js +120 -0
  236. package/dist/shell/proxy-policy.js.map +1 -0
  237. package/dist/shell/proxy.d.ts +47 -0
  238. package/dist/shell/proxy.js +253 -0
  239. package/dist/shell/proxy.js.map +1 -0
  240. package/dist/shell/serve.d.ts +86 -0
  241. package/dist/shell/serve.js +0 -0
  242. package/dist/shell/serve.js.map +1 -0
  243. package/dist/shell/stdio.d.ts +55 -0
  244. package/dist/shell/stdio.js +63 -0
  245. package/dist/shell/stdio.js.map +1 -0
  246. package/dist/storage/registry.d.ts +20 -0
  247. package/dist/storage/registry.js +38 -0
  248. package/dist/storage/registry.js.map +1 -0
  249. package/dist/storage/search-plan.d.ts +93 -0
  250. package/dist/storage/search-plan.js +15 -0
  251. package/dist/storage/search-plan.js.map +1 -0
  252. package/dist/storage/types.d.ts +448 -0
  253. package/dist/storage/types.js +14 -0
  254. package/dist/storage/types.js.map +1 -0
  255. package/dist/storage/versions-since.d.ts +72 -0
  256. package/dist/storage/versions-since.js +92 -0
  257. package/dist/storage/versions-since.js.map +1 -0
  258. package/dist/storage-postgres/adapter.d.ts +17 -0
  259. package/dist/storage-postgres/adapter.js +782 -0
  260. package/dist/storage-postgres/adapter.js.map +1 -0
  261. package/dist/storage-postgres/compile-postgres.d.ts +22 -0
  262. package/dist/storage-postgres/compile-postgres.js +830 -0
  263. package/dist/storage-postgres/compile-postgres.js.map +1 -0
  264. package/dist/storage-postgres/errors.d.ts +34 -0
  265. package/dist/storage-postgres/errors.js +50 -0
  266. package/dist/storage-postgres/errors.js.map +1 -0
  267. package/dist/storage-postgres/migrations/0001_init.sql +113 -0
  268. package/dist/storage-postgres/migrations/0002_content_hash.sql +7 -0
  269. package/dist/storage-postgres/migrations/index.d.ts +22 -0
  270. package/dist/storage-postgres/migrations/index.js +70 -0
  271. package/dist/storage-postgres/migrations/index.js.map +1 -0
  272. package/dist/storage-sqlite/adapter.d.ts +2 -0
  273. package/dist/storage-sqlite/adapter.js +688 -0
  274. package/dist/storage-sqlite/adapter.js.map +1 -0
  275. package/dist/storage-sqlite/compile-filter.d.ts +41 -0
  276. package/dist/storage-sqlite/compile-filter.js +881 -0
  277. package/dist/storage-sqlite/compile-filter.js.map +1 -0
  278. package/dist/storage-sqlite/compile-sqlite.d.ts +19 -0
  279. package/dist/storage-sqlite/compile-sqlite.js +176 -0
  280. package/dist/storage-sqlite/compile-sqlite.js.map +1 -0
  281. package/dist/storage-sqlite/migrations/0001_init.sql +123 -0
  282. package/dist/storage-sqlite/migrations/0002_content_hash.sql +7 -0
  283. package/dist/storage-sqlite/migrations/index.d.ts +6 -0
  284. package/dist/storage-sqlite/migrations/index.js +41 -0
  285. package/dist/storage-sqlite/migrations/index.js.map +1 -0
  286. package/dist/storage-sqlite/vec.d.ts +40 -0
  287. package/dist/storage-sqlite/vec.js +62 -0
  288. package/dist/storage-sqlite/vec.js.map +1 -0
  289. package/dist/sync/cursor.d.ts +36 -0
  290. package/dist/sync/cursor.js +66 -0
  291. package/dist/sync/cursor.js.map +1 -0
  292. package/dist/sync/daemon.d.ts +40 -0
  293. package/dist/sync/daemon.js +202 -0
  294. package/dist/sync/daemon.js.map +1 -0
  295. package/dist/sync/feed.d.ts +38 -0
  296. package/dist/sync/feed.js +125 -0
  297. package/dist/sync/feed.js.map +1 -0
  298. package/dist/sync/fs-store.d.ts +8 -0
  299. package/dist/sync/fs-store.js +76 -0
  300. package/dist/sync/fs-store.js.map +1 -0
  301. package/dist/sync/intrinsics.d.ts +52 -0
  302. package/dist/sync/intrinsics.js +76 -0
  303. package/dist/sync/intrinsics.js.map +1 -0
  304. package/dist/sync/paths.d.ts +38 -0
  305. package/dist/sync/paths.js +56 -0
  306. package/dist/sync/paths.js.map +1 -0
  307. package/dist/sync/push.d.ts +48 -0
  308. package/dist/sync/push.js +328 -0
  309. package/dist/sync/push.js.map +1 -0
  310. package/dist/sync/reconcile.d.ts +54 -0
  311. package/dist/sync/reconcile.js +252 -0
  312. package/dist/sync/reconcile.js.map +1 -0
  313. package/dist/sync/run.d.ts +27 -0
  314. package/dist/sync/run.js +46 -0
  315. package/dist/sync/run.js.map +1 -0
  316. package/dist/version.d.ts +2 -0
  317. package/dist/version.js +5 -0
  318. package/dist/version.js.map +1 -0
  319. package/package.json +80 -0
@@ -0,0 +1,830 @@
1
+ /**
2
+ * SearchPlan → Postgres SQL compiler (m5-plan WS5).
3
+ *
4
+ * Mirrors the SQLite compiler (src/storage-sqlite/compile-sqlite.ts)
5
+ * with dialect swaps:
6
+ * - `$n` placeholders instead of `?`; params are appended in the
7
+ * order they land in the SQL text.
8
+ * - jsonb access: `frontmatter->'k' = '"v"'::jsonb OR frontmatter->'k'
9
+ * @> '["v"]'::jsonb` covers scalar+list under one GIN.
10
+ * - `->>` + casts for typed compare (mirrors json_extract semantics).
11
+ * - `~` for CEL matches() and scope-glob regexes; POSIX ARE.
12
+ * - `position()` / `LIKE ESCAPE` for contains/startsWith/endsWith.
13
+ * - Native booleans (never `1`/`0` predicates).
14
+ * - FTS: `websearch_to_tsquery` (never throws on user input) +
15
+ * `ts_rank`; ordering by rank when text is present.
16
+ */
17
+ import { globToRegexSource } from "../kernel/auth/glob.js";
18
+ import { KernelError } from "../kernel/errors.js";
19
+ import { unwrapConstant, unwrapListHint } from "../kernel/query/ast.js";
20
+ import { INTRINSIC_PREFIX } from "../kernel/query/cel-parse.js";
21
+ import { asGraphCollection, asGraphMembership, assertNotReservedGraphName, } from "../kernel/query/graph-ast.js";
22
+ import { BODY_FIELD } from "../links/extract.js";
23
+ // A running builder that keeps params + `$n` counter aligned as we
24
+ // weave nested fragments together. Also carries the plan's scope + sigils
25
+ // so graph subqueries can apply the caller's full visibility filter to the
26
+ // OTHER endpoint (§11.2), plus an alias sequence so nested graph subqueries
27
+ // don't collide.
28
+ class Builder {
29
+ params = [];
30
+ scope = { kind: "allow_all" };
31
+ sigils = [];
32
+ aliasSeq = 0;
33
+ push(v) {
34
+ this.params.push(v);
35
+ return `$${this.params.length}`;
36
+ }
37
+ nextAlias() {
38
+ return `lv${this.aliasSeq++}`;
39
+ }
40
+ }
41
+ // -----------------------------------------------------------------------------
42
+ // Public entry
43
+ // -----------------------------------------------------------------------------
44
+ export function compileSearchPlan(plan) {
45
+ const b = new Builder();
46
+ // Graph subqueries apply the caller's full visibility filter (scope +
47
+ // sigil-exclusion) to the other endpoint (§11.2).
48
+ b.scope = plan.scope;
49
+ b.sigils = plan.sigils;
50
+ const clauses = ["versions.next_id IS NULL"];
51
+ // repo_id filter.
52
+ clauses.push(`versions.repo_id = ANY(${b.push(plan.repo_ids)}::bigint[])`);
53
+ if (plan.filter_ast) {
54
+ clauses.push(`(${compileExpr(plan.filter_ast, b)})`);
55
+ }
56
+ const sigilSql = compileSigilExclusion(plan.sigils, b, "versions");
57
+ if (sigilSql.length > 0)
58
+ clauses.push(sigilSql);
59
+ if (plan.scope.kind === "groups") {
60
+ const scopeSql = compileScopeGroups(plan.scope.groups, b, "versions");
61
+ if (scopeSql.length > 0)
62
+ clauses.push(`(${scopeSql})`);
63
+ else
64
+ clauses.push("false");
65
+ }
66
+ // allow_all: no clause.
67
+ if (plan.candidate_ids && plan.candidate_ids.length > 0) {
68
+ clauses.push(`versions.id = ANY(${b.push(plan.candidate_ids)}::bigint[])`);
69
+ }
70
+ // Candidate DOCUMENT-id whitelist (graph read surface). Restricts to live
71
+ // versions of these documents so a BFS round's visibility check is one pass.
72
+ if (plan.candidate_document_ids && plan.candidate_document_ids.length > 0) {
73
+ clauses.push(`versions.document_id = ANY(${b.push(plan.candidate_document_ids)}::bigint[])`);
74
+ }
75
+ const cols = `versions.id, versions.document_id, versions.repo_id,
76
+ versions.prev_id, versions.next_id, versions.path,
77
+ versions.frontmatter_raw, versions.frontmatter,
78
+ versions.body, versions.author, versions.created_at,
79
+ versions.content_hash`;
80
+ let sql;
81
+ if (plan.text !== undefined) {
82
+ // websearch_to_tsquery never throws on user input. Order by ts_rank
83
+ // then id for stability.
84
+ const textPh = b.push(plan.text);
85
+ clauses.push(`versions.fts_tsv @@ websearch_to_tsquery('english', ${textPh})`);
86
+ sql = `SELECT ${cols}
87
+ FROM versions
88
+ WHERE ${clauses.join(" AND ")}
89
+ ORDER BY ts_rank(versions.fts_tsv, websearch_to_tsquery('english', ${textPh})) DESC,
90
+ versions.id DESC
91
+ LIMIT ${b.push(plan.limit)}`;
92
+ }
93
+ else {
94
+ sql = `SELECT ${cols}
95
+ FROM versions
96
+ WHERE ${clauses.join(" AND ")}
97
+ ORDER BY versions.created_at DESC, versions.id DESC
98
+ LIMIT ${b.push(plan.limit)}`;
99
+ }
100
+ return { sql, params: b.params };
101
+ }
102
+ // -----------------------------------------------------------------------------
103
+ // Sigil exclusion + scope groups
104
+ // -----------------------------------------------------------------------------
105
+ function compileSigilExclusion(groups, b, alias) {
106
+ const parts = [];
107
+ for (const g of groups) {
108
+ if (g.sigils.length === 0)
109
+ continue;
110
+ if (g.repo_ids.length === 0)
111
+ continue;
112
+ const repoPh = b.push(g.repo_ids);
113
+ const notInAny = `${alias}.repo_id <> ALL(${repoPh}::bigint[])`;
114
+ const sigilChecks = [];
115
+ for (const sigil of g.sigils) {
116
+ const escaped = sigil.replace(/[\\%_]/g, (ch) => `\\${ch}`);
117
+ sigilChecks.push(`${alias}.path NOT LIKE ${b.push(`${escaped}%`)}`);
118
+ sigilChecks.push(`${alias}.path NOT LIKE ${b.push(`%/${escaped}%`)}`);
119
+ }
120
+ parts.push(`(${notInAny} OR (${sigilChecks.join(" AND ")}))`);
121
+ }
122
+ return parts.join(" AND ");
123
+ }
124
+ /**
125
+ * Full visibility predicate for a joined endpoint `alias` — read scope AND
126
+ * sigil-exclusion (§11.2 "visible graph = readable graph"). Returns "TRUE"
127
+ * when nothing constrains it.
128
+ */
129
+ function visibilityForAlias(b, alias) {
130
+ const scope = scopeFragmentForAlias(b, alias);
131
+ const sigils = compileSigilExclusion(b.sigils, b, alias);
132
+ if (!sigils)
133
+ return scope;
134
+ return `(${scope} AND ${sigils})`;
135
+ }
136
+ function compileScopeGroups(groups, b, alias) {
137
+ const parts = [];
138
+ for (const g of groups) {
139
+ if (g.globs.length === 0)
140
+ continue;
141
+ if (g.repos === "*") {
142
+ const globExpr = compileScopeGlobs(g.globs, b, alias);
143
+ parts.push(`(${globExpr})`);
144
+ }
145
+ else if (g.repos.length > 0) {
146
+ // Push the repo-array param BEFORE the globs so `$n` numbering matches
147
+ // the emitted text order (`repo_id = ANY($k) AND (<globs>)`). Getting
148
+ // this backwards silently swaps params — the bug the graph-scope
149
+ // parity test caught. (The `repos === "*"` branch has no repo param,
150
+ // so it was never exposed there.)
151
+ const ph = b.push(g.repos);
152
+ const globExpr = compileScopeGlobs(g.globs, b, alias);
153
+ parts.push(`(${alias}.repo_id = ANY(${ph}::bigint[]) AND (${globExpr}))`);
154
+ }
155
+ }
156
+ return parts.join(" OR ");
157
+ }
158
+ /**
159
+ * Scope predicate for an aliased `versions` row inside a graph subquery
160
+ * (§11.2 — the visible graph equals the readable graph). Mirrors the
161
+ * SQLite compiler's scopeFragmentForAlias.
162
+ */
163
+ function scopeFragmentForAlias(b, alias) {
164
+ if (b.scope.kind === "allow_all")
165
+ return "TRUE";
166
+ if (b.scope.kind === "deny_all")
167
+ return "FALSE";
168
+ const sql = compileScopeGroups(b.scope.groups, b, alias);
169
+ return sql.length > 0 ? `(${sql})` : "FALSE";
170
+ }
171
+ /**
172
+ * Gitignore-style last-match-wins list, encoded as a CASE nesting where
173
+ * later globs wrap earlier ones (so the outermost WHEN — the last glob
174
+ * — wins if it matches).
175
+ */
176
+ function compileScopeGlobs(globs, b, alias) {
177
+ // We need `$n`s to appear in outermost-first order in the emitted SQL.
178
+ // The compileWithReverseOrder pattern: build the fragment text first
179
+ // with named placeholders, then emit params in the correct order.
180
+ //
181
+ // Simpler: build a stack of {regex, verdict}, walk it outward and
182
+ // push params in that order as we build the string.
183
+ // Last-match-wins: the LAST glob is the OUTERMOST CASE (if it matches,
184
+ // its verdict wins; otherwise fall through to earlier globs). Build the
185
+ // nesting outermost-first via prefix/suffix so `$n` params land in text
186
+ // order (last glob's param first) — matching the SQLite compiler's shape.
187
+ let prefix = "";
188
+ let suffix = "";
189
+ for (let i = globs.length - 1; i >= 0; i--) {
190
+ const g = globs[i];
191
+ const negated = g.startsWith("!");
192
+ const raw = negated ? g.slice(1) : g;
193
+ const ph = b.push(`^${globToRegexSource(raw)}$`);
194
+ prefix += `(CASE WHEN ${alias}.path ~ ${ph} THEN ${negated ? "FALSE" : "TRUE"} ELSE `;
195
+ suffix = ` END)${suffix}`;
196
+ }
197
+ return `${prefix}FALSE${suffix}`;
198
+ }
199
+ // -----------------------------------------------------------------------------
200
+ // CEL AST → Postgres SQL
201
+ // -----------------------------------------------------------------------------
202
+ const INTRINSIC_COLUMNS = {
203
+ path: "versions.path",
204
+ // See compile-filter.ts for the $updated_at naming rationale.
205
+ updated_at: "versions.created_at",
206
+ body: "versions.body",
207
+ // Canonical content fingerprint (sync/history plan §2.5).
208
+ content_hash: "versions.content_hash",
209
+ };
210
+ function compileIntrinsic(mangledName) {
211
+ const name = mangledName.slice(INTRINSIC_PREFIX.length);
212
+ const column = INTRINSIC_COLUMNS[name];
213
+ if (!column) {
214
+ const expected = Object.keys(INTRINSIC_COLUMNS)
215
+ .map((k) => `$${k}`)
216
+ .join(", ");
217
+ throw new KernelError("filter_invalid", {
218
+ reason: `unknown intrinsic $${name} (expected ${expected})`,
219
+ });
220
+ }
221
+ return column;
222
+ }
223
+ /**
224
+ * jsonb path access. Returns the SQL for `frontmatter -> 'k' -> ...`.
225
+ * The `->>` variant (text) is used inside typed comparisons.
226
+ */
227
+ function frontmatterPath(parts) {
228
+ // Parts come from the CEL parser (identExpr.value.name /
229
+ // selectExpr.value.field) — the CEL grammar forbids newlines and
230
+ // other control chars in identifiers, so single-quote escaping is
231
+ // the only string-safety concern here.
232
+ if (parts.length === 0)
233
+ throw new Error("frontmatterPath: empty parts");
234
+ let expr = `versions.frontmatter -> '${parts[0].replace(/'/g, "''")}'`;
235
+ for (let i = 1; i < parts.length; i++) {
236
+ const p = parts[i];
237
+ expr = `${expr} -> '${p.replace(/'/g, "''")}'`;
238
+ }
239
+ return expr;
240
+ }
241
+ function frontmatterText(parts) {
242
+ // `->>` returns text. Convert the last hop to `->>` for that.
243
+ if (parts.length === 0)
244
+ throw new Error("frontmatterText: empty parts");
245
+ if (parts.length === 1) {
246
+ const p = parts[0];
247
+ return `versions.frontmatter ->> '${p.replace(/'/g, "''")}'`;
248
+ }
249
+ const prefix = frontmatterPath(parts.slice(0, -1));
250
+ const last = parts[parts.length - 1];
251
+ return `${prefix} ->> '${last.replace(/'/g, "''")}'`;
252
+ }
253
+ function collectSelectPath(expr) {
254
+ if (expr.exprKind.case === "identExpr")
255
+ return [expr.exprKind.value.name];
256
+ if (expr.exprKind.case !== "selectExpr")
257
+ return undefined;
258
+ const inner = expr.exprKind.value.operand;
259
+ if (!inner)
260
+ return undefined;
261
+ const innerPath = collectSelectPath(inner);
262
+ if (!innerPath)
263
+ return undefined;
264
+ return [...innerPath, expr.exprKind.value.field];
265
+ }
266
+ function compileExpr(expr, b) {
267
+ const kind = expr.exprKind.case;
268
+ switch (kind) {
269
+ case "constExpr":
270
+ return compileConst(expr, b);
271
+ case "identExpr":
272
+ return compileIdent(expr);
273
+ case "selectExpr":
274
+ return compileSelect(expr);
275
+ case "callExpr":
276
+ return compileCall(expr, b);
277
+ case "comprehensionExpr":
278
+ return compileComprehension(expr, b);
279
+ case "listExpr":
280
+ case "structExpr":
281
+ throw new KernelError("filter_invalid", {
282
+ reason: `${kind ?? "unknown"} expressions are not supported in filters`,
283
+ });
284
+ default:
285
+ throw new KernelError("filter_invalid", {
286
+ reason: `unsupported expression kind: ${String(kind)}`,
287
+ });
288
+ }
289
+ }
290
+ function compileConst(expr, b) {
291
+ const value = unwrapConstant(expr);
292
+ if (value === undefined) {
293
+ throw new KernelError("filter_invalid", { reason: "unsupported constant kind" });
294
+ }
295
+ if (value === null)
296
+ return "NULL";
297
+ if (typeof value === "boolean")
298
+ return value ? "TRUE" : "FALSE";
299
+ return b.push(value);
300
+ }
301
+ function compileIdent(expr) {
302
+ if (expr.exprKind.case !== "identExpr")
303
+ throw new Error("compileIdent: wrong kind");
304
+ const name = expr.exprKind.value.name;
305
+ if (name.startsWith(INTRINSIC_PREFIX))
306
+ return compileIntrinsic(name);
307
+ // Bare frontmatter key → text access. Comparisons wrap with casts.
308
+ return frontmatterText([name]);
309
+ }
310
+ function compileSelect(expr) {
311
+ if (expr.exprKind.case !== "selectExpr")
312
+ throw new Error("compileSelect: wrong kind");
313
+ const sel = expr.exprKind.value;
314
+ const path = collectSelectPath(expr);
315
+ if (!path) {
316
+ throw new KernelError("filter_invalid", {
317
+ reason: `unsupported select expression: field '${sel.field}'`,
318
+ });
319
+ }
320
+ const [root, ...rest] = path;
321
+ const rootExpr = root;
322
+ if (rootExpr.startsWith(INTRINSIC_PREFIX)) {
323
+ if (rest.length > 0) {
324
+ throw new KernelError("filter_invalid", {
325
+ reason: `intrinsics do not have subfields ($${rootExpr.slice(INTRINSIC_PREFIX.length)})`,
326
+ });
327
+ }
328
+ return compileIntrinsic(rootExpr);
329
+ }
330
+ return frontmatterText([rootExpr, ...rest]);
331
+ }
332
+ const BIN_OPS = {
333
+ "_==_": "=",
334
+ "_!=_": "<>",
335
+ "_<_": "<",
336
+ "_<=_": "<=",
337
+ "_>_": ">",
338
+ "_>=_": ">=",
339
+ "_&&_": "AND",
340
+ "_||_": "OR",
341
+ };
342
+ function compileCall(expr, b) {
343
+ if (expr.exprKind.case !== "callExpr")
344
+ throw new Error("compileCall: wrong kind");
345
+ const call = expr.exprKind.value;
346
+ const fn = call.function;
347
+ const args = call.args;
348
+ const target = call.target;
349
+ const binOp = BIN_OPS[fn];
350
+ if (binOp && args.length === 2) {
351
+ return compileBinary(binOp, args[0], args[1], b);
352
+ }
353
+ if (fn === "!_" && args.length === 1) {
354
+ return `NOT (${compileExpr(args[0], b)})`;
355
+ }
356
+ if (fn === "-_" && args.length === 1) {
357
+ return `-(${compileExpr(args[0], b)})`;
358
+ }
359
+ if (fn === "@in" && args.length === 2) {
360
+ return compileIn(args[0], args[1], b);
361
+ }
362
+ if (target === undefined) {
363
+ if (fn === "size" && args.length === 1)
364
+ return compileSize(args[0], b);
365
+ if (fn === "contains" && args.length === 2)
366
+ return compileContains(args[0], args[1], b);
367
+ if (fn === "startsWith" && args.length === 2)
368
+ return compileStartsWith(args[0], args[1], b);
369
+ if (fn === "endsWith" && args.length === 2)
370
+ return compileEndsWith(args[0], args[1], b);
371
+ if (fn === "matches" && args.length === 2)
372
+ return compileMatches(args[0], args[1], b);
373
+ if (fn === "list" && args.length === 1) {
374
+ throw new KernelError("filter_invalid", {
375
+ reason: "list() is a hint that must sit inside `in` or a comprehension (.all/.exists), not stand alone",
376
+ });
377
+ }
378
+ }
379
+ if (target) {
380
+ // `$backlinks_static().size()` — method-form size on a collection.
381
+ if (fn === "size" && args.length === 0) {
382
+ const collection = asGraphCollection(target);
383
+ if (collection)
384
+ return compileGraphCollectionSize(collection, b);
385
+ }
386
+ if (fn === "startsWith" && args.length === 1)
387
+ return compileStartsWith(target, args[0], b);
388
+ if (fn === "endsWith" && args.length === 1)
389
+ return compileEndsWith(target, args[0], b);
390
+ if (fn === "contains" && args.length === 1)
391
+ return compileContains(target, args[0], b);
392
+ if (fn === "matches" && args.length === 1)
393
+ return compileMatches(target, args[0], b);
394
+ }
395
+ // Graph membership predicates: $in_static / $has_static (§11.2).
396
+ const membership = asGraphMembership(expr);
397
+ if (membership)
398
+ return compileGraphMembership(membership, b);
399
+ const collection = asGraphCollection(expr);
400
+ if (collection) {
401
+ throw new KernelError("filter_invalid", {
402
+ reason: `$${collection.direction}() is a collection; use .size(), .exists(), or .all()`,
403
+ });
404
+ }
405
+ assertNotReservedGraphName(fn);
406
+ throw new KernelError("filter_invalid", {
407
+ reason: `unsupported function: ${target ? "<target>." : ""}${fn}(...) with ${args.length} arg(s)`,
408
+ });
409
+ }
410
+ // -----------------------------------------------------------------------------
411
+ // Graph predicates (design §11.2, WS4) — Postgres mirror of the SQLite
412
+ // emitters in compile-filter.ts. See there for the directional model.
413
+ // -----------------------------------------------------------------------------
414
+ function globRegex(glob) {
415
+ return `^${globToRegexSource(glob)}$`;
416
+ }
417
+ function graphSubqueryCore(direction, b, opts) {
418
+ const alias = opts.alias ?? b.nextAlias();
419
+ if (direction === "in" || direction === "backlinks") {
420
+ const parts = [
421
+ "FROM links l",
422
+ `JOIN versions ${alias} ON l.source_id = ${alias}.document_id AND ${alias}.next_id IS NULL`,
423
+ "WHERE l.target_id = versions.document_id",
424
+ ];
425
+ if (opts.glob !== undefined)
426
+ parts.push(`AND ${alias}.path ~ ${b.push(globRegex(opts.glob))}`);
427
+ if (opts.field !== undefined) {
428
+ parts.push(`AND l.field = ${b.push(opts.field === "$body" ? BODY_FIELD : opts.field)}`);
429
+ }
430
+ parts.push(`AND ${visibilityForAlias(b, alias)}`);
431
+ return { from: parts.join(" "), alias };
432
+ }
433
+ // has / links: outer = source; other end is the (maybe-dangling) target.
434
+ const parts = [
435
+ `FROM links l LEFT JOIN versions ${alias} ON l.target_id = ${alias}.document_id AND ${alias}.next_id IS NULL`,
436
+ "WHERE l.source_id = versions.document_id",
437
+ ];
438
+ if (opts.glob !== undefined) {
439
+ parts.push(`AND COALESCE(${alias}.path, l.target_norm) ~ ${b.push(globRegex(opts.glob))}`);
440
+ }
441
+ if (opts.field !== undefined) {
442
+ parts.push(`AND l.field = ${b.push(opts.field === "$body" ? BODY_FIELD : opts.field)}`);
443
+ }
444
+ parts.push(`AND (l.target_id IS NULL OR ${visibilityForAlias(b, alias)})`);
445
+ return { from: parts.join(" "), alias };
446
+ }
447
+ function compileGraphMembership(m, b) {
448
+ const core = graphSubqueryCore(m.direction, b, { glob: m.glob, field: m.field });
449
+ return `EXISTS (SELECT 1 ${core.from})`;
450
+ }
451
+ function compileGraphCollectionSize(c, b) {
452
+ const core = graphSubqueryCore(c.direction, b, {});
453
+ return `(SELECT COUNT(*) ${core.from})`;
454
+ }
455
+ function compileBinary(op, a, b_, b) {
456
+ // For comparison ops on frontmatter text vs a typed literal, we need
457
+ // a cast. Detect the RHS literal shape and cast the LHS accordingly.
458
+ const isBool = op === "AND" || op === "OR";
459
+ if (isBool) {
460
+ return `(${compileExpr(a, b)} ${op} ${compileExpr(b_, b)})`;
461
+ }
462
+ // For comparison, if the LHS is a bare frontmatter ident/select and
463
+ // the RHS is a constant, cast the text extract to the const's type.
464
+ const lhsFm = fmPathIfPure(a);
465
+ const rhsConst = unwrapConstant(b_);
466
+ if (lhsFm !== undefined && rhsConst !== undefined) {
467
+ const rhsSql = compileConst(b_, b);
468
+ return typedCompare(lhsFm, op, rhsConst, rhsSql);
469
+ }
470
+ // Otherwise, compare the two expressions verbatim. Both sides text.
471
+ return `(${compileExpr(a, b)} ${op} ${compileExpr(b_, b)})`;
472
+ }
473
+ function fmPathIfPure(expr) {
474
+ if (expr.exprKind.case === "identExpr") {
475
+ const name = expr.exprKind.value.name;
476
+ if (name.startsWith(INTRINSIC_PREFIX))
477
+ return undefined;
478
+ return [name];
479
+ }
480
+ if (expr.exprKind.case === "selectExpr") {
481
+ const path = collectSelectPath(expr);
482
+ if (!path)
483
+ return undefined;
484
+ if (path[0].startsWith(INTRINSIC_PREFIX))
485
+ return undefined;
486
+ return path;
487
+ }
488
+ return undefined;
489
+ }
490
+ function typedCompare(fmPath, op, rhsConst, rhsSql) {
491
+ const textExpr = frontmatterText(fmPath);
492
+ if (typeof rhsConst === "number") {
493
+ // json_extract in SQLite auto-typecasts number frontmatter to number.
494
+ // For PG we cast the ->>text back with `::numeric`, but a missing key
495
+ // yields NULL — NULL compares are FALSE (matches SQLite semantics of
496
+ // missing key → predicate false).
497
+ return `(${textExpr})::numeric ${op} ${rhsSql}::numeric`;
498
+ }
499
+ if (typeof rhsConst === "bigint") {
500
+ return `(${textExpr})::numeric ${op} ${rhsSql}::numeric`;
501
+ }
502
+ if (typeof rhsConst === "boolean") {
503
+ return `(${textExpr})::boolean ${op} ${rhsConst ? "TRUE" : "FALSE"}`;
504
+ }
505
+ // Strings and everything else — direct text compare.
506
+ return `(${textExpr}) ${op} ${rhsSql}`;
507
+ }
508
+ function compileContains(haystack, needle, b) {
509
+ const h = compileExpr(haystack, b);
510
+ const n = compileExpr(needle, b);
511
+ return `(position(${n} in ${h}) > 0)`;
512
+ }
513
+ function compileStartsWith(target, prefix, b) {
514
+ const constPrefix = unwrapConstant(prefix);
515
+ const t = compileExpr(target, b);
516
+ if (typeof constPrefix === "string") {
517
+ const escaped = constPrefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
518
+ return `(${t} LIKE ${b.push(`${escaped}%`)})`;
519
+ }
520
+ const p = compileExpr(prefix, b);
521
+ return `(substring(${t} from 1 for length(${p})) = ${p})`;
522
+ }
523
+ function compileEndsWith(target, suffix, b) {
524
+ const constSuffix = unwrapConstant(suffix);
525
+ const t = compileExpr(target, b);
526
+ if (typeof constSuffix === "string") {
527
+ const escaped = constSuffix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
528
+ return `(${t} LIKE ${b.push(`%${escaped}`)})`;
529
+ }
530
+ const s = compileExpr(suffix, b);
531
+ return `(substring(${t} from length(${t}) - length(${s}) + 1) = ${s})`;
532
+ }
533
+ function compileMatches(target, pattern, b) {
534
+ // POSIX ARE via `~`. Bad pattern → SQLSTATE 2201B, adapter maps to
535
+ // filter_invalid; here it's just a query error.
536
+ const t = compileExpr(target, b);
537
+ const p = compileExpr(pattern, b);
538
+ return `(${t} ~ ${p})`;
539
+ }
540
+ // -----------------------------------------------------------------------------
541
+ // list() polymorphism + membership
542
+ // -----------------------------------------------------------------------------
543
+ function compileIn(needle, container, b) {
544
+ const listHint = unwrapListHint(container);
545
+ if (!listHint) {
546
+ throw new KernelError("filter_invalid", {
547
+ reason: '@in without list(field): use `"x" in list(field)` for polymorphic membership',
548
+ });
549
+ }
550
+ const fmPath = fmPathIfPure(listHint);
551
+ if (!fmPath) {
552
+ throw new KernelError("filter_invalid", {
553
+ reason: "list() must wrap a frontmatter key",
554
+ });
555
+ }
556
+ const jsonbPath = frontmatterPath(fmPath);
557
+ const needleConst = unwrapConstant(needle);
558
+ if (needleConst !== undefined) {
559
+ // Build a jsonb literal for the value to compare/contain.
560
+ const jsonbLit = jsonbLiteral(needleConst);
561
+ if (jsonbLit === null) {
562
+ // Non-primitive needle — fall through to expression path below.
563
+ }
564
+ else {
565
+ // Scalar match: `path = '"v"'::jsonb`
566
+ // List match: `path @> '["v"]'::jsonb`
567
+ const scalarLit = b.push(jsonbLit.scalar);
568
+ const arrayLit = b.push(jsonbLit.array);
569
+ return `(${jsonbPath} = ${scalarLit}::jsonb OR ${jsonbPath} @> ${arrayLit}::jsonb)`;
570
+ }
571
+ }
572
+ // Dynamic needle — fall back to typed text compare + jsonb_array_elements_text.
573
+ const n = compileExpr(needle, b);
574
+ const textPath = frontmatterText(fmPath);
575
+ return `(${textPath} = ${n} OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(${jsonbPath}) v WHERE v = ${n}))`;
576
+ }
577
+ function jsonbLiteral(v) {
578
+ if (typeof v === "string") {
579
+ const j = JSON.stringify(v);
580
+ return { scalar: j, array: `[${j}]` };
581
+ }
582
+ if (typeof v === "number" || typeof v === "bigint") {
583
+ const s = String(v);
584
+ return { scalar: s, array: `[${s}]` };
585
+ }
586
+ if (typeof v === "boolean") {
587
+ return { scalar: v ? "true" : "false", array: v ? "[true]" : "[false]" };
588
+ }
589
+ return null;
590
+ }
591
+ function compileSize(inner, b) {
592
+ const collection = asGraphCollection(inner);
593
+ if (collection)
594
+ return compileGraphCollectionSize(collection, b);
595
+ const listHint = unwrapListHint(inner);
596
+ if (listHint) {
597
+ const fmPath = fmPathIfPure(listHint);
598
+ if (fmPath) {
599
+ const path = frontmatterPath(fmPath);
600
+ // jsonb_typeof: 'array' → jsonb_array_length; scalar → 1; null → 0.
601
+ return `(CASE
602
+ WHEN ${path} IS NULL THEN 0
603
+ WHEN jsonb_typeof(${path}) = 'array' THEN jsonb_array_length(${path})
604
+ ELSE 1
605
+ END)`;
606
+ }
607
+ }
608
+ // Fallback: string length.
609
+ return `length(${compileExpr(inner, b)})`;
610
+ }
611
+ // -----------------------------------------------------------------------------
612
+ // Comprehensions
613
+ // -----------------------------------------------------------------------------
614
+ function compileComprehension(expr, b) {
615
+ if (expr.exprKind.case !== "comprehensionExpr") {
616
+ throw new Error("compileComprehension: wrong kind");
617
+ }
618
+ const comp = expr.exprKind.value;
619
+ // Graph collection comprehension: $backlinks_static().exists(d, pred) etc.
620
+ const collection = asGraphCollection(comp.iterRange);
621
+ if (collection)
622
+ return compileGraphComprehension(comp, collection, b);
623
+ const listHint = unwrapListHint(comp.iterRange);
624
+ if (!listHint) {
625
+ throw new KernelError("filter_invalid", {
626
+ reason: "comprehensions must iterate over list(field)",
627
+ });
628
+ }
629
+ const fmPath = fmPathIfPure(listHint);
630
+ if (!fmPath) {
631
+ throw new KernelError("filter_invalid", {
632
+ reason: "list() must wrap a frontmatter key",
633
+ });
634
+ }
635
+ const shape = classifyComprehension(comp);
636
+ if (shape === "unsupported") {
637
+ throw new KernelError("filter_invalid", {
638
+ reason: "unsupported comprehension shape (use .all or .exists)",
639
+ });
640
+ }
641
+ const jsonbPath = frontmatterPath(fmPath);
642
+ const userPred = extractUserPredicate(comp.loopStep);
643
+ const arrayPred = compileWithIterSubstitution(userPred, comp.iterVar, "elem.value::text", b);
644
+ const scalarPred = compileWithIterSubstitution(userPred, comp.iterVar, frontmatterText(fmPath), b);
645
+ if (shape === "exists") {
646
+ return `(
647
+ (${jsonbPath} IS NOT NULL AND jsonb_typeof(${jsonbPath}) <> 'array' AND (${scalarPred}))
648
+ OR (jsonb_typeof(${jsonbPath}) = 'array' AND EXISTS (
649
+ SELECT 1 FROM jsonb_array_elements_text(${jsonbPath}) AS elem(value) WHERE ${arrayPred}
650
+ ))
651
+ )`;
652
+ }
653
+ return `(
654
+ CASE
655
+ WHEN ${jsonbPath} IS NULL THEN TRUE
656
+ WHEN jsonb_typeof(${jsonbPath}) <> 'array' THEN (${scalarPred})
657
+ ELSE NOT EXISTS (
658
+ SELECT 1 FROM jsonb_array_elements_text(${jsonbPath}) AS elem(value) WHERE NOT (${arrayPred})
659
+ )
660
+ END
661
+ )`;
662
+ }
663
+ function classifyComprehension(comp) {
664
+ const init = unwrapConstant(comp.accuInit);
665
+ if (init === true)
666
+ return "all";
667
+ if (init === false)
668
+ return "exists";
669
+ return "unsupported";
670
+ }
671
+ function extractUserPredicate(loopStep) {
672
+ if (loopStep.exprKind.case === "callExpr") {
673
+ const call = loopStep.exprKind.value;
674
+ if ((call.function === "_&&_" || call.function === "_||_") && call.args.length === 2) {
675
+ const [first, second] = call.args;
676
+ if (first?.exprKind.case === "identExpr" &&
677
+ (first.exprKind.value.name === "@result" || first.exprKind.value.name === "__result__")) {
678
+ return second;
679
+ }
680
+ if (second?.exprKind.case === "identExpr" &&
681
+ (second.exprKind.value.name === "@result" || second.exprKind.value.name === "__result__")) {
682
+ return first;
683
+ }
684
+ }
685
+ }
686
+ return loopStep;
687
+ }
688
+ /**
689
+ * Recursively compile a predicate expression, substituting occurrences
690
+ * of `iterVar` with the given SQL expression. Kept minimal — supports
691
+ * comparisons, boolean combinators, and the string methods we care
692
+ * about.
693
+ */
694
+ function compileWithIterSubstitution(expr, iterVar, iterSql, b) {
695
+ const kind = expr.exprKind.case;
696
+ if (kind === "identExpr" && expr.exprKind.value.name === iterVar) {
697
+ return iterSql;
698
+ }
699
+ if (kind === "callExpr") {
700
+ const call = expr.exprKind.value;
701
+ const target = call.target;
702
+ const compiledArgs = call.args.map((a) => compileWithIterSubstitution(a, iterVar, iterSql, b));
703
+ const compiledTarget = target
704
+ ? compileWithIterSubstitution(target, iterVar, iterSql, b)
705
+ : undefined;
706
+ return combineCallWithSubstitutedArgs(call.function, compiledTarget, compiledArgs, b);
707
+ }
708
+ if (kind === "selectExpr") {
709
+ const root = collectSelectPath(expr)?.[0];
710
+ if (root === iterVar) {
711
+ throw new KernelError("filter_invalid", {
712
+ reason: `member access on comprehension iter-var '${iterVar}' is not yet supported`,
713
+ });
714
+ }
715
+ return compileExpr(expr, b);
716
+ }
717
+ return compileExpr(expr, b);
718
+ }
719
+ function combineCallWithSubstitutedArgs(fn, target, args, b) {
720
+ const binOp = BIN_OPS[fn];
721
+ if (binOp && args.length === 2 && !target) {
722
+ return `(${args[0]} ${binOp} ${args[1]})`;
723
+ }
724
+ if (fn === "!_" && args.length === 1 && !target) {
725
+ return `NOT (${args[0]})`;
726
+ }
727
+ if (fn === "startsWith") {
728
+ const t = target ?? args[0];
729
+ const p = target ? args[0] : args[1];
730
+ if (t === undefined || p === undefined)
731
+ throw new KernelError("filter_invalid", { reason: "startsWith needs 2 args" });
732
+ return `(substring(${t} from 1 for length(${p})) = ${p})`;
733
+ }
734
+ if (fn === "endsWith") {
735
+ const t = target ?? args[0];
736
+ const p = target ? args[0] : args[1];
737
+ if (t === undefined || p === undefined)
738
+ throw new KernelError("filter_invalid", { reason: "endsWith needs 2 args" });
739
+ return `(substring(${t} from length(${t}) - length(${p}) + 1) = ${p})`;
740
+ }
741
+ if (fn === "contains") {
742
+ const t = target ?? args[0];
743
+ const n = target ? args[0] : args[1];
744
+ if (t === undefined || n === undefined)
745
+ throw new KernelError("filter_invalid", { reason: "contains needs 2 args" });
746
+ return `(position(${n} in ${t}) > 0)`;
747
+ }
748
+ // Silence unused
749
+ void b;
750
+ throw new KernelError("filter_invalid", {
751
+ reason: `unsupported call inside comprehension predicate: ${fn}`,
752
+ });
753
+ }
754
+ // -----------------------------------------------------------------------------
755
+ // Graph collection comprehensions (design §11.2, WS4).
756
+ // -----------------------------------------------------------------------------
757
+ function compileGraphComprehension(comp, collection, b) {
758
+ const shape = classifyComprehension(comp);
759
+ if (shape === "unsupported") {
760
+ throw new KernelError("filter_invalid", {
761
+ reason: "unsupported comprehension over a link collection (use .exists or .all)",
762
+ });
763
+ }
764
+ const alias = b.nextAlias();
765
+ const core = graphSubqueryCore(collection.direction, b, { alias });
766
+ const userPred = extractUserPredicate(comp.loopStep);
767
+ const pred = compileAgainstAlias(userPred, comp.iterVar, alias, b);
768
+ if (shape === "exists") {
769
+ return `EXISTS (SELECT 1 ${core.from} AND (${pred}))`;
770
+ }
771
+ return `NOT EXISTS (SELECT 1 ${core.from} AND NOT (${pred}))`;
772
+ }
773
+ /**
774
+ * Compile a predicate where the iter var denotes the OTHER document's row
775
+ * (aliased). `d.field` → that doc's frontmatter; `d.$path` etc. → its
776
+ * intrinsics; everything else compiles against the outer `versions` row.
777
+ */
778
+ function compileAgainstAlias(expr, iterVar, alias, b) {
779
+ const kind = expr.exprKind.case;
780
+ if (kind === "identExpr") {
781
+ if (expr.exprKind.value.name === iterVar) {
782
+ throw new KernelError("filter_invalid", {
783
+ reason: `bare '${iterVar}' is not usable; access a field like ${iterVar}.status or ${iterVar}.$path`,
784
+ });
785
+ }
786
+ return compileExpr(expr, b);
787
+ }
788
+ if (kind === "selectExpr") {
789
+ const path = collectSelectPath(expr);
790
+ if (path && path[0] === iterVar) {
791
+ return aliasFieldAccess(alias, path.slice(1));
792
+ }
793
+ return compileExpr(expr, b);
794
+ }
795
+ if (kind === "callExpr") {
796
+ const call = expr.exprKind.value;
797
+ const target = call.target
798
+ ? compileAgainstAlias(call.target, iterVar, alias, b)
799
+ : undefined;
800
+ const args = call.args.map((a) => compileAgainstAlias(a, iterVar, alias, b));
801
+ return combineCallWithSubstitutedArgs(call.function, target, args, b);
802
+ }
803
+ return compileExpr(expr, b);
804
+ }
805
+ /** Field/intrinsic access on the aliased other-document row (text form). */
806
+ function aliasFieldAccess(alias, tail) {
807
+ const head = tail[0];
808
+ if (head === undefined) {
809
+ throw new KernelError("filter_invalid", { reason: "empty field access on link iter var" });
810
+ }
811
+ if (head.startsWith(INTRINSIC_PREFIX)) {
812
+ if (tail.length > 1) {
813
+ throw new KernelError("filter_invalid", { reason: "intrinsics have no subfields" });
814
+ }
815
+ const name = head.slice(INTRINSIC_PREFIX.length);
816
+ const col = INTRINSIC_COLUMNS[name];
817
+ if (!col)
818
+ throw new KernelError("filter_invalid", { reason: `unknown intrinsic $${name}` });
819
+ return col.replace(/^versions\./, `${alias}.`);
820
+ }
821
+ // jsonb text access on the aliased frontmatter.
822
+ const escaped = tail.map((p) => p.replace(/'/g, "''"));
823
+ if (escaped.length === 1)
824
+ return `${alias}.frontmatter ->> '${escaped[0]}'`;
825
+ let e = `${alias}.frontmatter -> '${escaped[0]}'`;
826
+ for (let i = 1; i < escaped.length - 1; i++)
827
+ e = `${e} -> '${escaped[i]}'`;
828
+ return `${e} ->> '${escaped[escaped.length - 1]}'`;
829
+ }
830
+ //# sourceMappingURL=compile-postgres.js.map