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.
- hcr/.hcr/cpap_epoch.json +1 -0
- hcr/__init__.py +27 -0
- hcr/engine/__init__.py +33 -0
- hcr/engine/causal/__init__.py +16 -0
- hcr/engine/causal/ast_extractor.py +58 -0
- hcr/engine/causal/co_change_miner.py +96 -0
- hcr/engine/causal/cso_impact.py +110 -0
- hcr/engine/causal/dependency_graph.py +89 -0
- hcr/engine/causal/event_store.py +270 -0
- hcr/engine/causal/impact_analyzer.py +35 -0
- hcr/engine/causal/metrics.py +56 -0
- hcr/engine/causal/temporal_correlator.py +409 -0
- hcr/engine/core/__init__.py +3 -0
- hcr/engine/cso/__init__.py +5 -0
- hcr/engine/cso/agent_registry.py +55 -0
- hcr/engine/cso/backend.py +133 -0
- hcr/engine/cso/cso_migrator.py +74 -0
- hcr/engine/cso/cso_model.py +108 -0
- hcr/engine/cso/cso_store.py +970 -0
- hcr/engine/engine_api.py +1784 -0
- hcr/engine/inference/__init__.py +15 -0
- hcr/engine/inference/constraint_learner.py +233 -0
- hcr/engine/inference/narrator.py +151 -0
- hcr/engine/inference/predictor.py +160 -0
- hcr/engine/inference/tracer.py +234 -0
- hcr/engine/llm/__init__.py +11 -0
- hcr/engine/llm/llm_provider.py +191 -0
- hcr/engine/llm/providers/__init__.py +3 -0
- hcr/engine/llm/providers/anthropic.py +116 -0
- hcr/engine/llm/providers/google.py +115 -0
- hcr/engine/llm/providers/groq.py +168 -0
- hcr/engine/llm/providers/ollama.py +135 -0
- hcr/engine/llm/providers/openai.py +108 -0
- hcr/engine/memory/__init__.py +52 -0
- hcr/engine/memory/ccr_store.py +77 -0
- hcr/engine/memory/centrality.py +105 -0
- hcr/engine/memory/commit_extractor.py +279 -0
- hcr/engine/memory/constraint_propagation.py +478 -0
- hcr/engine/memory/context_export.py +178 -0
- hcr/engine/memory/cpap.py +542 -0
- hcr/engine/memory/dedup.py +150 -0
- hcr/engine/memory/embedding_store.py +549 -0
- hcr/engine/memory/feedback.py +426 -0
- hcr/engine/memory/fusion.py +147 -0
- hcr/engine/memory/git_ingestion.py +666 -0
- hcr/engine/memory/hyde.py +131 -0
- hcr/engine/memory/implicit_graph.py +40 -0
- hcr/engine/memory/merger.py +255 -0
- hcr/engine/memory/prefetch.py +148 -0
- hcr/engine/memory/projection.py +528 -0
- hcr/engine/memory/prospective.py +59 -0
- hcr/engine/memory/reranker.py +81 -0
- hcr/engine/memory/serializers.py +253 -0
- hcr/engine/memory/session_miner.py +238 -0
- hcr/engine/memory/sync.py +297 -0
- hcr/engine/state/__init__.py +11 -0
- hcr/engine/state/cognitive_state.py +140 -0
- hcr/engine/symbolic/__init__.py +6 -0
- hcr/engine/symbolic/friction_detector.py +86 -0
- hcr/engine/symbolic/profile_manager.py +103 -0
- hcr/engine/symbolic/rules.py +203 -0
- hcr/engine/symbolic/verifier.py +39 -0
- hcr/product/PRODUCT_SPEC.md +436 -0
- hcr/product/SYSTEM_DESIGN.md +1102 -0
- hcr/product/__init__.py +3 -0
- hcr/product/__main__.py +4 -0
- hcr/product/api/__init__.py +2 -0
- hcr/product/api/apikeys.py +141 -0
- hcr/product/api/auth.py +117 -0
- hcr/product/api/circuit_breaker.py +50 -0
- hcr/product/api/cpap_store.py +150 -0
- hcr/product/api/csos.py +118 -0
- hcr/product/api/device.py +158 -0
- hcr/product/api/main.py +80 -0
- hcr/product/api/mcp_endpoint.py +79 -0
- hcr/product/api/memory.py +114 -0
- hcr/product/api/middleware.py +74 -0
- hcr/product/api/preflight.py +76 -0
- hcr/product/api/rate_limit.py +113 -0
- hcr/product/api/state.py +146 -0
- hcr/product/api/stream.py +121 -0
- hcr/product/api/supabase_client.py +29 -0
- hcr/product/api/teams.py +543 -0
- hcr/product/api/telemetry.py +59 -0
- hcr/product/auth/__init__.py +3 -0
- hcr/product/auth/jwt_handler.py +78 -0
- hcr/product/auth/refresh.py +45 -0
- hcr/product/auth/token_store.py +123 -0
- hcr/product/auto_init.py +157 -0
- hcr/product/caching/__init__.py +5 -0
- hcr/product/caching/cache.py +119 -0
- hcr/product/cli/__init__.py +6 -0
- hcr/product/cli/auth_cmd.py +75 -0
- hcr/product/cli/banner.py +71 -0
- hcr/product/cli/commands/__init__.py +2 -0
- hcr/product/cli/commands/auth.py +196 -0
- hcr/product/cli/commands/doctor.py +346 -0
- hcr/product/cli/commands/export_cmd.py +145 -0
- hcr/product/cli/commands/forget.py +89 -0
- hcr/product/cli/commands/init_cmd.py +452 -0
- hcr/product/cli/commands/remember.py +58 -0
- hcr/product/cli/commands/search.py +174 -0
- hcr/product/cli/commands/setup_cmd.py +61 -0
- hcr/product/cli/commands/status.py +177 -0
- hcr/product/cli/commands/sync.py +447 -0
- hcr/product/cli/commands.py +170 -0
- hcr/product/cli/doctor.py +226 -0
- hcr/product/cli/explain.py +112 -0
- hcr/product/cli/install.py +75 -0
- hcr/product/cli/main.py +407 -0
- hcr/product/cli/resume.py +315 -0
- hcr/product/cli/setup.py +49 -0
- hcr/product/cli/theme.py +53 -0
- hcr/product/cli/ui.py +305 -0
- hcr/product/config.py +219 -0
- hcr/product/core/__init__.py +3 -0
- hcr/product/core/hook_health.py +124 -0
- hcr/product/daemon/__init__.py +4 -0
- hcr/product/daemon/__main__.py +19 -0
- hcr/product/daemon/git_hooks.py +93 -0
- hcr/product/daemon/terminal_logger.py +220 -0
- hcr/product/git_history_seeder.py +443 -0
- hcr/product/git_hook_trigger.py +262 -0
- hcr/product/hooks/__init__.py +2 -0
- hcr/product/hooks/claude_code.py +267 -0
- hcr/product/hooks/installer.py +119 -0
- hcr/product/integrations/__init__.py +2 -0
- hcr/product/integrations/config.py +204 -0
- hcr/product/integrations/mcp_server.py +3781 -0
- hcr/product/integrations/mcp_server_stdio.py +123 -0
- hcr/product/integrations/prometheus_metrics.py +380 -0
- hcr/product/integrations/request_context.py +43 -0
- hcr/product/integrations/telemetry_client.py +87 -0
- hcr/product/integrations/tools/__init__.py +65 -0
- hcr/product/integrations/tools/agent_tools.py +720 -0
- hcr/product/integrations/tools/base_tool.py +202 -0
- hcr/product/integrations/tools/ccr_tool.py +23 -0
- hcr/product/integrations/tools/context_tools.py +191 -0
- hcr/product/integrations/tools/coverage_tool.py +58 -0
- hcr/product/integrations/tools/cso_tools.py +95 -0
- hcr/product/integrations/tools/decision_tools.py +421 -0
- hcr/product/integrations/tools/file_tools.py +197 -0
- hcr/product/integrations/tools/health_tools.py +113 -0
- hcr/product/integrations/tools/impact_tools.py +174 -0
- hcr/product/integrations/tools/operator_tools.py +41 -0
- hcr/product/integrations/tools/output_synthesizer.py +781 -0
- hcr/product/integrations/tools/recommendation_tools.py +99 -0
- hcr/product/integrations/tools/risk_tools.py +82 -0
- hcr/product/integrations/tools/search_tools.py +349 -0
- hcr/product/integrations/tools/session_tools.py +354 -0
- hcr/product/integrations/tools/shared_state_tools.py +145 -0
- hcr/product/integrations/tools/state_tools.py +393 -0
- hcr/product/integrations/tools/task_tools.py +158 -0
- hcr/product/integrations/tools/telemetry_tools.py +226 -0
- hcr/product/integrations/tools/trigger_tools.py +66 -0
- hcr/product/integrations/tools/version_tools.py +144 -0
- hcr/product/integrations/tools/why_tools.py +99 -0
- hcr/product/integrations/transport.py +72 -0
- hcr/product/proxy/__init__.py +2 -0
- hcr/product/proxy/interceptor.py +132 -0
- hcr/product/security/__init__.py +2 -0
- hcr/product/security/policy_manager.py +147 -0
- hcr/product/server/__init__.py +3 -0
- hcr/product/state_capture/__init__.py +7 -0
- hcr/product/state_capture/file_watcher.py +353 -0
- hcr/product/state_capture/git_tracker.py +145 -0
- hcr/product/storage/__init__.py +6 -0
- hcr/product/storage/decisions_reader.py +56 -0
- hcr/product/storage/provenance.py +252 -0
- hcr/product/storage/semantic_decay.py +224 -0
- hcr/product/storage/state_persistence.py +661 -0
- hcr/product/storage/state_transaction.py +113 -0
- hcr/product/sync/__init__.py +2 -0
- hcr/product/sync/outbox.py +77 -0
- hcr_memory-0.5.0.dist-info/METADATA +632 -0
- hcr_memory-0.5.0.dist-info/RECORD +180 -0
- hcr_memory-0.5.0.dist-info/WHEEL +5 -0
- hcr_memory-0.5.0.dist-info/entry_points.txt +3 -0
- hcr_memory-0.5.0.dist-info/licenses/LICENSE +6 -0
- hcr_memory-0.5.0.dist-info/top_level.txt +1 -0
hcr/.hcr/cpap_epoch.json
ADDED
|
@@ -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)
|