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.
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.js +48 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +13 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/format.d.ts +22 -0
- package/dist/cli/format.js +148 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/main.d.ts +16 -0
- package/dist/cli/main.js +1518 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/client/kernel-client.d.ts +97 -0
- package/dist/client/kernel-client.js +14 -0
- package/dist/client/kernel-client.js.map +1 -0
- package/dist/client/local.d.ts +26 -0
- package/dist/client/local.js +103 -0
- package/dist/client/local.js.map +1 -0
- package/dist/client/remote-mcp.d.ts +35 -0
- package/dist/client/remote-mcp.js +174 -0
- package/dist/client/remote-mcp.js.map +1 -0
- package/dist/embed/backfill.d.ts +24 -0
- package/dist/embed/backfill.js +33 -0
- package/dist/embed/backfill.js.map +1 -0
- package/dist/embed/chunker.d.ts +31 -0
- package/dist/embed/chunker.js +121 -0
- package/dist/embed/chunker.js.map +1 -0
- package/dist/embed/cmd-hook.d.ts +21 -0
- package/dist/embed/cmd-hook.js +100 -0
- package/dist/embed/cmd-hook.js.map +1 -0
- package/dist/embed/config.d.ts +61 -0
- package/dist/embed/config.js +109 -0
- package/dist/embed/config.js.map +1 -0
- package/dist/embed/hook.d.ts +38 -0
- package/dist/embed/hook.js +56 -0
- package/dist/embed/hook.js.map +1 -0
- package/dist/embed/http-hook.d.ts +13 -0
- package/dist/embed/http-hook.js +39 -0
- package/dist/embed/http-hook.js.map +1 -0
- package/dist/embed/worker.d.ts +62 -0
- package/dist/embed/worker.js +228 -0
- package/dist/embed/worker.js.map +1 -0
- package/dist/fixtures/hwe/characters/albion-ever.md +12 -0
- package/dist/fixtures/hwe/characters/cent.md +11 -0
- package/dist/fixtures/hwe/concepts/cent-demiurge-resonance.md +10 -0
- package/dist/fixtures/hwe/concepts/phase-looper.md +10 -0
- package/dist/fixtures/hwe/entities/demiurge.md +11 -0
- package/dist/fixtures/hwe/entities/monad.md +11 -0
- package/dist/fixtures/hwe/moc/globocorp-network.md +19 -0
- package/dist/fixtures/hwe/organizations/globocorp.md +11 -0
- package/dist/fixtures/hwe/readme.md +24 -0
- package/dist/fixtures/starship/crew/aria-okonkwo.md +14 -0
- package/dist/fixtures/starship/crew/bexley-orr.md +14 -0
- package/dist/fixtures/starship/crew/dax-thorne.md +14 -0
- package/dist/fixtures/starship/crew/isolde-marsh.md +16 -0
- package/dist/fixtures/starship/crew/kestrel-vance.md +15 -0
- package/dist/fixtures/starship/crew/quill-vasquez.md +14 -0
- package/dist/fixtures/starship/crew/soren-halloway.md +16 -0
- package/dist/fixtures/starship/encounters/the-drift-choir.md +14 -0
- package/dist/fixtures/starship/encounters/the-hollow-signal.md +16 -0
- package/dist/fixtures/starship/encounters/the-wandering-buoy.md +14 -0
- package/dist/fixtures/starship/equipment/coolant-loop-b.md +13 -0
- package/dist/fixtures/starship/equipment/emergency-ration-bars.md +12 -0
- package/dist/fixtures/starship/equipment/long-range-sensors.md +13 -0
- package/dist/fixtures/starship/equipment/plasma-manifold-3.md +15 -0
- package/dist/fixtures/starship/equipment/shuttle-corvid.md +13 -0
- package/dist/fixtures/starship/logs/marsh-4419-1.md +13 -0
- package/dist/fixtures/starship/logs/okonkwo-4415-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4413-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4420-1.md +12 -0
- package/dist/fixtures/starship/logs/thorne-4413-2.md +13 -0
- package/dist/fixtures/starship/logs/vance-4419-2.md +13 -0
- package/dist/fixtures/starship/misc/the-galley-ficus.md +11 -0
- package/dist/fixtures/starship/missions/the-cinder-run.md +17 -0
- package/dist/fixtures/starship/missions/the-drift-choir.md +17 -0
- package/dist/fixtures/starship/missions/the-hollow-signal.md +17 -0
- package/dist/fixtures/starship/missions/the-silent-beacon.md +17 -0
- package/dist/fixtures/starship/moc/crew.md +19 -0
- package/dist/fixtures/starship/moc/encounters.md +14 -0
- package/dist/fixtures/starship/moc/missions.md +15 -0
- package/dist/fixtures/starship/readme.md +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/auth/glob.d.ts +46 -0
- package/dist/kernel/auth/glob.js +112 -0
- package/dist/kernel/auth/glob.js.map +1 -0
- package/dist/kernel/auth/scope.d.ts +53 -0
- package/dist/kernel/auth/scope.js +83 -0
- package/dist/kernel/auth/scope.js.map +1 -0
- package/dist/kernel/casefold.d.ts +37 -0
- package/dist/kernel/casefold.js +44 -0
- package/dist/kernel/casefold.js.map +1 -0
- package/dist/kernel/constants.d.ts +21 -0
- package/dist/kernel/constants.js +22 -0
- package/dist/kernel/constants.js.map +1 -0
- package/dist/kernel/context.d.ts +70 -0
- package/dist/kernel/context.js +114 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/deletion.d.ts +50 -0
- package/dist/kernel/deletion.js +83 -0
- package/dist/kernel/deletion.js.map +1 -0
- package/dist/kernel/diff.d.ts +36 -0
- package/dist/kernel/diff.js +61 -0
- package/dist/kernel/diff.js.map +1 -0
- package/dist/kernel/errors.d.ts +37 -0
- package/dist/kernel/errors.js +55 -0
- package/dist/kernel/errors.js.map +1 -0
- package/dist/kernel/frontmatter-input.d.ts +32 -0
- package/dist/kernel/frontmatter-input.js +85 -0
- package/dist/kernel/frontmatter-input.js.map +1 -0
- package/dist/kernel/graph.d.ts +34 -0
- package/dist/kernel/graph.js +559 -0
- package/dist/kernel/graph.js.map +1 -0
- package/dist/kernel/history.d.ts +74 -0
- package/dist/kernel/history.js +143 -0
- package/dist/kernel/history.js.map +1 -0
- package/dist/kernel/kernel.d.ts +162 -0
- package/dist/kernel/kernel.js +675 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/kernel/path-config.d.ts +101 -0
- package/dist/kernel/path-config.js +171 -0
- package/dist/kernel/path-config.js.map +1 -0
- package/dist/kernel/query/ast.d.ts +24 -0
- package/dist/kernel/query/ast.js +52 -0
- package/dist/kernel/query/ast.js.map +1 -0
- package/dist/kernel/query/cel-parse.d.ts +37 -0
- package/dist/kernel/query/cel-parse.js +88 -0
- package/dist/kernel/query/cel-parse.js.map +1 -0
- package/dist/kernel/query/degrees.d.ts +31 -0
- package/dist/kernel/query/degrees.js +135 -0
- package/dist/kernel/query/degrees.js.map +1 -0
- package/dist/kernel/query/graph-ast.d.ts +51 -0
- package/dist/kernel/query/graph-ast.js +139 -0
- package/dist/kernel/query/graph-ast.js.map +1 -0
- package/dist/kernel/query/query.d.ts +56 -0
- package/dist/kernel/query/query.js +276 -0
- package/dist/kernel/query/query.js.map +1 -0
- package/dist/kernel/validation.d.ts +64 -0
- package/dist/kernel/validation.js +183 -0
- package/dist/kernel/validation.js.map +1 -0
- package/dist/kernel/version-id.d.ts +10 -0
- package/dist/kernel/version-id.js +21 -0
- package/dist/kernel/version-id.js.map +1 -0
- package/dist/kernel/wire.d.ts +148 -0
- package/dist/kernel/wire.js +6 -0
- package/dist/kernel/wire.js.map +1 -0
- package/dist/links/backfill.d.ts +28 -0
- package/dist/links/backfill.js +33 -0
- package/dist/links/backfill.js.map +1 -0
- package/dist/links/extract.d.ts +29 -0
- package/dist/links/extract.js +233 -0
- package/dist/links/extract.js.map +1 -0
- package/dist/links/link-config.d.ts +70 -0
- package/dist/links/link-config.js +107 -0
- package/dist/links/link-config.js.map +1 -0
- package/dist/links/maintain.d.ts +37 -0
- package/dist/links/maintain.js +91 -0
- package/dist/links/maintain.js.map +1 -0
- package/dist/links/repair.d.ts +46 -0
- package/dist/links/repair.js +96 -0
- package/dist/links/repair.js.map +1 -0
- package/dist/links/resolve.d.ts +41 -0
- package/dist/links/resolve.js +121 -0
- package/dist/links/resolve.js.map +1 -0
- package/dist/links/stale.d.ts +32 -0
- package/dist/links/stale.js +73 -0
- package/dist/links/stale.js.map +1 -0
- package/dist/markdown/content-hash.d.ts +34 -0
- package/dist/markdown/content-hash.js +46 -0
- package/dist/markdown/content-hash.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +69 -0
- package/dist/markdown/frontmatter.js +147 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/hash-backfill.d.ts +22 -0
- package/dist/markdown/hash-backfill.js +43 -0
- package/dist/markdown/hash-backfill.js.map +1 -0
- package/dist/mcp/query-syntax.d.ts +20 -0
- package/dist/mcp/query-syntax.js +194 -0
- package/dist/mcp/query-syntax.js.map +1 -0
- package/dist/mcp/render.d.ts +31 -0
- package/dist/mcp/render.js +110 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts +60 -0
- package/dist/mcp/server.js +170 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +93 -0
- package/dist/mcp/tools.js +1218 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/rest/conditional.d.ts +37 -0
- package/dist/rest/conditional.js +58 -0
- package/dist/rest/conditional.js.map +1 -0
- package/dist/rest/negotiate.d.ts +33 -0
- package/dist/rest/negotiate.js +54 -0
- package/dist/rest/negotiate.js.map +1 -0
- package/dist/rest/routes.d.ts +37 -0
- package/dist/rest/routes.js +744 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/seed/fixture-seed.d.ts +35 -0
- package/dist/seed/fixture-seed.js +75 -0
- package/dist/seed/fixture-seed.js.map +1 -0
- package/dist/seed/starship-config.d.ts +3 -0
- package/dist/seed/starship-config.js +5 -0
- package/dist/seed/starship-config.js.map +1 -0
- package/dist/server/headers.d.ts +36 -0
- package/dist/server/headers.js +30 -0
- package/dist/server/headers.js.map +1 -0
- package/dist/server/http-error.d.ts +53 -0
- package/dist/server/http-error.js +104 -0
- package/dist/server/http-error.js.map +1 -0
- package/dist/server/serve.d.ts +56 -0
- package/dist/server/serve.js +135 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/shell/audit.d.ts +32 -0
- package/dist/shell/audit.js +28 -0
- package/dist/shell/audit.js.map +1 -0
- package/dist/shell/guard.d.ts +50 -0
- package/dist/shell/guard.js +176 -0
- package/dist/shell/guard.js.map +1 -0
- package/dist/shell/keys.d.ts +36 -0
- package/dist/shell/keys.js +64 -0
- package/dist/shell/keys.js.map +1 -0
- package/dist/shell/login.d.ts +56 -0
- package/dist/shell/login.js +130 -0
- package/dist/shell/login.js.map +1 -0
- package/dist/shell/oidc.d.ts +62 -0
- package/dist/shell/oidc.js +102 -0
- package/dist/shell/oidc.js.map +1 -0
- package/dist/shell/policy.d.ts +100 -0
- package/dist/shell/policy.js +232 -0
- package/dist/shell/policy.js.map +1 -0
- package/dist/shell/proxy-policy.d.ts +34 -0
- package/dist/shell/proxy-policy.js +120 -0
- package/dist/shell/proxy-policy.js.map +1 -0
- package/dist/shell/proxy.d.ts +47 -0
- package/dist/shell/proxy.js +253 -0
- package/dist/shell/proxy.js.map +1 -0
- package/dist/shell/serve.d.ts +86 -0
- package/dist/shell/serve.js +0 -0
- package/dist/shell/serve.js.map +1 -0
- package/dist/shell/stdio.d.ts +55 -0
- package/dist/shell/stdio.js +63 -0
- package/dist/shell/stdio.js.map +1 -0
- package/dist/storage/registry.d.ts +20 -0
- package/dist/storage/registry.js +38 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/search-plan.d.ts +93 -0
- package/dist/storage/search-plan.js +15 -0
- package/dist/storage/search-plan.js.map +1 -0
- package/dist/storage/types.d.ts +448 -0
- package/dist/storage/types.js +14 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/versions-since.d.ts +72 -0
- package/dist/storage/versions-since.js +92 -0
- package/dist/storage/versions-since.js.map +1 -0
- package/dist/storage-postgres/adapter.d.ts +17 -0
- package/dist/storage-postgres/adapter.js +782 -0
- package/dist/storage-postgres/adapter.js.map +1 -0
- package/dist/storage-postgres/compile-postgres.d.ts +22 -0
- package/dist/storage-postgres/compile-postgres.js +830 -0
- package/dist/storage-postgres/compile-postgres.js.map +1 -0
- package/dist/storage-postgres/errors.d.ts +34 -0
- package/dist/storage-postgres/errors.js +50 -0
- package/dist/storage-postgres/errors.js.map +1 -0
- package/dist/storage-postgres/migrations/0001_init.sql +113 -0
- package/dist/storage-postgres/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-postgres/migrations/index.d.ts +22 -0
- package/dist/storage-postgres/migrations/index.js +70 -0
- package/dist/storage-postgres/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/adapter.d.ts +2 -0
- package/dist/storage-sqlite/adapter.js +688 -0
- package/dist/storage-sqlite/adapter.js.map +1 -0
- package/dist/storage-sqlite/compile-filter.d.ts +41 -0
- package/dist/storage-sqlite/compile-filter.js +881 -0
- package/dist/storage-sqlite/compile-filter.js.map +1 -0
- package/dist/storage-sqlite/compile-sqlite.d.ts +19 -0
- package/dist/storage-sqlite/compile-sqlite.js +176 -0
- package/dist/storage-sqlite/compile-sqlite.js.map +1 -0
- package/dist/storage-sqlite/migrations/0001_init.sql +123 -0
- package/dist/storage-sqlite/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-sqlite/migrations/index.d.ts +6 -0
- package/dist/storage-sqlite/migrations/index.js +41 -0
- package/dist/storage-sqlite/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/vec.d.ts +40 -0
- package/dist/storage-sqlite/vec.js +62 -0
- package/dist/storage-sqlite/vec.js.map +1 -0
- package/dist/sync/cursor.d.ts +36 -0
- package/dist/sync/cursor.js +66 -0
- package/dist/sync/cursor.js.map +1 -0
- package/dist/sync/daemon.d.ts +40 -0
- package/dist/sync/daemon.js +202 -0
- package/dist/sync/daemon.js.map +1 -0
- package/dist/sync/feed.d.ts +38 -0
- package/dist/sync/feed.js +125 -0
- package/dist/sync/feed.js.map +1 -0
- package/dist/sync/fs-store.d.ts +8 -0
- package/dist/sync/fs-store.js +76 -0
- package/dist/sync/fs-store.js.map +1 -0
- package/dist/sync/intrinsics.d.ts +52 -0
- package/dist/sync/intrinsics.js +76 -0
- package/dist/sync/intrinsics.js.map +1 -0
- package/dist/sync/paths.d.ts +38 -0
- package/dist/sync/paths.js +56 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/push.d.ts +48 -0
- package/dist/sync/push.js +328 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/reconcile.d.ts +54 -0
- package/dist/sync/reconcile.js +252 -0
- package/dist/sync/reconcile.js.map +1 -0
- package/dist/sync/run.d.ts +27 -0
- package/dist/sync/run.js +46 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- 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
|