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,688 @@
|
|
|
1
|
+
import { RE2JS } from "@bufbuild/re2";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import { normalizeKey } from "../kernel/casefold.js";
|
|
4
|
+
import { contentHash } from "../markdown/content-hash.js";
|
|
5
|
+
import { GLOBAL_SCAN_CAP, SAFE_HEAD_TAIL, safeFrontier, safeHeadFromTail, } from "../storage/versions-since.js";
|
|
6
|
+
import { compileSearchPlan } from "./compile-sqlite.js";
|
|
7
|
+
import { migrate } from "./migrations/index.js";
|
|
8
|
+
import { decodeVectorBlob, encodeVectorBlob, loadSqliteVec } from "./vec.js";
|
|
9
|
+
const hydrateVersion = (row) => ({
|
|
10
|
+
...row,
|
|
11
|
+
frontmatter: JSON.parse(row.frontmatter),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Max ids per `IN (?,…)` chunk. SQLite's SQLITE_MAX_VARIABLE_NUMBER is 32766
|
|
15
|
+
* in modern builds; we leave generous headroom for the other bound params
|
|
16
|
+
* (repo_id, etc.) a query may carry alongside the id list.
|
|
17
|
+
*/
|
|
18
|
+
const ID_CHUNK_SIZE = 20000;
|
|
19
|
+
/**
|
|
20
|
+
* Run an `IN`-list query in chunks so a large id batch can't overflow
|
|
21
|
+
* SQLite's bound-variable cap, and flatten the per-chunk rows. `run` receives
|
|
22
|
+
* the `?,?,…` placeholder string and the chunk's ids. Callers whose query is
|
|
23
|
+
* `DISTINCT` and partitions rows by the chunked column (e.g. `source_id IN …`)
|
|
24
|
+
* get a correct union with no extra dedup; a `<= ID_CHUNK_SIZE` batch runs as
|
|
25
|
+
* a single query. An empty batch runs nothing.
|
|
26
|
+
*/
|
|
27
|
+
function chunkedInList(ids, run) {
|
|
28
|
+
if (ids.length === 0)
|
|
29
|
+
return [];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (let i = 0; i < ids.length; i += ID_CHUNK_SIZE) {
|
|
32
|
+
const chunk = ids.slice(i, i + ID_CHUNK_SIZE);
|
|
33
|
+
const ph = chunk.map(() => "?").join(",");
|
|
34
|
+
out.push(...run(ph, chunk));
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compile + cache RE2JS patterns per SqliteStorage instance. Returns null
|
|
40
|
+
* for patterns that fail to compile — matches SQLite's regexp() convention
|
|
41
|
+
* of "no match" rather than raising, so a bad pattern doesn't kill the
|
|
42
|
+
* whole query.
|
|
43
|
+
*/
|
|
44
|
+
const re2Cache = new WeakMap();
|
|
45
|
+
function compileRegexpCachedFor(db, pattern) {
|
|
46
|
+
let cache = re2Cache.get(db);
|
|
47
|
+
if (!cache) {
|
|
48
|
+
cache = new Map();
|
|
49
|
+
re2Cache.set(db, cache);
|
|
50
|
+
}
|
|
51
|
+
if (cache.has(pattern))
|
|
52
|
+
return cache.get(pattern) ?? null;
|
|
53
|
+
let compiled;
|
|
54
|
+
try {
|
|
55
|
+
compiled = RE2JS.compile(pattern);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
compiled = null;
|
|
59
|
+
}
|
|
60
|
+
cache.set(pattern, compiled);
|
|
61
|
+
return compiled;
|
|
62
|
+
}
|
|
63
|
+
function parseSqliteUrl(url) {
|
|
64
|
+
const m = url.match(/^sqlite:(.+)$/);
|
|
65
|
+
if (!m || !m[1]) {
|
|
66
|
+
throw new Error(`invalid sqlite database url: ${url}`);
|
|
67
|
+
}
|
|
68
|
+
return m[1];
|
|
69
|
+
}
|
|
70
|
+
class SqliteStorage {
|
|
71
|
+
db;
|
|
72
|
+
txDepth = 0;
|
|
73
|
+
constructor(path) {
|
|
74
|
+
this.db = new Database(path);
|
|
75
|
+
// Concurrency & correctness pragmas — see design §7.2.2 (obligation 3).
|
|
76
|
+
this.db.pragma("journal_mode = WAL");
|
|
77
|
+
this.db.pragma("synchronous = NORMAL");
|
|
78
|
+
this.db.pragma("foreign_keys = ON");
|
|
79
|
+
this.db.pragma("busy_timeout = 5000");
|
|
80
|
+
// regexp() user function — the SQL side of CEL's matches() (m2-plan
|
|
81
|
+
// §5). RE2JS keeps the semantics identical to Postgres's `~`.
|
|
82
|
+
const dbHandle = this.db;
|
|
83
|
+
this.db.function("regexp", { deterministic: true }, (pattern, input) => {
|
|
84
|
+
if (typeof pattern !== "string" || typeof input !== "string")
|
|
85
|
+
return 0;
|
|
86
|
+
const compiled = compileRegexpCachedFor(dbHandle, pattern);
|
|
87
|
+
if (!compiled)
|
|
88
|
+
return 0;
|
|
89
|
+
return compiled.matches(input) ? 1 : 0;
|
|
90
|
+
});
|
|
91
|
+
loadSqliteVec(this.db);
|
|
92
|
+
}
|
|
93
|
+
async close() {
|
|
94
|
+
this.db.close();
|
|
95
|
+
}
|
|
96
|
+
async migrate() {
|
|
97
|
+
migrate(this.db);
|
|
98
|
+
}
|
|
99
|
+
async tx(fn) {
|
|
100
|
+
// SQLite's transactions are on-connection and can hold across
|
|
101
|
+
// `await`s because better-sqlite3 is synchronous per call — no
|
|
102
|
+
// other statement runs on the connection between our steps.
|
|
103
|
+
// Kernel contract (design §7.2 / m5-plan WS1): tx bodies never
|
|
104
|
+
// await foreign I/O, only storage calls; that's safe.
|
|
105
|
+
if (this.txDepth === 0) {
|
|
106
|
+
this.db.exec("begin immediate");
|
|
107
|
+
this.txDepth++;
|
|
108
|
+
try {
|
|
109
|
+
const result = await fn();
|
|
110
|
+
this.db.exec("commit");
|
|
111
|
+
this.txDepth--;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.db.exec("rollback");
|
|
116
|
+
this.txDepth--;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Nested — use a savepoint.
|
|
121
|
+
const name = `sp_${this.txDepth}`;
|
|
122
|
+
this.db.exec(`savepoint ${name}`);
|
|
123
|
+
this.txDepth++;
|
|
124
|
+
try {
|
|
125
|
+
const result = await fn();
|
|
126
|
+
this.db.exec(`release ${name}`);
|
|
127
|
+
this.txDepth--;
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
this.db.exec(`rollback to ${name}`);
|
|
132
|
+
this.db.exec(`release ${name}`);
|
|
133
|
+
this.txDepth--;
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async repos_list() {
|
|
138
|
+
return this.db
|
|
139
|
+
.prepare("select id, slug, path_config, link_config, created_at from repos order by slug")
|
|
140
|
+
.all();
|
|
141
|
+
}
|
|
142
|
+
async repos_create(input) {
|
|
143
|
+
return this.db
|
|
144
|
+
.prepare("insert into repos(slug, slug_norm, created_at) values (?, ?, ?) returning id, slug, path_config, link_config, created_at")
|
|
145
|
+
.get(input.slug, normalizeKey(input.slug), input.created_at);
|
|
146
|
+
}
|
|
147
|
+
async repos_rename(id, new_slug) {
|
|
148
|
+
const row = this.db
|
|
149
|
+
.prepare("update repos set slug = ?, slug_norm = ? where id = ? returning id, slug, path_config, link_config, created_at")
|
|
150
|
+
.get(new_slug, normalizeKey(new_slug), id);
|
|
151
|
+
if (!row)
|
|
152
|
+
throw new Error(`repos_rename: repo ${id} not found`);
|
|
153
|
+
return row;
|
|
154
|
+
}
|
|
155
|
+
async repos_set_path_config(id, path_config) {
|
|
156
|
+
const row = this.db
|
|
157
|
+
.prepare("update repos set path_config = ? where id = ? returning id, slug, path_config, link_config, created_at")
|
|
158
|
+
.get(path_config, id);
|
|
159
|
+
if (!row)
|
|
160
|
+
throw new Error(`repos_set_path_config: repo ${id} not found`);
|
|
161
|
+
return row;
|
|
162
|
+
}
|
|
163
|
+
async repos_set_link_config(id, link_config) {
|
|
164
|
+
const row = this.db
|
|
165
|
+
.prepare("update repos set link_config = ? where id = ? returning id, slug, path_config, link_config, created_at")
|
|
166
|
+
.get(link_config, id);
|
|
167
|
+
if (!row)
|
|
168
|
+
throw new Error(`repos_set_link_config: repo ${id} not found`);
|
|
169
|
+
return row;
|
|
170
|
+
}
|
|
171
|
+
async repos_by_slug(slug) {
|
|
172
|
+
// Case-insensitive identity: fold the query key to slug_norm (§3.5.1).
|
|
173
|
+
return (this.db
|
|
174
|
+
.prepare("select id, slug, path_config, link_config, created_at from repos where slug_norm = ?")
|
|
175
|
+
.get(normalizeKey(slug)) ?? null);
|
|
176
|
+
}
|
|
177
|
+
async repos_by_id(id) {
|
|
178
|
+
return (this.db
|
|
179
|
+
.prepare("select id, slug, path_config, link_config, created_at from repos where id = ?")
|
|
180
|
+
.get(id) ?? null);
|
|
181
|
+
}
|
|
182
|
+
async documents_create(repo_id) {
|
|
183
|
+
return this.db
|
|
184
|
+
.prepare("insert into documents(repo_id) values (?) returning id, repo_id")
|
|
185
|
+
.get(repo_id);
|
|
186
|
+
}
|
|
187
|
+
async version_insert(input) {
|
|
188
|
+
return this.tx(async () => {
|
|
189
|
+
if (input.prev_id !== null) {
|
|
190
|
+
const updated = this.db
|
|
191
|
+
.prepare("update versions set next_id = id where id = ? and document_id = ? and next_id is null")
|
|
192
|
+
.run(input.prev_id, input.document_id);
|
|
193
|
+
if (updated.changes !== 1) {
|
|
194
|
+
throw new Error(`version_insert: prev_id ${input.prev_id} is not the current version of document ${input.document_id} (or does not exist)`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const inserted = this.db
|
|
198
|
+
.prepare(`insert into versions
|
|
199
|
+
(document_id, repo_id, prev_id, next_id, path, path_norm,
|
|
200
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash)
|
|
201
|
+
values (?, ?, ?, null, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
202
|
+
returning id, document_id, repo_id, prev_id, next_id, path,
|
|
203
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash`)
|
|
204
|
+
.get(input.document_id, input.repo_id, input.prev_id, input.path, normalizeKey(input.path), input.frontmatter_raw, JSON.stringify(input.frontmatter), input.body, input.author, input.created_at, contentHash(input.frontmatter_raw, input.body));
|
|
205
|
+
if (input.prev_id !== null) {
|
|
206
|
+
this.db
|
|
207
|
+
.prepare("update versions set next_id = ? where id = ?")
|
|
208
|
+
.run(inserted.id, input.prev_id);
|
|
209
|
+
}
|
|
210
|
+
return hydrateVersion(inserted);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async version_by_id(id) {
|
|
214
|
+
const row = this.db
|
|
215
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
216
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
217
|
+
from versions where id = ?`)
|
|
218
|
+
.get(id);
|
|
219
|
+
return row ? hydrateVersion(row) : null;
|
|
220
|
+
}
|
|
221
|
+
async version_current(repo_id, path) {
|
|
222
|
+
// Case-insensitive identity: fold the query key to path_norm (§3.5.1).
|
|
223
|
+
const row = this.db
|
|
224
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
225
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
226
|
+
from versions where repo_id = ? and path_norm = ? and next_id is null`)
|
|
227
|
+
.get(repo_id, normalizeKey(path));
|
|
228
|
+
return row ? hydrateVersion(row) : null;
|
|
229
|
+
}
|
|
230
|
+
async versions_current_by_paths(repo_id, paths) {
|
|
231
|
+
if (paths.length === 0)
|
|
232
|
+
return [];
|
|
233
|
+
const norms = paths.map(normalizeKey);
|
|
234
|
+
const ph = norms.map(() => "?").join(",");
|
|
235
|
+
const rows = this.db
|
|
236
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
237
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
238
|
+
from versions
|
|
239
|
+
where repo_id = ? and next_id is null and path_norm in (${ph})`)
|
|
240
|
+
.all(repo_id, ...norms);
|
|
241
|
+
return rows.map(hydrateVersion);
|
|
242
|
+
}
|
|
243
|
+
async versions_live_by_repo(repo_id) {
|
|
244
|
+
const rows = this.db
|
|
245
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
246
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
247
|
+
from versions
|
|
248
|
+
where repo_id = ? and next_id is null
|
|
249
|
+
order by path`)
|
|
250
|
+
.all(repo_id);
|
|
251
|
+
return rows.map(hydrateVersion);
|
|
252
|
+
}
|
|
253
|
+
async fts_index(_version_id, _body) {
|
|
254
|
+
// SQLite FTS5 external-content mode with AFTER INSERT triggers on
|
|
255
|
+
// `versions` keeps the index in sync automatically (see migration
|
|
256
|
+
// 0003_fts_docs.sql).
|
|
257
|
+
}
|
|
258
|
+
// Links derived index (design §11.2, migration 0005_links.sql).
|
|
259
|
+
async links_replace(repo_id, source_id, edges) {
|
|
260
|
+
return this.tx(async () => {
|
|
261
|
+
this.db.prepare("delete from links where source_id = ?").run(source_id);
|
|
262
|
+
if (edges.length === 0)
|
|
263
|
+
return;
|
|
264
|
+
const insert = this.db.prepare(`insert into links (repo_id, source_id, ord, field, target_raw, target_norm, target_id)
|
|
265
|
+
values (?, ?, ?, ?, ?, ?, ?)`);
|
|
266
|
+
for (const e of edges) {
|
|
267
|
+
insert.run(repo_id, source_id, e.ord, e.field, e.target_raw, e.target_norm, e.target_id);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async links_clear(source_id) {
|
|
272
|
+
this.db.prepare("delete from links where source_id = ?").run(source_id);
|
|
273
|
+
}
|
|
274
|
+
async links_resolve_dangling(repo_id, target_norm, document_id) {
|
|
275
|
+
const res = this.db
|
|
276
|
+
.prepare(`update links set target_id = ?
|
|
277
|
+
where repo_id = ? and target_norm = ? and target_id is null
|
|
278
|
+
and source_id <> ?`)
|
|
279
|
+
.run(document_id, repo_id, target_norm, document_id);
|
|
280
|
+
return res.changes;
|
|
281
|
+
}
|
|
282
|
+
async links_by_source(source_id) {
|
|
283
|
+
return this.db
|
|
284
|
+
.prepare(`select repo_id, source_id, ord, field, target_raw, target_norm, target_id
|
|
285
|
+
from links where source_id = ? order by ord`)
|
|
286
|
+
.all(source_id);
|
|
287
|
+
}
|
|
288
|
+
async links_by_repo(repo_id) {
|
|
289
|
+
return this.db
|
|
290
|
+
.prepare(`select repo_id, source_id, ord, field, target_raw, target_norm, target_id
|
|
291
|
+
from links where repo_id = ? order by source_id, ord`)
|
|
292
|
+
.all(repo_id);
|
|
293
|
+
}
|
|
294
|
+
async links_adjacent_out(repo_id, source_ids) {
|
|
295
|
+
// Chunk the id batch under SQLITE_MAX_VARIABLE_NUMBER (§graph WS1): a
|
|
296
|
+
// hub-heavy repo's neighbor fan-out can exceed the bound-variable cap.
|
|
297
|
+
// Chunks are disjoint by source_id, so unioning the DISTINCT results needs
|
|
298
|
+
// no further dedup.
|
|
299
|
+
return chunkedInList(source_ids, (ph, ids) => this.db
|
|
300
|
+
.prepare(`select distinct source_id, target_id, field
|
|
301
|
+
from links
|
|
302
|
+
where repo_id = ? and target_id is not null and source_id in (${ph})`)
|
|
303
|
+
.all(repo_id, ...ids));
|
|
304
|
+
}
|
|
305
|
+
async links_adjacent_in(repo_id, target_ids) {
|
|
306
|
+
return chunkedInList(target_ids, (ph, ids) => this.db
|
|
307
|
+
.prepare(`select distinct source_id, target_id, field
|
|
308
|
+
from links
|
|
309
|
+
where repo_id = ? and target_id is not null and target_id in (${ph})`)
|
|
310
|
+
.all(repo_id, ...ids));
|
|
311
|
+
}
|
|
312
|
+
async versions_current_by_documents(repo_id, document_ids) {
|
|
313
|
+
const rows = await chunkedInList(document_ids, (ph, ids) => this.db
|
|
314
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
315
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
316
|
+
from versions
|
|
317
|
+
where repo_id = ? and next_id is null and document_id in (${ph})`)
|
|
318
|
+
.all(repo_id, ...ids));
|
|
319
|
+
return rows.map(hydrateVersion);
|
|
320
|
+
}
|
|
321
|
+
async versions_live_document_ids_matching(repo_id, path_regexes) {
|
|
322
|
+
if (path_regexes.length === 0)
|
|
323
|
+
return [];
|
|
324
|
+
// Match live paths in SQL via the regexp() UDF — one OR'd term per pattern.
|
|
325
|
+
// Pattern lists are tiny (a graph call's root globs), so no chunking; the
|
|
326
|
+
// repo-scoped row set is what could be large, and it never leaves SQLite.
|
|
327
|
+
const ors = path_regexes.map(() => "regexp(?, path)").join(" OR ");
|
|
328
|
+
const rows = this.db
|
|
329
|
+
.prepare(`select document_id from versions
|
|
330
|
+
where repo_id = ? and next_id is null and (${ors})`)
|
|
331
|
+
.all(repo_id, ...path_regexes);
|
|
332
|
+
return rows.map((r) => r.document_id);
|
|
333
|
+
}
|
|
334
|
+
async versions_search(plan) {
|
|
335
|
+
if (plan.repo_ids.length === 0)
|
|
336
|
+
return [];
|
|
337
|
+
if (plan.scope.kind === "deny_all")
|
|
338
|
+
return [];
|
|
339
|
+
const { sql, params } = compileSearchPlan(plan);
|
|
340
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
341
|
+
return rows.map(hydrateVersion);
|
|
342
|
+
}
|
|
343
|
+
async version_history(document_id, opts) {
|
|
344
|
+
const clauses = [];
|
|
345
|
+
const params = [document_id];
|
|
346
|
+
if (opts?.before) {
|
|
347
|
+
clauses.push("chain.created_at < ?");
|
|
348
|
+
params.push(opts.before);
|
|
349
|
+
}
|
|
350
|
+
const where = clauses.length > 0 ? ` where ${clauses.join(" and ")}` : "";
|
|
351
|
+
const limitClause = opts?.limit ? " limit ?" : "";
|
|
352
|
+
if (opts?.limit)
|
|
353
|
+
params.push(opts.limit);
|
|
354
|
+
const rows = this.db
|
|
355
|
+
.prepare(`with recursive chain(
|
|
356
|
+
id, document_id, repo_id, prev_id, next_id, path,
|
|
357
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash, depth
|
|
358
|
+
) as (
|
|
359
|
+
select id, document_id, repo_id, prev_id, next_id, path,
|
|
360
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash, 0
|
|
361
|
+
from versions
|
|
362
|
+
where document_id = ? and next_id is null
|
|
363
|
+
union all
|
|
364
|
+
select v.id, v.document_id, v.repo_id, v.prev_id, v.next_id, v.path,
|
|
365
|
+
v.frontmatter_raw, v.frontmatter, v.body, v.author, v.created_at,
|
|
366
|
+
v.content_hash, c.depth + 1
|
|
367
|
+
from versions v
|
|
368
|
+
join chain c on v.id = c.prev_id
|
|
369
|
+
)
|
|
370
|
+
select id, document_id, repo_id, prev_id, next_id, path,
|
|
371
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
372
|
+
from chain${where}
|
|
373
|
+
order by depth asc${limitClause}`)
|
|
374
|
+
.all(...params);
|
|
375
|
+
return rows.map(hydrateVersion);
|
|
376
|
+
}
|
|
377
|
+
async versions_list(opts) {
|
|
378
|
+
// 1. Select the document ids in scope. Empty path_regexes → every doc in
|
|
379
|
+
// the repo; otherwise match paths via the regexp() UDF (same engine the
|
|
380
|
+
// scope-glob compiler uses). `ever` decides whether a match on ANY
|
|
381
|
+
// version counts, or only the current one.
|
|
382
|
+
const hasGlob = opts.path_regexes.length > 0;
|
|
383
|
+
const ors = opts.path_regexes.map(() => "regexp(?, path)").join(" OR ");
|
|
384
|
+
let docIds;
|
|
385
|
+
if (!hasGlob) {
|
|
386
|
+
docIds = this.db
|
|
387
|
+
.prepare("select distinct document_id from versions where repo_id = ?")
|
|
388
|
+
.all(opts.repo_id).map((r) => r.document_id);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const liveClause = opts.ever ? "" : " and next_id is null";
|
|
392
|
+
docIds = this.db
|
|
393
|
+
.prepare(`select distinct document_id from versions
|
|
394
|
+
where repo_id = ?${liveClause} and (${ors})`)
|
|
395
|
+
.all(opts.repo_id, ...opts.path_regexes).map((r) => r.document_id);
|
|
396
|
+
}
|
|
397
|
+
if (docIds.length === 0)
|
|
398
|
+
return [];
|
|
399
|
+
// 2. Walk those documents' versions, interleaved by id, within the cursor
|
|
400
|
+
// bounds. `ever: false` still returns the WHOLE chain of a selected live
|
|
401
|
+
// document (history of what lives here now); the path filter only chose
|
|
402
|
+
// the documents, not which of their versions to include.
|
|
403
|
+
const clauses = [];
|
|
404
|
+
const params = [];
|
|
405
|
+
if (opts.after_id !== undefined) {
|
|
406
|
+
clauses.push("id > ?");
|
|
407
|
+
params.push(opts.after_id);
|
|
408
|
+
}
|
|
409
|
+
if (opts.until_id !== undefined) {
|
|
410
|
+
clauses.push("id <= ?");
|
|
411
|
+
params.push(opts.until_id);
|
|
412
|
+
}
|
|
413
|
+
const rows = chunkedInList(docIds, (ph, chunk) => {
|
|
414
|
+
const where = [`document_id in (${ph})`, ...clauses].join(" and ");
|
|
415
|
+
return this.db
|
|
416
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
417
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
418
|
+
from versions where ${where}
|
|
419
|
+
order by id ${opts.order === "desc" ? "desc" : "asc"}
|
|
420
|
+
limit ?`)
|
|
421
|
+
.all(...chunk, ...params, opts.limit);
|
|
422
|
+
});
|
|
423
|
+
// chunkedInList concatenates per-chunk results; re-sort + re-limit so the
|
|
424
|
+
// global ordering/limit holds when docIds span multiple chunks.
|
|
425
|
+
rows.sort((a, b) => (opts.order === "desc" ? b.id - a.id : a.id - b.id));
|
|
426
|
+
return rows.slice(0, opts.limit).map(hydrateVersion);
|
|
427
|
+
}
|
|
428
|
+
async versions_since(opts) {
|
|
429
|
+
// Gaps are global, so scan the global id sequence (id, repo, age) — the
|
|
430
|
+
// repo filter never affects gap detection, only the delivered rows. A
|
|
431
|
+
// bounded scan keeps each poll cheap; truncating early only under-delivers
|
|
432
|
+
// (the next poll continues), never crosses a hole.
|
|
433
|
+
const light = this.db
|
|
434
|
+
.prepare(`select id, repo_id, created_at from versions
|
|
435
|
+
where id > ? order by id asc limit ?`)
|
|
436
|
+
.all(opts.after_id, GLOBAL_SCAN_CAP);
|
|
437
|
+
const { upper_id } = safeFrontier(light.map((r) => ({
|
|
438
|
+
id: r.id,
|
|
439
|
+
repo_id: r.repo_id,
|
|
440
|
+
created_at_ms: Date.parse(r.created_at),
|
|
441
|
+
})), opts.after_id, opts.repo_id, opts.limit, opts.now_ms, opts.window_ms);
|
|
442
|
+
if (upper_id <= opts.after_id)
|
|
443
|
+
return { rows: [], next_id: opts.after_id };
|
|
444
|
+
const repoClause = opts.repo_id === undefined ? "" : " and repo_id = ?";
|
|
445
|
+
const params = opts.repo_id === undefined
|
|
446
|
+
? [opts.after_id, upper_id]
|
|
447
|
+
: [opts.after_id, upper_id, opts.repo_id];
|
|
448
|
+
const rows = this.db
|
|
449
|
+
.prepare(`select id, document_id, repo_id, prev_id, next_id, path,
|
|
450
|
+
frontmatter_raw, frontmatter, body, author, created_at, content_hash
|
|
451
|
+
from versions
|
|
452
|
+
where id > ? and id <= ?${repoClause}
|
|
453
|
+
order by id asc`)
|
|
454
|
+
.all(...params);
|
|
455
|
+
return { rows: rows.map(hydrateVersion), next_id: upper_id };
|
|
456
|
+
}
|
|
457
|
+
async versions_paths_by_ids(ids) {
|
|
458
|
+
const map = new Map();
|
|
459
|
+
const rows = chunkedInList(ids, (ph, chunk) => this.db.prepare(`select id, path from versions where id in (${ph})`).all(...chunk));
|
|
460
|
+
for (const r of rows)
|
|
461
|
+
map.set(r.id, r.path);
|
|
462
|
+
return map;
|
|
463
|
+
}
|
|
464
|
+
async versions_safe_head(now_ms, window_ms) {
|
|
465
|
+
// The recent tail is all that can hold a hot (unsettled) gap; older gaps
|
|
466
|
+
// are burned and settled by construction.
|
|
467
|
+
const tail = this.db
|
|
468
|
+
.prepare("select id, created_at from versions order by id desc limit ?")
|
|
469
|
+
.all(SAFE_HEAD_TAIL);
|
|
470
|
+
tail.reverse(); // ascending by id
|
|
471
|
+
return safeHeadFromTail(tail.map((r) => ({ id: r.id, repo_id: 0, created_at_ms: Date.parse(r.created_at) })), now_ms, window_ms);
|
|
472
|
+
}
|
|
473
|
+
async versions_live_index(opts) {
|
|
474
|
+
return this.db
|
|
475
|
+
.prepare(`select id, path, content_hash from versions
|
|
476
|
+
where repo_id = ? and next_id is null and id > ? and id <= ?
|
|
477
|
+
order by id asc limit ?`)
|
|
478
|
+
.all(opts.repo_id, opts.after_id, opts.through_id, opts.limit);
|
|
479
|
+
}
|
|
480
|
+
async versions_missing_content_hash(opts) {
|
|
481
|
+
const repoClause = opts.repo_id === undefined ? "" : " and repo_id = ?";
|
|
482
|
+
const params = opts.repo_id === undefined
|
|
483
|
+
? [opts.after_id, opts.limit]
|
|
484
|
+
: [opts.after_id, opts.repo_id, opts.limit];
|
|
485
|
+
return this.db
|
|
486
|
+
.prepare(`select id, frontmatter_raw, body from versions
|
|
487
|
+
where content_hash is null and id > ?${repoClause}
|
|
488
|
+
order by id asc limit ?`)
|
|
489
|
+
.all(...params);
|
|
490
|
+
}
|
|
491
|
+
async versions_set_content_hash(updates) {
|
|
492
|
+
if (updates.length === 0)
|
|
493
|
+
return;
|
|
494
|
+
return this.tx(async () => {
|
|
495
|
+
const stmt = this.db.prepare("update versions set content_hash = ? where id = ?");
|
|
496
|
+
for (const u of updates)
|
|
497
|
+
stmt.run(u.content_hash, u.id);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
async chunks_upsert(version_id, model, chunks) {
|
|
501
|
+
if (chunks.length === 0) {
|
|
502
|
+
await this.tx(async () => {
|
|
503
|
+
this.db.prepare("delete from chunks where version_id = ?").run(version_id);
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Normalize each embedding to a Buffer (BLOB) at the adapter
|
|
508
|
+
// boundary. Callers pass either number[] (fresh from the hook) or
|
|
509
|
+
// Float32Array (reused via chunks_by_hash).
|
|
510
|
+
const encoded = chunks.map((c) => ({
|
|
511
|
+
...c,
|
|
512
|
+
embedding: c.embedding instanceof Float32Array
|
|
513
|
+
? Buffer.from(c.embedding.buffer, c.embedding.byteOffset, c.embedding.byteLength)
|
|
514
|
+
: encodeVectorBlob(c.embedding),
|
|
515
|
+
}));
|
|
516
|
+
const dim = encoded[0]?.embedding.byteLength;
|
|
517
|
+
if (dim === undefined)
|
|
518
|
+
throw new Error("chunks_upsert: unreachable");
|
|
519
|
+
for (const c of encoded) {
|
|
520
|
+
if (c.embedding.byteLength !== dim) {
|
|
521
|
+
throw new Error(`chunks_upsert: mixed embedding dimensions in one batch (${c.embedding.byteLength} vs ${dim})`);
|
|
522
|
+
}
|
|
523
|
+
if (c.model !== model) {
|
|
524
|
+
throw new Error(`chunks_upsert: mixed models in one batch (${JSON.stringify(c.model)} vs ${JSON.stringify(model)})`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
await this.tx(async () => {
|
|
528
|
+
this.db.prepare("delete from chunks where version_id = ?").run(version_id);
|
|
529
|
+
const insert = this.db.prepare("insert into chunks(version_id, ix, text, text_hash, model, embedding) values (?, ?, ?, ?, ?, ?)");
|
|
530
|
+
for (const c of encoded) {
|
|
531
|
+
insert.run(version_id, c.ix, c.text, c.text_hash, c.model, c.embedding);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async chunks_by_hash(model, text_hashes) {
|
|
536
|
+
if (text_hashes.length === 0)
|
|
537
|
+
return [];
|
|
538
|
+
const ph = text_hashes.map(() => "?").join(",");
|
|
539
|
+
const rows = this.db
|
|
540
|
+
.prepare(`select text_hash, embedding
|
|
541
|
+
from chunks
|
|
542
|
+
where model = ?
|
|
543
|
+
and text_hash in (${ph})
|
|
544
|
+
and embedding is not null
|
|
545
|
+
group by text_hash`)
|
|
546
|
+
.all(model, ...text_hashes);
|
|
547
|
+
return rows.map((r) => ({ text_hash: r.text_hash, embedding: decodeVectorBlob(r.embedding) }));
|
|
548
|
+
}
|
|
549
|
+
async chunks_by_version(version_id) {
|
|
550
|
+
const rows = this.db
|
|
551
|
+
.prepare(`select version_id, ix, text, text_hash, model, embedding
|
|
552
|
+
from chunks
|
|
553
|
+
where version_id = ?
|
|
554
|
+
order by ix`)
|
|
555
|
+
.all(version_id);
|
|
556
|
+
return rows.map((r) => ({
|
|
557
|
+
...r,
|
|
558
|
+
embedding: r.embedding ? decodeVectorBlob(r.embedding) : null,
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
async chunks_model_summary() {
|
|
562
|
+
return this.db
|
|
563
|
+
.prepare(`select model, count(*) as chunk_count
|
|
564
|
+
from chunks
|
|
565
|
+
group by model
|
|
566
|
+
order by model`)
|
|
567
|
+
.all();
|
|
568
|
+
}
|
|
569
|
+
async vector_search(repo_ids, model, embedding, k) {
|
|
570
|
+
if (repo_ids.length === 0 || k <= 0)
|
|
571
|
+
return [];
|
|
572
|
+
const queryBlob = encodeVectorBlob(embedding);
|
|
573
|
+
const repoPh = repo_ids.map(() => "?").join(",");
|
|
574
|
+
return this.db
|
|
575
|
+
.prepare(`with scored as (
|
|
576
|
+
select chunks.version_id as version_id,
|
|
577
|
+
chunks.ix as chunk_ix,
|
|
578
|
+
vec_distance_cosine(chunks.embedding, ?) as score,
|
|
579
|
+
row_number() over (
|
|
580
|
+
partition by chunks.version_id
|
|
581
|
+
order by vec_distance_cosine(chunks.embedding, ?) asc, chunks.ix asc
|
|
582
|
+
) as rn
|
|
583
|
+
from chunks
|
|
584
|
+
join versions on versions.id = chunks.version_id
|
|
585
|
+
where chunks.model = ?
|
|
586
|
+
and chunks.embedding is not null
|
|
587
|
+
and versions.next_id is null
|
|
588
|
+
and versions.repo_id in (${repoPh})
|
|
589
|
+
)
|
|
590
|
+
select version_id, chunk_ix, score
|
|
591
|
+
from scored
|
|
592
|
+
where rn = 1
|
|
593
|
+
order by score asc
|
|
594
|
+
limit ?`)
|
|
595
|
+
.all(queryBlob, queryBlob, model, ...repo_ids, k);
|
|
596
|
+
}
|
|
597
|
+
async backlog_enqueue(version_id) {
|
|
598
|
+
this.db
|
|
599
|
+
.prepare(`insert into embedding_backlog(version_id, attempts, last_error, next_retry_at)
|
|
600
|
+
values (?, 0, null, null)
|
|
601
|
+
on conflict(version_id) do update
|
|
602
|
+
set attempts = 0, last_error = null, next_retry_at = null`)
|
|
603
|
+
.run(version_id);
|
|
604
|
+
}
|
|
605
|
+
async backlog_dequeue(now, limit) {
|
|
606
|
+
if (limit <= 0)
|
|
607
|
+
return [];
|
|
608
|
+
return this.db
|
|
609
|
+
.prepare(`select version_id, attempts, last_error, next_retry_at
|
|
610
|
+
from embedding_backlog
|
|
611
|
+
where next_retry_at is null or next_retry_at <= ?
|
|
612
|
+
order by coalesce(next_retry_at, '') asc, version_id asc
|
|
613
|
+
limit ?`)
|
|
614
|
+
.all(now, limit);
|
|
615
|
+
}
|
|
616
|
+
async backlog_retain(input) {
|
|
617
|
+
this.db
|
|
618
|
+
.prepare(`update embedding_backlog
|
|
619
|
+
set attempts = ?, last_error = ?, next_retry_at = ?
|
|
620
|
+
where version_id = ?`)
|
|
621
|
+
.run(input.attempts, input.last_error, input.next_retry_at, input.version_id);
|
|
622
|
+
}
|
|
623
|
+
async backlog_delete(version_id) {
|
|
624
|
+
this.db.prepare("delete from embedding_backlog where version_id = ?").run(version_id);
|
|
625
|
+
}
|
|
626
|
+
async backlog_status(now) {
|
|
627
|
+
const pending = this.db.prepare("select count(*) as n from embedding_backlog").get().n;
|
|
628
|
+
const due = this.db
|
|
629
|
+
.prepare("select count(*) as n from embedding_backlog where next_retry_at is null or next_retry_at <= ?")
|
|
630
|
+
.get(now).n;
|
|
631
|
+
const failing = this.db.prepare("select count(*) as n from embedding_backlog where attempts > 0").get().n;
|
|
632
|
+
const oldestRow = this.db
|
|
633
|
+
.prepare(`select next_retry_at
|
|
634
|
+
from embedding_backlog
|
|
635
|
+
where next_retry_at is not null
|
|
636
|
+
order by next_retry_at asc
|
|
637
|
+
limit 1`)
|
|
638
|
+
.get();
|
|
639
|
+
const recent = this.db
|
|
640
|
+
.prepare(`select version_id, last_error
|
|
641
|
+
from embedding_backlog
|
|
642
|
+
where last_error is not null
|
|
643
|
+
order by next_retry_at desc
|
|
644
|
+
limit 5`)
|
|
645
|
+
.all();
|
|
646
|
+
const models = this.db
|
|
647
|
+
.prepare(`select model, count(*) as chunk_count
|
|
648
|
+
from chunks
|
|
649
|
+
group by model
|
|
650
|
+
order by model`)
|
|
651
|
+
.all();
|
|
652
|
+
return {
|
|
653
|
+
pending,
|
|
654
|
+
due,
|
|
655
|
+
failing,
|
|
656
|
+
oldest_next_retry_at: oldestRow?.next_retry_at ?? null,
|
|
657
|
+
recent_errors: recent,
|
|
658
|
+
models,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Refuse to open a pre-noauth database. Such a file carries an
|
|
663
|
+
* `api_tokens` table (and `user_version = 5`); the collapsed migration
|
|
664
|
+
* chain starts at 0001, so it would skip migration and run against a
|
|
665
|
+
* mismatched schema. Probe for the legacy marker and refuse with a clear
|
|
666
|
+
* message (noauth plan §1 — no upgrade path; re-ingest into a fresh db).
|
|
667
|
+
*/
|
|
668
|
+
assertNotLegacy() {
|
|
669
|
+
const row = this.db
|
|
670
|
+
.prepare("select name from sqlite_master where type = 'table' and name = 'api_tokens'")
|
|
671
|
+
.get();
|
|
672
|
+
if (row) {
|
|
673
|
+
throw new Error("this database predates the no-auth schema (found legacy api_tokens table). " +
|
|
674
|
+
"Pre-noauth databases are unsupported — re-ingest into a fresh database.");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
export const sqliteAdapter = {
|
|
679
|
+
scheme: "sqlite",
|
|
680
|
+
async open(config) {
|
|
681
|
+
const path = parseSqliteUrl(config.database);
|
|
682
|
+
const storage = new SqliteStorage(path);
|
|
683
|
+
storage.assertNotLegacy();
|
|
684
|
+
await storage.migrate();
|
|
685
|
+
return storage;
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
//# sourceMappingURL=adapter.js.map
|