sensemaking 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,25 +5,27 @@ Query a knowledge base you build with an agent: filter notes by frontmatter, the
5
5
  ## The problem
6
6
 
7
7
  When you work with an AI agent on anything substantial, you end up with a pile of small notes —
8
- findings, decisions, sources, summaries. The pile is the point: it's how knowledge accumulates
9
- instead of being re-derived every session.
8
+ findings, decisions, sources, summaries. Small, categorized notes are how knowledge accumulates
9
+ instead of being re-derived every session: structured frontmatter over free-form prose.
10
10
 
11
11
  But accumulation only pays off if it's findable. Past a couple dozen notes, an agent can't tell
12
- which fifteen of two hundred bear on its task, so it greps blindly, swallows the whole folder into
13
- context, or quietly rebuilds knowledge that already exists three files away.
12
+ which fifteen of two hundred bear on its task, so it greps, reads whole folders into context, or
13
+ rebuilds knowledge that already exists three files away.
14
14
 
15
15
  The classification an agent adds while writing — `status`, `type`, `tags`, `source` — is what keeps
16
16
  the pile navigable. `sensemaking` is the layer that acts on it, without an app running, a build
17
- step, or you re-explaining anything.
17
+ step, or you re-explaining anything. The intended shape is a small vault per project, not one big
18
+ one — starting a new one is a single command with no schema to design.
18
19
 
19
- ## How it works
20
+ ## The model
20
21
 
21
- Every file becomes a row; every frontmatter key becomes a column. You write named SQL queries once
22
- and run them by name.
22
+ Every file becomes a row; every frontmatter key becomes a column; the prose becomes a full-text
23
+ index you can join against. You write named SQL queries once and run them by name.
23
24
 
24
25
  ```markdown
25
26
  ---
26
27
  title: Ship the Q3 report
28
+ summary: where the Q3 numbers came from and who signed off
27
29
  status: active
28
30
  tags: [urgent, reports]
29
31
  ---
@@ -31,23 +33,38 @@ tags: [urgent, reports]
31
33
  Notes about the report…
32
34
  ```
33
35
 
34
- Two properties make it trustworthy:
36
+ ```sql
37
+ -- active notes that actually discuss revenue, best match first
38
+ SELECT f.path, content.title, content.summary, snippet(content, -1, '«', '»', '…', 10) AS hit
39
+ FROM frontmatter f JOIN content ON content.path = f.path
40
+ WHERE f.status = 'active' AND content MATCH 'revenue'
41
+ ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10
42
+ ```
35
43
 
36
- - **Never stale.** Every query re-checks the filesystem first, re-reading only what changed. The
37
- SQLite cache under `.sense/` is disposable delete it any time, the next query rebuilds it. An
38
- agent can trust a result without knowing when anything was last indexed.
39
- - **Headless.** Files on disk are the only source of truth. Nothing needs to be open not
40
- Obsidian, not a server, not a daemon.
44
+ The filter narrows the set, the search ranks it, and each row that comes back —
45
+ path, title, summary, matching excerptis enough to decide whether to open the file, without
46
+ carrying the file. On a real 26-note vault that's a few hundred tokens, against ~6,000 to read the
47
+ three files it points at. Cheap enough to run before deciding what to open; reading afterward is
48
+ the expensive step, and it happens through the filesystem, not SQL.
41
49
 
42
50
  ## Use it
43
51
 
44
52
  ```bash
45
53
  npm install -g sensemaking
46
- cd your-notes && sense init # writes a starter sense.config.json
54
+ cd your-notes && sense init # writes a minimal sense.config.json (globs only, no queries)
47
55
  ```
48
56
 
49
- Edit the queries to match your own frontmatter (the `$schema` line gives your editor autocomplete
50
- and validation):
57
+ Query immediately ad-hoc SQL needs no config beyond the globs:
58
+
59
+ ```
60
+ sense query "SELECT … FROM frontmatter" # ad-hoc SQL; positional args bind to ? placeholders
61
+ sense query "…" --format json # structured output (the default is a table)
62
+ sense --list # what named queries exist
63
+ sense status | rebuild # cache info / delete .sense/ and re-crawl
64
+ ```
65
+
66
+ When a query proves worth reusing, name it in `sense.config.json` (plain JSON; the `$schema` line
67
+ gives your editor autocomplete and validation) and run it as `sense <name> [params...]`:
51
68
 
52
69
  ```json
53
70
  {
@@ -55,28 +72,37 @@ and validation):
55
72
  "version": 1,
56
73
  "scan": { "include": ["**/*.md"] },
57
74
  "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"
75
+ "by-tag": "SELECT path, title, summary FROM frontmatter WHERE has(tags, ?) ORDER BY path"
60
76
  }
61
77
  }
62
78
  ```
63
79
 
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
80
  Discovery walks up from your cwd git-style, so you can run `sense` from anywhere in the tree
74
81
  (`--config <path>` overrides). Exit codes: `0` ok, `1` real error (SQLite's message verbatim),
75
82
  `2` usage error.
76
83
 
77
84
  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`.
85
+ `tags`), substring match on a string, always false on a missing key. Reserved names: `path`,
86
+ `_mtime`, `_size`, `content`.
87
+
88
+ ### Content search
89
+
90
+ `content` is an FTS5 table with columns `title`, `summary`, `text`, and `path` (for the join).
91
+ `content MATCH ?` takes FTS5 syntax (`a OR b`, `"exact phrase"`, `pref*`, `NEAR(a b, 5)`,
92
+ `summary: term`), with stemming on so `negotiate` matches "negotiating". Markdown syntax is
93
+ stripped at index time — search for the words, not the formatting around them, and excerpts come
94
+ back as clean prose. `bm25()` weights follow column order, so `bm25(content, 10.0, 5.0, 1.0)`
95
+ ranks a title hit above a passing mention; `snippet(content, -1, …)` excerpts whichever column
96
+ matched.
97
+
98
+ Prose is deliberately **not** a column on `frontmatter`, so `SELECT * FROM frontmatter` can never
99
+ dump your notes into an agent's context — reaching it takes an explicit join. Select `path`,
100
+ `title`, `summary`, and a `snippet()`, keep a `LIMIT`, and read the files worth reading. `sense`
101
+ warns on stderr when a result grows past 50 KB.
102
+
103
+ A one-line `summary:` in frontmatter is worth adding as you write — what's on the page and when
104
+ it's worth opening, like a skill's `description:`. It's both a column you can select (a search
105
+ result row often answers the question with no file read at all) and a weighted search field.
80
106
 
81
107
  ### For AI agents
82
108
 
@@ -88,41 +114,59 @@ npx skills add kmalakoff/sensemaking # the agent skill (add -g for global, -a
88
114
  The skill teaches an agent the essentials: discovery, `--list`, `--format json`, `has()`, when to
89
115
  use ad-hoc `query` versus saving a named one, and when to `rebuild`.
90
116
 
91
- ### Optional: a background pre-warmer
117
+ ## How it works, how it scales
118
+
119
+ `sense` is a command, not an app. Nothing has to be running: it starts, answers,
120
+ and exits.
121
+
122
+ Every query begins with a freshness check: each file's timestamp and size is
123
+ compared against the SQLite cache in `.sense/`, changed files are re-parsed, and
124
+ deleted files are dropped. When nothing has changed, that check is the entire
125
+ cost. Results are never stale, and the cache is disposable — `sense rebuild`
126
+ deletes and rebuilds it.
127
+
128
+ For a vault of a few hundred notes, the check takes a few milliseconds.
92
129
 
93
- `sense watch` runs the same reconcile ahead of time whenever the filesystem changes, so queries
94
- open on an already-warm cache. It's purely an optimization queries reconcile on open anyway, so
95
- a missed event can never make a result wrong and under ~1000 files you likely won't notice a
96
- difference. It runs in the foreground and never daemonizes; process supervision belongs to the OS.
97
- launchd and systemd examples: [WATCH.md](WATCH.md).
130
+ On a large vault (10,000+ files), the check grows with file count roughly a
131
+ tenth of a second at 10k and the first query after editing many files pays to
132
+ re-parse them. `sense watch` moves that parsing into the background: a job that
133
+ re-parses files as they change, so queries find the work already done. It is
134
+ optional queries always run their own check, so a stopped watcher never causes
135
+ a wrong answer, only a slower next query. launchd and systemd examples:
136
+ [WATCH.md](WATCH.md).
98
137
 
99
138
  ## Why not …
100
139
 
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.
140
+ - **Obsidian (Bases/Dataview):** filters this well, but only inside the running Electron app
141
+ scripts and agents can't query it headless. Your vault works unmodified either way; Obsidian
142
+ stays a fine viewer for the same files.
143
+ - **RAG / semantic search:** ranks by similarity, not hard constraints like "only active notes from
144
+ this project" that needs a `WHERE` clause. The two compose: filter first, rank later; vector
145
+ similarity, if added later, would join the same way.
146
+ - **An LLM-written index file** (à la Karpathy's llm-wiki `index.md`): a good instinct, but a
147
+ second artifact that drifts out of sync. `frontmatter` is that catalog, derived from the notes
148
+ themselves on every query.
149
+ - **Index-on-build tools (MarkdownDB and similar):** query a snapshot that's stale the moment a
150
+ file changes, instead of reconciling live.
151
+ - **Note CLIs (zk and similar):** good at their own model — tags, links, full text but can't
152
+ filter on arbitrary frontmatter fields, which is the whole point here.
113
153
  - **grep / one-off scripts:** fine until you want named, reusable, parameterized queries with real
114
- AND/OR/ORDER BY — at which point you've started writing a worse query engine.
154
+ AND/OR/ORDER BY — at which point you're writing your own query engine.
115
155
 
116
156
  `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.
157
+ parses, [remove-markdown](https://github.com/zuchka/remove-markdown) cleans the prose for indexing,
158
+ [fast-glob](https://github.com/mrmlnc/fast-glob) walks, and Node's built-in SQLite (`node:sqlite`)
159
+ does all the querying. Three small dependencies, no native builds, no background services required.
120
160
 
121
161
  ## Roadmap
122
162
 
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.
163
+ Vector similarity is the natural next facet a `doc_vec` table joined the same way `content` is,
164
+ so semantic recall composes with the frontmatter filter instead of living in a separate tool
165
+ (`SELECT path, distance`, never the embedding). It's deferred, not planned: today it would require
166
+ a native SQLite extension (sqlite-vec is pre-v1 and ships platform binaries), which breaks the
167
+ no-native-builds line above. It becomes worth revisiting when Node can do it dependency-free — and
168
+ only if BM25 demonstrably misses things; on a curated vault of a few hundred notes it often
169
+ doesn't.
126
170
 
127
171
  Beyond that, the corpus model isn't tied to markdown — anything carrying structured metadata
128
172
  (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 {