sensemaking 0.1.0 → 0.2.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 CHANGED
@@ -18,12 +18,13 @@ step, or you re-explaining anything.
18
18
 
19
19
  ## How it works
20
20
 
21
- Every file becomes a row; every frontmatter key becomes a column. You write named SQL queries once
22
- and run them by name.
21
+ Every file becomes a row; every frontmatter key becomes a column; the prose becomes a full-text
22
+ index you can join against. You write named SQL queries once and run them by name.
23
23
 
24
24
  ```markdown
25
25
  ---
26
26
  title: Ship the Q3 report
27
+ summary: where the Q3 numbers came from and who signed off
27
28
  status: active
28
29
  tags: [urgent, reports]
29
30
  ---
@@ -31,6 +32,20 @@ tags: [urgent, reports]
31
32
  Notes about the report…
32
33
  ```
33
34
 
35
+ ```sql
36
+ -- active notes that actually discuss revenue, best match first
37
+ SELECT f.path, content.title, content.summary, snippet(content, -1, '«', '»', '…', 10) AS hit
38
+ FROM frontmatter f JOIN content ON content.path = f.path
39
+ WHERE f.status = 'active' AND content MATCH 'revenue'
40
+ ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10
41
+ ```
42
+
43
+ The filter shrinks the haystack, the search finds the needle, and each row that comes back —
44
+ path, title, summary, matching excerpt — is enough to decide whether to open the file, without
45
+ carrying the file. On a real 26-note vault that's a few hundred tokens, against ~6,000 to read the
46
+ three files it points at. Cheap enough to run before deciding what to open; reading afterward is
47
+ the expensive step, and it happens through the filesystem, not SQL.
48
+
34
49
  Two properties make it trustworthy:
35
50
 
36
51
  - **Never stale.** Every query re-checks the filesystem first, re-reading only what changed. The
@@ -43,11 +58,20 @@ Two properties make it trustworthy:
43
58
 
44
59
  ```bash
45
60
  npm install -g sensemaking
46
- cd your-notes && sense init # writes a starter sense.config.json
61
+ cd your-notes && sense init # writes a minimal sense.config.json (globs only, no queries)
62
+ ```
63
+
64
+ Query immediately — ad-hoc SQL needs no config beyond the globs:
65
+
66
+ ```
67
+ sense query "SELECT … FROM frontmatter" # ad-hoc SQL; positional args bind to ? placeholders
68
+ sense query "…" --format json # structured output (the default is a table)
69
+ sense --list # what named queries exist
70
+ sense status | rebuild # cache info / delete .sense/ and re-crawl
47
71
  ```
48
72
 
49
- Edit the queries to match your own frontmatter (the `$schema` line gives your editor autocomplete
50
- and validation):
73
+ When a query proves worth reusing, name it in `sense.config.json` (plain JSON; the `$schema` line
74
+ gives your editor autocomplete and validation) and run it as `sense <name> [params...]`:
51
75
 
52
76
  ```json
53
77
  {
@@ -55,28 +79,37 @@ and validation):
55
79
  "version": 1,
56
80
  "scan": { "include": ["**/*.md"] },
57
81
  "queries": {
58
- "all": "SELECT path, title FROM docs ORDER BY path",
59
- "by-tag": "SELECT path, title FROM docs WHERE has(tags, ?) ORDER BY path"
82
+ "by-tag": "SELECT path, title, summary FROM frontmatter WHERE has(tags, ?) ORDER BY path"
60
83
  }
61
84
  }
62
85
  ```
63
86
 
64
- ```
65
- sense all # run a named query
66
- sense by-tag urgent # positional args bind to ? placeholders (count-checked)
67
- sense query "SELECT … FROM docs" # ad-hoc SQL for one-off questions, no config edit
68
- sense --list # what queries exist
69
- sense <name> --format json # structured output (the default is a table)
70
- sense status | rebuild # cache info / delete .sense/ and re-crawl
71
- ```
72
-
73
87
  Discovery walks up from your cwd git-style, so you can run `sense` from anywhere in the tree
74
88
  (`--config <path>` overrides). Exit codes: `0` ok, `1` real error (SQLite's message verbatim),
75
89
  `2` usage error.
76
90
 
77
91
  The one custom SQL function is `has(field, value)`: array membership on a JSON-array field (like
78
- `tags`), substring match on a string, always false on a missing key. Reserved columns: `path`,
79
- `_mtime`, `_size`.
92
+ `tags`), substring match on a string, always false on a missing key. Reserved names: `path`,
93
+ `_mtime`, `_size`, `content`.
94
+
95
+ ### Content search
96
+
97
+ `content` is an FTS5 table with columns `title`, `summary`, `text`, and `path` (for the join).
98
+ `content MATCH ?` takes FTS5 syntax (`a OR b`, `"exact phrase"`, `pref*`, `NEAR(a b, 5)`,
99
+ `summary: term`), with stemming on so `negotiate` matches "negotiating". Markdown syntax is
100
+ stripped at index time — search for the words, not the formatting around them, and excerpts come
101
+ back as clean prose. `bm25()` weights follow column order, so `bm25(content, 10.0, 5.0, 1.0)`
102
+ ranks a title hit above a passing mention; `snippet(content, -1, …)` excerpts whichever column
103
+ matched.
104
+
105
+ Prose is deliberately **not** a column on `frontmatter`, so `SELECT * FROM frontmatter` can never
106
+ dump your notes into an agent's context — reaching it takes an explicit join. Select `path`,
107
+ `title`, `summary`, and a `snippet()`, keep a `LIMIT`, and read the files worth reading. `sense`
108
+ warns on stderr when a result grows past 50 KB.
109
+
110
+ A one-line `summary:` in frontmatter is worth adding as you write — what's on the page and when
111
+ it's worth opening, like a skill's `description:`. It's both a column you can select (a search
112
+ result row often answers the question with no file read at all) and a weighted search field.
80
113
 
81
114
  ### For AI agents
82
115
 
@@ -98,31 +131,36 @@ launchd and systemd examples: [WATCH.md](WATCH.md).
98
131
 
99
132
  ## Why not …
100
133
 
101
- - **Obsidian (Bases/Dataview):** filters this well but only inside the running Electron app,
102
- through its own view formats. There's no headless mode for queries (only Sync), so scripts and
103
- agents must keep the app open. `sensemaking` needs no app and speaks plain SQL. Your vault works
104
- unmodified either way; Obsidian stays a fine viewer for the same files.
105
- - **RAG / semantic search:** retrieves by similarity and hopes relevance follows. That can't
106
- express "only active notes from this project" as a hard constraint — a `WHERE` clause can. The
107
- two compose rather than compete: filter first, rank later.
108
- - **Index-on-build tools (MarkdownDB and similar):** you run an explicit index step and query the
109
- snapshot, which is stale the moment a file changes. `sensemaking` reconciles on every query, so
110
- there's no stale window by construction.
111
- - **Note CLIs (zk and similar):** good at their own model — tags, links, full text — but they
112
- can't filter on arbitrary frontmatter fields, which is the whole point here.
134
+ - **Obsidian (Bases/Dataview):** filters this well, but only inside the running Electron app
135
+ scripts and agents can't query it headless. Your vault works unmodified either way; Obsidian
136
+ stays a fine viewer for the same files.
137
+ - **RAG / semantic search:** ranks by similarity, not hard constraints like "only active notes from
138
+ this project" that needs a `WHERE` clause. The two compose: filter first, rank later; vector
139
+ similarity, if added later, would join the same way.
140
+ - **An LLM-written index file** (à la Karpathy's llm-wiki `index.md`): a good instinct, but a
141
+ second artifact that drifts out of sync. `frontmatter` is that catalog, derived from the notes
142
+ themselves on every query.
143
+ - **Index-on-build tools (MarkdownDB and similar):** query a snapshot that's stale the moment a
144
+ file changes, instead of reconciling live.
145
+ - **Note CLIs (zk and similar):** good at their own model — tags, links, full text but can't
146
+ filter on arbitrary frontmatter fields, which is the whole point here.
113
147
  - **grep / one-off scripts:** fine until you want named, reusable, parameterized queries with real
114
148
  AND/OR/ORDER BY — at which point you've started writing a worse query engine.
115
149
 
116
150
  `sensemaking` is deliberately thin glue: [gray-matter](https://github.com/jonschlinkert/gray-matter)
117
- parses, [fast-glob](https://github.com/mrmlnc/fast-glob) walks, and Node's built-in SQLite
118
- (`node:sqlite`) does all the querying. Two dependencies, no native builds, no background services
119
- required.
151
+ parses, [remove-markdown](https://github.com/zuchka/remove-markdown) cleans the prose for indexing,
152
+ [fast-glob](https://github.com/mrmlnc/fast-glob) walks, and Node's built-in SQLite (`node:sqlite`)
153
+ does all the querying. Three small dependencies, no native builds, no background services required.
120
154
 
121
155
  ## Roadmap
122
156
 
123
- Content search is the next stage: BM25 relevance over note bodies, scoped to a frontmatter filter,
124
- so you can ask "the active within-tech notes, ranked by how well they discuss compensation" in one
125
- query. The filter shrinks the haystack; the search finds the needle.
157
+ Vector similarity is the natural next facet a `doc_vec` table joined the same way `content` is,
158
+ so semantic recall composes with the frontmatter filter instead of living in a separate tool
159
+ (`SELECT path, distance`, never the embedding). It's deferred, not planned: today it would require
160
+ a native SQLite extension (sqlite-vec is pre-v1 and ships platform binaries), which breaks the
161
+ no-native-builds line above. It becomes worth revisiting when Node can do it dependency-free — and
162
+ only if BM25 demonstrably misses things; on a curated vault of a few hundred notes it often
163
+ doesn't.
126
164
 
127
165
  Beyond that, the corpus model isn't tied to markdown — anything carrying structured metadata
128
166
  (document properties, sidecar JSON) can join it without changing the query surface.
package/dist/cjs/cli.js CHANGED
@@ -2,11 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- Object.defineProperty(exports, // Everything below that can throw (bad JSON, a config `version` newer than
6
- // this build supports, another watcher's fresh heartbeat, a SQLite error)
7
- // is caught by the top-level handler below and reported as exit 1 with the
8
- // error's message verbatim. Usage errors (missing/unknown query name, wrong
9
- // parameter count) exit(2) directly instead of throwing.
5
+ Object.defineProperty(exports, // Thrown errors -> exit 1 with the message verbatim; usage errors exit(2) directly.
10
6
  "default", {
11
7
  enumerable: true,
12
8
  get: function() {
@@ -239,10 +235,7 @@ function printWarnings(warnings) {
239
235
  }
240
236
  }
241
237
  }
242
- // Shared tail of both the named-query and ad-hoc `query` paths. An unbound
243
- // `?` silently binds NULL and returns misleading empty results — fail
244
- // loudly instead. (Naive count; queries putting '?' in string literals
245
- // would miscount, which none of ours do.)
238
+ // An unbound `?` silently binds NULL, so mismatched param counts fail loudly instead.
246
239
  function runSql(cfg, sql, params, format, label) {
247
240
  var _db_prepare;
248
241
  var _sql_match;
@@ -283,8 +276,6 @@ function cli(argv, name) {
283
276
  console.log(usage(name));
284
277
  process.exit(0);
285
278
  }
286
- // cli.ts's only config knowledge is the --config flag; discovery, parsing,
287
- // and version-gating all live in config.ts.
288
279
  resolveConfig = function resolveConfig() {
289
280
  return (0, _configts.loadConfig)(values.config);
290
281
  };
@@ -300,7 +291,7 @@ function cli(argv, name) {
300
291
  if (first === 'init') {
301
292
  configPath = (0, _configts.initConfig)(process.cwd());
302
293
  console.log("created ".concat(configPath));
303
- console.log('edit the queries to fit your tree, then: sense --list');
294
+ console.log('query away: sense query "SELECT path FROM frontmatter LIMIT 10"');
304
295
  process.exit(0);
305
296
  }
306
297
  if (!(first === 'watch')) return [
@@ -369,8 +360,6 @@ function cli(argv, name) {
369
360
  process.exit(0);
370
361
  }
371
362
  format = values.format === 'json' ? 'json' : 'table';
372
- // Ad-hoc SQL without touching the config -- for one-off questions;
373
- // save a query into sense.config.json only when it'll be reused.
374
363
  if (first === 'query') {
375
364
  _rest = _to_array(rest), sql = _rest[0], params = _rest.slice(1);
376
365
  if (!sql) {
@@ -403,8 +392,6 @@ function cli(argv, name) {
403
392
  ];
404
393
  case 4:
405
394
  err1 = _state.sent();
406
- // SQLite's own error message, a bad config, an unsupported config
407
- // version, or an already-active watcher — printed verbatim.
408
395
  console.error(err1.message);
409
396
  process.exit(1);
410
397
  return [
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport type { ResolvedConfig } from './config.ts';\nimport { initConfig, loadConfig } from './config.ts';\nimport { docCount, getMeta, open, rebuild } from './db.ts';\nimport type { Row } from './output.ts';\nimport { printRows } from './output.ts';\nimport type { WatchEvent } from './watch.ts';\nimport { runWatch } from './watch.ts';\n\nfunction usage(name: string): string {\n return `usage: ${name} <name> [params...] [--format table|json] [--config path]\\n` + ` ${name} query \"<sql>\" [params...]\\n` + ` ${name} --list\\n` + ` ${name} init\\n` + ` ${name} watch [--force]\\n` + ` ${name} status\\n` + ` ${name} rebuild`;\n}\n\nfunction parseCliArgs(argv: string[], name: string) {\n try {\n return parseArgs({\n args: argv,\n options: {\n format: { type: 'string', default: 'table' },\n config: { type: 'string' },\n list: { type: 'boolean', default: false },\n force: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false, short: 'h' },\n },\n allowPositionals: true,\n });\n } catch (err) {\n console.error((err as Error).message);\n console.error(usage(name));\n process.exit(2);\n }\n}\n\nfunction printWarnings(warnings: string[]): void {\n for (const w of warnings) console.warn(w);\n}\n\n// Shared tail of both the named-query and ad-hoc `query` paths. An unbound\n// `?` silently binds NULL and returns misleading empty results — fail\n// loudly instead. (Naive count; queries putting '?' in string literals\n// would miscount, which none of ours do.)\nfunction runSql(cfg: ResolvedConfig, sql: string, params: string[], format: 'table' | 'json', label: string): void {\n const placeholderCount = (sql.match(/\\?/g) ?? []).length;\n if (params.length !== placeholderCount) {\n console.error(`${label} expects ${placeholderCount} parameter(s), got ${params.length}`);\n process.exit(2);\n }\n const { db, warnings } = open(cfg);\n printWarnings(warnings);\n const rows = db.prepare(sql).all(...params) as Row[];\n printRows(rows, format);\n db.close();\n}\n\nfunction logWatchEvent(event: WatchEvent): void {\n if (event.type === 'started') {\n console.log(`sense watch: watching ${event.baseDir}`);\n console.log(`sense watch: db ${event.dbPath}`);\n return;\n }\n if (event.type === 'reconciled') {\n printWarnings(event.warnings);\n if (event.parsed > 0) {\n console.log(`sense watch: reconciled, ${event.parsed} file(s) reparsed (${event.total} total)`);\n }\n return;\n }\n console.error(`sense watch: reconcile error: ${event.message}`);\n}\n\n// Everything below that can throw (bad JSON, a config `version` newer than\n// this build supports, another watcher's fresh heartbeat, a SQLite error)\n// is caught by the top-level handler below and reported as exit 1 with the\n// error's message verbatim. Usage errors (missing/unknown query name, wrong\n// parameter count) exit(2) directly instead of throwing.\nexport default async function cli(argv: string[], name: string): Promise<void> {\n const { values, positionals } = parseCliArgs(argv, name);\n\n if (values.help) {\n console.log(usage(name));\n process.exit(0);\n }\n\n // cli.ts's only config knowledge is the --config flag; discovery, parsing,\n // and version-gating all live in config.ts.\n const resolveConfig = () => loadConfig(values.config);\n\n try {\n const [first, ...rest] = positionals;\n\n if (first === 'init') {\n const configPath = initConfig(process.cwd());\n console.log(`created ${configPath}`);\n console.log('edit the queries to fit your tree, then: sense --list');\n process.exit(0);\n }\n\n if (first === 'watch') {\n const cfg = resolveConfig();\n await runWatch(cfg, { force: values.force, onEvent: logWatchEvent });\n process.exit(0);\n }\n\n if (first === 'status') {\n const cfg = resolveConfig();\n const { db, dbPath, warnings } = open(cfg);\n printWarnings(warnings);\n console.log(`db: ${dbPath}`);\n console.log(`docs: ${docCount(db)}`);\n const heartbeat = getMeta(db, 'watch_heartbeat');\n if (heartbeat) {\n const ageSec = Math.round((Date.now() - Date.parse(heartbeat)) / 1000);\n console.log(`watcher: last heartbeat ${ageSec}s ago`);\n } else {\n console.log('watcher: no watcher');\n }\n db.close();\n process.exit(0);\n }\n\n if (first === 'rebuild') {\n const cfg = resolveConfig();\n const result = rebuild(cfg);\n printWarnings(result.warnings);\n console.log(`rebuilt: ${docCount(result.db)} docs`);\n result.db.close();\n process.exit(0);\n }\n\n if (values.list) {\n const cfg = resolveConfig();\n for (const queryName of Object.keys(cfg.queries).sort()) console.log(queryName);\n process.exit(0);\n }\n\n const format = values.format === 'json' ? 'json' : 'table';\n\n // Ad-hoc SQL without touching the config -- for one-off questions;\n // save a query into sense.config.json only when it'll be reused.\n if (first === 'query') {\n const [sql, ...params] = rest;\n if (!sql) {\n console.error(`usage: ${name} query \"<sql>\" [params...]`);\n process.exit(2);\n }\n runSql(resolveConfig(), sql, params, format, 'ad-hoc query');\n return;\n }\n\n const [name_, ...params] = [first, ...rest];\n\n if (!name_) {\n console.error(usage(name));\n process.exit(2);\n }\n\n const cfg = resolveConfig();\n\n const sql = cfg.queries[name_];\n if (!sql) {\n console.error(`unknown query: \"${name_}\"`);\n console.error(`valid queries: ${Object.keys(cfg.queries).sort().join(', ')}`);\n process.exit(2);\n }\n\n runSql(cfg, sql, params, format, `query \"${name_}\"`);\n } catch (err) {\n // SQLite's own error message, a bad config, an unsupported config\n // version, or an already-active watcher — printed verbatim.\n console.error((err as Error).message);\n process.exit(1);\n }\n}\n"],"names":["cli","usage","name","parseCliArgs","argv","parseArgs","args","options","format","type","default","config","list","force","help","short","allowPositionals","err","console","error","message","process","exit","printWarnings","warnings","w","warn","runSql","cfg","sql","params","label","db","placeholderCount","match","length","open","rows","prepare","all","printRows","close","logWatchEvent","event","log","baseDir","dbPath","parsed","total","values","positionals","resolveConfig","first","rest","configPath","heartbeat","ageSec","result","queryName","name_","loadConfig","initConfig","cwd","runWatch","onEvent","docCount","getMeta","Math","round","Date","now","parse","rebuild","Object","keys","queries","sort","join"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport type { ResolvedConfig } from './config.ts';\nimport { initConfig, loadConfig } from './config.ts';\nimport { docCount, getMeta, open, rebuild } from './db.ts';\nimport type { Row } from './output.ts';\nimport { printRows } from './output.ts';\nimport type { WatchEvent } from './watch.ts';\nimport { runWatch } from './watch.ts';\n\nfunction usage(name: string): string {\n return `usage: ${name} <name> [params...] [--format table|json] [--config path]\\n` + ` ${name} query \"<sql>\" [params...]\\n` + ` ${name} --list\\n` + ` ${name} init\\n` + ` ${name} watch [--force]\\n` + ` ${name} status\\n` + ` ${name} rebuild`;\n}\n\nfunction parseCliArgs(argv: string[], name: string) {\n try {\n return parseArgs({\n args: argv,\n options: {\n format: { type: 'string', default: 'table' },\n config: { type: 'string' },\n list: { type: 'boolean', default: false },\n force: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false, short: 'h' },\n },\n allowPositionals: true,\n });\n } catch (err) {\n console.error((err as Error).message);\n console.error(usage(name));\n process.exit(2);\n }\n}\n\nfunction printWarnings(warnings: string[]): void {\n for (const w of warnings) console.warn(w);\n}\n\n// An unbound `?` silently binds NULL, so mismatched param counts fail loudly instead.\nfunction runSql(cfg: ResolvedConfig, sql: string, params: string[], format: 'table' | 'json', label: string): void {\n const placeholderCount = (sql.match(/\\?/g) ?? []).length;\n if (params.length !== placeholderCount) {\n console.error(`${label} expects ${placeholderCount} parameter(s), got ${params.length}`);\n process.exit(2);\n }\n const { db, warnings } = open(cfg);\n printWarnings(warnings);\n const rows = db.prepare(sql).all(...params) as Row[];\n printRows(rows, format);\n db.close();\n}\n\nfunction logWatchEvent(event: WatchEvent): void {\n if (event.type === 'started') {\n console.log(`sense watch: watching ${event.baseDir}`);\n console.log(`sense watch: db ${event.dbPath}`);\n return;\n }\n if (event.type === 'reconciled') {\n printWarnings(event.warnings);\n if (event.parsed > 0) {\n console.log(`sense watch: reconciled, ${event.parsed} file(s) reparsed (${event.total} total)`);\n }\n return;\n }\n console.error(`sense watch: reconcile error: ${event.message}`);\n}\n\n// Thrown errors -> exit 1 with the message verbatim; usage errors exit(2) directly.\nexport default async function cli(argv: string[], name: string): Promise<void> {\n const { values, positionals } = parseCliArgs(argv, name);\n\n if (values.help) {\n console.log(usage(name));\n process.exit(0);\n }\n\n const resolveConfig = () => loadConfig(values.config);\n\n try {\n const [first, ...rest] = positionals;\n\n if (first === 'init') {\n const configPath = initConfig(process.cwd());\n console.log(`created ${configPath}`);\n console.log('query away: sense query \"SELECT path FROM frontmatter LIMIT 10\"');\n process.exit(0);\n }\n\n if (first === 'watch') {\n const cfg = resolveConfig();\n await runWatch(cfg, { force: values.force, onEvent: logWatchEvent });\n process.exit(0);\n }\n\n if (first === 'status') {\n const cfg = resolveConfig();\n const { db, dbPath, warnings } = open(cfg);\n printWarnings(warnings);\n console.log(`db: ${dbPath}`);\n console.log(`docs: ${docCount(db)}`);\n const heartbeat = getMeta(db, 'watch_heartbeat');\n if (heartbeat) {\n const ageSec = Math.round((Date.now() - Date.parse(heartbeat)) / 1000);\n console.log(`watcher: last heartbeat ${ageSec}s ago`);\n } else {\n console.log('watcher: no watcher');\n }\n db.close();\n process.exit(0);\n }\n\n if (first === 'rebuild') {\n const cfg = resolveConfig();\n const result = rebuild(cfg);\n printWarnings(result.warnings);\n console.log(`rebuilt: ${docCount(result.db)} docs`);\n result.db.close();\n process.exit(0);\n }\n\n if (values.list) {\n const cfg = resolveConfig();\n for (const queryName of Object.keys(cfg.queries).sort()) console.log(queryName);\n process.exit(0);\n }\n\n const format = values.format === 'json' ? 'json' : 'table';\n\n if (first === 'query') {\n const [sql, ...params] = rest;\n if (!sql) {\n console.error(`usage: ${name} query \"<sql>\" [params...]`);\n process.exit(2);\n }\n runSql(resolveConfig(), sql, params, format, 'ad-hoc query');\n return;\n }\n\n const [name_, ...params] = [first, ...rest];\n\n if (!name_) {\n console.error(usage(name));\n process.exit(2);\n }\n\n const cfg = resolveConfig();\n\n const sql = cfg.queries[name_];\n if (!sql) {\n console.error(`unknown query: \"${name_}\"`);\n console.error(`valid queries: ${Object.keys(cfg.queries).sort().join(', ')}`);\n process.exit(2);\n }\n\n runSql(cfg, sql, params, format, `query \"${name_}\"`);\n } catch (err) {\n console.error((err as Error).message);\n process.exit(1);\n }\n}\n"],"names":["cli","usage","name","parseCliArgs","argv","parseArgs","args","options","format","type","default","config","list","force","help","short","allowPositionals","err","console","error","message","process","exit","printWarnings","warnings","w","warn","runSql","cfg","sql","params","label","db","placeholderCount","match","length","open","rows","prepare","all","printRows","close","logWatchEvent","event","log","baseDir","dbPath","parsed","total","values","positionals","resolveConfig","first","rest","configPath","heartbeat","ageSec","result","queryName","name_","loadConfig","initConfig","cwd","runWatch","onEvent","docCount","getMeta","Math","round","Date","now","parse","rebuild","Object","keys","queries","sort","join"],"mappings":"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"}
@@ -99,10 +99,7 @@ function initConfig(dir) {
99
99
  '**/*.md'
100
100
  ]
101
101
  },
102
- queries: {
103
- all: 'SELECT path, title FROM docs ORDER BY path',
104
- 'by-tag': 'SELECT path, title FROM docs WHERE has(tags, ?) ORDER BY path'
105
- }
102
+ queries: {}
106
103
  };
107
104
  (0, _nodefs.writeFileSync)(configPath, "".concat(JSON.stringify(starter, null, 2), "\n"));
108
105
  return configPath;
@@ -132,9 +129,6 @@ function loadConfig(explicitPath) {
132
129
  }
133
130
  var raw = (0, _nodefs.readFileSync)(configPath, 'utf8');
134
131
  var cfg = JSON.parse(raw);
135
- // Missing `version` is treated as 1. A version newer than this build
136
- // supports is a hard error -- misinterpreting a future config format
137
- // silently would be worse than refusing to run.
138
132
  var version = (_cfg_version = cfg.version) !== null && _cfg_version !== void 0 ? _cfg_version : 1;
139
133
  if (version > SUPPORTED_CONFIG_VERSION) {
140
134
  throw new _errorsts.SenseError('CONFIG_VERSION_UNSUPPORTED', "config version ".concat(version, " requires a newer sense"));
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\n// All config concerns live here: the file name, discovery, parsing, and\n// version gating. db.ts consumes only the result -- it does no discovery\n// and no version logic of its own.\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// The highest `sense.config.json` `version` this build understands. Bumped\n// only when the config *format* changes in a breaking way -- unrelated to\n// the package's own semver.\nexport const SUPPORTED_CONFIG_VERSION = 1;\n\n// sense.config.json's on-disk shape.\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n // Config format version. Omitted = 1.\n version?: number;\n scan: { include: string[] };\n queries: Record<string, string>;\n}\n\n// A Config resolved to where it lives on disk (or, for tests, a baseDir\n// supplied directly with no backing file). `scan.include` globs and the\n// `.sense/` state dir both resolve relative to `baseDir`, never process cwd.\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n}\n\n// `sense init`: write a starter config into `dir` — the example doubles as\n// the documentation of the format. Refuses to overwrite an existing one.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const starter: Config = {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: 1,\n scan: { include: ['**/*.md'] },\n queries: {\n all: 'SELECT path, title FROM docs ORDER BY path',\n 'by-tag': 'SELECT path, title FROM docs WHERE has(tags, ?) ORDER BY path',\n },\n };\n writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\\n`);\n return configPath;\n}\n\n// Find sense.config.json by walking up from startDir, git-style.\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n// Resolve + load sense.config.json: `explicitPath` bypasses walk-up\n// discovery (as `--config` does on the CLI); otherwise discovery starts at\n// process cwd. Throws on a missing config, invalid JSON, or a `version`\n// newer than this build supports -- callers surface the message and exit 1.\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const cfg = JSON.parse(raw) as Config;\n\n // Missing `version` is treated as 1. A version newer than this build\n // supports is a hard error -- misinterpreting a future config format\n // silently would be worse than refusing to run.\n const version = cfg.version ?? 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n return { ...cfg, baseDir: dirname(configPath), configPath };\n}\n"],"names":["CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","findConfigPath","initConfig","loadConfig","dir","configPath","join","existsSync","SenseError","starter","$schema","version","scan","include","queries","all","writeFileSync","JSON","stringify","startDir","resolve","candidate","parent","dirname","explicitPath","cfg","process","cwd","found","raw","readFileSync","parse","baseDir"],"mappings":";;;;;;;;;;;QAQaA;eAAAA;;QACAC;eAAAA;;QAKAC;eAAAA;;QAyCGC;eAAAA;;QAnBAC;eAAAA;;QAkCAC;eAAAA;;;sBAtEwC;wBACjB;wBACZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMpB,IAAML,kBAAkB;AACxB,IAAMC,YAAY;AAKlB,IAAMC,2BAA2B;AAsBjC,SAASE,WAAWE,GAAW;IACpC,IAAMC,aAAaC,IAAAA,cAAI,EAACF,KAAKN;IAC7B,IAAIS,IAAAA,kBAAU,EAACF,aAAa;QAC1B,MAAM,IAAIG,oBAAU,CAAC,iBAAiB,AAAC,GAAuCJ,OAArCN,iBAAgB,uBAAyB,OAAJM;IAChF;IACA,IAAMK,UAAkB;QACtBC,SAAS;QACTC,SAAS;QACTC,MAAM;YAAEC,SAAS;gBAAC;aAAU;QAAC;QAC7BC,SAAS;YACPC,KAAK;YACL,UAAU;QACZ;IACF;IACAC,IAAAA,qBAAa,EAACX,YAAY,AAAC,GAAmC,OAAjCY,KAAKC,SAAS,CAACT,SAAS,MAAM,IAAG;IAC9D,OAAOJ;AACT;AAGO,SAASJ,eAAekB,QAAgB;IAC7C,IAAIf,MAAMgB,IAAAA,iBAAO,EAACD;IAClB,OAAS;QACP,IAAME,YAAYf,IAAAA,cAAI,EAACF,KAAKN;QAC5B,IAAIS,IAAAA,kBAAU,EAACc,YAAY,OAAOA;QAClC,IAAMC,SAASC,IAAAA,iBAAO,EAACnB;QACvB,IAAIkB,WAAWlB,KAAK,OAAO;QAC3BA,MAAMkB;IACR;AACF;AAMO,SAASnB,WAAWqB,YAAqB;QAmB9BC;IAlBhB,IAAIpB;IACJ,IAAImB,cAAc;QAChBnB,aAAae,IAAAA,iBAAO,EAACM,QAAQC,GAAG,IAAIH;QACpC,IAAI,CAACjB,IAAAA,kBAAU,EAACF,aAAa,MAAM,IAAIG,oBAAU,CAAC,oBAAoB,AAAC,qBAA+B,OAAXH;IAC7F,OAAO;QACL,IAAMuB,QAAQ3B,eAAeyB,QAAQC,GAAG;QACxC,IAAI,CAACC,OAAO;YACV,MAAM,IAAIpB,oBAAU,CAAC,oBAAoB,AAAC,kBAAuCkB,OAAtB5B,iBAAgB,QAAoB,OAAd4B,QAAQC,GAAG,IAAG;QACjG;QACAtB,aAAauB;IACf;IAEA,IAAMC,MAAMC,IAAAA,oBAAY,EAACzB,YAAY;IACrC,IAAMoB,MAAMR,KAAKc,KAAK,CAACF;IAEvB,qEAAqE;IACrE,qEAAqE;IACrE,gDAAgD;IAChD,IAAMlB,WAAUc,eAAAA,IAAId,OAAO,cAAXc,0BAAAA,eAAe;IAC/B,IAAId,UAAUX,0BAA0B;QACtC,MAAM,IAAIQ,oBAAU,CAAC,8BAA8B,AAAC,kBAAyB,OAARG,SAAQ;IAC/E;IAEA,OAAO,wCAAKc;QAAKO,SAAST,IAAAA,iBAAO,EAAClB;QAAaA,YAAAA;;AACjD"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands.\nexport const SUPPORTED_CONFIG_VERSION = 1;\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n scan: { include: string[] };\n queries: Record<string, string>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n}\n\n// Refuses to overwrite an existing config.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const starter: Config = {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: 1,\n scan: { include: ['**/*.md'] },\n queries: {},\n };\n writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const cfg = JSON.parse(raw) as Config;\n\n const version = cfg.version ?? 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n return { ...cfg, baseDir: dirname(configPath), configPath };\n}\n"],"names":["CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","findConfigPath","initConfig","loadConfig","dir","configPath","join","existsSync","SenseError","starter","$schema","version","scan","include","queries","writeFileSync","JSON","stringify","startDir","resolve","candidate","parent","dirname","explicitPath","cfg","process","cwd","found","raw","readFileSync","parse","baseDir"],"mappings":";;;;;;;;;;;QAIaA;eAAAA;;QACAC;eAAAA;;QAGAC;eAAAA;;QA+BGC;eAAAA;;QAfAC;eAAAA;;QA0BAC;eAAAA;;;sBAlDwC;wBACjB;wBACZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEpB,IAAML,kBAAkB;AACxB,IAAMC,YAAY;AAGlB,IAAMC,2BAA2B;AAgBjC,SAASE,WAAWE,GAAW;IACpC,IAAMC,aAAaC,IAAAA,cAAI,EAACF,KAAKN;IAC7B,IAAIS,IAAAA,kBAAU,EAACF,aAAa;QAC1B,MAAM,IAAIG,oBAAU,CAAC,iBAAiB,AAAC,GAAuCJ,OAArCN,iBAAgB,uBAAyB,OAAJM;IAChF;IACA,IAAMK,UAAkB;QACtBC,SAAS;QACTC,SAAS;QACTC,MAAM;YAAEC,SAAS;gBAAC;aAAU;QAAC;QAC7BC,SAAS,CAAC;IACZ;IACAC,IAAAA,qBAAa,EAACV,YAAY,AAAC,GAAmC,OAAjCW,KAAKC,SAAS,CAACR,SAAS,MAAM,IAAG;IAC9D,OAAOJ;AACT;AAEO,SAASJ,eAAeiB,QAAgB;IAC7C,IAAId,MAAMe,IAAAA,iBAAO,EAACD;IAClB,OAAS;QACP,IAAME,YAAYd,IAAAA,cAAI,EAACF,KAAKN;QAC5B,IAAIS,IAAAA,kBAAU,EAACa,YAAY,OAAOA;QAClC,IAAMC,SAASC,IAAAA,iBAAO,EAAClB;QACvB,IAAIiB,WAAWjB,KAAK,OAAO;QAC3BA,MAAMiB;IACR;AACF;AAEO,SAASlB,WAAWoB,YAAqB;QAgB9BC;IAfhB,IAAInB;IACJ,IAAIkB,cAAc;QAChBlB,aAAac,IAAAA,iBAAO,EAACM,QAAQC,GAAG,IAAIH;QACpC,IAAI,CAAChB,IAAAA,kBAAU,EAACF,aAAa,MAAM,IAAIG,oBAAU,CAAC,oBAAoB,AAAC,qBAA+B,OAAXH;IAC7F,OAAO;QACL,IAAMsB,QAAQ1B,eAAewB,QAAQC,GAAG;QACxC,IAAI,CAACC,OAAO;YACV,MAAM,IAAInB,oBAAU,CAAC,oBAAoB,AAAC,kBAAuCiB,OAAtB3B,iBAAgB,QAAoB,OAAd2B,QAAQC,GAAG,IAAG;QACjG;QACArB,aAAasB;IACf;IAEA,IAAMC,MAAMC,IAAAA,oBAAY,EAACxB,YAAY;IACrC,IAAMmB,MAAMR,KAAKc,KAAK,CAACF;IAEvB,IAAMjB,WAAUa,eAAAA,IAAIb,OAAO,cAAXa,0BAAAA,eAAe;IAC/B,IAAIb,UAAUX,0BAA0B;QACtC,MAAM,IAAIQ,oBAAU,CAAC,8BAA8B,AAAC,kBAAyB,OAARG,SAAQ;IAC/E;IAEA,OAAO,wCAAKa;QAAKO,SAAST,IAAAA,iBAAO,EAACjB;QAAaA,YAAAA;;AACjD"}
package/dist/cjs/db.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { Config, ResolvedConfig } from './config.js';
3
3
  export declare const DB_FILENAME = "cache.db";
4
- export declare const SCHEMA_VERSION = "1";
4
+ export declare const SCHEMA_VERSION = "2";
5
5
  export interface OpenResult {
6
6
  db: DatabaseSync;
7
7
  cfg: ResolvedConfig;
package/dist/cjs/db.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { Config, ResolvedConfig } from './config.js';
3
3
  export declare const DB_FILENAME = "cache.db";
4
- export declare const SCHEMA_VERSION = "1";
4
+ export declare const SCHEMA_VERSION = "2";
5
5
  export interface OpenResult {
6
6
  db: DatabaseSync;
7
7
  cfg: ResolvedConfig;
package/dist/cjs/db.js CHANGED
@@ -65,16 +65,11 @@ function _unsupported_iterable_to_array(o, minLen) {
65
65
  if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
66
66
  }
67
67
  var DB_FILENAME = 'cache.db';
68
- var SCHEMA_VERSION = '1';
69
- // Quote a SQL identifier, escaping embedded double quotes so an unusual
70
- // frontmatter key (however unlikely) can't break out of the identifier.
68
+ var SCHEMA_VERSION = '2';
71
69
  function quoteIdent(name) {
72
70
  return '"'.concat(name.split('"').join('""'), '"');
73
71
  }
74
- // The one custom SQL function in the whole tool: has(field, value).
75
- // - JSON-array field (stored as JSON text, e.g. `["a","b"]`) -> membership
76
- // - string field -> substring
77
- // - NULL -> false
72
+ // has(field, value): JSON-array field -> membership, string field -> substring, NULL -> false.
78
73
  function registerFunctions(db) {
79
74
  db.function('has', {
80
75
  deterministic: true,
@@ -83,7 +78,6 @@ function registerFunctions(db) {
83
78
  if (field === null || field === undefined) return 0;
84
79
  var needle = String(value);
85
80
  if (typeof field === 'string') {
86
- // Try JSON array first (arrays are stored as JSON text).
87
81
  if (field.startsWith('[')) {
88
82
  try {
89
83
  var parsed = JSON.parse(field);
@@ -92,25 +86,23 @@ function registerFunctions(db) {
92
86
  return String(item) === needle;
93
87
  }) ? 1 : 0;
94
88
  }
95
- } catch (unused) {
96
- // fall through to substring match
97
- }
89
+ } catch (unused) {}
98
90
  }
99
91
  return field.includes(needle) ? 1 : 0;
100
92
  }
101
- // Numbers, etc: coerce to string and substring-match.
102
93
  return String(field).includes(needle) ? 1 : 0;
103
94
  });
104
95
  }
105
96
  function getColumns(db) {
106
- var rows = db.prepare('PRAGMA table_info(docs)').all();
97
+ var rows = db.prepare('PRAGMA table_info(frontmatter)').all();
107
98
  return new Set(rows.map(function(r) {
108
99
  return r.name;
109
100
  }));
110
101
  }
102
+ // Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter` can't dump file text into context.
111
103
  function ensureSchema(db) {
112
- db.exec('CREATE TABLE IF NOT EXISTS docs ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_size" INTEGER)');
113
- db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
104
+ db.exec('CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_size" INTEGER)');
105
+ db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')");
114
106
  if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);
115
107
  }
116
108
  function getMeta(db, key) {
@@ -125,7 +117,7 @@ function setMeta(db, key, value) {
125
117
  db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
126
118
  }
127
119
  function docCount(db) {
128
- var row = db.prepare('SELECT COUNT(*) AS n FROM docs').get();
120
+ var row = db.prepare('SELECT COUNT(*) AS n FROM frontmatter').get();
129
121
  return row.n;
130
122
  }
131
123
  function reconcile(db, cfg, baseDir) {
@@ -133,7 +125,7 @@ function reconcile(db, cfg, baseDir) {
133
125
  var currentSet = new Set(files.map(function(f) {
134
126
  return f.relPath;
135
127
  }));
136
- var existingRows = db.prepare('SELECT "path", "_mtime", "_size" FROM docs').all();
128
+ var existingRows = db.prepare('SELECT "path", "_mtime", "_size" FROM frontmatter').all();
137
129
  var existing = new Map(existingRows.map(function(r) {
138
130
  return [
139
131
  r.path,
@@ -203,10 +195,8 @@ function reconcile(db, cfg, baseDir) {
203
195
  }
204
196
  }
205
197
  }
206
- // seenColumns already contains the reserved columns (they're real columns
207
- // of `docs`), so spreading it alone avoids duplicate names in the INSERT.
208
198
  var allColumns = _to_consumable_array(seenColumns);
209
- var insertSql = "INSERT OR REPLACE INTO docs (".concat(allColumns.map(quoteIdent).join(', '), ") VALUES (").concat(allColumns.map(function() {
199
+ var insertSql = "INSERT OR REPLACE INTO frontmatter (".concat(allColumns.map(quoteIdent).join(', '), ") VALUES (").concat(allColumns.map(function() {
210
200
  return '?';
211
201
  }).join(', '), ")");
212
202
  db.exec('BEGIN');
@@ -215,7 +205,7 @@ function reconcile(db, cfg, baseDir) {
215
205
  try {
216
206
  for(var _iterator2 = newColumns[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true){
217
207
  var col = _step2.value;
218
- db.exec("ALTER TABLE docs ADD COLUMN ".concat(quoteIdent(col)));
208
+ db.exec("ALTER TABLE frontmatter ADD COLUMN ".concat(quoteIdent(col)));
219
209
  }
220
210
  } catch (err) {
221
211
  _didIteratorError2 = true;
@@ -231,13 +221,16 @@ function reconcile(db, cfg, baseDir) {
231
221
  }
232
222
  }
233
223
  }
224
+ // FTS5 has no upsert, so delete-before-insert into `content`.
225
+ var delBody = db.prepare('DELETE FROM content WHERE "path" = ?');
234
226
  if (vanished.length > 0) {
235
- var del = db.prepare('DELETE FROM docs WHERE "path" = ?');
227
+ var del = db.prepare('DELETE FROM frontmatter WHERE "path" = ?');
236
228
  var _iteratorNormalCompletion3 = true, _didIteratorError3 = false, _iteratorError3 = undefined;
237
229
  try {
238
230
  for(var _iterator3 = vanished[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true){
239
231
  var path = _step3.value;
240
232
  del.run(path);
233
+ delBody.run(path);
241
234
  }
242
235
  } catch (err) {
243
236
  _didIteratorError3 = true;
@@ -256,6 +249,7 @@ function reconcile(db, cfg, baseDir) {
256
249
  }
257
250
  if (parsedDocs.length > 0) {
258
251
  var insert = db.prepare(insertSql);
252
+ var insertBody = db.prepare('INSERT INTO content (title, summary, text, "path") VALUES (?, ?, ?, ?)');
259
253
  var _iteratorNormalCompletion4 = true, _didIteratorError4 = false, _iteratorError4 = undefined;
260
254
  try {
261
255
  var _loop = function() {
@@ -269,6 +263,8 @@ function reconcile(db, cfg, baseDir) {
269
263
  return (_doc_data_col = doc.data[col]) !== null && _doc_data_col !== void 0 ? _doc_data_col : null;
270
264
  });
271
265
  (_insert = insert).run.apply(_insert, _to_consumable_array(values));
266
+ delBody.run(doc.relPath);
267
+ insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);
272
268
  };
273
269
  for(var _iterator4 = parsedDocs[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true)_loop();
274
270
  } catch (err) {
@@ -306,6 +302,17 @@ function open(cfg) {
306
302
  db.exec('PRAGMA journal_mode = WAL');
307
303
  db.exec('PRAGMA busy_timeout = 5000');
308
304
  registerFunctions(db);
305
+ db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
306
+ // Schema version mismatch: reconcile only reparses changed files, so an old cache can't be patched up incrementally -- rebuild instead.
307
+ var version = getMeta(db, 'schema_version');
308
+ if (version !== null && version !== SCHEMA_VERSION) {
309
+ db.close();
310
+ (0, _nodefs.rmSync)(stateDir, {
311
+ recursive: true,
312
+ force: true
313
+ });
314
+ return open(cfg);
315
+ }
309
316
  ensureSchema(db);
310
317
  var _reconcile = reconcile(db, cfg, cfg.baseDir), parsed = _reconcile.parsed, warnings = _reconcile.warnings;
311
318
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { STATE_DIR } from './config.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile } from './scan.ts';\n\n// Rows -> SQLite only: schema/ALTER, the reconcile diff + transaction,\n// pragmas, the meta table, has() registration. Filesystem reading and\n// frontmatter parsing live in scan.ts; this module never touches\n// node:fs/gray-matter directly and never prints (throws are for callers).\n\nexport const DB_FILENAME = 'cache.db';\nexport const SCHEMA_VERSION = '1';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n // Number of files (re)parsed by this open's reconcile -- 0 on a fully\n // warm cache. Tests use this as reconcile instrumentation.\n parsed: number;\n warnings: string[];\n}\n\n// Quote a SQL identifier, escaping embedded double quotes so an unusual\n// frontmatter key (however unlikely) can't break out of the identifier.\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// The one custom SQL function in the whole tool: has(field, value).\n// - JSON-array field (stored as JSON text, e.g. `[\"a\",\"b\"]`) -> membership\n// - string field -> substring\n// - NULL -> false\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n // Try JSON array first (arrays are stored as JSON text).\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {\n // fall through to substring match\n }\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n // Numbers, etc: coerce to string and substring-match.\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(docs)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nfunction ensureSchema(db: DatabaseSync): void {\n db.exec(`CREATE TABLE IF NOT EXISTS docs (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM docs').get() as { n: number };\n return row.n;\n}\n\n// Reconcile the `docs` table against the filesystem: glob current files via\n// scan.ts, reparse stale (mtime/size changed) + new files in one\n// transaction, DELETE vanished paths, ALTER TABLE ADD COLUMN for newly\n// discovered frontmatter keys. Returns the number of files (re)parsed and\n// any warnings scan.ts collected (e.g. reserved-key collisions).\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM docs`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n for (const file of toReparse) {\n const { doc, warnings: fileWarnings } = parseFile(file);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n\n // seenColumns already contains the reserved columns (they're real columns\n // of `docs`), so spreading it alone avoids duplicate names in the INSERT.\n const allColumns = [...seenColumns];\n const insertSql = `INSERT OR REPLACE INTO docs (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(() => '?').join(', ')})`;\n\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE docs ADD COLUMN ${quoteIdent(col)}`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM docs WHERE \"path\" = ?`);\n for (const path of vanished) del.run(path);\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n for (const doc of parsedDocs) {\n const values = allColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n insert.run(...values);\n }\n }\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n return { parsed: parsedDocs.length, warnings };\n}\n\n// open(resolved config): open (or create) the on-disk SQLite cache at\n// `<baseDir>/.sense/cache.db`, reconcile against the filesystem, and return\n// a live handle. The DB is a warm start, never a source of truth -- the\n// `.md` files remain truth. Takes only an already-resolved config; discovery\n// and version-gating are config.ts's job, not this one's.\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n registerFunctions(db);\n\n ensureSchema(db);\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Delete the `.sense/` state dir entirely and reconcile fresh -- the manual\n// reset for lingering columns or a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["DB_FILENAME","SCHEMA_VERSION","docCount","getMeta","open","rebuild","reconcile","setMeta","quoteIdent","name","split","join","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","Set","map","r","ensureSchema","exec","key","row","get","run","n","cfg","baseDir","files","listFiles","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","seenColumns","newColumns","parsedDocs","file","parseFile","doc","fileWarnings","push","Object","keys","data","add","allColumns","insertSql","col","del","insert","values","err","stateDir","STATE_DIR","mkdirSync","recursive","dbPath","DatabaseSync","rmSync","force"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { STATE_DIR } from './config.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile } from './scan.ts';\n\n// Rows -> SQLite: schema, reconcile, has(). Filesystem/frontmatter parsing lives in scan.ts.\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`.\nexport const SCHEMA_VERSION = '2';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// has(field, value): JSON-array field -> membership, string field -> substring, NULL -> false.\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {}\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter` can't dump file text into context.\nfunction ensureSchema(db: DatabaseSync): void {\n db.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, tokenize = 'porter unicode61')`);\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM frontmatter').get() as { n: number };\n return row.n;\n}\n\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n for (const file of toReparse) {\n const { doc, warnings: fileWarnings } = parseFile(file);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n\n const allColumns = [...seenColumns];\n const insertSql = `INSERT OR REPLACE INTO frontmatter (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(() => '?').join(', ')})`;\n\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}`);\n // FTS5 has no upsert, so delete-before-insert into `content`.\n const delBody = db.prepare(`DELETE FROM content WHERE \"path\" = ?`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM frontmatter WHERE \"path\" = ?`);\n for (const path of vanished) {\n del.run(path);\n delBody.run(path);\n }\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n const insertBody = db.prepare(`INSERT INTO content (title, summary, text, \"path\") VALUES (?, ?, ?, ?)`);\n for (const doc of parsedDocs) {\n const values = allColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n insert.run(...values);\n delBody.run(doc.relPath);\n insertBody.run(doc.search.title, doc.search.summary, doc.search.text, doc.relPath);\n }\n }\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n return { parsed: parsedDocs.length, warnings };\n}\n\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n registerFunctions(db);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema version mismatch: reconcile only reparses changed files, so an old cache can't be patched up incrementally -- rebuild instead.\n const version = getMeta(db, 'schema_version');\n if (version !== null && version !== SCHEMA_VERSION) {\n db.close();\n rmSync(stateDir, { recursive: true, force: true });\n return open(cfg);\n }\n\n ensureSchema(db);\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Manual reset for a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["DB_FILENAME","SCHEMA_VERSION","docCount","getMeta","open","rebuild","reconcile","setMeta","quoteIdent","name","split","join","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","Set","map","r","ensureSchema","exec","key","row","get","run","n","cfg","baseDir","files","listFiles","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","seenColumns","newColumns","parsedDocs","file","parseFile","doc","fileWarnings","push","Object","keys","data","add","allColumns","insertSql","col","delBody","del","insert","insertBody","values","search","title","summary","text","err","stateDir","STATE_DIR","mkdirSync","recursive","dbPath","DatabaseSync","version","close","rmSync","force"],"mappings":"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"}
@@ -1,6 +1,4 @@
1
- // Typed errors for library modules. Library code (config.ts, scan.ts,
2
- // db.ts, watch.ts) never calls process.exit and never prints -- it throws.
3
- // Only cli.ts catches, prints the message, and maps it to an exit code.
1
+ // Library code throws; only cli.ts prints and exits.
4
2
  "use strict";
5
3
  Object.defineProperty(exports, "__esModule", {
6
4
  value: true
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Typed errors for library modules. Library code (config.ts, scan.ts,\n// db.ts, watch.ts) never calls process.exit and never prints -- it throws.\n// Only cli.ts catches, prints the message, and maps it to an exit code.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","code","message","name","Error"],"mappings":"AAAA,sEAAsE;AACtE,2EAA2E;AAC3E,wEAAwE;;;;;+BAG3DA;;;eAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAN,IAAA,AAAMA,2BAAN;;cAAMA;aAAAA,WAGCC,IAAoB,EAAEC,OAAe;gCAHtCF;;gBAIT,kBAJSA;YAIHE;;QACN,MAAKC,IAAI,GAAG;QACZ,MAAKF,IAAI,GAAGA;;;WANHD;qBAAmBI"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","code","message","name","Error"],"mappings":"AAAA,qDAAqD;;;;;+BAGxCA;;;eAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAN,IAAA,AAAMA,2BAAN;;cAAMA;aAAAA,WAGCC,IAAoB,EAAEC,OAAe;gCAHtCF;;gBAIT,kBAJSA;YAIHE;;QACN,MAAKC,IAAI,GAAG;QACZ,MAAKF,IAAI,GAAGA;;;WANHD;qBAAmBI"}
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,4 @@
1
- // Public library API. cli.ts is a consumer of this surface, not a source of
2
- // truth for it -- anything a test or another tool needs from sense should be
3
- // exported here rather than reached into src/ by relative path.
1
+ // Public library API.
4
2
  "use strict";
5
3
  Object.defineProperty(exports, "__esModule", {
6
4
  value: true