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
brain/errors.py ADDED
@@ -0,0 +1,357 @@
1
+ """Project-specific exception hierarchy.
2
+
3
+ Internal helpers that can fail in user-visible ways raise these exceptions so
4
+ the CLI and MCP server layers can map them to their respective frameworks
5
+ (``typer.Exit`` / ``McpError``) without sharing framework-specific imports.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Sequence
13
+ from pathlib import Path
14
+
15
+
16
+ class BrainError(Exception):
17
+ """Base class for all brain-internal exceptions."""
18
+
19
+
20
+ class IdPrefixError(BrainError):
21
+ """Base class for failures resolving a UUID prefix to a document id."""
22
+
23
+
24
+ class IdPrefixTooShort(IdPrefixError):
25
+ """The supplied prefix is shorter than the 6-char minimum."""
26
+
27
+
28
+ class IdPrefixNotHex(IdPrefixError):
29
+ """The supplied prefix contains characters other than hex digits / hyphens."""
30
+
31
+
32
+ class IdPrefixNotFound(IdPrefixError):
33
+ """No document matches the supplied prefix."""
34
+
35
+ def __init__(self, prefix: str) -> None:
36
+ super().__init__(f"document not found: {prefix}")
37
+ self.prefix = prefix
38
+
39
+
40
+ class IdPrefixAmbiguous(IdPrefixError):
41
+ """Multiple documents match the supplied prefix."""
42
+
43
+ def __init__(self, prefix: str) -> None:
44
+ super().__init__(f"id prefix ambiguous: {prefix}")
45
+ self.prefix = prefix
46
+
47
+
48
+ class DirectoryRefreshError(BrainError):
49
+ """Raised when a Calendar / Contacts refresh fails (gws missing, JSON parse, etc.)."""
50
+
51
+
52
+ class EmbedError(BrainError):
53
+ """Base class for embedding-backend failures (network / transport / API / shape).
54
+
55
+ Every embedder backend raises a subclass: the Ollama-hosted models raise
56
+ :class:`brain.embeddings.OllamaEmbedError` and the Voyage SDK backend raises
57
+ :class:`brain.embeddings.VoyageEmbedError`. Callers that must degrade on an
58
+ embed failure regardless of the active backend (the MCP server's
59
+ ``_wrap_embed_error``, ``brain eval``'s per-query tolerance) ``except
60
+ EmbedError`` once instead of enumerating concrete backends. Inherits
61
+ :class:`BrainError` so the CLI / MCP layers map it without a
62
+ framework-specific import.
63
+ """
64
+
65
+
66
+ class AgeBootstrapError(BrainError):
67
+ """Raised when Apache AGE session/graph bootstrap fails (wave G0).
68
+
69
+ Wraps the raw ``psycopg.Error`` from ``LOAD 'age'`` / ``CREATE EXTENSION
70
+ age`` / ``create_graph`` so the public ``brain.db`` bootstrap helpers never
71
+ leak a framework-specific exception to the CLI / MCP layers (repo rule: a
72
+ custom exception inheriting :class:`BrainError`). The originating
73
+ ``psycopg.Error`` is preserved as ``__cause__`` (``raise ... from e``) for
74
+ diagnostics. The autocommit precondition violation in
75
+ :func:`brain.db.bootstrap_age` is a separate, plain :class:`BrainError`
76
+ (caller bug, not a DB failure)."""
77
+
78
+
79
+ class GraphBackendError(BrainError):
80
+ """Raised when an Apache AGE graph-backend operation fails (wave G0-4).
81
+
82
+ Wraps the raw ``psycopg.Error`` from a generated Cypher / catalog call so
83
+ the :mod:`brain.graph_rag.backends` layer never leaks a framework-specific
84
+ exception (repo rule: a custom exception inheriting :class:`BrainError`).
85
+ The originating ``psycopg.Error`` is preserved as ``__cause__``
86
+ (``raise ... from e``) for diagnostics.
87
+
88
+ Also raised for caller-side precondition violations surfaced before the DB
89
+ round-trip — an invalid graph name, a non-positive traversal depth /
90
+ frontier cap, an out-of-range edge-weight floor, a cross-tenant payload, or
91
+ an unparseable ``agtype`` result. Those are caller bugs (analogous to the
92
+ autocommit precondition in :func:`brain.db.bootstrap_age`), not DB
93
+ failures, but share the type so callers can ``except GraphBackendError``
94
+ once.
95
+ """
96
+
97
+
98
+ class GraphReconcileError(BrainError):
99
+ """Raised on a precondition failure in the GraphRAG reconcile layer (wave G1).
100
+
101
+ Surfaced by :mod:`brain.graph_rag.reconcile` before any graph write when the
102
+ caller asks to reconcile a document that does not exist (so there is no
103
+ ``documents.content_hash`` to anchor the per-aspect ``graph_index_state``
104
+ watermark). A caller bug — analogous to the cross-tenant payload guard in
105
+ :class:`GraphBackendError` — so it fails fast rather than silently writing a
106
+ half-formed graph. Inherits :class:`BrainError` so the CLI / MCP layers can
107
+ map it without a framework-specific import.
108
+ """
109
+
110
+
111
+ class CooccurrenceError(BrainError):
112
+ """Raised on invalid co-occurrence inputs (wave G1, GraphRAG).
113
+
114
+ Surfaced by :mod:`brain.graph_rag.cooccur` before any DB round-trip when a
115
+ derive-time parameter is degenerate — a non-positive sliding window (no pair
116
+ could ever co-occur) or a non-positive max-entities cap. These are caller
117
+ bugs (a misconfigured ``BRAIN_GRAPH_COOCCUR_WINDOW`` /
118
+ ``BRAIN_GRAPH_MAX_ENTITIES_PER_DOC``), so they fail fast rather than silently
119
+ producing an empty / wrong contribution set.
120
+ """
121
+
122
+
123
+ class WeightingError(BrainError):
124
+ """Raised on edge-weight inputs that cannot yield a normalized lift (wave G1).
125
+
126
+ Surfaced by :mod:`brain.graph_rag.weighting` when the supplied counts are
127
+ impossible for a real co-occurrence — a co-document count below 1 (no edge),
128
+ a marginal document count below 1, a co-document count exceeding the rarer
129
+ endpoint's marginal (you cannot co-occur in more documents than the rarer
130
+ entity appears in), or a generic-document-frequency ratio outside ``(0, 1]``.
131
+ The normalized lift is provably in ``(0, 1]`` once these are satisfied, so
132
+ the DB ``CHECK (weight > 0 AND weight <= 1)`` can never be violated.
133
+ """
134
+
135
+
136
+ class GraphTenantError(BrainError):
137
+ """Raised when a graph operation resolves to an empty ``tenant_id`` (wave G2).
138
+
139
+ GraphRAG is multi-tenant (spec §9 D9): every relational source-of-truth row,
140
+ AGE vertex/edge property, and generated query is scoped by ``tenant_id``,
141
+ which the schema declares ``TEXT NOT NULL``.
142
+ :func:`brain.graph_rag.tenancy.resolve_tenant` raises this before any DB
143
+ round-trip when neither an explicit ``--tenant`` override nor the configured
144
+ ``BRAIN_GRAPH_TENANT`` default yields a non-empty id. A caller bug (analogous
145
+ to the cross-tenant payload guard on :class:`GraphBackendError`), so it fails
146
+ fast rather than scoping a query to an empty tenant.
147
+ """
148
+
149
+
150
+ class GroupingError(BrainError):
151
+ """Raised on invalid scoped-subgraph grouping parameters (wave G2).
152
+
153
+ Surfaced by :mod:`brain.graph_rag.grouping` before any work when a
154
+ grouping knob is degenerate — a ``min_edge_weight`` / ``bridge_keep_weight``
155
+ outside ``[0.0, 1.0]`` or a non-positive ``theme_limit``. These are caller
156
+ bugs (a misconfigured ``BRAIN_GRAPH_MIN_EDGE_WEIGHT`` /
157
+ ``BRAIN_GRAPH_THEME_LIMIT``), so they fail fast rather than silently
158
+ producing an empty / wrong theme set. The grouping itself is pure logic,
159
+ never touching the DB, so this is the only failure mode.
160
+ """
161
+
162
+
163
+ class GraphModeUnavailable(BrainError):
164
+ """Raised when an explicit graph retrieval mode is not available in this wave.
165
+
166
+ Specifically, an explicit ``--mode global`` (CLI) / ``mode='global'`` (MCP)
167
+ request: global community-summary retrieval lands in G3, so the G2 core
168
+ **REJECTS** it (never degrades — only the *auto* router degrades
169
+ global→local; spec §17b decision 4). Raised by
170
+ :func:`brain.graph_rag.router.route` (and surfaced through
171
+ :func:`brain.graph_rag.retrieve.graph_rag_search`). The CLI maps it to
172
+ ``typer.BadParameter`` (exit 2) and the MCP server to
173
+ ``McpError(INVALID_PARAMS, ...)`` (waves G2-h/i). Inherits
174
+ :class:`BrainError` so those layers map it without a framework-specific
175
+ import.
176
+ """
177
+
178
+
179
+ class InteractionError(BrainError):
180
+ """Raised on invalid interaction inputs (unknown action / source).
181
+
182
+ The DB-level ``CHECK`` constraints on ``interactions.action`` and
183
+ ``interactions.source`` are the authoritative gate; this Python-side
184
+ error gives Typer / MCP a clean message before the SQL round-trip
185
+ when the enum value is obviously wrong (e.g., typo at the call site).
186
+ """
187
+
188
+
189
+ class PersonAmbiguous(BrainError):
190
+ """Multiple persons match a ``--person`` argument; caller must disambiguate."""
191
+
192
+ def __init__(self, query: str, candidates: list[str]) -> None:
193
+ candidate_list = ", ".join(candidates[:5])
194
+ super().__init__(
195
+ f"--person {query!r} matched {len(candidates)} people "
196
+ f"(candidates: {candidate_list}). Use a more specific name."
197
+ )
198
+ self.query = query
199
+ self.candidates = candidates
200
+
201
+
202
+ class PersonNotFound(BrainError):
203
+ """No person matched the ``--person`` argument."""
204
+
205
+ def __init__(self, query: str) -> None:
206
+ super().__init__(f"--person {query!r} matched no one in the directory")
207
+ self.query = query
208
+
209
+
210
+ class ElicitError(BrainError):
211
+ """Base class for tacit-knowledge elicitation failures."""
212
+
213
+
214
+ class ConnectError(BrainError):
215
+ """Raised on invalid inputs in the ``brain connect`` auto-link layer (Plan 07).
216
+
217
+ Surfaced by :mod:`brain.connect` before any DB round-trip when a scoring /
218
+ refresh parameter is degenerate — a non-positive candidate limit or
219
+ per-doc cap, or a confidence threshold outside ``(0.0, 1.0]``. These are
220
+ caller bugs (a misconfigured ``BRAIN_CONNECT_*`` knob), so they fail fast
221
+ rather than silently producing an empty / wrong suggestion set. Also raised
222
+ when a suggestion-id prefix cannot be resolved to a single
223
+ ``link_suggestions`` row. Inherits :class:`BrainError` so the CLI / MCP
224
+ layers map it without a framework-specific import.
225
+ """
226
+
227
+
228
+ class DemoError(BrainError):
229
+ """Raised when the ``brain demo`` sandbox cannot be provisioned / torn down.
230
+
231
+ Wraps Docker-CLI subprocess failures (daemon down, binary missing, timeout)
232
+ into a single actionable message that carries Docker's own stderr, so the
233
+ CLI layer maps it to a clean ``typer.Exit`` instead of leaking a raw
234
+ traceback. Inherits :class:`BrainError` so the demo CLI's existing
235
+ ``except BrainError`` catch handles it.
236
+ """
237
+
238
+
239
+ class ReviewError(BrainError):
240
+ """Raised when a ``brain review scan`` pass fails partway through (Plan 03).
241
+
242
+ The conflict scan calls the local Ollama enricher once per surviving
243
+ document pair; if Ollama becomes unreachable mid-scan
244
+ (:class:`OllamaUnavailable`), the findings produced before the failure are
245
+ already persisted (the connection is committed before this is raised), and
246
+ the scan stops with this partial-result error rather than silently
247
+ discarding work. ``findings`` carries the review findings written so far;
248
+ ``processed`` / ``total`` report how many entity candidates were adjudicated
249
+ before the failure so the CLI can print
250
+ "partial scan (N of M entities processed)". Inherits :class:`BrainError` so
251
+ the CLI / MCP layers map it without a framework-specific import.
252
+ """
253
+
254
+ def __init__(
255
+ self,
256
+ message: str,
257
+ *,
258
+ findings: Sequence[object] | None = None,
259
+ processed: int = 0,
260
+ total: int = 0,
261
+ ) -> None:
262
+ super().__init__(message)
263
+ self.findings: list[object] = list(findings or [])
264
+ self.processed = processed
265
+ self.total = total
266
+
267
+
268
+ class VaultNoteSyncError(BrainError):
269
+ """Raised when authoring a vault note fails to resolve or index.
270
+
271
+ Carries the per-file ``(path, reason)`` pairs (the same shape as
272
+ :class:`~brain.vault.sync.SyncReport.errors`) so the CLI can print each
273
+ one and exit non-zero — preserving ``brain note new``'s historical
274
+ behavior — while a library caller (the elicit session loop) can inspect
275
+ ``.errors`` programmatically.
276
+ """
277
+
278
+ def __init__(self, errors: Sequence[tuple[Path, str]]) -> None:
279
+ joined = "; ".join(f"{path}: {reason}" for path, reason in errors)
280
+ super().__init__(f"vault note sync failed: {joined}")
281
+ self.errors: list[tuple[Path, str]] = list(errors)
282
+
283
+
284
+ class EnrichmentError(BrainError):
285
+ """Raised when a per-document enrichment call fails unrecoverably.
286
+
287
+ "Unrecoverable" means the caller must NOT retry within this transaction
288
+ (e.g., the model returned malformed JSON twice in a row). The Q1-D
289
+ post-ingest hook catches this, logs a warning, and lets the ingest
290
+ commit with ``documents.summary`` still NULL — ``brain enrich --backfill``
291
+ can pick the row up later.
292
+ """
293
+
294
+
295
+ class OllamaUnavailable(EnrichmentError):
296
+ """The Ollama server is unreachable / returned a connection error / 5xx.
297
+
298
+ Distinct subclass of :class:`EnrichmentError` so the ingest hook can
299
+ ``except OllamaUnavailable`` specifically — the message it logs guides
300
+ the user to ``brain enrich --backfill`` once Ollama is back, while the
301
+ ``brain enrich --backfill`` CLI surfaces it as a clear "is Ollama
302
+ running?" error on the first row.
303
+ """
304
+
305
+
306
+ class AudioError(BrainError):
307
+ """Raised when ``brain audio`` script generation fails unrecoverably (Plan 04).
308
+
309
+ Covers the audio-overview script path: the graph layer is disabled, no
310
+ themes / communities surfaced for the requested person / topic, or the
311
+ script generator could not coax a structurally valid two-host dialogue out
312
+ of the model after its retry. Inherits :class:`BrainError` so the CLI maps
313
+ it to a clean red error + exit 1 without a framework-specific import. An
314
+ Ollama transport failure surfaces as :class:`OllamaUnavailable` (a separate
315
+ branch) rather than this error.
316
+ """
317
+
318
+
319
+ class TtsError(BrainError):
320
+ """Raised when a text-to-speech backend fails (Plan 04, Wave B).
321
+
322
+ Surfaced by :func:`brain.audio.make_tts_backend` for an unrecognized
323
+ ``--tts`` spec (caller bug) and by :class:`brain.audio.ShellTtsBackend`
324
+ when the user-supplied synthesis command is missing, exits non-zero, or
325
+ times out. The script ``.json`` / ``.md`` artifacts are always written
326
+ BEFORE synthesis, so they survive a TTS failure. Inherits
327
+ :class:`BrainError` so the CLI maps it to a clean red error + exit 1.
328
+ """
329
+
330
+
331
+ class IngestAmbiguousSource(BrainError):
332
+ """Raised when multiple documents share a single ``(source_kind, source_external_id)`` key.
333
+
334
+ This normally cannot happen — ``sources(kind, external_id)`` is UNIQUE
335
+ (migration 001), so one source row maps to exactly one document for
336
+ Krisp/Slack stdin ingests. The edge case arises when ``brain rm`` deletes
337
+ the document row but leaves the orphaned ``sources`` row behind, and two
338
+ concurrent ingests then both INSERT against that orphaned row. The result
339
+ is two ``documents`` rows sharing one source; the next re-ingest via
340
+ ``(kind, external_id)`` raises this error so the user can resolve the
341
+ duplicate manually rather than having ``--force`` silently pick one.
342
+ """
343
+
344
+
345
+ class DraftSkipped(BrainError):
346
+ """Reserved for future opt-in draft-skip paths.
347
+
348
+ .. deprecated::
349
+ No longer raised by the default Gmail ingest path (wave Q1-A,
350
+ 2026-05-11). All Gmail drafts are now ingested with
351
+ ``documents.draft = TRUE`` so the wiki quarantine (P1.6,
352
+ ``contentIndex.ts:397``) hides them from Quartz while
353
+ ``brain search`` / ``brain show`` still surface them. This class
354
+ is kept so future callers (e.g. a ``--skip-drafts`` flag) can
355
+ raise it without a schema change.
356
+ """
357
+
brain/eval/__init__.py ADDED
@@ -0,0 +1,129 @@
1
+ """Eval harness for second-brain hybrid search.
2
+
3
+ Public surface re-exported from sub-modules:
4
+
5
+ - :class:`EvalQuery` — a single query from the golden corpus
6
+ - :class:`EvalResult` / :class:`EvalReport` / :class:`CategorySummary` — run output types
7
+ - :func:`run_eval` — execute the harness against a live DB + embedder
8
+ - :func:`load_corpus` — parse a golden-corpus YAML file
9
+ - :class:`BaselineDiff` / :class:`QueryDiff` — diff data types
10
+ - :func:`save_baseline` / :func:`load_baseline` / :func:`diff_reports` — baseline I/O
11
+ - :class:`ConceptF1Report` / :class:`ConceptFixtureDoc` / :func:`concept_set_micro_f1`
12
+ / :func:`normalize_concept_pairs` / :func:`load_concept_fixture` — GraphRAG
13
+ concept-extractor eval gate (wave G2-j)
14
+ - :class:`LocalRetrievalScore` / :class:`ThemeRetrievalScore`
15
+ / :func:`score_local_docs` / :func:`score_themes` — GraphRAG graph-retrieval
16
+ eval scorers (wave G2-j)
17
+ - :class:`GraphDocEvalResult` / :class:`GraphThemesEvalResult`
18
+ / :class:`GraphEvalReport` / :func:`run_graph_eval` — parallel GraphRAG
19
+ graph-retrieval eval runner (wave G4-d; separate from the hybrid runner)
20
+ - :class:`GraphBaselineDiff` / :class:`GraphDocDiff` / :class:`GraphThemesDiff`
21
+ / :func:`save_graph_baseline` / :func:`load_graph_baseline`
22
+ / :func:`diff_graph_reports` — graph-eval canary baseline I/O (wave G4-d)
23
+ - :class:`EvalError` / :class:`EvalMetricError` / :class:`EvalCorpusError`
24
+ / :class:`EvalBaselineError` — exception hierarchy
25
+ """
26
+
27
+ from .answer_eval import (
28
+ AnswerEvalCase,
29
+ AnswerEvalReport,
30
+ AnswerScore,
31
+ answer_eval_report_to_dict,
32
+ load_answer_corpus,
33
+ run_answer_eval,
34
+ score_answer,
35
+ )
36
+ from .baseline import (
37
+ BaselineDiff,
38
+ QueryDiff,
39
+ diff_reports,
40
+ load_baseline,
41
+ mean_metrics_regressed,
42
+ save_baseline,
43
+ )
44
+ from .concept_extraction import (
45
+ ConceptF1Report,
46
+ ConceptFixtureDoc,
47
+ ConceptPair,
48
+ concept_set_micro_f1,
49
+ load_concept_fixture,
50
+ normalize_concept_pairs,
51
+ )
52
+ from .corpus import EvalQuery, load_corpus
53
+ from .errors import EvalBaselineError, EvalCorpusError, EvalError, EvalMetricError
54
+ from .graph_baseline import (
55
+ GraphBaselineDiff,
56
+ GraphDocDiff,
57
+ GraphThemesDiff,
58
+ diff_graph_reports,
59
+ load_graph_baseline,
60
+ save_graph_baseline,
61
+ )
62
+ from .graph_retrieval import (
63
+ LocalRetrievalScore,
64
+ ThemeRetrievalScore,
65
+ score_local_docs,
66
+ score_themes,
67
+ )
68
+ from .graph_runner import (
69
+ GraphDocEvalResult,
70
+ GraphEvalReport,
71
+ GraphThemesEvalResult,
72
+ run_graph_eval,
73
+ )
74
+ from .runner import CategorySummary, EvalReport, EvalResult, run_eval
75
+
76
+ __all__ = [
77
+ # Corpus
78
+ "EvalQuery",
79
+ "load_corpus",
80
+ # Answer-quality eval (Plan 06)
81
+ "AnswerEvalCase",
82
+ "AnswerScore",
83
+ "AnswerEvalReport",
84
+ "score_answer",
85
+ "run_answer_eval",
86
+ "load_answer_corpus",
87
+ "answer_eval_report_to_dict",
88
+ # Runner output
89
+ "EvalResult",
90
+ "EvalReport",
91
+ "CategorySummary",
92
+ "run_eval",
93
+ # Baseline I/O
94
+ "BaselineDiff",
95
+ "QueryDiff",
96
+ "save_baseline",
97
+ "load_baseline",
98
+ "diff_reports",
99
+ "mean_metrics_regressed",
100
+ # GraphRAG concept-extractor eval gate (G2-j)
101
+ "ConceptF1Report",
102
+ "ConceptFixtureDoc",
103
+ "ConceptPair",
104
+ "concept_set_micro_f1",
105
+ "load_concept_fixture",
106
+ "normalize_concept_pairs",
107
+ # GraphRAG graph-retrieval eval scorers (G2-j)
108
+ "LocalRetrievalScore",
109
+ "ThemeRetrievalScore",
110
+ "score_local_docs",
111
+ "score_themes",
112
+ # GraphRAG graph-retrieval eval runner (G4-d)
113
+ "GraphDocEvalResult",
114
+ "GraphThemesEvalResult",
115
+ "GraphEvalReport",
116
+ "run_graph_eval",
117
+ # GraphRAG graph-eval canary baseline I/O (G4-d)
118
+ "GraphBaselineDiff",
119
+ "GraphDocDiff",
120
+ "GraphThemesDiff",
121
+ "save_graph_baseline",
122
+ "load_graph_baseline",
123
+ "diff_graph_reports",
124
+ # Errors
125
+ "EvalError",
126
+ "EvalMetricError",
127
+ "EvalCorpusError",
128
+ "EvalBaselineError",
129
+ ]