sourcecode 1.35.0__tar.gz → 1.35.2__tar.gz
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.
- {sourcecode-1.35.0 → sourcecode-1.35.2}/PKG-INFO +2 -2
- {sourcecode-1.35.0 → sourcecode-1.35.2}/README.md +1 -1
- {sourcecode-1.35.0 → sourcecode-1.35.2}/pyproject.toml +1 -1
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/canonical_ir.py +20 -1
- sourcecode-1.35.2/src/sourcecode/cir_graphs.py +186 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/cli.py +151 -4
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/server.py +40 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/spring_findings.py +17 -0
- sourcecode-1.35.2/src/sourcecode/spring_impact.py +719 -0
- sourcecode-1.35.2/src/sourcecode/spring_model.py +399 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/spring_security_audit.py +70 -44
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/spring_tx_analyzer.py +63 -46
- {sourcecode-1.35.0 → sourcecode-1.35.2}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/.gitignore +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/.ruff.toml +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/CHANGELOG.md +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/LICENSE +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/SECURITY.md +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/raw +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.2}/src/sourcecode/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.2
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
|
|
40
40
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
41
41
|
|
|
42
|
-

|
|
43
43
|

|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -20,8 +20,11 @@ from dataclasses import dataclass, field
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Any, Optional
|
|
22
22
|
|
|
23
|
+
from sourcecode.cir_graphs import ImplementationGraph, InjectionGraph
|
|
23
24
|
from sourcecode.repository_ir import (
|
|
24
25
|
build_repo_ir,
|
|
26
|
+
)
|
|
27
|
+
from sourcecode.repository_ir import (
|
|
25
28
|
compute_blast_radius as _compute_blast_radius,
|
|
26
29
|
)
|
|
27
30
|
|
|
@@ -78,7 +81,7 @@ class CanonicalSecurity:
|
|
|
78
81
|
@classmethod
|
|
79
82
|
def from_policy_dict(
|
|
80
83
|
cls, d: dict, *, source_scope: str = "method"
|
|
81
|
-
) ->
|
|
84
|
+
) -> CanonicalSecurity:
|
|
82
85
|
"""Build from the policy dict emitted by _route_security_from_sym."""
|
|
83
86
|
return cls(
|
|
84
87
|
policy=d.get("policy", ""),
|
|
@@ -165,6 +168,15 @@ class CanonicalRepositoryIR:
|
|
|
165
168
|
endpoints: list[CanonicalEndpoint] # canonical endpoint list
|
|
166
169
|
security_index: dict[str, CanonicalSecurity] # handler_symbol → security
|
|
167
170
|
metadata: dict[str, Any] # stats, gaps, subsystems, etc.
|
|
171
|
+
# Derived graph indices — built from dependencies at CIR construction time.
|
|
172
|
+
# CH-001: interface → implementation(s) lookup
|
|
173
|
+
implementation_graph: ImplementationGraph = field(
|
|
174
|
+
default_factory=ImplementationGraph, repr=False, compare=False
|
|
175
|
+
)
|
|
176
|
+
# CH-002: DI injection dependency → dependents + field/constructor lifting
|
|
177
|
+
injection_graph: InjectionGraph = field(
|
|
178
|
+
default_factory=InjectionGraph, repr=False, compare=False
|
|
179
|
+
)
|
|
168
180
|
# Raw IR dict retained for projections that need full IR fields
|
|
169
181
|
# (e.g. project_blast_radius delegates to compute_blast_radius)
|
|
170
182
|
_raw_ir: dict = field(default_factory=dict, repr=False, compare=False)
|
|
@@ -339,6 +351,11 @@ def ir_dict_to_canonical(
|
|
|
339
351
|
IR_SCHEMA_VERSION, files, symbols, endpoints, call_graph
|
|
340
352
|
)
|
|
341
353
|
|
|
354
|
+
# Derived graph indices built from dependency edges
|
|
355
|
+
known_symbols: set[str] = set(symbols)
|
|
356
|
+
impl_graph = ImplementationGraph.build(dependencies, known_symbols)
|
|
357
|
+
inj_graph = InjectionGraph.build(dependencies)
|
|
358
|
+
|
|
342
359
|
return CanonicalRepositoryIR(
|
|
343
360
|
schema_version=IR_SCHEMA_VERSION,
|
|
344
361
|
cir_hash=cir_hash,
|
|
@@ -350,6 +367,8 @@ def ir_dict_to_canonical(
|
|
|
350
367
|
endpoints=endpoints,
|
|
351
368
|
security_index=security_index,
|
|
352
369
|
metadata=metadata,
|
|
370
|
+
implementation_graph=impl_graph,
|
|
371
|
+
injection_graph=inj_graph,
|
|
353
372
|
_raw_ir=ir,
|
|
354
373
|
)
|
|
355
374
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""cir_graphs.py — Derived graph indices built from CanonicalRepositoryIR.
|
|
2
|
+
|
|
3
|
+
ImplementationGraph (CH-001): interface → implementation(s) lookup.
|
|
4
|
+
InjectionGraph (CH-002): DI dependency → dependents lookup, with field/constructor lifting.
|
|
5
|
+
|
|
6
|
+
Both are built from cir.dependencies (implements + injects edges) and are keyed to
|
|
7
|
+
known CIR symbols only. External interfaces (java.io.Serializable, etc.) are excluded.
|
|
8
|
+
|
|
9
|
+
Architecture constraint: these classes depend only on CIR data. They must never import
|
|
10
|
+
from spring_model, spring_impact, or any semantic layer.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# ImplementationGraph — CH-001
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ImplementationGraph:
|
|
22
|
+
"""Maps interface FQNs to their in-repo implementing classes, and vice-versa.
|
|
23
|
+
|
|
24
|
+
Built from implements edges where BOTH ends are known CIR symbols (internal
|
|
25
|
+
interface/class pairs). External framework interfaces are excluded.
|
|
26
|
+
"""
|
|
27
|
+
_impl_of: dict[str, list[str]] = field(default_factory=dict)
|
|
28
|
+
_ifaces_of: dict[str, list[str]] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Queries
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def implementations_of(self, interface_fqn: str) -> list[str]:
|
|
35
|
+
"""Return FQNs of classes that implement interface_fqn (in-repo only)."""
|
|
36
|
+
return self._impl_of.get(interface_fqn, [])
|
|
37
|
+
|
|
38
|
+
def interfaces_of(self, class_fqn: str) -> list[str]:
|
|
39
|
+
"""Return FQNs of in-repo interfaces implemented by class_fqn."""
|
|
40
|
+
return self._ifaces_of.get(class_fqn, [])
|
|
41
|
+
|
|
42
|
+
def primary_implementation(self, interface_fqn: str) -> str | None:
|
|
43
|
+
"""Return the single implementation if unambiguous, else None.
|
|
44
|
+
|
|
45
|
+
A single implementation is unambiguous by definition.
|
|
46
|
+
Multiple implementations are ambiguous — callers must decide.
|
|
47
|
+
@Primary detection is not yet implemented (requires annotation data in CIR).
|
|
48
|
+
"""
|
|
49
|
+
impls = self._impl_of.get(interface_fqn, [])
|
|
50
|
+
return impls[0] if len(impls) == 1 else None
|
|
51
|
+
|
|
52
|
+
def has_implementations(self, interface_fqn: str) -> bool:
|
|
53
|
+
return bool(self._impl_of.get(interface_fqn))
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Builder
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def build(
|
|
61
|
+
cls,
|
|
62
|
+
dependencies: list[dict],
|
|
63
|
+
known_symbols: set[str],
|
|
64
|
+
) -> ImplementationGraph:
|
|
65
|
+
"""Build from CIR dependencies list, restricting to known in-repo symbols.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
dependencies: cir.dependencies — list of edge dicts with 'from'/'to'/'type'
|
|
69
|
+
known_symbols: set(cir.symbols) — only in-repo FQNs
|
|
70
|
+
|
|
71
|
+
Excludes implements edges where the interface (to_fqn) is NOT in known_symbols
|
|
72
|
+
(e.g. java.io.Serializable, org.springframework.* framework interfaces).
|
|
73
|
+
Includes edges where the implementing class (from_fqn) is NOT in known_symbols
|
|
74
|
+
only when the interface IS known — this handles partial-parse edge cases.
|
|
75
|
+
"""
|
|
76
|
+
impl_of: dict[str, list[str]] = {}
|
|
77
|
+
ifaces_of: dict[str, list[str]] = {}
|
|
78
|
+
|
|
79
|
+
for edge in dependencies:
|
|
80
|
+
if edge.get("type") != "implements":
|
|
81
|
+
continue
|
|
82
|
+
from_fqn = (edge.get("from") or "").strip()
|
|
83
|
+
to_fqn = (edge.get("to") or "").strip()
|
|
84
|
+
if not from_fqn or not to_fqn:
|
|
85
|
+
continue
|
|
86
|
+
# Only track when the interface is an in-repo symbol
|
|
87
|
+
if to_fqn not in known_symbols:
|
|
88
|
+
continue
|
|
89
|
+
# Ignore malformed FQNs (e.g. generic type fragments like "Long>")
|
|
90
|
+
if ">" in to_fqn or "<" in to_fqn:
|
|
91
|
+
continue
|
|
92
|
+
if ">" in from_fqn or "<" in from_fqn:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if from_fqn not in impl_of.get(to_fqn, []):
|
|
96
|
+
impl_of.setdefault(to_fqn, []).append(from_fqn)
|
|
97
|
+
if to_fqn not in ifaces_of.get(from_fqn, []):
|
|
98
|
+
ifaces_of.setdefault(from_fqn, []).append(to_fqn)
|
|
99
|
+
|
|
100
|
+
return cls(_impl_of=impl_of, _ifaces_of=ifaces_of)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# InjectionGraph — CH-002
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class InjectionGraph:
|
|
109
|
+
"""Maps DI injection edges to class-level dependency relationships.
|
|
110
|
+
|
|
111
|
+
Resolves field FQN and constructor FQN injectors to their enclosing class,
|
|
112
|
+
enabling BFS traversal to continue past injection boundaries.
|
|
113
|
+
|
|
114
|
+
Injects edge forms:
|
|
115
|
+
constructor: ClassName#<init> → DependencyFQN
|
|
116
|
+
field: ClassName#fieldName → DependencyFQN
|
|
117
|
+
lombok: ClassName → DependencyFQN (already class-level)
|
|
118
|
+
"""
|
|
119
|
+
_deps_of: dict[str, list[str]] = field(default_factory=dict)
|
|
120
|
+
_dependents_of: dict[str, list[str]] = field(default_factory=dict)
|
|
121
|
+
# Maps field/constructor FQN → enclosing class FQN
|
|
122
|
+
_injector_to_class: dict[str, str] = field(default_factory=dict)
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Queries
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def dependencies_of(self, class_fqn: str) -> list[str]:
|
|
129
|
+
"""Return service FQNs injected into class_fqn (de-duplicated, sorted)."""
|
|
130
|
+
return self._deps_of.get(class_fqn, [])
|
|
131
|
+
|
|
132
|
+
def dependents_of(self, service_fqn: str) -> list[str]:
|
|
133
|
+
"""Return class FQNs that inject service_fqn (class-level, de-duplicated)."""
|
|
134
|
+
return self._dependents_of.get(service_fqn, [])
|
|
135
|
+
|
|
136
|
+
def class_of_injector(self, injector_fqn: str) -> str | None:
|
|
137
|
+
"""Resolve a field/constructor FQN to its enclosing class.
|
|
138
|
+
|
|
139
|
+
Returns None if injector_fqn is not a known injection site.
|
|
140
|
+
"""
|
|
141
|
+
return self._injector_to_class.get(injector_fqn)
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Builder
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def build(cls, dependencies: list[dict]) -> InjectionGraph:
|
|
149
|
+
"""Build from CIR dependencies list.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
dependencies: cir.dependencies — list of edge dicts with 'from'/'to'/'type'
|
|
153
|
+
"""
|
|
154
|
+
deps_of: dict[str, list[str]] = {}
|
|
155
|
+
dependents_of: dict[str, list[str]] = {}
|
|
156
|
+
injector_to_class: dict[str, str] = {}
|
|
157
|
+
|
|
158
|
+
for edge in dependencies:
|
|
159
|
+
if edge.get("type") != "injects":
|
|
160
|
+
continue
|
|
161
|
+
from_fqn = (edge.get("from") or "").strip()
|
|
162
|
+
to_fqn = (edge.get("to") or "").strip()
|
|
163
|
+
if not from_fqn or not to_fqn:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Resolve injector to class level
|
|
167
|
+
if "#" in from_fqn:
|
|
168
|
+
class_fqn = from_fqn.rsplit("#", 1)[0]
|
|
169
|
+
injector_to_class[from_fqn] = class_fqn
|
|
170
|
+
else:
|
|
171
|
+
class_fqn = from_fqn
|
|
172
|
+
|
|
173
|
+
# Build class → [dep, ...] and service → [class, ...] indices
|
|
174
|
+
deps = deps_of.setdefault(class_fqn, [])
|
|
175
|
+
if to_fqn not in deps:
|
|
176
|
+
deps.append(to_fqn)
|
|
177
|
+
|
|
178
|
+
dependents = dependents_of.setdefault(to_fqn, [])
|
|
179
|
+
if class_fqn not in dependents:
|
|
180
|
+
dependents.append(class_fqn)
|
|
181
|
+
|
|
182
|
+
return cls(
|
|
183
|
+
_deps_of=deps_of,
|
|
184
|
+
_dependents_of=dependents_of,
|
|
185
|
+
_injector_to_class=injector_to_class,
|
|
186
|
+
)
|
|
@@ -225,6 +225,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
|
|
|
225
225
|
"cold-start",
|
|
226
226
|
# Spring semantic audit
|
|
227
227
|
"spring-audit",
|
|
228
|
+
# Spring impact chain
|
|
229
|
+
"impact-chain",
|
|
228
230
|
}
|
|
229
231
|
)
|
|
230
232
|
|
|
@@ -3766,7 +3768,7 @@ def spring_audit_cmd(
|
|
|
3766
3768
|
from sourcecode.spring_findings import SpringAuditResult, SpringFinding
|
|
3767
3769
|
from sourcecode.spring_tx_analyzer import run_tx_audit
|
|
3768
3770
|
from sourcecode.spring_security_audit import run_security_audit
|
|
3769
|
-
from sourcecode.
|
|
3771
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
3770
3772
|
|
|
3771
3773
|
target = path.resolve()
|
|
3772
3774
|
if not target.exists() or not target.is_dir():
|
|
@@ -3816,13 +3818,13 @@ def spring_audit_cmd(
|
|
|
3816
3818
|
return
|
|
3817
3819
|
|
|
3818
3820
|
cir = build_canonical_ir(file_list, target)
|
|
3819
|
-
|
|
3821
|
+
_model = SpringSemanticModel.build(cir)
|
|
3820
3822
|
|
|
3821
3823
|
results: list[SpringAuditResult] = []
|
|
3822
3824
|
if scope in ("all", "tx"):
|
|
3823
|
-
results.append(run_tx_audit(cir, root=target, min_severity=min_severity))
|
|
3825
|
+
results.append(run_tx_audit(cir, root=target, min_severity=min_severity, model=_model))
|
|
3824
3826
|
if scope in ("all", "security"):
|
|
3825
|
-
results.append(run_security_audit(cir, root=target, min_severity=min_severity,
|
|
3827
|
+
results.append(run_security_audit(cir, root=target, min_severity=min_severity, model=_model))
|
|
3826
3828
|
|
|
3827
3829
|
if len(results) == 1:
|
|
3828
3830
|
combined = results[0]
|
|
@@ -3843,6 +3845,18 @@ def spring_audit_cmd(
|
|
|
3843
3845
|
metadata=merged_meta,
|
|
3844
3846
|
).finalize()
|
|
3845
3847
|
|
|
3848
|
+
# Populate git_head from repo HEAD — non-fatal.
|
|
3849
|
+
try:
|
|
3850
|
+
import subprocess as _sub_sa
|
|
3851
|
+
_sha_r = _sub_sa.run(
|
|
3852
|
+
["git", "-C", str(target), "rev-parse", "--short", "HEAD"],
|
|
3853
|
+
capture_output=True, text=True, timeout=3,
|
|
3854
|
+
)
|
|
3855
|
+
if _sha_r.returncode == 0:
|
|
3856
|
+
combined.git_head = _sha_r.stdout.strip()
|
|
3857
|
+
except Exception:
|
|
3858
|
+
pass
|
|
3859
|
+
|
|
3846
3860
|
data = combined.to_dict()
|
|
3847
3861
|
|
|
3848
3862
|
# Non-fatal RIS side-effect — persist summary only (not full findings).
|
|
@@ -3867,6 +3881,139 @@ def spring_audit_cmd(
|
|
|
3867
3881
|
typer.echo("✓ copied to clipboard", err=True)
|
|
3868
3882
|
|
|
3869
3883
|
|
|
3884
|
+
# ── Spring Impact Chain ───────────────────────────────────────────────────────
|
|
3885
|
+
|
|
3886
|
+
|
|
3887
|
+
@app.command("impact-chain")
|
|
3888
|
+
def impact_chain_cmd(
|
|
3889
|
+
symbol: str = typer.Argument(
|
|
3890
|
+
...,
|
|
3891
|
+
help=(
|
|
3892
|
+
"Symbol to query: FQN, class name, or Class#method. "
|
|
3893
|
+
"Examples: OrderService, com.example.OrderService#placeOrder"
|
|
3894
|
+
),
|
|
3895
|
+
),
|
|
3896
|
+
path: Path = typer.Argument(
|
|
3897
|
+
Path("."),
|
|
3898
|
+
help="Repository root (default: current directory)",
|
|
3899
|
+
),
|
|
3900
|
+
depth: int = typer.Option(
|
|
3901
|
+
4,
|
|
3902
|
+
"--depth",
|
|
3903
|
+
help="Indirect caller BFS depth (1–8, default: 4).",
|
|
3904
|
+
min=1,
|
|
3905
|
+
max=8,
|
|
3906
|
+
),
|
|
3907
|
+
output_path: Optional[Path] = typer.Option(
|
|
3908
|
+
None, "--output", "-o",
|
|
3909
|
+
help="Write output to a file instead of stdout.",
|
|
3910
|
+
),
|
|
3911
|
+
format: str = typer.Option(
|
|
3912
|
+
"json", "--format", "-f",
|
|
3913
|
+
help="Output format: json (default) or yaml.",
|
|
3914
|
+
show_default=True,
|
|
3915
|
+
),
|
|
3916
|
+
copy: bool = typer.Option(
|
|
3917
|
+
False, "--copy", "-c",
|
|
3918
|
+
help="Copy output to clipboard after a successful run.",
|
|
3919
|
+
),
|
|
3920
|
+
) -> None:
|
|
3921
|
+
"""Spring impact-chain: systemic blast radius of a symbol with TX/SEC enrichment.
|
|
3922
|
+
|
|
3923
|
+
\b
|
|
3924
|
+
Given a symbol (class or method), returns:
|
|
3925
|
+
- direct_callers — symbols that directly call the target
|
|
3926
|
+
- indirect_callers — transitive callers (BFS up to --depth hops)
|
|
3927
|
+
- endpoints_affected — HTTP endpoints reachable through the call chain
|
|
3928
|
+
- transaction_boundary — @Transactional semantics on the target (if any)
|
|
3929
|
+
- security_surfaces — per-endpoint security policy + SEC findings
|
|
3930
|
+
- impact_findings — TX/SEC audit findings touching the call chain
|
|
3931
|
+
- risk_level — critical | high | medium | low
|
|
3932
|
+
|
|
3933
|
+
\b
|
|
3934
|
+
Consumes SpringSemanticModel — zero duplicate CIR traversals.
|
|
3935
|
+
JAVA/SPRING ONLY.
|
|
3936
|
+
|
|
3937
|
+
\b
|
|
3938
|
+
Examples:
|
|
3939
|
+
sourcecode impact-chain OrderService .
|
|
3940
|
+
sourcecode impact-chain com.example.OrderService#placeOrder /path/to/repo
|
|
3941
|
+
sourcecode impact-chain PaymentService . --depth 6 --output impact.json
|
|
3942
|
+
"""
|
|
3943
|
+
import json as _json
|
|
3944
|
+
|
|
3945
|
+
from sourcecode.repository_ir import find_java_files
|
|
3946
|
+
from sourcecode.canonical_ir import build_canonical_ir
|
|
3947
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
3948
|
+
from sourcecode.spring_impact import run_impact_chain
|
|
3949
|
+
from sourcecode.spring_findings import SpringAuditResult
|
|
3950
|
+
|
|
3951
|
+
target = path.resolve()
|
|
3952
|
+
if not target.exists() or not target.is_dir():
|
|
3953
|
+
_emit_error_json(
|
|
3954
|
+
INVALID_INPUT_CODE,
|
|
3955
|
+
f"'{target}' is not a valid directory.",
|
|
3956
|
+
path=str(target),
|
|
3957
|
+
hint="Pass an existing repository directory.",
|
|
3958
|
+
expected="A directory path.",
|
|
3959
|
+
)
|
|
3960
|
+
raise typer.Exit(code=1)
|
|
3961
|
+
|
|
3962
|
+
if format not in ("json", "yaml"):
|
|
3963
|
+
_emit_error_json(
|
|
3964
|
+
INVALID_INPUT_CODE,
|
|
3965
|
+
f"Invalid format '{format}'.",
|
|
3966
|
+
hint="format must be: json or yaml.",
|
|
3967
|
+
expected="json | yaml",
|
|
3968
|
+
)
|
|
3969
|
+
raise typer.Exit(code=1)
|
|
3970
|
+
|
|
3971
|
+
file_list = find_java_files(target)
|
|
3972
|
+
if not file_list:
|
|
3973
|
+
data = {
|
|
3974
|
+
"schema_version": "1.0",
|
|
3975
|
+
"symbol": symbol,
|
|
3976
|
+
"resolution": "not_found",
|
|
3977
|
+
"analysis_warnings": ["No Java files found in repository — Spring analysis requires Java source."],
|
|
3978
|
+
"risk_level": "unknown",
|
|
3979
|
+
"confidence": "low",
|
|
3980
|
+
"metadata": {},
|
|
3981
|
+
}
|
|
3982
|
+
output = _serialize_dict(data, format)
|
|
3983
|
+
if output_path is not None:
|
|
3984
|
+
output_path.write_text(output, encoding="utf-8")
|
|
3985
|
+
typer.echo("Impact chain written to " + str(output_path), err=True)
|
|
3986
|
+
else:
|
|
3987
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
3988
|
+
sys.stdout.buffer.write(b"\n")
|
|
3989
|
+
sys.stdout.buffer.flush()
|
|
3990
|
+
return
|
|
3991
|
+
|
|
3992
|
+
cir = build_canonical_ir(file_list, target)
|
|
3993
|
+
_model = SpringSemanticModel.build(cir)
|
|
3994
|
+
result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
|
|
3995
|
+
|
|
3996
|
+
data = result.to_dict()
|
|
3997
|
+
output = _serialize_dict(data, format)
|
|
3998
|
+
|
|
3999
|
+
if output_path is not None:
|
|
4000
|
+
output_path.write_text(output, encoding="utf-8")
|
|
4001
|
+
typer.echo(
|
|
4002
|
+
f"Impact chain written to {output_path} "
|
|
4003
|
+
f"(risk: {result.risk_level}, "
|
|
4004
|
+
f"{len(result.direct_callers)} direct callers, "
|
|
4005
|
+
f"{len(result.endpoints_affected)} endpoints)",
|
|
4006
|
+
err=True,
|
|
4007
|
+
)
|
|
4008
|
+
else:
|
|
4009
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
4010
|
+
sys.stdout.buffer.write(b"\n")
|
|
4011
|
+
sys.stdout.buffer.flush()
|
|
4012
|
+
if copy:
|
|
4013
|
+
if _copy_to_clipboard(output):
|
|
4014
|
+
typer.echo("✓ copied to clipboard", err=True)
|
|
4015
|
+
|
|
4016
|
+
|
|
3870
4017
|
# ── Enterprise Workflow Commands ──────────────────────────────────────────────
|
|
3871
4018
|
#
|
|
3872
4019
|
# These are the five canonical enterprise workflows. Each is a thin wrapper
|
|
@@ -649,6 +649,46 @@ def get_spring_audit(repo_path: str = ".", scope: str = "all") -> dict:
|
|
|
649
649
|
)
|
|
650
650
|
|
|
651
651
|
|
|
652
|
+
@mcp.tool()
|
|
653
|
+
def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) -> dict:
|
|
654
|
+
"""Spring impact-chain: systemic blast radius of a symbol with TX/SEC semantic enrichment. JAVA/SPRING ONLY.
|
|
655
|
+
|
|
656
|
+
Do NOT call this on non-Java repositories — it will return resolution=not_found.
|
|
657
|
+
|
|
658
|
+
Maps to: sourcecode impact-chain <symbol> <repo_path> [--depth <depth>]
|
|
659
|
+
Returns: ImpactChainResult with schema_version, symbol, resolution,
|
|
660
|
+
direct_callers, indirect_callers, endpoints_affected,
|
|
661
|
+
transaction_boundary (propagation/isolation/read_only),
|
|
662
|
+
security_surfaces (per-endpoint policy + finding IDs),
|
|
663
|
+
impact_findings (TX-001..005 + SEC-001..003 findings in call chain),
|
|
664
|
+
analysis_warnings, risk_level, confidence, metadata.
|
|
665
|
+
|
|
666
|
+
symbol: FQN, class name, or Class#method. Examples:
|
|
667
|
+
"OrderService", "com.example.OrderService#placeOrder"
|
|
668
|
+
repo_path: absolute path to the Java repository (default: current working directory).
|
|
669
|
+
depth: BFS depth for indirect caller traversal (1–8, default: 4).
|
|
670
|
+
"""
|
|
671
|
+
_raw = repo_path
|
|
672
|
+
try:
|
|
673
|
+
if not isinstance(repo_path, str):
|
|
674
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
675
|
+
if not isinstance(symbol, str) or not symbol.strip():
|
|
676
|
+
return _err("symbol must be a non-empty string", "INVALID_ARGUMENT")
|
|
677
|
+
if not isinstance(depth, int) or depth < 1 or depth > 8:
|
|
678
|
+
return _err("depth must be an integer between 1 and 8", "INVALID_ARGUMENT")
|
|
679
|
+
repo_path = _normalize_repo_path(repo_path)
|
|
680
|
+
_path_err = _check_repo_path(repo_path)
|
|
681
|
+
if _path_err is not None:
|
|
682
|
+
return _path_err
|
|
683
|
+
args = ["impact-chain", symbol.strip(), repo_path, "--depth", str(depth)]
|
|
684
|
+
return _execute(args)
|
|
685
|
+
except Exception as exc:
|
|
686
|
+
return _err(
|
|
687
|
+
f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
|
|
688
|
+
"INTERNAL_ERROR",
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
652
692
|
@mcp.tool()
|
|
653
693
|
def get_module_context(repo_path: str = ".", module: str = "") -> dict:
|
|
654
694
|
"""Compact analysis of a specific module or subdirectory within a repository.
|
|
@@ -128,3 +128,20 @@ class SpringAuditResult:
|
|
|
128
128
|
"limitations": self.limitations,
|
|
129
129
|
"metadata": self.metadata,
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Shared engine utilities — used by TxPatternEngine and SecurityScanner
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
SEVERITY_ORDER: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def deduplicate_findings(findings: list[SpringFinding]) -> list[SpringFinding]:
|
|
141
|
+
seen: set[str] = set()
|
|
142
|
+
out: list[SpringFinding] = []
|
|
143
|
+
for f in findings:
|
|
144
|
+
if f.id not in seen:
|
|
145
|
+
seen.add(f.id)
|
|
146
|
+
out.append(f)
|
|
147
|
+
return out
|