brainlayer 1.0.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 (53) hide show
  1. brainlayer/__init__.py +3 -0
  2. brainlayer/cli/__init__.py +1545 -0
  3. brainlayer/cli/wizard.py +132 -0
  4. brainlayer/cli_new.py +151 -0
  5. brainlayer/client.py +164 -0
  6. brainlayer/clustering.py +736 -0
  7. brainlayer/daemon.py +1105 -0
  8. brainlayer/dashboard/README.md +129 -0
  9. brainlayer/dashboard/__init__.py +5 -0
  10. brainlayer/dashboard/app.py +151 -0
  11. brainlayer/dashboard/search.py +229 -0
  12. brainlayer/dashboard/views.py +230 -0
  13. brainlayer/embeddings.py +131 -0
  14. brainlayer/engine.py +550 -0
  15. brainlayer/index_new.py +87 -0
  16. brainlayer/mcp/__init__.py +1558 -0
  17. brainlayer/migrate.py +205 -0
  18. brainlayer/paths.py +43 -0
  19. brainlayer/pipeline/__init__.py +47 -0
  20. brainlayer/pipeline/analyze_communication.py +508 -0
  21. brainlayer/pipeline/brain_graph.py +567 -0
  22. brainlayer/pipeline/chat_tags.py +63 -0
  23. brainlayer/pipeline/chunk.py +422 -0
  24. brainlayer/pipeline/classify.py +472 -0
  25. brainlayer/pipeline/cluster_sampling.py +73 -0
  26. brainlayer/pipeline/enrichment.py +810 -0
  27. brainlayer/pipeline/extract.py +66 -0
  28. brainlayer/pipeline/extract_claude_desktop.py +149 -0
  29. brainlayer/pipeline/extract_corrections.py +231 -0
  30. brainlayer/pipeline/extract_markdown.py +195 -0
  31. brainlayer/pipeline/extract_whatsapp.py +227 -0
  32. brainlayer/pipeline/git_overlay.py +301 -0
  33. brainlayer/pipeline/longitudinal_analyzer.py +568 -0
  34. brainlayer/pipeline/obsidian_export.py +455 -0
  35. brainlayer/pipeline/operation_grouping.py +486 -0
  36. brainlayer/pipeline/plan_linking.py +313 -0
  37. brainlayer/pipeline/sanitize.py +549 -0
  38. brainlayer/pipeline/semantic_style.py +574 -0
  39. brainlayer/pipeline/session_enrichment.py +472 -0
  40. brainlayer/pipeline/style_embed.py +67 -0
  41. brainlayer/pipeline/style_index.py +139 -0
  42. brainlayer/pipeline/temporal_chains.py +203 -0
  43. brainlayer/pipeline/time_batcher.py +248 -0
  44. brainlayer/pipeline/unified_timeline.py +569 -0
  45. brainlayer/storage.py +66 -0
  46. brainlayer/store.py +155 -0
  47. brainlayer/taxonomy.json +80 -0
  48. brainlayer/vector_store.py +1891 -0
  49. brainlayer-1.0.0.dist-info/METADATA +313 -0
  50. brainlayer-1.0.0.dist-info/RECORD +53 -0
  51. brainlayer-1.0.0.dist-info/WHEEL +4 -0
  52. brainlayer-1.0.0.dist-info/entry_points.txt +4 -0
  53. brainlayer-1.0.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,313 @@
1
+ """Plan Linking Pipeline — Link sessions to active plans.
2
+
3
+ Phase 8c: Match sessions to plans by branch name and PR number.
4
+ Scans docs/plan/*/README.md progress tables to build mappings.
5
+
6
+ Usage:
7
+ from brainlayer.pipeline.plan_linking import run_plan_linking
8
+ run_plan_linking(vector_store, repo_root="/path/to/your-project")
9
+ """
10
+
11
+ import logging
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def parse_plan_readme(
20
+ plan_dir: Path,
21
+ ) -> List[Dict[str, Any]]:
22
+ """Parse a plan README.md to extract branch/PR mappings.
23
+
24
+ Looks for:
25
+ - Markdown table rows with PR numbers (#NNN)
26
+ - Branch names in backticks or table cells
27
+ - Phase headers with branch names
28
+
29
+ Returns list of mapping dicts with:
30
+ plan_name, plan_phase, branch, pr_number
31
+ """
32
+ readme = plan_dir / "README.md"
33
+ if not readme.exists():
34
+ return []
35
+
36
+ plan_name = plan_dir.name
37
+ text = readme.read_text()
38
+ mappings: List[Dict[str, Any]] = []
39
+
40
+ # Pattern 1: Phase headers with branch names
41
+ # "### Phase N: Name — `feature/branch-name`"
42
+ header_pattern = re.compile(r"###\s+Phase\s+(\d+)[^—\n]*—\s*`([^`]+)`")
43
+ for match in header_pattern.finditer(text):
44
+ phase_num = match.group(1)
45
+ branch = match.group(2)
46
+ mappings.append(
47
+ {
48
+ "plan_name": plan_name,
49
+ "plan_phase": f"phase-{phase_num}",
50
+ "branch": branch,
51
+ "pr_number": None,
52
+ }
53
+ )
54
+
55
+ # Pattern 2: Table rows with PR numbers
56
+ # "| N | Description | ... | #NNN |" or "| ... | `branch` | ..."
57
+ # Match PR numbers like #121, #134, "#131, #132"
58
+ table_row_pattern = re.compile(r"^\|([^|]+)\|([^|]+)\|(.+)\|$", re.MULTILINE)
59
+ for match in table_row_pattern.finditer(text):
60
+ full_row = match.group(0)
61
+
62
+ # Skip header/separator rows
63
+ if "---" in full_row or "Step" in full_row or "Phase" in full_row:
64
+ if "Phase" in full_row and "|" in full_row:
65
+ # Could be a data row like "| 1 | Phase name |..."
66
+ cells = [c.strip() for c in full_row.split("|")]
67
+ cells = [c for c in cells if c]
68
+ if not cells or not cells[0].strip().isdigit():
69
+ continue
70
+ else:
71
+ continue
72
+
73
+ cells = [c.strip() for c in full_row.split("|")]
74
+ cells = [c for c in cells if c] # Remove empty from leading/trailing |
75
+
76
+ if not cells:
77
+ continue
78
+
79
+ # Extract PR numbers from any cell
80
+ pr_matches = re.findall(r"#(\d+)", full_row)
81
+ pr_numbers = [int(p) for p in pr_matches]
82
+
83
+ # Extract branch from backtick-quoted text
84
+ branch_match = re.search(r"`([^`]*feature/[^`]+)`", full_row)
85
+ branch = branch_match.group(1) if branch_match else None
86
+
87
+ # Extract phase/step info from first cell or description
88
+ phase_str = None
89
+ step_num = cells[0].strip() if cells else None
90
+
91
+ # Try to get phase from folder reference [phase-N]
92
+ folder_match = re.search(r"\[phase-([^\]]+)\]", full_row)
93
+ if folder_match:
94
+ phase_str = f"phase-{folder_match.group(1)}"
95
+ elif step_num and step_num.isdigit():
96
+ phase_str = f"step-{step_num}"
97
+
98
+ # Description for context
99
+ desc = cells[1].strip() if len(cells) > 1 else ""
100
+
101
+ # Create a mapping for each PR number
102
+ if pr_numbers:
103
+ for pr_num in pr_numbers:
104
+ mappings.append(
105
+ {
106
+ "plan_name": plan_name,
107
+ "plan_phase": phase_str or desc[:50],
108
+ "branch": branch,
109
+ "pr_number": pr_num,
110
+ }
111
+ )
112
+ elif branch:
113
+ mappings.append(
114
+ {
115
+ "plan_name": plan_name,
116
+ "plan_phase": phase_str or desc[:50],
117
+ "branch": branch,
118
+ "pr_number": None,
119
+ }
120
+ )
121
+
122
+ # Deduplicate by (plan_name, branch, pr_number)
123
+ seen: set = set()
124
+ unique: List[Dict[str, Any]] = []
125
+ for m in mappings:
126
+ key = (m["plan_name"], m.get("branch"), m.get("pr_number"))
127
+ if key not in seen:
128
+ seen.add(key)
129
+ unique.append(m)
130
+
131
+ return unique
132
+
133
+
134
+ def scan_all_plans(
135
+ repo_root: Path,
136
+ ) -> Tuple[Dict[str, Dict[str, Any]], Dict[int, Dict[str, Any]]]:
137
+ """Scan all plan READMEs and build lookup indexes.
138
+
139
+ Returns:
140
+ (branch_index, pr_index) where:
141
+ - branch_index maps branch_name → plan mapping
142
+ - pr_index maps pr_number → plan mapping
143
+ """
144
+ plan_dir = repo_root / "docs" / "plan"
145
+ if not plan_dir.exists():
146
+ logger.warning("Plan directory not found: %s", plan_dir)
147
+ return {}, {}
148
+
149
+ branch_index: Dict[str, Dict[str, Any]] = {}
150
+ pr_index: Dict[int, Dict[str, Any]] = {}
151
+
152
+ for subdir in sorted(plan_dir.iterdir()):
153
+ if not subdir.is_dir():
154
+ continue
155
+ mappings = parse_plan_readme(subdir)
156
+ for m in mappings:
157
+ if m.get("branch"):
158
+ branch_index[m["branch"]] = m
159
+ if m.get("pr_number"):
160
+ pr_index[m["pr_number"]] = m
161
+ if mappings:
162
+ logger.info(
163
+ "Plan '%s': %d mappings (%d branches, %d PRs)",
164
+ subdir.name,
165
+ len(mappings),
166
+ sum(1 for m in mappings if m.get("branch")),
167
+ sum(1 for m in mappings if m.get("pr_number")),
168
+ )
169
+
170
+ return branch_index, pr_index
171
+
172
+
173
+ def match_session_to_plan(
174
+ session: Dict[str, Any],
175
+ branch_index: Dict[str, Dict[str, Any]],
176
+ pr_index: Dict[int, Dict[str, Any]],
177
+ ) -> Optional[Dict[str, Any]]:
178
+ """Match a session to a plan using branch and PR number.
179
+
180
+ Priority:
181
+ 1. Exact branch match
182
+ 2. PR number match
183
+ 3. Branch prefix match (e.g., feature/llm- → local-llm-integration)
184
+ """
185
+ branch = session.get("branch")
186
+ pr_number = session.get("pr_number")
187
+
188
+ # 1. Exact branch match
189
+ if branch and branch in branch_index:
190
+ return branch_index[branch]
191
+
192
+ # 2. PR number match
193
+ if pr_number and pr_number in pr_index:
194
+ return pr_index[pr_number]
195
+
196
+ # 3. Branch prefix matching for known patterns
197
+ if branch:
198
+ prefix_patterns = {
199
+ "feature/componentize-": "componentize-app",
200
+ "feature/llm-": "local-llm-integration",
201
+ "feature/backend-": "backend-overhaul",
202
+ "feature/phase1-": "phase-1-ship",
203
+ "feature/phase2-": "phase-2-cloud",
204
+ "feature/phase3-": "phase-3-teller",
205
+ "feature/phase4-": "phase-4-tooling",
206
+ "feature/job-search-": "job-search-command-center",
207
+ "feature/style-card": "local-llm-integration",
208
+ }
209
+ for prefix, plan_name in prefix_patterns.items():
210
+ if branch.startswith(prefix):
211
+ # Extract phase or step from branch
212
+ phase_match = re.search(
213
+ r"(?:phase|step)[- ]?(\d+|[a-z]+)",
214
+ branch,
215
+ )
216
+ phase = None
217
+ if phase_match:
218
+ phase = f"step-{phase_match.group(1)}"
219
+ return {
220
+ "plan_name": plan_name,
221
+ "plan_phase": phase,
222
+ "branch": branch,
223
+ "pr_number": pr_number,
224
+ }
225
+
226
+ return None
227
+
228
+
229
+ def run_plan_linking(
230
+ vector_store: Any,
231
+ repo_root: Optional[str] = None,
232
+ project: Optional[str] = None,
233
+ force: bool = False,
234
+ ) -> Dict[str, int]:
235
+ """Run plan linking for all sessions with git context.
236
+
237
+ Args:
238
+ vector_store: VectorStore instance
239
+ repo_root: Path to project repo root (for plan READMEs)
240
+ project: Filter to specific project
241
+ force: Clear existing plan links first
242
+
243
+ Returns:
244
+ Dict with counts: sessions_checked, sessions_linked
245
+ """
246
+ if repo_root:
247
+ root = Path(repo_root)
248
+ else:
249
+ root = Path(__file__).parents[5] # pipeline → brainlayer → src → brainlayer → repo-root/
250
+
251
+ if force:
252
+ cleared = vector_store.clear_plan_links(project)
253
+ if cleared:
254
+ logger.info("Cleared %d existing plan links", cleared)
255
+
256
+ # Scan plan READMEs (no early-exit guard — the query
257
+ # filters for plan_name IS NULL when not forcing)
258
+ branch_index, pr_index = scan_all_plans(root)
259
+ logger.info(
260
+ "Built plan index: %d branches, %d PRs",
261
+ len(branch_index),
262
+ len(pr_index),
263
+ )
264
+
265
+ if not branch_index and not pr_index:
266
+ logger.warning("No plan mappings found")
267
+ return {"sessions_checked": 0, "sessions_linked": 0}
268
+
269
+ # Get all sessions with git context
270
+ cursor = vector_store.conn.cursor()
271
+ query = "SELECT session_id, project, branch, pr_number FROM session_context"
272
+ params: list = []
273
+ if project:
274
+ query += " WHERE project = ?"
275
+ params.append(project)
276
+
277
+ if not force:
278
+ if params:
279
+ query += " AND plan_name IS NULL"
280
+ else:
281
+ query += " WHERE plan_name IS NULL"
282
+
283
+ sessions = list(cursor.execute(query, params))
284
+ logger.info("Checking %d sessions for plan matches", len(sessions))
285
+
286
+ linked = 0
287
+ for row in sessions:
288
+ session = {
289
+ "session_id": row[0],
290
+ "project": row[1],
291
+ "branch": row[2],
292
+ "pr_number": row[3],
293
+ }
294
+ plan = match_session_to_plan(session, branch_index, pr_index)
295
+ if plan:
296
+ vector_store.update_session_plan(
297
+ session_id=session["session_id"],
298
+ plan_name=plan["plan_name"],
299
+ plan_phase=plan.get("plan_phase"),
300
+ story_id=plan.get("story_id"),
301
+ )
302
+ linked += 1
303
+ logger.debug(
304
+ "Linked session %s → %s/%s",
305
+ session["session_id"][:8],
306
+ plan["plan_name"],
307
+ plan.get("plan_phase", "?"),
308
+ )
309
+
310
+ return {
311
+ "sessions_checked": len(sessions),
312
+ "sessions_linked": linked,
313
+ }