sensemaking 0.17.1 → 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 +144 -3
- package/dist/cjs/cli/map.js.map +1 -1
- package/dist/cjs/cli/named.js +19 -11
- package/dist/cjs/cli/named.js.map +1 -1
- package/dist/cjs/cli/path.js +186 -26
- package/dist/cjs/cli/path.js.map +1 -1
- package/dist/cjs/cli/peek.js +145 -4
- package/dist/cjs/cli/peek.js.map +1 -1
- package/dist/cjs/cli/related.js +2 -2
- package/dist/cjs/cli/related.js.map +1 -1
- package/dist/cjs/cli/search.js +2 -2
- 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 +177 -52
- 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 +3 -3
- package/dist/cjs/embed/distribution.d.ts +3 -3
- package/dist/cjs/embed/distribution.js +162 -6
- package/dist/cjs/embed/distribution.js.map +1 -1
- package/dist/cjs/embed/langfit.d.cts +2 -2
- package/dist/cjs/embed/langfit.d.ts +2 -2
- package/dist/cjs/embed/langfit.js +57 -44
- 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/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 +4 -4
- package/dist/esm/cli/map.js.map +1 -1
- package/dist/esm/cli/named.js +2 -2
- package/dist/esm/cli/named.js.map +1 -1
- package/dist/esm/cli/path.js +4 -4
- package/dist/esm/cli/path.js.map +1 -1
- package/dist/esm/cli/peek.js +4 -4
- package/dist/esm/cli/peek.js.map +1 -1
- package/dist/esm/cli/related.js +2 -2
- package/dist/esm/cli/related.js.map +1 -1
- package/dist/esm/cli/search.js +1 -1
- 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 +23 -22
- 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 +3 -3
- package/dist/esm/embed/distribution.js +5 -5
- package/dist/esm/embed/distribution.js.map +1 -1
- package/dist/esm/embed/langfit.d.ts +2 -2
- package/dist/esm/embed/langfit.js +4 -4
- 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/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,57 @@
|
|
|
1
|
+
// Store-agnostic joining transaction helper: node:sqlite and DuckDB both support only
|
|
2
|
+
// BEGIN/COMMIT/ROLLBACK, no SAVEPOINT -- nesting joins the outer transaction instead of
|
|
3
|
+
// stacking, so both stores share one semantics through the one thing they have in common,
|
|
4
|
+
// exec(sql).
|
|
5
|
+
// Keyed by connection, not a module singleton: two connections can coexist in one process
|
|
6
|
+
// (e.g. watch.ts alongside a CLI query) and must not share depth.
|
|
7
|
+
const states = new WeakMap();
|
|
8
|
+
function stateFor(conn) {
|
|
9
|
+
let state = states.get(conn);
|
|
10
|
+
if (!state) {
|
|
11
|
+
state = {
|
|
12
|
+
depth: 0,
|
|
13
|
+
rollbackOnly: false
|
|
14
|
+
};
|
|
15
|
+
states.set(conn, state);
|
|
16
|
+
}
|
|
17
|
+
return state;
|
|
18
|
+
}
|
|
19
|
+
async function enter(conn, state) {
|
|
20
|
+
if (state.depth === 0) {
|
|
21
|
+
await conn.exec('BEGIN');
|
|
22
|
+
state.rollbackOnly = false;
|
|
23
|
+
}
|
|
24
|
+
state.depth++;
|
|
25
|
+
}
|
|
26
|
+
// Reaching depth 0 still rollback-only means an inner scope failed and an enclosing catch
|
|
27
|
+
// swallowed it; commit here would persist a partial write, so this rolls back and throws instead.
|
|
28
|
+
async function leaveOk(conn, state) {
|
|
29
|
+
state.depth--;
|
|
30
|
+
if (state.depth > 0) return;
|
|
31
|
+
if (state.rollbackOnly) {
|
|
32
|
+
await conn.exec('ROLLBACK');
|
|
33
|
+
throw new Error('transaction rolled back: a nested scope failed');
|
|
34
|
+
}
|
|
35
|
+
await conn.exec('COMMIT');
|
|
36
|
+
}
|
|
37
|
+
async function leaveErr(conn, state) {
|
|
38
|
+
state.rollbackOnly = true;
|
|
39
|
+
state.depth--;
|
|
40
|
+
if (state.depth === 0) await conn.exec('ROLLBACK');
|
|
41
|
+
}
|
|
42
|
+
// Depth-0 opens BEGIN; a nested call joins it and issues no SQL of its own. The outermost call
|
|
43
|
+
// owns COMMIT/ROLLBACK. Every store, and every internal write loop (reconcile, runBatch), goes
|
|
44
|
+
// through this one helper, so join semantics never differ by call site.
|
|
45
|
+
export async function withTransaction(conn, fn) {
|
|
46
|
+
const state = stateFor(conn);
|
|
47
|
+
await enter(conn, state);
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = await fn();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
await leaveErr(conn, state);
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
await leaveOk(conn, state);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/transaction.ts"],"sourcesContent":["// Store-agnostic joining transaction helper: node:sqlite and DuckDB both support only\n// BEGIN/COMMIT/ROLLBACK, no SAVEPOINT -- nesting joins the outer transaction instead of\n// stacking, so both stores share one semantics through the one thing they have in common,\n// exec(sql).\ninterface ExecConnection {\n exec(sql: string): Promise<void>;\n}\n\ninterface TxState {\n depth: number;\n rollbackOnly: boolean;\n}\n\n// Keyed by connection, not a module singleton: two connections can coexist in one process\n// (e.g. watch.ts alongside a CLI query) and must not share depth.\nconst states = new WeakMap<ExecConnection, TxState>();\n\nfunction stateFor(conn: ExecConnection): TxState {\n let state = states.get(conn);\n if (!state) {\n state = { depth: 0, rollbackOnly: false };\n states.set(conn, state);\n }\n return state;\n}\n\nasync function enter(conn: ExecConnection, state: TxState): Promise<void> {\n if (state.depth === 0) {\n await conn.exec('BEGIN');\n state.rollbackOnly = false;\n }\n state.depth++;\n}\n\n// Reaching depth 0 still rollback-only means an inner scope failed and an enclosing catch\n// swallowed it; commit here would persist a partial write, so this rolls back and throws instead.\nasync function leaveOk(conn: ExecConnection, state: TxState): Promise<void> {\n state.depth--;\n if (state.depth > 0) return;\n if (state.rollbackOnly) {\n await conn.exec('ROLLBACK');\n throw new Error('transaction rolled back: a nested scope failed');\n }\n await conn.exec('COMMIT');\n}\n\nasync function leaveErr(conn: ExecConnection, state: TxState): Promise<void> {\n state.rollbackOnly = true;\n state.depth--;\n if (state.depth === 0) await conn.exec('ROLLBACK');\n}\n\n// Depth-0 opens BEGIN; a nested call joins it and issues no SQL of its own. The outermost call\n// owns COMMIT/ROLLBACK. Every store, and every internal write loop (reconcile, runBatch), goes\n// through this one helper, so join semantics never differ by call site.\nexport async function withTransaction<T>(conn: ExecConnection, fn: () => Promise<T>): Promise<T> {\n const state = stateFor(conn);\n await enter(conn, state);\n let result: T;\n try {\n result = await fn();\n } catch (err) {\n await leaveErr(conn, state);\n throw err;\n }\n await leaveOk(conn, state);\n return result;\n}\n"],"names":["states","WeakMap","stateFor","conn","state","get","depth","rollbackOnly","set","enter","exec","leaveOk","Error","leaveErr","withTransaction","fn","result","err"],"mappings":"AAAA,sFAAsF;AACtF,wFAAwF;AACxF,0FAA0F;AAC1F,aAAa;AAUb,0FAA0F;AAC1F,kEAAkE;AAClE,MAAMA,SAAS,IAAIC;AAEnB,SAASC,SAASC,IAAoB;IACpC,IAAIC,QAAQJ,OAAOK,GAAG,CAACF;IACvB,IAAI,CAACC,OAAO;QACVA,QAAQ;YAAEE,OAAO;YAAGC,cAAc;QAAM;QACxCP,OAAOQ,GAAG,CAACL,MAAMC;IACnB;IACA,OAAOA;AACT;AAEA,eAAeK,MAAMN,IAAoB,EAAEC,KAAc;IACvD,IAAIA,MAAME,KAAK,KAAK,GAAG;QACrB,MAAMH,KAAKO,IAAI,CAAC;QAChBN,MAAMG,YAAY,GAAG;IACvB;IACAH,MAAME,KAAK;AACb;AAEA,0FAA0F;AAC1F,kGAAkG;AAClG,eAAeK,QAAQR,IAAoB,EAAEC,KAAc;IACzDA,MAAME,KAAK;IACX,IAAIF,MAAME,KAAK,GAAG,GAAG;IACrB,IAAIF,MAAMG,YAAY,EAAE;QACtB,MAAMJ,KAAKO,IAAI,CAAC;QAChB,MAAM,IAAIE,MAAM;IAClB;IACA,MAAMT,KAAKO,IAAI,CAAC;AAClB;AAEA,eAAeG,SAASV,IAAoB,EAAEC,KAAc;IAC1DA,MAAMG,YAAY,GAAG;IACrBH,MAAME,KAAK;IACX,IAAIF,MAAME,KAAK,KAAK,GAAG,MAAMH,KAAKO,IAAI,CAAC;AACzC;AAEA,+FAA+F;AAC/F,+FAA+F;AAC/F,wEAAwE;AACxE,OAAO,eAAeI,gBAAmBX,IAAoB,EAAEY,EAAoB;IACjF,MAAMX,QAAQF,SAASC;IACvB,MAAMM,MAAMN,MAAMC;IAClB,IAAIY;IACJ,IAAI;QACFA,SAAS,MAAMD;IACjB,EAAE,OAAOE,KAAK;QACZ,MAAMJ,SAASV,MAAMC;QACrB,MAAMa;IACR;IACA,MAAMN,QAAQR,MAAMC;IACpB,OAAOY;AACT"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type Capability = 'phrases' | 'snippets' | 'watch-concurrency' | 'lexical' | 'vectors';
|
|
2
|
+
export interface RunResult {
|
|
3
|
+
changes: number | bigint;
|
|
4
|
+
lastInsertRowid: number | bigint;
|
|
5
|
+
}
|
|
6
|
+
export interface Statement {
|
|
7
|
+
run(...params: unknown[]): Promise<RunResult>;
|
|
8
|
+
get(...params: unknown[]): Promise<unknown>;
|
|
9
|
+
all(...params: unknown[]): Promise<unknown[]>;
|
|
10
|
+
iterate(...params: unknown[]): AsyncIterable<unknown>;
|
|
11
|
+
columns(): Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
}>;
|
|
14
|
+
setReadBigInts(enabled: boolean): void;
|
|
15
|
+
}
|
|
16
|
+
export interface Connection {
|
|
17
|
+
exec(sql: string): Promise<void>;
|
|
18
|
+
prepare(sql: string): Promise<Statement>;
|
|
19
|
+
runBatch(sql: string, paramRows: unknown[][]): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface DocumentStore {
|
|
22
|
+
columns(): Promise<string[]>;
|
|
23
|
+
}
|
|
24
|
+
export interface LexicalHit {
|
|
25
|
+
path: string;
|
|
26
|
+
hit: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface LexicalQueryOptions {
|
|
29
|
+
whereJoin: string;
|
|
30
|
+
whereCond: string;
|
|
31
|
+
scopeCond: string;
|
|
32
|
+
limit: number;
|
|
33
|
+
}
|
|
34
|
+
export interface LexicalIndex {
|
|
35
|
+
query(terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]>;
|
|
36
|
+
}
|
|
37
|
+
export interface VectorCandidate {
|
|
38
|
+
path: string;
|
|
39
|
+
lines: string;
|
|
40
|
+
similarity: number;
|
|
41
|
+
}
|
|
42
|
+
export interface VectorSimilar {
|
|
43
|
+
path: string;
|
|
44
|
+
similarity: number;
|
|
45
|
+
}
|
|
46
|
+
export interface VectorWriteRow {
|
|
47
|
+
path: string;
|
|
48
|
+
chunk: number;
|
|
49
|
+
scale: number;
|
|
50
|
+
vector: Buffer;
|
|
51
|
+
}
|
|
52
|
+
export interface VectorStore {
|
|
53
|
+
pending(): Promise<Array<{
|
|
54
|
+
path: string;
|
|
55
|
+
chunk: number;
|
|
56
|
+
}>>;
|
|
57
|
+
writeVectors(rows: VectorWriteRow[]): Promise<void>;
|
|
58
|
+
candidates(queryVector: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;
|
|
59
|
+
similar(path: string, opts: {
|
|
60
|
+
exclude: Set<string>;
|
|
61
|
+
allowed?: Set<string>;
|
|
62
|
+
k: number;
|
|
63
|
+
}): Promise<VectorSimilar[]>;
|
|
64
|
+
hasVector(path: string): Promise<boolean>;
|
|
65
|
+
}
|
|
66
|
+
export interface RawStatement {
|
|
67
|
+
columns(): Array<{
|
|
68
|
+
name: string;
|
|
69
|
+
}>;
|
|
70
|
+
iterate(...params: unknown[]): AsyncIterable<unknown>;
|
|
71
|
+
}
|
|
72
|
+
export interface SqlSession {
|
|
73
|
+
prepare(sql: string): Promise<RawStatement>;
|
|
74
|
+
}
|
|
75
|
+
export interface Store {
|
|
76
|
+
readonly name: 'sqlite' | 'duckdb';
|
|
77
|
+
readonly capabilities: ReadonlySet<Capability>;
|
|
78
|
+
exec(sql: string): Promise<void>;
|
|
79
|
+
prepare(sql: string): Promise<Statement>;
|
|
80
|
+
runBatch(sql: string, paramRows: unknown[][]): Promise<void>;
|
|
81
|
+
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
82
|
+
reconcile(): Promise<{
|
|
83
|
+
parsed: number;
|
|
84
|
+
warnings: string[];
|
|
85
|
+
}>;
|
|
86
|
+
docs: DocumentStore;
|
|
87
|
+
lexical: LexicalIndex;
|
|
88
|
+
vectors: VectorStore;
|
|
89
|
+
raw: SqlSession;
|
|
90
|
+
close(): Promise<void>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The backing-store interface: a minimal portable statement surface (exec/prepare, used as-is
|
|
2
|
+
// by feature-owned tables and ad hoc queries) plus dedicated interfaces exactly where engines
|
|
3
|
+
// diverge (lexical index, vector scan, raw sql passthrough).
|
|
4
|
+
// 'lexical'/'vectors' mean the store's LexicalIndex/VectorStore are functionally implemented
|
|
5
|
+
// (rather than present-but-inert); 'phrases'/'snippets'/'watch-concurrency' are the finer
|
|
6
|
+
// behaviors sqlite's FTS5 path carries. A tree whose config needs a capability the chosen
|
|
7
|
+
// store lacks fails at open (or at first use for a lazily-detected need), named.
|
|
8
|
+
// 'phrases' means quoted-phrase (`"..."`) matching only -- not FTS5's wider query grammar
|
|
9
|
+
// (prefix `*`, boolean AND/OR/NOT, NEAR, `^`, column filters), which duckdb's lexical path
|
|
10
|
+
// rejects loudly rather than interpret (see store/duckdb/lexical.ts).
|
|
11
|
+
export { };
|
|
@@ -0,0 +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, used as-is\n// by feature-owned tables and ad hoc queries) plus dedicated interfaces exactly where engines\n// diverge (lexical index, vector scan, raw sql passthrough).\n\n// 'lexical'/'vectors' mean the store's LexicalIndex/VectorStore are functionally implemented\n// (rather than present-but-inert); 'phrases'/'snippets'/'watch-concurrency' are the finer\n// behaviors sqlite's FTS5 path carries. A tree whose config needs a capability the chosen\n// store lacks fails at open (or at first use for a lazily-detected need), named.\n// 'phrases' means quoted-phrase (`\"...\"`) matching only -- not FTS5's wider query grammar\n// (prefix `*`, boolean AND/OR/NOT, NEAR, `^`, column filters), which duckdb's lexical path\n// rejects loudly rather than interpret (see store/duckdb/lexical.ts).\nexport type Capability = 'phrases' | 'snippets' | 'watch-concurrency' | 'lexical' | 'vectors';\n\nexport interface RunResult {\n changes: number | bigint;\n lastInsertRowid: number | bigint;\n}\n\n// A prepared statement's async surface: every engine this store supports has to cross a real\n// async boundary for a query (DuckDB has no synchronous client at all), so run/get/all return\n// Promises. iterate() stays an async iterable for the same reason `sense sql` streams.\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 own hot loops (schema,\n// reconcile). Portable so a feature never imports an engine-specific client type. `runBatch`\n// is the one call a write loop goes through instead of preparing once and calling `run()` per\n// row itself: one crossing per loop, the engine's own bulk idiom underneath (see each store's\n// implementation for what that idiom is).\nexport interface Connection {\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n}\n\nexport interface DocumentStore {\n // Frontmatter column names (including internal ones; callers filter).\n columns(): Promise<string[]>;\n}\n\nexport interface LexicalHit {\n path: string;\n hit: string | null;\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 excerpt, 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 still 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: 'sqlite' | 'duckdb';\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, the graph\n // ring's temp-table writes, etc.) goes through this instead of looping over `run()` itself.\n // See Connection.runBatch -- same contract, same per-store implementation.\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Pins one snapshot across multi-statement reads; `map` and `peek` use it. A command whose\n // scope holds a network-bound write (search's embedding top-up) must stay outside it.\n // Nesting joins the enclosing transaction rather than opening a second one.\n transaction<T>(fn: () => Promise<T>): Promise<T>;\n // The whole file-sync pass (parse changed files, run every feature hook, resolve links,\n // recompute rank) behind one method: per-file iteration never crosses the async boundary.\n reconcile(): Promise<{ parsed: number; warnings: string[] }>;\n docs: DocumentStore;\n lexical: LexicalIndex;\n vectors: VectorStore;\n raw: SqlSession;\n close(): Promise<void>;\n}\n"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,8FAA8F;AAC9F,6DAA6D;AAE7D,6FAA6F;AAC7F,0FAA0F;AAC1F,0FAA0F;AAC1F,iFAAiF;AACjF,0FAA0F;AAC1F,2FAA2F;AAC3F,sEAAsE;AA8FtE,WAqBC"}
|
package/dist/esm/watch.d.ts
CHANGED
|
@@ -15,5 +15,8 @@ export type WatchEvent = {
|
|
|
15
15
|
export interface WatchOptions {
|
|
16
16
|
force?: boolean;
|
|
17
17
|
onEvent?: (event: WatchEvent) => void;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
debounceMs?: number;
|
|
20
|
+
heartbeatIntervalMs?: number;
|
|
18
21
|
}
|
|
19
22
|
export declare function runWatch(cfg: ResolvedConfig, opts?: WatchOptions): Promise<void>;
|
package/dist/esm/watch.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { watch as fsWatch } from 'node:fs';
|
|
2
2
|
import { STATE_DIR } from './config/index.js';
|
|
3
|
-
import { docCount, getMeta, open, reconcile, setMeta } from './db/index.js';
|
|
4
3
|
import { SenseError } from './errors.js';
|
|
4
|
+
import { guardedTick } from './lib/guarded-tick.js';
|
|
5
|
+
import { docCount, getMeta, openStore, setMeta } from './store/index.js';
|
|
5
6
|
// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.
|
|
6
7
|
const DEBOUNCE_MS = 200;
|
|
7
8
|
const HEARTBEAT_INTERVAL_MS = 5000;
|
|
8
9
|
const STALE_HEARTBEAT_MS = 15000;
|
|
9
|
-
// Runs in the foreground until SIGINT/SIGTERM. Throws WATCH_ACTIVE if another watcher's heartbeat is still fresh and --force wasn't given.
|
|
10
|
+
// Runs in the foreground until SIGINT/SIGTERM/signal abort. Throws WATCH_ACTIVE if another watcher's heartbeat is still fresh and --force wasn't given.
|
|
10
11
|
export async function runWatch(cfg, opts = {}) {
|
|
11
|
-
var _opts_onEvent;
|
|
12
|
+
var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs;
|
|
12
13
|
const onEvent = (_opts_onEvent = opts.onEvent) !== null && _opts_onEvent !== void 0 ? _opts_onEvent : ()=>{};
|
|
13
|
-
const
|
|
14
|
+
const debounceMs = (_opts_debounceMs = opts.debounceMs) !== null && _opts_debounceMs !== void 0 ? _opts_debounceMs : DEBOUNCE_MS;
|
|
15
|
+
const heartbeatIntervalMs = (_opts_heartbeatIntervalMs = opts.heartbeatIntervalMs) !== null && _opts_heartbeatIntervalMs !== void 0 ? _opts_heartbeatIntervalMs : HEARTBEAT_INTERVAL_MS;
|
|
16
|
+
const { store, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);
|
|
14
17
|
const baseDir = cfg.baseDir;
|
|
15
|
-
const existingHeartbeat = getMeta(
|
|
18
|
+
const existingHeartbeat = await getMeta(store, 'watch_heartbeat');
|
|
16
19
|
if (existingHeartbeat && !opts.force) {
|
|
17
20
|
const age = Date.now() - Date.parse(existingHeartbeat);
|
|
18
21
|
if (age >= 0 && age < STALE_HEARTBEAT_MS) {
|
|
19
|
-
|
|
22
|
+
await store.close();
|
|
20
23
|
throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -29,26 +32,27 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
29
32
|
onEvent({
|
|
30
33
|
type: 'reconciled',
|
|
31
34
|
parsed: initialParsed,
|
|
32
|
-
total: docCount(
|
|
35
|
+
total: await docCount(store),
|
|
33
36
|
warnings: initialWarnings
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
setMeta(
|
|
39
|
+
let stopping = false;
|
|
40
|
+
const touchHeartbeat = async ()=>{
|
|
41
|
+
await setMeta(store, 'watch_heartbeat', new Date().toISOString());
|
|
42
|
+
await setMeta(store, 'watch_pid', String(process.pid));
|
|
39
43
|
};
|
|
40
|
-
touchHeartbeat();
|
|
44
|
+
await touchHeartbeat();
|
|
41
45
|
let debounceTimer = null;
|
|
42
46
|
const scheduleReconcile = ()=>{
|
|
43
47
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
44
|
-
debounceTimer = setTimeout(()=>{
|
|
48
|
+
debounceTimer = setTimeout(async ()=>{
|
|
45
49
|
debounceTimer = null;
|
|
46
50
|
try {
|
|
47
|
-
const { parsed, warnings } = reconcile(
|
|
51
|
+
const { parsed, warnings } = await store.reconcile();
|
|
48
52
|
onEvent({
|
|
49
53
|
type: 'reconciled',
|
|
50
54
|
parsed,
|
|
51
|
-
total: docCount(
|
|
55
|
+
total: await docCount(store),
|
|
52
56
|
warnings
|
|
53
57
|
});
|
|
54
58
|
} catch (err) {
|
|
@@ -57,7 +61,7 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
57
61
|
message: err.message
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
|
-
},
|
|
64
|
+
}, debounceMs);
|
|
61
65
|
};
|
|
62
66
|
// Ignore our own state dir, or the heartbeat write would retrigger itself forever.
|
|
63
67
|
const watcher = fsWatch(baseDir, {
|
|
@@ -66,18 +70,31 @@ export async function runWatch(cfg, opts = {}) {
|
|
|
66
70
|
if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;
|
|
67
71
|
scheduleReconcile();
|
|
68
72
|
});
|
|
69
|
-
const heartbeatTimer = setInterval(touchHeartbeat,
|
|
73
|
+
const heartbeatTimer = setInterval(guardedTick(touchHeartbeat, ()=>stopping), heartbeatIntervalMs);
|
|
70
74
|
return new Promise((resolveShutdown)=>{
|
|
71
|
-
|
|
75
|
+
var _opts_signal, _opts_signal1;
|
|
76
|
+
// SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is
|
|
77
|
+
// unregistered here too so a second runWatch call in the same process starts clean.
|
|
78
|
+
const shutdown = async ()=>{
|
|
79
|
+
var _opts_signal;
|
|
80
|
+
if (stopping) return;
|
|
81
|
+
stopping = true;
|
|
82
|
+
process.off('SIGINT', shutdown);
|
|
83
|
+
process.off('SIGTERM', shutdown);
|
|
84
|
+
(_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.removeEventListener('abort', shutdown);
|
|
72
85
|
clearInterval(heartbeatTimer);
|
|
73
86
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
74
87
|
watcher.close();
|
|
75
|
-
setMeta(
|
|
76
|
-
setMeta(
|
|
77
|
-
|
|
88
|
+
await setMeta(store, 'watch_heartbeat', null);
|
|
89
|
+
await setMeta(store, 'watch_pid', null);
|
|
90
|
+
await store.close();
|
|
78
91
|
resolveShutdown();
|
|
79
92
|
};
|
|
80
|
-
process.once('SIGINT',
|
|
81
|
-
process.once('SIGTERM',
|
|
93
|
+
process.once('SIGINT', shutdown);
|
|
94
|
+
process.once('SIGTERM', shutdown);
|
|
95
|
+
if ((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted) shutdown();
|
|
96
|
+
else (_opts_signal1 = opts.signal) === null || _opts_signal1 === void 0 ? void 0 : _opts_signal1.addEventListener('abort', shutdown, {
|
|
97
|
+
once: true
|
|
98
|
+
});
|
|
82
99
|
});
|
|
83
100
|
}
|
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 { STATE_DIR } from './config/index.ts';\nimport {
|
|
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 { STATE_DIR } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { docCount, getMeta, openStore, setMeta } from './store/index.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;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\nexport type WatchEvent = { type: 'started'; baseDir: 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. Throws WATCH_ACTIVE if another watcher's heartbeat is still fresh and --force wasn't given.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;\n const { store, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n const baseDir = cfg.baseDir;\n\n const existingHeartbeat = await getMeta(store, 'watch_heartbeat');\n if (existingHeartbeat && !opts.force) {\n const age = Date.now() - Date.parse(existingHeartbeat);\n if (age >= 0 && age < STALE_HEARTBEAT_MS) {\n await store.close();\n throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n }\n\n onEvent({ type: 'started', baseDir, dbPath });\n if (initialWarnings.length > 0 || initialParsed > 0) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: await docCount(store), warnings: initialWarnings });\n }\n\n let stopping = false;\n const touchHeartbeat = async () => {\n await setMeta(store, 'watch_heartbeat', new Date().toISOString());\n await setMeta(store, 'watch_pid', String(process.pid));\n };\n await touchHeartbeat();\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(async () => {\n debounceTimer = null;\n try {\n const { parsed, warnings } = await store.reconcile();\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n }, debounceMs);\n };\n\n // Ignore our own state dir, or the heartbeat write would retrigger itself forever.\n const watcher = fsWatch(baseDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const heartbeatTimer = setInterval(\n guardedTick(touchHeartbeat, () => stopping),\n heartbeatIntervalMs\n );\n\n return new Promise<void>((resolveShutdown) => {\n // SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is\n // unregistered here too so a second runWatch call in the same process starts clean.\n const shutdown = async () => {\n if (stopping) return;\n stopping = true;\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n opts.signal?.removeEventListener('abort', shutdown);\n clearInterval(heartbeatTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await setMeta(store, 'watch_heartbeat', null);\n await setMeta(store, 'watch_pid', null);\n await store.close();\n resolveShutdown();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n if (opts.signal?.aborted) shutdown();\n else opts.signal?.addEventListener('abort', shutdown, { once: true });\n });\n}\n"],"names":["watch","fsWatch","STATE_DIR","SenseError","guardedTick","docCount","getMeta","openStore","setMeta","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","runWatch","cfg","opts","onEvent","debounceMs","heartbeatIntervalMs","store","dbPath","warnings","initialWarnings","parsed","initialParsed","baseDir","existingHeartbeat","force","age","Date","now","parse","close","Math","round","type","length","total","stopping","touchHeartbeat","toISOString","String","process","pid","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","reconcile","err","message","watcher","recursive","_event","filename","startsWith","heartbeatTimer","setInterval","Promise","resolveShutdown","shutdown","off","signal","removeEventListener","clearInterval","once","aborted","addEventListener"],"mappings":"AAAA,SAASA,SAASC,OAAO,QAAQ,UAAU;AAE3C,SAASC,SAAS,QAAQ,oBAAoB;AAC9C,SAASC,UAAU,QAAQ,cAAc;AACzC,SAASC,WAAW,QAAQ,wBAAwB;AACpD,SAASC,QAAQ,EAAEC,OAAO,EAAEC,SAAS,EAAEC,OAAO,QAAQ,mBAAmB;AAEzE,uJAAuJ;AACvJ,MAAMC,cAAc;AACpB,MAAMC,wBAAwB;AAC9B,MAAMC,qBAAqB;AAa3B,wJAAwJ;AACxJ,OAAO,eAAeC,SAASC,GAAmB,EAAEC,OAAqB,CAAC,CAAC;QACzDA,eACGA,kBACSA;IAF5B,MAAMC,WAAUD,gBAAAA,KAAKC,OAAO,cAAZD,2BAAAA,gBAAiB,KAAO;IACxC,MAAME,cAAaF,mBAAAA,KAAKE,UAAU,cAAfF,8BAAAA,mBAAmBL;IACtC,MAAMQ,uBAAsBH,4BAAAA,KAAKG,mBAAmB,cAAxBH,uCAAAA,4BAA4BJ;IACxD,MAAM,EAAEQ,KAAK,EAAEC,MAAM,EAAEC,UAAUC,eAAe,EAAEC,QAAQC,aAAa,EAAE,GAAG,MAAMhB,UAAUM;IAC5F,MAAMW,UAAUX,IAAIW,OAAO;IAE3B,MAAMC,oBAAoB,MAAMnB,QAAQY,OAAO;IAC/C,IAAIO,qBAAqB,CAACX,KAAKY,KAAK,EAAE;QACpC,MAAMC,MAAMC,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACL;QACpC,IAAIE,OAAO,KAAKA,MAAMhB,oBAAoB;YACxC,MAAMO,MAAMa,KAAK;YACjB,MAAM,IAAI5B,WAAW,gBAAgB,CAAC,0CAA0C,EAAE6B,KAAKC,KAAK,CAACN,MAAM,MAAM,+BAA+B,CAAC;QAC3I;IACF;IAEAZ,QAAQ;QAAEmB,MAAM;QAAWV;QAASL;IAAO;IAC3C,IAAIE,gBAAgBc,MAAM,GAAG,KAAKZ,gBAAgB,GAAG;QACnDR,QAAQ;YAAEmB,MAAM;YAAcZ,QAAQC;YAAea,OAAO,MAAM/B,SAASa;YAAQE,UAAUC;QAAgB;IAC/G;IAEA,IAAIgB,WAAW;IACf,MAAMC,iBAAiB;QACrB,MAAM9B,QAAQU,OAAO,mBAAmB,IAAIU,OAAOW,WAAW;QAC9D,MAAM/B,QAAQU,OAAO,aAAasB,OAAOC,QAAQC,GAAG;IACtD;IACA,MAAMJ;IAEN,IAAIK,gBAAuC;IAC3C,MAAMC,oBAAoB;QACxB,IAAID,eAAeE,aAAaF;QAChCA,gBAAgBG,WAAW;YACzBH,gBAAgB;YAChB,IAAI;gBACF,MAAM,EAAErB,MAAM,EAAEF,QAAQ,EAAE,GAAG,MAAMF,MAAM6B,SAAS;gBAClDhC,QAAQ;oBAAEmB,MAAM;oBAAcZ;oBAAQc,OAAO,MAAM/B,SAASa;oBAAQE;gBAAS;YAC/E,EAAE,OAAO4B,KAAK;gBACZjC,QAAQ;oBAAEmB,MAAM;oBAAmBe,SAAS,AAACD,IAAcC,OAAO;gBAAC;YACrE;QACF,GAAGjC;IACL;IAEA,mFAAmF;IACnF,MAAMkC,UAAUjD,QAAQuB,SAAS;QAAE2B,WAAW;IAAK,GAAG,CAACC,QAAQC;QAC7D,IAAI,OAAOA,aAAa,YAAYA,SAASC,UAAU,CAACpD,YAAY;QACpE0C;IACF;IACA,MAAMW,iBAAiBC,YACrBpD,YAAYkC,gBAAgB,IAAMD,WAClCpB;IAGF,OAAO,IAAIwC,QAAc,CAACC;YAmBpB5C,cACCA;QAnBL,oFAAoF;QACpF,oFAAoF;QACpF,MAAM6C,WAAW;gBAKf7C;YAJA,IAAIuB,UAAU;YACdA,WAAW;YACXI,QAAQmB,GAAG,CAAC,UAAUD;YACtBlB,QAAQmB,GAAG,CAAC,WAAWD;aACvB7C,eAAAA,KAAK+C,MAAM,cAAX/C,mCAAAA,aAAagD,mBAAmB,CAAC,SAASH;YAC1CI,cAAcR;YACd,IAAIZ,eAAeE,aAAaF;YAChCO,QAAQnB,KAAK;YACb,MAAMvB,QAAQU,OAAO,mBAAmB;YACxC,MAAMV,QAAQU,OAAO,aAAa;YAClC,MAAMA,MAAMa,KAAK;YACjB2B;QACF;QACAjB,QAAQuB,IAAI,CAAC,UAAUL;QACvBlB,QAAQuB,IAAI,CAAC,WAAWL;QACxB,KAAI7C,eAAAA,KAAK+C,MAAM,cAAX/C,mCAAAA,aAAamD,OAAO,EAAEN;cACrB7C,gBAAAA,KAAK+C,MAAM,cAAX/C,oCAAAA,cAAaoD,gBAAgB,CAAC,SAASP,UAAU;YAAEK,MAAM;QAAK;IACrE;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sensemaking",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@huggingface/tokenizers": "^0.1.3",
|
|
82
82
|
"franc-min": "^6.2.0",
|
|
83
|
+
"install-module-linked": "^1.6.5",
|
|
83
84
|
"mdast-util-from-markdown": "^2.0.3",
|
|
84
85
|
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
|
85
86
|
"mdast-util-gfm-footnote": "^2.0.0",
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"yaml": "^2.9.0"
|
|
95
96
|
},
|
|
96
97
|
"devDependencies": {
|
|
98
|
+
"@duckdb/node-api": "*",
|
|
97
99
|
"@types/mdast": "^4.0.4",
|
|
98
100
|
"@types/mocha": "*",
|
|
99
101
|
"@types/node": "*",
|
package/schema.json
CHANGED
|
@@ -112,6 +112,11 @@
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
|
+
"store": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"enum": ["sqlite", "duckdb"],
|
|
118
|
+
"description": "Backing store engine, defaulting to \"sqlite\" (zero-dependency, Node's built-in SQLite). \"duckdb\" is experimental: the first command that opens a duckdb tree installs @duckdb/node-api on its own (a one-time native download, about 110 MB). The same commands and table names run on both; what does not port is FTS5 syntax. Under duckdb, `search` text and raw MATCH reject FTS5's prefix (foo*), boolean (AND/OR/NOT), NEAR, initial-token (^), and column-filter (title:foo) operators with a named error (STORE_CAPABILITY_MISSING) that says how to rephrase or set \"store\" to \"sqlite\"; bare words and quoted phrases work on both. Raw SQL is a per-store dialect: the tables and the has/basename/segment functions are portable, but sqlite's FTS5 MATCH, snippet(), and bm25() do not run under duckdb (its fts extension has its own functions), so saved queries written in FTS5 syntax are sqlite dialect. Each store keeps its own cache file (.sense/cache.db, .sense/cache.duckdb); switching stores rebuilds the index rather than migrating it."
|
|
119
|
+
},
|
|
115
120
|
"queries": {
|
|
116
121
|
"type": "object",
|
|
117
122
|
"description": "Saved queries runnable as `sense <name> [params...]`, each naming the verb it runs, one to one with the two commands: `{ sql }` runs like `sense sql`, `{ search }` like `sense search`. A bare string is rejected -- it silently meant SQL. `{ sql }`: `?` placeholders bind to CLI positional args in order; deterministic and enumerating, including raw FTS5 `MATCH` for word search under your own SQL -- \"0 rows = not in the tree\" lives here. Tables: `frontmatter` (one row per file, one column per discovered frontmatter key, plus `path`/`_mtime`/`_size`/`_rank`/`_parse_error`, the last being NULL when the frontmatter parsed and the YAML message when it did not, in which case no other column is populated), `content` (FTS5: `title`, `summary`, `text`, `path`, plus machine-written `title_seg`/`summary_seg`/`text_seg` sidecars holding the grapheme phrases for Chinese, Japanese, Thai, Khmer, Lao, and Burmese text -- present for matching, not reading; a hand-written `MATCH` reaches them through `segment()`), `links` (`src`, `target`, `dst`), `sections` (`path`, `idx`, `level`, `heading`, `start_line`, `end_line`, `tokens`), and `preset_files` (`preset`, `path`) -- which presets cover which files. `has(field, value)`: array membership on a JSON-array field, substring match on a string (so has(f.status, 'active') also matches 'inactive'), false on NULL. `segment(terms)`: rewrites a run of unspaced-script text in `terms` into the ordered grapheme phrase the sidecar columns need; text with no such run passes through unchanged, so it is safe to add to any query. Exact matches: `=` for scalars, `EXISTS (SELECT 1 FROM json_each(f.tags) WHERE value = ?)` for array members. Canonical query: `SELECT f.path, content.title, content.summary, CASE WHEN length(content.text) <= 16384 THEN snippet(content, 2, '«', '»', '…', 10) END AS hit FROM frontmatter f JOIN content ON content.path = f.path WHERE content MATCH ? ORDER BY bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0) LIMIT 10`. snippet() names column 2 (`text`) explicitly rather than -1 (best column), since -1 could surface a sidecar as the excerpt. bm25's full form repeats the three weights onto the sidecars so a title hit reached through `title_seg` ranks like one reached through `title`; the plain `bm25(content, 10.0, 5.0, 1.0)` still runs (FTS5 defaults unnamed columns to 1.0) but ranks a sidecar match at body weight. The CASE bounds snippet(), which re-tokenizes each matched document and costs seconds per query once a tree holds a megabyte-scale note; `search` applies the same bound internally. Saved search object: one text driving every engine the scoped preset has -- word match, links, vectors -- fused into one ranked list, `via` labeling which engine produced each row; `search` text must be non-empty (a saved query saves a question -- a scope without one is just flags). `preset` names one declared preset (defaults to `default`); `include` is an ad hoc glob scope that replaces the preset's include/exclude entirely, same as `search --include`; `where` and `k` behave like the `search` command's flags. `sense <name>` behaves like `sense search <search> [--preset] [--include] [--where] [--k]` with zero flags; it takes no positional parameters. Running an entry validates it: a typo'd column or unknown preset errors and exits nonzero, and a parameterised entry validates with any argument, since SQL is prepared before parameters bind. Nothing asserts on the result itself; a returned row set is the reader's judgment. Reserved frontmatter keys: `path`, `_mtime`, `_size`, `_rank`, `_parse_error`, `content`, `links`, `sections`. Reserved names (unreachable as saved queries, any shape): `init`, `sql`, `search`, `map`, `peek`, `path`, `related`, `download`, `watch`, `status`.",
|
package/skills/sense/EXAMPLES.md
CHANGED
|
@@ -43,8 +43,8 @@ sense peek notes/architecture-decisions.md
|
|
|
43
43
|
notes/architecture-decisions.md (~4400 tokens)
|
|
44
44
|
title: Architecture decisions
|
|
45
45
|
sections:
|
|
46
|
-
## Storage layer
|
|
47
|
-
## Queue
|
|
46
|
+
## Storage layer: why SQLite [L41-88, ~610t]
|
|
47
|
+
## Queue: rejected options [L89-120, ~380t]
|
|
48
48
|
...
|
|
49
49
|
links out (7): notes/pricing-model.md, ...
|
|
50
50
|
backlinks (2): notes/_index.md, notes/roadmap.md
|
|
@@ -65,7 +65,7 @@ sense sql "SELECT path, round(_rank*100,2) r FROM frontmatter ORDER BY _rank DES
|
|
|
65
65
|
## E. Cold start on an unknown notes tree
|
|
66
66
|
|
|
67
67
|
```
|
|
68
|
-
sense map # fields, hubs, recent
|
|
68
|
+
sense map # fields, hubs, recent. Read this first
|
|
69
69
|
sense --list # named queries someone already saved
|
|
70
70
|
sense sql "SELECT DISTINCT type FROM frontmatter" # what a field's values are
|
|
71
71
|
```
|
package/skills/sense/SKILL.md
CHANGED
|
@@ -5,14 +5,16 @@ description: "Query a markdown tree with the sense CLI: filter notes by frontmat
|
|
|
5
5
|
|
|
6
6
|
# sense
|
|
7
7
|
|
|
8
|
-
SQL over a markdown tree, kept fresh by a filesystem check on every query. Every file becomes rows in `frontmatter` (one column per key, plus `path`/`_mtime`/`_ctime`/`_size`/`_rank`/`_parse_error`; `_ctime` is filesystem birthtime, which a clone or copy resets just like `_mtime`), `content` (
|
|
8
|
+
SQL over a markdown tree, kept fresh by a filesystem check on every query. Every file becomes rows in `frontmatter` (one column per key, plus `path`/`_mtime`/`_ctime`/`_size`/`_rank`/`_parse_error`; `_ctime` is filesystem birthtime, which a clone or copy resets just like `_mtime`), `content` (`title`, `summary`, `text`, `path`; an FTS5 index on the default store, with machine-written `title_seg`/`summary_seg`/`text_seg` sidecars used for matching Chinese, Japanese, Thai, Khmer, Lao, and Burmese text, not for reading), `links` (`src`, `target`, `dst`, `embed`; `NULL` dst = dead link; `embed` 1 for `![[...]]` embeds, 0 for links; one row per distinct written target and kind with alias and anchor stripped, so `[[Foo]]` and `[[Foo|alias]]` are one row while `[[Foo]]` and `[[notes/Foo]]` are two rows that can share a `dst`, and a target both linked and embedded is a row of each kind; extraction matches Obsidian's own graph: comments, code, and link-syntax text yield no rows, `[[#Anchor]]` is a self-edge, a frontmatter value that is exactly `[[X]]` is a link, and a basename collision resolves to the linking note itself, else the shortest path), `tags` (`path`, `tag`: frontmatter and inline `#tags` merged and deduplicated; nested tags stored full, so `book/scifi` matches `tag = 'book' OR tag LIKE 'book/%'`), `sections` (heading outline with line ranges and token estimates), and `preset_files` (`path`, `preset`: which presets cover which files). Features add their own storage; `map` and `status` report which are on.
|
|
9
|
+
|
|
10
|
+
**Stores.** The config's `store` key picks the backing store: `sqlite` (default, zero-dependency, Node's built-in SQLite) or `duckdb` (experimental; the first command that opens a duckdb tree installs `@duckdb/node-api` on its own, a one-time native download of about 110 MB). The tables, `?` placeholders, quoted identifiers, the `scope` binding, and the `has`/`basename`/`segment` functions are the same on both, so ordinary frontmatter SQL ports as written. What does not port is FTS5: `content` is an FTS5 table on sqlite and a plain table on duckdb, so hand-written `MATCH`, `snippet()`, `bm25()`, and sqlite's date-function forms run only on sqlite (duckdb has its own fts functions and date syntax), and `search` text under duckdb rejects FTS5's prefix (`foo*`), boolean (`AND`/`OR`/`NOT`), `NEAR`, initial-token (`^`), and column-filter (`title:foo`) operators with a named error (STORE_CAPABILITY_MISSING) that says how to rephrase or set `store` to `sqlite`; bare words and quoted phrases work on both stores.
|
|
9
11
|
|
|
10
12
|
## What each tool is for
|
|
11
13
|
|
|
12
14
|
Every result is a reference (path, metadata, excerpt), never file contents; prose enters context only when you Read it. Costs: `map` is fixed-size, a `search` row is tens of tokens, and a `peek` stays flat however large the note is. Which tool fits is a property of the question:
|
|
13
15
|
|
|
14
16
|
- A deterministic, factual answer over known fields (counts, filters, "which notes have X") is SQL: `sense sql`, a saved `{ sql }` entry, or `search --where`. Enumerates every match; same result regardless of phrasing.
|
|
15
|
-
- Locating notes about something is `search`, one text through every engine the scope has: word match (bare words AND-join, one absent word = zero lexical rows; write `a OR b OR c` for any-word), link-graph expansion, and vector similarity, fused into one ranked list. Read `via` per row: `match` rows contained your words; `vector`-only rows did not. A `vector`-only row means the search words don't appear in that note; it showed up because the model judged it semantically related. Vector rows are conceptual similarity, not typo-tolerance; false positives are expected, labeled, and bounded by `--k
|
|
17
|
+
- Locating notes about something is `search`, one text through every engine the scope has: word match (bare words AND-join, one absent word = zero lexical rows; write `a OR b OR c` for any-word), link-graph expansion, and vector similarity, fused into one ranked list. Read `via` per row: `match` rows contained your words; `vector`-only rows did not. A `vector`-only row means the search words don't appear in that note; it showed up because the model judged it semantically related. Vector rows are conceptual similarity, not typo-tolerance; false positives are expected, labeled, and bounded by `--k`, and they are the only rows a search can produce when note and query share no vocabulary at all, the paraphrase and category-for-instance cases words cannot reach. A scope searches with vectors when its preset's `signals` include `vectors` (on by default whenever the tree names an `embed` model); a preset that declares `"signals": {"words": 1, "links": 1}` searches on words and links only. A preset that asks for vectors when a local model path is missing its files is an error naming the fix, not a quieter result that would make the same search answer differently before and after.
|
|
16
18
|
- `map` answers "what is this tree" (fields, hub notes, recent changes) when the tree is unfamiliar.
|
|
17
19
|
- `peek <path>` prices a file before you pay for it: outline with `[L143-162, ~380t]` ranges and links both ways. Every list shows its first 20 with the true total; the `sections` and `links` tables hold the rest, so a peek costs a few hundred tokens on any note. Its link totals count distinct notes: "links out" dedupes written targets by resolved note and lists unresolved targets separately, while `COUNT(*) FROM links WHERE src = ?` counts every written target, resolved or not, so the raw count can read higher without either number being wrong.
|
|
18
20
|
- `path <a> <b>` walks the link graph for a chain connecting two notes, or reports none within the depth bound: it answers how they connect, not just that both exist.
|
|
@@ -27,11 +29,11 @@ Output defaults to a table, built for humans; `--format json` returns the same r
|
|
|
27
29
|
|
|
28
30
|
| signal | reads | uniquely finds | blind to |
|
|
29
31
|
|---|---|---|---|
|
|
30
|
-
| `words` | the text itself (BM25) | every literal occurrence: identifiers, error strings, names, exact phrases; deterministic | anything phrased differently
|
|
32
|
+
| `words` | the text itself (BM25) | every literal occurrence: identifiers, error strings, names, exact phrases; deterministic | anything phrased differently, e.g. a note saying "compensation floor" for the query "minimum pay" |
|
|
31
33
|
| `links` | connections authors wrote | what the tree's own structure treats as related: context the matching text never restates | notes nobody linked |
|
|
32
|
-
| `vectors` | per-chunk meaning-vectors | notes sharing no words with the query: paraphrase, the category for an instance, a concept restated | proving presence
|
|
34
|
+
| `vectors` | per-chunk meaning-vectors | notes sharing no words with the query: paraphrase, the category for an instance, a concept restated | proving presence. A vector row cannot show the words occur anywhere; `hit` can |
|
|
33
35
|
|
|
34
|
-
Each signal in the map carries a weight (`{"words": 1, "links": 1, "vectors": 1}` is the default when the key is omitted entirely); presence turns a signal on, and the number scales its share of the fused ranking. Equal weight (1 for everything) is what every number elsewhere in this doc describes. A weight above 1 pulls the fused ranking toward that signal's own ordering: measured on nfcorpus with the default static model and on MIRACL zh with an HTTP encoder, equal-weight fusion helps English nfcorpus but ranks the encoder's MIRACL-zh results below its own cosine-only ranking (benchmark/reports/2026-08-27-embedding-model-selection.md, weight-sweep table)
|
|
36
|
+
Each signal in the map carries a weight (`{"words": 1, "links": 1, "vectors": 1}` is the default when the key is omitted entirely); presence turns a signal on, and the number scales its share of the fused ranking. Equal weight (1 for everything) is what every number elsewhere in this doc describes. A weight above 1 pulls the fused ranking toward that signal's own ordering: measured on nfcorpus with the default static model and on MIRACL zh with an HTTP encoder, equal-weight fusion helps English nfcorpus but ranks the encoder's MIRACL-zh results below its own cosine-only ranking (benchmark/reports/2026-08-27-embedding-model-selection.md, weight-sweep table). There is no single weight that is right for both, so a preset that leans on a strong encoder is a candidate for a higher `vectors` weight, checked against that table rather than assumed.
|
|
35
37
|
|
|
36
38
|
Which signal carries a search is a property of the query, and the ends of the range are measured (BENCHMARKING.md, "Retrieval quality"): on the vocabulary-gap corpus, 31% of queries have no relevant lexical row in their top 10 and vector rows are the only recall; on a corpus whose queries quote their documents, words alone hit 99.7% and vectors add nothing. Real trees sit between. An exact identifier is words territory; "notes about X" where X could be phrased many ways is where vector rows carry; "how do these connect" is the link graph (`path`, `peek`).
|
|
37
39
|
|
|
@@ -52,7 +54,7 @@ sense --list | status | download
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
- Terms pass verbatim to FTS5 MATCH. Bare words AND-join (one absent word means zero rows), so write `OR` yourself when you want any-word matching; double-quote punctuated terms (`"customer-facing"`, `"founder's"`); invalid syntax is an error, not a rewrite. The same rules apply to search commands you write into subagent briefs.
|
|
55
|
-
- When a search misses, both sides have levers. Lexical: OR-in synonyms and concrete instances (the index only knows the words in the files; a note about a specific tool rarely names its category), raise `--k` (a row costs tens of tokens), widen the scope (`--preset`, or `--include` for an ad-hoc glob). Vector: restate the concept in different words
|
|
57
|
+
- When a search misses, both sides have levers. Lexical: OR-in synonyms and concrete instances (the index only knows the words in the files; a note about a specific tool rarely names its category), raise `--k` (a row costs tens of tokens), widen the scope (`--preset`, or `--include` for an ad-hoc glob). Vector: restate the concept in different words. Vectors rank the meaning of the whole query text, so a rephrase moves them even when every literal word still misses. Then pivot through the nearest hit with `related <path>` to walk its meaning-neighbourhood. Vector-only rows in a miss are themselves evidence: `similarity` and the snippet say whether the concept exists in the tree under other words. Each widening adds candidates and dilutes ranking, so the noise trade-off runs both ways.
|
|
56
58
|
- A frontmatter query enumerates its matches deterministically; search ranks by term overlap, so results shift as phrasing shifts. Trade-off: a query needs a known field, search doesn't.
|
|
57
59
|
- The `via` column says what produced each row: `match` (words hit), `link` (connected to notes that hit), `vector` (near in meaning), and combinations. The `lines` column, when set, points at the section that earned the row (the best-matching chunk on vector rows, the term cluster's section on large lexical notes) and is a direct `Read` range; null means the whole note is the reference.
|
|
58
60
|
- Scope is one vocabulary shared by `search`, `map`, `peek`, `path`, and `related`: bare command uses the config's `default` preset; `--preset <name>` picks another (unknown names error, listing what's declared); `--include <glob>` and `--exclude <glob>` are ad-hoc globs for one command, each overriding its own side of the preset, so one does not clear the other; `--no-exclude` drops the preset's `exclude` for one command, the only way to widen past it without editing config (it widens the query scope, not the index: a file no preset covers is never indexed). `--where` takes any SQL condition against frontmatter alias `f`, not only field equality: `"f.status = 'active' AND has(f.tags, 'x')"`, `"datetime(f.created) >= datetime(?)"`. There is no whole-index flag: a broad `default` preset, or a declared `all` preset (`include ["**/*"]`), is the whole tree. `sense status` shows every preset with its coverage. `sql` scopes differently: it runs over the whole index by default, and `--preset <name>` *binds* the scope as a temporary `scope(path)` table your statement joins, rather than filtering behind the query's back (`JOIN scope ON scope."path" = f.path`). Naming a preset without joining `scope` is a usage error, since it would return everything while reading as scoped. Without the flag, join `preset_files` directly, which is the same coverage under a preset name you write into the SQL.
|
|
@@ -108,13 +110,13 @@ WHERE a.dst = ? AND b.dst IS NOT NULL AND b.dst <> a.dst;
|
|
|
108
110
|
|
|
109
111
|
- A saved `{ sql }` written against `scope` is preset-agnostic: `sense <name> --preset raw` re-points the same statement at another layer, so one entry serves every preset instead of one copy each.
|
|
110
112
|
- `content MATCH` only works against the fts5 table by its own name, never through an alias or a view: `FROM content c ... WHERE c MATCH 'x'` fails with `no such column: c`. This is why `--preset` binds a table to join rather than shadowing the tables.
|
|
111
|
-
- `content MATCH` takes FTS5 syntax: `a OR b`, `"phrase"`, `pref*`, `NEAR(a b, 5)`, `summary: term`. Stemmed; markdown stripped at index time. Double-quote any term with punctuation. Bare `customer-facing` errors (`-` reads as a column filter), bare apostrophes are syntax errors: write `"customer-facing"`, `"founder's"`.
|
|
112
|
-
- A language written without word spaces (Chinese, Japanese, Thai, Khmer, Lao, Burmese) is indexed per grapheme and searched as an ordered grapheme phrase against the `_seg` sidecar columns: substring semantics, what `grep` gives
|
|
113
|
-
- Rank with `ORDER BY bm25(content, 10.0, 5.0, 1.0)` (title > summary > body); the full form `bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)` mirrors the same weights onto the `_seg` sidecars, so a title hit found through `title_seg` ranks like one found through `title` (the three-weight form still runs
|
|
113
|
+
- `content MATCH` takes FTS5 syntax: `a OR b`, `"phrase"`, `pref*`, `NEAR(a b, 5)`, `summary: term`. Stemmed; markdown stripped at index time. Double-quote any term with punctuation. Bare `customer-facing` errors (`-` reads as a column filter), bare apostrophes are syntax errors: write `"customer-facing"`, `"founder's"`. This is the default sqlite store's grammar: under `duckdb` the operator forms (`a OR b`, `pref*`, `NEAR`, `^`, column filters) are a named error naming the rephrase, and `MATCH` itself does not run (see Stores).
|
|
114
|
+
- A language written without word spaces (Chinese, Japanese, Thai, Khmer, Lao, Burmese) is indexed per grapheme and searched as an ordered grapheme phrase against the `_seg` sidecar columns: substring semantics, what `grep` gives, a query matches wherever its exact text occurs, including inside a longer run (`京都` matches `东京都政府`, correctly, because it's there at position 2), and needs no minimum length. No decision is needed for these languages. Hand-written SQL is not rewritten for you, so a raw `content MATCH '数据库'` finds nothing: write `content MATCH segment(?)` and bind the terms. `segment()` returns text with no such run unchanged, so it is safe to leave in a query whatever the tree's language.
|
|
115
|
+
- Rank with `ORDER BY bm25(content, 10.0, 5.0, 1.0)` (title > summary > body); the full form `bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)` mirrors the same weights onto the `_seg` sidecars, so a title hit found through `title_seg` ranks like one found through `title` (the three-weight form still runs, FTS5 defaults unnamed columns to 1.0, but ranks a sidecar match at body weight). Excerpt with `snippet(content, 2, '«', '»', '…', 10)`, naming the `text` column explicitly: `-1` means best column, which can surface a machine-spaced sidecar as the excerpt (`search` itself always names column 2). snippet() re-tokenizes each matched doc and its cost grows superlinearly with doc size, measured ~10 s per query on a tree holding one 1 MB note. `search` bounds this itself (docs past 16 KB get an equivalent excerpt another way); in hand-written SQL, guard it: `CASE WHEN length(text) <= 16384 THEN snippet(...) END`, or select `title`/`summary` instead of an excerpt.
|
|
114
116
|
- Select `content.title`/`content.summary` (always exist, empty when absent) rather than `f.title`/`f.summary` (discovered columns; error on trees that never declare them).
|
|
115
|
-
- Frontmatter values keep their YAML type: strings are TEXT, whole numbers and booleans are INTEGER (`true` stores as 1, so `WHERE flag = 1` matches and `WHERE flag = 'true'` matches nothing), fractions are REAL, lists and maps are JSON text. `map` prints the observed type per field, and a field showing two types (`integer,text`) has drifted across notes. A list key written with no items (`tags:` above a bare `-`) is a list holding one null, stored as the JSON text `[null]`: `IS NULL` does not find it (the column holds a string), `json_each` yields one empty member per such row, and `map` counts it as covered because the key is present. `has(tags, 'x')` reads it correctly as no match. To separate written-but-empty from absent, compare against the text: `WHERE tags = '[null]'`.
|
|
116
|
-
- **Dead links need the attachment filter.** `dst IS NULL` alone is not "broken link": a wikilink to anything that is not markdown (`[[Board.base]]`, `![[Pasted image.png]]`, `[[spec.pdf]]`) can never resolve, because sense indexes markdown and resolution only tries the exact path or `+.md`. Those are out of the index's universe, not broken. On a 1,400-note Obsidian vault the unfiltered query returns 143 rows where 14 are real. Exclude anything carrying a file extension, as in the recipe above, and widen the exclusion if your notes have dotted titles (`[[Node.js]]` carries one too, so a stricter list
|
|
117
|
-
- `has(field, value)`: array membership on JSON-array fields, substring on strings, false on NULL. This is the `includes()` convention. Substring means `has(f.status, 'active')` also matches `inactive`; exact scalar match is `f.status = ?`, deliberate substring is `LIKE`, exact array membership is `EXISTS (SELECT 1 FROM json_each(f.tags) WHERE value = ?)`. To aggregate per member instead, use `json_each(frontmatter.<field>)` (above)
|
|
117
|
+
- Frontmatter values keep their YAML type: strings are TEXT, whole numbers and booleans are INTEGER (`true` stores as 1, so `WHERE flag = 1` matches and `WHERE flag = 'true'` matches nothing), fractions are REAL, lists and maps are JSON text. On the `duckdb` store the discovered columns are VARIANT: homogeneous keys, which are nearly all of them, compare identically, but a numeric predicate against a key that holds numbers in some notes and text in others raises a comparison error where sqlite orders by storage class silently. `map` prints the observed type per field, and a field showing two types (`integer,text`) has drifted across notes. A list key written with no items (`tags:` above a bare `-`) is a list holding one null, stored as the JSON text `[null]`: `IS NULL` does not find it (the column holds a string), `json_each` yields one empty member per such row, and `map` counts it as covered because the key is present. `has(tags, 'x')` reads it correctly as no match. To separate written-but-empty from absent, compare against the text: `WHERE tags = '[null]'`.
|
|
118
|
+
- **Dead links need the attachment filter.** `dst IS NULL` alone is not "broken link": a wikilink to anything that is not markdown (`[[Board.base]]`, `![[Pasted image.png]]`, `[[spec.pdf]]`) can never resolve, because sense indexes markdown and resolution only tries the exact path or `+.md`. Those are out of the index's universe, not broken. On a 1,400-note Obsidian vault the unfiltered query returns 143 rows where 14 are real. Exclude anything carrying a file extension, as in the recipe above, and widen the exclusion if your notes have dotted titles (`[[Node.js]]` carries one too, so a stricter list, `'*.png'`, `'*.pdf'`, `'*.base'`, and whatever else your vault attaches, is safer on a tree whose titles use dots). Scope it with `preset_files` as well: template and skill files are full of `[[Note Name]]` examples that are deliberately unresolved.
|
|
119
|
+
- `has(field, value)`: array membership on JSON-array fields, substring on strings, false on NULL. This is the `includes()` convention. Substring means `has(f.status, 'active')` also matches `inactive`; exact scalar match is `f.status = ?`, deliberate substring is `LIKE`, exact array membership is `EXISTS (SELECT 1 FROM json_each(f.tags) WHERE value = ?)`. To aggregate per member instead, use `json_each(frontmatter.<field>)` (above). GROUP BY on the raw column splits `["a","b"]` and `["b","a"]` into separate buckets.
|
|
118
120
|
- Compare dates through `datetime()`, which resolves ISO 8601 offsets to UTC: `WHERE datetime(created) >= datetime(?)`. Bare string comparison is only safe when every note uses the same offset.
|
|
119
121
|
- Date spellings SQLite rejects (`-0800`, `-08`, a space separator) are normalized at index time, offset preserved. One it cannot fix is left as written and warned about by path: `datetime()` returns NULL there, so the row is invisible to a date comparison rather than excluded by it. List them with `WHERE d IS NOT NULL AND datetime(d) IS NULL`.
|
|
120
122
|
- **SQLite's `now` is UTC, so any query about "today" needs `'localtime'`.** `date('now')` reads as tomorrow from mid-afternoon onward in the Americas, which silently flips "scheduled today" into "overdue" every evening: write `date('now','localtime')` and `datetime('now','start of day','localtime')`. This only matters where the boundary carries the meaning; a `'-90 day'` window is unaffected by a few hours of skew.
|
|
@@ -128,7 +130,7 @@ Worked traces: [EXAMPLES.md](EXAMPLES.md).
|
|
|
128
130
|
- `map` and `status` report each preset's coverage (files matched, embedded count). Indexing derives from presets, so the coverage numbers are how you see what a config actually indexes and embeds. A scope with fewer signals just uses fewer (a preset without the vectors signal searches lexically); a saved search naming an unknown preset errors when run, listing the declared ones.
|
|
129
131
|
- Save a query into `sense.config.json` only when it will be reused; run ad-hoc otherwise.
|
|
130
132
|
- A one-line `summary:` per note is optional and pays twice: it appears in result rows and is a weighted search field. Date comparisons work for dates written as ISO 8601 (`2026-08-12`, or with time and offset); other formats do not compare. Field names in examples (`status`, `tags`, `created`) are illustrative; your tree defines its own.
|
|
131
|
-
- Reserved frontmatter keys (dropped with a warning): `path`, `_mtime`, `_ctime`, `_size`, `_rank`, `_parse_error`, `content`, `links`, `sections`. The `tags` frontmatter column and the `tags` table coexist, mirroring Obsidian's own split: the column is the raw YAML list one note's frontmatter declares (Obsidian's `tags` property), the table is the merged, deduplicated frontmatter+inline set per note (what Obsidian's tag pane and Bases' `file.tags` read). "What is tagged X" is a table query; the column answers only what a note's frontmatter literally says. Inline tags inside `%%...%%` comments are indexed
|
|
133
|
+
- Reserved frontmatter keys (dropped with a warning): `path`, `_mtime`, `_ctime`, `_size`, `_rank`, `_parse_error`, `content`, `links`, `sections`. The `tags` frontmatter column and the `tags` table coexist, mirroring Obsidian's own split: the column is the raw YAML list one note's frontmatter declares (Obsidian's `tags` property), the table is the merged, deduplicated frontmatter+inline set per note (what Obsidian's tag pane and Bases' `file.tags` read). "What is tagged X" is a table query; the column answers only what a note's frontmatter literally says. Inline tags inside `%%...%%` comments are indexed, and some trees run their whole maintenance-tag system in comments.
|
|
132
134
|
- A note whose frontmatter does not parse is indexed with **no** frontmatter columns and `_parse_error` set to the YAML message, which carries the line. Nothing is half-recovered: a non-NULL value is a value the author wrote. So a NULL column means the key was absent *or* the note did not parse, and `_parse_error` is how you tell: `WHERE status IS NULL AND _parse_error IS NULL` is "genuinely missing status". List what needs fixing with `sense sql "SELECT path, _parse_error FROM frontmatter WHERE _parse_error IS NOT NULL"`; fixing a file clears it on the next command. `sense status` reports the count.
|
|
133
|
-
- Exit codes: `0` ok, `1` error (
|
|
135
|
+
- Exit codes: `0` ok, `1` error (store message verbatim), `2` usage (unknown query, wrong param count).
|
|
134
136
|
- Doubted cache: delete the directory `sense status` prints on its `cache:` line. Rarely needed; every query reconciles first.
|
|
@@ -91,13 +91,13 @@ LIMIT 20
|
|
|
91
91
|
|
|
92
92
|
Run as `sense sql "..." current-note.md`, or saved with the `?` in place and the path passed
|
|
93
93
|
as the parameter. The `or:` block reproduces Obsidian's semantics exactly: rows qualify by
|
|
94
|
-
overlap, by linking to the note, or by being linked from it
|
|
94
|
+
overlap, by linking to the note, or by being linked from it, so leaf notes the current note
|
|
95
95
|
links to appear even at zero overlap, as they do in Obsidian.
|
|
96
96
|
|
|
97
97
|
## Notes that recur across translations
|
|
98
98
|
|
|
99
99
|
- Multiple views over the same base share the base-level filter; write it once per query
|
|
100
|
-
rather than factoring it out
|
|
100
|
+
rather than factoring it out. Saved queries are self-contained by design.
|
|
101
101
|
- `sort` keys referencing formulas sort by the SELECT alias.
|
|
102
102
|
- A `groupBy` view keeps its rows; see the window-function shape in SKILL.md. Only a
|
|
103
103
|
deliberately collapsed report wants `GROUP BY`.
|
|
@@ -8,7 +8,7 @@ description: "Translate an Obsidian Bases .base file into sense SQL that returns
|
|
|
8
8
|
A `.base` file is YAML: filters selecting notes, formulas computing values, and views ordering
|
|
9
9
|
and grouping them. Obsidian evaluates it against its own metadata cache; sense holds the same
|
|
10
10
|
data in SQL tables. Every Bases construct that selects or computes rows has a SQL equivalent.
|
|
11
|
-
What has none is presentation (`columnSize`, card layout)
|
|
11
|
+
What has none is presentation (`columnSize`, card layout), those change pixels, not rows, so
|
|
12
12
|
a translation loses nothing by ignoring them.
|
|
13
13
|
|
|
14
14
|
Translate one view to one query: base-level `filters` AND the view's `filters`, the view's
|
|
@@ -64,7 +64,7 @@ did, and `SELECT name FROM pragma_table_info('frontmatter')` lists what exists.
|
|
|
64
64
|
`isEmpty()` has four true cases because empty is stored three ways: NULL (key absent), `''`
|
|
65
65
|
(empty string value), `'[]'` (a list written `[]`), and `'[null]'` (a list key above a bare
|
|
66
66
|
`-`). Obsidian's `isEmpty()` is true for all of them; `IS NULL` alone finds only the first.
|
|
67
|
-
The IN list is the whole test
|
|
67
|
+
The IN list is the whole test. `json_array_length()` is not: it reads `'[null]'` as length
|
|
68
68
|
1 and throws on plain strings. The same trap inside a list: `json_each` hands a string member
|
|
69
69
|
to `value` as plain text, so `json_type(value)` throws `malformed JSON` on it; the scan's own
|
|
70
70
|
`type` column is the discriminator.
|
|
@@ -103,12 +103,12 @@ A formula referencing another formula becomes a CTE layer: SQL cannot read a SEL
|
|
|
103
103
|
the same SELECT list, so each dependency level computes its formulas as columns and the next
|
|
104
104
|
level reads them (`WITH t AS (SELECT ..., <level-1 formulas> FROM frontmatter) SELECT ...,
|
|
105
105
|
<level-2 formulas> FROM t`). A five-formula chain is however many *levels* it has, not five
|
|
106
|
-
CTEs
|
|
106
|
+
CTEs. Formulas that only read base columns share one layer.
|
|
107
107
|
|
|
108
108
|
## Views
|
|
109
109
|
|
|
110
110
|
- `sort:` (multi-key, each with direction) -> `ORDER BY a DESC, b ASC`. `limit:` -> `LIMIT`.
|
|
111
|
-
- `groupBy` does not collapse rows
|
|
111
|
+
- `groupBy` does not collapse rows. Obsidian shows every row bucketed under headers with
|
|
112
112
|
per-group summaries. The SQL producing the same rows and numbers is ordering plus window
|
|
113
113
|
functions, not GROUP BY:
|
|
114
114
|
|
|
@@ -120,7 +120,7 @@ CTEs -- formulas that only read base columns share one layer.
|
|
|
120
120
|
ORDER BY f.status, f.days DESC
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
A collapsed one-row-per-group report is plain `GROUP BY
|
|
123
|
+
A collapsed one-row-per-group report is plain `GROUP BY`. That is a different result than
|
|
124
124
|
the Bases view shows.
|
|
125
125
|
- View `summaries` (Sum, Average, Median via ordering, Unique, Filled, Checked) are the
|
|
126
126
|
matching aggregates, windowed as above to keep the rows, or a separate aggregate query.
|
|
@@ -128,7 +128,7 @@ CTEs -- formulas that only read base columns share one layer.
|
|
|
128
128
|
## `this`
|
|
129
129
|
|
|
130
130
|
`this` is the note the base is evaluated against: the embedding note, or Obsidian's active
|
|
131
|
-
pane. sense has no pane, so the caller supplies the path as a bound parameter
|
|
131
|
+
pane. sense has no pane, so the caller supplies the path as a bound parameter: `sense sql
|
|
132
132
|
"..." <path>`, or a saved query run as `sense <name> <path>`. A CTE keeps it single-bind:
|
|
133
133
|
|
|
134
134
|
```sql
|