hcr-memory 0.5.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 (180) hide show
  1. hcr/.hcr/cpap_epoch.json +1 -0
  2. hcr/__init__.py +27 -0
  3. hcr/engine/__init__.py +33 -0
  4. hcr/engine/causal/__init__.py +16 -0
  5. hcr/engine/causal/ast_extractor.py +58 -0
  6. hcr/engine/causal/co_change_miner.py +96 -0
  7. hcr/engine/causal/cso_impact.py +110 -0
  8. hcr/engine/causal/dependency_graph.py +89 -0
  9. hcr/engine/causal/event_store.py +270 -0
  10. hcr/engine/causal/impact_analyzer.py +35 -0
  11. hcr/engine/causal/metrics.py +56 -0
  12. hcr/engine/causal/temporal_correlator.py +409 -0
  13. hcr/engine/core/__init__.py +3 -0
  14. hcr/engine/cso/__init__.py +5 -0
  15. hcr/engine/cso/agent_registry.py +55 -0
  16. hcr/engine/cso/backend.py +133 -0
  17. hcr/engine/cso/cso_migrator.py +74 -0
  18. hcr/engine/cso/cso_model.py +108 -0
  19. hcr/engine/cso/cso_store.py +970 -0
  20. hcr/engine/engine_api.py +1784 -0
  21. hcr/engine/inference/__init__.py +15 -0
  22. hcr/engine/inference/constraint_learner.py +233 -0
  23. hcr/engine/inference/narrator.py +151 -0
  24. hcr/engine/inference/predictor.py +160 -0
  25. hcr/engine/inference/tracer.py +234 -0
  26. hcr/engine/llm/__init__.py +11 -0
  27. hcr/engine/llm/llm_provider.py +191 -0
  28. hcr/engine/llm/providers/__init__.py +3 -0
  29. hcr/engine/llm/providers/anthropic.py +116 -0
  30. hcr/engine/llm/providers/google.py +115 -0
  31. hcr/engine/llm/providers/groq.py +168 -0
  32. hcr/engine/llm/providers/ollama.py +135 -0
  33. hcr/engine/llm/providers/openai.py +108 -0
  34. hcr/engine/memory/__init__.py +52 -0
  35. hcr/engine/memory/ccr_store.py +77 -0
  36. hcr/engine/memory/centrality.py +105 -0
  37. hcr/engine/memory/commit_extractor.py +279 -0
  38. hcr/engine/memory/constraint_propagation.py +478 -0
  39. hcr/engine/memory/context_export.py +178 -0
  40. hcr/engine/memory/cpap.py +542 -0
  41. hcr/engine/memory/dedup.py +150 -0
  42. hcr/engine/memory/embedding_store.py +549 -0
  43. hcr/engine/memory/feedback.py +426 -0
  44. hcr/engine/memory/fusion.py +147 -0
  45. hcr/engine/memory/git_ingestion.py +666 -0
  46. hcr/engine/memory/hyde.py +131 -0
  47. hcr/engine/memory/implicit_graph.py +40 -0
  48. hcr/engine/memory/merger.py +255 -0
  49. hcr/engine/memory/prefetch.py +148 -0
  50. hcr/engine/memory/projection.py +528 -0
  51. hcr/engine/memory/prospective.py +59 -0
  52. hcr/engine/memory/reranker.py +81 -0
  53. hcr/engine/memory/serializers.py +253 -0
  54. hcr/engine/memory/session_miner.py +238 -0
  55. hcr/engine/memory/sync.py +297 -0
  56. hcr/engine/state/__init__.py +11 -0
  57. hcr/engine/state/cognitive_state.py +140 -0
  58. hcr/engine/symbolic/__init__.py +6 -0
  59. hcr/engine/symbolic/friction_detector.py +86 -0
  60. hcr/engine/symbolic/profile_manager.py +103 -0
  61. hcr/engine/symbolic/rules.py +203 -0
  62. hcr/engine/symbolic/verifier.py +39 -0
  63. hcr/product/PRODUCT_SPEC.md +436 -0
  64. hcr/product/SYSTEM_DESIGN.md +1102 -0
  65. hcr/product/__init__.py +3 -0
  66. hcr/product/__main__.py +4 -0
  67. hcr/product/api/__init__.py +2 -0
  68. hcr/product/api/apikeys.py +141 -0
  69. hcr/product/api/auth.py +117 -0
  70. hcr/product/api/circuit_breaker.py +50 -0
  71. hcr/product/api/cpap_store.py +150 -0
  72. hcr/product/api/csos.py +118 -0
  73. hcr/product/api/device.py +158 -0
  74. hcr/product/api/main.py +80 -0
  75. hcr/product/api/mcp_endpoint.py +79 -0
  76. hcr/product/api/memory.py +114 -0
  77. hcr/product/api/middleware.py +74 -0
  78. hcr/product/api/preflight.py +76 -0
  79. hcr/product/api/rate_limit.py +113 -0
  80. hcr/product/api/state.py +146 -0
  81. hcr/product/api/stream.py +121 -0
  82. hcr/product/api/supabase_client.py +29 -0
  83. hcr/product/api/teams.py +543 -0
  84. hcr/product/api/telemetry.py +59 -0
  85. hcr/product/auth/__init__.py +3 -0
  86. hcr/product/auth/jwt_handler.py +78 -0
  87. hcr/product/auth/refresh.py +45 -0
  88. hcr/product/auth/token_store.py +123 -0
  89. hcr/product/auto_init.py +157 -0
  90. hcr/product/caching/__init__.py +5 -0
  91. hcr/product/caching/cache.py +119 -0
  92. hcr/product/cli/__init__.py +6 -0
  93. hcr/product/cli/auth_cmd.py +75 -0
  94. hcr/product/cli/banner.py +71 -0
  95. hcr/product/cli/commands/__init__.py +2 -0
  96. hcr/product/cli/commands/auth.py +196 -0
  97. hcr/product/cli/commands/doctor.py +346 -0
  98. hcr/product/cli/commands/export_cmd.py +145 -0
  99. hcr/product/cli/commands/forget.py +89 -0
  100. hcr/product/cli/commands/init_cmd.py +452 -0
  101. hcr/product/cli/commands/remember.py +58 -0
  102. hcr/product/cli/commands/search.py +174 -0
  103. hcr/product/cli/commands/setup_cmd.py +61 -0
  104. hcr/product/cli/commands/status.py +177 -0
  105. hcr/product/cli/commands/sync.py +447 -0
  106. hcr/product/cli/commands.py +170 -0
  107. hcr/product/cli/doctor.py +226 -0
  108. hcr/product/cli/explain.py +112 -0
  109. hcr/product/cli/install.py +75 -0
  110. hcr/product/cli/main.py +407 -0
  111. hcr/product/cli/resume.py +315 -0
  112. hcr/product/cli/setup.py +49 -0
  113. hcr/product/cli/theme.py +53 -0
  114. hcr/product/cli/ui.py +305 -0
  115. hcr/product/config.py +219 -0
  116. hcr/product/core/__init__.py +3 -0
  117. hcr/product/core/hook_health.py +124 -0
  118. hcr/product/daemon/__init__.py +4 -0
  119. hcr/product/daemon/__main__.py +19 -0
  120. hcr/product/daemon/git_hooks.py +93 -0
  121. hcr/product/daemon/terminal_logger.py +220 -0
  122. hcr/product/git_history_seeder.py +443 -0
  123. hcr/product/git_hook_trigger.py +262 -0
  124. hcr/product/hooks/__init__.py +2 -0
  125. hcr/product/hooks/claude_code.py +267 -0
  126. hcr/product/hooks/installer.py +119 -0
  127. hcr/product/integrations/__init__.py +2 -0
  128. hcr/product/integrations/config.py +204 -0
  129. hcr/product/integrations/mcp_server.py +3781 -0
  130. hcr/product/integrations/mcp_server_stdio.py +123 -0
  131. hcr/product/integrations/prometheus_metrics.py +380 -0
  132. hcr/product/integrations/request_context.py +43 -0
  133. hcr/product/integrations/telemetry_client.py +87 -0
  134. hcr/product/integrations/tools/__init__.py +65 -0
  135. hcr/product/integrations/tools/agent_tools.py +720 -0
  136. hcr/product/integrations/tools/base_tool.py +202 -0
  137. hcr/product/integrations/tools/ccr_tool.py +23 -0
  138. hcr/product/integrations/tools/context_tools.py +191 -0
  139. hcr/product/integrations/tools/coverage_tool.py +58 -0
  140. hcr/product/integrations/tools/cso_tools.py +95 -0
  141. hcr/product/integrations/tools/decision_tools.py +421 -0
  142. hcr/product/integrations/tools/file_tools.py +197 -0
  143. hcr/product/integrations/tools/health_tools.py +113 -0
  144. hcr/product/integrations/tools/impact_tools.py +174 -0
  145. hcr/product/integrations/tools/operator_tools.py +41 -0
  146. hcr/product/integrations/tools/output_synthesizer.py +781 -0
  147. hcr/product/integrations/tools/recommendation_tools.py +99 -0
  148. hcr/product/integrations/tools/risk_tools.py +82 -0
  149. hcr/product/integrations/tools/search_tools.py +349 -0
  150. hcr/product/integrations/tools/session_tools.py +354 -0
  151. hcr/product/integrations/tools/shared_state_tools.py +145 -0
  152. hcr/product/integrations/tools/state_tools.py +393 -0
  153. hcr/product/integrations/tools/task_tools.py +158 -0
  154. hcr/product/integrations/tools/telemetry_tools.py +226 -0
  155. hcr/product/integrations/tools/trigger_tools.py +66 -0
  156. hcr/product/integrations/tools/version_tools.py +144 -0
  157. hcr/product/integrations/tools/why_tools.py +99 -0
  158. hcr/product/integrations/transport.py +72 -0
  159. hcr/product/proxy/__init__.py +2 -0
  160. hcr/product/proxy/interceptor.py +132 -0
  161. hcr/product/security/__init__.py +2 -0
  162. hcr/product/security/policy_manager.py +147 -0
  163. hcr/product/server/__init__.py +3 -0
  164. hcr/product/state_capture/__init__.py +7 -0
  165. hcr/product/state_capture/file_watcher.py +353 -0
  166. hcr/product/state_capture/git_tracker.py +145 -0
  167. hcr/product/storage/__init__.py +6 -0
  168. hcr/product/storage/decisions_reader.py +56 -0
  169. hcr/product/storage/provenance.py +252 -0
  170. hcr/product/storage/semantic_decay.py +224 -0
  171. hcr/product/storage/state_persistence.py +661 -0
  172. hcr/product/storage/state_transaction.py +113 -0
  173. hcr/product/sync/__init__.py +2 -0
  174. hcr/product/sync/outbox.py +77 -0
  175. hcr_memory-0.5.0.dist-info/METADATA +632 -0
  176. hcr_memory-0.5.0.dist-info/RECORD +180 -0
  177. hcr_memory-0.5.0.dist-info/WHEEL +5 -0
  178. hcr_memory-0.5.0.dist-info/entry_points.txt +3 -0
  179. hcr_memory-0.5.0.dist-info/licenses/LICENSE +6 -0
  180. hcr_memory-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ {"epoch": "90af4049", "git_head": "", "frozen_at": "2026-05-29T06:21:56.605905+00:00", "cso_ids": [], "insertion_ranks": {}, "layer2_bytes": "## Project Memory\n"}
hcr/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """
4
+ HCR — Hybrid Cognitive Runtime
5
+
6
+ Persistent developer memory layer exposed as an MCP server.
7
+ The key insight: AI tools are stateless. HCR provides the stateful substrate
8
+ they lack — decisions, constraints, risks, and causal relationships that
9
+ persist across sessions, tools, and teammates.
10
+
11
+ HOW IT WORKS (3 layers):
12
+ 1. hcr/engine/ — Core reasoning: CSO store, memory fabric, inference.
13
+ 2. hcr/product/ — Product surface: MCP server, CLI, API, auth, storage.
14
+ 3. .hcr/ — Per-project data: cso.db, embeddings.db, feedback.db.
15
+
16
+ ENTRY POINTS:
17
+ python -m hcr.product.integrations.mcp_server_stdio # MCP server (for IDEs)
18
+ hcr init / hcr status / hcr resume # CLI
19
+
20
+ START READING:
21
+ hcr/engine/engine_api.py # HCREngine (central object)
22
+ hcr/product/integrations/mcp_server.py # MCP tool dispatch
23
+ hcr/engine/cso/cso_model.py # What a CSO is
24
+ """
25
+
26
+ __version__ = "0.5.0"
27
+
hcr/engine/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """
4
+ hcr.engine — Core reasoning engine
5
+
6
+ PACKAGE MAP — read in this order to understand the system:
7
+
8
+ engine_api.py — HCREngine: the central object. Owns all stores,
9
+ handles file-edit hot path, exposes the API that
10
+ MCP tools call. Start here.
11
+
12
+ cso/ — CSO (Cognitive State Object) data model + SQLite store.
13
+ cso_model.py: typed record (DECISION, CONSTRAINT, RISK,
14
+ TASK, OUTCOME, ...) with causal_in/causal_out edges.
15
+ cso_store.py: SQLite + WAL persistence at .hcr/cso.db.
16
+
17
+ memory/ — Cognitive State Fabric: retrieval intelligence.
18
+ See memory/__init__.py for per-file descriptions.
19
+
20
+ causal/ — Causal analysis: AST dependency graph, impact BFS,
21
+ event store for temporal reasoning.
22
+
23
+ inference/ — Inference engines: Causal Predictor, Constraint Learner,
24
+ Session Narrator, Root Cause Tracer (hcr_why).
25
+
26
+ symbolic/ — SymbolicVerifier: fires rules against new CSOs,
27
+ emits RISK CSOs when rules match.
28
+
29
+ llm/ — LLMProvider: unified interface for Groq / Gemini / Ollama.
30
+
31
+ state/ — CognitiveState dataclass + state transitions (legacy,
32
+ kept for backwards compat alongside CSO v2.0 store).
33
+ """
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """
4
+ HCR Causal Intelligence Package
5
+
6
+ The Temporal Causal Graph — HCR's core moat.
7
+ Tracks file dependencies, temporal event chains, and predicts impact.
8
+ """
9
+
10
+ from .dependency_graph import DependencyGraph
11
+ from .event_store import EventStore, CausalEvent
12
+ from .impact_analyzer import ImpactAnalyzer
13
+ from .cso_impact import query_cso_impact
14
+ from .temporal_correlator import TemporalCorrelator
15
+
16
+ __all__ = ["DependencyGraph", "EventStore", "CausalEvent", "ImpactAnalyzer", "query_cso_impact", "TemporalCorrelator"]
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """
4
+ AST Extractor
5
+
6
+ Extracts semantic dependencies (imports, function calls, class usage)
7
+ from Python source code to build the causal graph.
8
+ """
9
+
10
+ import ast
11
+ from pathlib import Path
12
+ from typing import List, Dict, Set
13
+
14
+ class DependencyExtractor(ast.NodeVisitor):
15
+ def __init__(self, current_module: str):
16
+ self.current_module = current_module
17
+ self.imports: Set[str] = set()
18
+ self.calls: Set[str] = set()
19
+
20
+ def visit_Import(self, node: ast.Import):
21
+ for alias in node.names:
22
+ self.imports.add(alias.name)
23
+ self.generic_visit(node)
24
+
25
+ def visit_ImportFrom(self, node: ast.ImportFrom):
26
+ if node.module:
27
+ self.imports.add(node.module)
28
+ self.generic_visit(node)
29
+
30
+ def visit_Call(self, node: ast.Call):
31
+ if isinstance(node.func, ast.Name):
32
+ self.calls.add(node.func.id)
33
+ elif isinstance(node.func, ast.Attribute):
34
+ self.calls.add(node.func.attr)
35
+ self.generic_visit(node)
36
+
37
+ def extract_dependencies(file_path: Path) -> Dict[str, List[str]]:
38
+ """Parse a python file and extract its dependencies."""
39
+ if not file_path.exists() or file_path.suffix != '.py':
40
+ return {"imports": [], "calls": []}
41
+
42
+ try:
43
+ content = file_path.read_text(encoding="utf-8")
44
+ tree = ast.parse(content)
45
+
46
+ # Simple module name extraction based on file path for this prototype
47
+ module_name = file_path.stem
48
+ extractor = DependencyExtractor(module_name)
49
+ extractor.visit(tree)
50
+
51
+ return {
52
+ "imports": list(extractor.imports),
53
+ "calls": list(extractor.calls)
54
+ }
55
+ except Exception as e:
56
+ # Ignore syntax errors or parsing issues for now
57
+ return {"imports": [], "calls": []}
58
+
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """Git co-change mining: extract file pairs frequently committed together."""
4
+
5
+ import subprocess
6
+ import logging
7
+ from collections import defaultdict
8
+ from pathlib import Path
9
+ from typing import Dict, List, Tuple
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _MIN_CO_CHANGES = 3 # minimum co-occurrences to consider a relationship
14
+ _MAX_COMMITS_SCAN = 500 # only scan recent history (performance)
15
+
16
+
17
+ def mine_co_changes(
18
+ project_path: Path,
19
+ min_count: int = _MIN_CO_CHANGES,
20
+ max_commits: int = _MAX_COMMITS_SCAN,
21
+ ) -> Dict[str, List[Tuple[str, int]]]:
22
+ """
23
+ Parse git log and return co-change map: {file_path: [(co_changed_file, count), ...]}.
24
+ Only pairs with count >= min_count are included.
25
+ """
26
+ try:
27
+ result = subprocess.run(
28
+ ["git", "log", f"--max-count={max_commits}", "--name-only", "--pretty=format:COMMIT"],
29
+ capture_output=True,
30
+ text=True,
31
+ cwd=str(project_path),
32
+ timeout=30,
33
+ )
34
+ if result.returncode != 0:
35
+ logger.debug("git log failed: %s", result.stderr[:200])
36
+ return {}
37
+ return _parse_co_changes(result.stdout, min_count)
38
+ except Exception as e:
39
+ logger.debug("co-change mining error: %s", e)
40
+ return {}
41
+
42
+
43
+ def _parse_co_changes(git_log_output: str, min_count: int) -> Dict[str, List[Tuple[str, int]]]:
44
+ """Parse git log --name-only output into co-change pairs."""
45
+ pair_counts: Dict[Tuple[str, str], int] = defaultdict(int)
46
+
47
+ current_files: List[str] = []
48
+ for line in git_log_output.splitlines():
49
+ line = line.strip()
50
+ if line == "COMMIT":
51
+ # Process previous commit's files
52
+ if len(current_files) >= 2:
53
+ for i, f1 in enumerate(current_files):
54
+ for f2 in current_files[i + 1:]:
55
+ key = (min(f1, f2), max(f1, f2))
56
+ pair_counts[key] += 1
57
+ current_files = []
58
+ elif line:
59
+ current_files.append(line)
60
+
61
+ # Process last commit
62
+ if len(current_files) >= 2:
63
+ for i, f1 in enumerate(current_files):
64
+ for f2 in current_files[i + 1:]:
65
+ key = (min(f1, f2), max(f1, f2))
66
+ pair_counts[key] += 1
67
+
68
+ # Build adjacency map
69
+ co_change_map: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
70
+ for (f1, f2), count in pair_counts.items():
71
+ if count >= min_count:
72
+ co_change_map[f1].append((f2, count))
73
+ co_change_map[f2].append((f1, count))
74
+
75
+ # Sort each list by count desc
76
+ for path in co_change_map:
77
+ co_change_map[path].sort(key=lambda x: x[1], reverse=True)
78
+
79
+ return dict(co_change_map)
80
+
81
+
82
+ def get_co_change_candidates(
83
+ file_path: str,
84
+ co_change_map: Dict[str, List[Tuple[str, int]]],
85
+ ) -> List[str]:
86
+ """Return list of co-changed file paths for a given file, sorted by frequency."""
87
+ # Normalize path separators
88
+ normalized = file_path.replace("\\", "/")
89
+ # Try exact match first, then suffix match
90
+ candidates = co_change_map.get(normalized, [])
91
+ if not candidates:
92
+ for k, v in co_change_map.items():
93
+ if k.endswith(normalized) or normalized.endswith(k):
94
+ candidates = v
95
+ break
96
+ return [f for f, _ in candidates]
@@ -0,0 +1,110 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """
4
+ CSO-graph-based impact analysis for hcr_analyze_impact (v2.0).
5
+
6
+ Traverses causal_in edges stored in the CSOStore to find all files
7
+ that are transitively impacted by a change to the given file_path.
8
+
9
+ Semantic attention filter (Phase 2): when a query_embedding is provided,
10
+ only traverse edges where the candidate CSO's embedding similarity to the
11
+ query exceeds similarity_threshold. Prevents graph wandering.
12
+ """
13
+ import logging
14
+ from typing import Any, List, Optional, Set
15
+
16
+ from hcr.engine.cso.cso_store import CSOStore
17
+
18
+ _logger = logging.getLogger("HCR-CSOImpact")
19
+
20
+
21
+ def query_cso_impact(
22
+ store: CSOStore,
23
+ file_path: str,
24
+ max_depth: int = 3,
25
+ query_embedding: Optional[List[float]] = None,
26
+ similarity_threshold: float = 0.65,
27
+ embedding_store: Optional[Any] = None,
28
+ feedback_store: Optional[Any] = None,
29
+ ) -> List[str]:
30
+ """Return files transitively impacted by a change to *file_path*.
31
+
32
+ Performs a semantic attention BFS over causal_in edges up to *max_depth* hops.
33
+ When query_embedding is provided, only expands edges where the candidate
34
+ CSO's embedding similarity exceeds similarity_threshold.
35
+
36
+ When embedding_store is provided, fuses the BFS result with a semantic ANN
37
+ search via Reciprocal Rank Fusion (RRF) for a richer ranked result list.
38
+
39
+ Args:
40
+ store: Initialised CSOStore to query.
41
+ file_path: The changed file (as stored in CSO payload["file_path"]).
42
+ max_depth: Maximum BFS depth (clamped to >= 1).
43
+ query_embedding: Optional embedding vector for semantic filtering.
44
+ similarity_threshold: Minimum cosine similarity to traverse an edge.
45
+ embedding_store: Optional EmbeddingStore for RRF semantic fusion.
46
+
47
+ Returns:
48
+ List of file_path strings that are downstream of the given file.
49
+ Empty list if no causal edges exist for this file.
50
+ """
51
+ max_depth = max(1, max_depth)
52
+
53
+ all_csos = store.query(cso_type=None, limit=2000)
54
+
55
+ id_to_file: dict = {}
56
+ file_to_ids: dict = {}
57
+ for cso in all_csos:
58
+ fp = cso.payload.get("file_path", "")
59
+ if fp:
60
+ id_to_file[cso.id] = fp
61
+ file_to_ids.setdefault(fp, set()).add(cso.id)
62
+
63
+ frontier: Set[str] = set(file_to_ids.get(file_path, []))
64
+ visited: Set[str] = set(frontier)
65
+ impacted_files: Set[str] = set()
66
+
67
+ for _ in range(max_depth):
68
+ if not frontier:
69
+ break
70
+ next_frontier: Set[str] = set()
71
+ for cso in all_csos:
72
+ if not any(cause_id in frontier for cause_id in cso.causal_in):
73
+ continue
74
+ if cso.id in visited:
75
+ continue
76
+ # Semantic attention filter: skip if query provided and similarity low
77
+ if query_embedding and hasattr(cso, "_embedding") and cso._embedding:
78
+ from hcr.engine.memory.fusion import _cosine
79
+ if _cosine(cso._embedding, query_embedding) < similarity_threshold:
80
+ continue
81
+ visited.add(cso.id)
82
+ next_frontier.add(cso.id)
83
+ fp = cso.payload.get("file_path", "")
84
+ if fp and fp != file_path:
85
+ impacted_files.add(fp)
86
+ frontier = next_frontier
87
+
88
+ # CSF Phase 2d: RRF fusion with semantic ANN search when embedding_store available
89
+ if embedding_store is not None:
90
+ try:
91
+ from hcr.engine.memory.fusion import reciprocal_rank_fusion
92
+ causal_ranked = [(fp, 1.0 / (i + 1)) for i, fp in enumerate(impacted_files)]
93
+ semantic_hits = embedding_store.search(file_path, top_k=20)
94
+ semantic_ranked = []
95
+ for cso_id, dist in semantic_hits:
96
+ fp = id_to_file.get(cso_id, "")
97
+ if fp and fp != file_path:
98
+ semantic_ranked.append((fp, dist))
99
+ weights = None
100
+ if feedback_store is not None:
101
+ try:
102
+ weights = feedback_store.get_weights()
103
+ except Exception:
104
+ _logger.debug("Feedback weights lookup failed", exc_info=True)
105
+ fused = reciprocal_rank_fusion(semantic_ranked, causal_ranked, weights=weights)
106
+ return [fp for fp, _ in fused if fp]
107
+ except Exception:
108
+ _logger.debug("RRF fusion fallback to plain BFS", exc_info=True)
109
+
110
+ return list(impacted_files)
@@ -0,0 +1,89 @@
1
+ # Copyright (c) 2025-2026 Rishi Praseeth Krishnan. All rights reserved.
2
+ # Proprietary and confidential. Unauthorized copying or distribution prohibited.
3
+ """
4
+ Dependency Graph
5
+
6
+ An in-memory directed graph representing file and functional dependencies
7
+ derived from the AST and causal events.
8
+ """
9
+
10
+ from typing import Dict, List, Set
11
+ from pathlib import Path
12
+
13
+ class DependencyGraph:
14
+ def __init__(self):
15
+ # Maps a node (file path or module) to a list of nodes it depends on
16
+ self.forward_edges: Dict[str, Set[str]] = {}
17
+ # Maps a node to a list of nodes that depend on it
18
+ self.reverse_edges: Dict[str, Set[str]] = {}
19
+ # Latent links discovered by LLM (cause, effect, type, reason)
20
+ self.latent_edges: List[dict] = []
21
+
22
+ def add_dependency(self, source: str, target: str):
23
+ """Add a directed edge from source to target (source depends on target)."""
24
+ if source not in self.forward_edges:
25
+ self.forward_edges[source] = set()
26
+ self.forward_edges[source].add(target)
27
+
28
+ if target not in self.reverse_edges:
29
+ self.reverse_edges[target] = set()
30
+ self.reverse_edges[target].add(source)
31
+
32
+ def add_latent_link(self, source: str, target: str, link_type: str = "latent", reason: str = ""):
33
+ """Add a latent link discovered by neural inference."""
34
+ self.latent_edges.append({
35
+ "source": source,
36
+ "target": target,
37
+ "type": link_type,
38
+ "reason": reason
39
+ })
40
+ # Also add to standard graph for impact analysis
41
+ self.add_dependency(source, target)
42
+
43
+ def get_metrics(self, node: str) -> dict:
44
+ """Calculate and return metrics for a specific node"""
45
+ from .metrics import MetricsAnalyzer
46
+ fragility = MetricsAnalyzer.calculate_fragility(node)
47
+ centrality = MetricsAnalyzer.calculate_centrality(
48
+ node,
49
+ self.forward_edges,
50
+ self.reverse_edges
51
+ )
52
+ return {
53
+ "fragility": fragility,
54
+ "centrality": centrality,
55
+ "risk_score": round((fragility + centrality) / 2, 2)
56
+ }
57
+
58
+ def to_dict(self) -> dict:
59
+ """Convert graph to dictionary with node metrics and latent links"""
60
+ nodes = list(set(list(self.forward_edges.keys()) + list(self.reverse_edges.keys())))
61
+ node_data = {node: self.get_metrics(node) for node in nodes}
62
+
63
+ return {
64
+ "forward": self.forward_edges,
65
+ "reverse": self.reverse_edges,
66
+ "latent_links": self.latent_edges,
67
+ "metrics": node_data
68
+ }
69
+
70
+ def get_dependencies(self, node: str) -> List[str]:
71
+ """Get all nodes that the given node depends on."""
72
+ return list(self.forward_edges.get(node, set()))
73
+
74
+ def get_dependents(self, node: str) -> List[str]:
75
+ """Get all nodes that depend on the given node."""
76
+ return list(self.reverse_edges.get(node, set()))
77
+
78
+ def update_file_dependencies(self, file_path: str, dependencies: List[str]):
79
+ """Replace existing dependencies for a file with a new set."""
80
+ # Remove old forward edges
81
+ old_deps = self.forward_edges.get(file_path, set())
82
+ for target in old_deps:
83
+ if file_path in self.reverse_edges.get(target, set()):
84
+ self.reverse_edges[target].remove(file_path)
85
+
86
+ # Set new dependencies
87
+ self.forward_edges[file_path] = set()
88
+ for target in dependencies:
89
+ self.add_dependency(file_path, target)