sensemaking 0.17.0 → 0.18.0
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 +15 -13
- package/dist/cjs/cli/map.js +147 -6
- package/dist/cjs/cli/map.js.map +1 -1
- package/dist/cjs/cli/named.js +20 -12
- package/dist/cjs/cli/named.js.map +1 -1
- package/dist/cjs/cli/path.js +190 -29
- package/dist/cjs/cli/path.js.map +1 -1
- package/dist/cjs/cli/peek.js +148 -7
- package/dist/cjs/cli/peek.js.map +1 -1
- package/dist/cjs/cli/related.js +5 -5
- package/dist/cjs/cli/related.js.map +1 -1
- package/dist/cjs/cli/search.js +5 -5
- package/dist/cjs/cli/search.js.map +1 -1
- package/dist/cjs/cli/shared.d.cts +3 -3
- package/dist/cjs/cli/shared.d.ts +3 -3
- package/dist/cjs/cli/shared.js +156 -58
- package/dist/cjs/cli/shared.js.map +1 -1
- package/dist/cjs/cli/sql.js +151 -10
- package/dist/cjs/cli/sql.js.map +1 -1
- package/dist/cjs/cli/status.js +187 -62
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/commands/map.d.cts +2 -2
- package/dist/cjs/commands/map.d.ts +2 -2
- package/dist/cjs/commands/map.js +373 -67
- package/dist/cjs/commands/map.js.map +1 -1
- package/dist/cjs/commands/peek.d.cts +2 -2
- package/dist/cjs/commands/peek.d.ts +2 -2
- package/dist/cjs/commands/peek.js +321 -79
- package/dist/cjs/commands/peek.js.map +1 -1
- package/dist/cjs/commands/related.d.cts +2 -2
- package/dist/cjs/commands/related.d.ts +2 -2
- package/dist/cjs/commands/related.js +55 -10
- package/dist/cjs/commands/related.js.map +1 -1
- package/dist/cjs/commands/scope.d.cts +7 -7
- package/dist/cjs/commands/scope.d.ts +7 -7
- package/dist/cjs/commands/scope.js +382 -45
- package/dist/cjs/commands/scope.js.map +1 -1
- package/dist/cjs/commands/search.d.cts +2 -2
- package/dist/cjs/commands/search.d.ts +2 -2
- package/dist/cjs/commands/search.js +292 -99
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/commands/signals.d.cts +4 -10
- package/dist/cjs/commands/signals.d.ts +4 -10
- package/dist/cjs/commands/signals.js +91 -45
- package/dist/cjs/commands/signals.js.map +1 -1
- package/dist/cjs/commands/status.d.cts +2 -2
- package/dist/cjs/commands/status.d.ts +2 -2
- package/dist/cjs/commands/status.js +247 -10
- package/dist/cjs/commands/status.js.map +1 -1
- package/dist/cjs/config/access.d.cts +1 -0
- package/dist/cjs/config/access.d.ts +1 -0
- package/dist/cjs/config/access.js +7 -0
- package/dist/cjs/config/access.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 +3 -0
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/config/types.d.cts +1 -0
- package/dist/cjs/config/types.d.ts +1 -0
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/config/validate.js +10 -1
- package/dist/cjs/config/validate.js.map +1 -1
- package/dist/cjs/embed/distribution.d.cts +4 -0
- package/dist/cjs/embed/distribution.d.ts +4 -0
- package/dist/cjs/embed/distribution.js +186 -0
- package/dist/cjs/embed/distribution.js.map +1 -0
- package/dist/cjs/embed/langfit.d.cts +2 -5
- package/dist/cjs/embed/langfit.d.ts +2 -5
- package/dist/cjs/embed/langfit.js +73 -63
- package/dist/cjs/embed/langfit.js.map +1 -1
- package/dist/cjs/embed/query.d.cts +6 -13
- package/dist/cjs/embed/query.d.ts +6 -13
- package/dist/cjs/embed/query.js +56 -168
- package/dist/cjs/embed/query.js.map +1 -1
- package/dist/cjs/embed/static.js +13 -3
- package/dist/cjs/embed/static.js.map +1 -1
- package/dist/cjs/embed/types.d.cts +1 -0
- package/dist/cjs/embed/types.d.ts +1 -0
- package/dist/cjs/embed/types.js +11 -2
- package/dist/cjs/embed/types.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 +218 -11
- package/dist/cjs/features/embed.js.map +1 -1
- package/dist/cjs/features/index.d.cts +1 -1
- package/dist/cjs/features/index.d.ts +1 -1
- package/dist/cjs/features/index.js +6 -0
- package/dist/cjs/features/index.js.map +1 -1
- package/dist/cjs/features/links.d.cts +7 -2
- package/dist/cjs/features/links.d.ts +7 -2
- package/dist/cjs/features/links.js +710 -198
- package/dist/cjs/features/links.js.map +1 -1
- package/dist/cjs/features/rank.js +231 -31
- package/dist/cjs/features/rank.js.map +1 -1
- package/dist/cjs/features/sections.js +218 -8
- package/dist/cjs/features/sections.js.map +1 -1
- package/dist/cjs/features/tags.js +236 -40
- package/dist/cjs/features/tags.js.map +1 -1
- package/dist/cjs/features/types.d.cts +9 -5
- package/dist/cjs/features/types.d.ts +9 -5
- package/dist/cjs/graph/traverse.d.cts +2 -2
- package/dist/cjs/graph/traverse.d.ts +2 -2
- package/dist/cjs/graph/traverse.js +321 -42
- package/dist/cjs/graph/traverse.js.map +1 -1
- package/dist/cjs/index.d.cts +3 -2
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/guarded-tick.d.cts +1 -0
- package/dist/cjs/lib/guarded-tick.d.ts +1 -0
- package/dist/cjs/lib/guarded-tick.js +19 -0
- package/dist/cjs/lib/guarded-tick.js.map +1 -0
- package/dist/cjs/output/column-hint.d.cts +1 -2
- package/dist/cjs/output/column-hint.d.ts +1 -2
- package/dist/cjs/output/column-hint.js +1 -4
- package/dist/cjs/output/column-hint.js.map +1 -1
- package/dist/cjs/output/output.d.cts +2 -1
- package/dist/cjs/output/output.d.ts +2 -1
- package/dist/cjs/output/output.js +515 -97
- package/dist/cjs/output/output.js.map +1 -1
- package/dist/cjs/scan/frontmatter.js.map +1 -1
- package/dist/cjs/scan/index.js.map +1 -1
- package/dist/cjs/scan/reparse.d.cts +10 -0
- package/dist/cjs/scan/reparse.d.ts +10 -0
- package/dist/cjs/scan/reparse.js +104 -0
- package/dist/cjs/scan/reparse.js.map +1 -0
- package/dist/cjs/store/duckdb/batch.d.cts +16 -0
- package/dist/cjs/store/duckdb/batch.d.ts +16 -0
- package/dist/cjs/store/duckdb/batch.js +208 -0
- package/dist/cjs/store/duckdb/batch.js.map +1 -0
- package/dist/cjs/store/duckdb/connection.d.cts +5 -0
- package/dist/cjs/store/duckdb/connection.d.ts +5 -0
- package/dist/cjs/store/duckdb/connection.js +772 -0
- package/dist/cjs/store/duckdb/connection.js.map +1 -0
- package/dist/cjs/store/duckdb/lexical.d.cts +10 -0
- package/dist/cjs/store/duckdb/lexical.d.ts +10 -0
- package/dist/cjs/store/duckdb/lexical.js +502 -0
- package/dist/cjs/store/duckdb/lexical.js.map +1 -0
- package/dist/cjs/store/duckdb/native.d.cts +3 -0
- package/dist/cjs/store/duckdb/native.d.ts +3 -0
- package/dist/cjs/store/duckdb/native.js +360 -0
- package/dist/cjs/store/duckdb/native.js.map +1 -0
- package/dist/cjs/store/duckdb/open.d.cts +12 -0
- package/dist/cjs/store/duckdb/open.d.ts +12 -0
- package/dist/cjs/store/duckdb/open.js +501 -0
- package/dist/cjs/store/duckdb/open.js.map +1 -0
- package/dist/cjs/store/duckdb/reconcile.d.cts +6 -0
- package/dist/cjs/store/duckdb/reconcile.d.ts +6 -0
- package/dist/cjs/store/duckdb/reconcile.js +753 -0
- package/dist/cjs/store/duckdb/reconcile.js.map +1 -0
- package/dist/cjs/store/duckdb/sql-functions.d.cts +2 -0
- package/dist/cjs/store/duckdb/sql-functions.d.ts +2 -0
- package/dist/cjs/store/duckdb/sql-functions.js +203 -0
- package/dist/cjs/store/duckdb/sql-functions.js.map +1 -0
- package/dist/cjs/store/duckdb/store.d.cts +5 -0
- package/dist/cjs/store/duckdb/store.d.ts +5 -0
- package/dist/cjs/store/duckdb/store.js +504 -0
- package/dist/cjs/store/duckdb/store.js.map +1 -0
- package/dist/cjs/store/duckdb/vectors.d.cts +9 -0
- package/dist/cjs/store/duckdb/vectors.d.ts +9 -0
- package/dist/cjs/store/duckdb/vectors.js +502 -0
- package/dist/cjs/store/duckdb/vectors.js.map +1 -0
- package/dist/cjs/store/index.d.cts +7 -0
- package/dist/cjs/store/index.d.ts +7 -0
- package/dist/cjs/store/index.js +201 -0
- package/dist/cjs/store/index.js.map +1 -0
- package/dist/cjs/store/meta.d.cts +3 -0
- package/dist/cjs/store/meta.d.ts +3 -0
- package/dist/cjs/store/meta.js +215 -0
- package/dist/cjs/store/meta.js.map +1 -0
- package/dist/cjs/store/shared.d.cts +5 -0
- package/dist/cjs/store/shared.d.ts +5 -0
- package/dist/cjs/store/shared.js +252 -0
- package/dist/cjs/store/shared.js.map +1 -0
- package/dist/cjs/store/sql-functions.d.cts +2 -0
- package/dist/cjs/store/sql-functions.d.ts +2 -0
- package/dist/cjs/store/sql-functions.js +48 -0
- package/dist/cjs/store/sql-functions.js.map +1 -0
- package/dist/cjs/store/sqlite/connection.d.cts +3 -0
- package/dist/cjs/store/sqlite/connection.d.ts +3 -0
- package/dist/cjs/store/sqlite/connection.js +516 -0
- package/dist/cjs/store/sqlite/connection.js.map +1 -0
- package/dist/cjs/store/sqlite/lexical.d.cts +2 -0
- package/dist/cjs/store/sqlite/lexical.d.ts +2 -0
- package/dist/cjs/store/sqlite/lexical.js +177 -0
- package/dist/cjs/store/sqlite/lexical.js.map +1 -0
- package/dist/cjs/{db → store/sqlite}/open.d.cts +5 -5
- package/dist/cjs/{db → store/sqlite}/open.d.ts +5 -5
- package/dist/cjs/store/sqlite/open.js +604 -0
- package/dist/cjs/store/sqlite/open.js.map +1 -0
- package/dist/cjs/store/sqlite/reconcile.d.cts +10 -0
- package/dist/cjs/store/sqlite/reconcile.d.ts +10 -0
- package/dist/cjs/store/sqlite/reconcile.js +1020 -0
- package/dist/cjs/store/sqlite/reconcile.js.map +1 -0
- package/dist/cjs/store/sqlite/sql-functions.js +34 -0
- package/dist/cjs/store/sqlite/sql-functions.js.map +1 -0
- package/dist/cjs/store/sqlite/store.d.cts +5 -0
- package/dist/cjs/store/sqlite/store.d.ts +5 -0
- package/dist/cjs/store/sqlite/store.js +588 -0
- package/dist/cjs/store/sqlite/store.js.map +1 -0
- package/dist/cjs/store/sqlite/vectors.d.cts +13 -0
- package/dist/cjs/store/sqlite/vectors.d.ts +13 -0
- package/dist/cjs/store/sqlite/vectors.js +470 -0
- package/dist/cjs/store/sqlite/vectors.js.map +1 -0
- package/dist/cjs/store/transaction.d.cts +5 -0
- package/dist/cjs/store/transaction.d.ts +5 -0
- package/dist/cjs/store/transaction.js +297 -0
- package/dist/cjs/store/transaction.js.map +1 -0
- package/dist/cjs/store/types.d.cts +91 -0
- package/dist/cjs/store/types.d.ts +91 -0
- package/dist/cjs/store/types.js +15 -0
- package/dist/cjs/store/types.js.map +1 -0
- package/dist/cjs/watch.d.cts +3 -0
- package/dist/cjs/watch.d.ts +3 -0
- package/dist/cjs/watch.js +215 -73
- package/dist/cjs/watch.js.map +1 -1
- package/dist/esm/cli/map.js +5 -5
- package/dist/esm/cli/map.js.map +1 -1
- package/dist/esm/cli/named.js +3 -3
- package/dist/esm/cli/named.js.map +1 -1
- package/dist/esm/cli/path.js +6 -5
- package/dist/esm/cli/path.js.map +1 -1
- package/dist/esm/cli/peek.js +5 -5
- package/dist/esm/cli/peek.js.map +1 -1
- package/dist/esm/cli/related.js +3 -3
- package/dist/esm/cli/related.js.map +1 -1
- package/dist/esm/cli/search.js +2 -2
- package/dist/esm/cli/search.js.map +1 -1
- package/dist/esm/cli/shared.d.ts +3 -3
- package/dist/esm/cli/shared.js +17 -17
- package/dist/esm/cli/shared.js.map +1 -1
- package/dist/esm/cli/sql.js +2 -2
- package/dist/esm/cli/sql.js.map +1 -1
- package/dist/esm/cli/status.js +25 -24
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/commands/map.d.ts +2 -2
- package/dist/esm/commands/map.js +51 -41
- package/dist/esm/commands/map.js.map +1 -1
- package/dist/esm/commands/peek.d.ts +2 -2
- package/dist/esm/commands/peek.js +58 -50
- package/dist/esm/commands/peek.js.map +1 -1
- package/dist/esm/commands/related.d.ts +2 -2
- package/dist/esm/commands/related.js +9 -9
- package/dist/esm/commands/related.js.map +1 -1
- package/dist/esm/commands/scope.d.ts +7 -7
- package/dist/esm/commands/scope.js +28 -17
- package/dist/esm/commands/scope.js.map +1 -1
- package/dist/esm/commands/search.d.ts +2 -2
- package/dist/esm/commands/search.js +42 -35
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/commands/signals.d.ts +4 -10
- package/dist/esm/commands/signals.js +17 -13
- package/dist/esm/commands/signals.js.map +1 -1
- package/dist/esm/commands/status.d.ts +2 -2
- package/dist/esm/commands/status.js +13 -7
- package/dist/esm/commands/status.js.map +1 -1
- package/dist/esm/config/access.d.ts +1 -0
- package/dist/esm/config/access.js +5 -0
- package/dist/esm/config/access.js.map +1 -1
- package/dist/esm/config/index.d.ts +1 -1
- package/dist/esm/config/index.js +1 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/config/types.d.ts +1 -0
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/config/validate.js +10 -1
- package/dist/esm/config/validate.js.map +1 -1
- package/dist/esm/embed/distribution.d.ts +4 -0
- package/dist/esm/embed/distribution.js +11 -0
- package/dist/esm/embed/distribution.js.map +1 -0
- package/dist/esm/embed/langfit.d.ts +2 -5
- package/dist/esm/embed/langfit.js +13 -13
- package/dist/esm/embed/langfit.js.map +1 -1
- package/dist/esm/embed/query.d.ts +6 -13
- package/dist/esm/embed/query.js +23 -84
- package/dist/esm/embed/query.js.map +1 -1
- package/dist/esm/embed/static.js +6 -1
- package/dist/esm/embed/static.js.map +1 -1
- package/dist/esm/embed/types.d.ts +1 -0
- package/dist/esm/embed/types.js +5 -3
- package/dist/esm/embed/types.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 +22 -10
- package/dist/esm/features/embed.js.map +1 -1
- package/dist/esm/features/index.d.ts +1 -1
- package/dist/esm/features/index.js +1 -1
- package/dist/esm/features/index.js.map +1 -1
- package/dist/esm/features/links.d.ts +7 -2
- package/dist/esm/features/links.js +124 -97
- package/dist/esm/features/links.js.map +1 -1
- package/dist/esm/features/rank.js +15 -8
- package/dist/esm/features/rank.js.map +1 -1
- package/dist/esm/features/sections.js +20 -7
- package/dist/esm/features/sections.js.map +1 -1
- package/dist/esm/features/tags.js +19 -18
- package/dist/esm/features/tags.js.map +1 -1
- package/dist/esm/features/types.d.ts +9 -5
- package/dist/esm/features/types.js +1 -1
- package/dist/esm/features/types.js.map +1 -1
- package/dist/esm/graph/traverse.d.ts +2 -2
- package/dist/esm/graph/traverse.js +26 -23
- package/dist/esm/graph/traverse.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/guarded-tick.d.ts +1 -0
- package/dist/esm/lib/guarded-tick.js +8 -0
- package/dist/esm/lib/guarded-tick.js.map +1 -0
- package/dist/esm/output/column-hint.d.ts +1 -2
- package/dist/esm/output/column-hint.js +3 -2
- package/dist/esm/output/column-hint.js.map +1 -1
- package/dist/esm/output/output.d.ts +2 -1
- package/dist/esm/output/output.js +27 -23
- package/dist/esm/output/output.js.map +1 -1
- package/dist/esm/scan/frontmatter.js +1 -1
- package/dist/esm/scan/frontmatter.js.map +1 -1
- package/dist/esm/scan/index.js +1 -1
- package/dist/esm/scan/index.js.map +1 -1
- package/dist/esm/scan/reparse.d.ts +10 -0
- package/dist/esm/scan/reparse.js +30 -0
- package/dist/esm/scan/reparse.js.map +1 -0
- package/dist/esm/store/duckdb/batch.d.ts +16 -0
- package/dist/esm/store/duckdb/batch.js +96 -0
- package/dist/esm/store/duckdb/batch.js.map +1 -0
- package/dist/esm/store/duckdb/connection.d.ts +5 -0
- package/dist/esm/store/duckdb/connection.js +97 -0
- package/dist/esm/store/duckdb/connection.js.map +1 -0
- package/dist/esm/store/duckdb/lexical.d.ts +10 -0
- package/dist/esm/store/duckdb/lexical.js +163 -0
- package/dist/esm/store/duckdb/lexical.js.map +1 -0
- package/dist/esm/store/duckdb/native.d.ts +3 -0
- package/dist/esm/store/duckdb/native.js +96 -0
- package/dist/esm/store/duckdb/native.js.map +1 -0
- package/dist/esm/store/duckdb/open.d.ts +12 -0
- package/dist/esm/store/duckdb/open.js +127 -0
- package/dist/esm/store/duckdb/open.js.map +1 -0
- package/dist/esm/store/duckdb/reconcile.d.ts +6 -0
- package/dist/esm/store/duckdb/reconcile.js +141 -0
- package/dist/esm/store/duckdb/reconcile.js.map +1 -0
- package/dist/esm/store/duckdb/sql-functions.d.ts +2 -0
- package/dist/esm/store/duckdb/sql-functions.js +56 -0
- package/dist/esm/store/duckdb/sql-functions.js.map +1 -0
- package/dist/esm/store/duckdb/store.d.ts +5 -0
- package/dist/esm/store/duckdb/store.js +104 -0
- package/dist/esm/store/duckdb/store.js.map +1 -0
- package/dist/esm/store/duckdb/vectors.d.ts +9 -0
- package/dist/esm/store/duckdb/vectors.js +171 -0
- package/dist/esm/store/duckdb/vectors.js.map +1 -0
- package/dist/esm/store/index.d.ts +7 -0
- package/dist/esm/store/index.js +35 -0
- package/dist/esm/store/index.js.map +1 -0
- package/dist/esm/store/meta.d.ts +3 -0
- package/dist/esm/store/meta.js +17 -0
- package/dist/esm/store/meta.js.map +1 -0
- package/dist/esm/store/shared.d.ts +5 -0
- package/dist/esm/store/shared.js +24 -0
- package/dist/esm/store/shared.js.map +1 -0
- package/dist/esm/store/sql-functions.d.ts +2 -0
- package/dist/esm/store/sql-functions.js +29 -0
- package/dist/esm/store/sql-functions.js.map +1 -0
- package/dist/esm/store/sqlite/connection.d.ts +3 -0
- package/dist/esm/store/sqlite/connection.js +50 -0
- package/dist/esm/store/sqlite/connection.js.map +1 -0
- package/dist/esm/store/sqlite/lexical.d.ts +2 -0
- package/dist/esm/store/sqlite/lexical.js +21 -0
- package/dist/esm/store/sqlite/lexical.js.map +1 -0
- package/dist/esm/{db → store/sqlite}/open.d.ts +5 -5
- package/dist/esm/store/sqlite/open.js +193 -0
- package/dist/esm/store/sqlite/open.js.map +1 -0
- package/dist/esm/store/sqlite/reconcile.d.ts +10 -0
- package/dist/esm/{db → store/sqlite}/reconcile.js +101 -99
- package/dist/esm/store/sqlite/reconcile.js.map +1 -0
- package/dist/esm/store/sqlite/sql-functions.js +23 -0
- package/dist/esm/store/sqlite/sql-functions.js.map +1 -0
- package/dist/esm/store/sqlite/store.d.ts +5 -0
- package/dist/esm/store/sqlite/store.js +82 -0
- package/dist/esm/store/sqlite/store.js.map +1 -0
- package/dist/esm/store/sqlite/vectors.d.ts +13 -0
- package/dist/esm/store/sqlite/vectors.js +90 -0
- package/dist/esm/store/sqlite/vectors.js.map +1 -0
- package/dist/esm/store/transaction.d.ts +5 -0
- package/dist/esm/store/transaction.js +57 -0
- package/dist/esm/store/transaction.js.map +1 -0
- package/dist/esm/store/types.d.ts +91 -0
- package/dist/esm/store/types.js +11 -0
- package/dist/esm/store/types.js.map +1 -0
- package/dist/esm/watch.d.ts +3 -0
- package/dist/esm/watch.js +39 -22
- package/dist/esm/watch.js.map +1 -1
- package/package.json +3 -1
- package/schema.json +5 -0
- package/skills/sense/EXAMPLES.md +3 -3
- package/skills/sense/SKILL.md +16 -14
- package/skills/sense-bases/EXAMPLES.md +2 -2
- package/skills/sense-bases/SKILL.md +6 -6
- package/skills/sense-setup/EXAMPLES.md +1 -1
- package/skills/sense-setup/SKILL.md +5 -4
- package/dist/cjs/db/index.d.cts +0 -4
- package/dist/cjs/db/index.d.ts +0 -4
- package/dist/cjs/db/index.js +0 -42
- package/dist/cjs/db/index.js.map +0 -1
- package/dist/cjs/db/open.js +0 -230
- package/dist/cjs/db/open.js.map +0 -1
- package/dist/cjs/db/reconcile.d.cts +0 -10
- package/dist/cjs/db/reconcile.d.ts +0 -10
- package/dist/cjs/db/reconcile.js +0 -579
- package/dist/cjs/db/reconcile.js.map +0 -1
- package/dist/cjs/db/shared.d.cts +0 -5
- package/dist/cjs/db/shared.d.ts +0 -5
- package/dist/cjs/db/shared.js +0 -45
- package/dist/cjs/db/shared.js.map +0 -1
- package/dist/cjs/db/sql-functions.js +0 -64
- package/dist/cjs/db/sql-functions.js.map +0 -1
- package/dist/esm/db/index.d.ts +0 -4
- package/dist/esm/db/index.js +0 -5
- package/dist/esm/db/index.js.map +0 -1
- package/dist/esm/db/open.js +0 -165
- package/dist/esm/db/open.js.map +0 -1
- package/dist/esm/db/reconcile.d.ts +0 -10
- package/dist/esm/db/reconcile.js.map +0 -1
- package/dist/esm/db/shared.d.ts +0 -5
- package/dist/esm/db/shared.js +0 -20
- package/dist/esm/db/shared.js.map +0 -1
- package/dist/esm/db/sql-functions.js +0 -47
- package/dist/esm/db/sql-functions.js.map +0 -1
- /package/dist/cjs/{db → store/sqlite}/sql-functions.d.cts +0 -0
- /package/dist/cjs/{db → store/sqlite}/sql-functions.d.ts +0 -0
- /package/dist/esm/{db → store/sqlite}/sql-functions.d.ts +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// D1: fts BM25 for ranking, contains() scans for exact substring / phrase verification /
|
|
2
|
+
// unspaced scripts, JS excerpts for every doc (no snippet()).
|
|
3
|
+
import { SenseError } from '../../errors.js';
|
|
4
|
+
import { UNSPACED_SCRIPTS } from '../../text/segment.js';
|
|
5
|
+
import { withTransaction } from '../transaction.js';
|
|
6
|
+
const FIELDS = [
|
|
7
|
+
'title',
|
|
8
|
+
'summary',
|
|
9
|
+
'text'
|
|
10
|
+
];
|
|
11
|
+
// Mirrors sqlite's bm25(content, 10.0, 5.0, 1.0, ...) column-weight intent; DuckDB's
|
|
12
|
+
// match_bm25 has no field_weights argument (verified 1.5.5), so each field is scored
|
|
13
|
+
// separately and combined here.
|
|
14
|
+
const FIELD_WEIGHT = {
|
|
15
|
+
title: 10,
|
|
16
|
+
summary: 5,
|
|
17
|
+
text: 1
|
|
18
|
+
};
|
|
19
|
+
const UNSPACED_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');
|
|
20
|
+
// FTS5 operator syntax (sqlite.org/fts5.html sec. 3) that words/substrings below would
|
|
21
|
+
// otherwise silently treat as literal terms instead of honoring (PRINCIPLES.md #6). Checked
|
|
22
|
+
// against terms with quoted spans blanked -- those go through contains() and are supported.
|
|
23
|
+
const FTS5_OPERATORS = [
|
|
24
|
+
{
|
|
25
|
+
label: 'prefix query',
|
|
26
|
+
re: /[\p{L}\p{N}_]+\*(?=\s|$)/u
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'boolean operator',
|
|
30
|
+
re: /(?:^|\s)(?:AND|OR|NOT)(?=\s|$)/
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: 'NEAR operator',
|
|
34
|
+
re: /(?:^|\s)NEAR\b/
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'initial-token operator',
|
|
38
|
+
re: /(?:^|\s)\^\S+/
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: 'column filter',
|
|
42
|
+
re: /(?:^|\s)[\p{L}_]\w*\s*:/u
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
function unsupportedOperator(terms) {
|
|
46
|
+
const withoutPhrases = terms.replace(/"[^"]*"/g, ' ');
|
|
47
|
+
for (const { label, re } of FTS5_OPERATORS){
|
|
48
|
+
const m = withoutPhrases.match(re);
|
|
49
|
+
if (m) return {
|
|
50
|
+
label,
|
|
51
|
+
token: m[0].trim()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// A run whose script marks no word boundaries makes match_bm25's whitespace tokenizer index
|
|
57
|
+
// the whole run as one token (same gap FTS5 has without the `_seg` sidecar), so such runs --
|
|
58
|
+
// and any author-quoted phrase, any script -- go through contains() instead of the fts index.
|
|
59
|
+
function splitTerms(terms) {
|
|
60
|
+
const words = [];
|
|
61
|
+
const substrings = [];
|
|
62
|
+
const withoutPhrases = terms.replace(/"([^"]*)"/g, (_m, inner)=>{
|
|
63
|
+
const phrase = inner.trim();
|
|
64
|
+
if (phrase.length === 0) return ' ';
|
|
65
|
+
substrings.push(phrase);
|
|
66
|
+
if (!UNSPACED_RUN.test(phrase)) {
|
|
67
|
+
for (const w of phrase.split(/\s+/))if (w.length > 0) words.push(w);
|
|
68
|
+
}
|
|
69
|
+
return ' ';
|
|
70
|
+
});
|
|
71
|
+
for (const tok of withoutPhrases.split(/\s+/)){
|
|
72
|
+
if (tok.length === 0) continue;
|
|
73
|
+
if (UNSPACED_RUN.test(tok)) substrings.push(tok);
|
|
74
|
+
else words.push(tok);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
words,
|
|
78
|
+
substrings
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// No incremental update (verified 1.5.5: PRAGMA create_fts_index is rebuild-only), so this
|
|
82
|
+
// pays the full rebuild -- but only once per store instance's first lexical query, and only
|
|
83
|
+
// when content actually changed since (see FtsIndexState / duckdb/store.ts's markStale()).
|
|
84
|
+
async function ensureFtsFresh(conn, state) {
|
|
85
|
+
if (!state.stale) return;
|
|
86
|
+
await conn.exec('INSTALL fts; LOAD fts;');
|
|
87
|
+
await withTransaction(conn, async ()=>{
|
|
88
|
+
// stopwords='none': sqlite's porter/unicode61 tokenizer never removes stopwords either
|
|
89
|
+
// (verified 1.5.5), and the fts extension's default 571-word English list would otherwise
|
|
90
|
+
// silently drop common query words (e.g. "and") from the index but not from match_bm25's
|
|
91
|
+
// conjunctive gate, making a bare multi-word query that contains one match nothing.
|
|
92
|
+
await conn.exec(`PRAGMA create_fts_index('content', 'path', 'title', 'summary', 'text', stopwords='none', overwrite=1)`);
|
|
93
|
+
});
|
|
94
|
+
state.stale = false;
|
|
95
|
+
}
|
|
96
|
+
// Per-word-term and per-substring-term SQL fragments, field-weighted the same way for both
|
|
97
|
+
// (title 10 / summary 5 / text 1), plus the params in the exact left-to-right order they are
|
|
98
|
+
// emitted -- positional `?` binding requires that order to match the assembled SQL text.
|
|
99
|
+
function buildScoreAndGate(words, substrings) {
|
|
100
|
+
const scoreParts = [];
|
|
101
|
+
const gateParts = [];
|
|
102
|
+
const params = [];
|
|
103
|
+
if (words.length > 0) {
|
|
104
|
+
const wordQuery = words.join(' ');
|
|
105
|
+
for (const field of FIELDS){
|
|
106
|
+
scoreParts.push(`${FIELD_WEIGHT[field]}.0 * COALESCE(fts_main_content.match_bm25(content."path", ?, fields := '${field}', conjunctive := false), 0)`);
|
|
107
|
+
params.push(wordQuery);
|
|
108
|
+
}
|
|
109
|
+
gateParts.push(`fts_main_content.match_bm25(content."path", ?, conjunctive := true) IS NOT NULL`);
|
|
110
|
+
params.push(wordQuery);
|
|
111
|
+
}
|
|
112
|
+
for (const raw of substrings){
|
|
113
|
+
const needle = raw.toLowerCase();
|
|
114
|
+
for (const field of FIELDS){
|
|
115
|
+
scoreParts.push(`${FIELD_WEIGHT[field]}.0 * CAST(contains(lower(content.${field}), ?) AS INTEGER)`);
|
|
116
|
+
params.push(needle);
|
|
117
|
+
}
|
|
118
|
+
gateParts.push(`contains(lower(content.title) || ' ' || lower(content.summary) || ' ' || lower(content.text), ?)`);
|
|
119
|
+
params.push(needle);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
scoreSql: scoreParts.join(' + '),
|
|
123
|
+
gateSql: gateParts.join(' AND '),
|
|
124
|
+
params
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Ranked word-match query, scoped by the caller-built SQL fragments (same fragments sqlite's
|
|
128
|
+
// queryLexical takes). `hit` is always NULL: no snippet() equivalent exists, so every row goes
|
|
129
|
+
// through the caller's JS excerpt fallback (commands/search.ts) rather than a second excerpt path.
|
|
130
|
+
export async function queryLexical(conn, terms, opts, state) {
|
|
131
|
+
const unsupported = unsupportedOperator(terms);
|
|
132
|
+
if (unsupported !== null) {
|
|
133
|
+
throw new SenseError('STORE_CAPABILITY_MISSING', `store "duckdb" does not implement FTS5's ${unsupported.label} ("${unsupported.token}") in this build; rephrase "${terms.trim()}" without it, or set "store" to "sqlite" in this tree's config to search it as written`);
|
|
134
|
+
}
|
|
135
|
+
const { whereJoin, whereCond, scopeCond, limit } = opts;
|
|
136
|
+
const { words, substrings } = splitTerms(terms);
|
|
137
|
+
if (words.length === 0 && substrings.length === 0) return [];
|
|
138
|
+
if (words.length > 0) await ensureFtsFresh(conn, state);
|
|
139
|
+
const { scoreSql, gateSql, params } = buildScoreAndGate(words, substrings);
|
|
140
|
+
const sql = `SELECT path, NULL AS hit FROM (
|
|
141
|
+
SELECT content."path" AS path, (${scoreSql}) AS score
|
|
142
|
+
FROM content
|
|
143
|
+
${whereJoin}
|
|
144
|
+
WHERE ${gateSql}
|
|
145
|
+
${whereCond} ${scopeCond}
|
|
146
|
+
) sq ORDER BY score DESC, path LIMIT ?`;
|
|
147
|
+
const stmt = await conn.prepare(sql);
|
|
148
|
+
return await stmt.all(...params, limit);
|
|
149
|
+
}
|
|
150
|
+
// One instance per store: `stale` starts true (a fresh connection cannot know whether an
|
|
151
|
+
// on-disk fts schema still matches `content`), and store.ts's reconcile wrapper flips it back
|
|
152
|
+
// to true whenever content changes, so the next query rebuilds before ranking against it.
|
|
153
|
+
export function createLexicalIndex(conn) {
|
|
154
|
+
const state = {
|
|
155
|
+
stale: true
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
query: (terms, opts)=>queryLexical(conn, terms, opts, state),
|
|
159
|
+
markStale: ()=>{
|
|
160
|
+
state.stale = true;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/lexical.ts"],"sourcesContent":["// D1: fts BM25 for ranking, contains() scans for exact substring / phrase verification /\n// unspaced scripts, JS excerpts for every doc (no snippet()).\nimport { SenseError } from '../../errors.ts';\nimport { UNSPACED_SCRIPTS } from '../../text/segment.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\nconst FIELDS = ['title', 'summary', 'text'] as const;\ntype Field = (typeof FIELDS)[number];\n// Mirrors sqlite's bm25(content, 10.0, 5.0, 1.0, ...) column-weight intent; DuckDB's\n// match_bm25 has no field_weights argument (verified 1.5.5), so each field is scored\n// separately and combined here.\nconst FIELD_WEIGHT: Record<Field, number> = { title: 10, summary: 5, text: 1 };\n\nconst UNSPACED_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');\n\n// FTS5 operator syntax (sqlite.org/fts5.html sec. 3) that words/substrings below would\n// otherwise silently treat as literal terms instead of honoring (PRINCIPLES.md #6). Checked\n// against terms with quoted spans blanked -- those go through contains() and are supported.\nconst FTS5_OPERATORS: Array<{ label: string; re: RegExp }> = [\n { label: 'prefix query', re: /[\\p{L}\\p{N}_]+\\*(?=\\s|$)/u },\n { label: 'boolean operator', re: /(?:^|\\s)(?:AND|OR|NOT)(?=\\s|$)/ },\n { label: 'NEAR operator', re: /(?:^|\\s)NEAR\\b/ },\n { label: 'initial-token operator', re: /(?:^|\\s)\\^\\S+/ },\n { label: 'column filter', re: /(?:^|\\s)[\\p{L}_]\\w*\\s*:/u },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, ' ');\n for (const { label, re } of FTS5_OPERATORS) {\n const m = withoutPhrases.match(re);\n if (m) return { label, token: m[0].trim() };\n }\n return null;\n}\n\ninterface FtsIndexState {\n stale: boolean;\n}\n\n// A run whose script marks no word boundaries makes match_bm25's whitespace tokenizer index\n// the whole run as one token (same gap FTS5 has without the `_seg` sidecar), so such runs --\n// and any author-quoted phrase, any script -- go through contains() instead of the fts index.\nfunction splitTerms(terms: string): { words: string[]; substrings: string[] } {\n const words: string[] = [];\n const substrings: string[] = [];\n const withoutPhrases = terms.replace(/\"([^\"]*)\"/g, (_m, inner: string) => {\n const phrase = inner.trim();\n if (phrase.length === 0) return ' ';\n substrings.push(phrase);\n if (!UNSPACED_RUN.test(phrase)) for (const w of phrase.split(/\\s+/)) if (w.length > 0) words.push(w);\n return ' ';\n });\n for (const tok of withoutPhrases.split(/\\s+/)) {\n if (tok.length === 0) continue;\n if (UNSPACED_RUN.test(tok)) substrings.push(tok);\n else words.push(tok);\n }\n return { words, substrings };\n}\n\n// No incremental update (verified 1.5.5: PRAGMA create_fts_index is rebuild-only), so this\n// pays the full rebuild -- but only once per store instance's first lexical query, and only\n// when content actually changed since (see FtsIndexState / duckdb/store.ts's markStale()).\nasync function ensureFtsFresh(conn: Connection, state: FtsIndexState): Promise<void> {\n if (!state.stale) return;\n await conn.exec('INSTALL fts; LOAD fts;');\n await withTransaction(conn, async () => {\n // stopwords='none': sqlite's porter/unicode61 tokenizer never removes stopwords either\n // (verified 1.5.5), and the fts extension's default 571-word English list would otherwise\n // silently drop common query words (e.g. \"and\") from the index but not from match_bm25's\n // conjunctive gate, making a bare multi-word query that contains one match nothing.\n await conn.exec(`PRAGMA create_fts_index('content', 'path', 'title', 'summary', 'text', stopwords='none', overwrite=1)`);\n });\n state.stale = false;\n}\n\n// Per-word-term and per-substring-term SQL fragments, field-weighted the same way for both\n// (title 10 / summary 5 / text 1), plus the params in the exact left-to-right order they are\n// emitted -- positional `?` binding requires that order to match the assembled SQL text.\nfunction buildScoreAndGate(words: string[], substrings: string[]): { scoreSql: string; gateSql: string; params: unknown[] } {\n const scoreParts: string[] = [];\n const gateParts: string[] = [];\n const params: unknown[] = [];\n\n if (words.length > 0) {\n const wordQuery = words.join(' ');\n for (const field of FIELDS) {\n scoreParts.push(`${FIELD_WEIGHT[field]}.0 * COALESCE(fts_main_content.match_bm25(content.\"path\", ?, fields := '${field}', conjunctive := false), 0)`);\n params.push(wordQuery);\n }\n gateParts.push(`fts_main_content.match_bm25(content.\"path\", ?, conjunctive := true) IS NOT NULL`);\n params.push(wordQuery);\n }\n\n for (const raw of substrings) {\n const needle = raw.toLowerCase();\n for (const field of FIELDS) {\n scoreParts.push(`${FIELD_WEIGHT[field]}.0 * CAST(contains(lower(content.${field}), ?) AS INTEGER)`);\n params.push(needle);\n }\n gateParts.push(`contains(lower(content.title) || ' ' || lower(content.summary) || ' ' || lower(content.text), ?)`);\n params.push(needle);\n }\n\n return { scoreSql: scoreParts.join(' + '), gateSql: gateParts.join(' AND '), params };\n}\n\n// Ranked word-match query, scoped by the caller-built SQL fragments (same fragments sqlite's\n// queryLexical takes). `hit` is always NULL: no snippet() equivalent exists, so every row goes\n// through the caller's JS excerpt fallback (commands/search.ts) rather than a second excerpt path.\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions, state: FtsIndexState): Promise<LexicalHit[]> {\n const unsupported = unsupportedOperator(terms);\n if (unsupported !== null) {\n throw new SenseError('STORE_CAPABILITY_MISSING', `store \"duckdb\" does not implement FTS5's ${unsupported.label} (\"${unsupported.token}\") in this build; rephrase \"${terms.trim()}\" without it, or set \"store\" to \"sqlite\" in this tree's config to search it as written`);\n }\n const { whereJoin, whereCond, scopeCond, limit } = opts;\n const { words, substrings } = splitTerms(terms);\n if (words.length === 0 && substrings.length === 0) return [];\n\n if (words.length > 0) await ensureFtsFresh(conn, state);\n\n const { scoreSql, gateSql, params } = buildScoreAndGate(words, substrings);\n const sql = `SELECT path, NULL AS hit FROM (\n SELECT content.\"path\" AS path, (${scoreSql}) AS score\n FROM content\n ${whereJoin}\n WHERE ${gateSql}\n ${whereCond} ${scopeCond}\n ) sq ORDER BY score DESC, path LIMIT ?`;\n const stmt = await conn.prepare(sql);\n return (await stmt.all(...params, limit)) as unknown as LexicalHit[];\n}\n\n// One instance per store: `stale` starts true (a fresh connection cannot know whether an\n// on-disk fts schema still matches `content`), and store.ts's reconcile wrapper flips it back\n// to true whenever content changes, so the next query rebuilds before ranking against it.\nexport function createLexicalIndex(conn: Connection): { query: (terms: string, opts: LexicalQueryOptions) => Promise<LexicalHit[]>; markStale: () => void } {\n const state: FtsIndexState = { stale: true };\n return {\n query: (terms, opts) => queryLexical(conn, terms, opts, state),\n markStale: () => {\n state.stale = true;\n },\n };\n}\n"],"names":["SenseError","UNSPACED_SCRIPTS","withTransaction","FIELDS","FIELD_WEIGHT","title","summary","text","UNSPACED_RUN","RegExp","FTS5_OPERATORS","label","re","unsupportedOperator","terms","withoutPhrases","replace","m","match","token","trim","splitTerms","words","substrings","_m","inner","phrase","length","push","test","w","split","tok","ensureFtsFresh","conn","state","stale","exec","buildScoreAndGate","scoreParts","gateParts","params","wordQuery","join","field","raw","needle","toLowerCase","scoreSql","gateSql","queryLexical","opts","unsupported","whereJoin","whereCond","scopeCond","limit","sql","stmt","prepare","all","createLexicalIndex","query","markStale"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import Module from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { SenseError } from '../../errors.js';
|
|
6
|
+
export const DUCKDB_PACKAGE = '@duckdb/node-api';
|
|
7
|
+
// install-module-linked itself is small, but its target (@duckdb/node-api) is not: deferred the
|
|
8
|
+
// same way embed/static.ts defers @huggingface/tokenizers, so requiring it stays inside the
|
|
9
|
+
// already-gated branch below rather than running for every store/index.ts load.
|
|
10
|
+
const _require = typeof require === 'undefined' ? Module.createRequire(import.meta.url) : require;
|
|
11
|
+
const _dirname = dirname(typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url));
|
|
12
|
+
// Walks up from this file's own directory to find the real package.json, rather than a fixed
|
|
13
|
+
// dot-count from __dirname (xz-compat's `../../node_modules`): `tsds test:node` runs straight
|
|
14
|
+
// off src/, one directory shallower than dist/{cjs,esm}/store/duckdb sits under the built
|
|
15
|
+
// package, so a hardcoded depth is right for one and wrong for the other. Works the same way
|
|
16
|
+
// for a globally-installed `sense`, whose node_modules this resolves to -- never the caller's
|
|
17
|
+
// cwd.
|
|
18
|
+
function packageNodeModules() {
|
|
19
|
+
let dir = _dirname;
|
|
20
|
+
for(;;){
|
|
21
|
+
const candidate = join(dir, 'package.json');
|
|
22
|
+
if (existsSync(candidate)) {
|
|
23
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
24
|
+
if (pkg.name === 'sensemaking') return join(dir, 'node_modules');
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir) throw new Error('could not locate the sensemaking package root to install @duckdb/node-api into');
|
|
28
|
+
dir = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const installing = new Map();
|
|
32
|
+
// De-dupes concurrent installs within this process (see spawn-term's loadInk for the shape).
|
|
33
|
+
// The install here is async -- the load site already awaits a dynamic import -- so a bare
|
|
34
|
+
// attempted flag (xz-compat's `installationAttempted`, enough for its synchronous single-target
|
|
35
|
+
// install) isn't enough on its own; concurrent callers instead await the same in-flight promise,
|
|
36
|
+
// keyed by specifier and target so tests exercising several installs in one process don't share
|
|
37
|
+
// one slot. A failed install is not cached, so a later call retries.
|
|
38
|
+
function installOnce(specifier, nodeModulesPath) {
|
|
39
|
+
const key = `${specifier}\n${nodeModulesPath}`;
|
|
40
|
+
let promise = installing.get(key);
|
|
41
|
+
if (!promise) {
|
|
42
|
+
const installModuleLinked = _require('install-module-linked').default;
|
|
43
|
+
promise = installModuleLinked(specifier, nodeModulesPath).then(()=>{}, (err)=>{
|
|
44
|
+
installing.delete(key);
|
|
45
|
+
throw err;
|
|
46
|
+
});
|
|
47
|
+
installing.set(key, promise);
|
|
48
|
+
}
|
|
49
|
+
return promise;
|
|
50
|
+
}
|
|
51
|
+
// Node caches a bare specifier's package resolution -- including a "not found" miss -- for the
|
|
52
|
+
// life of the process, so retrying `import(importName)` right after installing sees the same
|
|
53
|
+
// stale miss and falls through to a bogus index.js guess instead of the package's real entry
|
|
54
|
+
// (verified against a real global install: the plain-specifier retry failed there, this did
|
|
55
|
+
// not). Reading the freshly-installed package's own package.json and importing its entry file
|
|
56
|
+
// by an absolute file: URL sidesteps that stale cache instead of re-resolving the bare name.
|
|
57
|
+
function resolveEntryUrl(importName, nodeModulesPath) {
|
|
58
|
+
var _pkg_main;
|
|
59
|
+
const pkgDir = join(nodeModulesPath, ...importName.split('/'));
|
|
60
|
+
const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
61
|
+
const entry = typeof pkg.exports === 'string' ? pkg.exports : (_pkg_main = pkg.main) !== null && _pkg_main !== void 0 ? _pkg_main : 'index.js';
|
|
62
|
+
return pathToFileURL(join(pkgDir, entry)).href;
|
|
63
|
+
}
|
|
64
|
+
// Import-then-install-then-retry, over an injectable target so the failure path (import AND
|
|
65
|
+
// install both fail) can be exercised for real in tests without downloading @duckdb/node-api's
|
|
66
|
+
// real ~110MB native binding. Production always passes @duckdb/node-api's own name; the thrown
|
|
67
|
+
// error names @duckdb/node-api regardless, since that is what a real user needs to install.
|
|
68
|
+
export async function loadOrInstall(importName, nodeModulesPath) {
|
|
69
|
+
try {
|
|
70
|
+
return await import(importName);
|
|
71
|
+
} catch {
|
|
72
|
+
console.error(`sense: store "duckdb" needs ${DUCKDB_PACKAGE}; installing it now (one-time download, ~110MB)...`);
|
|
73
|
+
try {
|
|
74
|
+
await installOnce(importName, nodeModulesPath);
|
|
75
|
+
} catch (installErr) {
|
|
76
|
+
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "duckdb" needs ${DUCKDB_PACKAGE}, and installing it automatically failed (${installErr.message}); this can happen offline, in a sandboxed environment, or when node_modules is read-only or owned by another user (e.g. under a global install) -- run \`npm install ${DUCKDB_PACKAGE}\` yourself and try again`);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await import(resolveEntryUrl(importName, nodeModulesPath));
|
|
80
|
+
} catch (loadErr) {
|
|
81
|
+
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "duckdb" installed ${DUCKDB_PACKAGE} but it could not be loaded (${loadErr.message}); it may not be available for this platform (${process.platform}-${process.arch}) -- run \`npm install ${DUCKDB_PACKAGE}\` to see the underlying error`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let duckdbApiPromise;
|
|
86
|
+
// The one accessor every consumer of @duckdb/node-api (open.ts, sql-functions.ts, vectors.ts)
|
|
87
|
+
// must go through, rather than each doing its own `import('@duckdb/node-api')`: once a fresh
|
|
88
|
+
// install has happened in this process, Node keeps returning that pre-install "not found" miss
|
|
89
|
+
// to any NEW bare-specifier lookup of the same path, so a second independent import elsewhere
|
|
90
|
+
// in the process fails even though the package is now genuinely on disk (the same stale-cache
|
|
91
|
+
// problem loadOrInstall works around for its own retry). Caching the resolved module here means
|
|
92
|
+
// only the first caller ever resolves the bare specifier at all.
|
|
93
|
+
export function duckdbApi() {
|
|
94
|
+
if (!duckdbApiPromise) duckdbApiPromise = loadOrInstall(DUCKDB_PACKAGE, packageNodeModules());
|
|
95
|
+
return duckdbApiPromise;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/native.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport Module from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { SenseError } from '../../errors.ts';\n\nexport const DUCKDB_PACKAGE = '@duckdb/node-api';\n\n// install-module-linked itself is small, but its target (@duckdb/node-api) is not: deferred the\n// same way embed/static.ts defers @huggingface/tokenizers, so requiring it stays inside the\n// already-gated branch below rather than running for every store/index.ts load.\nconst _require = typeof require === 'undefined' ? Module.createRequire(import.meta.url) : require;\nconst _dirname = dirname(typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url));\n\n// Walks up from this file's own directory to find the real package.json, rather than a fixed\n// dot-count from __dirname (xz-compat's `../../node_modules`): `tsds test:node` runs straight\n// off src/, one directory shallower than dist/{cjs,esm}/store/duckdb sits under the built\n// package, so a hardcoded depth is right for one and wrong for the other. Works the same way\n// for a globally-installed `sense`, whose node_modules this resolves to -- never the caller's\n// cwd.\nfunction packageNodeModules(): string {\n let dir = _dirname;\n for (;;) {\n const candidate = join(dir, 'package.json');\n if (existsSync(candidate)) {\n const pkg = JSON.parse(readFileSync(candidate, 'utf8')) as { name?: string };\n if (pkg.name === 'sensemaking') return join(dir, 'node_modules');\n }\n const parent = dirname(dir);\n if (parent === dir) throw new Error('could not locate the sensemaking package root to install @duckdb/node-api into');\n dir = parent;\n }\n}\n\nconst installing = new Map<string, Promise<void>>();\n\n// De-dupes concurrent installs within this process (see spawn-term's loadInk for the shape).\n// The install here is async -- the load site already awaits a dynamic import -- so a bare\n// attempted flag (xz-compat's `installationAttempted`, enough for its synchronous single-target\n// install) isn't enough on its own; concurrent callers instead await the same in-flight promise,\n// keyed by specifier and target so tests exercising several installs in one process don't share\n// one slot. A failed install is not cached, so a later call retries.\nfunction installOnce(specifier: string, nodeModulesPath: string): Promise<void> {\n const key = `${specifier}\\n${nodeModulesPath}`;\n let promise = installing.get(key);\n if (!promise) {\n const installModuleLinked = (_require('install-module-linked') as { default: (specifier: string, nodeModulesPath: string) => Promise<string> }).default;\n promise = installModuleLinked(specifier, nodeModulesPath).then(\n () => {},\n (err) => {\n installing.delete(key);\n throw err;\n }\n );\n installing.set(key, promise);\n }\n return promise;\n}\n\n// Node caches a bare specifier's package resolution -- including a \"not found\" miss -- for the\n// life of the process, so retrying `import(importName)` right after installing sees the same\n// stale miss and falls through to a bogus index.js guess instead of the package's real entry\n// (verified against a real global install: the plain-specifier retry failed there, this did\n// not). Reading the freshly-installed package's own package.json and importing its entry file\n// by an absolute file: URL sidesteps that stale cache instead of re-resolving the bare name.\nfunction resolveEntryUrl(importName: string, nodeModulesPath: string): string {\n const pkgDir = join(nodeModulesPath, ...importName.split('/'));\n const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) as { main?: string; exports?: string };\n const entry = typeof pkg.exports === 'string' ? pkg.exports : (pkg.main ?? 'index.js');\n return pathToFileURL(join(pkgDir, entry)).href;\n}\n\n// Import-then-install-then-retry, over an injectable target so the failure path (import AND\n// install both fail) can be exercised for real in tests without downloading @duckdb/node-api's\n// real ~110MB native binding. Production always passes @duckdb/node-api's own name; the thrown\n// error names @duckdb/node-api regardless, since that is what a real user needs to install.\nexport async function loadOrInstall<T>(importName: string, nodeModulesPath: string): Promise<T> {\n try {\n return (await import(importName)) as T;\n } catch {\n console.error(`sense: store \"duckdb\" needs ${DUCKDB_PACKAGE}; installing it now (one-time download, ~110MB)...`);\n try {\n await installOnce(importName, nodeModulesPath);\n } catch (installErr) {\n throw new SenseError(\n 'STORE_DEPENDENCY_MISSING',\n `store \"duckdb\" needs ${DUCKDB_PACKAGE}, and installing it automatically failed (${(installErr as Error).message}); this can happen offline, in a sandboxed environment, or when node_modules is read-only or owned by another user (e.g. under a global install) -- run \\`npm install ${DUCKDB_PACKAGE}\\` yourself and try again`\n );\n }\n try {\n return (await import(resolveEntryUrl(importName, nodeModulesPath))) as T;\n } catch (loadErr) {\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" installed ${DUCKDB_PACKAGE} but it could not be loaded (${(loadErr as Error).message}); it may not be available for this platform (${process.platform}-${process.arch}) -- run \\`npm install ${DUCKDB_PACKAGE}\\` to see the underlying error`);\n }\n }\n}\n\nlet duckdbApiPromise: Promise<typeof import('@duckdb/node-api')> | undefined;\n\n// The one accessor every consumer of @duckdb/node-api (open.ts, sql-functions.ts, vectors.ts)\n// must go through, rather than each doing its own `import('@duckdb/node-api')`: once a fresh\n// install has happened in this process, Node keeps returning that pre-install \"not found\" miss\n// to any NEW bare-specifier lookup of the same path, so a second independent import elsewhere\n// in the process fails even though the package is now genuinely on disk (the same stale-cache\n// problem loadOrInstall works around for its own retry). Caching the resolved module here means\n// only the first caller ever resolves the bare specifier at all.\nexport function duckdbApi(): Promise<typeof import('@duckdb/node-api')> {\n if (!duckdbApiPromise) duckdbApiPromise = loadOrInstall<typeof import('@duckdb/node-api')>(DUCKDB_PACKAGE, packageNodeModules());\n return duckdbApiPromise;\n}\n"],"names":["existsSync","readFileSync","Module","dirname","join","fileURLToPath","pathToFileURL","SenseError","DUCKDB_PACKAGE","_require","require","createRequire","url","_dirname","__filename","packageNodeModules","dir","candidate","pkg","JSON","parse","name","parent","Error","installing","Map","installOnce","specifier","nodeModulesPath","key","promise","get","installModuleLinked","default","then","err","delete","set","resolveEntryUrl","importName","pkgDir","split","entry","exports","main","href","loadOrInstall","console","error","installErr","message","loadErr","process","platform","arch","duckdbApiPromise","duckdbApi"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../../config/index.js';
|
|
2
|
+
import type { Store } from '../types.js';
|
|
3
|
+
export declare const DB_FILENAME = "cache.duckdb";
|
|
4
|
+
export declare const SCHEMA_VERSION = "2";
|
|
5
|
+
export interface OpenResult {
|
|
6
|
+
store: Store;
|
|
7
|
+
cfg: ResolvedConfig;
|
|
8
|
+
dbPath: string;
|
|
9
|
+
parsed: number;
|
|
10
|
+
warnings: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function openDuckdb(cfg: ResolvedConfig): Promise<OpenResult>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { featureSignature, STATE_DIR } from '../../config/index.js';
|
|
4
|
+
import { STORE_DIMS } from '../../embed/types.js';
|
|
5
|
+
import { SenseError } from '../../errors.js';
|
|
6
|
+
import { activeFeatures, FEATURES } from '../../features/index.js';
|
|
7
|
+
import { getMeta, setMeta } from '../shared.js';
|
|
8
|
+
import { clearCache } from '../sqlite/open.js';
|
|
9
|
+
import { createConnection } from './connection.js';
|
|
10
|
+
import { DUCKDB_PACKAGE, duckdbApi } from './native.js';
|
|
11
|
+
import { reconcile } from './reconcile.js';
|
|
12
|
+
import { registerFunctions } from './sql-functions.js';
|
|
13
|
+
import { createStore } from './store.js';
|
|
14
|
+
export const DB_FILENAME = 'cache.duckdb';
|
|
15
|
+
// Independent of sqlite's SCHEMA_VERSION -- the two stores' cache shapes evolve separately
|
|
16
|
+
// (VARIANT frontmatter columns instead of untyped ones), and the store name already joins the
|
|
17
|
+
// feature signature (see featureSignature), so a config's `store` key switching cleanly
|
|
18
|
+
// rebuilds rather than reusing the other engine's stale cache shape.
|
|
19
|
+
export const SCHEMA_VERSION = '2';
|
|
20
|
+
// `content` is a plain table (not FTS-virtual): D1 builds DuckDB's fts index over it lazily,
|
|
21
|
+
// only when a lexical query actually runs (see duckdb/lexical.ts), and reads it directly for
|
|
22
|
+
// contains() substring/phrase/unspaced-script verification either way. No tokenizer resolution
|
|
23
|
+
// -- this store always uses the fts extension's default (porter) stemmer, unlike sqlite's
|
|
24
|
+
// configurable `content.tokenize` (not read here; D1 does not extend that knob to DuckDB).
|
|
25
|
+
async function ensureSchema(conn, cfg) {
|
|
26
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" DOUBLE, "_ctime" DOUBLE, "_size" INTEGER, "_parse_error" TEXT)`);
|
|
27
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS content ("path" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT)`);
|
|
28
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
|
|
29
|
+
await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');
|
|
30
|
+
for (const feature of activeFeatures(cfg)){
|
|
31
|
+
// Native FLOAT[STORE_DIMS] instead of the embed feature's engine-neutral BLOB+scale DDL
|
|
32
|
+
// (see duckdb/vectors.ts). `scale` is kept, unused, so the feature's shared reconcile-time
|
|
33
|
+
// INSERT/DELETE (src/features/embed.ts) still names a column that exists on both stores.
|
|
34
|
+
if (feature.name === 'embed') {
|
|
35
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings ("path" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector FLOAT[${STORE_DIMS}], PRIMARY KEY ("path", chunk))`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
await feature.schema(conn);
|
|
39
|
+
}
|
|
40
|
+
if (await getMeta(conn, 'schema_version') === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);
|
|
41
|
+
if (await getMeta(conn, 'features') === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));
|
|
42
|
+
}
|
|
43
|
+
async function connect(cfg) {
|
|
44
|
+
const stateDir = join(cfg.baseDir, STATE_DIR);
|
|
45
|
+
mkdirSync(stateDir, {
|
|
46
|
+
recursive: true
|
|
47
|
+
});
|
|
48
|
+
const dbPath = join(stateDir, DB_FILENAME);
|
|
49
|
+
// Dynamic, not a top-level import: sqlite trees must never even attempt to resolve this
|
|
50
|
+
// optional peer dependency, so nothing in this store's module graph imports it as a value
|
|
51
|
+
// until a duckdb tree is actually opened (types-only imports elsewhere are erased and cost
|
|
52
|
+
// nothing either way). Installed on first use if missing, and shared with sql-functions.ts
|
|
53
|
+
// and vectors.ts (see native.ts's duckdbApi).
|
|
54
|
+
let DuckDBInstance;
|
|
55
|
+
try {
|
|
56
|
+
({ DuckDBInstance } = await duckdbApi());
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err instanceof SenseError) throw err;
|
|
59
|
+
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "duckdb" needs the ${DUCKDB_PACKAGE} package (${err.message})`);
|
|
60
|
+
}
|
|
61
|
+
let duckdb;
|
|
62
|
+
let instance;
|
|
63
|
+
try {
|
|
64
|
+
// On-disk files default to an older storage format for cross-version compatibility, which
|
|
65
|
+
// rejects VARIANT columns ("VARIANT columns are not supported in storage versions prior to
|
|
66
|
+
// v1.5.0"); this store's dynamic frontmatter columns need VARIANT (see reconcile.ts), so
|
|
67
|
+
// the format floor is pinned explicitly. :memory: databases are unaffected either way.
|
|
68
|
+
instance = await DuckDBInstance.create(dbPath, {
|
|
69
|
+
storage_compatibility_version: 'v1.5.0'
|
|
70
|
+
});
|
|
71
|
+
duckdb = await instance.connect();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// create() may have succeeded before connect() failed: close it, or its WAL stays open.
|
|
74
|
+
instance === null || instance === void 0 ? void 0 : instance.closeSync();
|
|
75
|
+
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "duckdb" failed to open ${dbPath}: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
// A throw below (schema, reconcile's COLUMN_LIMIT) would leak the open instance, whose WAL
|
|
78
|
+
// then locks the .duckdb file undeletable on Windows. closed marks the rebuild branch.
|
|
79
|
+
let closed = false;
|
|
80
|
+
try {
|
|
81
|
+
await registerFunctions(duckdb);
|
|
82
|
+
const conn = createConnection(duckdb);
|
|
83
|
+
await conn.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
|
|
84
|
+
const version = await getMeta(conn, 'schema_version');
|
|
85
|
+
const features = await getMeta(conn, 'features');
|
|
86
|
+
const wantFeatures = featureSignature(cfg, FEATURES);
|
|
87
|
+
if (version !== null && version !== SCHEMA_VERSION || features !== null && features !== wantFeatures) {
|
|
88
|
+
// Reconcile only reparses changed files, so a stale cache can't be patched incrementally --
|
|
89
|
+
// rebuild instead, same as sqlite's open() (see its comment for why this is announced).
|
|
90
|
+
const reason = version !== null && version !== SCHEMA_VERSION ? 'cache format changed (new sensemaking version)' : 'config change (features, tokenizer, or presets)';
|
|
91
|
+
console.error(`sense: ${reason}; rebuilding the index`);
|
|
92
|
+
// Close before clearCache deletes the files underneath the still-open instance.
|
|
93
|
+
closed = true;
|
|
94
|
+
duckdb.disconnectSync();
|
|
95
|
+
instance.closeSync();
|
|
96
|
+
clearCache(cfg);
|
|
97
|
+
return connect(cfg);
|
|
98
|
+
}
|
|
99
|
+
await ensureSchema(conn, cfg);
|
|
100
|
+
const { parsed, warnings } = await reconcile(conn, cfg, cfg.baseDir);
|
|
101
|
+
return {
|
|
102
|
+
instance,
|
|
103
|
+
duckdb,
|
|
104
|
+
conn,
|
|
105
|
+
cfg,
|
|
106
|
+
dbPath,
|
|
107
|
+
parsed,
|
|
108
|
+
warnings
|
|
109
|
+
};
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (!closed) {
|
|
112
|
+
duckdb.disconnectSync();
|
|
113
|
+
instance === null || instance === void 0 ? void 0 : instance.closeSync();
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function openDuckdb(cfg) {
|
|
119
|
+
const { instance, duckdb, conn, cfg: resolvedCfg, dbPath, parsed, warnings } = await connect(cfg);
|
|
120
|
+
return {
|
|
121
|
+
store: createStore(instance, duckdb, conn, resolvedCfg, resolvedCfg.baseDir),
|
|
122
|
+
cfg: resolvedCfg,
|
|
123
|
+
dbPath,
|
|
124
|
+
parsed,
|
|
125
|
+
warnings
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/open.ts"],"sourcesContent":["import { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { DuckDBConnection, DuckDBInstance } from '@duckdb/node-api';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature, STATE_DIR } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport { clearCache } from '../sqlite/open.ts';\nimport type { Connection, Store } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { DUCKDB_PACKAGE, duckdbApi } from './native.ts';\nimport { reconcile } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.duckdb';\n// Independent of sqlite's SCHEMA_VERSION -- the two stores' cache shapes evolve separately\n// (VARIANT frontmatter columns instead of untyped ones), and the store name already joins the\n// feature signature (see featureSignature), so a config's `store` key switching cleanly\n// rebuilds rather than reusing the other engine's stale cache shape.\nexport const SCHEMA_VERSION = '2';\n\nexport interface OpenResult {\n store: Store;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\n// `content` is a plain table (not FTS-virtual): D1 builds DuckDB's fts index over it lazily,\n// only when a lexical query actually runs (see duckdb/lexical.ts), and reads it directly for\n// contains() substring/phrase/unspaced-script verification either way. No tokenizer resolution\n// -- this store always uses the fts extension's default (porter) stemmer, unlike sqlite's\n// configurable `content.tokenize` (not read here; D1 does not extend that knob to DuckDB).\nasync function ensureSchema(conn: Connection, cfg: Config): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" DOUBLE, \"_ctime\" DOUBLE, \"_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)`);\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 FLOAT[STORE_DIMS] instead of the embed feature's engine-neutral BLOB+scale DDL\n // (see duckdb/vectors.ts). `scale` is kept, unused, so the feature's shared reconcile-time\n // INSERT/DELETE (src/features/embed.ts) still names a column that exists on both stores.\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 FLOAT[${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 connect(cfg: ResolvedConfig): Promise<{ instance: DuckDBInstance; duckdb: DuckDBConnection; conn: Connection; cfg: ResolvedConfig; dbPath: string; parsed: number; warnings: string[] }> {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n // Dynamic, not a top-level import: sqlite trees must never even attempt to resolve this\n // optional peer dependency, so nothing in this store's module graph imports it as a value\n // until a duckdb tree is actually opened (types-only imports elsewhere are erased and cost\n // nothing either way). Installed on first use if missing, and shared with sql-functions.ts\n // and vectors.ts (see native.ts's duckdbApi).\n let DuckDBInstance: typeof import('@duckdb/node-api').DuckDBInstance;\n try {\n ({ DuckDBInstance } = await duckdbApi());\n } catch (err) {\n if (err instanceof SenseError) throw err;\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" needs the ${DUCKDB_PACKAGE} package (${(err as Error).message})`);\n }\n\n let duckdb: DuckDBConnection;\n let instance: DuckDBInstance | undefined;\n try {\n // On-disk files default to an older storage format for cross-version compatibility, which\n // rejects VARIANT columns (\"VARIANT columns are not supported in storage versions prior to\n // v1.5.0\"); this store's dynamic frontmatter columns need VARIANT (see reconcile.ts), so\n // the format floor is pinned explicitly. :memory: databases are unaffected either way.\n instance = await DuckDBInstance.create(dbPath, { storage_compatibility_version: 'v1.5.0' });\n duckdb = await instance.connect();\n } catch (err) {\n // create() may have succeeded before connect() failed: close it, or its WAL stays open.\n instance?.closeSync();\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" failed to open ${dbPath}: ${(err as Error).message}`);\n }\n // A throw below (schema, reconcile's COLUMN_LIMIT) would leak the open instance, whose WAL\n // then locks the .duckdb file undeletable on Windows. closed marks the rebuild branch.\n let closed = false;\n try {\n await registerFunctions(duckdb);\n const conn = createConnection(duckdb);\n\n await conn.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n const version = await getMeta(conn, 'schema_version');\n const features = await getMeta(conn, 'features');\n const wantFeatures = featureSignature(cfg, FEATURES);\n if ((version !== null && version !== SCHEMA_VERSION) || (features !== null && features !== wantFeatures)) {\n // Reconcile only reparses changed files, so a stale cache can't be patched incrementally --\n // rebuild instead, same as sqlite's open() (see its comment for why this is announced).\n const reason = version !== null && version !== SCHEMA_VERSION ? 'cache format changed (new sensemaking version)' : 'config change (features, tokenizer, or presets)';\n console.error(`sense: ${reason}; rebuilding the index`);\n // Close before clearCache deletes the files underneath the still-open instance.\n closed = true;\n duckdb.disconnectSync();\n instance.closeSync();\n clearCache(cfg);\n return connect(cfg);\n }\n\n await ensureSchema(conn, cfg);\n\n const { parsed, warnings } = await reconcile(conn, cfg, cfg.baseDir);\n return { instance, duckdb, conn, cfg, dbPath, parsed, warnings };\n } catch (err) {\n if (!closed) {\n duckdb.disconnectSync();\n instance?.closeSync();\n }\n throw err;\n }\n}\n\nexport async function openDuckdb(cfg: ResolvedConfig): Promise<OpenResult> {\n const { instance, duckdb, conn, cfg: resolvedCfg, dbPath, parsed, warnings } = await connect(cfg);\n return { store: createStore(instance, duckdb, conn, resolvedCfg, resolvedCfg.baseDir), cfg: resolvedCfg, dbPath, parsed, warnings };\n}\n"],"names":["mkdirSync","join","featureSignature","STATE_DIR","STORE_DIMS","SenseError","activeFeatures","FEATURES","getMeta","setMeta","clearCache","createConnection","DUCKDB_PACKAGE","duckdbApi","reconcile","registerFunctions","createStore","DB_FILENAME","SCHEMA_VERSION","ensureSchema","conn","cfg","exec","feature","name","schema","connect","stateDir","baseDir","recursive","dbPath","DuckDBInstance","err","message","duckdb","instance","create","storage_compatibility_version","closeSync","closed","version","features","wantFeatures","reason","console","error","disconnectSync","parsed","warnings","openDuckdb","resolvedCfg","store"],"mappings":"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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { SenseError } from '../../errors.js';
|
|
2
|
+
import { activeFeatures } from '../../features/index.js';
|
|
3
|
+
import { progress } from '../../output/progress.js';
|
|
4
|
+
import { listFiles, RESERVED_COLUMNS } from '../../scan/index.js';
|
|
5
|
+
import { reparseFiles } from '../../scan/reparse.js';
|
|
6
|
+
import { getColumns, quoteIdent } from '../shared.js';
|
|
7
|
+
import { withTransaction } from '../transaction.js';
|
|
8
|
+
// Mirrors src/store/sqlite/reconcile.ts's frontmatter-upsert shape (dynamic ALTER TABLE per
|
|
9
|
+
// discovered key, ON CONFLICT upsert keeping the row's identity stable). `content` is a plain
|
|
10
|
+
// table, not FTS-virtual (D1: DuckDB's fts index is built lazily, on first lexical query, from
|
|
11
|
+
// this table -- see duckdb/lexical.ts), so its rows are maintained here unconditionally. The one
|
|
12
|
+
// forced divergence for frontmatter itself is the ALTER TABLE type: DuckDB requires a declared
|
|
13
|
+
// column type where sqlite accepts none, so every dynamic frontmatter column is VARIANT (the
|
|
14
|
+
// only DuckDB type that can hold the different JS types mapValue() produces across files for
|
|
15
|
+
// the same key).
|
|
16
|
+
const CORE_FRONTMATTER_COLUMNS = new Set([
|
|
17
|
+
'path',
|
|
18
|
+
'_mtime',
|
|
19
|
+
'_ctime',
|
|
20
|
+
'_size',
|
|
21
|
+
'_parse_error'
|
|
22
|
+
]);
|
|
23
|
+
// No rowid coupling needed (unlike sqlite's content, which links to frontmatter's rowid):
|
|
24
|
+
// `path` is content's own primary key, so this is a plain per-doc row.
|
|
25
|
+
const INSERT_CONTENT_SQL = `INSERT INTO content ("path", title, summary, text) VALUES (?, ?, ?, ?)`;
|
|
26
|
+
function contentRow(doc) {
|
|
27
|
+
return [
|
|
28
|
+
doc.relPath,
|
|
29
|
+
doc.search.title,
|
|
30
|
+
doc.search.summary,
|
|
31
|
+
doc.search.text
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
// No compile-time column cap in DuckDB (unlike SQLite's SQLITE_MAX_COLUMN); kept as a sanity
|
|
35
|
+
// fence anyway so a runaway frontmatter generator fails with a clear message instead of an
|
|
36
|
+
// unbounded ALTER TABLE loop.
|
|
37
|
+
const MAX_FRONTMATTER_COLUMNS = 10000;
|
|
38
|
+
export async function reconcile(conn, cfg, baseDir) {
|
|
39
|
+
const files = listFiles(cfg, baseDir);
|
|
40
|
+
const currentSet = new Set(files.map((f)=>f.relPath));
|
|
41
|
+
const existingStmt = await conn.prepare('SELECT "path", "_mtime", "_size" FROM frontmatter');
|
|
42
|
+
const existingRows = await existingStmt.all();
|
|
43
|
+
const existing = new Map(existingRows.map((r)=>[
|
|
44
|
+
r.path,
|
|
45
|
+
r
|
|
46
|
+
]));
|
|
47
|
+
const vanished = existingRows.filter((r)=>!currentSet.has(r.path)).map((r)=>r.path);
|
|
48
|
+
const toReparse = files.filter((f)=>{
|
|
49
|
+
const row = existing.get(f.relPath);
|
|
50
|
+
return !row || row._mtime !== f.mtimeMs || row._size !== f.size;
|
|
51
|
+
});
|
|
52
|
+
if (vanished.length === 0 && toReparse.length === 0) return {
|
|
53
|
+
parsed: 0,
|
|
54
|
+
warnings: []
|
|
55
|
+
};
|
|
56
|
+
const features = activeFeatures(cfg);
|
|
57
|
+
const seenColumns = await getColumns(conn);
|
|
58
|
+
const report = progress('reparsing files', toReparse.length);
|
|
59
|
+
const { docs: parsedDocs, warnings, newColumns } = reparseFiles(toReparse, features, cfg, seenColumns, report.tick);
|
|
60
|
+
report.finish();
|
|
61
|
+
for (const col of newColumns)seenColumns.add(col);
|
|
62
|
+
const allColumns = [
|
|
63
|
+
...seenColumns
|
|
64
|
+
];
|
|
65
|
+
if (allColumns.length > MAX_FRONTMATTER_COLUMNS) {
|
|
66
|
+
throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${allColumns.length} columns, crossing this store's sanity limit (${MAX_FRONTMATTER_COLUMNS}). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);
|
|
67
|
+
}
|
|
68
|
+
const writableColumns = allColumns.filter((c)=>CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));
|
|
69
|
+
const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(()=>'?').join(', ')}) ON CONFLICT("path") DO UPDATE SET ${writableColumns.filter((c)=>c !== 'path').map((c)=>`${quoteIdent(c)} = excluded.${quoteIdent(c)}`).join(', ')}`;
|
|
70
|
+
const added = toReparse.filter((f)=>!existing.has(f.relPath)).map((f)=>f.relPath);
|
|
71
|
+
const delta = {
|
|
72
|
+
files,
|
|
73
|
+
reparsed: parsedDocs.map((d)=>d.relPath),
|
|
74
|
+
added,
|
|
75
|
+
vanished
|
|
76
|
+
};
|
|
77
|
+
const addedSet = new Set(added);
|
|
78
|
+
const reparsedExisting = parsedDocs.map((d)=>d.relPath).filter((p)=>!addedSet.has(p));
|
|
79
|
+
await withTransaction(conn, async ()=>{
|
|
80
|
+
var _feature_remove, _feature_afterReconcile;
|
|
81
|
+
// DuckDB's ALTER TABLE ADD COLUMN requires a type; VARIANT is the only one that can hold
|
|
82
|
+
// the mixed bigint/number/string/null shapes mapValue() produces for one key across files.
|
|
83
|
+
for (const col of newColumns)await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)} VARIANT`);
|
|
84
|
+
if (parsedDocs.length > 0) {
|
|
85
|
+
const rows = parsedDocs.map((doc)=>writableColumns.map((col)=>{
|
|
86
|
+
var _doc_data_col;
|
|
87
|
+
if (col === 'path') return doc.relPath;
|
|
88
|
+
if (col === '_mtime') return doc.mtimeMs;
|
|
89
|
+
if (col === '_ctime') return doc.ctimeMs;
|
|
90
|
+
if (col === '_size') return doc.size;
|
|
91
|
+
if (col === '_parse_error') return doc.parseError;
|
|
92
|
+
return (_doc_data_col = doc.data[col]) !== null && _doc_data_col !== void 0 ? _doc_data_col : null;
|
|
93
|
+
}));
|
|
94
|
+
await conn.runBatch(insertSql, rows);
|
|
95
|
+
}
|
|
96
|
+
const contentTouched = [
|
|
97
|
+
...vanished,
|
|
98
|
+
...reparsedExisting
|
|
99
|
+
];
|
|
100
|
+
if (contentTouched.length > 0) await conn.runBatch('DELETE FROM content WHERE "path" = ?', contentTouched.map((p)=>[
|
|
101
|
+
p
|
|
102
|
+
]));
|
|
103
|
+
if (parsedDocs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, parsedDocs.map(contentRow));
|
|
104
|
+
if (vanished.length > 0) await conn.runBatch('DELETE FROM frontmatter WHERE "path" = ?', vanished.map((p)=>[
|
|
105
|
+
p
|
|
106
|
+
]));
|
|
107
|
+
// A preset edit forces a full rebuild, so an unchanged doc's coverage is already correct;
|
|
108
|
+
// new docs have nothing to clear, which keeps cold builds linear.
|
|
109
|
+
const presetTouched = [
|
|
110
|
+
...vanished,
|
|
111
|
+
...reparsedExisting
|
|
112
|
+
];
|
|
113
|
+
if (presetTouched.length > 0) await conn.runBatch('DELETE FROM preset_files WHERE "path" = ?', presetTouched.map((p)=>[
|
|
114
|
+
p
|
|
115
|
+
]));
|
|
116
|
+
const presetRows = [];
|
|
117
|
+
for (const doc of parsedDocs)for (const presetName of doc.presets)presetRows.push([
|
|
118
|
+
doc.relPath,
|
|
119
|
+
presetName
|
|
120
|
+
]);
|
|
121
|
+
if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files ("path", preset) VALUES (?, ?)', presetRows);
|
|
122
|
+
const removedPaths = [
|
|
123
|
+
...vanished,
|
|
124
|
+
...reparsedExisting
|
|
125
|
+
];
|
|
126
|
+
if (removedPaths.length > 0) for (const feature of features)await ((_feature_remove = feature.remove) === null || _feature_remove === void 0 ? void 0 : _feature_remove.call(feature, conn, removedPaths, delta));
|
|
127
|
+
for (const feature of features){
|
|
128
|
+
var _feature_store;
|
|
129
|
+
const docsForFeature = parsedDocs.map((doc)=>({
|
|
130
|
+
path: doc.relPath,
|
|
131
|
+
extracted: doc.extracted[feature.name]
|
|
132
|
+
}));
|
|
133
|
+
await ((_feature_store = feature.store) === null || _feature_store === void 0 ? void 0 : _feature_store.call(feature, conn, docsForFeature, delta));
|
|
134
|
+
}
|
|
135
|
+
for (const feature of features)await ((_feature_afterReconcile = feature.afterReconcile) === null || _feature_afterReconcile === void 0 ? void 0 : _feature_afterReconcile.call(feature, conn, delta));
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
parsed: parsedDocs.length,
|
|
139
|
+
warnings
|
|
140
|
+
};
|
|
141
|
+
}
|