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.
- brainlayer/__init__.py +3 -0
- brainlayer/cli/__init__.py +1545 -0
- brainlayer/cli/wizard.py +132 -0
- brainlayer/cli_new.py +151 -0
- brainlayer/client.py +164 -0
- brainlayer/clustering.py +736 -0
- brainlayer/daemon.py +1105 -0
- brainlayer/dashboard/README.md +129 -0
- brainlayer/dashboard/__init__.py +5 -0
- brainlayer/dashboard/app.py +151 -0
- brainlayer/dashboard/search.py +229 -0
- brainlayer/dashboard/views.py +230 -0
- brainlayer/embeddings.py +131 -0
- brainlayer/engine.py +550 -0
- brainlayer/index_new.py +87 -0
- brainlayer/mcp/__init__.py +1558 -0
- brainlayer/migrate.py +205 -0
- brainlayer/paths.py +43 -0
- brainlayer/pipeline/__init__.py +47 -0
- brainlayer/pipeline/analyze_communication.py +508 -0
- brainlayer/pipeline/brain_graph.py +567 -0
- brainlayer/pipeline/chat_tags.py +63 -0
- brainlayer/pipeline/chunk.py +422 -0
- brainlayer/pipeline/classify.py +472 -0
- brainlayer/pipeline/cluster_sampling.py +73 -0
- brainlayer/pipeline/enrichment.py +810 -0
- brainlayer/pipeline/extract.py +66 -0
- brainlayer/pipeline/extract_claude_desktop.py +149 -0
- brainlayer/pipeline/extract_corrections.py +231 -0
- brainlayer/pipeline/extract_markdown.py +195 -0
- brainlayer/pipeline/extract_whatsapp.py +227 -0
- brainlayer/pipeline/git_overlay.py +301 -0
- brainlayer/pipeline/longitudinal_analyzer.py +568 -0
- brainlayer/pipeline/obsidian_export.py +455 -0
- brainlayer/pipeline/operation_grouping.py +486 -0
- brainlayer/pipeline/plan_linking.py +313 -0
- brainlayer/pipeline/sanitize.py +549 -0
- brainlayer/pipeline/semantic_style.py +574 -0
- brainlayer/pipeline/session_enrichment.py +472 -0
- brainlayer/pipeline/style_embed.py +67 -0
- brainlayer/pipeline/style_index.py +139 -0
- brainlayer/pipeline/temporal_chains.py +203 -0
- brainlayer/pipeline/time_batcher.py +248 -0
- brainlayer/pipeline/unified_timeline.py +569 -0
- brainlayer/storage.py +66 -0
- brainlayer/store.py +155 -0
- brainlayer/taxonomy.json +80 -0
- brainlayer/vector_store.py +1891 -0
- brainlayer-1.0.0.dist-info/METADATA +313 -0
- brainlayer-1.0.0.dist-info/RECORD +53 -0
- brainlayer-1.0.0.dist-info/WHEEL +4 -0
- brainlayer-1.0.0.dist-info/entry_points.txt +4 -0
- 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
|
+
}
|