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,782 @@
1
+ /**
2
+ * Postgres adapter (m5-plan WS4).
3
+ *
4
+ * Contract:
5
+ * - Kernel calls are async; storage owns SQL emission.
6
+ * - `tx()` wraps `begin isolation level repeatable read` and reuses the
7
+ * same PoolClient inside via AsyncLocalStorage. Nested tx uses
8
+ * savepoints. On 40001/40P01 the whole tx body retries with jittered
9
+ * backoff up to 3 times.
10
+ * - int8 parser: PG returns bigints as strings by default; a custom
11
+ * parser converts them to JS numbers with a SafeInteger guard so id
12
+ * drift is loud, not silent.
13
+ * - Vectors serialize as pgvector's `'[…]'` literal; on read they
14
+ * arrive as a string and parse back to Float32Array.
15
+ */
16
+ import { AsyncLocalStorage } from "node:async_hooks";
17
+ import pg, { Pool } from "pg";
18
+ import { normalizeKey } from "../kernel/casefold.js";
19
+ import { KernelError } from "../kernel/errors.js";
20
+ import { contentHash } from "../markdown/content-hash.js";
21
+ import { GLOBAL_SCAN_CAP, SAFE_HEAD_TAIL, safeFrontier, safeHeadFromTail, } from "../storage/versions-since.js";
22
+ import { compileSearchPlan } from "./compile-postgres.js";
23
+ import { isRegexInvalid, isSerializationRetryable, isVersionRaceViolation } from "./errors.js";
24
+ import { migrate } from "./migrations/index.js";
25
+ // PG type OID for INT8 (bigint). We parse it to a JS number with a
26
+ // SafeInteger guard so schema drift (an id truly exceeding 2^53) fails
27
+ // loud, not silently rounds. The parser is installed on a per-pool
28
+ // TypeOverrides so importing this adapter never mutates the
29
+ // process-wide `pg.types` registry.
30
+ const PG_OID_INT8 = 20;
31
+ function safeInt8Parser(val) {
32
+ const n = Number(val);
33
+ if (!Number.isSafeInteger(n)) {
34
+ throw new Error(`postgres int8 value ${val} is not a safe JS integer`);
35
+ }
36
+ return n;
37
+ }
38
+ function makeTypeOverrides() {
39
+ // pg's TypeOverrides constructor takes an optional parent registry
40
+ // and lets you override per OID. We narrow the return to
41
+ // CustomTypesConfig (the Pool option's shape) so callers see the
42
+ // config object rather than the concrete TypeOverrides class.
43
+ // biome-ignore lint/suspicious/noExplicitAny: pg's types export lacks TypeOverrides.
44
+ const overrides = new pg.TypeOverrides();
45
+ overrides.setTypeParser(PG_OID_INT8, safeInt8Parser);
46
+ return overrides;
47
+ }
48
+ function parseVectorLiteral(v) {
49
+ if (v instanceof Float32Array)
50
+ return v;
51
+ if (typeof v !== "string")
52
+ throw new Error(`unexpected vector shape: ${typeof v}`);
53
+ // pgvector emits "[a,b,c]" without spaces (defensive: allow spaces).
54
+ const trimmed = v.startsWith("[") && v.endsWith("]") ? v.slice(1, -1) : v;
55
+ if (trimmed.length === 0)
56
+ return new Float32Array(0);
57
+ const parts = trimmed.split(",");
58
+ const out = new Float32Array(parts.length);
59
+ for (let i = 0; i < parts.length; i++) {
60
+ out[i] = Number.parseFloat(parts[i]);
61
+ }
62
+ return out;
63
+ }
64
+ function toVectorLiteral(v) {
65
+ // pgvector accepts "[a,b,c]"; JSON.stringify gives the exact form.
66
+ const arr = v instanceof Float32Array ? Array.from(v) : v;
67
+ return `[${arr.join(",")}]`;
68
+ }
69
+ function parsePostgresUrl(url) {
70
+ if (!url.startsWith("postgres:") && !url.startsWith("postgresql:")) {
71
+ throw new Error(`invalid postgres database url: ${url}`);
72
+ }
73
+ return url;
74
+ }
75
+ class PostgresStorage {
76
+ pool;
77
+ txClient;
78
+ // Savepoint depth per-tx-client; keyed off the client object.
79
+ txDepthByClient = new WeakMap();
80
+ constructor(url) {
81
+ this.pool = new Pool({ connectionString: url, types: makeTypeOverrides() });
82
+ this.txClient = new AsyncLocalStorage();
83
+ }
84
+ // Route each call to the tx client (if inside a tx) or borrow a fresh
85
+ // one from the pool.
86
+ async withClient(fn) {
87
+ const inTx = this.txClient.getStore();
88
+ if (inTx)
89
+ return fn(inTx);
90
+ const client = await this.pool.connect();
91
+ try {
92
+ return await fn(client);
93
+ }
94
+ finally {
95
+ client.release();
96
+ }
97
+ }
98
+ async close() {
99
+ await this.pool.end();
100
+ }
101
+ async migrate() {
102
+ const client = await this.pool.connect();
103
+ try {
104
+ await migrate(client);
105
+ }
106
+ finally {
107
+ client.release();
108
+ }
109
+ }
110
+ async tx(fn) {
111
+ const existing = this.txClient.getStore();
112
+ if (existing) {
113
+ // Nested — use a savepoint. Depth is per-client so parallel tx
114
+ // on different clients don't clash.
115
+ const depth = (this.txDepthByClient.get(existing) ?? 0) + 1;
116
+ this.txDepthByClient.set(existing, depth);
117
+ const name = `sp_${depth}`;
118
+ await existing.query(`savepoint ${name}`);
119
+ try {
120
+ const result = await fn();
121
+ await existing.query(`release savepoint ${name}`);
122
+ return result;
123
+ }
124
+ catch (err) {
125
+ await existing.query(`rollback to savepoint ${name}`).catch(() => { });
126
+ await existing.query(`release savepoint ${name}`).catch(() => { });
127
+ throw err;
128
+ }
129
+ finally {
130
+ const now = this.txDepthByClient.get(existing) ?? 0;
131
+ this.txDepthByClient.set(existing, Math.max(0, now - 1));
132
+ }
133
+ }
134
+ // Top-level tx — REPEATABLE READ + retry on 40001/40P01.
135
+ const maxAttempts = 3;
136
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
137
+ const client = await this.pool.connect();
138
+ try {
139
+ await client.query("begin isolation level repeatable read");
140
+ this.txDepthByClient.set(client, 0);
141
+ try {
142
+ const result = await this.txClient.run(client, fn);
143
+ await client.query("commit");
144
+ return result;
145
+ }
146
+ catch (err) {
147
+ await client.query("rollback").catch(() => { });
148
+ throw err;
149
+ }
150
+ }
151
+ catch (err) {
152
+ // Retry serialization failures AND partial-index race violations.
153
+ // Both indicate a concurrent writer beat us; a fresh tx re-runs
154
+ // the kernel's app-level pre-check which raises the right
155
+ // KernelError (create_conflict / stale_prev / path_taken).
156
+ const retryable = isSerializationRetryable(err) || isVersionRaceViolation(err);
157
+ if (retryable && attempt < maxAttempts) {
158
+ const delayMs = Math.floor(Math.random() * 25 * attempt);
159
+ await new Promise((r) => setTimeout(r, delayMs));
160
+ continue;
161
+ }
162
+ throw err;
163
+ }
164
+ finally {
165
+ this.txDepthByClient.delete(client);
166
+ client.release();
167
+ }
168
+ }
169
+ // Unreachable: the loop either returns or throws on every attempt.
170
+ throw new Error("tx: retry loop exited without decision (unreachable)");
171
+ }
172
+ async repos_list() {
173
+ return this.withClient(async (c) => {
174
+ const res = await c.query("select id, slug, path_config, link_config, created_at from repos order by slug");
175
+ return res.rows.map(hydrateRepo);
176
+ });
177
+ }
178
+ async repos_create(input) {
179
+ return this.withClient(async (c) => {
180
+ const res = await c.query("insert into repos(slug, slug_norm, created_at) values ($1, $2, $3) returning id, slug, path_config, link_config, created_at", [input.slug, normalizeKey(input.slug), input.created_at]);
181
+ return hydrateRepo(res.rows[0]);
182
+ });
183
+ }
184
+ async repos_rename(id, new_slug) {
185
+ return this.withClient(async (c) => {
186
+ const res = await c.query("update repos set slug = $1, slug_norm = $2 where id = $3 returning id, slug, path_config, link_config, created_at", [new_slug, normalizeKey(new_slug), id]);
187
+ if (res.rows.length === 0)
188
+ throw new Error(`repos_rename: repo ${id} not found`);
189
+ return hydrateRepo(res.rows[0]);
190
+ });
191
+ }
192
+ async repos_set_path_config(id, path_config) {
193
+ return this.withClient(async (c) => {
194
+ // path_config on the wire is JSON text; store as jsonb.
195
+ const res = await c.query("update repos set path_config = $1::jsonb where id = $2 returning id, slug, path_config, link_config, created_at", [path_config, id]);
196
+ if (res.rows.length === 0)
197
+ throw new Error(`repos_set_path_config: repo ${id} not found`);
198
+ return hydrateRepo(res.rows[0]);
199
+ });
200
+ }
201
+ async repos_set_link_config(id, link_config) {
202
+ return this.withClient(async (c) => {
203
+ // link_config on the wire is JSON text; store as jsonb.
204
+ const res = await c.query("update repos set link_config = $1::jsonb where id = $2 returning id, slug, path_config, link_config, created_at", [link_config, id]);
205
+ if (res.rows.length === 0)
206
+ throw new Error(`repos_set_link_config: repo ${id} not found`);
207
+ return hydrateRepo(res.rows[0]);
208
+ });
209
+ }
210
+ async repos_by_slug(slug) {
211
+ return this.withClient(async (c) => {
212
+ const res = await c.query("select id, slug, path_config, link_config, created_at from repos where slug_norm = $1", [normalizeKey(slug)]);
213
+ return res.rows[0] ? hydrateRepo(res.rows[0]) : null;
214
+ });
215
+ }
216
+ async repos_by_id(id) {
217
+ return this.withClient(async (c) => {
218
+ const res = await c.query("select id, slug, path_config, link_config, created_at from repos where id = $1", [id]);
219
+ return res.rows[0] ? hydrateRepo(res.rows[0]) : null;
220
+ });
221
+ }
222
+ async documents_create(repo_id) {
223
+ return this.withClient(async (c) => {
224
+ const res = await c.query("insert into documents(repo_id) values ($1) returning id, repo_id", [repo_id]);
225
+ return res.rows[0];
226
+ });
227
+ }
228
+ async version_insert(input) {
229
+ return this.tx(async () => {
230
+ // Same three-statement dance the SQLite adapter uses (design
231
+ // §7.2.2 obligation 1). Partial unique indexes enforce the
232
+ // invariants at the engine layer.
233
+ return this.withClient(async (c) => {
234
+ if (input.prev_id !== null) {
235
+ const upd = await c.query("update versions set next_id = id where id = $1 and document_id = $2 and next_id is null", [input.prev_id, input.document_id]);
236
+ if (upd.rowCount !== 1) {
237
+ throw new Error(`version_insert: prev_id ${input.prev_id} is not the current version of document ${input.document_id} (or does not exist)`);
238
+ }
239
+ }
240
+ const ins = await c.query(`insert into versions
241
+ (document_id, repo_id, prev_id, next_id, path, path_norm,
242
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash)
243
+ values ($1, $2, $3, null, $4, $5, $6, $7::jsonb, $8, $9, $10, $11)
244
+ returning id, document_id, repo_id, prev_id, next_id, path,
245
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash`, [
246
+ input.document_id,
247
+ input.repo_id,
248
+ input.prev_id,
249
+ input.path,
250
+ normalizeKey(input.path),
251
+ input.frontmatter_raw,
252
+ JSON.stringify(input.frontmatter),
253
+ input.body,
254
+ input.author,
255
+ input.created_at,
256
+ contentHash(input.frontmatter_raw, input.body),
257
+ ]);
258
+ const inserted = ins.rows[0];
259
+ if (input.prev_id !== null) {
260
+ await c.query("update versions set next_id = $1 where id = $2", [
261
+ inserted.id,
262
+ input.prev_id,
263
+ ]);
264
+ }
265
+ return inserted;
266
+ });
267
+ });
268
+ }
269
+ async version_by_id(id) {
270
+ return this.withClient(async (c) => {
271
+ const res = await c.query(`select id, document_id, repo_id, prev_id, next_id, path,
272
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
273
+ from versions where id = $1`, [id]);
274
+ return res.rows[0] ?? null;
275
+ });
276
+ }
277
+ async versions_current_by_paths(repo_id, paths) {
278
+ if (paths.length === 0)
279
+ return [];
280
+ const norms = paths.map(normalizeKey);
281
+ return this.withClient(async (c) => {
282
+ const res = await c.query(`select id, document_id, repo_id, prev_id, next_id, path,
283
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
284
+ from versions
285
+ where repo_id = $1 and next_id is null and path_norm = ANY($2::text[])`, [repo_id, norms]);
286
+ return res.rows;
287
+ });
288
+ }
289
+ async version_current(repo_id, path) {
290
+ return this.withClient(async (c) => {
291
+ // Case-insensitive identity: fold the query key to path_norm (§3.5.1).
292
+ const res = await c.query(`select id, document_id, repo_id, prev_id, next_id, path,
293
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
294
+ from versions where repo_id = $1 and path_norm = $2 and next_id is null`, [repo_id, normalizeKey(path)]);
295
+ return res.rows[0] ?? null;
296
+ });
297
+ }
298
+ async versions_live_by_repo(repo_id) {
299
+ return this.withClient(async (c) => {
300
+ const res = await c.query(`select id, document_id, repo_id, prev_id, next_id, path,
301
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
302
+ from versions
303
+ where repo_id = $1 and next_id is null
304
+ order by path`, [repo_id]);
305
+ return res.rows;
306
+ });
307
+ }
308
+ async fts_index(_version_id, _body) {
309
+ // The generated `fts_tsv` column keeps the index in sync automatically.
310
+ // No-op here for interface symmetry.
311
+ }
312
+ // Links derived index (design §11.2, migration 0003_links.sql).
313
+ async links_replace(repo_id, source_id, edges) {
314
+ return this.tx(async () => {
315
+ return this.withClient(async (c) => {
316
+ await c.query("delete from links where source_id = $1", [source_id]);
317
+ for (const e of edges) {
318
+ await c.query(`insert into links (repo_id, source_id, ord, field, target_raw, target_norm, target_id)
319
+ values ($1, $2, $3, $4, $5, $6, $7)`, [repo_id, source_id, e.ord, e.field, e.target_raw, e.target_norm, e.target_id]);
320
+ }
321
+ });
322
+ });
323
+ }
324
+ async links_clear(source_id) {
325
+ return this.withClient(async (c) => {
326
+ await c.query("delete from links where source_id = $1", [source_id]);
327
+ });
328
+ }
329
+ async links_resolve_dangling(repo_id, target_norm, document_id) {
330
+ return this.withClient(async (c) => {
331
+ const res = await c.query(`update links set target_id = $1
332
+ where repo_id = $2 and target_norm = $3 and target_id is null
333
+ and source_id <> $1`, [document_id, repo_id, target_norm]);
334
+ return res.rowCount ?? 0;
335
+ });
336
+ }
337
+ async links_by_source(source_id) {
338
+ return this.withClient(async (c) => {
339
+ const res = await c.query(`select repo_id, source_id, ord, field, target_raw, target_norm, target_id
340
+ from links where source_id = $1 order by ord`, [source_id]);
341
+ return res.rows;
342
+ });
343
+ }
344
+ async links_by_repo(repo_id) {
345
+ return this.withClient(async (c) => {
346
+ const res = await c.query(`select repo_id, source_id, ord, field, target_raw, target_norm, target_id
347
+ from links where repo_id = $1 order by source_id, ord`, [repo_id]);
348
+ return res.rows;
349
+ });
350
+ }
351
+ async links_adjacent_out(repo_id, source_ids) {
352
+ if (source_ids.length === 0)
353
+ return [];
354
+ // `= ANY($2::bigint[])` binds the whole id batch as one array param, so
355
+ // there's no per-id placeholder and no Postgres parameter-limit concern.
356
+ return this.withClient(async (c) => {
357
+ const res = await c.query(`select distinct source_id, target_id, field
358
+ from links
359
+ where repo_id = $1 and target_id is not null and source_id = ANY($2::bigint[])`, [repo_id, source_ids]);
360
+ return res.rows;
361
+ });
362
+ }
363
+ async links_adjacent_in(repo_id, target_ids) {
364
+ if (target_ids.length === 0)
365
+ return [];
366
+ return this.withClient(async (c) => {
367
+ const res = await c.query(`select distinct source_id, target_id, field
368
+ from links
369
+ where repo_id = $1 and target_id is not null and target_id = ANY($2::bigint[])`, [repo_id, target_ids]);
370
+ return res.rows;
371
+ });
372
+ }
373
+ async versions_current_by_documents(repo_id, document_ids) {
374
+ if (document_ids.length === 0)
375
+ return [];
376
+ return this.withClient(async (c) => {
377
+ const res = await c.query(`select id, document_id, repo_id, prev_id, next_id, path,
378
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
379
+ from versions
380
+ where repo_id = $1 and next_id is null and document_id = ANY($2::bigint[])`, [repo_id, document_ids]);
381
+ return res.rows;
382
+ });
383
+ }
384
+ async versions_live_document_ids_matching(repo_id, path_regexes) {
385
+ if (path_regexes.length === 0)
386
+ return [];
387
+ // `path ~ ANY($2)` matches against the whole regex array in one predicate;
388
+ // POSIX ARE, same engine the scope-glob compiler uses.
389
+ return this.withClient(async (c) => {
390
+ const res = await c.query(`select document_id from versions
391
+ where repo_id = $1 and next_id is null and path ~ ANY($2::text[])`, [repo_id, path_regexes]);
392
+ return res.rows.map((r) => r.document_id);
393
+ });
394
+ }
395
+ async versions_search(plan) {
396
+ if (plan.repo_ids.length === 0)
397
+ return [];
398
+ if (plan.scope.kind === "deny_all")
399
+ return [];
400
+ return this.withClient(async (c) => {
401
+ const { sql, params } = compileSearchPlan(plan);
402
+ try {
403
+ const res = await c.query(sql, params);
404
+ return res.rows;
405
+ }
406
+ catch (err) {
407
+ // A user regex that survives CEL parse but fails Postgres's
408
+ // POSIX ARE compiler surfaces as SQLSTATE 2201B. Map it to
409
+ // filter_invalid so the wire error catalog stays consistent
410
+ // (SQLite's RE2 UDF returns 0 for bad patterns, so this is
411
+ // the PG-only surface for the same failure mode).
412
+ if (isRegexInvalid(err)) {
413
+ throw new KernelError("filter_invalid", {
414
+ reason: `invalid regex in filter: ${err.message ?? "2201B"}`,
415
+ });
416
+ }
417
+ throw err;
418
+ }
419
+ });
420
+ }
421
+ async version_history(document_id, opts) {
422
+ return this.withClient(async (c) => {
423
+ const params = [document_id];
424
+ const clauses = [];
425
+ if (opts?.before) {
426
+ params.push(opts.before);
427
+ clauses.push(`chain.created_at < $${params.length}`);
428
+ }
429
+ const where = clauses.length > 0 ? ` where ${clauses.join(" and ")}` : "";
430
+ let limitClause = "";
431
+ if (opts?.limit) {
432
+ params.push(opts.limit);
433
+ limitClause = ` limit $${params.length}`;
434
+ }
435
+ const res = await c.query(`with recursive chain as (
436
+ select id, document_id, repo_id, prev_id, next_id, path,
437
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash, 0 as depth
438
+ from versions
439
+ where document_id = $1 and next_id is null
440
+ union all
441
+ select v.id, v.document_id, v.repo_id, v.prev_id, v.next_id, v.path,
442
+ v.frontmatter_raw, v.frontmatter, v.body, v.author, v.created_at,
443
+ v.content_hash, c.depth + 1
444
+ from versions v
445
+ join chain c on v.id = c.prev_id
446
+ )
447
+ select id, document_id, repo_id, prev_id, next_id, path,
448
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
449
+ from chain${where}
450
+ order by depth asc${limitClause}`, params);
451
+ return res.rows;
452
+ });
453
+ }
454
+ async versions_list(opts) {
455
+ return this.withClient(async (c) => {
456
+ // 1. Document ids in scope. Empty globs → all docs; else match by path
457
+ // regex (POSIX ARE, `path ~ ANY`), with `ever` gating live-only.
458
+ const hasGlob = opts.path_regexes.length > 0;
459
+ let docIds;
460
+ if (!hasGlob) {
461
+ const res = await c.query("select distinct document_id from versions where repo_id = $1", [opts.repo_id]);
462
+ docIds = res.rows.map((r) => Number(r.document_id));
463
+ }
464
+ else {
465
+ const liveClause = opts.ever ? "" : " and next_id is null";
466
+ const res = await c.query(`select distinct document_id from versions
467
+ where repo_id = $1${liveClause} and path ~ ANY($2::text[])`, [opts.repo_id, opts.path_regexes]);
468
+ docIds = res.rows.map((r) => Number(r.document_id));
469
+ }
470
+ if (docIds.length === 0)
471
+ return [];
472
+ // 2. Their versions, interleaved by id, within cursor bounds.
473
+ const params = [docIds];
474
+ let sql = `select id, document_id, repo_id, prev_id, next_id, path,
475
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
476
+ from versions where document_id = ANY($1::bigint[])`;
477
+ if (opts.after_id !== undefined) {
478
+ params.push(opts.after_id);
479
+ sql += ` and id > $${params.length}`;
480
+ }
481
+ if (opts.until_id !== undefined) {
482
+ params.push(opts.until_id);
483
+ sql += ` and id <= $${params.length}`;
484
+ }
485
+ params.push(opts.limit);
486
+ sql += ` order by id ${opts.order === "desc" ? "desc" : "asc"} limit $${params.length}`;
487
+ const res = await c.query(sql, params);
488
+ return res.rows;
489
+ });
490
+ }
491
+ async versions_since(opts) {
492
+ return this.withClient(async (c) => {
493
+ // Global scan (id, repo, age) — gaps are only meaningful on the global
494
+ // id sequence; the repo filter narrows the delivered rows only. On PG
495
+ // burned ids (rolled-back nextval) and commit-visibility skew make gaps
496
+ // routine, which is exactly what the safety window handles.
497
+ const lightRes = await c.query("select id, repo_id, created_at from versions where id > $1 order by id asc limit $2", [opts.after_id, GLOBAL_SCAN_CAP]);
498
+ const { upper_id } = safeFrontier(lightRes.rows.map((r) => ({
499
+ id: Number(r.id),
500
+ repo_id: Number(r.repo_id),
501
+ created_at_ms: Date.parse(r.created_at),
502
+ })), opts.after_id, opts.repo_id, opts.limit, opts.now_ms, opts.window_ms);
503
+ if (upper_id <= opts.after_id)
504
+ return { rows: [], next_id: opts.after_id };
505
+ const repoClause = opts.repo_id === undefined ? "" : " and repo_id = $3";
506
+ const params = opts.repo_id === undefined
507
+ ? [opts.after_id, upper_id]
508
+ : [opts.after_id, upper_id, opts.repo_id];
509
+ const res = await c.query(`select id, document_id, repo_id, prev_id, next_id, path,
510
+ frontmatter_raw, frontmatter, body, author, created_at, content_hash
511
+ from versions
512
+ where id > $1 and id <= $2${repoClause}
513
+ order by id asc`, params);
514
+ return { rows: res.rows, next_id: upper_id };
515
+ });
516
+ }
517
+ async versions_paths_by_ids(ids) {
518
+ const map = new Map();
519
+ if (ids.length === 0)
520
+ return map;
521
+ return this.withClient(async (c) => {
522
+ const res = await c.query("select id, path from versions where id = any($1)", [ids]);
523
+ for (const r of res.rows)
524
+ map.set(Number(r.id), r.path);
525
+ return map;
526
+ });
527
+ }
528
+ async versions_safe_head(now_ms, window_ms) {
529
+ return this.withClient(async (c) => {
530
+ const res = await c.query("select id, created_at from versions order by id desc limit $1", [SAFE_HEAD_TAIL]);
531
+ const tail = res.rows
532
+ .map((r) => ({ id: Number(r.id), repo_id: 0, created_at_ms: Date.parse(r.created_at) }))
533
+ .reverse(); // ascending by id
534
+ return safeHeadFromTail(tail, now_ms, window_ms);
535
+ });
536
+ }
537
+ async versions_live_index(opts) {
538
+ return this.withClient(async (c) => {
539
+ const res = await c.query(`select id, path, content_hash from versions
540
+ where repo_id = $1 and next_id is null and id > $2 and id <= $3
541
+ order by id asc limit $4`, [opts.repo_id, opts.after_id, opts.through_id, opts.limit]);
542
+ return res.rows.map((r) => ({
543
+ id: Number(r.id),
544
+ path: r.path,
545
+ content_hash: r.content_hash,
546
+ }));
547
+ });
548
+ }
549
+ async versions_missing_content_hash(opts) {
550
+ return this.withClient(async (c) => {
551
+ const repoClause = opts.repo_id === undefined ? "" : " and repo_id = $3";
552
+ const params = opts.repo_id === undefined
553
+ ? [opts.after_id, opts.limit]
554
+ : [opts.after_id, opts.limit, opts.repo_id];
555
+ const res = await c.query(`select id, frontmatter_raw, body from versions
556
+ where content_hash is null and id > $1${repoClause}
557
+ order by id asc limit $2`, params);
558
+ return res.rows.map((r) => ({
559
+ id: Number(r.id),
560
+ frontmatter_raw: r.frontmatter_raw,
561
+ body: r.body,
562
+ }));
563
+ });
564
+ }
565
+ async versions_set_content_hash(updates) {
566
+ if (updates.length === 0)
567
+ return;
568
+ return this.tx(async () => {
569
+ await this.withClient(async (c) => {
570
+ for (const u of updates) {
571
+ await c.query("update versions set content_hash = $1 where id = $2", [
572
+ u.content_hash,
573
+ u.id,
574
+ ]);
575
+ }
576
+ });
577
+ });
578
+ }
579
+ async chunks_upsert(version_id, model, chunks) {
580
+ if (chunks.length === 0) {
581
+ await this.tx(async () => {
582
+ await this.withClient((c) => c.query("delete from chunks where version_id = $1", [version_id]));
583
+ });
584
+ return;
585
+ }
586
+ // Validate cross-batch invariants before touching the db.
587
+ let dim = null;
588
+ for (const c of chunks) {
589
+ const len = c.embedding.length;
590
+ if (dim === null)
591
+ dim = len;
592
+ else if (len !== dim) {
593
+ throw new Error(`chunks_upsert: mixed embedding dimensions in one batch (${len} vs ${dim})`);
594
+ }
595
+ if (c.model !== model) {
596
+ throw new Error(`chunks_upsert: mixed models in one batch (${JSON.stringify(c.model)} vs ${JSON.stringify(model)})`);
597
+ }
598
+ }
599
+ await this.tx(async () => {
600
+ await this.withClient(async (c) => {
601
+ await c.query("delete from chunks where version_id = $1", [version_id]);
602
+ for (const chunk of chunks) {
603
+ await c.query("insert into chunks(version_id, ix, text, text_hash, model, embedding) values ($1, $2, $3, $4, $5, $6::vector)", [
604
+ version_id,
605
+ chunk.ix,
606
+ chunk.text,
607
+ chunk.text_hash,
608
+ chunk.model,
609
+ toVectorLiteral(chunk.embedding),
610
+ ]);
611
+ }
612
+ });
613
+ });
614
+ }
615
+ async chunks_by_hash(model, text_hashes) {
616
+ if (text_hashes.length === 0)
617
+ return [];
618
+ return this.withClient(async (c) => {
619
+ const res = await c.query(`select text_hash, embedding
620
+ from chunks
621
+ where model = $1
622
+ and text_hash = any($2::text[])
623
+ and embedding is not null`, [model, [...text_hashes]]);
624
+ // GROUP BY-per-hash: one row per hash, arbitrary embedding.
625
+ const seen = new Map();
626
+ for (const r of res.rows) {
627
+ if (!seen.has(r.text_hash))
628
+ seen.set(r.text_hash, r.embedding);
629
+ }
630
+ return [...seen.entries()].map(([text_hash, embedding]) => ({
631
+ text_hash,
632
+ embedding: parseVectorLiteral(embedding),
633
+ }));
634
+ });
635
+ }
636
+ async chunks_by_version(version_id) {
637
+ return this.withClient(async (c) => {
638
+ const res = await c.query(`select version_id, ix, text, text_hash, model, embedding
639
+ from chunks
640
+ where version_id = $1
641
+ order by ix`, [version_id]);
642
+ return res.rows.map((r) => ({
643
+ version_id: r.version_id,
644
+ ix: r.ix,
645
+ text: r.text,
646
+ text_hash: r.text_hash,
647
+ model: r.model,
648
+ embedding: r.embedding !== null ? parseVectorLiteral(r.embedding) : null,
649
+ }));
650
+ });
651
+ }
652
+ async chunks_model_summary() {
653
+ return this.withClient(async (c) => {
654
+ const res = await c.query(`select model, count(*)::int as chunk_count
655
+ from chunks
656
+ group by model
657
+ order by model`);
658
+ return res.rows;
659
+ });
660
+ }
661
+ async vector_search(repo_ids, model, embedding, k) {
662
+ if (repo_ids.length === 0 || k <= 0)
663
+ return [];
664
+ return this.withClient(async (c) => {
665
+ // Brute-force `<=>` (cosine distance). ROW_NUMBER-per-version
666
+ // collapses to best chunk per version — mirrors the SQLite
667
+ // adapter's shape.
668
+ const res = await c.query(`with scored as (
669
+ select chunks.version_id,
670
+ chunks.ix as chunk_ix,
671
+ (chunks.embedding <=> $1::vector) as score,
672
+ row_number() over (
673
+ partition by chunks.version_id
674
+ order by (chunks.embedding <=> $1::vector) asc, chunks.ix asc
675
+ ) as rn
676
+ from chunks
677
+ join versions on versions.id = chunks.version_id
678
+ where chunks.model = $2
679
+ and chunks.embedding is not null
680
+ and versions.next_id is null
681
+ and versions.repo_id = any($3::bigint[])
682
+ )
683
+ select version_id, chunk_ix, score::float8 as score
684
+ from scored
685
+ where rn = 1
686
+ order by score asc
687
+ limit $4`, [toVectorLiteral(embedding), model, [...repo_ids], k]);
688
+ return res.rows.map((r) => ({
689
+ version_id: r.version_id,
690
+ chunk_ix: r.chunk_ix,
691
+ score: Number(r.score),
692
+ }));
693
+ });
694
+ }
695
+ async backlog_enqueue(version_id) {
696
+ await this.withClient((c) => c.query(`insert into embedding_backlog(version_id, attempts, last_error, next_retry_at)
697
+ values ($1, 0, null, null)
698
+ on conflict(version_id) do update
699
+ set attempts = 0, last_error = null, next_retry_at = null`, [version_id]));
700
+ }
701
+ async backlog_dequeue(now, limit) {
702
+ if (limit <= 0)
703
+ return [];
704
+ return this.withClient(async (c) => {
705
+ const res = await c.query(`select version_id, attempts, last_error, next_retry_at
706
+ from embedding_backlog
707
+ where next_retry_at is null or next_retry_at <= $1
708
+ order by coalesce(next_retry_at, '') asc, version_id asc
709
+ limit $2`, [now, limit]);
710
+ return res.rows;
711
+ });
712
+ }
713
+ async backlog_retain(input) {
714
+ await this.withClient((c) => c.query(`update embedding_backlog
715
+ set attempts = $1, last_error = $2, next_retry_at = $3
716
+ where version_id = $4`, [input.attempts, input.last_error, input.next_retry_at, input.version_id]));
717
+ }
718
+ async backlog_delete(version_id) {
719
+ await this.withClient((c) => c.query("delete from embedding_backlog where version_id = $1", [version_id]));
720
+ }
721
+ async backlog_status(now) {
722
+ return this.withClient(async (c) => {
723
+ // Collapse the three counts + oldest into one query via `filter`
724
+ // and a min() aggregate — one round-trip instead of four.
725
+ const summary = (await c.query(`select count(*)::text as pending,
726
+ count(*) filter (where next_retry_at is null or next_retry_at <= $1)::text as due,
727
+ count(*) filter (where attempts > 0)::text as failing,
728
+ min(next_retry_at) filter (where next_retry_at is not null) as oldest
729
+ from embedding_backlog`, [now])).rows[0];
730
+ const recent = (await c.query(`select version_id, last_error from embedding_backlog
731
+ where last_error is not null
732
+ order by next_retry_at desc limit 5`)).rows;
733
+ const models = (await c.query("select model, count(*)::int as chunk_count from chunks group by model order by model")).rows;
734
+ return {
735
+ pending: Number(summary?.pending ?? "0"),
736
+ due: Number(summary?.due ?? "0"),
737
+ failing: Number(summary?.failing ?? "0"),
738
+ oldest_next_retry_at: summary?.oldest ?? null,
739
+ recent_errors: recent,
740
+ models,
741
+ };
742
+ });
743
+ }
744
+ /**
745
+ * Refuse to open a pre-noauth database. Such a database carries an
746
+ * `api_tokens` table; the collapsed migration chain starts at 0001 and
747
+ * would run against a mismatched schema. Probe for the legacy marker and
748
+ * refuse with a clear message (noauth plan §1 — no upgrade path).
749
+ */
750
+ async assertNotLegacy() {
751
+ const res = await this.withClient((c) => c.query("select to_regclass('public.api_tokens') is not null as exists"));
752
+ if (res.rows[0]?.exists) {
753
+ throw new Error("this database predates the no-auth schema (found legacy api_tokens table). " +
754
+ "Pre-noauth databases are unsupported — re-ingest into a fresh database.");
755
+ }
756
+ }
757
+ }
758
+ function hydrateRepo(row) {
759
+ // path_config / link_config arrive as already-parsed objects from jsonb.
760
+ // RepoRow types them as string | null (the SQLite raw shape); serialize
761
+ // back to a string so the kernel's JSON.parse path is uniform across
762
+ // adapters.
763
+ const asText = (v) => v === null || v === undefined ? null : JSON.stringify(v);
764
+ return {
765
+ id: row.id,
766
+ slug: row.slug,
767
+ path_config: asText(row.path_config),
768
+ link_config: asText(row.link_config),
769
+ created_at: row.created_at,
770
+ };
771
+ }
772
+ export const postgresAdapter = {
773
+ scheme: "postgres",
774
+ async open(config) {
775
+ const url = parsePostgresUrl(config.database);
776
+ const storage = new PostgresStorage(url);
777
+ await storage.assertNotLegacy();
778
+ await storage.migrate();
779
+ return storage;
780
+ },
781
+ };
782
+ //# sourceMappingURL=adapter.js.map