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/graphify.py ADDED
@@ -0,0 +1,352 @@
1
+ """Graphify discovery build (GRF-01, ADR-0003 Ruling 6 + Ruling (a)).
2
+
3
+ Periodically builds a derived, DISCOVERY-ONLY graph: nodes = indexed notes,
4
+ edges = explicit wikilinks (``kind: WIKILINK``, exact reuse of
5
+ ``brain.graph.build_graph``) plus a capped, scored layer of embedding-neighbour
6
+ proposals (``kind: INFERRED``). Same doctrine as ``brain.graph``:
7
+ ``authoritative: false``, never authoritative on its own, and NEVER
8
+ auto-written into a note body — INFERRED edges are candidates for human review
9
+ only (a hot-queue entry / ``brain graphify`` output), gated through
10
+ ``egress.apply_gate`` before they reach any surface, exactly like
11
+ ``graph_expand`` candidates.
12
+
13
+ ADR-0003 Ruling (a) explicitly SUPERSEDES the earlier "documented only"
14
+ disposition (``core.py`` graphify branch, ``routines/manifest.json`` row
15
+ graphify-discovery) on two grounds, both closed by this module's design:
16
+
17
+ 1. *"No clean fold"* — this module gives the maintain branch a single
18
+ function to call, exactly like ``health``/``integrity``.
19
+ 2. *"Runtime budget"* — three caps bound the monthly build:
20
+ * **Drift gate** — a corpus manifest keyed by ``(note id, content
21
+ hash)``; unchanged since the last build => a no-op in milliseconds
22
+ (``manifest_unchanged``). The content hash is the SAME
23
+ ``content_hash`` column the index's own incremental sync (IDX-03)
24
+ already maintains — this module never re-hashes note bodies.
25
+ * **Embedding reuse** — INFERRED edges are scored from vectors ALREADY
26
+ stored in the index (``note_vectors`` reads the persisted per-chunk
27
+ vectors via the vector backend's ``get_vectors``/``search``
28
+ contract); this module never re-embeds the corpus.
29
+ * **Wall-clock budget** — the caller (``BrainCore.graphify``) times the
30
+ build and flags ``action_required`` past a 5-minute soft ceiling
31
+ (target <=60s at the current corpus scale, ADR-0003 Ruling 6).
32
+
33
+ Provenance note (session s10): ADR-0003 Appendix B pins ONLY
34
+ ``90 System/_ppr/ppr.py`` from the reference vault (already reused by s08's
35
+ whole-corpus PageRank in ``brain.graph.revisit_sample``) — no
36
+ ``_graphify``-named script is in the fingerprint list. Per the session brief
37
+ this build therefore ports the DESIGN directly from Ruling 6 (this module has
38
+ no upstream file to sha256-verify), not any unverified reference-vault file.
39
+
40
+ INFERRED-edge precision (HARDENED:grill): raw cosine similarity is tempered by
41
+ (a) a small "2-hop bridge" boost when two notes already share a wikilink
42
+ neighbour (a structural signal that they are plausibly related even though
43
+ they are not directly linked), and (b) a small recency boost for two recently
44
+ updated notes. Skip rules: no self-links (guaranteed — pairs are built over
45
+ ``i < j`` distinct ids); no already-linked pairs (skipped against the
46
+ explicit wikilink graph); no frontmatter/code-fence "anchors" — moot by
47
+ construction, because INFERRED edges score MEAN CHUNK VECTORS, and chunks are
48
+ built from ``note.body`` which ``frontmatter.parse_text`` has already split
49
+ the YAML frontmatter block out of (``brain.notes.load_note``) before
50
+ ``chunk_text`` ever sees it; a code fence inside the body is just more text
51
+ fed to the embedder, not a link anchor, so there is no anchor-parsing path
52
+ for stray frontmatter/code-fence content to leak into.
53
+ """
54
+ from __future__ import annotations
55
+
56
+ import datetime
57
+ import math
58
+ from typing import Any
59
+
60
+ from .graph import LinkGraph
61
+
62
+ GRAPH_SCHEMA_VERSION = 1
63
+ PROVENANCE = "graphify-derived (discovery-only)"
64
+
65
+ # ADR-0003 Ruling 6 caps.
66
+ DEFAULT_TOPK = 5 # per-note INFERRED cap (k <= 5)
67
+ DEFAULT_SCORE_FLOOR = 0.72 # fixed cosine threshold for a proposal
68
+ DEFAULT_GLOBAL_CAP_MULTIPLIER = 2.0 # INFERRED <= 2x explicit-edge count
69
+ DEFAULT_BUDGET_SECONDS = 60.0 # ADR target at current corpus scale
70
+ ACTION_REQUIRED_SECONDS = 300.0 # ADR: log action_required past 5 min
71
+
72
+ # HARDENED:grill tempering constants — small, capped nudges; cosine (already
73
+ # floor-gated) stays the dominant term.
74
+ _BRIDGE_BOOST_PER_SHARED = 0.05
75
+ _BRIDGE_BOOST_CAP = 0.15
76
+ _RECENCY_BOOST_CAP = 0.10
77
+ _RECENCY_HALF_LIFE_DAYS = 730.0 # ~2 years: beyond this, no recency boost
78
+
79
+
80
+ def corpus_manifest(conn) -> dict[str, str]:
81
+ """Per-note ``id -> content_hash`` manifest. REUSES the index's own
82
+ ``content_hash`` column (already maintained by incremental sync's
83
+ path+hash comparison, IDX-03) instead of re-hashing note bodies."""
84
+ rows = conn.execute("SELECT id, content_hash FROM notes").fetchall()
85
+ return {r[0]: r[1] or "" for r in rows}
86
+
87
+
88
+ def manifest_unchanged(old_state: dict[str, Any] | None, new_manifest: dict[str, str]) -> bool:
89
+ """True iff the corpus identity is unchanged since the last build — the
90
+ drift gate (ADR-0003 Ruling 6, ground 2). ``old_state`` is the persisted
91
+ ``manifest.json`` dict (``{"notes": {...}, "generation": N, ...}``)."""
92
+ if not old_state:
93
+ return False
94
+ return old_state.get("notes") == new_manifest
95
+
96
+
97
+ def _cosine(a: list[float], b: list[float]) -> float:
98
+ dot = sum(x * y for x, y in zip(a, b))
99
+ na = math.sqrt(sum(x * x for x in a))
100
+ nb = math.sqrt(sum(y * y for y in b))
101
+ if na == 0.0 or nb == 0.0:
102
+ return 0.0
103
+ return dot / (na * nb)
104
+
105
+
106
+ def note_vectors(conn, backend) -> dict[str, list[float]]:
107
+ """One representative vector per note: the mean of its chunk vectors
108
+ ALREADY stored by the index (embedding reuse — this never re-embeds).
109
+ A note with no indexed chunks (e.g. an empty body) is simply absent."""
110
+ note_rows = conn.execute("SELECT rowid, id FROM notes").fetchall()
111
+ chunk_rows = conn.execute("SELECT rowid, note_rowid FROM chunks").fetchall()
112
+ by_note: dict[int, list[int]] = {}
113
+ for crowid, nrowid in chunk_rows:
114
+ by_note.setdefault(nrowid, []).append(crowid)
115
+ all_chunk_ids = [c for cs in by_note.values() for c in cs]
116
+ vecs = backend.get_vectors(conn, all_chunk_ids) if all_chunk_ids else {}
117
+ out: dict[str, list[float]] = {}
118
+ for note_rowid, note_id in note_rows:
119
+ cvecs = [vecs[c] for c in by_note.get(note_rowid, []) if c in vecs]
120
+ if not cvecs:
121
+ continue
122
+ dim = len(cvecs[0])
123
+ out[note_id] = [sum(v[i] for v in cvecs) / len(cvecs) for i in range(dim)]
124
+ return out
125
+
126
+
127
+ def _bridge_boost(link_graph: LinkGraph, a: str, b: str) -> tuple[float, int]:
128
+ """A small boost when ``a``/``b`` already share a wikilink neighbour (a
129
+ "2-hop bridge" — structural evidence they are plausibly related even
130
+ though no direct link exists yet)."""
131
+ adj = link_graph.undirected_adj
132
+ shared = len(adj.get(a, set()) & adj.get(b, set()))
133
+ return min(shared, 3) * _BRIDGE_BOOST_PER_SHARED, shared
134
+
135
+
136
+ def _parse_date(raw: Any) -> datetime.date | None:
137
+ try:
138
+ return datetime.date.fromisoformat(str(raw)[:10])
139
+ except (TypeError, ValueError):
140
+ return None
141
+
142
+
143
+ def _recency_boost(today: datetime.date, updated_a: Any, updated_b: Any) -> float:
144
+ """A small boost for two notes both updated recently. Missing/unparsable
145
+ dates contribute no boost (neutral) rather than raising."""
146
+ da, db = _parse_date(updated_a), _parse_date(updated_b)
147
+ if da is None or db is None:
148
+ return 0.0
149
+ avg_age = ((today - da).days + (today - db).days) / 2.0
150
+ frac = max(0.0, 1.0 - avg_age / _RECENCY_HALF_LIFE_DAYS)
151
+ return frac * _RECENCY_BOOST_CAP
152
+
153
+
154
+ def build_inferred_edges(
155
+ conn,
156
+ backend,
157
+ link_graph: LinkGraph,
158
+ *,
159
+ today: datetime.date,
160
+ topk: int = DEFAULT_TOPK,
161
+ score_floor: float = DEFAULT_SCORE_FLOOR,
162
+ global_cap_multiplier: float = DEFAULT_GLOBAL_CAP_MULTIPLIER,
163
+ explicit_edge_count: int = 0,
164
+ ) -> list[dict[str, Any]]:
165
+ """Capped, scored embedding-neighbour proposals (ADR-0003 Ruling 6).
166
+
167
+ For each note, probe the vector backend (ANN under sqlite-vec, exact under
168
+ the brute-force fallback — same adapter contract ``near_dup`` already
169
+ uses) with that note's mean chunk vector to NOMINATE candidate neighbours,
170
+ then recompute TRUE cosine between the two notes' own mean vectors
171
+ (backend-independent scoring, mirrors ``BrainIndex.near_dup``). Skip
172
+ rules: no self (guaranteed by construction), no already-linked pair
173
+ (checked against ``link_graph``). A qualifying pair's raw cosine must
174
+ clear ``score_floor``; the final ``score`` additionally tempers cosine
175
+ with a 2-hop bridge boost + a recency boost (HARDENED:grill) and is what
176
+ the greedy cap-respecting selection below ranks by.
177
+
178
+ Caps are enforced ONCE, globally, by a single greedy walk over ALL
179
+ qualifying candidate pairs sorted by final score descending: a pair is
180
+ selected only while BOTH endpoints are still under the per-note ``topk``
181
+ degree AND the running total is still under
182
+ ``global_cap_multiplier * explicit_edge_count``. This is what makes the
183
+ per-note cap hold even for a node that is somebody else's top-k neighbour
184
+ without being in its own — inbound proposals are capped exactly like
185
+ outbound ones.
186
+ """
187
+ vecs = note_vectors(conn, backend)
188
+ ids = sorted(vecs)
189
+ if len(ids) < 2:
190
+ return []
191
+
192
+ chunk_to_note = {
193
+ int(crid): nid
194
+ for nid, crid in conn.execute(
195
+ "SELECT n.id, c.rowid FROM chunks c JOIN notes n ON n.rowid = c.note_rowid"
196
+ ).fetchall()
197
+ }
198
+ updated_by_id = dict(conn.execute("SELECT id, updated FROM notes").fetchall())
199
+
200
+ probe_k = max(topk * 4, topk + 10)
201
+ seen_pairs: dict[tuple[str, str], dict[str, Any]] = {}
202
+ for a in ids:
203
+ hits = backend.search(conn, vecs[a], probe_k)
204
+ best_per_neighbour: dict[str, float] = {}
205
+ for chunk_rowid, _backend_score in hits:
206
+ b = chunk_to_note.get(int(chunk_rowid))
207
+ if b is None or b == a or b not in vecs:
208
+ continue
209
+ cosine = _cosine(vecs[a], vecs[b])
210
+ if cosine > best_per_neighbour.get(b, -1.0):
211
+ best_per_neighbour[b] = cosine
212
+ # This node's own top-`topk` neighbours by raw cosine, above the floor.
213
+ ranked = sorted(best_per_neighbour.items(), key=lambda kv: -kv[1])[:topk]
214
+ for b, cosine in ranked:
215
+ if cosine < score_floor:
216
+ continue
217
+ if b in link_graph.undirected_adj.get(a, set()):
218
+ continue # already linked — no INFERRED duplicate of a real edge
219
+ key = (a, b) if a < b else (b, a)
220
+ if key in seen_pairs and seen_pairs[key]["cosine"] >= cosine:
221
+ continue
222
+ boost, shared = _bridge_boost(link_graph, a, b)
223
+ recency = _recency_boost(today, updated_by_id.get(a), updated_by_id.get(b))
224
+ score = cosine * (1.0 + boost + recency)
225
+ reason = f"embedding cosine {cosine:.3f}"
226
+ if shared:
227
+ reason += f"; {shared} shared wikilink neighbour(s)"
228
+ if recency > 0:
229
+ reason += "; both recently updated"
230
+ seen_pairs[key] = {
231
+ "kind": "INFERRED", "from": key[0], "to": key[1],
232
+ "cosine": round(cosine, 6), "score": round(score, 6),
233
+ "reason": reason,
234
+ }
235
+
236
+ global_cap = int(global_cap_multiplier * explicit_edge_count)
237
+ ordered = sorted(seen_pairs.values(), key=lambda e: -e["score"])
238
+ degree: dict[str, int] = {}
239
+ selected: list[dict[str, Any]] = []
240
+ for edge in ordered:
241
+ if len(selected) >= global_cap:
242
+ break
243
+ a, b = edge["from"], edge["to"]
244
+ if degree.get(a, 0) >= topk or degree.get(b, 0) >= topk:
245
+ continue
246
+ degree[a] = degree.get(a, 0) + 1
247
+ degree[b] = degree.get(b, 0) + 1
248
+ selected.append(edge)
249
+ return selected
250
+
251
+
252
+ def build_graph_artifact(
253
+ conn,
254
+ backend,
255
+ link_graph: LinkGraph,
256
+ *,
257
+ today: datetime.date,
258
+ topk: int = DEFAULT_TOPK,
259
+ score_floor: float = DEFAULT_SCORE_FLOOR,
260
+ global_cap_multiplier: float = DEFAULT_GLOBAL_CAP_MULTIPLIER,
261
+ ) -> dict[str, Any]:
262
+ """The build proper (sans provenance/generation stamping, which the
263
+ HOST-only caller adds — this stays a pure function of an open index
264
+ connection + backend + a prebuilt wikilink graph)."""
265
+ nodes = [
266
+ {"id": r[0], "type": r[1], "classification": r[2]}
267
+ for r in conn.execute("SELECT id, type, classification FROM notes").fetchall()
268
+ ]
269
+ wikilink_edges = [
270
+ {"kind": "WIKILINK", "from": a, "to": b}
271
+ for a, outs in sorted(link_graph.out.items())
272
+ for b in sorted(outs)
273
+ ]
274
+ inferred_edges = build_inferred_edges(
275
+ conn, backend, link_graph, today=today, topk=topk, score_floor=score_floor,
276
+ global_cap_multiplier=global_cap_multiplier,
277
+ explicit_edge_count=len(wikilink_edges),
278
+ )
279
+ return {
280
+ "nodes": nodes,
281
+ "edges": wikilink_edges + inferred_edges,
282
+ "corpus": {
283
+ "note_count": len(nodes),
284
+ "explicit_edge_count": len(wikilink_edges),
285
+ "inferred_edge_count": len(inferred_edges),
286
+ },
287
+ }
288
+
289
+
290
+ def validate_artifact(
291
+ artifact: dict[str, Any], *, topk: int = DEFAULT_TOPK,
292
+ global_cap_multiplier: float = DEFAULT_GLOBAL_CAP_MULTIPLIER,
293
+ ) -> tuple[bool, list[str]]:
294
+ """Schema + cap validation run BEFORE the atomic publish (HARDENED:codex):
295
+ a build that fails this never replaces the consumable ``graph.json``."""
296
+ problems: list[str] = []
297
+ if artifact.get("authoritative") is not False:
298
+ problems.append("authoritative must be false")
299
+ if artifact.get("schema_version") != GRAPH_SCHEMA_VERSION:
300
+ problems.append(f"schema_version must be {GRAPH_SCHEMA_VERSION}")
301
+ if not artifact.get("provenance"):
302
+ problems.append("missing provenance stamp")
303
+ edges = artifact.get("edges") or []
304
+ explicit = [e for e in edges if e.get("kind") == "WIKILINK"]
305
+ inferred = [e for e in edges if e.get("kind") == "INFERRED"]
306
+ cap = global_cap_multiplier * len(explicit)
307
+ if len(inferred) > cap + 1e-9:
308
+ problems.append(f"INFERRED edge count {len(inferred)} exceeds global cap {cap}")
309
+ degree: dict[str, int] = {}
310
+ for e in inferred:
311
+ degree[e["from"]] = degree.get(e["from"], 0) + 1
312
+ degree[e["to"]] = degree.get(e["to"], 0) + 1
313
+ over = {n: d for n, d in degree.items() if d > topk}
314
+ if over:
315
+ problems.append(f"per-node INFERRED cap ({topk}) exceeded: {over}")
316
+ return (not problems), problems
317
+
318
+
319
+ def top_candidates(edges: list[dict[str, Any]], *, limit: int) -> list[dict[str, Any]]:
320
+ """Top-``limit`` INFERRED edges by score — the human-review candidate
321
+ list (never auto-written into a note; review-only, hot-queue shaped by
322
+ the caller)."""
323
+ inferred = [e for e in edges if e.get("kind") == "INFERRED"]
324
+ return sorted(inferred, key=lambda e: -e["score"])[:limit]
325
+
326
+
327
+ def read_published_inferred_edges(graph_json_path) -> list[tuple[str, str]]:
328
+ """Read the published graph's INFERRED edges as ``(a, b)`` pairs, for the
329
+ OPTIONAL ``graph_expand(..., use_inferred=True)`` consumer (ADR-0003
330
+ Ruling 6, "Optional"). Degrades to ``[]`` on anything short of a valid,
331
+ non-partial artifact — missing file, unreadable JSON, or a stale
332
+ ``authoritative``/``schema_version`` stamp — never raises. A build failure
333
+ marker lives at a SEPARATE path (``BUILD_FAILED.json``) by construction,
334
+ so a partial build is never even a candidate for this reader to pick up."""
335
+ import json as _json
336
+ from pathlib import Path
337
+
338
+ p = Path(graph_json_path)
339
+ try:
340
+ artifact = _json.loads(p.read_text(encoding="utf-8"))
341
+ except (OSError, ValueError):
342
+ return []
343
+ if not isinstance(artifact, dict):
344
+ return []
345
+ if artifact.get("authoritative") is not False:
346
+ return []
347
+ if artifact.get("schema_version") != GRAPH_SCHEMA_VERSION:
348
+ return []
349
+ return [
350
+ (e["from"], e["to"]) for e in artifact.get("edges") or []
351
+ if e.get("kind") == "INFERRED" and e.get("from") and e.get("to")
352
+ ]