sensemaking 0.24.7 → 0.25.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/README.md +21 -12
- package/dist/cjs/cli/build.d.cts +3 -0
- package/dist/cjs/cli/build.d.ts +3 -0
- package/dist/cjs/cli/build.js +195 -0
- package/dist/cjs/cli/build.js.map +1 -0
- package/dist/cjs/cli/index.d.cts +7 -6
- package/dist/cjs/cli/index.d.ts +7 -6
- package/dist/cjs/cli/index.js +12 -6
- package/dist/cjs/cli/index.js.map +1 -1
- package/dist/cjs/cli/init.js +2 -2
- package/dist/cjs/cli/init.js.map +1 -1
- package/dist/cjs/cli/map.js +7 -2
- package/dist/cjs/cli/map.js.map +1 -1
- package/dist/cjs/cli/named.js +63 -21
- package/dist/cjs/cli/named.js.map +1 -1
- package/dist/cjs/cli/path.js +7 -2
- package/dist/cjs/cli/path.js.map +1 -1
- package/dist/cjs/cli/peek.js +7 -2
- package/dist/cjs/cli/peek.js.map +1 -1
- package/dist/cjs/cli/related.js +55 -6
- package/dist/cjs/cli/related.js.map +1 -1
- package/dist/cjs/cli/search.js +54 -15
- package/dist/cjs/cli/search.js.map +1 -1
- package/dist/cjs/cli/shared.d.cts +9 -3
- package/dist/cjs/cli/shared.d.ts +9 -3
- package/dist/cjs/cli/shared.js +109 -74
- package/dist/cjs/cli/shared.js.map +1 -1
- package/dist/cjs/cli/sql.js +8 -3
- package/dist/cjs/cli/sql.js.map +1 -1
- package/dist/cjs/cli/status.js +142 -50
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/cli/types.d.cts +4 -2
- package/dist/cjs/cli/types.d.ts +4 -2
- package/dist/cjs/cli.js +10 -4
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/commands/map.js +10 -6
- package/dist/cjs/commands/map.js.map +1 -1
- package/dist/cjs/commands/peek.js +10 -6
- package/dist/cjs/commands/peek.js.map +1 -1
- package/dist/cjs/commands/related.d.cts +1 -0
- package/dist/cjs/commands/related.d.ts +1 -0
- package/dist/cjs/commands/related.js +56 -21
- package/dist/cjs/commands/related.js.map +1 -1
- package/dist/cjs/commands/scope.js +1 -1
- package/dist/cjs/commands/scope.js.map +1 -1
- package/dist/cjs/commands/search.js +102 -84
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/config/index.d.cts +1 -1
- package/dist/cjs/config/index.d.ts +1 -1
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/config/load.d.cts +6 -1
- package/dist/cjs/config/load.d.ts +6 -1
- package/dist/cjs/config/load.js +38 -1
- package/dist/cjs/config/load.js.map +1 -1
- package/dist/cjs/config/types.d.cts +2 -1
- package/dist/cjs/config/types.d.ts +2 -1
- package/dist/cjs/config/types.js +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/config/validate.js +20 -0
- package/dist/cjs/config/validate.js.map +1 -1
- package/dist/cjs/embed/query.d.cts +2 -1
- package/dist/cjs/embed/query.d.ts +2 -1
- package/dist/cjs/embed/query.js +186 -109
- package/dist/cjs/embed/query.js.map +1 -1
- package/dist/cjs/errors.d.cts +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/features/embed.js.map +1 -1
- package/dist/cjs/graph/traverse.js +11 -6
- package/dist/cjs/graph/traverse.js.map +1 -1
- package/dist/cjs/index.d.cts +3 -3
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +3 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/atomic-write.js +20 -6
- package/dist/cjs/lib/atomic-write.js.map +1 -1
- package/dist/cjs/lib/serial-query.d.cts +1 -0
- package/dist/cjs/lib/serial-query.d.ts +1 -0
- package/dist/cjs/lib/serial-query.js +22 -0
- package/dist/cjs/lib/serial-query.js.map +1 -0
- package/dist/cjs/scan/index.d.cts +5 -0
- package/dist/cjs/scan/index.d.ts +5 -0
- package/dist/cjs/scan/index.js +12 -4
- package/dist/cjs/scan/index.js.map +1 -1
- package/dist/cjs/scan/pool.d.cts +5 -0
- package/dist/cjs/scan/pool.d.ts +5 -0
- package/dist/cjs/scan/pool.js +167 -55
- package/dist/cjs/scan/pool.js.map +1 -1
- package/dist/cjs/store/builder.d.cts +6 -1
- package/dist/cjs/store/builder.d.ts +6 -1
- package/dist/cjs/store/builder.js +135 -5
- package/dist/cjs/store/builder.js.map +1 -1
- package/dist/cjs/store/duckdb/lexical.d.cts +2 -0
- package/dist/cjs/store/duckdb/lexical.d.ts +2 -0
- package/dist/cjs/store/duckdb/lexical.js +139 -7
- package/dist/cjs/store/duckdb/lexical.js.map +1 -1
- package/dist/cjs/store/duckdb/open.d.cts +3 -3
- package/dist/cjs/store/duckdb/open.d.ts +3 -3
- package/dist/cjs/store/duckdb/open.js +53 -41
- package/dist/cjs/store/duckdb/open.js.map +1 -1
- package/dist/cjs/store/duckdb/reconcile.js +2 -2
- package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
- package/dist/cjs/store/index.d.cts +23 -4
- package/dist/cjs/store/index.d.ts +23 -4
- package/dist/cjs/store/index.js +224 -15
- package/dist/cjs/store/index.js.map +1 -1
- package/dist/cjs/store/open.d.cts +9 -1
- package/dist/cjs/store/open.d.ts +9 -1
- package/dist/cjs/store/open.js +479 -128
- package/dist/cjs/store/open.js.map +1 -1
- package/dist/cjs/store/reconcile.js +93 -63
- package/dist/cjs/store/reconcile.js.map +1 -1
- package/dist/cjs/store/sqlite/open.d.cts +3 -3
- package/dist/cjs/store/sqlite/open.d.ts +3 -3
- package/dist/cjs/store/sqlite/open.js +44 -36
- package/dist/cjs/store/sqlite/open.js.map +1 -1
- package/dist/cjs/store/turso/open.d.cts +3 -3
- package/dist/cjs/store/turso/open.d.ts +3 -3
- package/dist/cjs/store/turso/open.js +83 -66
- package/dist/cjs/store/turso/open.js.map +1 -1
- package/dist/cjs/store/turso/store.d.cts +3 -1
- package/dist/cjs/store/turso/store.d.ts +3 -1
- package/dist/cjs/store/turso/store.js +46 -31
- package/dist/cjs/store/turso/store.js.map +1 -1
- package/dist/cjs/store/types.d.cts +12 -3
- package/dist/cjs/store/types.d.ts +12 -3
- package/dist/cjs/store/types.js.map +1 -1
- package/dist/cjs/watch.js +155 -35
- package/dist/cjs/watch.js.map +1 -1
- package/dist/esm/cli/build.d.ts +3 -0
- package/dist/esm/cli/build.js +18 -0
- package/dist/esm/cli/build.js.map +1 -0
- package/dist/esm/cli/index.d.ts +7 -6
- package/dist/esm/cli/index.js +8 -6
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/cli/init.js +2 -2
- package/dist/esm/cli/init.js.map +1 -1
- package/dist/esm/cli/map.js +8 -2
- package/dist/esm/cli/map.js.map +1 -1
- package/dist/esm/cli/named.js +38 -14
- package/dist/esm/cli/named.js.map +1 -1
- package/dist/esm/cli/path.js +8 -2
- package/dist/esm/cli/path.js.map +1 -1
- package/dist/esm/cli/peek.js +8 -2
- package/dist/esm/cli/peek.js.map +1 -1
- package/dist/esm/cli/related.js +21 -3
- package/dist/esm/cli/related.js.map +1 -1
- package/dist/esm/cli/search.js +28 -7
- package/dist/esm/cli/search.js.map +1 -1
- package/dist/esm/cli/shared.d.ts +9 -3
- package/dist/esm/cli/shared.js +44 -24
- package/dist/esm/cli/shared.js.map +1 -1
- package/dist/esm/cli/sql.js +8 -2
- package/dist/esm/cli/sql.js.map +1 -1
- package/dist/esm/cli/status.js +166 -111
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/cli/types.d.ts +4 -2
- package/dist/esm/cli/types.js.map +1 -1
- package/dist/esm/cli.js +9 -4
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/commands/map.js +5 -1
- package/dist/esm/commands/map.js.map +1 -1
- package/dist/esm/commands/peek.js +5 -1
- package/dist/esm/commands/peek.js.map +1 -1
- package/dist/esm/commands/related.d.ts +1 -0
- package/dist/esm/commands/related.js +19 -10
- package/dist/esm/commands/related.js.map +1 -1
- package/dist/esm/commands/scope.js +1 -1
- package/dist/esm/commands/scope.js.map +1 -1
- package/dist/esm/commands/search.js +11 -10
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/config/index.d.ts +1 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/config/load.d.ts +6 -1
- package/dist/esm/config/load.js +32 -3
- package/dist/esm/config/load.js.map +1 -1
- package/dist/esm/config/types.d.ts +2 -1
- package/dist/esm/config/types.js +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/config/validate.js +20 -0
- package/dist/esm/config/validate.js.map +1 -1
- package/dist/esm/embed/query.d.ts +2 -1
- package/dist/esm/embed/query.js +40 -34
- package/dist/esm/embed/query.js.map +1 -1
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/features/embed.js.map +1 -1
- package/dist/esm/graph/traverse.js +5 -1
- package/dist/esm/graph/traverse.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/atomic-write.js +14 -6
- package/dist/esm/lib/atomic-write.js.map +1 -1
- package/dist/esm/lib/serial-query.d.ts +1 -0
- package/dist/esm/lib/serial-query.js +13 -0
- package/dist/esm/lib/serial-query.js.map +1 -0
- package/dist/esm/scan/index.d.ts +5 -0
- package/dist/esm/scan/index.js +7 -3
- package/dist/esm/scan/index.js.map +1 -1
- package/dist/esm/scan/pool.d.ts +5 -0
- package/dist/esm/scan/pool.js +41 -16
- package/dist/esm/scan/pool.js.map +1 -1
- package/dist/esm/store/builder.d.ts +6 -1
- package/dist/esm/store/builder.js +19 -0
- package/dist/esm/store/builder.js.map +1 -1
- package/dist/esm/store/duckdb/lexical.d.ts +2 -0
- package/dist/esm/store/duckdb/lexical.js +32 -4
- package/dist/esm/store/duckdb/lexical.js.map +1 -1
- package/dist/esm/store/duckdb/open.d.ts +3 -3
- package/dist/esm/store/duckdb/open.js +13 -6
- package/dist/esm/store/duckdb/open.js.map +1 -1
- package/dist/esm/store/duckdb/reconcile.js +2 -2
- package/dist/esm/store/duckdb/reconcile.js.map +1 -1
- package/dist/esm/store/index.d.ts +23 -4
- package/dist/esm/store/index.js +80 -15
- package/dist/esm/store/index.js.map +1 -1
- package/dist/esm/store/open.d.ts +9 -1
- package/dist/esm/store/open.js +175 -84
- package/dist/esm/store/open.js.map +1 -1
- package/dist/esm/store/reconcile.js +12 -0
- package/dist/esm/store/reconcile.js.map +1 -1
- package/dist/esm/store/sqlite/open.d.ts +3 -3
- package/dist/esm/store/sqlite/open.js +9 -6
- package/dist/esm/store/sqlite/open.js.map +1 -1
- package/dist/esm/store/turso/open.d.ts +3 -3
- package/dist/esm/store/turso/open.js +14 -10
- package/dist/esm/store/turso/open.js.map +1 -1
- package/dist/esm/store/turso/store.d.ts +3 -1
- package/dist/esm/store/turso/store.js +5 -1
- package/dist/esm/store/turso/store.js.map +1 -1
- package/dist/esm/store/types.d.ts +12 -3
- package/dist/esm/store/types.js.map +1 -1
- package/dist/esm/watch.js +57 -19
- package/dist/esm/watch.js.map +1 -1
- package/package.json +1 -1
- package/schema.json +7 -2
- package/skills/sense/SKILL.md +3 -3
- package/skills/sense-bases/SKILL.md +1 -2
- package/skills/sense-setup/SKILL.md +1 -1
- package/skills/sense-setup/references/embeddings.md +1 -1
- package/skills/sense-setup/references/store-benchmarks.md +6 -6
|
@@ -13,7 +13,7 @@ import { createStore } from './store.js';
|
|
|
13
13
|
export const DB_FILENAME = 'cache.db';
|
|
14
14
|
// Cache shape version, independent of the config's own `version`. Bumping it rebuilds
|
|
15
15
|
// existing trees on first query.
|
|
16
|
-
export const SCHEMA_VERSION = '
|
|
16
|
+
export const SCHEMA_VERSION = '21';
|
|
17
17
|
// unicode61 splits on spaces, so a language written without them indexes a whole run as one
|
|
18
18
|
// token and word search finds nothing; the `_seg` sidecars (always populated) cover that case.
|
|
19
19
|
const TOKENIZE = 'porter unicode61';
|
|
@@ -27,6 +27,7 @@ async function createContentTable(conn) {
|
|
|
27
27
|
async function ensureSchemaTables(conn, cfg) {
|
|
28
28
|
await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_ctime" REAL, "_size" INTEGER, "_parse_error" TEXT)`);
|
|
29
29
|
await createContentTable(conn);
|
|
30
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources ("path" TEXT PRIMARY KEY, text TEXT NOT NULL)`);
|
|
30
31
|
// Coverage, not ownership: a path can appear under several presets. path leads the PK so the
|
|
31
32
|
// per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.
|
|
32
33
|
await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
|
|
@@ -49,15 +50,17 @@ function setJournalWal(db) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
async function connect(dbPath, _cfg) {
|
|
53
|
-
const db = new DatabaseSync(dbPath
|
|
53
|
+
async function connect(dbPath, _cfg, options) {
|
|
54
|
+
const db = new DatabaseSync(dbPath, options.existingOnly ? {
|
|
55
|
+
readOnly: true
|
|
56
|
+
} : {});
|
|
54
57
|
try {
|
|
55
58
|
// Before journal_mode, not after: converting a fresh database to WAL takes a brief exclusive
|
|
56
59
|
// lock, and with no timeout set yet a second process opening the same tree fails in 1ms.
|
|
57
60
|
db.exec('PRAGMA busy_timeout = 30000');
|
|
58
61
|
// busy_timeout does not cover the WAL conversion itself: SQLite does not invoke the busy
|
|
59
62
|
// handler for it, so a concurrent cold open needs its own bounded wait.
|
|
60
|
-
setJournalWal(db);
|
|
63
|
+
if (!options.observational) setJournalWal(db);
|
|
61
64
|
registerFunctions(db);
|
|
62
65
|
const conn = createConnection(db);
|
|
63
66
|
return {
|
|
@@ -95,6 +98,6 @@ export const sqliteOpenDialect = {
|
|
|
95
98
|
setDerivedBusyTimeout,
|
|
96
99
|
createStore: (handle, conn)=>createStore(handle.db, conn)
|
|
97
100
|
};
|
|
98
|
-
export async function openSqlite(cfg) {
|
|
99
|
-
return openWithDialect(cfg, sqliteOpenDialect);
|
|
101
|
+
export async function openSqlite(cfg, options) {
|
|
102
|
+
return openWithDialect(cfg, sqliteOpenDialect, options);
|
|
100
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/open.ts"],"sourcesContent":["// The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with\n// both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { sqliteDialect } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`. Bumping it rebuilds\n// existing trees on first query.\nexport const SCHEMA_VERSION = '
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/open.ts"],"sourcesContent":["// The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with\n// both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { sqliteDialect } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`. Bumping it rebuilds\n// existing trees on first query.\nexport const SCHEMA_VERSION = '21';\n\nexport type { OpenResult };\n\n// unicode61 splits on spaces, so a language written without them indexes a whole run as one\n// token and word search finds nothing; the `_seg` sidecars (always populated) cover that case.\nconst TOKENIZE = 'porter unicode61';\n\ninterface SqliteHandle {\n db: DatabaseSync;\n}\n\n// The `_seg` sidecars are appended after path, never inserted: bm25() and snippet(content, 2)\n// are documented against the first three columns. Each holds its field's exploded unspaced runs.\nasync function createContentTable(conn: Connection): Promise<void> {\n await conn.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, title_seg, summary_seg, text_seg, tokenize = '${TOKENIZE}')`);\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`\n// can't dump file text into context. Features add their own tables after the core ones.\nasync function ensureSchemaTables(conn: Connection, cfg: Config): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n await createContentTable(conn);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n // Coverage, not ownership: a path can appear under several presets. path leads the PK so the\n // per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n for (const feature of activeFeatures(cfg)) await feature.schema(conn);\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n if ((await getMeta(conn, 'features')) === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));\n}\n\n// Two processes opening the same fresh tree both try to convert it, and the loser gets SQLITE_BUSY\n// with no busy handler behind it. Bounded because a lock held past this is a real problem, not a race.\nfunction setJournalWal(db: DatabaseSync): void {\n const deadline = Date.now() + 5000;\n for (;;) {\n try {\n db.exec('PRAGMA journal_mode = WAL');\n return;\n } catch (err) {\n if (Date.now() >= deadline || !/database is locked|busy/i.test((err as Error).message)) throw err;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);\n }\n }\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: SqliteHandle; conn: Connection }> {\n const db = new DatabaseSync(dbPath, options.existingOnly ? { readOnly: true } : {});\n try {\n // Before journal_mode, not after: converting a fresh database to WAL takes a brief exclusive\n // lock, and with no timeout set yet a second process opening the same tree fails in 1ms.\n db.exec('PRAGMA busy_timeout = 30000');\n // busy_timeout does not cover the WAL conversion itself: SQLite does not invoke the busy\n // handler for it, so a concurrent cold open needs its own bounded wait.\n if (!options.observational) setJournalWal(db);\n registerFunctions(db);\n const conn = createConnection(db);\n return { handle: { db }, conn };\n } catch (err) {\n // A throw here must release this handle, or on Windows the leaked WAL db is undeletable and\n // scratch cleanup fails with EPERM/EBUSY.\n db.close();\n throw err;\n }\n}\n\nasync function close(handle: SqliteHandle): Promise<void> {\n handle.db.close();\n}\n\nasync function ensureSchema(_handle: SqliteHandle, conn: Connection, cfg: Config): Promise<void> {\n // One writer at a time: the feature hooks check a column then add it, so two cold opens racing\n // here both see it missing and the second ALTER fails with a duplicate column.\n await withTransaction(conn, () => ensureSchemaTables(conn, cfg), BEGIN_WRITE);\n}\n\nasync function setDerivedBusyTimeout(handle: SqliteHandle, _conn: Connection, ms: number): Promise<void> {\n handle.db.exec(`PRAGMA busy_timeout = ${ms}`);\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const sqliteOpenDialect: OpenDialect<SqliteHandle> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: sqliteDialect,\n connect,\n close,\n ensureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn) => createStore(handle.db, conn),\n};\n\nexport async function openSqlite(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, sqliteOpenDialect, options);\n}\n"],"names":["DatabaseSync","featureSignature","activeFeatures","FEATURES","openWithDialect","getMeta","setMeta","BEGIN_WRITE","withTransaction","createConnection","sqliteDialect","registerFunctions","createStore","DB_FILENAME","SCHEMA_VERSION","TOKENIZE","createContentTable","conn","exec","ensureSchemaTables","cfg","feature","schema","setJournalWal","db","deadline","Date","now","err","test","message","Atomics","wait","Int32Array","SharedArrayBuffer","connect","dbPath","_cfg","options","existingOnly","readOnly","observational","handle","close","ensureSchema","_handle","setDerivedBusyTimeout","_conn","ms","sqliteOpenDialect","filename","schemaVersion","reconcileDialect","openSqlite"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Database } from '@tursodatabase/database';
|
|
2
2
|
import type { ResolvedConfig } from '../../config/index.js';
|
|
3
|
-
import type { OpenResult } from '../open.js';
|
|
3
|
+
import type { InternalOpenOptions, OpenResult } from '../open.js';
|
|
4
4
|
import type { OpenDialect } from '../types.js';
|
|
5
5
|
export declare const DB_FILENAME = "cache.turso.db";
|
|
6
|
-
export declare const SCHEMA_VERSION = "
|
|
6
|
+
export declare const SCHEMA_VERSION = "9";
|
|
7
7
|
export type { OpenResult };
|
|
8
8
|
export declare const tursoOpenDialect: OpenDialect<Database>;
|
|
9
|
-
export declare function openTurso(cfg: ResolvedConfig): Promise<OpenResult>;
|
|
9
|
+
export declare function openTurso(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult>;
|
|
@@ -11,12 +11,13 @@ import { createStore } from './store.js';
|
|
|
11
11
|
export const DB_FILENAME = 'cache.turso.db';
|
|
12
12
|
// Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves
|
|
13
13
|
// separately. Covers the FTS indexes, the "_ngram" sidecar columns, and embeddings.vector's width.
|
|
14
|
-
export const SCHEMA_VERSION = '
|
|
14
|
+
export const SCHEMA_VERSION = '9';
|
|
15
15
|
// This store's Handle (types.ts's OpenDialect<Handle>) is the connected Database itself: no
|
|
16
16
|
// extra native state to thread, unlike duckdb's separate instance/connection pair.
|
|
17
17
|
async function ensureSchema(_handle, conn, cfg) {
|
|
18
18
|
await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_ctime" REAL, "_size" INTEGER, "_parse_error" TEXT)`);
|
|
19
19
|
await conn.exec(`CREATE TABLE IF NOT EXISTS content ("path" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT, title_stem TEXT, summary_stem TEXT, text_stem TEXT, title_ngram TEXT, summary_ngram TEXT, text_ngram TEXT)`);
|
|
20
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources ("path" TEXT PRIMARY KEY, text TEXT NOT NULL)`);
|
|
20
21
|
for (const ddl of CONTENT_FTS_DDL)await conn.exec(ddl);
|
|
21
22
|
await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
|
|
22
23
|
await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');
|
|
@@ -32,14 +33,14 @@ async function ensureSchema(_handle, conn, cfg) {
|
|
|
32
33
|
if (await getMeta(conn, 'schema_version') === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);
|
|
33
34
|
if (await getMeta(conn, 'features') === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));
|
|
34
35
|
}
|
|
35
|
-
async function close(handle) {
|
|
36
|
-
await checkpointWal(handle);
|
|
36
|
+
async function close(handle, options = {}) {
|
|
37
|
+
if (!options.observational) await checkpointWal(handle);
|
|
37
38
|
await handle.close();
|
|
38
39
|
}
|
|
39
40
|
async function setDerivedBusyTimeout(_handle, conn, ms) {
|
|
40
41
|
await conn.exec(`PRAGMA busy_timeout = ${ms}`);
|
|
41
42
|
}
|
|
42
|
-
async function connect(dbPath, _cfg) {
|
|
43
|
+
async function connect(dbPath, _cfg, options) {
|
|
43
44
|
// Dynamic, not a top-level import: a sqlite or duckdb tree must never attempt to resolve this
|
|
44
45
|
// optional dependency until a turso tree is actually opened. Installed on first use if missing.
|
|
45
46
|
let turso;
|
|
@@ -51,13 +52,16 @@ async function connect(dbPath, _cfg) {
|
|
|
51
52
|
}
|
|
52
53
|
let db;
|
|
53
54
|
try {
|
|
54
|
-
//
|
|
55
|
-
//
|
|
55
|
+
// Turso's readonly mode rejects native FTS SELECTs and TEMP query tables. Existing-only opens
|
|
56
|
+
// stay query-capable; shared no-build orchestration and observational close prevent upkeep writes.
|
|
56
57
|
db = await turso.connect(dbPath, {
|
|
57
58
|
...CONNECT_OPTS,
|
|
58
59
|
experimental: [
|
|
59
60
|
...CONNECT_OPTS.experimental
|
|
60
|
-
]
|
|
61
|
+
],
|
|
62
|
+
...options.existingOnly ? {
|
|
63
|
+
fileMustExist: true
|
|
64
|
+
} : {}
|
|
61
65
|
});
|
|
62
66
|
} catch (err) {
|
|
63
67
|
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "turso" failed to open ${dbPath}: ${err.message}`);
|
|
@@ -80,8 +84,8 @@ export const tursoOpenDialect = {
|
|
|
80
84
|
isLocked: (err)=>/File is locked by another process|locked a portion of the file/.test(err.message),
|
|
81
85
|
ensureSchema,
|
|
82
86
|
setDerivedBusyTimeout,
|
|
83
|
-
createStore: (handle, conn)=>createStore(handle, conn)
|
|
87
|
+
createStore: (handle, conn, _cfg, options)=>createStore(handle, conn, options)
|
|
84
88
|
};
|
|
85
|
-
export async function openTurso(cfg) {
|
|
86
|
-
return openWithDialect(cfg, tursoOpenDialect);
|
|
89
|
+
export async function openTurso(cfg, options) {
|
|
90
|
+
return openWithDialect(cfg, tursoOpenDialect, options);
|
|
87
91
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/open.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { checkpointWal, createConnection } from './connection.ts';\nimport { CONNECT_OPTS, TURSO_PACKAGE, tursoApi } from './native.ts';\nimport { CONTENT_FTS_DDL, tursoDialect } from './reconcile.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.turso.db';\n// Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves\n// separately. Covers the FTS indexes, the \"_ngram\" sidecar columns, and embeddings.vector's width.\nexport const SCHEMA_VERSION = '
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/open.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { checkpointWal, createConnection } from './connection.ts';\nimport { CONNECT_OPTS, TURSO_PACKAGE, tursoApi } from './native.ts';\nimport { CONTENT_FTS_DDL, tursoDialect } from './reconcile.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.turso.db';\n// Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves\n// separately. Covers the FTS indexes, the \"_ngram\" sidecar columns, and embeddings.vector's width.\nexport const SCHEMA_VERSION = '9';\n\nexport type { OpenResult };\n\n// This store's Handle (types.ts's OpenDialect<Handle>) is the connected Database itself: no\n// extra native state to thread, unlike duckdb's separate instance/connection pair.\nasync function ensureSchema(_handle: Database, conn: Connection, cfg: Config): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS content (\"path\" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT, title_stem TEXT, summary_stem TEXT, text_stem TEXT, title_ngram TEXT, summary_ngram TEXT, text_ngram TEXT)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n for (const ddl of CONTENT_FTS_DDL) await conn.exec(ddl);\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n for (const feature of activeFeatures(cfg)) {\n // Native F32_BLOB(STORE_DIMS) instead of the embed feature's engine-neutral BLOB DDL. `scale`\n // is kept unused, so the shared reconcile-time INSERT/DELETE names a column both stores have.\n if (feature.name === 'embed') {\n await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector F32_BLOB(${STORE_DIMS}), PRIMARY KEY (\"path\", chunk))`);\n continue;\n }\n await feature.schema(conn);\n }\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n if ((await getMeta(conn, 'features')) === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));\n}\n\nasync function close(handle: Database, options: { observational?: boolean } = {}): Promise<void> {\n if (!options.observational) await checkpointWal(handle);\n await handle.close();\n}\n\nasync function setDerivedBusyTimeout(_handle: Database, conn: Connection, ms: number): Promise<void> {\n await conn.exec(`PRAGMA busy_timeout = ${ms}`);\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: Database; conn: Connection }> {\n // Dynamic, not a top-level import: a sqlite or duckdb tree must never attempt to resolve this\n // optional dependency until a turso tree is actually opened. Installed on first use if missing.\n let turso: Awaited<ReturnType<typeof tursoApi>>;\n try {\n turso = await tursoApi();\n } catch (err) {\n if (err instanceof SenseError) throw err;\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"turso\" needs the ${TURSO_PACKAGE} package (${(err as Error).message})`);\n }\n\n let db: Database;\n try {\n // Turso's readonly mode rejects native FTS SELECTs and TEMP query tables. Existing-only opens\n // stay query-capable; shared no-build orchestration and observational close prevent upkeep writes.\n db = await turso.connect(dbPath, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental], ...(options.existingOnly ? { fileMustExist: true } : {}) });\n } catch (err) {\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"turso\" failed to open ${dbPath}: ${(err as Error).message}`);\n }\n return { handle: db, conn: createConnection(db) };\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const tursoOpenDialect: OpenDialect<Database> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: tursoDialect,\n connect,\n close,\n // \"Locking error: Failed locking file ...\", worded per platform: posix \"File is locked by another\n // process\", Windows \"another process has locked a portion of the file (os error 33)\". Distinct from\n // the write-time \"database is locked\" its connect-time `timeout` covers; that one never reaches here.\n isLocked: (err) => /File is locked by another process|locked a portion of the file/.test(err.message),\n ensureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn, _cfg, options) => createStore(handle, conn, options),\n};\n\nexport async function openTurso(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, tursoOpenDialect, options);\n}\n"],"names":["featureSignature","STORE_DIMS","SenseError","activeFeatures","FEATURES","openWithDialect","getMeta","setMeta","checkpointWal","createConnection","CONNECT_OPTS","TURSO_PACKAGE","tursoApi","CONTENT_FTS_DDL","tursoDialect","createStore","DB_FILENAME","SCHEMA_VERSION","ensureSchema","_handle","conn","cfg","exec","ddl","feature","name","schema","close","handle","options","observational","setDerivedBusyTimeout","ms","connect","dbPath","_cfg","turso","err","message","db","experimental","existingOnly","fileMustExist","tursoOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","openTurso"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Database } from '@tursodatabase/database';
|
|
2
2
|
import type { Capability, Connection, Store } from '../types.js';
|
|
3
3
|
export declare const CAPABILITIES: ReadonlySet<Capability>;
|
|
4
|
-
export declare function createStore(db: Database, conn: Connection
|
|
4
|
+
export declare function createStore(db: Database, conn: Connection, options?: {
|
|
5
|
+
observational?: boolean;
|
|
6
|
+
}): Store;
|
|
@@ -16,7 +16,7 @@ export const CAPABILITIES = new Set([
|
|
|
16
16
|
const BLOAT_FACTOR = 1.5;
|
|
17
17
|
// Shares one Connection instance (conn) with the builder's own reconcile call so transaction depth
|
|
18
18
|
// (see transaction.ts) is tracked against the same object everywhere.
|
|
19
|
-
export function createStore(db, conn) {
|
|
19
|
+
export function createStore(db, conn, options = {}) {
|
|
20
20
|
return {
|
|
21
21
|
name: 'turso',
|
|
22
22
|
capabilities: CAPABILITIES,
|
|
@@ -74,6 +74,10 @@ export function createStore(db, conn) {
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
async close () {
|
|
77
|
+
if (options.observational) {
|
|
78
|
+
await db.close();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
77
81
|
// Checkpoint before measuring: the last reconcile's pages sit in the WAL, not the file yet.
|
|
78
82
|
await checkpointWal(db);
|
|
79
83
|
let reclaimPath = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/store.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { getColumns, getMeta, setMeta } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { cacheFilePath, checkpointWal, fileSize, reclaimSpace } from './connection.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\n// No 'segment': has()/basename() resolve via SQL rewrite below (sql-functions.ts), but segment() has no SQL form and the client cannot register it as a UDF.\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['vectors']);\n\n// close() reclaims once the cache file outgrows its compact size by this factor: the disk\n// overhead a user should accept (PLAN 3.52), in postgres autovacuum scale-factor shape.\nconst BLOAT_FACTOR = 1.5;\n\n// Shares one Connection instance (conn) with the builder's own reconcile call so transaction depth\n// (see transaction.ts) is tracked against the same object everywhere.\nexport function createStore(db: Database, conn: Connection): Store {\n return {\n name: 'turso',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n fieldStats: (columns, scopeWhere) => fieldStats(conn, columns, scopeWhere),\n },\n lexical: {\n query: (terms, opts) => queryLexical(conn, terms, opts),\n },\n // The column's fixed DDL width (STORE_DIMS) is what every scan binds against, not the\n // interface's per-call storeDims -- see vectors.ts's padded() for why a shorter vector is still correct against a wider column.\n vectors: {\n pending: () => pendingRows(conn),\n writeVectors: (rows) => writeVectorBatch(conn, STORE_DIMS, rows),\n candidates: (qv, _storeDims, fetch, allowed) => scanCandidates(conn, qv, STORE_DIMS, fetch, allowed),\n similar: (path, opts) => scanSimilar(conn, STORE_DIMS, path, opts),\n hasVector: (path) => hasVectorRow(conn, path),\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min). Turso's PRAGMA busy_timeout names its column \"busy_timeout\" (spike-verified), not \"timeout\" like real SQLite.\n const row = (await (await db.prepare('PRAGMA busy_timeout')).get()) as { busy_timeout: number };\n return { busy_timeout: `${row.busy_timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = await db.prepare(rewriteFunctions(sql));\n stmt.safeIntegers(true); // int64 past 2^53 arrives as BigInt instead of losing precision\n return {\n columns: () => stmt.columns(),\n // sense sql streams through the client's own async generator.\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...params);\n },\n };\n },\n },\n async close() {\n // Checkpoint before measuring: the last reconcile's pages sit in the WAL, not the file yet.\n await checkpointWal(db);\n let reclaimPath: string | null = null;\n try {\n const path = await cacheFilePath(conn);\n const base = Number((await getMeta(conn, 'compact_size')) ?? '0');\n const size = path === null ? null : await fileSize(path);\n if (size !== null) {\n // No baseline yet (cold build or an upgraded cache): record it, never reclaim here, so a\n // cold build pays no VACUUM (PLAN 3.49 Track A) and an upgraded cache heals on a crossing.\n if (base === 0) {\n await setMeta(conn, 'compact_size', String(size));\n // The meta write landed past the checkpoint; leave no WAL for the next opener.\n await checkpointWal(db);\n } else if (size > BLOAT_FACTOR * base) {\n reclaimPath = path;\n }\n }\n } catch (err) {\n console.error(`sense: turso cache bloat check failed, the cache will keep its space until one succeeds: ${(err as Error).message}`);\n }\n await db.close();\n // After this store's connection is closed, never before: turso hands the file to one\n // connection at a time, and reclaimSpace opens its own to carry the `vacuum` flag (PLAN 3.54).\n if (reclaimPath !== null) await reclaimSpace(reclaimPath);\n },\n };\n}\n"],"names":["STORE_DIMS","getColumns","getMeta","setMeta","withTransaction","hasVectorRow","pendingRows","cacheFilePath","checkpointWal","fileSize","reclaimSpace","fieldStats","queryLexical","rewriteFunctions","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","BLOAT_FACTOR","createStore","db","conn","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","vectors","pending","writeVectors","rows","candidates","qv","_storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","raw","stmt","safeIntegers","iterate","params","close","reclaimPath","base","Number","size","String","err","console","error","message"],"mappings":"AACA,SAASA,UAAU,QAAQ,uBAAuB;AAClD,SAASC,UAAU,EAAEC,OAAO,EAAEC,OAAO,QAAQ,eAAe;AAC5D,SAASC,eAAe,QAAQ,oBAAoB;AAEpD,SAASC,YAAY,EAAEC,WAAW,QAAQ,gBAAgB;AAC1D,SAASC,aAAa,EAAEC,aAAa,EAAEC,QAAQ,EAAEC,YAAY,QAAQ,kBAAkB;AACvF,SAASC,UAAU,QAAQ,kBAAkB;AAC7C,SAASC,YAAY,QAAQ,eAAe;AAC5C,SAASC,gBAAgB,QAAQ,qBAAqB;AACtD,SAASC,cAAc,EAAEC,WAAW,EAAEC,gBAAgB,QAAQ,eAAe;AAE7E,6JAA6J;AAC7J,OAAO,MAAMC,eAAwC,IAAIC,IAAI;IAAC;CAAU,EAAE;AAE1E,0FAA0F;AAC1F,wFAAwF;AACxF,MAAMC,eAAe;AAErB,mGAAmG;AACnG,sEAAsE;AACtE,OAAO,SAASC,YAAYC,EAAY,EAAEC,IAAgB;
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/store.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { getColumns, getMeta, setMeta } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { cacheFilePath, checkpointWal, fileSize, reclaimSpace } from './connection.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\n// No 'segment': has()/basename() resolve via SQL rewrite below (sql-functions.ts), but segment() has no SQL form and the client cannot register it as a UDF.\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['vectors']);\n\n// close() reclaims once the cache file outgrows its compact size by this factor: the disk\n// overhead a user should accept (PLAN 3.52), in postgres autovacuum scale-factor shape.\nconst BLOAT_FACTOR = 1.5;\n\n// Shares one Connection instance (conn) with the builder's own reconcile call so transaction depth\n// (see transaction.ts) is tracked against the same object everywhere.\nexport function createStore(db: Database, conn: Connection, options: { observational?: boolean } = {}): Store {\n return {\n name: 'turso',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n fieldStats: (columns, scopeWhere) => fieldStats(conn, columns, scopeWhere),\n },\n lexical: {\n query: (terms, opts) => queryLexical(conn, terms, opts),\n },\n // The column's fixed DDL width (STORE_DIMS) is what every scan binds against, not the\n // interface's per-call storeDims -- see vectors.ts's padded() for why a shorter vector is still correct against a wider column.\n vectors: {\n pending: () => pendingRows(conn),\n writeVectors: (rows) => writeVectorBatch(conn, STORE_DIMS, rows),\n candidates: (qv, _storeDims, fetch, allowed) => scanCandidates(conn, qv, STORE_DIMS, fetch, allowed),\n similar: (path, opts) => scanSimilar(conn, STORE_DIMS, path, opts),\n hasVector: (path) => hasVectorRow(conn, path),\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min). Turso's PRAGMA busy_timeout names its column \"busy_timeout\" (spike-verified), not \"timeout\" like real SQLite.\n const row = (await (await db.prepare('PRAGMA busy_timeout')).get()) as { busy_timeout: number };\n return { busy_timeout: `${row.busy_timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = await db.prepare(rewriteFunctions(sql));\n stmt.safeIntegers(true); // int64 past 2^53 arrives as BigInt instead of losing precision\n return {\n columns: () => stmt.columns(),\n // sense sql streams through the client's own async generator.\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...params);\n },\n };\n },\n },\n async close() {\n if (options.observational) {\n await db.close();\n return;\n }\n // Checkpoint before measuring: the last reconcile's pages sit in the WAL, not the file yet.\n await checkpointWal(db);\n let reclaimPath: string | null = null;\n try {\n const path = await cacheFilePath(conn);\n const base = Number((await getMeta(conn, 'compact_size')) ?? '0');\n const size = path === null ? null : await fileSize(path);\n if (size !== null) {\n // No baseline yet (cold build or an upgraded cache): record it, never reclaim here, so a\n // cold build pays no VACUUM (PLAN 3.49 Track A) and an upgraded cache heals on a crossing.\n if (base === 0) {\n await setMeta(conn, 'compact_size', String(size));\n // The meta write landed past the checkpoint; leave no WAL for the next opener.\n await checkpointWal(db);\n } else if (size > BLOAT_FACTOR * base) {\n reclaimPath = path;\n }\n }\n } catch (err) {\n console.error(`sense: turso cache bloat check failed, the cache will keep its space until one succeeds: ${(err as Error).message}`);\n }\n await db.close();\n // After this store's connection is closed, never before: turso hands the file to one\n // connection at a time, and reclaimSpace opens its own to carry the `vacuum` flag (PLAN 3.54).\n if (reclaimPath !== null) await reclaimSpace(reclaimPath);\n },\n };\n}\n"],"names":["STORE_DIMS","getColumns","getMeta","setMeta","withTransaction","hasVectorRow","pendingRows","cacheFilePath","checkpointWal","fileSize","reclaimSpace","fieldStats","queryLexical","rewriteFunctions","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","BLOAT_FACTOR","createStore","db","conn","options","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","vectors","pending","writeVectors","rows","candidates","qv","_storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","raw","stmt","safeIntegers","iterate","params","close","observational","reclaimPath","base","Number","size","String","err","console","error","message"],"mappings":"AACA,SAASA,UAAU,QAAQ,uBAAuB;AAClD,SAASC,UAAU,EAAEC,OAAO,EAAEC,OAAO,QAAQ,eAAe;AAC5D,SAASC,eAAe,QAAQ,oBAAoB;AAEpD,SAASC,YAAY,EAAEC,WAAW,QAAQ,gBAAgB;AAC1D,SAASC,aAAa,EAAEC,aAAa,EAAEC,QAAQ,EAAEC,YAAY,QAAQ,kBAAkB;AACvF,SAASC,UAAU,QAAQ,kBAAkB;AAC7C,SAASC,YAAY,QAAQ,eAAe;AAC5C,SAASC,gBAAgB,QAAQ,qBAAqB;AACtD,SAASC,cAAc,EAAEC,WAAW,EAAEC,gBAAgB,QAAQ,eAAe;AAE7E,6JAA6J;AAC7J,OAAO,MAAMC,eAAwC,IAAIC,IAAI;IAAC;CAAU,EAAE;AAE1E,0FAA0F;AAC1F,wFAAwF;AACxF,MAAMC,eAAe;AAErB,mGAAmG;AACnG,sEAAsE;AACtE,OAAO,SAASC,YAAYC,EAAY,EAAEC,IAAgB,EAAEC,UAAuC,CAAC,CAAC;IACnG,OAAO;QACLC,MAAM;QACNC,cAAcR;QACd,MAAMS,MAAKC,GAAW;YACpB,MAAML,KAAKI,IAAI,CAACC;QAClB;QACA,MAAMC,SAAQD,GAAW;YACvB,OAAOL,KAAKM,OAAO,CAACD;QACtB;QACA,MAAME,UAASF,GAAW,EAAEG,SAAsB;YAChD,MAAMR,KAAKO,QAAQ,CAACF,KAAKG;QAC3B;QACA,MAAMC,aAAeC,EAAoB;YACvC,OAAO5B,gBAAgBkB,MAAMU;QAC/B;QACAC,MAAM;YACJ,MAAMC;gBACJ,OAAO;uBAAK,MAAMjC,WAAWqB;iBAAO;YACtC;YACAX,YAAY,CAACuB,SAASC,aAAexB,WAAWW,MAAMY,SAASC;QACjE;QACAC,SAAS;YACPC,OAAO,CAACC,OAAOC,OAAS3B,aAAaU,MAAMgB,OAAOC;QACpD;QACA,sFAAsF;QACtF,gIAAgI;QAChIC,SAAS;YACPC,SAAS,IAAMnC,YAAYgB;YAC3BoB,cAAc,CAACC,OAAS3B,iBAAiBM,MAAMtB,YAAY2C;YAC3DC,YAAY,CAACC,IAAIC,YAAYC,OAAOC,UAAYlC,eAAeQ,MAAMuB,IAAI7C,YAAY+C,OAAOC;YAC5FC,SAAS,CAACC,MAAMX,OAASxB,YAAYO,MAAMtB,YAAYkD,MAAMX;YAC7DY,WAAW,CAACD,OAAS7C,aAAaiB,MAAM4B;QAC1C;QACA,MAAME;YACJ,qFAAqF;YACrF,sKAAsK;YACtK,MAAMC,MAAO,MAAM,AAAC,CAAA,MAAMhC,GAAGO,OAAO,CAAC,sBAAqB,EAAG0B,GAAG;YAChE,OAAO;gBAAEC,cAAc,GAAGF,IAAIE,YAAY,CAAC,kFAAkF,CAAC;YAAC;QACjI;QACAC,KAAK;YACH,MAAM5B,SAAQD,GAAW;gBACvB,MAAM8B,OAAO,MAAMpC,GAAGO,OAAO,CAACf,iBAAiBc;gBAC/C8B,KAAKC,YAAY,CAAC,OAAO,gEAAgE;gBACzF,OAAO;oBACLxB,SAAS,IAAMuB,KAAKvB,OAAO;oBAC3B,8DAA8D;oBAC9DyB,SAAS,gBAAiB,GAAGC,MAAiB;wBAC5C,OAAOH,KAAKE,OAAO,IAAIC;oBACzB;gBACF;YACF;QACF;QACA,MAAMC;YACJ,IAAItC,QAAQuC,aAAa,EAAE;gBACzB,MAAMzC,GAAGwC,KAAK;gBACd;YACF;YACA,4FAA4F;YAC5F,MAAMrD,cAAca;YACpB,IAAI0C,cAA6B;YACjC,IAAI;oBAEmB;gBADrB,MAAMb,OAAO,MAAM3C,cAAce;gBACjC,MAAM0C,OAAOC,QAAQ,OAAA,MAAM/D,QAAQoB,MAAM,6BAApB,kBAAA,OAAwC;gBAC7D,MAAM4C,OAAOhB,SAAS,OAAO,OAAO,MAAMzC,SAASyC;gBACnD,IAAIgB,SAAS,MAAM;oBACjB,yFAAyF;oBACzF,2FAA2F;oBAC3F,IAAIF,SAAS,GAAG;wBACd,MAAM7D,QAAQmB,MAAM,gBAAgB6C,OAAOD;wBAC3C,+EAA+E;wBAC/E,MAAM1D,cAAca;oBACtB,OAAO,IAAI6C,OAAO/C,eAAe6C,MAAM;wBACrCD,cAAcb;oBAChB;gBACF;YACF,EAAE,OAAOkB,KAAK;gBACZC,QAAQC,KAAK,CAAC,CAAC,yFAAyF,EAAE,AAACF,IAAcG,OAAO,EAAE;YACpI;YACA,MAAMlD,GAAGwC,KAAK;YACd,qFAAqF;YACrF,+FAA+F;YAC/F,IAAIE,gBAAgB,MAAM,MAAMrD,aAAaqD;QAC/C;IACF;AACF"}
|
|
@@ -34,15 +34,24 @@ export interface OpenDialect<Handle> {
|
|
|
34
34
|
filename: string;
|
|
35
35
|
schemaVersion: string;
|
|
36
36
|
reconcileDialect: ReconcileDialect;
|
|
37
|
-
connect(dbPath: string, cfg: ResolvedConfig
|
|
37
|
+
connect(dbPath: string, cfg: ResolvedConfig, options: {
|
|
38
|
+
existingOnly: boolean;
|
|
39
|
+
observational: boolean;
|
|
40
|
+
}): Promise<{
|
|
38
41
|
handle: Handle;
|
|
39
42
|
conn: Connection;
|
|
40
43
|
}>;
|
|
41
|
-
close(handle: Handle
|
|
44
|
+
close(handle: Handle, options?: {
|
|
45
|
+
observational?: boolean;
|
|
46
|
+
}): Promise<void>;
|
|
42
47
|
isLocked?(err: Error): boolean;
|
|
43
48
|
ensureSchema(handle: Handle, conn: Connection, cfg: Config): Promise<void>;
|
|
44
49
|
setDerivedBusyTimeout?(handle: Handle, conn: Connection, ms: number): Promise<void>;
|
|
45
|
-
|
|
50
|
+
prepareLexical?(conn: Connection): Promise<void>;
|
|
51
|
+
assertLexicalReady?(conn: Connection): Promise<void>;
|
|
52
|
+
createStore(handle: Handle, conn: Connection, cfg: ResolvedConfig, options: {
|
|
53
|
+
observational: boolean;
|
|
54
|
+
}): Store;
|
|
46
55
|
}
|
|
47
56
|
export interface FieldStat {
|
|
48
57
|
field: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/types.ts"],"sourcesContent":["// The backing-store interface: a minimal portable statement surface (exec/prepare), plus\n// dedicated interfaces exactly where engines diverge (lexical index, vector scan, raw sql).\n\nimport type { Config, ResolvedConfig, StoreName } from '../config/index.ts';\nimport type { ReconcileDelta } from '../features/types.ts';\nimport type { ParsedDoc } from '../scan/index.ts';\n\n// 'vectors': the store's VectorStore is functionally implemented, not present-but-inert; checked\n// at open (index.ts) before a config with an \"embed\" block is allowed onto a store lacking it.\n// 'segment': the engine can run segment() (Intl.Segmenter grapheme clustering) as a SQL function;\n// turso's client cannot register it and has no rewrite for it, unlike has()/basename(), which now\n// work everywhere. An array, not a bare union, so a runtime check reads the same list the type does.\nexport const CAPABILITY_NAMES = ['vectors', 'segment'] as const;\nexport type Capability = (typeof CAPABILITY_NAMES)[number];\n\nexport interface RunResult {\n changes: number | bigint;\n lastInsertRowid: number | bigint;\n}\n\n// A prepared statement's async surface: every supported engine crosses a real async boundary\n// for a query (DuckDB has no synchronous client), so run/get/all return Promises.\nexport interface Statement {\n run(...params: unknown[]): Promise<RunResult>;\n get(...params: unknown[]): Promise<unknown>;\n all(...params: unknown[]): Promise<unknown[]>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n columns(): Array<{ name: string }>;\n setReadBigInts(enabled: boolean): void;\n}\n\n// Connection surface feature-owned SQL runs against inside a store's hot loops (schema,\n// reconcile), portable so a feature never imports an engine-specific client type.\nexport interface Connection {\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Bulk-inserts rows that cannot conflict, through a path that binds no per-value parameters.\n // Optional: a store that has nothing faster than its own INSERT omits it and callers fall back\n // (appendRows in shared.ts). `columns` names the values each row carries; a table column the\n // caller does not write takes its default.\n appendRows?(table: string, columns: string[], rows: unknown[][]): Promise<void>;\n}\n\n// One reconcile algorithm (src/store/reconcile.ts), parameterised per engine. reconcileContent is\n// the load-bearing member: it lands every content-table change and owns its own multi-step\n// strategy (sqlite FTS5 incremental, duckdb combined delete/insert, turso incremental-or-rebuild).\nexport interface ReconcileDialect {\n // BEGIN mode for the whole reconcile: sqlite/turso 'BEGIN IMMEDIATE', duckdb 'BEGIN'.\n beginMode(): string;\n // Throws SenseError('COLUMN_LIMIT', ...) past this store's own column ceiling and reasoning.\n checkColumnLimit(count: number): void;\n // Adds `names` to frontmatter, already filtered to columns this connection doesn't have yet.\n // sqlite/turso loop (their ADD COLUMN is metadata-only); duckdb issues one statement per call.\n addColumns(conn: Connection, names: string[]): Promise<void>;\n // Deletes content rows for `touched`, inserts rows for `docs`; `delta` carries the tree state a\n // strategy may need. Must not open its own transaction, and must not return before its own\n // multi-step strategy (e.g. turso's DROP/rebuild) completes.\n reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, cfg: Config): Promise<void>;\n // Records this reconcile's write-transaction duration. sqlite/turso use it for open()'s derived\n // busy_timeout; duckdb has no such PRAGMA and omits it.\n recordDuration?(conn: Connection, ms: number): Promise<void>;\n}\n\n// One open algorithm (src/store/open.ts), parameterised per engine. `Handle` is whatever this\n// store needs to close the connection and construct its Store (sqlite: {db}; duckdb:\n// {instance, duckdb}; turso: db) -- opaque to the shared orchestration, threaded through unchanged.\nexport interface OpenDialect<Handle> {\n // Cache filename under STATE_DIR, e.g. 'cache.db'.\n filename: string;\n // Cache shape version, independent of the config's own `version`; bumping it rebuilds an existing tree.\n schemaVersion: string;\n reconcileDialect: ReconcileDialect;\n // Opens the physical connection and applies pragmas due before any SQL runs (sqlite: busy_timeout\n // + WAL; turso: connect-time timeout; duckdb: none).\n connect(dbPath: string, cfg: ResolvedConfig): Promise<{ handle: Handle; conn: Connection }>;\n // Releases the handle, for both the rebuild-and-reopen branch and error cleanup on this attempt.\n close(handle: Handle): Promise<void>;\n // True when connect() failed because another process holds the cache file, which the orchestration\n // retries. Each engine words it differently, so the match is the dialect's own (`native-not-emulated`).\n // Absent for sqlite, whose file lock is shared and whose concurrent-open failure is a different defect.\n // Matching on message text is forced, not chosen: measured 2026-09-02, duckdb throws a plain Error\n // whose only own properties are stack and message, and turso sets code to the constant\n // 'GenericFailure' on every failure alike with rawCode undefined. Neither exposes anything a\n // predicate could switch on, so each dialect pins its engine's wordings and unit-tests them.\n isLocked?(err: Error): boolean;\n // Schema DDL beyond frontmatter/preset_files/meta (content table, feature hooks); sole owner of\n // whether it wraps itself in a write transaction (sqlite: yes, guards a cold-open ALTER race; duckdb/turso: no).\n ensureSchema(handle: Handle, conn: Connection, cfg: Config): Promise<void>;\n // Installs the derived busy_timeout PRAGMA right before reconcile (sqlite/turso); absent for\n // duckdb, which has no such PRAGMA.\n setDerivedBusyTimeout?(handle: Handle, conn: Connection, ms: number): Promise<void>;\n createStore(handle: Handle, conn: Connection, cfg: ResolvedConfig): Store;\n}\n\nexport interface FieldStat {\n field: string;\n coverage: number;\n type: string;\n}\n\nexport interface DocumentStore {\n // Frontmatter column names (including internal ones; callers filter).\n columns(): Promise<string[]>;\n // Per-column coverage (non-null count) and observed type set, aggregated in one SQL query,\n // never one row per note. `scopeWhere` is a caller-built WHERE fragment, per LexicalQueryOptions.\n fieldStats(columns: string[], scopeWhere: string): Promise<FieldStat[]>;\n}\n\n// A ranked match, nothing more. Snippets are cut from the document above the store\n// (commands/search.ts), identically for every engine, so no store carries them.\nexport interface LexicalHit {\n path: string;\n}\n\nexport interface LexicalQueryOptions {\n whereJoin: string;\n whereCond: string;\n scopeCond: string;\n limit: number;\n}\n\nexport interface LexicalIndex {\n // Ranked word-match query with snippet, scoped by the caller-built SQL fragments (the same\n // fragments narrowByWhere/materializeScope produce elsewhere).\n query(terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]>;\n}\n\nexport interface VectorCandidate {\n path: string;\n lines: string;\n similarity: number;\n}\n\nexport interface VectorSimilar {\n path: string;\n similarity: number;\n}\n\nexport interface VectorWriteRow {\n path: string;\n chunk: number;\n scale: number;\n vector: Buffer;\n}\n\nexport interface VectorStore {\n // Rows whose vector is NULL: never embedded, or added since.\n pending(): Promise<Array<{ path: string; chunk: number }>>;\n // One batch write per call (never per row) so a provider's embedding batch stays inside a\n // single store method.\n writeVectors(rows: VectorWriteRow[]): Promise<void>;\n candidates(queryVector: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;\n similar(path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]>;\n hasVector(path: string): Promise<boolean>;\n}\n\n// The `sense sql` passthrough: string in, streamed rows out. Each store registers the same\n// sense-supplied functions and applies its own read-bigints/error-translation behavior.\nexport interface RawStatement {\n columns(): Array<{ name: string }>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n}\n\nexport interface SqlSession {\n prepare(sql: string): Promise<RawStatement>;\n}\n\nexport interface Store {\n readonly name: StoreName;\n readonly capabilities: ReadonlySet<Capability>;\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n // One crossing, N rows: every external write loop (search's candidate insert, graph ring's\n // temp-table writes) goes through this instead of looping over `run()`. Same contract as Connection.runBatch.\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Pins one snapshot across multi-statement reads; `map` and `peek` use it. A network-bound\n // write (search's embedding top-up) must stay outside it. Nesting joins the enclosing transaction.\n transaction<T>(fn: () => Promise<T>): Promise<T>;\n docs: DocumentStore;\n lexical: LexicalIndex;\n vectors: VectorStore;\n raw: SqlSession;\n // Engine-level facts for `sense status` (e.g. a derived busy_timeout PRAGMA reading); each\n // store owns what it reports and how it is worded. Empty when there is nothing to report.\n engineStatus(): Promise<Record<string, string>>;\n close(): Promise<void>;\n}\n"],"names":["CAPABILITY_NAMES"],"mappings":"AAAA,yFAAyF;AACzF,4FAA4F;AAM5F,iGAAiG;AACjG,+FAA+F;AAC/F,kGAAkG;AAClG,kGAAkG;AAClG,qGAAqG;AACrG,OAAO,MAAMA,mBAAmB;IAAC;IAAW;CAAU,CAAU"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/types.ts"],"sourcesContent":["// The backing-store interface: a minimal portable statement surface (exec/prepare), plus\n// dedicated interfaces exactly where engines diverge (lexical index, vector scan, raw sql).\n\nimport type { Config, ResolvedConfig, StoreName } from '../config/index.ts';\nimport type { ReconcileDelta } from '../features/types.ts';\nimport type { ParsedDoc } from '../scan/index.ts';\n\n// 'vectors': the store's VectorStore is functionally implemented, not present-but-inert; checked\n// at open (index.ts) before a config with an \"embed\" block is allowed onto a store lacking it.\n// 'segment': the engine can run segment() (Intl.Segmenter grapheme clustering) as a SQL function;\n// turso's client cannot register it and has no rewrite for it, unlike has()/basename(), which now\n// work everywhere. An array, not a bare union, so a runtime check reads the same list the type does.\nexport const CAPABILITY_NAMES = ['vectors', 'segment'] as const;\nexport type Capability = (typeof CAPABILITY_NAMES)[number];\n\nexport interface RunResult {\n changes: number | bigint;\n lastInsertRowid: number | bigint;\n}\n\n// A prepared statement's async surface: every supported engine crosses a real async boundary\n// for a query (DuckDB has no synchronous client), so run/get/all return Promises.\nexport interface Statement {\n run(...params: unknown[]): Promise<RunResult>;\n get(...params: unknown[]): Promise<unknown>;\n all(...params: unknown[]): Promise<unknown[]>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n columns(): Array<{ name: string }>;\n setReadBigInts(enabled: boolean): void;\n}\n\n// Connection surface feature-owned SQL runs against inside a store's hot loops (schema,\n// reconcile), portable so a feature never imports an engine-specific client type.\nexport interface Connection {\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Bulk-inserts rows that cannot conflict, through a path that binds no per-value parameters.\n // Optional: a store that has nothing faster than its own INSERT omits it and callers fall back\n // (appendRows in shared.ts). `columns` names the values each row carries; a table column the\n // caller does not write takes its default.\n appendRows?(table: string, columns: string[], rows: unknown[][]): Promise<void>;\n}\n\n// One reconcile algorithm (src/store/reconcile.ts), parameterised per engine. reconcileContent is\n// the load-bearing member: it lands every content-table change and owns its own multi-step\n// strategy (sqlite FTS5 incremental, duckdb combined delete/insert, turso incremental-or-rebuild).\nexport interface ReconcileDialect {\n // BEGIN mode for the whole reconcile: sqlite/turso 'BEGIN IMMEDIATE', duckdb 'BEGIN'.\n beginMode(): string;\n // Throws SenseError('COLUMN_LIMIT', ...) past this store's own column ceiling and reasoning.\n checkColumnLimit(count: number): void;\n // Adds `names` to frontmatter, already filtered to columns this connection doesn't have yet.\n // sqlite/turso loop (their ADD COLUMN is metadata-only); duckdb issues one statement per call.\n addColumns(conn: Connection, names: string[]): Promise<void>;\n // Deletes content rows for `touched`, inserts rows for `docs`; `delta` carries the tree state a\n // strategy may need. Must not open its own transaction, and must not return before its own\n // multi-step strategy (e.g. turso's DROP/rebuild) completes.\n reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, cfg: Config): Promise<void>;\n // Records this reconcile's write-transaction duration. sqlite/turso use it for open()'s derived\n // busy_timeout; duckdb has no such PRAGMA and omits it.\n recordDuration?(conn: Connection, ms: number): Promise<void>;\n}\n\n// One open algorithm (src/store/open.ts), parameterised per engine. `Handle` is whatever this\n// store needs to close the connection and construct its Store (sqlite: {db}; duckdb:\n// {instance, duckdb}; turso: db) -- opaque to the shared orchestration, threaded through unchanged.\nexport interface OpenDialect<Handle> {\n // Cache filename under STATE_DIR, e.g. 'cache.db'.\n filename: string;\n // Cache shape version, independent of the config's own `version`; bumping it rebuilds an existing tree.\n schemaVersion: string;\n reconcileDialect: ReconcileDialect;\n // Opens the physical connection and applies pragmas due before any SQL runs (sqlite: busy_timeout\n // + WAL; turso: connect-time timeout; duckdb: none).\n connect(dbPath: string, cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: Handle; conn: Connection }>;\n // Releases the handle, for both the rebuild-and-reopen branch and error cleanup on this attempt.\n close(handle: Handle, options?: { observational?: boolean }): Promise<void>;\n // True when connect() failed because another process holds the cache file, which the orchestration\n // retries. Each engine words it differently, so the match is the dialect's own (`native-not-emulated`).\n // Absent for sqlite, whose file lock is shared and whose concurrent-open failure is a different defect.\n // Matching on message text is forced, not chosen: measured 2026-09-02, duckdb throws a plain Error\n // whose only own properties are stack and message, and turso sets code to the constant\n // 'GenericFailure' on every failure alike with rawCode undefined. Neither exposes anything a\n // predicate could switch on, so each dialect pins its engine's wordings and unit-tests them.\n isLocked?(err: Error): boolean;\n // Schema DDL beyond frontmatter/preset_files/meta (content table, feature hooks); sole owner of\n // whether it wraps itself in a write transaction (sqlite: yes, guards a cold-open ALTER race; duckdb/turso: no).\n ensureSchema(handle: Handle, conn: Connection, cfg: Config): Promise<void>;\n // Installs the derived busy_timeout PRAGMA right before reconcile (sqlite/turso); absent for\n // duckdb, which has no such PRAGMA.\n setDerivedBusyTimeout?(handle: Handle, conn: Connection, ms: number): Promise<void>;\n // Persistent lexical preparation belongs to build, while no-build calls only the readiness\n // check. SQLite and Turso maintain lexical state during reconcile and omit both hooks.\n prepareLexical?(conn: Connection): Promise<void>;\n assertLexicalReady?(conn: Connection): Promise<void>;\n createStore(handle: Handle, conn: Connection, cfg: ResolvedConfig, options: { observational: boolean }): Store;\n}\n\nexport interface FieldStat {\n field: string;\n coverage: number;\n type: string;\n}\n\nexport interface DocumentStore {\n // Frontmatter column names (including internal ones; callers filter).\n columns(): Promise<string[]>;\n // Per-column coverage (non-null count) and observed type set, aggregated in one SQL query,\n // never one row per note. `scopeWhere` is a caller-built WHERE fragment, per LexicalQueryOptions.\n fieldStats(columns: string[], scopeWhere: string): Promise<FieldStat[]>;\n}\n\n// A ranked match, nothing more. Snippets are cut from the document above the store\n// (commands/search.ts), identically for every engine, so no store carries them.\nexport interface LexicalHit {\n path: string;\n}\n\nexport interface LexicalQueryOptions {\n whereJoin: string;\n whereCond: string;\n scopeCond: string;\n limit: number;\n}\n\nexport interface LexicalIndex {\n // Ranked word-match query with snippet, scoped by the caller-built SQL fragments (the same\n // fragments narrowByWhere/materializeScope produce elsewhere).\n query(terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]>;\n}\n\nexport interface VectorCandidate {\n path: string;\n lines: string;\n similarity: number;\n}\n\nexport interface VectorSimilar {\n path: string;\n similarity: number;\n}\n\nexport interface VectorWriteRow {\n path: string;\n chunk: number;\n scale: number;\n vector: Buffer;\n}\n\nexport interface VectorStore {\n // Rows whose vector is NULL: never embedded, or added since.\n pending(): Promise<Array<{ path: string; chunk: number }>>;\n // One batch write per call (never per row) so a provider's embedding batch stays inside a\n // single store method.\n writeVectors(rows: VectorWriteRow[]): Promise<void>;\n candidates(queryVector: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;\n similar(path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]>;\n hasVector(path: string): Promise<boolean>;\n}\n\n// The `sense sql` passthrough: string in, streamed rows out. Each store registers the same\n// sense-supplied functions and applies its own read-bigints/error-translation behavior.\nexport interface RawStatement {\n columns(): Array<{ name: string }>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n}\n\nexport interface SqlSession {\n prepare(sql: string): Promise<RawStatement>;\n}\n\nexport interface Store {\n readonly name: StoreName;\n readonly capabilities: ReadonlySet<Capability>;\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n // One crossing, N rows: every external write loop (search's candidate insert, graph ring's\n // temp-table writes) goes through this instead of looping over `run()`. Same contract as Connection.runBatch.\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Pins one snapshot across multi-statement reads; `map` and `peek` use it. A network-bound\n // write (search's embedding top-up) must stay outside it. Nesting joins the enclosing transaction.\n transaction<T>(fn: () => Promise<T>): Promise<T>;\n docs: DocumentStore;\n lexical: LexicalIndex;\n vectors: VectorStore;\n raw: SqlSession;\n // Engine-level facts for `sense status` (e.g. a derived busy_timeout PRAGMA reading); each\n // store owns what it reports and how it is worded. Empty when there is nothing to report.\n engineStatus(): Promise<Record<string, string>>;\n close(): Promise<void>;\n}\n"],"names":["CAPABILITY_NAMES"],"mappings":"AAAA,yFAAyF;AACzF,4FAA4F;AAM5F,iGAAiG;AACjG,+FAA+F;AAC/F,kGAAkG;AAClG,kGAAkG;AAClG,qGAAqG;AACrG,OAAO,MAAMA,mBAAmB;IAAC;IAAW;CAAU,CAAU"}
|
package/dist/esm/watch.js
CHANGED
|
@@ -4,7 +4,8 @@ import { guardedTick } from './lib/guarded-tick.js';
|
|
|
4
4
|
import { shouldReconcileWatchNotification } from './lib/watch-notification.js';
|
|
5
5
|
import { docCount, openStore } from './store/index.js';
|
|
6
6
|
import { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.js';
|
|
7
|
-
// Watch
|
|
7
|
+
// Watch prepares the index for snapshot queries; filesystem events request debounced builds
|
|
8
|
+
// and periodic reconciliation recovers missed notifications.
|
|
8
9
|
const DEBOUNCE_MS = 200;
|
|
9
10
|
// Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to
|
|
10
11
|
// one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.
|
|
@@ -29,7 +30,27 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
29
30
|
claimCloseStarted = true;
|
|
30
31
|
return claim.close();
|
|
31
32
|
};
|
|
33
|
+
let watcher;
|
|
34
|
+
let startup = true;
|
|
35
|
+
let startupNotification = false;
|
|
36
|
+
let scheduleReconcile;
|
|
37
|
+
let stopping = false;
|
|
32
38
|
try {
|
|
39
|
+
var _opts_signal;
|
|
40
|
+
// Subscribe before the first build. Native watchers have no ready barrier, so startup also
|
|
41
|
+
// performs an unconditional catch-up pass after the initial build.
|
|
42
|
+
watcher = fsWatch(rootDir, {
|
|
43
|
+
recursive: true
|
|
44
|
+
}, (_event, filename)=>{
|
|
45
|
+
if (!shouldReconcileWatchNotification(filename)) return;
|
|
46
|
+
if (startup) startupNotification = true;
|
|
47
|
+
else scheduleReconcile === null || scheduleReconcile === void 0 ? void 0 : scheduleReconcile();
|
|
48
|
+
});
|
|
49
|
+
watcher.on('error', (err)=>{
|
|
50
|
+
claimFailure !== null && claimFailure !== void 0 ? claimFailure : claimFailure = err;
|
|
51
|
+
requestShutdown === null || requestShutdown === void 0 ? void 0 : requestShutdown(err);
|
|
52
|
+
});
|
|
53
|
+
const startupEvents = [];
|
|
33
54
|
const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);
|
|
34
55
|
let initialTotal = 0;
|
|
35
56
|
try {
|
|
@@ -39,7 +60,28 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
39
60
|
await initialStore.close();
|
|
40
61
|
}
|
|
41
62
|
if (claimFailure) throw claimFailure;
|
|
42
|
-
|
|
63
|
+
startupEvents.push({
|
|
64
|
+
parsed: initialParsed,
|
|
65
|
+
total: initialTotal,
|
|
66
|
+
warnings: initialWarnings
|
|
67
|
+
});
|
|
68
|
+
// Always reopen once after subscribing, then drain notifications observed while startup was
|
|
69
|
+
// in progress. Each pass closes before the next one starts, so no stores overlap.
|
|
70
|
+
do {
|
|
71
|
+
startupNotification = false;
|
|
72
|
+
const { store, parsed, warnings } = await openStore(cfg);
|
|
73
|
+
try {
|
|
74
|
+
if (parsed > 0 || warnings.length > 0) startupEvents.push({
|
|
75
|
+
parsed,
|
|
76
|
+
total: await docCount(store),
|
|
77
|
+
warnings
|
|
78
|
+
});
|
|
79
|
+
} finally{
|
|
80
|
+
await store.close();
|
|
81
|
+
}
|
|
82
|
+
if (claimFailure) throw claimFailure;
|
|
83
|
+
}while (startupNotification && !((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted))
|
|
84
|
+
startup = false;
|
|
43
85
|
// At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is
|
|
44
86
|
// coalesced into a single rerun instead of opening a second, overlapping connection.
|
|
45
87
|
let running = null;
|
|
@@ -85,21 +127,15 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
85
127
|
startCycle(alwaysEmit);
|
|
86
128
|
};
|
|
87
129
|
let debounceTimer = null;
|
|
88
|
-
|
|
130
|
+
// Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.
|
|
131
|
+
// An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.
|
|
132
|
+
scheduleReconcile = ()=>{
|
|
89
133
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
90
134
|
debounceTimer = setTimeout(()=>{
|
|
91
135
|
debounceTimer = null;
|
|
92
136
|
requestCycle(true);
|
|
93
137
|
}, debounceMs);
|
|
94
138
|
};
|
|
95
|
-
// Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.
|
|
96
|
-
// An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.
|
|
97
|
-
const watcher = fsWatch(rootDir, {
|
|
98
|
-
recursive: true
|
|
99
|
-
}, (_event, filename)=>{
|
|
100
|
-
if (!shouldReconcileWatchNotification(filename)) return;
|
|
101
|
-
scheduleReconcile();
|
|
102
|
-
});
|
|
103
139
|
const reconcileTimer = setInterval(guardedTick(async ()=>requestCycle(false), ()=>stopping), reconcileIntervalMs);
|
|
104
140
|
return await new Promise((resolveShutdown, rejectShutdown)=>{
|
|
105
141
|
var _opts_signal;
|
|
@@ -115,7 +151,7 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
115
151
|
(_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.removeEventListener('abort', stopNormally);
|
|
116
152
|
clearInterval(reconcileTimer);
|
|
117
153
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
118
|
-
watcher.close();
|
|
154
|
+
watcher === null || watcher === void 0 ? void 0 : watcher.close();
|
|
119
155
|
await running;
|
|
120
156
|
let finalError;
|
|
121
157
|
try {
|
|
@@ -159,13 +195,14 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
159
195
|
rootDir,
|
|
160
196
|
dbPath
|
|
161
197
|
});
|
|
162
|
-
if (!stopping
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
parsed
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
if (!stopping) {
|
|
199
|
+
for (const event of startupEvents){
|
|
200
|
+
if (stopping) break;
|
|
201
|
+
if (event.warnings.length > 0 || event.parsed > 0) onEvent({
|
|
202
|
+
type: 'reconciled',
|
|
203
|
+
...event
|
|
204
|
+
});
|
|
205
|
+
}
|
|
169
206
|
}
|
|
170
207
|
} catch (err) {
|
|
171
208
|
shutdown(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -173,6 +210,7 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
173
210
|
}
|
|
174
211
|
});
|
|
175
212
|
} catch (err) {
|
|
213
|
+
watcher === null || watcher === void 0 ? void 0 : watcher.close();
|
|
176
214
|
if (claimCloseStarted) throw err;
|
|
177
215
|
try {
|
|
178
216
|
await closeClaim();
|
package/dist/esm/watch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { shouldReconcileWatchNotification } from './lib/watch-notification.ts';\nimport { docCount, openStore } from './store/index.ts';\nimport { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to\n// one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const reconcileIntervalMs = opts.heartbeatIntervalMs ?? WATCH_HEARTBEAT_INTERVAL_MS;\n if (!Number.isFinite(reconcileIntervalMs) || reconcileIntervalMs <= 0) throw new SenseError('CONFIG_INVALID', 'watch heartbeatIntervalMs must be a finite positive number');\n // Reconciliation may be parked for a caller, but live ownership must renew before fixed expiry.\n const claimIntervalMs = Math.min(reconcileIntervalMs, WATCH_HEARTBEAT_INTERVAL_MS);\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const configDir = cfg.configDir ?? rootDir;\n let claimFailure: Error | null = null;\n let requestShutdown: ((reason: Error) => void) | undefined;\n const claim = await startWatchClaim(configDir, opts.force ?? false, claimIntervalMs, (err) => {\n claimFailure ??= err;\n requestShutdown?.(err);\n });\n let claimCloseStarted = false;\n const closeClaim = (): Promise<void> => {\n claimCloseStarted = true;\n return claim.close();\n };\n try {\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n let initialTotal = 0;\n try {\n // total is read before close: an event fires only once nothing is held open again.\n initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n } finally {\n await initialStore.close();\n }\n if (claimFailure) throw claimFailure;\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.\n // An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (!shouldReconcileWatchNotification(filename)) return;\n scheduleReconcile();\n });\n const reconcileTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n reconcileIntervalMs\n );\n\n return await new Promise<void>((resolveShutdown, rejectShutdown) => {\n let shutdownReason: Error | null = null;\n const shutdown = (reason?: Error) => {\n shutdownReason ??= reason ?? null;\n if (stopping) return;\n stopping = true;\n const shutdownTask = (async () => {\n process.off('SIGINT', stopNormally);\n process.off('SIGTERM', stopNormally);\n opts.signal?.removeEventListener('abort', stopNormally);\n clearInterval(reconcileTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n let finalError: unknown;\n try {\n // A normal stop performs one final freshness pass. Ownership loss drains existing work\n // but starts nothing new because the replacement watcher now owns that responsibility.\n if (!shutdownReason) {\n const { store: finalStore } = await openStore(cfg);\n await finalStore.close();\n }\n } catch (err) {\n finalError = err;\n }\n try {\n await closeClaim();\n } catch (err) {\n if (finalError) throw new AggregateError([finalError, err], 'watch shutdown and claim cleanup both failed');\n throw err;\n }\n if (finalError) throw finalError;\n if (shutdownReason) throw shutdownReason;\n })();\n shutdownTask.then(resolveShutdown, rejectShutdown);\n };\n const stopNormally = () => shutdown();\n requestShutdown = shutdown;\n process.once('SIGINT', stopNormally);\n process.once('SIGTERM', stopNormally);\n if (claimFailure) shutdown(claimFailure);\n else if (opts.signal?.aborted) shutdown();\n else {\n opts.signal?.addEventListener('abort', stopNormally, { once: true });\n try {\n onEvent({ type: 'started', rootDir, dbPath });\n if (!stopping && (initialWarnings.length > 0 || initialParsed > 0)) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n } catch (err) {\n shutdown(err instanceof Error ? err : new Error(String(err)));\n }\n }\n });\n } catch (err) {\n if (claimCloseStarted) throw err;\n try {\n await closeClaim();\n } catch (closeError) {\n throw new AggregateError([err, closeError], 'watch setup and claim cleanup both failed');\n }\n throw err;\n } finally {\n requestShutdown = undefined;\n }\n}\n"],"names":["watch","fsWatch","SenseError","guardedTick","shouldReconcileWatchNotification","docCount","openStore","startWatchClaim","WATCH_HEARTBEAT_INTERVAL_MS","DEBOUNCE_MS","runWatch","cfg","opts","onEvent","debounceMs","reconcileIntervalMs","heartbeatIntervalMs","Number","isFinite","claimIntervalMs","Math","min","rootDir","baseDir","configDir","claimFailure","requestShutdown","claim","force","err","claimCloseStarted","closeClaim","close","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","initialTotal","length","stopping","running","queuedAlways","tick","alwaysEmit","type","total","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","reconcileTimer","setInterval","Promise","resolveShutdown","rejectShutdown","shutdownReason","shutdown","reason","shutdownTask","process","off","stopNormally","signal","removeEventListener","clearInterval","finalError","finalStore","AggregateError","once","aborted","addEventListener","Error","String","closeError","undefined"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { shouldReconcileWatchNotification } from './lib/watch-notification.ts';\nimport { docCount, openStore } from './store/index.ts';\nimport { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.ts';\n\n// Watch prepares the index for snapshot queries; filesystem events request debounced builds\n// and periodic reconciliation recovers missed notifications.\nconst DEBOUNCE_MS = 200;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to\n// one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const reconcileIntervalMs = opts.heartbeatIntervalMs ?? WATCH_HEARTBEAT_INTERVAL_MS;\n if (!Number.isFinite(reconcileIntervalMs) || reconcileIntervalMs <= 0) throw new SenseError('CONFIG_INVALID', 'watch heartbeatIntervalMs must be a finite positive number');\n // Reconciliation may be parked for a caller, but live ownership must renew before fixed expiry.\n const claimIntervalMs = Math.min(reconcileIntervalMs, WATCH_HEARTBEAT_INTERVAL_MS);\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const configDir = cfg.configDir ?? rootDir;\n let claimFailure: Error | null = null;\n let requestShutdown: ((reason: Error) => void) | undefined;\n const claim = await startWatchClaim(configDir, opts.force ?? false, claimIntervalMs, (err) => {\n claimFailure ??= err;\n requestShutdown?.(err);\n });\n let claimCloseStarted = false;\n const closeClaim = (): Promise<void> => {\n claimCloseStarted = true;\n return claim.close();\n };\n let watcher: ReturnType<typeof fsWatch> | undefined;\n let startup = true;\n let startupNotification = false;\n let scheduleReconcile: (() => void) | undefined;\n let stopping = false;\n try {\n // Subscribe before the first build. Native watchers have no ready barrier, so startup also\n // performs an unconditional catch-up pass after the initial build.\n watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (!shouldReconcileWatchNotification(filename)) return;\n if (startup) startupNotification = true;\n else scheduleReconcile?.();\n });\n watcher.on('error', (err) => {\n claimFailure ??= err;\n requestShutdown?.(err);\n });\n\n const startupEvents: Array<{ parsed: number; total: number; warnings: string[] }> = [];\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n let initialTotal = 0;\n try {\n // total is read before close: an event fires only once nothing is held open again.\n initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n } finally {\n await initialStore.close();\n }\n if (claimFailure) throw claimFailure;\n\n startupEvents.push({ parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n // Always reopen once after subscribing, then drain notifications observed while startup was\n // in progress. Each pass closes before the next one starts, so no stores overlap.\n do {\n startupNotification = false;\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n if (parsed > 0 || warnings.length > 0) startupEvents.push({ parsed, total: await docCount(store), warnings });\n } finally {\n await store.close();\n }\n if (claimFailure) throw claimFailure;\n } while (startupNotification && !opts.signal?.aborted);\n startup = false;\n\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n // Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.\n // An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.\n scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n const reconcileTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n reconcileIntervalMs\n );\n\n return await new Promise<void>((resolveShutdown, rejectShutdown) => {\n let shutdownReason: Error | null = null;\n const shutdown = (reason?: Error) => {\n shutdownReason ??= reason ?? null;\n if (stopping) return;\n stopping = true;\n const shutdownTask = (async () => {\n process.off('SIGINT', stopNormally);\n process.off('SIGTERM', stopNormally);\n opts.signal?.removeEventListener('abort', stopNormally);\n clearInterval(reconcileTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher?.close();\n await running;\n let finalError: unknown;\n try {\n // A normal stop performs one final freshness pass. Ownership loss drains existing work\n // but starts nothing new because the replacement watcher now owns that responsibility.\n if (!shutdownReason) {\n const { store: finalStore } = await openStore(cfg);\n await finalStore.close();\n }\n } catch (err) {\n finalError = err;\n }\n try {\n await closeClaim();\n } catch (err) {\n if (finalError) throw new AggregateError([finalError, err], 'watch shutdown and claim cleanup both failed');\n throw err;\n }\n if (finalError) throw finalError;\n if (shutdownReason) throw shutdownReason;\n })();\n shutdownTask.then(resolveShutdown, rejectShutdown);\n };\n const stopNormally = () => shutdown();\n requestShutdown = shutdown;\n process.once('SIGINT', stopNormally);\n process.once('SIGTERM', stopNormally);\n if (claimFailure) shutdown(claimFailure);\n else if (opts.signal?.aborted) shutdown();\n else {\n opts.signal?.addEventListener('abort', stopNormally, { once: true });\n try {\n onEvent({ type: 'started', rootDir, dbPath });\n if (!stopping) {\n for (const event of startupEvents) {\n if (stopping) break;\n if (event.warnings.length > 0 || event.parsed > 0) onEvent({ type: 'reconciled', ...event });\n }\n }\n } catch (err) {\n shutdown(err instanceof Error ? err : new Error(String(err)));\n }\n }\n });\n } catch (err) {\n watcher?.close();\n if (claimCloseStarted) throw err;\n try {\n await closeClaim();\n } catch (closeError) {\n throw new AggregateError([err, closeError], 'watch setup and claim cleanup both failed');\n }\n throw err;\n } finally {\n requestShutdown = undefined;\n }\n}\n"],"names":["watch","fsWatch","SenseError","guardedTick","shouldReconcileWatchNotification","docCount","openStore","startWatchClaim","WATCH_HEARTBEAT_INTERVAL_MS","DEBOUNCE_MS","runWatch","cfg","opts","onEvent","debounceMs","reconcileIntervalMs","heartbeatIntervalMs","Number","isFinite","claimIntervalMs","Math","min","rootDir","baseDir","configDir","claimFailure","requestShutdown","claim","force","err","claimCloseStarted","closeClaim","close","watcher","startup","startupNotification","scheduleReconcile","stopping","recursive","_event","filename","on","startupEvents","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","initialTotal","length","push","total","signal","aborted","running","queuedAlways","tick","alwaysEmit","type","message","startCycle","then","again","requestCycle","debounceTimer","clearTimeout","setTimeout","reconcileTimer","setInterval","Promise","resolveShutdown","rejectShutdown","shutdownReason","shutdown","reason","shutdownTask","process","off","stopNormally","removeEventListener","clearInterval","finalError","finalStore","AggregateError","once","addEventListener","event","Error","String","closeError","undefined"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sensemaking",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Query and search your markdown notes with context-aware progressive disclosure: SQL over frontmatter, links, and text, plus semantic search and link-graph ranking. No server, no build step.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|