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/schema.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "type": "object",
20
20
  "additionalProperties": false,
21
21
  "required": ["include"],
22
- "description": "Which files become rows in the `docs` table.",
22
+ "description": "Which files become rows in the `frontmatter` table.",
23
23
  "properties": {
24
24
  "include": {
25
25
  "type": "array",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "queries": {
33
33
  "type": "object",
34
- "description": "Named SQL queries runnable as `sense <name> [params...]`. Each value is a SQL SELECT against the `docs` table (one row per file, one column per discovered frontmatter key, plus reserved `path`/`_mtime`/`_size`). `?` placeholders bind to CLI positional arguments in order. The custom `has(field, value)` function does array-membership on a JSON-array field, substring match on a string field, and is always false on NULL. The names `init`, `query`, `watch`, `status`, and `rebuild` are reserved subcommands — a query with one of those names is unreachable from the CLI.",
34
+ "description": "Named SQL queries runnable as `sense <name> [params...]`. Tables: `frontmatter` (one row per file, one column per discovered frontmatter key, plus `path`/`_mtime`/`_size`) and `content`, an FTS5 index (`title`, `summary`, `text`, `path`) for content search. `?` placeholders bind to CLI positional args in order. `has(field, value)`: array membership on a JSON-array field, substring match on a string, false on NULL. Canonical query: `SELECT f.path, content.title, content.summary, snippet(content, -1, '«', '»', '…', 10) AS hit FROM frontmatter f JOIN content ON content.path = f.path WHERE content MATCH ? ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10`. Reserved frontmatter keys: `path`, `_mtime`, `_size`, `content`. Reserved query names (unreachable as subcommands): `init`, `query`, `watch`, `status`, `rebuild`.",
35
35
  "additionalProperties": { "type": "string" }
36
36
  }
37
37
  }
@@ -0,0 +1,115 @@
1
+ # sense: worked examples
2
+
3
+ Every query returns *references with evidence* — enough to decide which files to open, never the
4
+ files themselves. Reading happens afterward, through the filesystem, on the paths that earned it.
5
+
6
+ Token counts below are from a real 26-note vault (~62 KB of markdown): a search costs ~1–2% of
7
+ reading the files it points at.
8
+
9
+ ## A. "Does the vault say anything about X?" (discovery search)
10
+
11
+ The question an agent should ask *before* reading anything — cheap enough to run speculatively.
12
+
13
+ ```
14
+ sense query "SELECT f.path, content.title, content.summary, snippet(content, -1, '«', '»', '…', 10) AS hit
15
+ FROM frontmatter f JOIN content ON content.path = f.path
16
+ WHERE content MATCH ?
17
+ ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10" "compensation OR salary" --format json
18
+ ```
19
+
20
+ ```json
21
+ [
22
+ {
23
+ "path": "knowledge/compensation-floor.md",
24
+ "title": "Compensation floor",
25
+ "summary": "The comp floor and how to apply it when screening",
26
+ "hit": "«Compensation» floor Kevin's «compensation» preference for the later job…"
27
+ },
28
+ {
29
+ "path": "methodology/jobs-table-schema.md",
30
+ "title": "Jobs table schema",
31
+ "summary": "",
32
+ "hit": "…B Company employer C «Salary» as posted; blank if not…"
33
+ }
34
+ ]
35
+ ```
36
+
37
+ ~55 tokens per row. Decide from `title`/`summary`/`hit`; often the row itself answers the
38
+ question. If not, `Read knowledge/compensation-floor.md` (~800 tokens) — one file, not the vault
39
+ (~16,000 tokens).
40
+
41
+ Note the `content.title`/`content.summary` spelling: those columns always exist on `content`
42
+ (empty string when a note lacks the key), so this query works on any vault. `f.title`/`f.summary`
43
+ only work once some note actually declares the key — `frontmatter` columns are discovered, not fixed.
44
+
45
+ ## B. Filtering on frontmatter you already know (no content search)
46
+
47
+ When the fields are known — typically because an agent wrote the notes to a schema — plain SQL on
48
+ `frontmatter` is the whole query. No join needed when you aren't searching prose:
49
+
50
+ ```
51
+ sense query "SELECT path, title, summary FROM frontmatter
52
+ WHERE status = 'active' AND has(track, ?) ORDER BY updated DESC" within-tech --format json
53
+ ```
54
+
55
+ `has()` does array membership on JSON-array fields (`track: [a, b]`), substring on strings, false
56
+ on missing keys. A note missing `summary` yields NULL — costs nothing, breaks nothing. (Only if
57
+ *no* note in the vault declares a key does its column not exist at all — pattern D's pragma shows
58
+ what's there.)
59
+
60
+ ## C. Filter + search + rank in one query
61
+
62
+ The case neither grep nor a frontmatter-only tool can do: a hard constraint AND a relevance
63
+ ranking, composed.
64
+
65
+ ```
66
+ sense query "SELECT f.path, content.title, content.summary, snippet(content, -1, '«', '»', '…', 10) AS hit
67
+ FROM frontmatter f JOIN content ON content.path = f.path
68
+ WHERE f.status = 'active' AND has(f.track, 'within-tech') AND content MATCH ?
69
+ ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10" remote --format json
70
+ ```
71
+
72
+ The frontmatter conditions are hard filters (a non-active note can never appear); the `MATCH` +
73
+ `bm25()` ranks whatever survives. The join exists **only** for `MATCH`/`bm25()`/`snippet()` —
74
+ never use it to fetch text.
75
+
76
+ ## D. Cold start: a vault you didn't build
77
+
78
+ ```
79
+ sense --list # named queries, if any
80
+ sense query "SELECT name FROM pragma_table_info('frontmatter') ORDER BY name" # discover the fields
81
+ sense query "SELECT DISTINCT type FROM frontmatter" # discover a field's values
82
+ sense query "SELECT count(*) AS n FROM frontmatter" # corpus size
83
+ ```
84
+
85
+ Each costs a few dozen tokens and turns an unknown corpus into a queryable schema. From there,
86
+ patterns A–C apply.
87
+
88
+ ## E. Search syntax worth knowing
89
+
90
+ ```
91
+ "exact phrase" # phrase match
92
+ compensat* # prefix
93
+ summary: onboarding # only match in the summary field
94
+ title: retro OR text: retrospective
95
+ NEAR(salary negotiate, 8)
96
+ ```
97
+
98
+ Stemming is on: `negotiate` finds "negotiating". Markdown is stripped from the index, so search
99
+ for the words, not the syntax around them.
100
+
101
+ ## F. Anti-patterns
102
+
103
+ ```
104
+ sense query "SELECT text FROM content" # dumps every note into context
105
+ ```
106
+ This is the one query shape that defeats the tool's purpose. `sense` warns on stderr when a
107
+ result exceeds 50 KB (stdout stays clean for --format json), but the fix is upstream: select
108
+ `path` + `snippet()`, keep a `LIMIT`, and `Read` the files that deserve it.
109
+
110
+ Other traps:
111
+ - **No `LIMIT`** on a search — fine on a tiny vault, a context bomb on a big one. Habit: always.
112
+ - **Fetching content through SQL** because the join is there. The join ranks; the filesystem
113
+ retrieves. An agent already has `Read` and `grep` for the path the query returned.
114
+ - **Adding a named query for a one-off question.** `sense query "<sql>"` exists so the config
115
+ only accumulates queries that are genuinely reused.
@@ -1,85 +1,104 @@
1
1
  ---
2
2
  name: sense
3
- description: Query a markdown tree's YAML frontmatter with SQL via the sense CLI. Use when the user wants to query, filter, count, or report on a folder of markdown notes by their frontmatter fields, when a directory has a sense.config.json, when asked what named queries exist, or when asked to add a new query to one.
3
+ description: Query a markdown tree with SQL via the sense CLI — filter notes by YAML frontmatter, then full-text search inside them. Use when the user wants to query, filter, count, search, or report on a folder of markdown notes, when you need to find which notes discuss a topic before reading them, when a directory has a sense.config.json, when asked what named queries exist, or when asked to add a new query to one.
4
4
  ---
5
5
 
6
6
  # sense
7
7
 
8
- `sense` runs SQL over the frontmatter of a tree of markdown files. Each file is a row in a
9
- `docs` table (one column per frontmatter key); the file's own content is never queried, only its
10
- frontmatter. A running app is **never** required — files on disk are the only source of truth.
8
+ `sense` runs SQL over a tree of markdown files. Two tables, two facets of the same document:
11
9
 
12
- ## Before anything else
10
+ | table | one row per file holds… | for… |
11
+ |---------------|------------------------------------------------------------------------|-------------------|
12
+ | `frontmatter` | one column per frontmatter key (+ reserved `path`, `_mtime`, `_size`) | filtering |
13
+ | `content` | FTS5 index: `title`, `summary`, `text` (+ `path` to join on) | searching/ranking |
14
+
15
+ No running app required — every query re-checks the filesystem first, so results are never stale.
13
16
 
14
- Confirm `sense` is installed (`sense --list` or `which sense`; if missing, `npm install -g sense`)
15
- and that a config exists. Discovery walks **up** from cwd looking for `sense.config.json`, same as
16
- git looks for `.git` — you don't need to `cd` to the project root, and `--config <path>` bypasses
17
- discovery entirely. If no config exists yet in the tree you're being asked to query, run
18
- `sense init` at its root to write a starter config, then tailor the queries.
17
+ ## The flow: query decide Read
19
18
 
20
- ## See what's queryable
19
+ A query result is *references with evidence*, never file contents: `path`, `title`, `summary` (if
20
+ present), `hit` (matching excerpt) — enough to decide whether a file is worth opening at ~2% of
21
+ the token cost of reading it. Then `Read` the one or two paths that matter.
21
22
 
22
23
  ```
23
- sense --list
24
+ sense query "SELECT f.path, content.title, content.summary, snippet(content, -1, '«', '»', '…', 10) AS hit
25
+ FROM frontmatter f JOIN content ON content.path = f.path
26
+ WHERE content MATCH ?
27
+ ORDER BY bm25(content, 10.0, 5.0, 1.0) LIMIT 10" "compensation OR equity" --format json
24
28
  ```
25
29
 
26
- Prints the named queries defined in `sense.config.json`, sorted. Read that file directly if you
27
- need to see the SQL, not just the names.
30
+ Select `title`/`summary` from `content`, where they always exist (empty when a note lacks the
31
+ key) on `frontmatter` they're discovered columns, present only if some note declares them, so
32
+ `f.summary` errors on a vault with no summaries yet. Add frontmatter conditions
33
+ (`f.status = 'active' AND has(f.track, ?)`) to the same WHERE — the filter and the search compose
34
+ in one query. Worked traces for the common cases (discovery,
35
+ known-field filtering, cold start, anti-patterns): [EXAMPLES.md](EXAMPLES.md).
28
36
 
29
- ## Run a query
37
+ ## Before anything else
38
+
39
+ Confirm `sense` is installed (`sense --list` or `which sense`; if missing,
40
+ `npm install -g sensemaking`) and that a config exists. Discovery walks **up** from cwd looking for
41
+ `sense.config.json`, same as git looks for `.git` — you don't need to `cd` to the project root, and
42
+ `--config <path>` bypasses discovery entirely. If no config exists yet, `sense init` at the tree's
43
+ root writes a minimal one (globs only, no queries — ad-hoc `sense query` works immediately).
44
+
45
+ ## Cold start: discover what's queryable
30
46
 
31
47
  ```
32
- sense <name> [params...] --format json
48
+ sense --list # named queries, if any
49
+ sense query "SELECT name FROM pragma_table_info('frontmatter') ORDER BY name" # what frontmatter fields exist
50
+ sense query "SELECT DISTINCT status FROM frontmatter" # what values a field takes
33
51
  ```
34
52
 
35
- **Prefer `--format json` when consuming output as an agent** it's structured and avoids parsing
36
- a padded text table. `--format table` (the default) is for humans at a terminal.
53
+ If you wrote the notes, you already know the fields; the pragma is for vaults you didn't build.
37
54
 
38
- Named queries may contain `?` placeholders; positional arguments after the name bind to them in
39
- order. The parameter count is checked strictly — passing the wrong number of arguments is a usage
40
- error (exit 2), never a silent empty result.
55
+ ## Run a query
41
56
 
42
57
  ```
43
- sense by-tag urgent --format json
58
+ sense <name> [params...] --format json # named query from sense.config.json
59
+ sense query "<sql>" [params...] # ad-hoc SQL, no config edit
44
60
  ```
45
61
 
46
- ## One-off questions: `query`, not config edits
62
+ **Prefer `--format json` when consuming output as an agent.** Positional args bind to `?`
63
+ placeholders in order, count-checked strictly (wrong count = exit 2, never a silent empty result).
64
+ Save a query into `sense.config.json` (plain JSON, edit directly) only when it's meant to be
65
+ reused as a named view — never add-then-remove one for a one-off question.
47
66
 
48
- For an ad-hoc question, run SQL directly — do NOT add a temporary query to
49
- `sense.config.json` and remove it afterward:
67
+ ## Search syntax and ranking
50
68
 
51
- ```
52
- sense query "SELECT path FROM docs WHERE has(phase, 'screen') AND NOT has(phase, 'explore')" --format json
53
- sense query "SELECT path FROM docs WHERE has(tags, ?)" urgent --format json
54
- ```
69
+ - `content MATCH ?` takes FTS5 syntax: `a OR b`, `a AND b`, `"exact phrase"`, `pref*`,
70
+ `NEAR(a b, 5)`, column-scoped `summary: onboarding`.
71
+ - `bm25(content, 10.0, 5.0, 1.0)` weights follow column order (title, summary, text), so a
72
+ title hit outranks a passing mention. Lower is better; plain `ORDER BY bm25(…)` sorts best-first.
73
+ - `snippet(content, -1, '«', '»', '…', 10)` — bounded excerpt; `-1` picks whichever column
74
+ matched; the last argument is the excerpt budget in tokens.
75
+ - Stemming is on (`negotiate` matches "negotiating"); markdown syntax is stripped at index time,
76
+ so snippets are clean prose and `**bold**` matches `bold`.
55
77
 
56
- Save a query into `sense.config.json` only when it's meant to be reused as a named view.
78
+ ## Keep results small
57
79
 
58
- ## `has()` semantics
80
+ - Select `path, title, summary` + a `snippet()` — never `SELECT text FROM content`, which dumps
81
+ the whole tree into context (`sense` warns on stderr past 50 KB).
82
+ - Always `LIMIT`. Ten rows is plenty; widen only if they all look wrong.
83
+ - `SELECT * FROM frontmatter` is safe — prose is deliberately not a `frontmatter` column.
59
84
 
60
- The one custom SQL function, for frontmatter fields that are arrays or free text:
85
+ ## When writing notes, not just querying them
61
86
 
62
- | field type | `has(field, value)` means |
63
- |--------------------------|---------------------------------|
64
- | JSON array (e.g. `tags`) | array membership |
65
- | string | substring match |
66
- | NULL (key absent) | always false |
87
+ Give every note a one-line `summary:` in its frontmatter — what's on the page and when it's worth
88
+ opening, like a skill's `description:`. It pays twice: it appears in result rows (often answering
89
+ the question with no file read at all), and it's a weighted search field. Keep it to one line.
67
90
 
68
- ## Adding a named query
91
+ Reserved frontmatter key names (dropped with a warning): `path`, `_mtime`, `_size`, `content`.
69
92
 
70
- Edit `sense.config.json` directly — it's plain JSON, not something sense itself writes:
93
+ ## `has()` semantics
71
94
 
72
- ```json
73
- {
74
- "scan": { "include": ["**/*.md"] },
75
- "queries": {
76
- "my-query": "SELECT path, title FROM docs WHERE has(tags, ?) ORDER BY path"
77
- }
78
- }
79
- ```
80
- `scan.include` globs and query results are both relative to the config file's own directory,
81
- regardless of your cwd. Reserved columns (don't use as frontmatter key names): `path`, `_mtime`,
82
- `_size`.
95
+ The one custom SQL function, for frontmatter fields that are arrays or free text:
96
+
97
+ | field type | `has(field, value)` means |
98
+ |--------------------------|---------------------------|
99
+ | JSON array (e.g. `tags`) | array membership |
100
+ | string | substring match |
101
+ | NULL (key absent) | always false |
83
102
 
84
103
  ## Exit codes
85
104
 
@@ -88,8 +107,6 @@ regardless of your cwd. Reserved columns (don't use as frontmatter key names): `
88
107
 
89
108
  ## When results look stale
90
109
 
91
- `sense rebuild` deletes the local `.sense/` cache and re-crawls every file from scratch. Every
92
- query already reconciles the cache against the filesystem on open, so this is rarely needed use
93
- it if you doubt the cache rather than trying to debug it. `sense status` shows the doc count, db
94
- path, and whether a background `sense watch` process is running (it's an optional pre-warmer, not
95
- a correctness requirement).
110
+ `sense rebuild` deletes the local `.sense/` cache and re-crawls from scratch. Rarely needed since
111
+ every query reconciles on open use it if you doubt the cache. `sense status` shows doc count, db
112
+ path, and whether a background `sense watch` (optional pre-warmer) is running.