secondbrain-py 0.2.1__py3-none-any.whl

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 (273) hide show
  1. brain/__init__.py +0 -0
  2. brain/__main__.py +18 -0
  3. brain/_capture_command.py +445 -0
  4. brain/_compose.py +52 -0
  5. brain/activity.py +206 -0
  6. brain/ask.py +631 -0
  7. brain/audio.py +591 -0
  8. brain/backfill/__init__.py +12 -0
  9. brain/backfill/search_extras.py +141 -0
  10. brain/backfill/source_rows.py +101 -0
  11. brain/bin/__init__.py +1 -0
  12. brain/bin/_launcher.py +107 -0
  13. brain/bin/down.py +8 -0
  14. brain/bin/launchd.py +268 -0
  15. brain/bin/monitor.py +570 -0
  16. brain/bin/rebuild.py +8 -0
  17. brain/bin/status.py +8 -0
  18. brain/bin/up.py +8 -0
  19. brain/brief.py +272 -0
  20. brain/capture.py +49 -0
  21. brain/chat.py +293 -0
  22. brain/cli.py +9760 -0
  23. brain/cli_claude.py +81 -0
  24. brain/cli_connect.py +285 -0
  25. brain/cli_demo.py +266 -0
  26. brain/config.py +1949 -0
  27. brain/connect.py +925 -0
  28. brain/db.py +540 -0
  29. brain/demo/__init__.py +452 -0
  30. brain/demo/corpus/manifest.json +403 -0
  31. brain/demo/embedder.py +74 -0
  32. brain/durations.py +84 -0
  33. brain/edit_session.py +156 -0
  34. brain/editor.py +67 -0
  35. brain/elicit/__init__.py +16 -0
  36. brain/elicit/detectors.py +250 -0
  37. brain/elicit/drafter.py +70 -0
  38. brain/elicit/queue.py +220 -0
  39. brain/elicit/schema.py +48 -0
  40. brain/elicit/session.py +445 -0
  41. brain/embedding_targets.py +54 -0
  42. brain/embeddings.py +424 -0
  43. brain/enrichment.py +808 -0
  44. brain/errors.py +357 -0
  45. brain/eval/__init__.py +129 -0
  46. brain/eval/answer_eval.py +281 -0
  47. brain/eval/baseline.py +265 -0
  48. brain/eval/concept_extraction.py +378 -0
  49. brain/eval/corpus.py +152 -0
  50. brain/eval/errors.py +19 -0
  51. brain/eval/graph_baseline.py +226 -0
  52. brain/eval/graph_retrieval.py +202 -0
  53. brain/eval/graph_runner.py +319 -0
  54. brain/eval/metrics.py +101 -0
  55. brain/eval/runner.py +223 -0
  56. brain/format.py +783 -0
  57. brain/gaps.py +390 -0
  58. brain/graph_rag/__init__.py +94 -0
  59. brain/graph_rag/_retrieval_common.py +113 -0
  60. brain/graph_rag/aggregates.py +303 -0
  61. brain/graph_rag/aliases/__init__.py +583 -0
  62. brain/graph_rag/backends/__init__.py +10 -0
  63. brain/graph_rag/backends/_age_helpers.py +473 -0
  64. brain/graph_rag/backends/age.py +782 -0
  65. brain/graph_rag/backends/base.py +272 -0
  66. brain/graph_rag/build.py +344 -0
  67. brain/graph_rag/communities.py +644 -0
  68. brain/graph_rag/communities_summary.py +437 -0
  69. brain/graph_rag/concepts.py +202 -0
  70. brain/graph_rag/cooccur.py +193 -0
  71. brain/graph_rag/cross_type.py +312 -0
  72. brain/graph_rag/extract.py +885 -0
  73. brain/graph_rag/fuse.py +371 -0
  74. brain/graph_rag/global_.py +412 -0
  75. brain/graph_rag/grouping.py +372 -0
  76. brain/graph_rag/person_resolver.py +167 -0
  77. brain/graph_rag/reconcile.py +792 -0
  78. brain/graph_rag/relational.py +353 -0
  79. brain/graph_rag/retrieve.py +526 -0
  80. brain/graph_rag/router.py +288 -0
  81. brain/graph_rag/schema.py +320 -0
  82. brain/graph_rag/sync.py +237 -0
  83. brain/graph_rag/tenancy.py +43 -0
  84. brain/graph_rag/themes.py +501 -0
  85. brain/graph_rag/weighting.py +202 -0
  86. brain/ingest/__init__.py +1926 -0
  87. brain/ingest/chunker.py +249 -0
  88. brain/ingest/docx.py +40 -0
  89. brain/ingest/gmail.py +621 -0
  90. brain/ingest/markdown.py +37 -0
  91. brain/ingest/pdf.py +61 -0
  92. brain/ingest/stdin.py +22 -0
  93. brain/ingest/sub_tokens.py +91 -0
  94. brain/ingest/text.py +16 -0
  95. brain/interactions.py +205 -0
  96. brain/maintenance.py +355 -0
  97. brain/mcp_server.py +3405 -0
  98. brain/migrations/001_init.sql +43 -0
  99. brain/migrations/002_qwen3_embedding.sql +17 -0
  100. brain/migrations/003_vault_model.sql +41 -0
  101. brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
  102. brain/migrations/005_derived_links.sql +67 -0
  103. brain/migrations/006_dedup_file_by_source_path.sql +25 -0
  104. brain/migrations/007_email_thread_and_draft.sql +15 -0
  105. brain/migrations/008_gmail_thread_unique.sql +11 -0
  106. brain/migrations/009_chunks_weighted_tsv.sql +28 -0
  107. brain/migrations/010_interactions.sql +30 -0
  108. brain/migrations/011_documents_summary.sql +23 -0
  109. brain/migrations/012_graphrag.sql +171 -0
  110. brain/migrations/013_graphrag_communities.sql +125 -0
  111. brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
  112. brain/migrations/015_interactions_graph_targets.sql +89 -0
  113. brain/migrations/016_index_hygiene.sql +61 -0
  114. brain/migrations/017_elicit.sql +30 -0
  115. brain/migrations/018_review_gap_signal_kinds.sql +40 -0
  116. brain/migrations/019_search_queries.sql +35 -0
  117. brain/migrations/020_link_suggestions.sql +40 -0
  118. brain/migrations/021_timeline_doc_date.sql +34 -0
  119. brain/migrations/022_link_suggestions_undirected.sql +84 -0
  120. brain/migrations/023_search_queries_fts_count.sql +28 -0
  121. brain/quartz_overrides/__init__.py +8 -0
  122. brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
  123. brain/quartz_overrides/quartz/build.ts +568 -0
  124. brain/quartz_overrides/quartz/cli/args.js +152 -0
  125. brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
  126. brain/quartz_overrides/quartz/cli/handlers.js +636 -0
  127. brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
  128. brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
  129. brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
  130. brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
  131. brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
  132. brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
  133. brain/quartz_overrides/quartz/components/Search.tsx +161 -0
  134. brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
  135. brain/quartz_overrides/quartz/components/index.ts +92 -0
  136. brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
  137. brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
  138. brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
  139. brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
  140. brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
  141. brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
  142. brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
  143. brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
  144. brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
  145. brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
  146. brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
  147. brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
  148. brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
  149. brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
  150. brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
  151. brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
  152. brain/quartz_overrides/quartz/processors/parse.ts +371 -0
  153. brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
  154. brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
  155. brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
  156. brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
  157. brain/quartz_overrides/quartz/static/emailThread.js +334 -0
  158. brain/quartz_overrides/quartz/static/favicon.ico +0 -0
  159. brain/quartz_overrides/quartz/static/icon.png +0 -0
  160. brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
  161. brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
  162. brain/quartz_overrides/quartz/static/reload.js +168 -0
  163. brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
  164. brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
  165. brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
  166. brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
  167. brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
  168. brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
  169. brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
  170. brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
  171. brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
  172. brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
  173. brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
  174. brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
  175. brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
  176. brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
  177. brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
  178. brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
  179. brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
  180. brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
  181. brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
  182. brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
  183. brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
  184. brain/quartz_overrides/quartz/styles/custom.scss +89 -0
  185. brain/quartz_overrides/quartz/styles/graph.scss +505 -0
  186. brain/quartz_overrides/quartz/util/ctx.ts +92 -0
  187. brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
  188. brain/quartz_overrides/quartz/util/path.ts +358 -0
  189. brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
  190. brain/quartz_overrides/quartz.config.ts +270 -0
  191. brain/quartz_overrides/quartz.layout.ts +314 -0
  192. brain/queries.py +1188 -0
  193. brain/rank_fusion.py +8 -0
  194. brain/resurface.py +210 -0
  195. brain/review/__init__.py +26 -0
  196. brain/review/emit.py +27 -0
  197. brain/review/queries.py +436 -0
  198. brain/review/render.py +196 -0
  199. brain/review/scans.py +355 -0
  200. brain/review/weekly.py +413 -0
  201. brain/search.py +704 -0
  202. brain/set_similarity.py +15 -0
  203. brain/setup.py +1205 -0
  204. brain/tags.py +56 -0
  205. brain/templates/Caddyfile.j2 +9 -0
  206. brain/templates/__init__.py +1 -0
  207. brain/templates/bin/__init__.py +1 -0
  208. brain/templates/bin/_brain-brief-fg.sh +25 -0
  209. brain/templates/bin/_brain-build-fg.sh +53 -0
  210. brain/templates/bin/_brain-watcher-fg.sh +65 -0
  211. brain/templates/bin/brain-down.sh +89 -0
  212. brain/templates/bin/brain-status.sh +83 -0
  213. brain/templates/bin/brain-up.sh +221 -0
  214. brain/templates/docker/age/Dockerfile +79 -0
  215. brain/templates/docker-compose.stock.yml.j2 +26 -0
  216. brain/templates/docker-compose.yml.j2 +34 -0
  217. brain/templates/env.example +190 -0
  218. brain/templates/launchd/__init__.py +1 -0
  219. brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
  220. brain/templates/launchd/com.brain.build.plist.j2 +46 -0
  221. brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
  222. brain/templates/skill/SKILL.md +63 -0
  223. brain/templates/skill/__init__.py +1 -0
  224. brain/timeline.py +834 -0
  225. brain/todo.py +124 -0
  226. brain/uninstall.py +185 -0
  227. brain/vault/__init__.py +115 -0
  228. brain/vault/_atomic.py +25 -0
  229. brain/vault/daily_index.py +228 -0
  230. brain/vault/derived_links/__init__.py +50 -0
  231. brain/vault/derived_links/directory.py +683 -0
  232. brain/vault/derived_links/fence.py +408 -0
  233. brain/vault/derived_links/gws.py +64 -0
  234. brain/vault/derived_links/participants.py +143 -0
  235. brain/vault/derived_links/pass_runner.py +362 -0
  236. brain/vault/derived_links/rules.py +137 -0
  237. brain/vault/export.py +683 -0
  238. brain/vault/frontmatter.py +165 -0
  239. brain/vault/graph.py +620 -0
  240. brain/vault/graph_format.py +388 -0
  241. brain/vault/link_rewrite.py +235 -0
  242. brain/vault/links.py +260 -0
  243. brain/vault/note_builder.py +211 -0
  244. brain/vault/paths.py +55 -0
  245. brain/vault/quartz_overlay.py +236 -0
  246. brain/vault/rename.py +591 -0
  247. brain/vault/resolver.py +304 -0
  248. brain/vault/slug.py +127 -0
  249. brain/vault/sync.py +1513 -0
  250. brain/vault/sync_summaries.py +264 -0
  251. brain/vault/templates.py +145 -0
  252. brain/vault/watch.py +1052 -0
  253. brain/wiki/__init__.py +6 -0
  254. brain/wiki/_github_slugger.py +76 -0
  255. brain/wiki/_person_name.py +314 -0
  256. brain/wiki/build_homepage.py +541 -0
  257. brain/wiki/build_partial.py +273 -0
  258. brain/wiki/build_people.py +934 -0
  259. brain/wiki/build_related.py +758 -0
  260. brain/wiki/build_swap.py +585 -0
  261. brain/wiki/build_watcher.py +975 -0
  262. brain/wiki/edit_classifier.py +215 -0
  263. brain/wiki/errors.py +10 -0
  264. brain/wiki/fastpath_manifest.py +475 -0
  265. brain/wiki/fastpath_state.py +174 -0
  266. brain/wiki/install.py +296 -0
  267. brain/wiki/slug.py +111 -0
  268. secondbrain_py-0.2.1.dist-info/METADATA +195 -0
  269. secondbrain_py-0.2.1.dist-info/RECORD +273 -0
  270. secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
  271. secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
  272. secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
  273. secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,33 @@
1
+ -- Migration 014 — GraphRAG community summary staleness tracker (wave G3-c).
2
+ --
3
+ -- Additive + idempotent. Adds ONE nullable column to ``graph_communities``:
4
+ --
5
+ -- summary_members_hash TEXT NULL
6
+ -- The ``members_hash`` value that the community's CURRENT ``summary`` was
7
+ -- generated from. The eager summary pass (§17c Q10,
8
+ -- ``brain.graph_rag.communities_summary.summarize_communities``) treats a
9
+ -- community as NEEDING a (re)summary when
10
+ -- ``summary IS NULL OR summary_members_hash IS DISTINCT FROM members_hash``
11
+ -- i.e. a never-summarized community OR one whose membership changed since
12
+ -- its last summary. The G3-b delta-gate
13
+ -- (``brain.graph_rag.communities._update_reused_community``) MOVES
14
+ -- ``members_hash`` on a membership change while PRESERVING the old summary;
15
+ -- this column lets that staleness be detected WITHOUT ever blanking the
16
+ -- live summary — the existing summary stays queryable until a fresh one
17
+ -- replaces it.
18
+ --
19
+ -- ``IS DISTINCT FROM`` (not ``<>``) is what makes the predicate NULL-safe: a row
20
+ -- whose ``summary_members_hash`` is NULL (every row right after this migration,
21
+ -- and every freshly-minted community) is correctly seen as stale relative to its
22
+ -- non-NULL ``members_hash`` and gets summarized on the next pass.
23
+ --
24
+ -- No new index: the staleness scan is tenant-scoped over the (small) community
25
+ -- set, already covered by the ``(tenant_id, level)`` index from migration 013;
26
+ -- a dedicated index would not pay for itself at personal-corpus community counts.
27
+
28
+ BEGIN;
29
+
30
+ ALTER TABLE graph_communities
31
+ ADD COLUMN IF NOT EXISTS summary_members_hash TEXT NULL;
32
+
33
+ COMMIT;
@@ -0,0 +1,89 @@
1
+ -- Migration 015 — generalize ``interactions`` for graph-target feedback
2
+ -- (wave G4-a; spec docs/specs/2026-05-20-graphrag-design.md §17d Q2/Q5).
3
+ --
4
+ -- Additive + idempotent. Migration 010 created ``interactions`` as a
5
+ -- DOCUMENT-only feedback log (``document_id`` NOT NULL). G4 makes the graph
6
+ -- surfaces — entity / community / theme — FIRST-CLASS rateable targets while
7
+ -- keeping every existing document-only caller (``brain rate``, MCP
8
+ -- ``brain_show``) working unchanged. This migration:
9
+ --
10
+ -- 1. ``document_id`` → NULLABLE (a graph-target row has no document).
11
+ -- 2. ADD ``target_type TEXT NULL`` — one of 'entity' | 'community' | 'theme'.
12
+ -- 3. ADD ``target_id TEXT NULL`` — the durable id of the graph target
13
+ -- (entity UUID / community_key / theme key). TEXT (not UUID) because the
14
+ -- three target kinds do not share a single id type; the target_type names
15
+ -- which space the id lives in.
16
+ -- 4. ADD ``graph_retrieved BOOLEAN NOT NULL DEFAULT FALSE`` — PROVENANCE only
17
+ -- (a graph surface produced this row). It is NOT part of the target model;
18
+ -- a document row surfaced via a graph path is still a document row with
19
+ -- ``graph_retrieved = TRUE``. Existing rows backfill to FALSE.
20
+ --
21
+ -- Two CHECK constraints (the authoritative gate; the Python writer mirrors them
22
+ -- in ``brain.interactions``):
23
+ --
24
+ -- * target_type domain — NULL, or one of the three known kinds.
25
+ -- * EXACTLY ONE target shape (XOR) — a row is EITHER a document row
26
+ -- (``document_id`` set, both target_* NULL) OR a graph-target row
27
+ -- (``document_id`` NULL, both target_* set). The two disjuncts are mutually
28
+ -- exclusive (one demands ``document_id`` NOT NULL, the other NULL), so their
29
+ -- OR is a true XOR. Every pre-015 row satisfies the first disjunct, so the
30
+ -- constraint validates cleanly against existing data.
31
+ --
32
+ -- An additive partial index on (target_type, target_id) supports graph-target
33
+ -- lookups; it excludes the document-only rows (target_type NULL) to stay small.
34
+ --
35
+ -- NEVER edit a shipped migration — this is a NEW numbered file. Migration 010
36
+ -- and all prior migrations are untouched.
37
+
38
+ BEGIN;
39
+
40
+ -- 1. document_id becomes nullable. Idempotent: DROP NOT NULL on an already
41
+ -- nullable column is a successful no-op.
42
+ ALTER TABLE interactions
43
+ ALTER COLUMN document_id DROP NOT NULL;
44
+
45
+ -- 2-4. Additive columns. graph_retrieved backfills FALSE on existing rows.
46
+ ALTER TABLE interactions
47
+ ADD COLUMN IF NOT EXISTS target_type TEXT,
48
+ ADD COLUMN IF NOT EXISTS target_id TEXT,
49
+ ADD COLUMN IF NOT EXISTS graph_retrieved BOOLEAN NOT NULL DEFAULT FALSE;
50
+
51
+ -- 5. target_type domain CHECK. Postgres has no ADD CONSTRAINT IF NOT EXISTS,
52
+ -- so guard on pg_constraint by name to keep re-runs safe.
53
+ DO $$
54
+ BEGIN
55
+ IF NOT EXISTS (
56
+ SELECT 1 FROM pg_constraint WHERE conname = 'interactions_target_type_chk'
57
+ ) THEN
58
+ ALTER TABLE interactions
59
+ ADD CONSTRAINT interactions_target_type_chk
60
+ CHECK (target_type IS NULL
61
+ OR target_type IN ('entity', 'community', 'theme'));
62
+ END IF;
63
+ END$$;
64
+
65
+ -- 6. XOR target-shape CHECK: document-only OR graph-target-only, never both,
66
+ -- never neither.
67
+ DO $$
68
+ BEGIN
69
+ IF NOT EXISTS (
70
+ SELECT 1 FROM pg_constraint WHERE conname = 'interactions_target_xor_chk'
71
+ ) THEN
72
+ ALTER TABLE interactions
73
+ ADD CONSTRAINT interactions_target_xor_chk
74
+ CHECK (
75
+ (document_id IS NOT NULL
76
+ AND target_type IS NULL AND target_id IS NULL)
77
+ OR
78
+ (document_id IS NULL
79
+ AND target_type IS NOT NULL AND target_id IS NOT NULL)
80
+ );
81
+ END IF;
82
+ END$$;
83
+
84
+ -- 7. Partial index for graph-target lookups (entity/community/theme rows only).
85
+ CREATE INDEX IF NOT EXISTS interactions_target_idx
86
+ ON interactions (target_type, target_id)
87
+ WHERE target_type IS NOT NULL;
88
+
89
+ COMMIT;
@@ -0,0 +1,61 @@
1
+ -- Migration 016: index hygiene — drop one provably dead index, add two missing.
2
+ --
3
+ -- All operations are idempotent:
4
+ -- ``DROP INDEX IF EXISTS`` is a no-op when already gone;
5
+ -- ``CREATE INDEX IF NOT EXISTS`` is a no-op when already present.
6
+ --
7
+ -- 1. DROP documents_tsv_idx — the only PROVABLY dead index.
8
+ -- Code-level evidence: migration 009 moved FTS to the weighted
9
+ -- chunks.tsv (multi-field tsvector). The documents.tsv column is still
10
+ -- present (generated, no migration cost to keep), but no search query
11
+ -- plan uses this index. It is a ~12 MB GIN that carries unnecessary
12
+ -- write-amplification on every document INSERT / UPDATE.
13
+ --
14
+ -- 2. DEFERRED — the following six filter indexes also showed idx_scan=0 at
15
+ -- the 2026-05-25 audit, but the prod stats were reset by the 2026-05-22
16
+ -- AGE cutover (pg_restore). Three days of observations is too short a
17
+ -- window to call filter indexes "dead":
18
+ --
19
+ -- derived_links_rule_idx (on derived_links.rule)
20
+ -- documents_tags_idx (GIN on documents.tags)
21
+ -- directory_entries_email_idx (on directory_entries.email)
22
+ -- idx_documents_draft (on documents.draft)
23
+ -- idx_documents_sent_at (on documents.sent_at)
24
+ -- idx_documents_thread_id (on documents.thread_id)
25
+ --
26
+ -- Revisit after ~30 days of post-cutover prod traffic. Drop in a
27
+ -- migration 017 if idx_scan remains 0 at that point.
28
+ --
29
+ -- 3. NEVER DROP uq_documents_gmail_thread — it is a UNIQUE constraint
30
+ -- (indisunique=True, migration 008) that enforces the invariant
31
+ -- "one email_thread row per thread_id". UNIQUE indexes legitimately
32
+ -- show idx_scan=0 because uniqueness enforcement does not increment
33
+ -- idx_scan. Dropping it would silently remove a data-integrity guard.
34
+ --
35
+ -- 4. NEW: GIN index on documents.participants — the ``&&`` array-overlap
36
+ -- operator used by the --person filter (Q1-C) forces a seq-scan without
37
+ -- this index.
38
+ --
39
+ -- 5. NEW: partial btree index on documents.source_path — the file-ingest
40
+ -- dedup path (``WHERE source_path = %s``) forces a seq-scan without it.
41
+ -- Scoped to IS NOT NULL rows (stdin ingests use content_hash dedup and
42
+ -- have source_path NULL), keeping the index small.
43
+
44
+ BEGIN;
45
+
46
+ -- Drop the single provably-dead index (FTS moved to chunks.tsv in 009).
47
+ DROP INDEX IF EXISTS documents_tsv_idx;
48
+
49
+ -- GIN for ``documents.participants && ARRAY[...]`` array-overlap queries
50
+ -- (the --person filter in Q1-C; currently a full seq-scan).
51
+ CREATE INDEX IF NOT EXISTS documents_participants_idx
52
+ ON documents USING GIN (participants);
53
+
54
+ -- Partial btree for file-ingest dedup: ``WHERE source_path = %s``.
55
+ -- Restricts to IS NOT NULL so the index never covers stdin rows (which use
56
+ -- content_hash dedup instead), keeping the index small.
57
+ CREATE INDEX IF NOT EXISTS documents_source_path_idx
58
+ ON documents (source_path)
59
+ WHERE source_path IS NOT NULL;
60
+
61
+ COMMIT;
@@ -0,0 +1,30 @@
1
+ -- 017_elicit.sql — tacit-knowledge elicitation gap-state table.
2
+ -- Mutable lifecycle (surfaced → snoozed → dismissed → resolved); NOT in interactions
3
+ -- (that table is append-only). See docs/specs/2026-06-01-tacit-knowledge-elicitation-design.md.
4
+
5
+ CREATE TABLE IF NOT EXISTS elicitation_gaps (
6
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ tenant_id TEXT NOT NULL DEFAULT 'default',
8
+ signal_kind TEXT NOT NULL
9
+ CHECK (signal_kind IN ('delta','orphan','contradiction','user_flagged')),
10
+ target_type TEXT NOT NULL
11
+ CHECK (target_type IN ('person','org','project','topic','tool','doc')),
12
+ target_id TEXT NOT NULL,
13
+ score DOUBLE PRECISION NOT NULL CHECK (score >= 0),
14
+ evidence_ids TEXT[] NOT NULL DEFAULT '{}',
15
+ rationale TEXT NOT NULL DEFAULT '',
16
+ status TEXT NOT NULL DEFAULT 'surfaced'
17
+ CHECK (status IN ('surfaced','snoozed','dismissed','resolved')),
18
+ snoozed_until TIMESTAMPTZ,
19
+ resolved_note_id UUID REFERENCES documents(id) ON DELETE SET NULL,
20
+ first_surfaced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
21
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
22
+ );
23
+
24
+ CREATE UNIQUE INDEX IF NOT EXISTS elicitation_gaps_tenant_signal_target_idx
25
+ ON elicitation_gaps (tenant_id, signal_kind, target_id)
26
+ WHERE status <> 'resolved';
27
+
28
+ CREATE INDEX IF NOT EXISTS elicitation_gaps_queue_idx
29
+ ON elicitation_gaps (tenant_id, status, score DESC)
30
+ WHERE status IN ('surfaced','snoozed');
@@ -0,0 +1,40 @@
1
+ -- 018_review_gap_signal_kinds.sql
2
+ -- Additive + idempotent. Extends elicitation_gaps.signal_kind CHECK to the full
3
+ -- superset of all known signal kinds so no later migration needs to rewrite it.
4
+ -- Plan 03 adds 'stale'; Plan 08 (migration 019) depends on 'search_failure'
5
+ -- already being valid here, so this migration ships the complete six-value set.
6
+ -- Uses the same DO-block guard pattern as migration 015.
7
+ -- NEVER edit shipped migrations 001-017.
8
+
9
+ BEGIN;
10
+
11
+ DO $$
12
+ BEGIN
13
+ -- Drop the four-value constraint from 017 if it still exists.
14
+ -- (The inline CHECK from CREATE TABLE gets the auto-generated name below.)
15
+ IF EXISTS (
16
+ SELECT 1 FROM pg_constraint
17
+ WHERE conname = 'elicitation_gaps_signal_kind_check'
18
+ AND conrelid = 'elicitation_gaps'::regclass
19
+ ) THEN
20
+ ALTER TABLE elicitation_gaps
21
+ DROP CONSTRAINT elicitation_gaps_signal_kind_check;
22
+ END IF;
23
+
24
+ -- Add the six-value superset constraint under a versioned name.
25
+ -- Idempotent: no-op if already present from a prior run of this migration.
26
+ IF NOT EXISTS (
27
+ SELECT 1 FROM pg_constraint
28
+ WHERE conname = 'elicitation_gaps_signal_kind_v2_check'
29
+ AND conrelid = 'elicitation_gaps'::regclass
30
+ ) THEN
31
+ ALTER TABLE elicitation_gaps
32
+ ADD CONSTRAINT elicitation_gaps_signal_kind_v2_check
33
+ CHECK (signal_kind IN (
34
+ 'delta', 'orphan', 'contradiction', 'user_flagged',
35
+ 'stale', 'search_failure'
36
+ ));
37
+ END IF;
38
+ END$$;
39
+
40
+ COMMIT;
@@ -0,0 +1,35 @@
1
+ -- 019_search_queries.sql — lightweight log of every hybrid-search call.
2
+ -- Plan 08 (`brain gaps`): a repeated zero-result / no-click query is direct
3
+ -- evidence the brain is missing knowledge the user needs. ``interactions``
4
+ -- structurally cannot hold a zero-result search (migration 015's
5
+ -- document-XOR-graph-target CHECK requires a target, and a failed search has
6
+ -- none), so search-failure mining needs its own append-only table.
7
+ --
8
+ -- Additive + idempotent. Creates ONLY the ``search_queries`` table + indexes.
9
+ -- The ``elicitation_gaps.signal_kind`` CHECK already includes 'search_failure'
10
+ -- (shipped by migration 018) — this migration does NOT touch that constraint.
11
+ -- NEVER edit shipped migrations 001-018.
12
+
13
+ BEGIN;
14
+
15
+ CREATE TABLE IF NOT EXISTS search_queries (
16
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17
+ tenant_id TEXT NOT NULL DEFAULT 'default',
18
+ query TEXT NOT NULL,
19
+ result_count INT NOT NULL DEFAULT 0,
20
+ session_id UUID, -- nullable; MCP path mints one, CLI path NULL
21
+ source TEXT NOT NULL
22
+ CHECK (source IN ('cli', 'mcp', 'wiki')),
23
+ at TIMESTAMPTZ NOT NULL DEFAULT NOW()
24
+ );
25
+
26
+ -- Recency-ordered tenant scan backs both the read view and the detector window.
27
+ CREATE INDEX IF NOT EXISTS search_queries_tenant_at_idx
28
+ ON search_queries (tenant_id, at DESC);
29
+
30
+ -- Partial index for the zero-result hot path (the headline gap signal).
31
+ CREATE INDEX IF NOT EXISTS search_queries_zero_result_idx
32
+ ON search_queries (tenant_id, at DESC)
33
+ WHERE result_count = 0;
34
+
35
+ COMMIT;
@@ -0,0 +1,40 @@
1
+ -- Plan 07 — `brain connect` proactive auto-link suggestions.
2
+ --
3
+ -- Additive only: nothing in prior migrations is altered or dropped. One new
4
+ -- table backs the proactive suggestion queue. Each row is a candidate
5
+ -- (source_doc → target_doc) wikilink the user has NOT yet drawn, scored by a
6
+ -- RRF blend of an entity-graph affinity leg and an embedding affinity leg
7
+ -- (see ``src/brain/connect.py``). The accept/reject state machine is a
8
+ -- DEDICATED concern, separate from the ``interactions`` log (migration 010):
9
+ -- a suggestion is ``pending`` until the user accepts (optionally writing the
10
+ -- wikilink into the source vault file) or rejects it.
11
+ --
12
+ -- The ``UNIQUE (source_doc_id, target_doc_id)`` constraint makes
13
+ -- ``brain connect refresh`` safely idempotent: re-running upserts the score on
14
+ -- ``pending`` rows only (``ON CONFLICT ... DO UPDATE ... WHERE status =
15
+ -- 'pending'``), so accepted/rejected rows are frozen and never resurface.
16
+ -- Directed pairs are distinct rows: A→B and B→A may both be suggested.
17
+
18
+ BEGIN;
19
+
20
+ CREATE TABLE IF NOT EXISTS link_suggestions (
21
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
22
+ source_doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
23
+ target_doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
24
+ score FLOAT NOT NULL,
25
+ graph_score FLOAT,
26
+ embed_score FLOAT,
27
+ suggested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
28
+ status TEXT NOT NULL DEFAULT 'pending'
29
+ CHECK (status IN ('pending', 'accepted', 'rejected')),
30
+ actioned_at TIMESTAMPTZ,
31
+ CHECK (source_doc_id <> target_doc_id),
32
+ UNIQUE (source_doc_id, target_doc_id)
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_link_suggestions_status
36
+ ON link_suggestions (status);
37
+ CREATE INDEX IF NOT EXISTS idx_link_suggestions_source
38
+ ON link_suggestions (source_doc_id);
39
+
40
+ COMMIT;
@@ -0,0 +1,34 @@
1
+ -- Migration 021 — optional `doc_date` generated column for `brain timeline`
2
+ -- (Plan 05, Phase 3). Slot 021 per the cross-plan assignment in
3
+ -- docs/plans/2026-06-03-brain-next-features/README.md (migrations 018/019/020
4
+ -- belong to Plans 03/08/07 respectively; 016/017 already shipped).
5
+ --
6
+ -- ADDITIVE ONLY. The timeline's temporal anchor is
7
+ -- COALESCE(documents.sent_at, documents.ingested_at) — event time when known
8
+ -- (emails, Krisp), else ingest time. Phases 1 and 2 compute that inline in
9
+ -- every query. This migration materializes it as a generated STORED column +
10
+ -- a B-tree index so the repeated COALESCE expression is replaced by a single
11
+ -- indexed read. `brain.timeline._doc_date_expr` auto-detects the column via
12
+ -- information_schema and falls back to the inline COALESCE when it is absent,
13
+ -- so applying this migration is purely an optimization — zero behavior change.
14
+ --
15
+ -- References only `documents` columns frozen by earlier shipped migrations
16
+ -- (sent_at + participants from 007; ingested_at from 001). It does NOT
17
+ -- reference any object from migrations 018–020 (which may not yet exist when
18
+ -- this file is applied — the runner tolerates name-order gaps).
19
+ --
20
+ -- ⚠ Generated-column caveat: `ADD COLUMN … GENERATED … STORED` rewrites the
21
+ -- `documents` table in Postgres. At personal-corpus scale (≤ 5 000 rows) this
22
+ -- completes in well under a second, but it is not zero-cost. Existing rows are
23
+ -- backfilled automatically by Postgres. Idempotent (`IF NOT EXISTS`).
24
+
25
+ BEGIN;
26
+
27
+ ALTER TABLE documents
28
+ ADD COLUMN IF NOT EXISTS doc_date TIMESTAMPTZ
29
+ GENERATED ALWAYS AS (COALESCE(sent_at, ingested_at)) STORED;
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_documents_doc_date
32
+ ON documents (doc_date DESC);
33
+
34
+ COMMIT;
@@ -0,0 +1,84 @@
1
+ -- Plan 07 follow-up — make `link_suggestions` pairs UNDIRECTED.
2
+ --
3
+ -- Additive/idempotent: alters the migration-020 table in place. No data is
4
+ -- destroyed beyond redundant mirror rows (see the dedup step below).
5
+ --
6
+ -- THE BUG THIS FIXES (verified in live QA): migration 020 keyed uniqueness on
7
+ -- the DIRECTED pair ``UNIQUE (source_doc_id, target_doc_id)``, so A→B and B→A
8
+ -- were stored as two separate rows. ``brain connect refresh`` scores every
9
+ -- source doc independently, so it wrote BOTH orientations of the same unordered
10
+ -- pair — ~52% of the live pending queue was mirror duplicates. Worse, accepting
11
+ -- (or rejecting) one orientation left its mirror ``pending``, forcing the
12
+ -- reviewer to action the same pair twice.
13
+ --
14
+ -- THE FIX: a suggested link pair is undirected for review purposes. We keep
15
+ -- ``source_doc_id`` / ``target_doc_id`` as the BEST-SCORING orientation (so the
16
+ -- ``accept --write`` vault writeback still appends the ``## See Also`` wikilink
17
+ -- into a sensible "source" doc), but enforce ONE row per UNORDERED pair via a
18
+ -- functional unique index on ``(LEAST(source, target), GREATEST(source,
19
+ -- target))``. The application upsert (``connect._upsert_suggestion``) targets
20
+ -- this index and only flips orientation when a strictly-better score arrives;
21
+ -- the mirror of an already accepted/rejected pair can no longer be inserted.
22
+ --
23
+ -- This supersedes migration 020's comment ("Directed pairs are distinct rows").
24
+ -- Migration 020 is frozen and left untouched, per the repo migration rules.
25
+
26
+ BEGIN;
27
+
28
+ -- Step 1 — collapse existing mirror rows to one canonical row per UNORDERED
29
+ -- pair BEFORE the unique index is added (otherwise index creation would fail on
30
+ -- the live corpus's 501 mirror pairs). For each unordered pair keep the single
31
+ -- highest-priority row and delete the rest:
32
+ -- 1. status priority: accepted (3) > rejected (2) > pending (1)
33
+ -- 2. then higher blended ``score`` (keeps the better-scoring orientation)
34
+ -- 3. then lower ``id`` (stable, deterministic tie-break)
35
+ --
36
+ -- Consequences of this ordering, by case:
37
+ -- * pending + pending → keep the better-scoring orientation, drop the mirror
38
+ -- (the common ~52% case). Accepted/rejected untouched.
39
+ -- * pending + decided → keep the decided (accepted/rejected) row, drop the
40
+ -- stale pending mirror. NO accepted/rejected row is
41
+ -- deleted here.
42
+ -- * decided + decided → only reachable if the SAME pair was both accepted in
43
+ -- one orientation and rejected/accepted in the other
44
+ -- (contradictory historical decisions). The unique
45
+ -- index cannot hold both, so exactly one must go; we
46
+ -- keep accepted-over-rejected, then higher score. This
47
+ -- is the ONLY path that can drop a decided row, and it
48
+ -- only triggers on self-contradictory legacy data.
49
+ WITH ranked AS (
50
+ SELECT id,
51
+ row_number() OVER (
52
+ PARTITION BY LEAST(source_doc_id, target_doc_id),
53
+ GREATEST(source_doc_id, target_doc_id)
54
+ ORDER BY CASE status
55
+ WHEN 'accepted' THEN 3
56
+ WHEN 'rejected' THEN 2
57
+ ELSE 1
58
+ END DESC,
59
+ score DESC,
60
+ id ASC
61
+ ) AS rn
62
+ FROM link_suggestions
63
+ )
64
+ DELETE FROM link_suggestions
65
+ WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
66
+
67
+ -- Step 2 — drop the directed unique constraint from migration 020. The new
68
+ -- functional index (Step 3) is strictly stronger, so this would otherwise be a
69
+ -- redundant, confusing second uniqueness rule and an ambiguous ON CONFLICT
70
+ -- inference target.
71
+ ALTER TABLE link_suggestions
72
+ DROP CONSTRAINT IF EXISTS link_suggestions_source_doc_id_target_doc_id_key;
73
+
74
+ -- Step 3 — enforce ONE row per UNORDERED pair regardless of which doc is stored
75
+ -- as source vs target. ``LEAST`` / ``GREATEST`` over the two UUIDs canonicalize
76
+ -- the pair; the index value is identical for A→B and B→A. This is the inference
77
+ -- target for ``connect._upsert_suggestion``'s ON CONFLICT.
78
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_link_suggestions_unordered_pair
79
+ ON link_suggestions (
80
+ LEAST(source_doc_id, target_doc_id),
81
+ GREATEST(source_doc_id, target_doc_id)
82
+ );
83
+
84
+ COMMIT;
@@ -0,0 +1,28 @@
1
+ -- 023_search_queries_fts_count.sql — record the FTS (lexical) leg hit count.
2
+ -- Plan 08 (`brain gaps`) design gap, found in live QA: hybrid search's VECTOR
3
+ -- leg always returns nearest neighbours, so an off-corpus query logs
4
+ -- result_count > 0 (filler) and the result_count = 0 detector never fires. The
5
+ -- FTS (lexical) leg matching ZERO chunks is the real "knowledge gap" signal:
6
+ -- the corpus has no lexical trace of the query. This column records that leg's
7
+ -- hit count so `brain gaps` can key off `fts_count = 0`.
8
+ --
9
+ -- Additive + idempotent. Adds ONLY the nullable ``fts_count`` column + one
10
+ -- partial index. Old rows keep ``fts_count IS NULL`` and the detector falls
11
+ -- back to the historical ``result_count = 0`` semantics for them. NULL (not 0)
12
+ -- is the right default: a 0 default would mislabel every legacy row as a
13
+ -- lexical miss. NEVER edit shipped migrations 001-021.
14
+ --
15
+ -- Numbered 023 to avoid colliding with the concurrently-shipping 022
16
+ -- (link_suggestions); this migration references NO 022 objects.
17
+
18
+ BEGIN;
19
+
20
+ ALTER TABLE search_queries ADD COLUMN IF NOT EXISTS fts_count INT;
21
+
22
+ -- Partial index for the lexical-miss hot path (the new headline gap signal),
23
+ -- mirroring the existing result_count = 0 partial index from migration 019.
24
+ CREATE INDEX IF NOT EXISTS search_queries_fts_zero_idx
25
+ ON search_queries (tenant_id, at DESC)
26
+ WHERE fts_count = 0;
27
+
28
+ COMMIT;
@@ -0,0 +1,8 @@
1
+ """Quartz overlay sources — TypeScript/SCSS files that customise Quartz rendering.
2
+
3
+ This package exists solely so that ``importlib.resources.files("brain.quartz_overrides")``
4
+ resolves to this directory regardless of whether the brain package is installed in
5
+ editable mode (``pip install -e``) or as a regular wheel. The Python files inside are
6
+ not imported directly; the overlay step in ``brain.vault.quartz_overlay`` walks the
7
+ directory tree and copies everything here into a live Quartz workspace.
8
+ """
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env -S node --no-deprecation
2
+ // Brain wiki overlay — quartz/bootstrap-cli.mjs
3
+ //
4
+ // This file shadows the upstream ``~/brain-vault/.quartz/quartz/bootstrap-cli.mjs``.
5
+ // All upstream commands are preserved verbatim. The only addition is the
6
+ // ``build-partial`` command, which wires to ``partialBuildContent`` (exported from
7
+ // the handlers overlay at quartz/cli/handlers.js via build_partial_handler.js).
8
+ //
9
+ // T4 addition (Plan B v3 — per-file emit fast path):
10
+ // ``build-partial`` — partial rebuild for a single changed slug.
11
+ // Usage: node bootstrap-cli.mjs build-partial --slug <slug> [--directory <vault>] [--output <dir>]
12
+ // Handler: partialBuildContent (from ./cli/handlers.js → ./cli/build_partial_handler.js)
13
+ // Args: BuildPartialArgv (from ./cli/args.js)
14
+
15
+ import yargs from "yargs"
16
+ import { hideBin } from "yargs/helpers"
17
+ import {
18
+ handleBuild,
19
+ handleCreate,
20
+ handleUpdate,
21
+ handleRestore,
22
+ handleSync,
23
+ partialBuildContent,
24
+ } from "./cli/handlers.js"
25
+ import { CommonArgv, BuildArgv, CreateArgv, SyncArgv, BuildPartialArgv } from "./cli/args.js"
26
+ import { version } from "./cli/constants.js"
27
+
28
+ yargs(hideBin(process.argv))
29
+ .scriptName("quartz")
30
+ .version(version)
31
+ .usage("$0 <cmd> [args]")
32
+ .command("create", "Initialize Quartz", CreateArgv, async (argv) => {
33
+ await handleCreate(argv)
34
+ })
35
+ .command("update", "Get the latest Quartz updates", CommonArgv, async (argv) => {
36
+ await handleUpdate(argv)
37
+ })
38
+ .command(
39
+ "restore",
40
+ "Try to restore your content folder from the cache",
41
+ CommonArgv,
42
+ async (argv) => {
43
+ await handleRestore(argv)
44
+ },
45
+ )
46
+ .command("sync", "Sync your Quartz to and from GitHub.", SyncArgv, async (argv) => {
47
+ await handleSync(argv)
48
+ })
49
+ .command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
50
+ await handleBuild(argv)
51
+ })
52
+ // brain overlay (T4): fast partial rebuild for a single changed slug.
53
+ // Re-parses one file, emits its HTML (skipping ContentIndex), and bumps .build-id.
54
+ .command(
55
+ "build-partial",
56
+ "Partial rebuild for a single changed slug (brain fast path)",
57
+ BuildPartialArgv,
58
+ async (argv) => {
59
+ await partialBuildContent(argv)
60
+ },
61
+ )
62
+ .showHelpOnFail(false)
63
+ .help()
64
+ .strict()
65
+ .demandCommand().argv