brainiac-cli 0.16.0__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 (77) hide show
  1. brain/__init__.py +39 -0
  2. brain/__main__.py +14 -0
  3. brain/_assets/AGENTS.md +564 -0
  4. brain/_assets/overlay/template/brand/brand-guide.md +24 -0
  5. brain/_assets/overlay/template/keywords/glossary.md +15 -0
  6. brain/_assets/overlay/template/people/roster.md +14 -0
  7. brain/_assets/overlay/template/voice/voice-profile.md +34 -0
  8. brain/_assets/routines/manifest.json +257 -0
  9. brain/_assets/scripts/brain-brief-mac.plist +59 -0
  10. brain/_assets/scripts/brain-brief.sh +74 -0
  11. brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
  12. brain/_assets/scripts/brain-synthesis.sh +214 -0
  13. brain/_assets/scripts/install-brief-mac.sh +152 -0
  14. brain/_assets/scripts/install-brief-windows.ps1 +97 -0
  15. brain/_assets/scripts/register_tasks.py +386 -0
  16. brain/_assets/templates/company.md +21 -0
  17. brain/_assets/templates/concept.md +27 -0
  18. brain/_assets/templates/daily.md +20 -0
  19. brain/_assets/templates/decision.md +42 -0
  20. brain/_assets/templates/meeting.md +33 -0
  21. brain/_assets/templates/person.md +20 -0
  22. brain/_assets/templates/project.md +23 -0
  23. brain/_assets/templates/state-moc.md +40 -0
  24. brain/_version.py +12 -0
  25. brain/anchor.py +121 -0
  26. brain/audit.py +422 -0
  27. brain/backup.py +210 -0
  28. brain/brief.py +417 -0
  29. brain/capture.py +117 -0
  30. brain/chunk.py +249 -0
  31. brain/classification.py +134 -0
  32. brain/cli.py +1906 -0
  33. brain/config.py +368 -0
  34. brain/connect.py +362 -0
  35. brain/context.py +108 -0
  36. brain/core.py +3018 -0
  37. brain/doctor.py +1161 -0
  38. brain/egress.py +148 -0
  39. brain/embed.py +857 -0
  40. brain/encryption.py +217 -0
  41. brain/frontmatter.py +102 -0
  42. brain/golden_probe.py +678 -0
  43. brain/graph.py +369 -0
  44. brain/graphify.py +352 -0
  45. brain/index.py +1576 -0
  46. brain/ingest/__init__.py +19 -0
  47. brain/ingest/handlers/__init__.py +43 -0
  48. brain/ingest/handlers/base.py +95 -0
  49. brain/ingest/handlers/docx.py +78 -0
  50. brain/ingest/handlers/email.py +228 -0
  51. brain/ingest/handlers/html.py +142 -0
  52. brain/ingest/handlers/image.py +91 -0
  53. brain/ingest/handlers/pdf.py +99 -0
  54. brain/ingest/handlers/pptx.py +69 -0
  55. brain/ingest/handlers/tables.py +41 -0
  56. brain/ingest/handlers/text.py +43 -0
  57. brain/ingest/handlers/xlsx.py +100 -0
  58. brain/ingest/handlers/zip.py +163 -0
  59. brain/ingest/pipeline.py +839 -0
  60. brain/ingest/transcript.py +158 -0
  61. brain/init.py +870 -0
  62. brain/maintenance.py +2266 -0
  63. brain/mcp_adapter.py +217 -0
  64. brain/multihop.py +232 -0
  65. brain/notes.py +195 -0
  66. brain/overlay.py +183 -0
  67. brain/projection.py +79 -0
  68. brain/rerank.py +425 -0
  69. brain/snapshot.py +231 -0
  70. brain/update.py +743 -0
  71. brain/vectors.py +225 -0
  72. brainiac_cli-0.16.0.dist-info/METADATA +306 -0
  73. brainiac_cli-0.16.0.dist-info/RECORD +77 -0
  74. brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
  75. brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
  76. brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
  77. brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
brain/graph.py ADDED
@@ -0,0 +1,369 @@
1
+ """Derived wikilink graph: BFS + Personalized PageRank, DISCOVERY-ONLY (RET-03).
2
+
3
+ Structure in this vault is wikilinks, not folders (substrate-spec §1). For a
4
+ multi-entity / multi-hop question, following ``[[links]]`` between notes surfaces
5
+ context that neither lexical nor dense retrieval reaches in one shot. Two
6
+ on-demand traversals are offered:
7
+
8
+ * **Wikilink-BFS** — breadth-first neighbours of a seed set out to ``depth``
9
+ hops (treating links as undirected so backlinks count).
10
+ * **Personalized PageRank (PPR)** — random-walk-with-restart biased to the
11
+ seed set; ranks the whole reachable neighbourhood by graph centrality
12
+ *relative to the seeds*. Better than raw BFS when many notes are 1-2 hops
13
+ out and you want the few that matter.
14
+
15
+ THE GRAPH IS DISCOVERY-ONLY AND NEVER AUTHORITATIVE. It is derived from note
16
+ bodies (rebuildable, disposable) and its edges are heuristic (a ``[[link]]`` is
17
+ an association, not a verified claim). Every result is tagged
18
+ ``authoritative: False`` / ``provenance: "graph-derived (discovery-only)"``. Its
19
+ sole job is to nominate candidate note ids to feed back into the AUTHORITATIVE
20
+ surfaces (hybrid_search / get / grep); a curated note and the retrieval cascade
21
+ WIN on any conflict. Callers must never quote a graph result as fact without
22
+ confirming it on the cited note.
23
+
24
+ Built on demand from the ``notes`` table (id + body) of an open index
25
+ connection — no schema change, no migration, no persisted edge table.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import datetime
30
+ import re
31
+ import sqlite3
32
+ from dataclasses import dataclass, field
33
+ from typing import Any, Iterable
34
+
35
+ # [[target]] | [[target|alias]] | [[target#heading]] | [[target#heading|alias]]
36
+ # Alias text is matched non-greedily and right-anchored to the FINAL ]] so an
37
+ # alias containing nested brackets (e.g. "display [x]") doesn't truncate the
38
+ # match at the first ']' and drop the link entirely (M-5).
39
+ _WIKILINK = re.compile(r"\[\[([^\]\|#]+)(?:#[^\]\|]+)?(?:\|.+?)?\]\]")
40
+
41
+ PROVENANCE = "graph-derived (discovery-only)"
42
+
43
+
44
+ def parse_wikilinks(body: str) -> list[str]:
45
+ """Return the raw link targets in a note body, order-preserving, de-duped.
46
+
47
+ Strips ``#heading`` anchors and ``|alias`` display text; the target is the
48
+ note reference (an id, a path stem, or a title)."""
49
+ seen: dict[str, None] = {}
50
+ for m in _WIKILINK.finditer(body or ""):
51
+ target = m.group(1).strip()
52
+ if target:
53
+ seen.setdefault(target, None)
54
+ return list(seen)
55
+
56
+
57
+ @dataclass
58
+ class LinkGraph:
59
+ """A directed wikilink graph with a resolver for id/stem/title targets.
60
+
61
+ ``out[a]`` are the notes ``a`` links to; ``inn[b]`` are the notes that link
62
+ to ``b``. ``undirected_adj`` merges both directions (discovery treats a link
63
+ as a symmetric association)."""
64
+
65
+ out: dict[str, set[str]] = field(default_factory=dict)
66
+ inn: dict[str, set[str]] = field(default_factory=dict)
67
+ nodes: set[str] = field(default_factory=set)
68
+ unresolved: dict[str, list[str]] = field(default_factory=dict)
69
+
70
+ def neighbours(self, node: str) -> set[str]:
71
+ return self.out.get(node, set()) | self.inn.get(node, set())
72
+
73
+ @property
74
+ def undirected_adj(self) -> dict[str, set[str]]:
75
+ adj: dict[str, set[str]] = {n: set() for n in self.nodes}
76
+ for a, outs in self.out.items():
77
+ for b in outs:
78
+ adj.setdefault(a, set()).add(b)
79
+ adj.setdefault(b, set()).add(a)
80
+ return adj
81
+
82
+
83
+ def _build_resolver(rows: list[tuple[str, str, str]]) -> dict[str, str]:
84
+ """Map every alias (id, path stem, lowercased title) -> canonical note id."""
85
+ resolver: dict[str, str] = {}
86
+ for nid, title, path in rows:
87
+ resolver[nid] = nid
88
+ resolver[nid.lower()] = nid
89
+ stem = path.rsplit("/", 1)[-1]
90
+ if stem.endswith(".md"):
91
+ stem = stem[:-3]
92
+ resolver.setdefault(stem, nid)
93
+ resolver.setdefault(stem.lower(), nid)
94
+ if title:
95
+ resolver.setdefault(title.lower(), nid)
96
+ return resolver
97
+
98
+
99
+ def build_graph(
100
+ conn: sqlite3.Connection, *, extra_edges: Iterable[tuple[str, str]] | None = None
101
+ ) -> LinkGraph:
102
+ """Build the wikilink graph on demand from the index's ``notes`` table.
103
+
104
+ Discovery-only and derived: nothing is persisted; re-call to rebuild.
105
+
106
+ ``extra_edges`` (GRF-01, ADR-0003 Ruling 6, "Optional") folds additional
107
+ undirected adjacency pairs — the graphify build's INFERRED edges — into
108
+ the SAME graph before BFS/PPR run, so ``graph_expand`` can optionally
109
+ treat them as discovery-only traversal input. Never persisted here; the
110
+ caller decides whether to pass any."""
111
+ rows = conn.execute("SELECT id, title, path, body FROM notes").fetchall()
112
+ id_rows = [(r[0], r[1] or "", r[2] or "") for r in rows]
113
+ resolver = _build_resolver(id_rows)
114
+ g = LinkGraph()
115
+ for nid, _title, _path in id_rows:
116
+ g.nodes.add(nid)
117
+ g.out.setdefault(nid, set())
118
+ g.inn.setdefault(nid, set())
119
+ for nid, _title, _path, body in ((r[0], r[1], r[2], r[3]) for r in rows):
120
+ for target in parse_wikilinks(body or ""):
121
+ tgt = resolver.get(target) or resolver.get(target.lower())
122
+ if tgt is None:
123
+ g.unresolved.setdefault(nid, []).append(target)
124
+ continue
125
+ if tgt == nid:
126
+ continue
127
+ g.out[nid].add(tgt)
128
+ g.inn[tgt].add(nid)
129
+ for a, b in extra_edges or ():
130
+ if a in g.nodes and b in g.nodes and a != b:
131
+ g.out[a].add(b)
132
+ g.inn[b].add(a)
133
+ return g
134
+
135
+
136
+ def wikilink_bfs(
137
+ g: LinkGraph, seeds: Iterable[str], *, depth: int = 2
138
+ ) -> list[dict[str, Any]]:
139
+ """Breadth-first neighbours of the seeds out to ``depth`` hops (undirected).
140
+
141
+ Returns [{id, hops}] excluding the seeds themselves, nearest-first."""
142
+ adj = g.undirected_adj
143
+ seed_set = {s for s in seeds if s in g.nodes}
144
+ visited: dict[str, int] = {s: 0 for s in seed_set}
145
+ frontier = set(seed_set)
146
+ for hop in range(1, max(0, depth) + 1):
147
+ nxt: set[str] = set()
148
+ for node in frontier:
149
+ for nb in adj.get(node, set()):
150
+ if nb not in visited:
151
+ visited[nb] = hop
152
+ nxt.add(nb)
153
+ frontier = nxt
154
+ if not frontier:
155
+ break
156
+ out = [
157
+ {"id": nid, "hops": hops, "authoritative": False, "provenance": PROVENANCE}
158
+ for nid, hops in visited.items()
159
+ if nid not in seed_set
160
+ ]
161
+ out.sort(key=lambda d: (d["hops"], d["id"]))
162
+ return out
163
+
164
+
165
+ def personalized_pagerank(
166
+ g: LinkGraph,
167
+ seeds: Iterable[str],
168
+ *,
169
+ alpha: float = 0.85,
170
+ iters: int = 40,
171
+ tol: float = 1e-9,
172
+ ) -> dict[str, float]:
173
+ """Random-walk-with-restart biased to ``seeds`` over the undirected graph.
174
+
175
+ ``r = (1-alpha)*p + alpha * sum_j r[j] * (1/deg[j]) for j in adj``, where
176
+ ``p`` restarts uniformly onto the seed set. Returns id -> score (sums ~1)."""
177
+ adj = g.undirected_adj
178
+ nodes = list(g.nodes)
179
+ if not nodes:
180
+ return {}
181
+ seed_set = [s for s in seeds if s in g.nodes]
182
+ if not seed_set:
183
+ return {}
184
+ restart = {n: 0.0 for n in nodes}
185
+ for s in seed_set:
186
+ restart[s] = 1.0 / len(seed_set)
187
+ rank = dict(restart)
188
+ deg = {n: len(adj.get(n, set())) for n in nodes}
189
+ for _ in range(max(1, iters)):
190
+ nxt = {n: (1.0 - alpha) * restart[n] for n in nodes}
191
+ for j in nodes:
192
+ dj = deg[j]
193
+ if dj == 0:
194
+ # Dangling node: teleport its mass back to the restart set.
195
+ share = alpha * rank[j]
196
+ for s in seed_set:
197
+ nxt[s] += share / len(seed_set)
198
+ continue
199
+ spread = alpha * rank[j] / dj
200
+ for nb in adj[j]:
201
+ nxt[nb] += spread
202
+ delta = sum(abs(nxt[n] - rank[n]) for n in nodes)
203
+ rank = nxt
204
+ if delta < tol:
205
+ break
206
+ return rank
207
+
208
+
209
+ def graph_expand(
210
+ conn: sqlite3.Connection,
211
+ seeds: Iterable[str],
212
+ *,
213
+ depth: int = 2,
214
+ k: int = 10,
215
+ use_ppr: bool = True,
216
+ extra_edges: Iterable[tuple[str, str]] | None = None,
217
+ ) -> dict[str, Any]:
218
+ """On-demand multi-hop expansion for multi-entity / multi-hop queries.
219
+
220
+ Combines wikilink-BFS (reachability + hop distance) with PPR (centrality
221
+ relative to the seeds) and returns DISCOVERY-ONLY candidate note ids to feed
222
+ back into the authoritative surfaces. Never authoritative on its own.
223
+
224
+ ``extra_edges`` (GRF-01, optional): graphify's INFERRED edges, folded into
225
+ the SAME derived graph when the caller opts in (``brain graph-expand
226
+ --use-inferred``) — still discovery-only, still gated the same way."""
227
+ g = build_graph(conn, extra_edges=extra_edges)
228
+ seed_list = list(dict.fromkeys(seeds))
229
+ resolver = _build_resolver(
230
+ [(r[0], r[1] or "", r[2] or "")
231
+ for r in conn.execute("SELECT id, title, path FROM notes").fetchall()]
232
+ )
233
+ resolved_seeds = [resolver.get(s) or resolver.get(s.lower()) or s for s in seed_list]
234
+ known_seeds = [s for s in resolved_seeds if s in g.nodes]
235
+
236
+ bfs = wikilink_bfs(g, known_seeds, depth=depth)
237
+ bfs_hops = {d["id"]: d["hops"] for d in bfs}
238
+
239
+ ppr_scores: dict[str, float] = (
240
+ personalized_pagerank(g, known_seeds) if use_ppr else {}
241
+ )
242
+
243
+ candidates = set(bfs_hops)
244
+ if use_ppr:
245
+ candidates |= {n for n, s in ppr_scores.items() if s > 0.0}
246
+ candidates -= set(known_seeds)
247
+
248
+ ranked = sorted(
249
+ candidates,
250
+ key=lambda n: (-ppr_scores.get(n, 0.0), bfs_hops.get(n, 99), n),
251
+ )[:k]
252
+
253
+ # Title + classification lookup (classification lets the CLI egress gate
254
+ # filter graph candidates so the discovery surface cannot leak the existence
255
+ # of a withheld note).
256
+ meta = {
257
+ r[0]: (r[1], r[2])
258
+ for r in conn.execute("SELECT id, title, classification FROM notes").fetchall()
259
+ }
260
+ results = [
261
+ {
262
+ "id": nid,
263
+ "title": meta.get(nid, (nid, ""))[0],
264
+ "classification": meta.get(nid, (nid, ""))[1],
265
+ "hops": bfs_hops.get(nid),
266
+ "ppr": round(ppr_scores.get(nid, 0.0), 6),
267
+ "authoritative": False,
268
+ "provenance": PROVENANCE,
269
+ }
270
+ for nid in ranked
271
+ ]
272
+ return {
273
+ "seeds": seed_list,
274
+ "resolved_seeds": known_seeds,
275
+ "unresolved_seeds": [s for s in resolved_seeds if s not in g.nodes],
276
+ "depth": depth,
277
+ "method": ("wikilink-bfs+ppr" if use_ppr else "wikilink-bfs")
278
+ + ("+inferred" if extra_edges else ""),
279
+ "authoritative": False,
280
+ "provenance": PROVENANCE,
281
+ "note": "discovery-only candidate ids; confirm on the cited note via "
282
+ "`brain get <id>` before asserting — never authoritative.",
283
+ "results": results,
284
+ }
285
+
286
+
287
+ # --------------------------------------------------------------------------
288
+ # Curation folds (AUT-02, ADR-0003 Ruling 5 Sunday branch): stale wikilink
289
+ # targets + a staleness revisit sample. Both reuse this same derived graph —
290
+ # DISCOVERY-ONLY, never authoritative, exactly like graph_expand above.
291
+ # --------------------------------------------------------------------------
292
+ def stale_wikilink_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]:
293
+ """Outbound wikilinks whose target has vanished (no note resolves to it
294
+ any more) or has moved to ``archive/`` while still linked from an active
295
+ note. UNFILTERED (note-shaped, by id) — caller egress-gates ``from`` and
296
+ ``target`` before surfacing, same discipline as ``near_dup``."""
297
+ rows = conn.execute(
298
+ "SELECT id, title, path, classification, body FROM notes"
299
+ ).fetchall()
300
+ resolver = _build_resolver([(r[0], r[1] or "", r[2] or "") for r in rows])
301
+ meta = {
302
+ r[0]: {"id": r[0], "title": r[1], "path": r[2], "classification": r[3]}
303
+ for r in rows
304
+ }
305
+ out: list[dict[str, Any]] = []
306
+ for nid, _title, _path, _cls, body in rows:
307
+ for target in parse_wikilinks(body or ""):
308
+ resolved = resolver.get(target) or resolver.get(target.lower())
309
+ if resolved is None:
310
+ out.append({
311
+ "from": meta[nid], "target": None, "target_text": target,
312
+ "reason": "vanished",
313
+ })
314
+ continue
315
+ if resolved == nid:
316
+ continue
317
+ tpath = (meta[resolved]["path"] or "").replace("\\", "/")
318
+ if tpath.startswith("archive/") or "/archive/" in tpath:
319
+ out.append({
320
+ "from": meta[nid], "target": meta[resolved], "target_text": target,
321
+ "reason": "archived",
322
+ })
323
+ return out
324
+
325
+
326
+ def revisit_sample(
327
+ conn: sqlite3.Connection, today: datetime.date, *, k: int = 10
328
+ ) -> list[dict[str, Any]]:
329
+ """Notes overdue for a re-read, ranked by ``age_days * (centrality + 1)``.
330
+
331
+ Centrality is the existing wikilink-BFS+PPR module's Personalized
332
+ PageRank run with EVERY note as its own seed (uniform restart across the
333
+ whole corpus) — i.e. standard whole-corpus PageRank, reusing
334
+ ``personalized_pagerank`` rather than a new ranking module (this
335
+ supersedes the curation skill's previously-documented "global centrality
336
+ is gap G3, age-only fallback" framing). The ``+1`` smoothing means an
337
+ isolated/orphan note (centrality 0) still ranks by age alone rather than
338
+ scoring zero and never surfacing.
339
+
340
+ Upgrade path (s10, grf-01): once ``graphify`` builds INFERRED
341
+ (embedding-neighbour) edges, feed the merged explicit+INFERRED graph into
342
+ this same function for a richer centrality signal — the ranking formula
343
+ itself does not need to change.
344
+
345
+ UNFILTERED (note-shaped, by id) — caller egress-gates before surfacing.
346
+ """
347
+ g = build_graph(conn)
348
+ centrality = personalized_pagerank(g, list(g.nodes)) if g.nodes else {}
349
+ rows = conn.execute(
350
+ "SELECT id, title, path, classification, updated FROM notes"
351
+ ).fetchall()
352
+ scored: list[dict[str, Any]] = []
353
+ for nid, title, path, classification, updated in rows:
354
+ age_days = 0
355
+ try:
356
+ age_days = max(
357
+ (today - datetime.date.fromisoformat(str(updated)[:10])).days, 0
358
+ )
359
+ except (TypeError, ValueError):
360
+ pass
361
+ cscore = centrality.get(nid, 0.0)
362
+ score = age_days * (cscore + 1.0)
363
+ scored.append({
364
+ "id": nid, "title": title, "path": path, "classification": classification,
365
+ "updated": updated, "age_days": age_days,
366
+ "centrality": round(cscore, 6), "score": round(score, 3),
367
+ })
368
+ scored.sort(key=lambda d: (-d["score"], d["id"]))
369
+ return scored[:k]