superlocalmemory 4.0.5 → 4.0.6
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.
- package/CHANGELOG.md +57 -0
- package/README.md +10 -6
- package/package.json +3 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/truth.py +80 -10
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +14 -3
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +76 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/maintenance.py +43 -1
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +21 -0
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/server/routes/brain.py +283 -15
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/database.py +36 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +28 -11
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +257 -77
- package/src/superlocalmemory/ui/js/od-graph.js +147 -6
|
@@ -31,6 +31,7 @@ from superlocalmemory.code_graph.models import (
|
|
|
31
31
|
NodeKind,
|
|
32
32
|
ParseResult,
|
|
33
33
|
)
|
|
34
|
+
from superlocalmemory.code_graph.resolver import ImportResolver
|
|
34
35
|
from superlocalmemory.storage.models import _new_id
|
|
35
36
|
|
|
36
37
|
logger = logging.getLogger(__name__)
|
|
@@ -65,6 +66,140 @@ def _sha256(data: bytes) -> str:
|
|
|
65
66
|
return hashlib.sha256(data).hexdigest()
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
def _assign_edge_sources(
|
|
70
|
+
nodes: list[GraphNode],
|
|
71
|
+
edges: list[GraphEdge],
|
|
72
|
+
) -> list[GraphEdge]:
|
|
73
|
+
"""Replace ``__unresolved__`` source_node_ids on CALLS edges.
|
|
74
|
+
|
|
75
|
+
Locates the innermost function/method whose line-range contains the call
|
|
76
|
+
site. Falls back to the file-level node when no function wraps the call
|
|
77
|
+
(e.g. top-level script code). IMPORTS and other non-CALLS edges whose
|
|
78
|
+
source is still ``__unresolved__`` are dropped — they cannot be resolved
|
|
79
|
+
without storing the module path in the edge, which is a separate defect.
|
|
80
|
+
|
|
81
|
+
Returns a new list of edges.
|
|
82
|
+
"""
|
|
83
|
+
# ── build per-file line-range index ────────────────────────────────
|
|
84
|
+
# {file_path: [(line_start, line_end, node_id), ...]}
|
|
85
|
+
# Sorted by range size ascending so the NARROWEST (innermost) match wins.
|
|
86
|
+
file_ranges: dict[str, list[tuple[int, int, str]]] = {}
|
|
87
|
+
file_node_ids: dict[str, str] = {} # file_path → FILE node_id
|
|
88
|
+
|
|
89
|
+
for node in nodes:
|
|
90
|
+
if node.kind == NodeKind.FILE:
|
|
91
|
+
file_node_ids[node.file_path] = node.node_id
|
|
92
|
+
elif node.kind in (NodeKind.FUNCTION, NodeKind.METHOD):
|
|
93
|
+
fp = node.file_path
|
|
94
|
+
file_ranges.setdefault(fp, []).append(
|
|
95
|
+
(node.line_start, node.line_end, node.node_id)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
for fp in file_ranges:
|
|
99
|
+
file_ranges[fp].sort(key=lambda t: t[1] - t[0]) # narrowest first
|
|
100
|
+
|
|
101
|
+
resolved: list[GraphEdge] = []
|
|
102
|
+
dropped = 0
|
|
103
|
+
|
|
104
|
+
for edge in edges:
|
|
105
|
+
if edge.source_node_id != "__unresolved__":
|
|
106
|
+
resolved.append(edge)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if edge.kind != EdgeKind.CALLS:
|
|
110
|
+
# IMPORTS and other edges: source unknown — drop.
|
|
111
|
+
# Root cause: extract_imports() does not store the module path in
|
|
112
|
+
# extra_json, so targets are also unresolvable. Tracked as a
|
|
113
|
+
# separate pre-existing defect; fixing it here would require an
|
|
114
|
+
# extractor change beyond this PR scope.
|
|
115
|
+
dropped += 1
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# ── find innermost enclosing function ──────────────────────────
|
|
119
|
+
enclosing_id: str | None = None
|
|
120
|
+
for ls, le, nid in file_ranges.get(edge.file_path, []):
|
|
121
|
+
if ls <= edge.line <= le:
|
|
122
|
+
enclosing_id = nid
|
|
123
|
+
break # narrowest first → stop at first hit
|
|
124
|
+
|
|
125
|
+
# ── fallback: top-level call, use FILE node ─────────────────────
|
|
126
|
+
if enclosing_id is None:
|
|
127
|
+
enclosing_id = file_node_ids.get(edge.file_path)
|
|
128
|
+
|
|
129
|
+
if enclosing_id is None:
|
|
130
|
+
logger.debug(
|
|
131
|
+
"_assign_edge_sources: no source node for call at %s:%d — dropping",
|
|
132
|
+
edge.file_path, edge.line,
|
|
133
|
+
)
|
|
134
|
+
dropped += 1
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
resolved.append(GraphEdge(
|
|
138
|
+
edge_id=edge.edge_id,
|
|
139
|
+
kind=edge.kind,
|
|
140
|
+
source_node_id=enclosing_id,
|
|
141
|
+
target_node_id=edge.target_node_id,
|
|
142
|
+
file_path=edge.file_path,
|
|
143
|
+
line=edge.line,
|
|
144
|
+
confidence=edge.confidence,
|
|
145
|
+
extra_json=edge.extra_json,
|
|
146
|
+
created_at=edge.created_at,
|
|
147
|
+
updated_at=edge.updated_at,
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
if dropped:
|
|
151
|
+
logger.debug(
|
|
152
|
+
"_assign_edge_sources: dropped %d edge(s) with unresolvable source",
|
|
153
|
+
dropped,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return resolved
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _clean_and_resolve_edges(
|
|
160
|
+
nodes: list[GraphNode],
|
|
161
|
+
edges: list[GraphEdge],
|
|
162
|
+
import_maps_by_file: dict[str, dict[str, tuple[str, str]]],
|
|
163
|
+
repo_root: Path,
|
|
164
|
+
config: CodeGraphConfig,
|
|
165
|
+
) -> list[GraphEdge]:
|
|
166
|
+
"""Full resolution pipeline applied after parse_all collects all nodes.
|
|
167
|
+
|
|
168
|
+
1. Assign real source_node_ids to CALLS edges (line-range lookup).
|
|
169
|
+
2. Drop IMPORTS edges whose endpoints are still ``__unresolved__``.
|
|
170
|
+
3. Run ImportResolver.resolve_call_targets to replace ``__call__<name>``
|
|
171
|
+
targets with real node_ids or drop the edge (external calls).
|
|
172
|
+
4. Final defensive pass: drop any edge whose endpoints are still absent
|
|
173
|
+
from the full node set (belt-and-braces, should be a no-op after 1-3).
|
|
174
|
+
|
|
175
|
+
Returns a clean edge list safe to hand to GraphStore.
|
|
176
|
+
"""
|
|
177
|
+
# Step 1: fix sources
|
|
178
|
+
edges = _assign_edge_sources(nodes, edges)
|
|
179
|
+
|
|
180
|
+
# Step 2: resolve targets
|
|
181
|
+
resolver = ImportResolver(repo_root, config)
|
|
182
|
+
edges = resolver.resolve_call_targets(nodes, edges, import_maps_by_file)
|
|
183
|
+
|
|
184
|
+
# Step 3: final drop of any remaining stragglers
|
|
185
|
+
valid_ids = {n.node_id for n in nodes}
|
|
186
|
+
clean: list[GraphEdge] = []
|
|
187
|
+
stray = 0
|
|
188
|
+
for e in edges:
|
|
189
|
+
if e.source_node_id in valid_ids and e.target_node_id in valid_ids:
|
|
190
|
+
clean.append(e)
|
|
191
|
+
else:
|
|
192
|
+
stray += 1
|
|
193
|
+
|
|
194
|
+
if stray:
|
|
195
|
+
logger.debug(
|
|
196
|
+
"_clean_and_resolve_edges: final pass dropped %d stray edge(s)",
|
|
197
|
+
stray,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return clean
|
|
201
|
+
|
|
202
|
+
|
|
68
203
|
def _make_qualified_name(
|
|
69
204
|
file_path: str, name: str, parent_name: str | None
|
|
70
205
|
) -> str:
|
|
@@ -111,11 +246,12 @@ def _parse_file_standalone(
|
|
|
111
246
|
else:
|
|
112
247
|
return {"nodes": [], "edges": [], "errors": [f"Unsupported language: {language}"]}
|
|
113
248
|
|
|
114
|
-
nodes, edges = extractor.
|
|
249
|
+
nodes, edges, import_map = extractor.extract_with_import_map()
|
|
115
250
|
|
|
116
251
|
return {
|
|
117
252
|
"nodes": nodes,
|
|
118
253
|
"edges": edges,
|
|
254
|
+
"import_map": import_map,
|
|
119
255
|
"errors": [],
|
|
120
256
|
}
|
|
121
257
|
except Exception as exc:
|
|
@@ -191,7 +327,7 @@ class CodeParser:
|
|
|
191
327
|
file_path: Path,
|
|
192
328
|
source_bytes: bytes,
|
|
193
329
|
language: str,
|
|
194
|
-
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
330
|
+
) -> tuple[list[GraphNode], list[GraphEdge], dict[str, tuple[str, str]]]:
|
|
195
331
|
"""Parse a single file and return extracted nodes and edges.
|
|
196
332
|
|
|
197
333
|
Raises UnsupportedLanguageError if language is not supported.
|
|
@@ -235,7 +371,7 @@ class CodeParser:
|
|
|
235
371
|
from superlocalmemory.code_graph.extractors.typescript import TypeScriptExtractor
|
|
236
372
|
extractor = TypeScriptExtractor(root, source_bytes, file_path_str, self._config)
|
|
237
373
|
|
|
238
|
-
extracted_nodes, extracted_edges = extractor.
|
|
374
|
+
extracted_nodes, extracted_edges, file_import_map = extractor.extract_with_import_map()
|
|
239
375
|
|
|
240
376
|
# Check if test file
|
|
241
377
|
is_test = _is_test_file(file_path_str, self._config)
|
|
@@ -277,7 +413,7 @@ class CodeParser:
|
|
|
277
413
|
|
|
278
414
|
all_edges = extracted_edges + contains_edges + tested_by_edges
|
|
279
415
|
|
|
280
|
-
return all_nodes, all_edges
|
|
416
|
+
return all_nodes, all_edges, file_import_map
|
|
281
417
|
|
|
282
418
|
def parse_all(
|
|
283
419
|
self, repo_root: Path
|
|
@@ -293,6 +429,11 @@ class CodeParser:
|
|
|
293
429
|
all_nodes: list[GraphNode] = []
|
|
294
430
|
all_edges: list[GraphEdge] = []
|
|
295
431
|
all_file_records: list[FileRecord] = []
|
|
432
|
+
# Keyed by relative file_path string; populated by the parallel path
|
|
433
|
+
# (the sequential path goes through parse_file which does not expose
|
|
434
|
+
# the per-file import map, so it stays empty — resolver Strategy 2/3
|
|
435
|
+
# still works without it).
|
|
436
|
+
import_maps_by_file: dict[str, dict[str, tuple[str, str]]] = {}
|
|
296
437
|
|
|
297
438
|
# Read files and prepare tasks
|
|
298
439
|
tasks: list[tuple[Path, bytes, str]] = []
|
|
@@ -313,10 +454,16 @@ class CodeParser:
|
|
|
313
454
|
|
|
314
455
|
# Parse with ProcessPoolExecutor for parallel CPU-bound work
|
|
315
456
|
# For small numbers of files, run sequentially to avoid overhead
|
|
316
|
-
|
|
457
|
+
# Use the sequential (in-process) path for small repos or when the
|
|
458
|
+
# caller explicitly requests single-worker execution. parallel_workers=1
|
|
459
|
+
# is the only way to guarantee in-process parsing (no subprocess spawn).
|
|
460
|
+
if len(tasks) <= 2 or self._config.parallel_workers == 1:
|
|
317
461
|
for rel_path, source_bytes, language in tasks:
|
|
318
462
|
try:
|
|
319
|
-
nodes, edges = self.parse_file(
|
|
463
|
+
nodes, edges, file_import_map = self.parse_file(
|
|
464
|
+
rel_path, source_bytes, language
|
|
465
|
+
)
|
|
466
|
+
import_maps_by_file[str(rel_path)] = file_import_map
|
|
320
467
|
all_nodes.extend(nodes)
|
|
321
468
|
all_edges.extend(edges)
|
|
322
469
|
all_file_records.append(FileRecord(
|
|
@@ -330,105 +477,138 @@ class CodeParser:
|
|
|
330
477
|
))
|
|
331
478
|
except Exception as exc:
|
|
332
479
|
logger.warning("Failed to parse %s: %s", rel_path, exc)
|
|
333
|
-
|
|
480
|
+
# import_maps_by_file is now populated on the sequential path:
|
|
481
|
+
# parse_file returns the per-file import map (Strategy 1 works
|
|
482
|
+
# for small repos / parallel_workers=1 runs too).
|
|
483
|
+
else:
|
|
484
|
+
# Parallel execution
|
|
485
|
+
parse_failures = 0 # count of futures that raised (worker crashes)
|
|
486
|
+
config_dict = {
|
|
487
|
+
field_name: getattr(self._config, field_name)
|
|
488
|
+
for field_name in CodeGraphConfig.__dataclass_fields__
|
|
489
|
+
if not isinstance(getattr(self._config, field_name), Path)
|
|
490
|
+
}
|
|
491
|
+
# Convert Path fields to strings
|
|
492
|
+
config_dict["repo_root"] = str(self._config.repo_root)
|
|
493
|
+
|
|
494
|
+
workers = min(self._config.parallel_workers, len(tasks))
|
|
495
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
496
|
+
future_map = {}
|
|
497
|
+
for rel_path, source_bytes, language in tasks:
|
|
498
|
+
future = executor.submit(
|
|
499
|
+
_parse_file_standalone,
|
|
500
|
+
str(rel_path),
|
|
501
|
+
source_bytes,
|
|
502
|
+
language,
|
|
503
|
+
config_dict,
|
|
504
|
+
)
|
|
505
|
+
future_map[future] = (rel_path, source_bytes, language)
|
|
506
|
+
|
|
507
|
+
for future in as_completed(future_map):
|
|
508
|
+
rel_path, source_bytes, language = future_map[future]
|
|
509
|
+
try:
|
|
510
|
+
result = future.result(timeout=self._config.parse_timeout_seconds)
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
logger.warning("Parse failed for %s: %s", rel_path, exc)
|
|
513
|
+
parse_failures += 1
|
|
514
|
+
continue
|
|
334
515
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
516
|
+
if result["errors"]:
|
|
517
|
+
for err in result["errors"]:
|
|
518
|
+
logger.warning("Parse error in %s: %s", rel_path, err)
|
|
519
|
+
if not result["nodes"]:
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
file_nodes = result["nodes"]
|
|
523
|
+
file_edges = result["edges"]
|
|
524
|
+
# Collect per-file import map for Strategy 1 resolution.
|
|
525
|
+
import_maps_by_file[str(rel_path)] = result.get("import_map", {})
|
|
526
|
+
|
|
527
|
+
# Build the full parse result with file node and CONTAINS edges
|
|
528
|
+
file_path_str = str(rel_path)
|
|
529
|
+
content_hash = _sha256(source_bytes)
|
|
530
|
+
|
|
531
|
+
file_node = GraphNode(
|
|
532
|
+
node_id=_new_id(),
|
|
533
|
+
kind=NodeKind.FILE,
|
|
534
|
+
name=rel_path.name,
|
|
535
|
+
qualified_name=file_path_str,
|
|
536
|
+
file_path=file_path_str,
|
|
537
|
+
line_start=0,
|
|
538
|
+
line_end=0,
|
|
539
|
+
language=language,
|
|
540
|
+
content_hash=content_hash,
|
|
541
|
+
)
|
|
343
542
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
543
|
+
is_test = _is_test_file(file_path_str, self._config)
|
|
544
|
+
if is_test:
|
|
545
|
+
marked: list[GraphNode] = []
|
|
546
|
+
for n in file_nodes:
|
|
547
|
+
if n.kind in (NodeKind.FUNCTION, NodeKind.METHOD):
|
|
548
|
+
marked.append(GraphNode(
|
|
549
|
+
node_id=n.node_id, kind=n.kind, name=n.name,
|
|
550
|
+
qualified_name=n.qualified_name,
|
|
551
|
+
file_path=n.file_path,
|
|
552
|
+
line_start=n.line_start, line_end=n.line_end,
|
|
553
|
+
language=n.language, parent_name=n.parent_name,
|
|
554
|
+
signature=n.signature, docstring=n.docstring,
|
|
555
|
+
is_test=True, content_hash=n.content_hash,
|
|
556
|
+
extra_json=n.extra_json,
|
|
557
|
+
))
|
|
558
|
+
else:
|
|
559
|
+
marked.append(n)
|
|
560
|
+
file_nodes = marked
|
|
561
|
+
|
|
562
|
+
contains = self._generate_contains_edges(file_node, file_nodes)
|
|
563
|
+
tested_by = self._generate_tested_by_edges(file_nodes, file_edges)
|
|
564
|
+
|
|
565
|
+
final_nodes = [file_node] + file_nodes
|
|
566
|
+
final_edges = file_edges + contains + tested_by
|
|
567
|
+
|
|
568
|
+
all_nodes.extend(final_nodes)
|
|
569
|
+
all_edges.extend(final_edges)
|
|
570
|
+
|
|
571
|
+
try:
|
|
572
|
+
mtime = (repo_root / rel_path).stat().st_mtime
|
|
573
|
+
except OSError:
|
|
574
|
+
mtime = 0.0
|
|
364
575
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
576
|
+
all_file_records.append(FileRecord(
|
|
577
|
+
file_path=file_path_str,
|
|
578
|
+
content_hash=content_hash,
|
|
579
|
+
mtime=mtime,
|
|
580
|
+
language=language,
|
|
581
|
+
node_count=len(final_nodes),
|
|
582
|
+
edge_count=len(final_edges),
|
|
583
|
+
last_indexed=time.time(),
|
|
584
|
+
))
|
|
370
585
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
is_test = _is_test_file(file_path_str, self._config)
|
|
391
|
-
if is_test:
|
|
392
|
-
marked: list[GraphNode] = []
|
|
393
|
-
for n in file_nodes:
|
|
394
|
-
if n.kind in (NodeKind.FUNCTION, NodeKind.METHOD):
|
|
395
|
-
marked.append(GraphNode(
|
|
396
|
-
node_id=n.node_id, kind=n.kind, name=n.name,
|
|
397
|
-
qualified_name=n.qualified_name,
|
|
398
|
-
file_path=n.file_path,
|
|
399
|
-
line_start=n.line_start, line_end=n.line_end,
|
|
400
|
-
language=n.language, parent_name=n.parent_name,
|
|
401
|
-
signature=n.signature, docstring=n.docstring,
|
|
402
|
-
is_test=True, content_hash=n.content_hash,
|
|
403
|
-
extra_json=n.extra_json,
|
|
404
|
-
))
|
|
405
|
-
else:
|
|
406
|
-
marked.append(n)
|
|
407
|
-
file_nodes = marked
|
|
408
|
-
|
|
409
|
-
contains = self._generate_contains_edges(file_node, file_nodes)
|
|
410
|
-
tested_by = self._generate_tested_by_edges(file_nodes, file_edges)
|
|
411
|
-
|
|
412
|
-
final_nodes = [file_node] + file_nodes
|
|
413
|
-
final_edges = file_edges + contains + tested_by
|
|
414
|
-
|
|
415
|
-
all_nodes.extend(final_nodes)
|
|
416
|
-
all_edges.extend(final_edges)
|
|
586
|
+
# ── Fail-fast: majority pool failure ──────────────────────────────
|
|
587
|
+
# >50% failure rate means worker processes were killed (OOM, missing
|
|
588
|
+
# deps in subprocess env) — NOT a legitimate empty repo. Returning
|
|
589
|
+
# silently would let build_code_graph report success with zero nodes,
|
|
590
|
+
# which is worse than the original FK crash.
|
|
591
|
+
# Threshold rationale: ≥50% is a clear systemic failure; it allows
|
|
592
|
+
# for a tail of legitimately-malformed files without triggering on
|
|
593
|
+
# ordinary parse errors.
|
|
594
|
+
if parse_failures > 0 and len(tasks) > 0:
|
|
595
|
+
failure_rate = parse_failures / len(tasks)
|
|
596
|
+
if failure_rate > 0.50:
|
|
597
|
+
raise RuntimeError(
|
|
598
|
+
f"Parsing aborted: {parse_failures}/{len(tasks)} files "
|
|
599
|
+
f"failed ({failure_rate:.0%}). Likely cause: process pool "
|
|
600
|
+
f"worker crash or missing tree-sitter grammars in subprocess. "
|
|
601
|
+
f"Set CodeGraphConfig(parallel_workers=1) for in-process parsing."
|
|
602
|
+
)
|
|
417
603
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
mtime=mtime,
|
|
427
|
-
language=language,
|
|
428
|
-
node_count=len(final_nodes),
|
|
429
|
-
edge_count=len(final_edges),
|
|
430
|
-
last_indexed=time.time(),
|
|
431
|
-
))
|
|
604
|
+
# ── Resolution pipeline ────────────────────────────────────────────────
|
|
605
|
+
# Resolve placeholder edge endpoints before returning. Ensures that
|
|
606
|
+
# every edge exiting parse_all has both endpoints in the returned node
|
|
607
|
+
# set, so GraphStore never encounters a FOREIGN KEY constraint failure.
|
|
608
|
+
# This is Fix A: wiring resolver.py into the parse pipeline.
|
|
609
|
+
all_edges = _clean_and_resolve_edges(
|
|
610
|
+
all_nodes, all_edges, import_maps_by_file, repo_root, self._config
|
|
611
|
+
)
|
|
432
612
|
|
|
433
613
|
return all_nodes, all_edges, all_file_records
|
|
434
614
|
|