sourcecode 2.4.0__py3-none-any.whl → 2.5.1__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.
- sourcecode/__init__.py +1 -1
- sourcecode/archetype.py +46 -0
- sourcecode/cli.py +429 -3
- sourcecode/context_graph.py +32 -24
- sourcecode/repository_ir.py +116 -2
- sourcecode/retrieval/__init__.py +192 -0
- sourcecode/retrieval/context.py +241 -0
- sourcecode/retrieval/errors.py +41 -0
- sourcecode/retrieval/executor.py +51 -0
- sourcecode/retrieval/planner.py +297 -0
- sourcecode/retrieval/query.py +484 -0
- sourcecode/retrieval/request.py +94 -0
- sourcecode/retrieval/resolution.py +135 -0
- sourcecode/retrieval/result.py +144 -0
- sourcecode/retrieval/retriever.py +62 -0
- sourcecode/retrieval/runtime.py +259 -0
- sourcecode/retrieval/steps.py +242 -0
- sourcecode/retrieval/steps_endpoint.py +311 -0
- sourcecode/retrieval/steps_graph.py +248 -0
- sourcecode/retrieval/steps_impact.py +293 -0
- sourcecode/retrieval/steps_intf.py +247 -0
- sourcecode/retrieval/steps_struct.py +338 -0
- sourcecode/retrieval/steps_txsec.py +351 -0
- sourcecode/security_config.py +4 -4
- sourcecode/spring_impact.py +52 -10
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.1.dist-info}/METADATA +4 -4
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.1.dist-info}/RECORD +30 -12
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.1.dist-info}/WHEEL +0 -0
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.1.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""GRAPH-analysis pack step handlers (Fase 11).
|
|
2
|
+
|
|
3
|
+
Three intents (EXECUTION_PATHS_THROUGH, TRANSACTIONS_REACHING, SUBSYSTEM_COUPLING)
|
|
4
|
+
expressed purely as compositions of these projection steps. NO new analyzer: every step
|
|
5
|
+
reuses an engine already wired into the shared `KnowledgeContext`.
|
|
6
|
+
|
|
7
|
+
* Execution paths → `spring_impact.run_impact_chain` (the SAME Spring-aware call-chain
|
|
8
|
+
engine the `impact-chain` command uses — multi-hop caller reachability with TX/SEC
|
|
9
|
+
enrichment). Distinct from BLAST_RADIUS, which uses the structural repo_ir engine.
|
|
10
|
+
* Transactions reaching → the caller reachability from run_impact_chain ∩ the
|
|
11
|
+
`SpringSemanticModel.tx_index` boundaries.
|
|
12
|
+
* Subsystem coupling → the module graph summary (hubs / cycles / orphans, from
|
|
13
|
+
`GraphAnalyzer`) + the repo_ir subsystem partition.
|
|
14
|
+
|
|
15
|
+
The two symbol-scoped intents reuse the impact pack's `ResolveTarget` head (cross-pack step
|
|
16
|
+
reuse). No step opens source or parses.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from sourcecode.retrieval.query import (
|
|
23
|
+
CollectGraphEvidence,
|
|
24
|
+
ResolveExecutionPaths,
|
|
25
|
+
ResolveSubsystemCoupling,
|
|
26
|
+
ResolveTransactionsReaching,
|
|
27
|
+
Step,
|
|
28
|
+
)
|
|
29
|
+
from sourcecode.retrieval.result import (
|
|
30
|
+
Explanation,
|
|
31
|
+
KnowledgeEntity,
|
|
32
|
+
Observation,
|
|
33
|
+
Provenance,
|
|
34
|
+
RetrievalEvidence,
|
|
35
|
+
)
|
|
36
|
+
from sourcecode.retrieval.steps import (
|
|
37
|
+
SLOT_CONFIDENCE,
|
|
38
|
+
SLOT_ENTITIES,
|
|
39
|
+
SLOT_EVIDENCE,
|
|
40
|
+
SLOT_EXPLANATION,
|
|
41
|
+
SLOT_OBSERVATIONS,
|
|
42
|
+
SLOT_PROVENANCE,
|
|
43
|
+
)
|
|
44
|
+
from sourcecode.retrieval.steps_impact import SLOT_TARGET
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from sourcecode.retrieval.runtime import StepContext, StepRegistry
|
|
48
|
+
|
|
49
|
+
SLOT_SERVICES = "graph_services"
|
|
50
|
+
_MODULE_PREFIX = "module:"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── execution paths (spring_impact.run_impact_chain) ─────────────────────────────
|
|
54
|
+
def resolve_execution_paths(step: Step, ctx: "StepContext") -> None:
|
|
55
|
+
"""The multi-hop caller reachability through the target symbol, from run_impact_chain.
|
|
56
|
+
Reuses the impact pack's ResolveTarget for the symbol."""
|
|
57
|
+
assert isinstance(step, ResolveExecutionPaths)
|
|
58
|
+
result = _impact_chain(ctx)
|
|
59
|
+
entities = [
|
|
60
|
+
KnowledgeEntity(identity=fqn, kind="execution_path", score=0.0, attributes={"reach": "direct"})
|
|
61
|
+
for fqn in result.direct_callers
|
|
62
|
+
] + [
|
|
63
|
+
KnowledgeEntity(identity=fqn, kind="execution_path", score=0.0, attributes={"reach": "indirect"})
|
|
64
|
+
for fqn in result.indirect_callers
|
|
65
|
+
]
|
|
66
|
+
ctx.put(SLOT_ENTITIES, entities)
|
|
67
|
+
ctx.put(
|
|
68
|
+
SLOT_OBSERVATIONS,
|
|
69
|
+
[
|
|
70
|
+
Observation(name="direct_callers", value=len(result.direct_callers), detail="run_impact_chain"),
|
|
71
|
+
Observation(name="indirect_callers", value=len(result.indirect_callers), detail="run_impact_chain"),
|
|
72
|
+
Observation(name="endpoints_affected", value=len(result.endpoints_affected), detail="run_impact_chain"),
|
|
73
|
+
Observation(name="risk_level", value=result.risk_level, detail="run_impact_chain"),
|
|
74
|
+
Observation(name="risk_score", value=result.risk_score, detail="run_impact_chain"),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
ctx.put(
|
|
78
|
+
SLOT_EXPLANATION,
|
|
79
|
+
Explanation(
|
|
80
|
+
winner=result.symbol,
|
|
81
|
+
score=float(result.risk_score or 0.0),
|
|
82
|
+
confidence=result.confidence,
|
|
83
|
+
top_contributions=(),
|
|
84
|
+
runner_up=None,
|
|
85
|
+
engine_explanation=result.explanation or "", # verbatim engine text
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
ctx.put(SLOT_CONFIDENCE, result.confidence)
|
|
89
|
+
_add_service(ctx, "run_impact_chain")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── transactions reaching (impact chain callers ∩ tx_index) ──────────────────────
|
|
93
|
+
def resolve_transactions_reaching(step: Step, ctx: "StepContext") -> None:
|
|
94
|
+
"""The @Transactional boundaries whose enclosing class can reach the target through the
|
|
95
|
+
call chain. Reuses run_impact_chain (reachability) + the tx boundary index."""
|
|
96
|
+
assert isinstance(step, ResolveTransactionsReaching)
|
|
97
|
+
result = _impact_chain(ctx)
|
|
98
|
+
caller_classes = {_enclosing_class(c) for c in result.direct_callers} | {
|
|
99
|
+
_enclosing_class(c) for c in result.indirect_callers
|
|
100
|
+
}
|
|
101
|
+
boundaries = [
|
|
102
|
+
b for b in ctx.knowledge.spring_model.tx_index.all_declared
|
|
103
|
+
if _enclosing_class(b.symbol) in caller_classes
|
|
104
|
+
]
|
|
105
|
+
ctx.put(
|
|
106
|
+
SLOT_ENTITIES,
|
|
107
|
+
[
|
|
108
|
+
KnowledgeEntity(
|
|
109
|
+
identity=b.symbol,
|
|
110
|
+
kind="transaction_reaching",
|
|
111
|
+
score=0.0,
|
|
112
|
+
attributes={
|
|
113
|
+
"propagation": b.propagation,
|
|
114
|
+
"read_only": b.read_only,
|
|
115
|
+
"confidence": b.confidence,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
for b in boundaries
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
ctx.put(
|
|
122
|
+
SLOT_EVIDENCE,
|
|
123
|
+
[
|
|
124
|
+
RetrievalEvidence(
|
|
125
|
+
signal="transactional",
|
|
126
|
+
detail=f"@Transactional({b.propagation}) reaches {result.symbol}",
|
|
127
|
+
weight=1.0,
|
|
128
|
+
strength=1.0,
|
|
129
|
+
mass=1.0,
|
|
130
|
+
contribution=1.0,
|
|
131
|
+
provider="SpringSemanticModel + run_impact_chain",
|
|
132
|
+
source_file=b.source_file or None,
|
|
133
|
+
)
|
|
134
|
+
for b in boundaries
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
ctx.put(
|
|
138
|
+
SLOT_OBSERVATIONS,
|
|
139
|
+
[
|
|
140
|
+
Observation(name="reaching_transactions", value=len(boundaries), detail="tx ∩ callers"),
|
|
141
|
+
Observation(name="caller_classes", value=len(caller_classes), detail="run_impact_chain"),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
ctx.put(SLOT_CONFIDENCE, "high" if boundaries else "low")
|
|
145
|
+
_add_service(ctx, "run_impact_chain")
|
|
146
|
+
_add_service(ctx, "SpringSemanticModel")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── subsystem coupling (module graph summary + subsystems) ───────────────────────
|
|
150
|
+
def resolve_subsystem_coupling(step: Step, ctx: "StepContext") -> None:
|
|
151
|
+
"""Coupling structure of the repository: the module graph's hub / cycle / orphan
|
|
152
|
+
summary + the subsystem partition. Reuses the built module graph and repo_ir."""
|
|
153
|
+
assert isinstance(step, ResolveSubsystemCoupling)
|
|
154
|
+
mg = ctx.knowledge.module_graph
|
|
155
|
+
if mg is None:
|
|
156
|
+
summary = None
|
|
157
|
+
hubs: list = []
|
|
158
|
+
else:
|
|
159
|
+
summary = mg.summary
|
|
160
|
+
hubs = list(getattr(summary, "hubs", []) or [])
|
|
161
|
+
entities = [
|
|
162
|
+
KnowledgeEntity(
|
|
163
|
+
identity=_node_path(h), kind="coupling_hub", score=0.0, attributes={"role": "hub"}
|
|
164
|
+
)
|
|
165
|
+
for h in hubs
|
|
166
|
+
]
|
|
167
|
+
ctx.put(SLOT_ENTITIES, entities)
|
|
168
|
+
subsystems = ctx.knowledge.repo_ir.get("subsystems", []) or []
|
|
169
|
+
obs = [Observation(name="subsystems", value=len(subsystems), detail="repo_ir partition")]
|
|
170
|
+
if summary is not None:
|
|
171
|
+
obs += [
|
|
172
|
+
Observation(name="hubs", value=len(hubs), detail="module graph hubs"),
|
|
173
|
+
Observation(name="import_cycles", value=getattr(summary, "cycle_count", 0), detail="module graph"),
|
|
174
|
+
Observation(name="orphans", value=len(getattr(summary, "orphans", []) or []), detail="module graph"),
|
|
175
|
+
Observation(name="nodes", value=getattr(summary, "node_count", 0), detail="module graph"),
|
|
176
|
+
Observation(name="edges", value=getattr(summary, "edge_count", 0), detail="module graph"),
|
|
177
|
+
]
|
|
178
|
+
ctx.put(SLOT_OBSERVATIONS, obs)
|
|
179
|
+
ctx.put(SLOT_CONFIDENCE, "high" if summary is not None else "low")
|
|
180
|
+
_add_service(ctx, "GraphAnalyzer(module_graph)")
|
|
181
|
+
_add_service(ctx, "build_repo_ir(subsystems)")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── terminal evidence (generic across the pack) ──────────────────────────────────
|
|
185
|
+
def collect_graph_evidence(step: Step, ctx: "StepContext") -> None:
|
|
186
|
+
"""Assemble confidence, provenance, and (unless a resolve step already set a richer
|
|
187
|
+
one) a default explanation. GENERIC across the graph pack."""
|
|
188
|
+
assert isinstance(step, CollectGraphEvidence)
|
|
189
|
+
confidence = ctx.get(SLOT_CONFIDENCE, "medium")
|
|
190
|
+
if not ctx.has(SLOT_EXPLANATION):
|
|
191
|
+
ctx.put(
|
|
192
|
+
SLOT_EXPLANATION,
|
|
193
|
+
Explanation(
|
|
194
|
+
winner=None,
|
|
195
|
+
score=0.0,
|
|
196
|
+
confidence=confidence,
|
|
197
|
+
top_contributions=(),
|
|
198
|
+
runner_up=None,
|
|
199
|
+
engine_explanation="",
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
ctx.put(
|
|
203
|
+
SLOT_PROVENANCE,
|
|
204
|
+
Provenance(
|
|
205
|
+
services=tuple(ctx.get(SLOT_SERVICES, [])),
|
|
206
|
+
signals_used=("call_chain", "tx_index", "module_graph", "subsystems"),
|
|
207
|
+
signals_missing=(),
|
|
208
|
+
knowledge_source="spring_impact + SpringSemanticModel + module graph",
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ── helpers ──────────────────────────────────────────────────────────────────────
|
|
214
|
+
def _impact_chain(ctx: "StepContext"):
|
|
215
|
+
from sourcecode.spring_impact import run_impact_chain
|
|
216
|
+
|
|
217
|
+
target = ctx.require(SLOT_TARGET)
|
|
218
|
+
return run_impact_chain(
|
|
219
|
+
ctx.knowledge.context_graph.cir,
|
|
220
|
+
target,
|
|
221
|
+
root=ctx.knowledge.root,
|
|
222
|
+
model=ctx.knowledge.spring_model,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _enclosing_class(symbol: str) -> str:
|
|
227
|
+
return symbol.split("#", 1)[0]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _node_path(node_id) -> str:
|
|
231
|
+
if not node_id:
|
|
232
|
+
return ""
|
|
233
|
+
return node_id[len(_MODULE_PREFIX):] if node_id.startswith(_MODULE_PREFIX) else node_id
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _add_service(ctx: "StepContext", name: str) -> None:
|
|
237
|
+
services = list(ctx.get(SLOT_SERVICES, []))
|
|
238
|
+
if name not in services:
|
|
239
|
+
services.append(name)
|
|
240
|
+
ctx.put(SLOT_SERVICES, services)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def register_graph_steps(registry: "StepRegistry") -> None:
|
|
244
|
+
"""Register the GRAPH-analysis handlers (the sanctioned extension seam)."""
|
|
245
|
+
registry.register(ResolveExecutionPaths.kind, resolve_execution_paths)
|
|
246
|
+
registry.register(ResolveTransactionsReaching.kind, resolve_transactions_reaching)
|
|
247
|
+
registry.register(ResolveSubsystemCoupling.kind, resolve_subsystem_coupling)
|
|
248
|
+
registry.register(CollectGraphEvidence.kind, collect_graph_evidence)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""IMPACT-pack step handlers (Fase 5).
|
|
2
|
+
|
|
3
|
+
Five intents (BLAST_RADIUS, AFFECTED_ENDPOINTS, AFFECTED_INTEGRATIONS,
|
|
4
|
+
TRANSACTION_BOUNDARIES, CROSS_MODULE_IMPACT) expressed purely as compositions of these
|
|
5
|
+
reusable steps. Maximal reuse: every intent shares `ResolveTarget → ComputeBlastRadius →
|
|
6
|
+
… → CollectImpactEvidence → BuildResult`; only the single projection step differs.
|
|
7
|
+
|
|
8
|
+
All logic REUSES existing engines and recomputes nothing new:
|
|
9
|
+
* `ComputeBlastRadius` calls `repository_ir.compute_blast_radius` over the acquired repo IR
|
|
10
|
+
— the SAME engine the legacy `impact` command uses, so results are identical.
|
|
11
|
+
* Four of the five projections read that ONE blast dict (callers / endpoints / tx
|
|
12
|
+
boundaries / cross-module are all already computed by the engine).
|
|
13
|
+
* `ResolveIntegrations` additionally reuses `SemanticIntegrationEngine`, scoped to the
|
|
14
|
+
blast cone.
|
|
15
|
+
|
|
16
|
+
No step opens source or parses: the repo IR / ContextGraph are acquired once by the shared
|
|
17
|
+
`KnowledgeContext` (the acquisition boundary), and steps only read them.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from sourcecode.retrieval.errors import StepContractError
|
|
24
|
+
from sourcecode.retrieval.query import (
|
|
25
|
+
CollectImpactEvidence,
|
|
26
|
+
ComputeBlastRadius,
|
|
27
|
+
RankByRisk,
|
|
28
|
+
ResolveDependents,
|
|
29
|
+
ResolveEndpoints,
|
|
30
|
+
ResolveIntegrations,
|
|
31
|
+
ResolveModules,
|
|
32
|
+
ResolveTarget,
|
|
33
|
+
ResolveTransactions,
|
|
34
|
+
Step,
|
|
35
|
+
)
|
|
36
|
+
from sourcecode.retrieval.result import (
|
|
37
|
+
Explanation,
|
|
38
|
+
KnowledgeEntity,
|
|
39
|
+
Observation,
|
|
40
|
+
Provenance,
|
|
41
|
+
RetrievalEvidence,
|
|
42
|
+
)
|
|
43
|
+
from sourcecode.retrieval.steps import (
|
|
44
|
+
SLOT_CONFIDENCE,
|
|
45
|
+
SLOT_ENTITIES,
|
|
46
|
+
SLOT_EVIDENCE,
|
|
47
|
+
SLOT_EXPLANATION,
|
|
48
|
+
SLOT_OBSERVATIONS,
|
|
49
|
+
SLOT_PROVENANCE,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from sourcecode.retrieval.runtime import StepContext, StepRegistry
|
|
54
|
+
|
|
55
|
+
# impact-specific slots
|
|
56
|
+
SLOT_TARGET = "impact_target"
|
|
57
|
+
SLOT_BLAST = "blast_radius"
|
|
58
|
+
SLOT_SERVICES = "impact_services"
|
|
59
|
+
|
|
60
|
+
_TARGET_ANCHOR_KINDS = ("symbol", "file")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── shared knowledge-producing steps ─────────────────────────────────────────────
|
|
64
|
+
def resolve_target(step: Step, ctx: "StepContext") -> None:
|
|
65
|
+
"""Pick the impact target from the request's symbol/file anchor. The token is passed
|
|
66
|
+
to the blast engine, which does its own (legacy-identical) resolution."""
|
|
67
|
+
assert isinstance(step, ResolveTarget)
|
|
68
|
+
anchor = next((a for a in ctx.anchors if a.anchor.kind in _TARGET_ANCHOR_KINDS), None)
|
|
69
|
+
if anchor is None:
|
|
70
|
+
raise StepContractError("impact intents require a 'symbol' or 'file' anchor")
|
|
71
|
+
target = anchor.identities[0] if anchor.identities else anchor.anchor.raw
|
|
72
|
+
ctx.put(SLOT_TARGET, target)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def compute_blast_radius(step: Step, ctx: "StepContext") -> None:
|
|
76
|
+
"""Run `repository_ir.compute_blast_radius` once over the acquired repo IR. Every
|
|
77
|
+
projection step reads the resulting blast dict."""
|
|
78
|
+
assert isinstance(step, ComputeBlastRadius)
|
|
79
|
+
from sourcecode.repository_ir import compute_blast_radius as _compute_blast_radius
|
|
80
|
+
|
|
81
|
+
target = ctx.require(SLOT_TARGET)
|
|
82
|
+
blast = _compute_blast_radius(ctx.knowledge.repo_ir, target, max_depth=step.max_depth)
|
|
83
|
+
ctx.put(SLOT_BLAST, blast)
|
|
84
|
+
_add_service(ctx, "compute_blast_radius")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── projection steps (each reads the ONE blast dict) ─────────────────────────────
|
|
88
|
+
def resolve_dependents(step: Step, ctx: "StepContext") -> None:
|
|
89
|
+
blast = ctx.require(SLOT_BLAST)
|
|
90
|
+
entities = [
|
|
91
|
+
KnowledgeEntity(identity=fqn, kind="dependent", score=0.0, attributes={"reach": "direct"})
|
|
92
|
+
for fqn in blast.get("direct_callers", [])
|
|
93
|
+
] + [
|
|
94
|
+
KnowledgeEntity(identity=fqn, kind="dependent", score=0.0, attributes={"reach": "indirect"})
|
|
95
|
+
for fqn in blast.get("indirect_callers", [])
|
|
96
|
+
]
|
|
97
|
+
ctx.put(SLOT_ENTITIES, entities)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def resolve_endpoints(step: Step, ctx: "StepContext") -> None:
|
|
101
|
+
blast = ctx.require(SLOT_BLAST)
|
|
102
|
+
ctx.put(
|
|
103
|
+
SLOT_ENTITIES,
|
|
104
|
+
[
|
|
105
|
+
KnowledgeEntity(
|
|
106
|
+
identity=ep.get("handler") or f"{ep.get('method', '')} {ep.get('path', '')}".strip(),
|
|
107
|
+
kind="endpoint",
|
|
108
|
+
score=0.0,
|
|
109
|
+
attributes={"method": ep.get("method", ""), "path": ep.get("path", "")},
|
|
110
|
+
)
|
|
111
|
+
for ep in blast.get("endpoints_affected", [])
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def resolve_transactions(step: Step, ctx: "StepContext") -> None:
|
|
117
|
+
blast = ctx.require(SLOT_BLAST)
|
|
118
|
+
ctx.put(
|
|
119
|
+
SLOT_ENTITIES,
|
|
120
|
+
[
|
|
121
|
+
KnowledgeEntity(identity=fqn, kind="transaction_boundary", score=0.0)
|
|
122
|
+
for fqn in blast.get("transactional_boundaries_touched", [])
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve_modules(step: Step, ctx: "StepContext") -> None:
|
|
128
|
+
blast = ctx.require(SLOT_BLAST)
|
|
129
|
+
ctx.put(
|
|
130
|
+
SLOT_ENTITIES,
|
|
131
|
+
[
|
|
132
|
+
KnowledgeEntity(
|
|
133
|
+
identity=m.get("module", ""),
|
|
134
|
+
kind="module",
|
|
135
|
+
score=0.0,
|
|
136
|
+
attributes={k: v for k, v in m.items() if k != "module"},
|
|
137
|
+
)
|
|
138
|
+
for m in blast.get("cross_module_impact", [])
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def resolve_integrations(step: Step, ctx: "StepContext") -> None:
|
|
144
|
+
"""Reuse `SemanticIntegrationEngine`; keep only integrations declared by a type inside
|
|
145
|
+
the blast cone (matched target ∪ direct ∪ indirect callers)."""
|
|
146
|
+
assert isinstance(step, ResolveIntegrations)
|
|
147
|
+
from sourcecode.semantic_integration_engine import SemanticIntegrationEngine
|
|
148
|
+
|
|
149
|
+
blast = ctx.require(SLOT_BLAST)
|
|
150
|
+
cone = (
|
|
151
|
+
set(blast.get("matched_fqns", []))
|
|
152
|
+
| set(blast.get("direct_callers", []))
|
|
153
|
+
| set(blast.get("indirect_callers", []))
|
|
154
|
+
)
|
|
155
|
+
integrations = SemanticIntegrationEngine(ctx.knowledge.context_graph).detect()
|
|
156
|
+
_add_service(ctx, "SemanticIntegrationEngine")
|
|
157
|
+
ctx.put(
|
|
158
|
+
SLOT_ENTITIES,
|
|
159
|
+
[
|
|
160
|
+
KnowledgeEntity(
|
|
161
|
+
identity=f"{i.kind}:{i.target or i.client}",
|
|
162
|
+
kind="integration",
|
|
163
|
+
score=0.0,
|
|
164
|
+
attributes={"integration_kind": i.kind, "client": i.client, "target": i.target},
|
|
165
|
+
)
|
|
166
|
+
for i in integrations
|
|
167
|
+
if i.client in cone
|
|
168
|
+
],
|
|
169
|
+
)
|
|
170
|
+
# integration evidence carries the declaring site (relpath:line) via the engine
|
|
171
|
+
ctx.put(
|
|
172
|
+
SLOT_EVIDENCE,
|
|
173
|
+
[
|
|
174
|
+
_integration_evidence(i)
|
|
175
|
+
for i in integrations
|
|
176
|
+
if i.client in cone
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── ranking + terminal-evidence steps ────────────────────────────────────────────
|
|
182
|
+
def rank_by_risk(step: Step, ctx: "StepContext") -> None:
|
|
183
|
+
"""Order impacted entities using the engine's OWN direct/indirect classification — no
|
|
184
|
+
new score is computed. Deterministic: direct before indirect, then identity."""
|
|
185
|
+
assert isinstance(step, RankByRisk)
|
|
186
|
+
entities = ctx.get(SLOT_ENTITIES, [])
|
|
187
|
+
order = {"direct": 0, "indirect": 1}
|
|
188
|
+
ctx.put(
|
|
189
|
+
SLOT_ENTITIES,
|
|
190
|
+
sorted(entities, key=lambda e: (order.get(e.attributes.get("reach"), 2), e.identity)),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def collect_impact_evidence(step: Step, ctx: "StepContext") -> None:
|
|
195
|
+
"""GENERIC across all impact intents: set confidence, explanation (engine text
|
|
196
|
+
passthrough — no synthesized prose), provenance, and risk observations from the blast
|
|
197
|
+
dict. Reuses the engine's own risk/confidence; computes nothing new."""
|
|
198
|
+
assert isinstance(step, CollectImpactEvidence)
|
|
199
|
+
blast = ctx.require(SLOT_BLAST)
|
|
200
|
+
confidence = blast.get("confidence_level", "low")
|
|
201
|
+
ctx.put(SLOT_CONFIDENCE, confidence)
|
|
202
|
+
# B3: evidence parity — projections other than ResolveIntegrations produce entities but
|
|
203
|
+
# no evidence rows (the blast engine yields FQNs, not source spans). Emit one grounded-
|
|
204
|
+
# as-far-as-possible evidence row per entity so `evidence` mirrors `entities` and every
|
|
205
|
+
# hit is auditable. No span is fabricated: source_file stays None where the engine gives
|
|
206
|
+
# none (the same honest convention the archetype signals use).
|
|
207
|
+
if not ctx.has(SLOT_EVIDENCE):
|
|
208
|
+
ctx.put(
|
|
209
|
+
SLOT_EVIDENCE,
|
|
210
|
+
[
|
|
211
|
+
RetrievalEvidence(
|
|
212
|
+
signal=e.kind,
|
|
213
|
+
detail=e.identity,
|
|
214
|
+
weight=1.0,
|
|
215
|
+
strength=1.0,
|
|
216
|
+
mass=1.0,
|
|
217
|
+
contribution=1.0,
|
|
218
|
+
provider="compute_blast_radius",
|
|
219
|
+
source_file=None,
|
|
220
|
+
source_line=None,
|
|
221
|
+
)
|
|
222
|
+
for e in ctx.get(SLOT_ENTITIES, [])
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
ctx.put(
|
|
226
|
+
SLOT_OBSERVATIONS,
|
|
227
|
+
[
|
|
228
|
+
Observation(name="risk_score", value=blast.get("risk_score", 0.0), detail="compute_blast_radius"),
|
|
229
|
+
Observation(name="risk_level", value=blast.get("risk_level", "unknown"), detail="compute_blast_radius"),
|
|
230
|
+
Observation(name="confidence_score", value=blast.get("confidence_score", 0.0), detail="compute_blast_radius"),
|
|
231
|
+
Observation(name="resolution", value=blast.get("resolution", ""), detail="target resolution"),
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
ctx.put(
|
|
235
|
+
SLOT_EXPLANATION,
|
|
236
|
+
Explanation(
|
|
237
|
+
winner=blast.get("target"),
|
|
238
|
+
score=float(blast.get("risk_score", 0.0)),
|
|
239
|
+
confidence=confidence,
|
|
240
|
+
top_contributions=(),
|
|
241
|
+
runner_up=None,
|
|
242
|
+
engine_explanation=blast.get("explanation", ""), # verbatim engine text
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
ctx.put(
|
|
246
|
+
SLOT_PROVENANCE,
|
|
247
|
+
Provenance(
|
|
248
|
+
services=tuple(ctx.get(SLOT_SERVICES, [])),
|
|
249
|
+
signals_used=("reverse_graph", "route_surface", "subsystems"),
|
|
250
|
+
signals_missing=(),
|
|
251
|
+
knowledge_source="repo IR (build_repo_ir)",
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ── helpers ──────────────────────────────────────────────────────────────────────
|
|
257
|
+
def _add_service(ctx: "StepContext", name: str) -> None:
|
|
258
|
+
services = list(ctx.get(SLOT_SERVICES, []))
|
|
259
|
+
if name not in services:
|
|
260
|
+
services.append(name)
|
|
261
|
+
ctx.put(SLOT_SERVICES, services)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _integration_evidence(i) -> RetrievalEvidence:
|
|
265
|
+
file = line = None
|
|
266
|
+
if i.evidence and ":" in i.evidence:
|
|
267
|
+
f, _, ln = i.evidence.rpartition(":")
|
|
268
|
+
file = f or None
|
|
269
|
+
line = int(ln) if ln.isdigit() else None
|
|
270
|
+
return RetrievalEvidence(
|
|
271
|
+
signal=f"integration_{i.kind}",
|
|
272
|
+
detail=f"{i.client} → {i.target or i.kind} ({i.basis})",
|
|
273
|
+
weight=1.0,
|
|
274
|
+
strength=1.0,
|
|
275
|
+
mass=1.0,
|
|
276
|
+
contribution=1.0,
|
|
277
|
+
provider="SemanticIntegrationEngine",
|
|
278
|
+
source_file=file,
|
|
279
|
+
source_line=line,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def register_impact_steps(registry: "StepRegistry") -> None:
|
|
284
|
+
"""Register the IMPACT-pack handlers into a registry (the sanctioned extension seam)."""
|
|
285
|
+
registry.register(ResolveTarget.kind, resolve_target)
|
|
286
|
+
registry.register(ComputeBlastRadius.kind, compute_blast_radius)
|
|
287
|
+
registry.register(ResolveDependents.kind, resolve_dependents)
|
|
288
|
+
registry.register(ResolveEndpoints.kind, resolve_endpoints)
|
|
289
|
+
registry.register(ResolveIntegrations.kind, resolve_integrations)
|
|
290
|
+
registry.register(ResolveTransactions.kind, resolve_transactions)
|
|
291
|
+
registry.register(ResolveModules.kind, resolve_modules)
|
|
292
|
+
registry.register(RankByRisk.kind, rank_by_risk)
|
|
293
|
+
registry.register(CollectImpactEvidence.kind, collect_impact_evidence)
|