sensemaking 0.2.1 → 0.3.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.
Files changed (182) hide show
  1. package/README.md +107 -132
  2. package/dist/cjs/cli.js +172 -182
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/commands/find.d.cts +3 -0
  5. package/dist/cjs/commands/find.d.ts +3 -0
  6. package/dist/cjs/commands/find.js +73 -0
  7. package/dist/cjs/commands/find.js.map +1 -0
  8. package/dist/cjs/commands/index.d.cts +5 -0
  9. package/dist/cjs/commands/index.d.ts +5 -0
  10. package/dist/cjs/commands/index.js +94 -0
  11. package/dist/cjs/commands/index.js.map +1 -0
  12. package/dist/cjs/commands/init.d.cts +3 -0
  13. package/dist/cjs/commands/init.d.ts +3 -0
  14. package/dist/cjs/commands/init.js +18 -0
  15. package/dist/cjs/commands/init.js.map +1 -0
  16. package/dist/cjs/commands/map.d.cts +3 -0
  17. package/dist/cjs/commands/map.d.ts +3 -0
  18. package/dist/cjs/commands/map.js +21 -0
  19. package/dist/cjs/commands/map.js.map +1 -0
  20. package/dist/cjs/commands/named.d.cts +2 -0
  21. package/dist/cjs/commands/named.d.ts +2 -0
  22. package/dist/cjs/commands/named.js +23 -0
  23. package/dist/cjs/commands/named.js.map +1 -0
  24. package/dist/cjs/commands/peek.d.cts +3 -0
  25. package/dist/cjs/commands/peek.d.ts +3 -0
  26. package/dist/cjs/commands/peek.js +69 -0
  27. package/dist/cjs/commands/peek.js.map +1 -0
  28. package/dist/cjs/commands/query.d.cts +3 -0
  29. package/dist/cjs/commands/query.d.ts +3 -0
  30. package/dist/cjs/commands/query.js +43 -0
  31. package/dist/cjs/commands/query.js.map +1 -0
  32. package/dist/cjs/commands/rebuild.d.cts +3 -0
  33. package/dist/cjs/commands/rebuild.d.ts +3 -0
  34. package/dist/cjs/commands/rebuild.js +20 -0
  35. package/dist/cjs/commands/rebuild.js.map +1 -0
  36. package/dist/cjs/commands/status.d.cts +3 -0
  37. package/dist/cjs/commands/status.d.ts +3 -0
  38. package/dist/cjs/commands/status.js +24 -0
  39. package/dist/cjs/commands/status.js.map +1 -0
  40. package/dist/cjs/commands/types.d.cts +15 -0
  41. package/dist/cjs/commands/types.d.ts +15 -0
  42. package/dist/cjs/commands/types.js +5 -0
  43. package/dist/cjs/commands/types.js.map +1 -0
  44. package/dist/cjs/commands/vault.d.cts +6 -0
  45. package/dist/cjs/commands/vault.d.ts +6 -0
  46. package/dist/cjs/commands/vault.js +94 -0
  47. package/dist/cjs/commands/vault.js.map +1 -0
  48. package/dist/cjs/commands/watch.d.cts +3 -0
  49. package/dist/cjs/commands/watch.d.ts +3 -0
  50. package/dist/cjs/commands/watch.js +173 -0
  51. package/dist/cjs/commands/watch.js.map +1 -0
  52. package/dist/cjs/config.d.cts +11 -1
  53. package/dist/cjs/config.d.ts +11 -1
  54. package/dist/cjs/config.js +82 -9
  55. package/dist/cjs/config.js.map +1 -1
  56. package/dist/cjs/db.d.cts +1 -1
  57. package/dist/cjs/db.d.ts +1 -1
  58. package/dist/cjs/db.js +107 -16
  59. package/dist/cjs/db.js.map +1 -1
  60. package/dist/cjs/errors.d.cts +1 -1
  61. package/dist/cjs/errors.d.ts +1 -1
  62. package/dist/cjs/errors.js.map +1 -1
  63. package/dist/cjs/features/index.d.cts +7 -0
  64. package/dist/cjs/features/index.d.ts +7 -0
  65. package/dist/cjs/features/index.js +36 -0
  66. package/dist/cjs/features/index.js.map +1 -0
  67. package/dist/cjs/features/links.d.cts +4 -0
  68. package/dist/cjs/features/links.d.ts +4 -0
  69. package/dist/cjs/features/links.js +217 -0
  70. package/dist/cjs/features/links.js.map +1 -0
  71. package/dist/cjs/features/rank.d.cts +2 -0
  72. package/dist/cjs/features/rank.d.ts +2 -0
  73. package/dist/cjs/features/rank.js +95 -0
  74. package/dist/cjs/features/rank.js.map +1 -0
  75. package/dist/cjs/features/sections.d.cts +9 -0
  76. package/dist/cjs/features/sections.d.ts +9 -0
  77. package/dist/cjs/features/sections.js +54 -0
  78. package/dist/cjs/features/sections.js.map +1 -0
  79. package/dist/cjs/features/types.d.cts +11 -0
  80. package/dist/cjs/features/types.d.ts +11 -0
  81. package/dist/cjs/features/types.js +5 -0
  82. package/dist/cjs/features/types.js.map +1 -0
  83. package/dist/cjs/graph.d.cts +7 -0
  84. package/dist/cjs/graph.d.ts +7 -0
  85. package/dist/cjs/graph.js +237 -0
  86. package/dist/cjs/graph.js.map +1 -0
  87. package/dist/cjs/index.d.cts +9 -2
  88. package/dist/cjs/index.d.ts +9 -2
  89. package/dist/cjs/index.js +39 -0
  90. package/dist/cjs/index.js.map +1 -1
  91. package/dist/cjs/output.d.cts +21 -0
  92. package/dist/cjs/output.d.ts +21 -0
  93. package/dist/cjs/output.js +112 -3
  94. package/dist/cjs/output.js.map +1 -1
  95. package/dist/cjs/scan.d.cts +3 -1
  96. package/dist/cjs/scan.d.ts +3 -1
  97. package/dist/cjs/scan.js +62 -14
  98. package/dist/cjs/scan.js.map +1 -1
  99. package/dist/cjs/verbs.d.cts +31 -0
  100. package/dist/cjs/verbs.d.ts +31 -0
  101. package/dist/cjs/verbs.js +284 -0
  102. package/dist/cjs/verbs.js.map +1 -0
  103. package/dist/esm/cli.js +63 -107
  104. package/dist/esm/cli.js.map +1 -1
  105. package/dist/esm/commands/find.d.ts +3 -0
  106. package/dist/esm/commands/find.js +14 -0
  107. package/dist/esm/commands/find.js.map +1 -0
  108. package/dist/esm/commands/index.d.ts +5 -0
  109. package/dist/esm/commands/index.js +13 -0
  110. package/dist/esm/commands/index.js.map +1 -0
  111. package/dist/esm/commands/init.d.ts +3 -0
  112. package/dist/esm/commands/init.js +7 -0
  113. package/dist/esm/commands/init.js.map +1 -0
  114. package/dist/esm/commands/map.d.ts +3 -0
  115. package/dist/esm/commands/map.js +10 -0
  116. package/dist/esm/commands/map.js.map +1 -0
  117. package/dist/esm/commands/named.d.ts +2 -0
  118. package/dist/esm/commands/named.js +12 -0
  119. package/dist/esm/commands/named.js.map +1 -0
  120. package/dist/esm/commands/peek.d.ts +3 -0
  121. package/dist/esm/commands/peek.js +12 -0
  122. package/dist/esm/commands/peek.js.map +1 -0
  123. package/dist/esm/commands/query.d.ts +3 -0
  124. package/dist/esm/commands/query.js +7 -0
  125. package/dist/esm/commands/query.js.map +1 -0
  126. package/dist/esm/commands/rebuild.d.ts +3 -0
  127. package/dist/esm/commands/rebuild.js +9 -0
  128. package/dist/esm/commands/rebuild.js.map +1 -0
  129. package/dist/esm/commands/status.d.ts +3 -0
  130. package/dist/esm/commands/status.js +13 -0
  131. package/dist/esm/commands/status.js.map +1 -0
  132. package/dist/esm/commands/types.d.ts +15 -0
  133. package/dist/esm/commands/types.js +1 -0
  134. package/dist/esm/commands/types.js.map +1 -0
  135. package/dist/esm/commands/vault.d.ts +6 -0
  136. package/dist/esm/commands/vault.js +29 -0
  137. package/dist/esm/commands/vault.js.map +1 -0
  138. package/dist/esm/commands/watch.d.ts +3 -0
  139. package/dist/esm/commands/watch.js +19 -0
  140. package/dist/esm/commands/watch.js.map +1 -0
  141. package/dist/esm/config.d.ts +11 -1
  142. package/dist/esm/config.js +68 -12
  143. package/dist/esm/config.js.map +1 -1
  144. package/dist/esm/db.d.ts +1 -1
  145. package/dist/esm/db.js +27 -9
  146. package/dist/esm/db.js.map +1 -1
  147. package/dist/esm/errors.d.ts +1 -1
  148. package/dist/esm/errors.js.map +1 -1
  149. package/dist/esm/features/index.d.ts +7 -0
  150. package/dist/esm/features/index.js +14 -0
  151. package/dist/esm/features/index.js.map +1 -0
  152. package/dist/esm/features/links.d.ts +4 -0
  153. package/dist/esm/features/links.js +81 -0
  154. package/dist/esm/features/links.js.map +1 -0
  155. package/dist/esm/features/rank.d.ts +2 -0
  156. package/dist/esm/features/rank.js +17 -0
  157. package/dist/esm/features/rank.js.map +1 -0
  158. package/dist/esm/features/sections.d.ts +9 -0
  159. package/dist/esm/features/sections.js +41 -0
  160. package/dist/esm/features/sections.js.map +1 -0
  161. package/dist/esm/features/types.d.ts +11 -0
  162. package/dist/esm/features/types.js +3 -0
  163. package/dist/esm/features/types.js.map +1 -0
  164. package/dist/esm/graph.d.ts +7 -0
  165. package/dist/esm/graph.js +79 -0
  166. package/dist/esm/graph.js.map +1 -0
  167. package/dist/esm/index.d.ts +9 -2
  168. package/dist/esm/index.js +4 -1
  169. package/dist/esm/index.js.map +1 -1
  170. package/dist/esm/output.d.ts +21 -0
  171. package/dist/esm/output.js +28 -0
  172. package/dist/esm/output.js.map +1 -1
  173. package/dist/esm/scan.d.ts +3 -1
  174. package/dist/esm/scan.js +56 -7
  175. package/dist/esm/scan.js.map +1 -1
  176. package/dist/esm/verbs.d.ts +31 -0
  177. package/dist/esm/verbs.js +139 -0
  178. package/dist/esm/verbs.js.map +1 -0
  179. package/package.json +4 -4
  180. package/schema.json +23 -4
  181. package/skills/sense/EXAMPLES.md +46 -81
  182. package/skills/sense/SKILL.md +58 -92
package/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "sense.config.json",
4
- "description": "Config for sense -- SQL over the frontmatter of a markdown tree.",
4
+ "description": "Config for sense -- SQL over the frontmatter, content, links, and structure of a markdown tree.",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
7
  "required": ["scan", "queries"],
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "version": {
14
14
  "type": "integer",
15
- "enum": [1],
16
- "description": "Config format version. Bumped only on breaking changes to this file's shape. Omit to default to 1; a version newer than this sense build supports makes it exit with an error rather than misinterpret the file."
15
+ "enum": [1, 2],
16
+ "description": "Config format version. Older versions are auto-migrated (and the file rewritten) on load; a version newer than this sense build supports makes it exit with an error rather than misinterpret the file. Omit to default to 1."
17
17
  },
18
18
  "scan": {
19
19
  "type": "object",
@@ -29,9 +29,28 @@
29
29
  }
30
30
  }
31
31
  },
32
+ "features": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "description": "Independently toggleable features. Absent block or key means enabled; set false to disable. Toggling a feature rebuilds the cache. Verbs degrade rather than fail: `find` without links is BM25-only, `map` without rank omits hubs, `peek` without sections omits the outline.",
36
+ "properties": {
37
+ "links": {
38
+ "type": "boolean",
39
+ "description": "Extract wikilinks and relative markdown links into the `links` table (src, target as written, dst resolved path or NULL for dead links). Powers backlinks, graph expansion in `find`, and `peek`'s link lists."
40
+ },
41
+ "sections": {
42
+ "type": "boolean",
43
+ "description": "Extract headings into the `sections` table (path, idx, level, heading, start_line, end_line, tokens estimate). Powers `peek`'s outline so agents can read line ranges instead of whole files."
44
+ },
45
+ "rank": {
46
+ "type": "boolean",
47
+ "description": "PageRank over resolved links into `frontmatter._rank` at reconcile -- a static importance prior. Powers `map`'s hub list; usable in any ORDER BY. Requires `links`."
48
+ }
49
+ }
50
+ },
32
51
  "queries": {
33
52
  "type": "object",
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`.",
53
+ "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`/`_rank`), `content` (FTS5: `title`, `summary`, `text`, `path`), `links` (`src`, `target`, `dst`), and `sections` (`path`, `idx`, `level`, `heading`, `start_line`, `end_line`, `tokens`). `?` 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`, `_rank`, `content`, `links`, `sections`. Reserved query names (unreachable as subcommands): `init`, `query`, `find`, `map`, `peek`, `watch`, `status`, `rebuild`.",
35
54
  "additionalProperties": { "type": "string" }
36
55
  }
37
56
  }
@@ -1,115 +1,80 @@
1
1
  # sense: worked examples
2
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.
3
+ Outputs below are illustrative. Every result is a reference; reading happens afterward, through
4
+ the filesystem, on the paths that earned it.
5
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.
6
+ ## A. "Does the vault say anything about X?"
12
7
 
13
8
  ```
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
9
+ sense find "pricing OR billing OR invoicing" --k 10 --format json
18
10
  ```
19
11
 
20
12
  ```json
21
13
  [
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
- }
14
+ { "path": "notes/pricing-model.md", "title": "Pricing model",
15
+ "summary": "Tiered per-seat pricing; floor and discount rules", "hit": "«Pricing» floor for annual…", "via": "match+link", "score": 0.0333 },
16
+ { "path": "notes/renewal-playbook.md", "title": "Renewal playbook",
17
+ "summary": "Renewal sequence and owners", "hit": "…«billing» contact confirms the PO…", "via": "match", "score": 0.0313 }
34
18
  ]
35
19
  ```
36
20
 
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.
21
+ ~30 tokens per row; often the `summary` answers the question with no read at all. A row with
22
+ `via: "link"` never contained the termsit is linked from notes that did, usually a signal
23
+ worth following, not noise.
44
24
 
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:
25
+ ## B. Known-field filtering (no search)
49
26
 
50
27
  ```
51
- sense query "SELECT path, title, summary FROM frontmatter
52
- WHERE status = 'active' AND has(track, ?) ORDER BY updated DESC" within-tech --format json
28
+ sense query "SELECT path, title, status FROM frontmatter WHERE status = 'active' AND has(tags, ?)" pricing --format json
53
29
  ```
54
30
 
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
31
+ Plain SQL on discovered columns. Combine with search by joining `content` and adding
32
+ `AND content MATCH ?` filter and rank in one query.
61
33
 
62
- The case neither grep nor a frontmatter-only tool can do: a hard constraint AND a relevance
63
- ranking, composed.
34
+ ## C. Structure before reading
64
35
 
65
36
  ```
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
37
+ sense peek notes/architecture-decisions.md
70
38
  ```
71
39
 
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
40
  ```
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
41
+ notes/architecture-decisions.md (~4400 tokens)
42
+ title: Architecture decisions
43
+ sections:
44
+ ## Storage layer why SQLite [L41-88, ~610t]
45
+ ## Queue — rejected options [L89-120, ~380t]
46
+ ...
47
+ links out (7): notes/pricing-model.md, ...
48
+ backlinks (2): notes/_index.md, notes/roadmap.md
83
49
  ```
84
50
 
85
- Each costs a few dozen tokens and turns an unknown corpus into a queryable schema. From there,
86
- patterns A–C apply.
51
+ The note is ~4,400 tokens; the peek is ~500. If only one section matters, `Read` its line range
52
+ (~400 tokens) — a tenth of the file.
87
53
 
88
- ## E. Search syntax worth knowing
54
+ ## D. The graph
89
55
 
90
56
  ```
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)
57
+ sense query "SELECT src FROM links WHERE dst = ?" notes/pricing-model.md # who cites this
58
+ sense query "SELECT src, target FROM links WHERE dst IS NULL" # dead links
59
+ sense query "SELECT path, round(_rank*100,2) r FROM frontmatter ORDER BY _rank DESC LIMIT 5" # load-bearing notes
96
60
  ```
97
61
 
98
- Stemming is on: `negotiate` finds "negotiating". Markdown is stripped from the index, so search
99
- for the words, not the syntax around them.
62
+ `sense map` prints the hub list without SQL; use these when you need it filtered or joined.
100
63
 
101
- ## F. Anti-patterns
64
+ ## E. Cold start on an unknown vault
102
65
 
103
66
  ```
104
- sense query "SELECT text FROM content" # dumps every note into context
67
+ sense map # fields, hubs, recent read this first
68
+ sense --list # named queries someone already saved
69
+ sense query "SELECT DISTINCT type FROM frontmatter" # what a field's values are
105
70
  ```
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.
71
+
72
+ ## Anti-patterns
73
+
74
+ | don't | because | instead |
75
+ |---|---|---|
76
+ | `SELECT text FROM content` | dumps the vault into context | `snippet(content, -1, '«', '»', '…', 10)` |
77
+ | `sense find "pricing"` | one word misses synonyms | `"pricing OR billing OR invoicing"` |
78
+ | `Read` a 4,000-token file for one section | 10× the tokens needed | `peek` first, `Read` the line range |
79
+ | queries without `LIMIT` | unbounded output | `LIMIT 10`, widen only if all rows look wrong |
80
+ | save every query to config | config churn for one-offs | ad-hoc `sense query`; save only reusable views |
@@ -1,112 +1,78 @@
1
1
  ---
2
2
  name: sense
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.
3
+ description: Query a markdown tree with the sense CLI — filter notes by frontmatter, full-text search the prose, follow wikilinks/backlinks, and read note outlines. 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 you want a note's backlinks or structure, when a directory has a sense.config.json, or when asked to add a named query to one.
4
4
  ---
5
5
 
6
6
  # sense
7
7
 
8
- `sense` runs SQL over a tree of markdown files. Two tables, two facets of the same document:
8
+ SQL over a markdown tree, kept fresh by a filesystem check on every query. Four tables per file:
9
+ `frontmatter` (one column per key, plus `path`/`_mtime`/`_size`/`_rank`), `content` (FTS5:
10
+ `title`, `summary`, `text`), `links` (`src`, `target`, `dst` — `NULL` dst = dead link),
11
+ `sections` (heading outline with line ranges and token estimates).
9
12
 
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 |
13
+ ## The descent
14
14
 
15
- No running app required every query re-checks the filesystem first, so results are never stale.
15
+ Spend tokens in this order (progressive disclosure: metadata first, payloads just in time); go
16
+ only as deep as the question needs.
16
17
 
17
- ## The flow: query decide Read
18
+ 1. `sense map` — orient once: fields, hub notes, recent changes. Fixed-size output.
19
+ 2. `sense find "<terms>"` — locate: ranked references with excerpts, ~30 tokens/row.
20
+ 3. `sense peek <path>` — structure before reading: outline with `[L143-162, ~380t]` ranges,
21
+ links both ways. ~17% the cost of reading the file.
22
+ 4. `Read` the line range peek gave you — not the whole file.
18
23
 
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.
24
+ Every result is a reference, never file contents. Prefer `--format json` when consuming output.
22
25
 
23
- ```
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
28
- ```
29
-
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).
36
-
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
26
+ ## Verbs
46
27
 
47
28
  ```
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
29
+ sense find "pricing OR billing OR invoicing" --where "f.status = 'active'" --k 10
30
+ sense peek notes/pricing-model.md # a unique basename also works
31
+ sense map
32
+ sense query "<sql>" [params...] # ad-hoc SQL; ? binds positional args, count-checked
33
+ sense <name> [params...] # named query from sense.config.json
34
+ sense --list | status | rebuild
51
35
  ```
52
36
 
53
- If you wrote the notes, you already know the fields; the pragma is for vaults you didn't build.
37
+ - **Expand terms before searching.** Write `pricing OR billing OR invoicing`, not one word you
38
+ know the synonyms; the index only knows the words in the files.
39
+ - **Over-fetch, then choose.** `--k 20`, read the rows, open the 2–3 that matter.
40
+ - `find` fuses BM25 with link-graph expansion; the `via` column says what produced each row —
41
+ `match` (terms hit), `link` (connected to notes that hit), `match+link` (both).
42
+ - `--where` takes a frontmatter condition against alias `f`, e.g. `"f.status = 'active' AND has(f.tags, 'x')"`.
54
43
 
55
- ## Run a query
44
+ ## SQL, when the verbs aren't enough
56
45
 
57
46
  ```
58
- sense <name> [params...] --format json # named query from sense.config.json
59
- sense query "<sql>" [params...] # ad-hoc SQL, no config edit
47
+ sense query "SELECT name FROM pragma_table_info('frontmatter')" # what fields exist
48
+ sense query "SELECT DISTINCT status FROM frontmatter" # what values a field takes
49
+ sense query "SELECT src FROM links WHERE dst = ?" notes/pricing-model.md # backlinks
50
+ sense query "SELECT src, target FROM links WHERE dst IS NULL" # dead links
51
+ sense query "SELECT path FROM frontmatter WHERE path NOT IN (SELECT dst FROM links WHERE dst IS NOT NULL)" # orphans
52
+ sense query "SELECT heading, start_line, tokens FROM sections WHERE path = ?" a.md # budget a read
60
53
  ```
61
54
 
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.
66
-
67
- ## Search syntax and ranking
68
-
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`.
77
-
78
- ## Keep results small
79
-
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.
84
-
85
- ## When writing notes, not just querying them
86
-
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.
90
-
91
- Reserved frontmatter key names (dropped with a warning): `path`, `_mtime`, `_size`, `content`.
92
-
93
- ## `has()` semantics
94
-
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 |
102
-
103
- ## Exit codes
104
-
105
- `0` success · `1` a real error (bad config, SQL error — the SQLite message is printed verbatim) ·
106
- `2` usage error (missing query name, unknown query name, wrong parameter count).
107
-
108
- ## When results look stale
109
-
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.
55
+ - `content MATCH` takes FTS5 syntax: `a OR b`, `"phrase"`, `pref*`, `NEAR(a b, 5)`,
56
+ `summary: term`. Stemmed; markdown stripped at index time.
57
+ - Rank with `ORDER BY bm25(content, 10.0, 5.0, 1.0)` (title > summary > body); excerpt with
58
+ `snippet(content, -1, '«', '»', '…', 10)`.
59
+ - Select `content.title`/`content.summary` (always exist, empty when absent) rather than
60
+ `f.title`/`f.summary` (discovered columns error on vaults that never declare them).
61
+ - `has(field, value)`: array membership on JSON-array fields, substring on strings, false on NULL.
62
+ - Never `SELECT text FROM content` that is the whole vault into context. `SELECT * FROM
63
+ frontmatter` is safe; prose is not a frontmatter column. Always `LIMIT`.
64
+
65
+ Worked traces: [EXAMPLES.md](EXAMPLES.md).
66
+
67
+ ## Setup and upkeep
68
+
69
+ - Missing CLI: `npm install -g sensemaking`. Missing config: `sense init` at the tree root.
70
+ Discovery walks up from cwd; `--config <path>` overrides.
71
+ - Save a query into `sense.config.json` only when it will be reused; run ad-hoc otherwise.
72
+ - When writing notes, give each a one-line `summary:` — it appears in every result row and is a
73
+ weighted search field.
74
+ - Reserved frontmatter keys (dropped with a warning): `path`, `_mtime`, `_size`, `_rank`,
75
+ `content`, `links`, `sections`.
76
+ - Exit codes: `0` ok, `1` error (SQLite message verbatim), `2` usage (unknown query, wrong
77
+ param count).
78
+ - Doubted cache: `sense rebuild`. Rarely needed — every query reconciles first.