sensemaking 0.27.1 → 0.28.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.
Files changed (138) hide show
  1. package/README.md +2 -2
  2. package/bin/cli.js +16 -7
  3. package/dist/cjs/cli/constants.d.cts +2 -0
  4. package/dist/cjs/cli/constants.d.ts +2 -0
  5. package/dist/cjs/cli/constants.js +21 -0
  6. package/dist/cjs/cli/constants.js.map +1 -0
  7. package/dist/cjs/cli/named.js +2 -1
  8. package/dist/cjs/cli/named.js.map +1 -1
  9. package/dist/cjs/cli/shared.js +5 -4
  10. package/dist/cjs/cli/shared.js.map +1 -1
  11. package/dist/cjs/cli.js +10 -9
  12. package/dist/cjs/cli.js.map +1 -1
  13. package/dist/cjs/embed/query.js.map +1 -1
  14. package/dist/cjs/store/duckdb/engine.d.cts +11 -0
  15. package/dist/cjs/store/duckdb/engine.d.ts +11 -0
  16. package/dist/cjs/store/duckdb/engine.js +193 -0
  17. package/dist/cjs/store/duckdb/engine.js.map +1 -0
  18. package/dist/cjs/store/duckdb/lexical.js +34 -20
  19. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  20. package/dist/cjs/store/duckdb/native.d.cts +5 -2
  21. package/dist/cjs/store/duckdb/native.d.ts +5 -2
  22. package/dist/cjs/store/duckdb/native.js +157 -13
  23. package/dist/cjs/store/duckdb/native.js.map +1 -1
  24. package/dist/cjs/store/duckdb/open.js +151 -3
  25. package/dist/cjs/store/duckdb/open.js.map +1 -1
  26. package/dist/cjs/store/duckdb/ordered-bm25.d.cts +3 -3
  27. package/dist/cjs/store/duckdb/ordered-bm25.d.ts +3 -3
  28. package/dist/cjs/store/duckdb/ordered-bm25.js +4 -8
  29. package/dist/cjs/store/duckdb/ordered-bm25.js.map +1 -1
  30. package/dist/cjs/store/engine.d.cts +28 -0
  31. package/dist/cjs/store/engine.d.ts +28 -0
  32. package/dist/cjs/store/engine.js +362 -0
  33. package/dist/cjs/store/engine.js.map +1 -0
  34. package/dist/cjs/store/native.d.cts +8 -5
  35. package/dist/cjs/store/native.d.ts +8 -5
  36. package/dist/cjs/store/native.js +78 -73
  37. package/dist/cjs/store/native.js.map +1 -1
  38. package/dist/cjs/store/open.js +100 -40
  39. package/dist/cjs/store/open.js.map +1 -1
  40. package/dist/cjs/store/sqlite/engine.d.cts +12 -0
  41. package/dist/cjs/store/sqlite/engine.d.ts +12 -0
  42. package/dist/cjs/store/sqlite/engine.js +53 -0
  43. package/dist/cjs/store/sqlite/engine.js.map +1 -0
  44. package/dist/cjs/store/sqlite/lexical.js +3 -4
  45. package/dist/cjs/store/sqlite/lexical.js.map +1 -1
  46. package/dist/cjs/store/sqlite/open.js +119 -6
  47. package/dist/cjs/store/sqlite/open.js.map +1 -1
  48. package/dist/cjs/store/turso/engine.d.cts +9 -0
  49. package/dist/cjs/store/turso/engine.d.ts +9 -0
  50. package/dist/cjs/store/turso/engine.js +189 -0
  51. package/dist/cjs/store/turso/engine.js.map +1 -0
  52. package/dist/cjs/store/turso/lexical.js +16 -11
  53. package/dist/cjs/store/turso/lexical.js.map +1 -1
  54. package/dist/cjs/store/turso/native.d.cts +5 -1
  55. package/dist/cjs/store/turso/native.d.ts +5 -1
  56. package/dist/cjs/store/turso/native.js +157 -8
  57. package/dist/cjs/store/turso/native.js.map +1 -1
  58. package/dist/cjs/store/turso/open.js +172 -27
  59. package/dist/cjs/store/turso/open.js.map +1 -1
  60. package/dist/cjs/store/turso/reconcile.d.cts +0 -1
  61. package/dist/cjs/store/turso/reconcile.d.ts +0 -1
  62. package/dist/cjs/store/turso/reconcile.js +22 -23
  63. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  64. package/dist/cjs/store/types.d.cts +2 -0
  65. package/dist/cjs/store/types.d.ts +2 -0
  66. package/dist/cjs/store/types.js.map +1 -1
  67. package/dist/esm/cli/constants.d.ts +2 -0
  68. package/dist/esm/cli/constants.js +2 -0
  69. package/dist/esm/cli/constants.js.map +1 -0
  70. package/dist/esm/cli/named.js +2 -1
  71. package/dist/esm/cli/named.js.map +1 -1
  72. package/dist/esm/cli/shared.js +6 -5
  73. package/dist/esm/cli/shared.js.map +1 -1
  74. package/dist/esm/cli.js +10 -9
  75. package/dist/esm/cli.js.map +1 -1
  76. package/dist/esm/embed/query.js +1 -1
  77. package/dist/esm/embed/query.js.map +1 -1
  78. package/dist/esm/store/duckdb/engine.d.ts +11 -0
  79. package/dist/esm/store/duckdb/engine.js +18 -0
  80. package/dist/esm/store/duckdb/engine.js.map +1 -0
  81. package/dist/esm/store/duckdb/lexical.js +11 -11
  82. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  83. package/dist/esm/store/duckdb/native.d.ts +5 -2
  84. package/dist/esm/store/duckdb/native.js +16 -12
  85. package/dist/esm/store/duckdb/native.js.map +1 -1
  86. package/dist/esm/store/duckdb/open.js +40 -2
  87. package/dist/esm/store/duckdb/open.js.map +1 -1
  88. package/dist/esm/store/duckdb/ordered-bm25.d.ts +3 -3
  89. package/dist/esm/store/duckdb/ordered-bm25.js +4 -9
  90. package/dist/esm/store/duckdb/ordered-bm25.js.map +1 -1
  91. package/dist/esm/store/engine.d.ts +28 -0
  92. package/dist/esm/store/engine.js +85 -0
  93. package/dist/esm/store/engine.js.map +1 -0
  94. package/dist/esm/store/native.d.ts +8 -5
  95. package/dist/esm/store/native.js +59 -57
  96. package/dist/esm/store/native.js.map +1 -1
  97. package/dist/esm/store/open.js +24 -7
  98. package/dist/esm/store/open.js.map +1 -1
  99. package/dist/esm/store/sqlite/engine.d.ts +12 -0
  100. package/dist/esm/store/sqlite/engine.js +16 -0
  101. package/dist/esm/store/sqlite/engine.js.map +1 -0
  102. package/dist/esm/store/sqlite/lexical.js +2 -3
  103. package/dist/esm/store/sqlite/lexical.js.map +1 -1
  104. package/dist/esm/store/sqlite/open.js +38 -7
  105. package/dist/esm/store/sqlite/open.js.map +1 -1
  106. package/dist/esm/store/turso/engine.d.ts +9 -0
  107. package/dist/esm/store/turso/engine.js +14 -0
  108. package/dist/esm/store/turso/engine.js.map +1 -0
  109. package/dist/esm/store/turso/lexical.js +9 -9
  110. package/dist/esm/store/turso/lexical.js.map +1 -1
  111. package/dist/esm/store/turso/native.d.ts +5 -1
  112. package/dist/esm/store/turso/native.js +16 -10
  113. package/dist/esm/store/turso/native.js.map +1 -1
  114. package/dist/esm/store/turso/open.js +44 -3
  115. package/dist/esm/store/turso/open.js.map +1 -1
  116. package/dist/esm/store/turso/reconcile.d.ts +0 -1
  117. package/dist/esm/store/turso/reconcile.js +3 -8
  118. package/dist/esm/store/turso/reconcile.js.map +1 -1
  119. package/dist/esm/store/types.d.ts +2 -0
  120. package/dist/esm/store/types.js.map +1 -1
  121. package/package.json +21 -7
  122. package/skills/sense/EXAMPLES.md +0 -99
  123. package/skills/sense/SKILL.md +0 -145
  124. package/skills/sense/references/search.md +0 -63
  125. package/skills/sense/references/sql.md +0 -120
  126. package/skills/sense/references/stores/duckdb.md +0 -35
  127. package/skills/sense/references/stores/sqlite.md +0 -66
  128. package/skills/sense/references/stores/turso.md +0 -36
  129. package/skills/sense/references/terminology.md +0 -32
  130. package/skills/sense-bases/EXAMPLES.md +0 -103
  131. package/skills/sense-bases/SKILL.md +0 -147
  132. package/skills/sense-setup/EXAMPLES.md +0 -129
  133. package/skills/sense-setup/SKILL.md +0 -112
  134. package/skills/sense-setup/references/embeddings.md +0 -51
  135. package/skills/sense-setup/references/store-benchmarks.md +0 -16
  136. package/skills/sense-setup/references/stores/duckdb.md +0 -28
  137. package/skills/sense-setup/references/stores/sqlite.md +0 -21
  138. package/skills/sense-setup/references/stores/turso.md +0 -25
@@ -1,66 +0,0 @@
1
- # SQLite query guide
2
-
3
- Read this guide when `store` is `sqlite` or omitted.
4
-
5
- SQLite is the default store. It uses Node's built-in SQLite, keeps the cache at `.sense/cache.db`, supports concurrent sense commands through WAL, and implements the full search grammar documented here.
6
-
7
- ## `sense search` grammar
8
-
9
- Search text uses FTS5 syntax:
10
-
11
- - Bare words are joined with AND. If one word is absent, the lexical signal returns no row.
12
- - `a OR b` matches either word.
13
- - `pref*` matches a token prefix.
14
- - `"exact phrase"` requires adjacent tokens.
15
- - `NEAR(a b, 5)` limits token distance.
16
- - `^term` requires the token at the start of a column.
17
- - `title:term` and `summary:term` filter by indexed column.
18
- - Parentheses group expressions.
19
-
20
- Double-quote terms containing punctuation. Bare `customer-facing` treats the hyphen as syntax, and a bare apostrophe is invalid.
21
-
22
- ## Raw text search
23
-
24
- `content` is an FTS5 virtual table. Refer to it by its table name in `MATCH`, even when the `FROM` clause has an alias:
25
-
26
- ```sql
27
- SELECT content.path, content.title
28
- FROM content
29
- WHERE content MATCH ?
30
- ORDER BY bm25(content, 10.0, 5.0, 1.0)
31
- LIMIT 20;
32
- ```
33
-
34
- The three BM25 weights rank title above summary above body. For queries that can match the machine-written unspaced-script sidecars, use all eight weights so the sidecars receive the same field weighting:
35
-
36
- ```sql
37
- ORDER BY bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)
38
- ```
39
-
40
- Raw `snippet(content, 2, '«', '»', '…', 10)` targets the authored body column. Avoid `-1` because it may choose a machine-written sidecar. SQLite's `snippet()` tokenizes the matched text again, so prefer `sense search` for large notes. Sense builds its own bounded passages on every store.
41
-
42
- ## Functions and unspaced scripts
43
-
44
- `has()`, `basename()`, and `segment()` are registered SQL functions. `sense search` handles Chinese, Japanese, Thai, Khmer, Lao, and Burmese automatically. Hand-written `MATCH` cannot be rewritten, so pass such terms through `segment()`:
45
-
46
- ```sql
47
- SELECT path FROM content WHERE content MATCH segment(?);
48
- ```
49
-
50
- `segment()` leaves other text unchanged.
51
-
52
- ## Dates and values
53
-
54
- Use SQLite's `datetime()` to compare ISO 8601 values with different offsets:
55
-
56
- ```sql
57
- WHERE datetime(created) >= datetime(?)
58
- ```
59
-
60
- SQLite's `now` is UTC. A calendar-day query in the machine's local timezone needs `localtime`:
61
-
62
- ```sql
63
- WHERE date(created) = date('now', 'localtime')
64
- ```
65
-
66
- Frontmatter values use SQLite storage classes. Booleans are integers, so compare true with 1. Lists and maps are JSON text and can be expanded with `json_each()`.
@@ -1,36 +0,0 @@
1
- # Turso query guide
2
-
3
- Read this guide when `store` is `turso`.
4
-
5
- Sense stores the cache at `.sense/cache.turso.db` and uses `@tursodatabase/database` with Tantivy full-text indexes. Use this experimental adapter when the tree benefits from the embedded Turso engine or its text-index behavior.
6
-
7
- The first open installs the native package if needed. Turso holds the cache file for the connection's lifetime, so another Sense command waits for the current command or watcher cycle to close it. The upstream engine supports more deployment and concurrency options than this adapter currently exposes. Sense opens a local file and does not configure sync, remote access, concurrent writers, or encryption.
8
-
9
- ## `sense search` grammar
10
-
11
- Bare words and quoted phrases work. Sense applies shared stemming and joins bare words with AND. It verifies phrases against authored text and uses n-gram sidecars for unspaced scripts.
12
-
13
- The portable search command does not accept FTS5 prefix, boolean, `NEAR`, initial-token or boost, column-filter, or grouping operators. Examples include `foo*`, `a OR b`, `NEAR(a b)`, `^term`, `title:term`, and `(a b)`. Sense raises `STORE_CAPABILITY_MISSING` instead of passing those forms to Tantivy.
14
-
15
- To express alternatives, run separate bounded searches and combine the paths, or use a frontmatter filter when the distinction is structured.
16
-
17
- ## Raw text search
18
-
19
- `content` is a regular table with store-owned stem and n-gram columns. SQLite's `content MATCH`, `snippet()`, and `bm25()` do not run here. Use `sense search` for ranked text retrieval.
20
-
21
- Turso exposes native `fts_match()` and `fts_score()`, but their valid scoring forms depend on the exact index projection. Sense owns those calls so that scores do not silently flatten. Saved queries should not call the store-owned text indexes.
22
-
23
- ## Functions and types
24
-
25
- `has()` and `basename()` work in raw SQL. Sense rewrites them into portable SQL because the client cannot register user-defined functions. Nested calls and placeholders are supported.
26
-
27
- `segment()` is unavailable. A query that calls it raises `STORE_CAPABILITY_MISSING`. `sense search` still handles unspaced scripts without it.
28
-
29
- Turso follows SQLite syntax for common date and JSON operations. Use `datetime()` for ISO 8601 comparisons and add `localtime` when a calendar-day query means the machine's local day:
30
-
31
- ```sql
32
- WHERE datetime(created) >= datetime(?)
33
- WHERE date(created) = date('now', 'localtime')
34
- ```
35
-
36
- Frontmatter lists and maps are stored as JSON text and can be expanded with `json_each()`.
@@ -1,32 +0,0 @@
1
- # Sense terminology
2
-
3
- This is the shared vocabulary for Sense's README, command help, API documentation and skills. Use these names when describing results and output controls.
4
-
5
- | Term | Meaning |
6
- |---|---|
7
- | Note | One Markdown file. A document is the same unit; SQL and some status fields use that name. |
8
- | Tree | The directory of notes configured as Sense's root. |
9
- | Frontmatter | The YAML metadata at the beginning of a note, such as its title, tags or status. |
10
- | Search result | One matching note, returned with its path, ranking information and any snippets. A result is not a section or a chunk. |
11
- | Snippet | A short passage selected around matching words, marked with `«»`. A note can return multiple snippets. A snippet need not contain a whole section. |
12
- | Heading | A Markdown heading such as `## Installation`. Its level is its depth, from 1 to 6. |
13
- | Section | A heading and the content following it until the next heading of any level, or the end of the note. Text before the first heading is not a section in the `sections` table. A parent section does not include subsequent subsections. |
14
- | Section description | The heading text, level, start and end lines, and estimated token count returned by `peek`. It does not contain the section's prose. |
15
- | Outline | The section descriptions in document order. It is a view of the note's structure, not another stored type of content. |
16
- | Chunk | A span of note content prepared for embedding. Chunking follows document structure and a token budget; a chunk is not necessarily one section or one snippet. |
17
- | Line range | Inclusive, one-based lines in the indexed source note, such as `L12-28`. Search can point to a section or an embedding chunk. A null range provides no narrower location. |
18
- | Outbound link | A link from the current note to another indexed note. `peek.outbound` returns distinct destination paths. |
19
- | Backlink | A link from another indexed note to the current note. `peek.backlinks` returns distinct source paths. |
20
- | Unresolved link | A written link target that did not resolve to an indexed note. This does not establish that the link is broken: attachments and notes outside the index can also be unresolved. `peek.unresolved` returns written targets. |
21
- | Scope | The indexed notes selected for an operation by its preset and filters. Scope is not an access-control boundary. |
22
- | Search signal | One source of ranking evidence: word matches, link relationships or vector similarity. A signal's rank is a candidate's position within that signal, not the final combined position. |
23
-
24
- ## Reading a section description
25
-
26
- The illustrative entry `## Installation [L12-28, ~90t]` describes a section headed “Installation”, starting at line 12 and ending at line 28, with an estimated 90 tokens. `peek` returns that description. Reading the indicated lines retrieves the content. With nested headings, the description ends before the next heading, even when that heading is deeper.
27
-
28
- ## Naming output controls
29
-
30
- Name the object and the unit: a snippet count is how many passages are returned; a snippet character limit controls passage length; a section count is how many section descriptions are returned; a link count controls link entries. A count limit is not pagination. An offset, when supported by an operation, skips entries before returning the requested count. Raw SQL supports `LIMIT` and `OFFSET`; search and peek do not currently expose offset options.
31
-
32
- Peek count limits request prefixes of the ordered section and link lists. Each search or peek reads one committed index snapshot; separately issued operations can observe later commits. See [finite retrieval and paging](search.md#finite-retrieval-and-paging) for search budgets and paging retained results.
@@ -1,103 +0,0 @@
1
- # sense-bases: worked translations
2
-
3
- Two real `.base` shapes translated end to end. Field names are illustrative; your tree
4
- defines its own.
5
-
6
- ## A. Filtered, sorted table with formulas
7
-
8
- The base: notes categorized as Movies, excluding templates, with a "To-watch" view of unseen
9
- entries sorted by score.
10
-
11
- ```yaml
12
- filters:
13
- and:
14
- - categories.contains(link("Movies"))
15
- - '!file.name.contains("Template")'
16
- views:
17
- - type: table
18
- name: To-watch
19
- filters:
20
- and:
21
- - last.isEmpty()
22
- - rating.isEmpty()
23
- order: [file.name, year, scoreImdb, runtime, director]
24
- sort:
25
- - { property: scoreImdb, direction: DESC }
26
- - { property: file.name, direction: ASC }
27
- ```
28
-
29
- One view, one query. Base filters AND view filters; `order` is the SELECT list; `sort` the
30
- ORDER BY. The link comparison is the literal written text (`[[Movies]]`), the name test uses
31
- `basename` so a folder named Templates does not false-positive:
32
-
33
- ```sql
34
- SELECT basename(f.path, '.md') AS name, f.year, f.scoreImdb, f.runtime, f.director
35
- FROM frontmatter f
36
- WHERE EXISTS (SELECT 1 FROM json_each(f.categories) WHERE value = '[[Movies]]')
37
- AND instr(basename(f.path), 'Template') = 0
38
- AND (f.last IS NULL OR f.last IN ('', '[]', '[null]'))
39
- AND (f.rating IS NULL OR f.rating IN ('', '[]', '[null]'))
40
- ORDER BY f.scoreImdb DESC, name ASC
41
- ```
42
-
43
- Saved in `sense.config.json` under `queries` as e.g. `to-watch`, it runs as `sense to-watch`.
44
-
45
- ## B. `this`-relative: the Related pattern
46
-
47
- The base: for the current note, every other note ranked by shared outgoing links, including
48
- anything it links or that links to it.
49
-
50
- ```yaml
51
- filters:
52
- and:
53
- - file.path != this.file.path
54
- formulas:
55
- Related: list(this.file.links).filter(list(file.links).containsAny(value)).unique()
56
- LinksOverlap: formula.Related.length
57
- views:
58
- - type: table
59
- name: Related
60
- filters:
61
- or:
62
- - formula.LinksOverlap > 2
63
- - file.hasLink(this)
64
- - this.file.hasLink(file)
65
- sort:
66
- - { property: formula.LinksOverlap, direction: DESC }
67
- limit: 20
68
- ```
69
-
70
- `this` becomes one bound path. The chained list formula (`filter(...containsAny).unique()`)
71
- is set intersection, which SQL states as an IN-filtered count; the `Related` list itself is
72
- the same rows through `GROUP_CONCAT`:
73
-
74
- ```sql
75
- WITH me AS (SELECT ? AS p),
76
- mine AS (SELECT DISTINCT dst FROM links, me WHERE src = me.p AND dst IS NOT NULL),
77
- scored AS (
78
- SELECT f.path,
79
- (SELECT COUNT(DISTINCT l.dst) FROM links l
80
- WHERE l.src = f.path AND l.dst IN (SELECT dst FROM mine)) AS overlap,
81
- EXISTS (SELECT 1 FROM links, me WHERE src = f.path AND dst = me.p) AS links_to_me,
82
- EXISTS (SELECT 1 FROM links, me WHERE src = me.p AND dst = f.path) AS linked_by_me,
83
- (SELECT GROUP_CONCAT(DISTINCT l.dst) FROM links l
84
- WHERE l.src = f.path AND l.dst IN (SELECT dst FROM mine)) AS shared_links
85
- FROM frontmatter f, me WHERE f.path != me.p)
86
- SELECT path, overlap, shared_links FROM scored
87
- WHERE overlap > 2 OR links_to_me OR linked_by_me
88
- ORDER BY overlap DESC
89
- LIMIT 20
90
- ```
91
-
92
- Run as `sense sql "..." current-note.md`, or saved with the `?` in place and the path passed
93
- as the parameter. The `or:` block reproduces Obsidian's semantics exactly: rows qualify by
94
- overlap, by linking to the note, or by being linked from it, so leaf notes the current note
95
- links to appear even at zero overlap, as they do in Obsidian.
96
-
97
- ## Notes that recur across translations
98
-
99
- - Multiple views over the same base share the base-level filter; write it once per query
100
- rather than factoring it out. Saved queries are self-contained by design.
101
- - `sort` keys referencing formulas sort by the SELECT alias.
102
- - A `groupBy` view keeps its rows; see the window-function shape in SKILL.md. Only a
103
- deliberately collapsed report wants `GROUP BY`.
@@ -1,147 +0,0 @@
1
- ---
2
- name: sense-bases
3
- description: "Translate an Obsidian Bases .base file into sense SQL that returns the same rows. Use when asked to convert, import, or reproduce a .base file (or a Bases view, filter, or formula) with sense, or when a markdown tree contains .base files whose views someone wants queryable outside Obsidian."
4
- ---
5
-
6
- # sense-bases
7
-
8
- A `.base` file is YAML: filters selecting notes, formulas computing values, and views ordering
9
- and grouping them. Obsidian evaluates it against its own metadata cache; sense holds the same
10
- data in SQL tables. Every Bases construct that selects or computes rows has a SQL equivalent.
11
- What has none is presentation (`columnSize`, card layout), those change pixels, not rows, so
12
- a translation loses nothing by ignoring them.
13
-
14
- Translate one view to one query: base-level `filters` AND the view's `filters`, the view's
15
- `order`/`sort`/`limit` as the SELECT list, ORDER BY, and LIMIT. A base with four views becomes
16
- four queries; saving them under the base's name in `sense.config.json`'s `queries` block keeps
17
- them runnable as `sense <name>`. The general query surface is the `sense` skill; this one only
18
- maps Bases constructs onto it.
19
-
20
- ## Properties
21
-
22
- | Bases | sense | notes |
23
- |---|---|---|
24
- | `note.field` / bare `field` | `f.<field>` | frontmatter column, same name |
25
- | `file.name` | `basename(f.path, '.md')` | the Name column: no directory, no extension |
26
- | a name with extension | `basename(f.path)` | the suffix arg is Unix basename's: present, stripped |
27
- | `file.folder` | `f.path LIKE 'Folder/%'` | for `inFolder("Folder")` |
28
- | `file.ext` | `'md'` | sense indexes only markdown |
29
- | `file.size` | `f._size` | bytes |
30
- | `file.mtime` | `datetime(f._mtime / 1000, 'unixepoch')` | stored as ms |
31
- | `file.ctime` | `datetime(f._ctime / 1000, 'unixepoch')` | see the caveat below |
32
- | `file.tags` | the `tags` table | frontmatter + inline `#tags`, deduplicated, Obsidian's grain |
33
- | `file.links` | `links WHERE embed = 0` | one row per distinct written target |
34
- | `file.embeds` | `links WHERE embed = 1` | |
35
- | `file.backlinks` | `links WHERE dst = f.path` | |
36
- | `this` | a bound `?` | see below |
37
-
38
- `file.ctime` is the filesystem's creation time. A `git clone` or a copy resets it, so on a
39
- tree that arrived as a checkout, every ctime is the checkout date and age formulas are wrong
40
- about authorship without being wrong about the filesystem. `map` warns when modification
41
- times have this shape; creation times share it.
42
-
43
- ## Predicates
44
-
45
- | Bases | sense |
46
- |---|---|
47
- | `==` `!=` `>` `<` `>=` `<=` | same |
48
- | `&&` `\|\|` `!` | `AND` `OR` `NOT` |
49
- | `and:` / `or:` / `not:` filter blocks | parenthesized `AND` / `OR` / `NOT` |
50
- | `field.isEmpty()` | `(f.field IS NULL OR f.field IN ('', '[]', '[null]'))` |
51
- | `file.hasTag("book")` | `EXISTS (SELECT 1 FROM tags WHERE tags.path = f.path AND (tag = 'book' OR tag LIKE 'book/%'))` |
52
- | `file.hasLink("Note")` | `EXISTS (SELECT 1 FROM links WHERE src = f.path AND dst = 'Note.md')` |
53
- | `list.contains(x)` | `EXISTS (SELECT 1 FROM json_each(f.list) WHERE value = x)` |
54
- | `string.contains(x)` | `instr(f.string, x) > 0` |
55
- | `list.containsAny(...)` | the `json_each` EXISTS with `value IN (...)` |
56
- | `/regex/.matches(x)` | no SQLite regex; `LIKE`/`GLOB` cover anchored and wildcard shapes |
57
- | `value.isType("object")` | `json_each`'s own `type` column: `... FROM json_each(f.field) j WHERE j.type = 'object'` |
58
-
59
- A field no note in the tree declares has no column at all, so a filter naming it errors with
60
- `no such column` instead of treating every row as empty. Obsidian's evaluator returns empty
61
- for unknown properties; SQL does not. Dropping the clause states the same thing the error
62
- did, and `SELECT name FROM pragma_table_info('frontmatter')` lists what exists.
63
-
64
- `isEmpty()` has four true cases because empty is stored three ways: NULL (key absent), `''`
65
- (empty string value), `'[]'` (a list written `[]`), and `'[null]'` (a list key above a bare
66
- `-`). Obsidian's `isEmpty()` is true for all of them; `IS NULL` alone finds only the first.
67
- The IN list is the whole test. `json_array_length()` is not: it reads `'[null]'` as length
68
- 1 and throws on plain strings. The same trap inside a list: `json_each` hands a string member
69
- to `value` as plain text, so `json_type(value)` throws `malformed JSON` on it; the scan's own
70
- `type` column is the discriminator.
71
-
72
- `contains(link("Movies"))` compares against a link value. In frontmatter, a list of links
73
- holds the written text, so the `json_each` comparison value is the literal `[[Movies]]`.
74
- `hasLink` compares resolved paths: `links.dst` is the resolved target (`NULL` for dead
75
- links), so the comparison value is the target's path, not its display name.
76
-
77
- sense's own `has(field, x)` is looser than both `contains` variants: exact membership on JSON
78
- arrays but substring on strings, so `has(f.status, 'active')` also matches `inactive`. It
79
- reads shorter when the field is known to be a list; the `json_each` form is the exact
80
- translation.
81
-
82
- ## Formulas
83
-
84
- Formulas are SELECT expressions. The Bases functions map onto SQLite's:
85
-
86
- | Bases | sense |
87
- |---|---|
88
- | `if(c, a, b)` | `iif(c, a, b)` or `CASE WHEN` |
89
- | `now()` / `today()` | `datetime('now')` / `date('now')` |
90
- | `date(x)` | `datetime(x)` |
91
- | `(date1 - date2).days` | `julianday(date1) - julianday(date2)` |
92
- | `(now() - acquired).months` | `(julianday('now') - julianday(f.acquired)) / 30.44` |
93
- | `x.round(n)` / `x.toFixed(n)` | `round(x, n)` / `printf('%.2f', x)` |
94
- | `d.format("YYYY-MM-DD")` | `strftime('%Y-%m-%d', d)` |
95
- | `x.toString()` | `CAST(x AS TEXT)` |
96
- | `list.length` | `json_array_length(f.list)` on a JSON column, a COUNT subquery on a table |
97
- | `list(a).filter(list(b).containsAny(value)).unique()` | an EXISTS-joined subquery; worked example in EXAMPLES.md |
98
-
99
- Bases durations are typed; SQL date arithmetic is julianday day-fractions. A formula chaining
100
- duration fields (`.days.round()`) flattens to arithmetic on the julianday difference.
101
-
102
- A formula referencing another formula becomes a CTE layer: SQL cannot read a SELECT alias in
103
- the same SELECT list, so each dependency level computes its formulas as columns and the next
104
- level reads them (`WITH t AS (SELECT ..., <level-1 formulas> FROM frontmatter) SELECT ...,
105
- <level-2 formulas> FROM t`). A five-formula chain is however many *levels* it has, not five
106
- CTEs. Formulas that only read base columns share one layer.
107
-
108
- ## Views
109
-
110
- - `sort:` (multi-key, each with direction) -> `ORDER BY a DESC, b ASC`. `limit:` -> `LIMIT`.
111
- - `groupBy` does not collapse rows. Obsidian shows every row bucketed under headers with
112
- per-group summaries. The SQL producing the same rows and numbers is ordering plus window
113
- functions, not GROUP BY:
114
-
115
- ```sql
116
- SELECT f.path, f.status, f.days,
117
- AVG(f.days) OVER (PARTITION BY f.status) AS group_avg,
118
- COUNT(*) OVER (PARTITION BY f.status) AS group_n
119
- FROM frontmatter f WHERE f.days IS NOT NULL
120
- ORDER BY f.status, f.days DESC
121
- ```
122
-
123
- A collapsed one-row-per-group report is plain `GROUP BY`. That is a different result than
124
- the Bases view shows.
125
- - View `summaries` (Sum, Average, Median via ordering, Unique, Filled, Checked) are the
126
- matching aggregates, windowed as above to keep the rows, or a separate aggregate query.
127
-
128
- ## `this`
129
-
130
- `this` is the note the base is evaluated against: the embedding note, or Obsidian's active
131
- pane. sense has no pane, so the caller supplies the path as a bound parameter: `sense sql
132
- "..." <path>`, or a saved query run as `sense <name> <path>`. A CTE keeps it single-bind:
133
-
134
- ```sql
135
- WITH me AS (SELECT ? AS p)
136
- ```
137
-
138
- and every `this.file.*` expression joins `me`. The full Related.base translation in
139
- EXAMPLES.md is the worked case: link overlap, mutual-link filters, and the shared-target list
140
- via `GROUP_CONCAT`, all from one bound path.
141
-
142
- ## Not translatable, and why it costs nothing
143
-
144
- - `columnSize`, card/table/list/map chrome: pixel layout over the same rows.
145
- - Obsidian choosing `this` from the active pane: there is no pane; the path parameter is the
146
- resolution.
147
- - Live re-evaluation as files change: default CLI queries incrementally prepare the needed capabilities; `--no-build` reads the last completed generation and requires its requested readiness without scanning live files. Nothing re-renders on its own.
@@ -1,129 +0,0 @@
1
- # sense-setup: worked configurations
2
-
3
- Four tree shapes, each with its config and the commands an agent actually runs. Field names and folder names are illustrative; your tree defines its own.
4
-
5
- ## A. Compiled wiki over immutable sources (the llm-wiki pattern)
6
-
7
- `raw/` holds ingested sources: big, never hand-edited. `wiki/` holds agent-compiled pages: linked, curated. The human drops sources and asks questions; the agent compiles and cites.
8
-
9
- ```json
10
- {
11
- "version": 5,
12
- "presets": {
13
- "default": { "include": ["wiki/**/*.md"], "k": 10 },
14
- "raw": { "include": ["raw/**/*.md"], "k": 5 }
15
- },
16
- "embed": { "model": "minishlab/potion-retrieval-32M", "provider": "static" },
17
- "queries": {
18
- "uncompiled": { "sql": "SELECT path, _mtime FROM frontmatter WHERE path LIKE 'raw/%' AND path NOT IN (SELECT dst FROM links WHERE dst IS NOT NULL) ORDER BY _mtime DESC" },
19
- "stubs": { "sql": "SELECT path, _size FROM frontmatter WHERE path LIKE 'wiki/%' AND _size < 500 ORDER BY _size" },
20
- "dead-links": { "sql": "SELECT src, target FROM links WHERE dst IS NULL ORDER BY src" }
21
- }
22
- }
23
- ```
24
-
25
- ```
26
- sense uncompiled # compile queue: raw files nothing cites yet
27
- sense search "how does attention scale" # wiki only (default preset)
28
- sense search "rotary embeddings" --preset raw # cite from sources, k=5
29
- sense dead-links # rows are broken citations to fix
30
- ```
31
-
32
- What the shape buys: bare search never ranks raw noise above compiled pages; the compile queue, stub list, and citation integrity are one saved query each. The maintenance loop is `uncompiled` → write the wiki page citing its sources → `dead-links` stays empty.
33
-
34
- ## B. Nightly agent memory, consolidated (the dreaming pattern)
35
-
36
- `memory/` accumulates small notes written at session end, each with `project`, `created`, and `kind` (observation / steer / decision) frontmatter. A consolidation agent runs periodically: prune, merge, surface contradictions for the human. Retired notes move to `archive/`: still queryable under their own preset, out of the default scope.
37
-
38
- ```json
39
- {
40
- "version": 5,
41
- "presets": {
42
- "default": { "include": ["memory/**/*.md"], "k": 10 },
43
- "archive": { "include": ["archive/**/*.md"], "k": 10 }
44
- },
45
- "embed": { "model": "minishlab/potion-retrieval-32M", "provider": "static" },
46
- "queries": {
47
- "project": { "sql": "SELECT path, kind, created, title FROM frontmatter WHERE project = ? ORDER BY created DESC" },
48
- "steers": { "sql": "SELECT path, created, title FROM frontmatter WHERE kind = 'steer' AND project = ? ORDER BY created" },
49
- "retirement": { "sql": "SELECT path, project, created FROM frontmatter WHERE datetime(created) < datetime('now','-90 day') AND path NOT IN (SELECT dst FROM links WHERE dst IS NOT NULL)" },
50
- "today": { "sql": "SELECT path, title FROM frontmatter WHERE date(created) = date('now','localtime') ORDER BY created" },
51
- "unfiled": { "sql": "SELECT path FROM frontmatter WHERE project IS NULL" }
52
- }
53
- }
54
- ```
55
-
56
- ```
57
- sense project acme-app # one project's notes, newest first
58
- sense today # `now` is UTC: a query whose boundary is a day needs 'localtime'
59
- # `retirement` above is a 90-day window, so it does not
60
- sense search "prefers terse commit messages" --k 5
61
- → memory/acme-app/2026-08-02-commits.md via: match
62
- → memory/acme-app/2026-06-11-style.md via: vector similarity: 0.71 # near-duplicate → merge candidate
63
- sense steers acme-app # oldest first: does a new steer override an old one?
64
- sense retirement # old + uncited → move to archive/
65
- sense unfiled # rows are notes missing a project. File them
66
- ```
67
-
68
- Presets are structural (live vs archived); per-project filtering is metadata (`project = ?`). One tree serves every project. Semantic search over the memory preset is the near-duplicate detector: search a new note's own summary and read `similarity` within the results. Whether an old steer was overridden is a question for the human, found by search, never decided by it.
69
-
70
- ## C. Evidence corpus: claims trace to sources
71
-
72
- `sources/` (immutable imports), `notes/` (one reading note per source), `reviews/` (synthesis whose claims must cite notes). The same shape fits incident reports and postmortems, user research and findings, due diligence and memos.
73
-
74
- ```json
75
- {
76
- "version": 5,
77
- "presets": {
78
- "default": { "include": ["reviews/**/*.md", "notes/**/*.md"], "k": 10 },
79
- "source": { "include": ["sources/**/*.md"], "k": 5 }
80
- },
81
- "embed": { "model": "minishlab/potion-retrieval-32M", "provider": "static" },
82
- "queries": {
83
- "unsupported": { "sql": "SELECT path, title FROM frontmatter WHERE path LIKE 'reviews/%' AND path NOT IN (SELECT src FROM links WHERE dst LIKE 'notes/%')" },
84
- "unread": { "sql": "SELECT path FROM frontmatter WHERE path LIKE 'sources/%' AND path NOT IN (SELECT dst FROM links WHERE dst IS NOT NULL)" },
85
- "by-topic": { "sql": "SELECT path, title FROM frontmatter WHERE path NOT LIKE 'sources/%' AND has(topics, ?) ORDER BY path" }
86
- }
87
- }
88
- ```
89
-
90
- ```
91
- sense search "replication failures in priming studies" # reviews + notes; sources never dilute
92
- sense search "the claim's exact phrasing" --preset source # citation pull on demand
93
- sense unsupported # rows are synthesis claims with no note behind them
94
- sense unread # the reading queue: sources no note cites
95
- ```
96
-
97
- ## D. A plain vault: zero configuration
98
-
99
- Someone else's Obsidian vault, heterogeneous with no structure worth declaring. The `sense init` starter is left untouched. The workflow is discovery:
100
-
101
- ```
102
- sense map # fields in use, hub notes, recent changes
103
- sense search "dataview queries" # words + links + meaning, one ranked list
104
- sense sql "SELECT j.value AS tag, COUNT(*) n FROM frontmatter, json_each(frontmatter.tags) j GROUP BY j.value ORDER BY n DESC LIMIT 20"
105
- sense peek "Plugins/dataview.md" # outline + links before reading
106
- ```
107
-
108
- Presets earn their place only when a tree has parts deserving different treatment; a tree that is one kind of thing needs none of the vocabulary above. On a big vault, raise `default`'s `k` and read `lines` ranges instead of whole files, or start from the starter's `large` preset.
109
-
110
- ## E. One vault, separate consumers
111
-
112
- Keep each consumer's configuration and cache under its own configuration directory while indexing one shared vault. `root` is relative to this config file, so saved SQL continues to use stable vault-relative paths.
113
-
114
- `~/.config/obsidian/tasks/sense.config.json`:
115
-
116
- ```json
117
- {
118
- "version": 5,
119
- "root": "../../../.obsidian",
120
- "presets": {
121
- "default": { "include": ["vault/tasks/**/*.md"], "k": 20 }
122
- },
123
- "queries": {
124
- "active": { "sql": "SELECT path, title FROM frontmatter WHERE path LIKE 'vault/tasks/%' AND status = 'active' ORDER BY path" }
125
- }
126
- }
127
- ```
128
-
129
- The database is `~/.config/obsidian/tasks/.sense/cache.db`; the indexed row remains `vault/tasks/...`, never a path back out through the configuration directory. A `general/` sibling can point at the same root with different presets and a separate `.sense/` cache.
@@ -1,112 +0,0 @@
1
- ---
2
- name: sense-setup
3
- description: "Set up the sense CLI on a markdown tree and make the decisions that shape its index: run sense init, choose sqlite, duckdb, or turso, define presets and search signals, configure embeddings, and design queryable note conventions. Use when creating or restructuring a markdown knowledge base, editing sense.config.json, choosing a store or embedding model, or deciding how agents should write notes for later retrieval."
4
- ---
5
-
6
- # sense setup and tree design
7
-
8
- Use this skill to create or change a sense configuration. Querying an existing tree belongs to the `sense` skill. Worked configurations for common tree shapes are in [EXAMPLES.md](EXAMPLES.md).
9
-
10
- ## Set up the tree
11
-
12
- Install the CLI and initialize it at the markdown root:
13
-
14
- ```sh
15
- npm install -g sensemaking
16
- cd path/to/notes
17
- sense init
18
- sense status
19
- ```
20
-
21
- `sense init` writes `sense.config.json`. Config discovery walks upward from the current directory. `--config <path>` selects another file.
22
-
23
- By default, the config indexes its own directory. A top-level `root` can point at another markdown tree. Relative roots resolve from the config directory, and `.sense/` remains beside the config. Use this when separate consumers need their own presets, saved queries, or caches over one tree.
24
-
25
- Globs and indexed paths are relative to the selected root. Run `sense status` and `sense map` after changing the config to confirm preset coverage and the selected store. A change that affects indexed content rebuilds the relevant cache and reports the reason. A build-enabled command replaces an incompatible derived cache format and repairs incomplete feature state; config `"build": false` or `--no-build` reports the required build without rebuilding or repairing the Sense index.
26
-
27
- ## Choose the store
28
-
29
- Choose from the intended use of the cache. Start with `sqlite` when no surrounding workflow favors
30
- another engine, then consider interoperability, SQL, connection behavior, and representative Sense
31
- measurements. A current timing result does not define an engine's long-term suitability.
32
-
33
- | Store | Choose it when | Main trade-off |
34
- |---|---|---|
35
- | `sqlite` | The tree needs the smallest setup, advanced FTS5 search syntax, or concurrent Sense commands | Included with Node; raw SQL is SQLite |
36
- | `duckdb` | The cache belongs in DuckDB analytical work over large datasets or in a local/cloud workflow, or needs DuckDB types and SQL | Experimental Sense adapter, large native install, one open connection at a time |
37
- | `turso` | The tree benefits from the embedded Rust engine or Tantivy index, or is evaluating the engine as its adapter evolves | Experimental Sense adapter, restricted search grammar, one open connection at a time |
38
-
39
- Read the matching selection guide before recommending or configuring a non-default store:
40
-
41
- - [SQLite selection guide](references/stores/sqlite.md)
42
- - [DuckDB selection guide](references/stores/duckdb.md)
43
- - [Turso selection guide](references/stores/turso.md)
44
-
45
- Read the [current store benchmark summary](references/store-benchmarks.md) only when performance could change the choice. The release assessment generates that shipped summary from the latest accepted all-store run.
46
-
47
- Each store uses a separate cache file. Changing `store` rebuilds the index instead of migrating the old cache. Commands and public tables are shared, while raw SQL and advanced word-search syntax follow the selected engine.
48
-
49
- ## Design presets
50
-
51
- A preset is a named, self-contained scope. The required `default` preset serves bare commands. Other presets are selected by name.
52
-
53
- | Field | Meaning | Default |
54
- |---|---|---|
55
- | `include` and `exclude` | Files covered by the preset | `include` is required |
56
- | `k` | Search result count | 10 |
57
- | `signals` | Enabled search signals and their reciprocal-rank weights | Every available signal at weight 1 |
58
- | `where` | Standing frontmatter filter | None |
59
-
60
- A file is indexed when any preset includes it. Presets can overlap. Files outside every preset do not enter the index.
61
-
62
- Use paths for stable layers such as `raw/`, `notes/`, and `archive/`. Use frontmatter filters for changing state such as `status`, `project`, and dates. A preset controls indexing, so its coverage must be computable from paths before frontmatter queries run.
63
-
64
- Use separate presets when parts of the tree need different search signals or result counts. A single-purpose tree does not need extra preset vocabulary.
65
-
66
- ## Configure vectors
67
-
68
- Vectors require two choices. The top-level `embed` block names the model and provider. Each preset's `signals` decides whether that scope uses vectors.
69
-
70
- ```json
71
- {
72
- "embed": {
73
- "model": "minishlab/potion-retrieval-32M",
74
- "provider": "static"
75
- },
76
- "presets": {
77
- "default": {
78
- "include": ["notes/**/*.md"],
79
- "signals": { "words": 1, "links": 1, "vectors": 1 }
80
- },
81
- "raw": {
82
- "include": ["raw/**/*.md"],
83
- "signals": { "words": 1, "links": 1 }
84
- }
85
- }
86
- }
87
- ```
88
-
89
- The config's `"build": true` default lets CLI queries prepare their needed capabilities. Set `"build": false` for manual-build or watch workflows. The first build-enabled vector search downloads a named static model and embeds its eligible scope. `sense build` and `sense watch` prepare all configured capabilities regardless of this setting; `sense download` fetches the model earlier when CI, offline work, or timing makes that useful. A config change that alters the provider, model, effective endpoint, vector coverage, or chunking can invalidate or rebuild vectors. Config `"build": false` or `--no-build` requires the requested vector scope to be ready and does not read newer live files.
90
-
91
- Read [embedding setup](references/embeddings.md) when choosing a provider or model, supporting a non-English tree, changing chunk size, or tuning signal weights.
92
-
93
- ## Design queryable notes
94
-
95
- Sense accepts heterogeneous markdown. These choices decide which queries will be reliable:
96
-
97
- - Consistent frontmatter fields make exact filters and saved reports possible. Inspect actual coverage and types with `sense map`.
98
- - ISO 8601 dates can be compared by the selected store's date functions. Mixed date formats can still be stored, but do not make a dependable time range.
99
- - A one-line `summary` appears in results and receives more lexical weight than body text. Its cost is keeping it current.
100
- - Folders are the natural unit for preset coverage. Frontmatter is the natural unit for status and ownership.
101
- - Small notes give precise hits and cheap whole-file reads. Large notes still work because `peek`, `sections`, and search `lines` point at ranges, and vector chunking splits long sections.
102
- - Save recurring questions under `queries`. Use `{ "sql": "..." }` for deterministic filters and reports, and `{ "search": "..." }` for ranked retrieval.
103
-
104
- Field names in examples are illustrative. The tree defines its own schema. Reserved frontmatter keys are `path`, `_mtime`, `_ctime`, `_size`, `_rank`, `_parse_error`, `content`, `links`, and `sections`; sense drops them with a warning.
105
-
106
- ## Validate the result
107
-
108
- Run `sense status` to confirm the store, cache, document count, embedding state, watcher state, and preset coverage. Run `sense map` to confirm the discovered fields and their observed types.
109
-
110
- Run every saved query after editing it. A parameterized SQL query can use any value because preparing the statement validates its columns and syntax before the parameter changes the result.
111
-
112
- Record choices that should outlive the setup conversation in the tree's maintained agent guidance or in an authored note. The sense config should contain executable settings and reusable queries, not prose policy.