sourcecode 2.4.0__py3-none-any.whl → 2.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.
- 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 +1 -1
- 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-2.4.0.dist-info → sourcecode-2.5.0.dist-info}/METADATA +4 -4
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.0.dist-info}/RECORD +29 -11
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.0.dist-info}/WHEEL +0 -0
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.4.0.dist-info → sourcecode-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Step handlers — the reusable, single-responsibility execution units (Fase 4 §3).
|
|
2
|
+
|
|
3
|
+
Each handler is a pure procedure over `(step, StepContext)`: it reads knowledge and/or
|
|
4
|
+
prior slots, and writes its output into the shared blackboard (or the terminal result).
|
|
5
|
+
Handlers are the ONLY place execution logic lives; steps themselves are inert data and
|
|
6
|
+
intents are declarative plans. A handler:
|
|
7
|
+
|
|
8
|
+
* reuses ONLY existing engines/providers — never a new engine, never a re-parse;
|
|
9
|
+
* never touches the parser or source directly;
|
|
10
|
+
* passes results to later steps through named blackboard slots.
|
|
11
|
+
|
|
12
|
+
Fase 4 implements exactly the four handlers the two architecture intents need
|
|
13
|
+
(`ARCHITECTURAL_CORE`, `ARCHITECTURAL_STYLE`). Both intents reuse the SAME handlers — they
|
|
14
|
+
differ only in the `Characterize` dimension parameter, which is the proof that a new intent
|
|
15
|
+
is a new *plan*, not new *runtime*. Steps for other catalogue intents (Resolve, BlastRadius,
|
|
16
|
+
Transactions, Endpoints, Integrations, …) are deliberately NOT built here.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from sourcecode.retrieval.errors import UnknownDimensionError
|
|
23
|
+
from sourcecode.retrieval.query import (
|
|
24
|
+
BuildResult,
|
|
25
|
+
Characterize,
|
|
26
|
+
CollectEvidence,
|
|
27
|
+
RankByScore,
|
|
28
|
+
Step,
|
|
29
|
+
)
|
|
30
|
+
from sourcecode.retrieval.result import (
|
|
31
|
+
Explanation,
|
|
32
|
+
KnowledgeEntity,
|
|
33
|
+
KnowledgeResult,
|
|
34
|
+
Observation,
|
|
35
|
+
Provenance,
|
|
36
|
+
RetrievalEvidence,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from sourcecode.retrieval.runtime import StepContext, StepRegistry
|
|
41
|
+
|
|
42
|
+
# ── blackboard slot keys ─────────────────────────────────────────────────────────
|
|
43
|
+
# Generic result slots — read by the GENERIC BuildResult; written by any pack.
|
|
44
|
+
SLOT_ENTITIES = "entities"
|
|
45
|
+
SLOT_EVIDENCE = "evidence"
|
|
46
|
+
SLOT_OBSERVATIONS = "observations"
|
|
47
|
+
SLOT_CONFIDENCE = "confidence"
|
|
48
|
+
SLOT_EXPLANATION = "explanation"
|
|
49
|
+
SLOT_PROVENANCE = "provenance"
|
|
50
|
+
# Archetype-pack slots.
|
|
51
|
+
SLOT_DIMENSION = "dimension_result"
|
|
52
|
+
SLOT_GRAPH_METRICS = "graph_metrics"
|
|
53
|
+
SLOT_SIGNALS_USED = "signals_used"
|
|
54
|
+
SLOT_SIGNALS_MISSING = "signals_missing"
|
|
55
|
+
SLOT_KNOWLEDGE_SOURCE = "knowledge_source"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── handlers ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
def characterize(step: Step, ctx: "StepContext") -> None:
|
|
60
|
+
"""Reuse `ArchetypeClassifier`; select the requested archetype dimension. Writes the
|
|
61
|
+
dimension result, graph metrics, and signal provenance to the blackboard."""
|
|
62
|
+
assert isinstance(step, Characterize)
|
|
63
|
+
from sourcecode.archetype import ArchetypeClassifier
|
|
64
|
+
|
|
65
|
+
analysis = ArchetypeClassifier().analyze(
|
|
66
|
+
ctx.knowledge.source_map,
|
|
67
|
+
root=ctx.knowledge.root,
|
|
68
|
+
module_graph=ctx.knowledge.module_graph,
|
|
69
|
+
)
|
|
70
|
+
dim = analysis.dimensions.get(step.dimension)
|
|
71
|
+
if dim is None:
|
|
72
|
+
raise UnknownDimensionError(f"archetype has no dimension {step.dimension!r}")
|
|
73
|
+
ctx.put(SLOT_DIMENSION, dim)
|
|
74
|
+
ctx.put(SLOT_GRAPH_METRICS, dict(analysis.graph_metrics))
|
|
75
|
+
ctx.put(SLOT_SIGNALS_USED, tuple(analysis.signals_used))
|
|
76
|
+
ctx.put(SLOT_SIGNALS_MISSING, tuple(analysis.signals_missing))
|
|
77
|
+
ctx.put(SLOT_KNOWLEDGE_SOURCE, analysis.generated_from)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def rank_by_score(step: Step, ctx: "StepContext") -> None:
|
|
81
|
+
"""Order the dimension's candidates by their EXISTING engine scores (no new score).
|
|
82
|
+
Emits ranked `KnowledgeEntity`s."""
|
|
83
|
+
dim = ctx.require(SLOT_DIMENSION)
|
|
84
|
+
ranked = sorted(dim.candidates, key=lambda c: (-c.score, c.label))
|
|
85
|
+
ctx.put(
|
|
86
|
+
SLOT_ENTITIES,
|
|
87
|
+
[
|
|
88
|
+
KnowledgeEntity(
|
|
89
|
+
identity=c.label,
|
|
90
|
+
kind=f"{dim.dimension}_candidate",
|
|
91
|
+
score=c.score,
|
|
92
|
+
attributes={"is_primary": c.label == dim.primary},
|
|
93
|
+
)
|
|
94
|
+
for c in ranked
|
|
95
|
+
if c.score > 0
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def collect_evidence(step: Step, ctx: "StepContext") -> None:
|
|
101
|
+
"""Assemble the winning candidate's evidence + graph-topology observations AND the
|
|
102
|
+
generic confidence/explanation/provenance slots the shared BuildResult consumes.
|
|
103
|
+
Reuses values already computed; recomputes nothing."""
|
|
104
|
+
dim = ctx.require(SLOT_DIMENSION)
|
|
105
|
+
graph_metrics = ctx.require(SLOT_GRAPH_METRICS)
|
|
106
|
+
winner = _winner(dim)
|
|
107
|
+
runner = _runner_up(dim)
|
|
108
|
+
evidence = [
|
|
109
|
+
RetrievalEvidence(
|
|
110
|
+
signal=e.signal,
|
|
111
|
+
detail=e.detail,
|
|
112
|
+
weight=e.weight,
|
|
113
|
+
strength=e.strength,
|
|
114
|
+
mass=e.mass,
|
|
115
|
+
contribution=e.contribution,
|
|
116
|
+
provider="ArchetypeClassifier",
|
|
117
|
+
)
|
|
118
|
+
for e in sorted(
|
|
119
|
+
(winner.evidence if winner is not None else []),
|
|
120
|
+
key=lambda e: e.contribution,
|
|
121
|
+
reverse=True,
|
|
122
|
+
)
|
|
123
|
+
]
|
|
124
|
+
ctx.put(SLOT_EVIDENCE, evidence)
|
|
125
|
+
ctx.put(
|
|
126
|
+
SLOT_OBSERVATIONS,
|
|
127
|
+
[
|
|
128
|
+
Observation(
|
|
129
|
+
name=k,
|
|
130
|
+
value=round(v, 4),
|
|
131
|
+
detail="module-graph topology metric (GraphEvidenceProvider)",
|
|
132
|
+
)
|
|
133
|
+
for k, v in sorted(graph_metrics.items())
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
ctx.put(SLOT_CONFIDENCE, dim.confidence)
|
|
137
|
+
ctx.put(
|
|
138
|
+
SLOT_EXPLANATION,
|
|
139
|
+
Explanation(
|
|
140
|
+
winner=dim.primary,
|
|
141
|
+
score=(winner.score if winner is not None else 0.0),
|
|
142
|
+
confidence=dim.confidence,
|
|
143
|
+
top_contributions=tuple((e.signal, e.contribution) for e in evidence[:3]),
|
|
144
|
+
runner_up=(runner.label if runner is not None else None),
|
|
145
|
+
engine_explanation=dim.explanation,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
ctx.put(
|
|
149
|
+
SLOT_PROVENANCE,
|
|
150
|
+
Provenance(
|
|
151
|
+
services=("ArchetypeClassifier", "GraphEvidenceProvider"),
|
|
152
|
+
signals_used=ctx.get(SLOT_SIGNALS_USED, ()),
|
|
153
|
+
signals_missing=ctx.get(SLOT_SIGNALS_MISSING, ()),
|
|
154
|
+
knowledge_source=ctx.get(SLOT_KNOWLEDGE_SOURCE, ""),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def build_result(step: Step, ctx: "StepContext") -> None:
|
|
160
|
+
"""GENERIC terminal step: assemble the typed `KnowledgeResult` from the generic result
|
|
161
|
+
slots any pack populated. Knows nothing about a specific intent — this is why one
|
|
162
|
+
BuildResult serves every pack (design §4.5).
|
|
163
|
+
|
|
164
|
+
Applies the request's `limit` option (B4): when set (>0) and it bites, the entity tuple
|
|
165
|
+
is capped and an `entities_truncated` observation records the total. `limit == 0`
|
|
166
|
+
(the default) is unbounded, so engine-faithful results are never silently dropped."""
|
|
167
|
+
default_expl = Explanation(
|
|
168
|
+
winner=None,
|
|
169
|
+
score=0.0,
|
|
170
|
+
confidence=ctx.get(SLOT_CONFIDENCE, "low"),
|
|
171
|
+
top_contributions=(),
|
|
172
|
+
runner_up=None,
|
|
173
|
+
engine_explanation="",
|
|
174
|
+
)
|
|
175
|
+
default_prov = Provenance(services=(), signals_used=(), signals_missing=(), knowledge_source="")
|
|
176
|
+
entities = tuple(ctx.get(SLOT_ENTITIES, []))
|
|
177
|
+
observations = list(ctx.get(SLOT_OBSERVATIONS, []))
|
|
178
|
+
limit = getattr(ctx.options, "limit", 0)
|
|
179
|
+
if limit and len(entities) > limit:
|
|
180
|
+
observations.append(
|
|
181
|
+
Observation(
|
|
182
|
+
name="entities_truncated",
|
|
183
|
+
value=len(entities),
|
|
184
|
+
detail=f"limit={limit}; returned first {limit} of {len(entities)}",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
entities = entities[:limit]
|
|
188
|
+
ctx.set_result(
|
|
189
|
+
KnowledgeResult(
|
|
190
|
+
intent=ctx.intent.value,
|
|
191
|
+
entities=entities,
|
|
192
|
+
evidence=tuple(ctx.get(SLOT_EVIDENCE, [])),
|
|
193
|
+
confidence=ctx.get(SLOT_CONFIDENCE, "low"),
|
|
194
|
+
observations=tuple(observations),
|
|
195
|
+
explanation=ctx.get(SLOT_EXPLANATION, default_expl),
|
|
196
|
+
provenance=ctx.get(SLOT_PROVENANCE, default_prov),
|
|
197
|
+
query=tuple(t.step for t in ctx.trace) + (type(step).__name__,),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ── registry wiring ──────────────────────────────────────────────────────────────
|
|
203
|
+
def default_registry() -> "StepRegistry":
|
|
204
|
+
"""The default step registry: the arch handlers + the IMPACT-pack handlers. The
|
|
205
|
+
registry is the sanctioned extension seam — adding a pack registers its handlers here;
|
|
206
|
+
the Runtime engine (`runtime.py`) is unchanged. Adding an intent that composes already
|
|
207
|
+
registered steps needs no change even here."""
|
|
208
|
+
from sourcecode.retrieval.runtime import StepRegistry
|
|
209
|
+
from sourcecode.retrieval.steps_endpoint import register_endpoint_steps
|
|
210
|
+
from sourcecode.retrieval.steps_graph import register_graph_steps
|
|
211
|
+
from sourcecode.retrieval.steps_impact import register_impact_steps
|
|
212
|
+
from sourcecode.retrieval.steps_intf import register_intf_steps
|
|
213
|
+
from sourcecode.retrieval.steps_struct import register_struct_steps
|
|
214
|
+
from sourcecode.retrieval.steps_txsec import register_txsec_steps
|
|
215
|
+
|
|
216
|
+
registry = StepRegistry()
|
|
217
|
+
registry.register(Characterize.kind, characterize)
|
|
218
|
+
registry.register(RankByScore.kind, rank_by_score)
|
|
219
|
+
registry.register(CollectEvidence.kind, collect_evidence)
|
|
220
|
+
registry.register(BuildResult.kind, build_result)
|
|
221
|
+
register_impact_steps(registry)
|
|
222
|
+
register_txsec_steps(registry)
|
|
223
|
+
register_intf_steps(registry)
|
|
224
|
+
register_struct_steps(registry)
|
|
225
|
+
register_endpoint_steps(registry)
|
|
226
|
+
register_graph_steps(registry)
|
|
227
|
+
return registry
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ── helpers over the reused DimensionResult ─────────────────────────────────────
|
|
231
|
+
def _winner(dim) -> object:
|
|
232
|
+
if dim.primary is None:
|
|
233
|
+
return None
|
|
234
|
+
return next((c for c in dim.candidates if c.label == dim.primary), None)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _runner_up(dim) -> object:
|
|
238
|
+
ranked = sorted(dim.candidates, key=lambda c: (-c.score, c.label))
|
|
239
|
+
for c in ranked:
|
|
240
|
+
if c.label != dim.primary and c.score > 0:
|
|
241
|
+
return c
|
|
242
|
+
return None
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""ENDPOINT & CALL deep-dive pack step handlers (Fase 10).
|
|
2
|
+
|
|
3
|
+
Three anchored intents (ENDPOINT_CONTRACT, ENDPOINT_SECURITY, CALL_NEIGHBORHOOD)
|
|
4
|
+
expressed purely as compositions of these projection steps. NO new analyzer: every step
|
|
5
|
+
reuses an engine already wired into the shared `KnowledgeContext` and projects its
|
|
6
|
+
already-computed output, scoped to the requested anchor.
|
|
7
|
+
|
|
8
|
+
* Contract → `build_validation_surface` (the SAME engine `validation-points` uses),
|
|
9
|
+
scoped to one endpoint.
|
|
10
|
+
* Security → `SpringSemanticModel.endpoint_index` (per-endpoint `CanonicalSecurity`)
|
|
11
|
+
+ `run_security_audit` findings scoped to the handler.
|
|
12
|
+
* Neighborhood → `SpringSemanticModel.call_adj` (forward callees + derived reverse callers
|
|
13
|
+
of the target symbol).
|
|
14
|
+
|
|
15
|
+
Endpoint intents bind an `endpoint` / `symbol` reference via the shared `ResolveEndpointRef`
|
|
16
|
+
step; CALL_NEIGHBORHOOD reuses the impact pack's `ResolveTarget` (cross-pack step reuse).
|
|
17
|
+
No step opens source or parses.
|
|
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
|
+
CollectEndpointEvidence,
|
|
26
|
+
ResolveCallNeighborhood,
|
|
27
|
+
ResolveEndpointContract,
|
|
28
|
+
ResolveEndpointRef,
|
|
29
|
+
ResolveEndpointSecurity,
|
|
30
|
+
Step,
|
|
31
|
+
)
|
|
32
|
+
from sourcecode.retrieval.result import (
|
|
33
|
+
Explanation,
|
|
34
|
+
KnowledgeEntity,
|
|
35
|
+
Observation,
|
|
36
|
+
Provenance,
|
|
37
|
+
RetrievalEvidence,
|
|
38
|
+
)
|
|
39
|
+
from sourcecode.retrieval.steps import (
|
|
40
|
+
SLOT_CONFIDENCE,
|
|
41
|
+
SLOT_ENTITIES,
|
|
42
|
+
SLOT_EVIDENCE,
|
|
43
|
+
SLOT_EXPLANATION,
|
|
44
|
+
SLOT_OBSERVATIONS,
|
|
45
|
+
SLOT_PROVENANCE,
|
|
46
|
+
)
|
|
47
|
+
from sourcecode.retrieval.steps_impact import SLOT_TARGET
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from sourcecode.retrieval.runtime import StepContext, StepRegistry
|
|
51
|
+
|
|
52
|
+
SLOT_ENDPOINT_REF = "endpoint_ref"
|
|
53
|
+
SLOT_SERVICES = "endpoint_services"
|
|
54
|
+
_REF_ANCHOR_KINDS = ("endpoint", "symbol", "path")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── shared endpoint-reference binding step ───────────────────────────────────────
|
|
58
|
+
def resolve_endpoint_ref(step: Step, ctx: "StepContext") -> None:
|
|
59
|
+
"""Bind the request's endpoint/symbol/path anchor to a reference token, matched
|
|
60
|
+
structurally against the endpoint model / validation surface downstream."""
|
|
61
|
+
assert isinstance(step, ResolveEndpointRef)
|
|
62
|
+
anchor = next((a for a in ctx.anchors if a.anchor.kind in _REF_ANCHOR_KINDS), None)
|
|
63
|
+
if anchor is None:
|
|
64
|
+
raise StepContractError(
|
|
65
|
+
"endpoint intents require an 'endpoint', 'path', or 'symbol' anchor"
|
|
66
|
+
)
|
|
67
|
+
ctx.put(SLOT_ENDPOINT_REF, anchor.identities[0] if anchor.identities else anchor.anchor.raw)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── contract (build_validation_surface, scoped) ──────────────────────────────────
|
|
71
|
+
def resolve_endpoint_contract(step: Step, ctx: "StepContext") -> None:
|
|
72
|
+
assert isinstance(step, ResolveEndpointContract)
|
|
73
|
+
from sourcecode.validation_surface import build_validation_surface
|
|
74
|
+
|
|
75
|
+
ref = ctx.require(SLOT_ENDPOINT_REF)
|
|
76
|
+
surface = build_validation_surface(ctx.knowledge.root, graph=ctx.knowledge.context_graph)
|
|
77
|
+
endpoints = surface.get("endpoints", []) or []
|
|
78
|
+
matched = [ep for ep in endpoints if _endpoint_matches(ep, ref)]
|
|
79
|
+
ctx.put(
|
|
80
|
+
SLOT_ENTITIES,
|
|
81
|
+
[
|
|
82
|
+
KnowledgeEntity(
|
|
83
|
+
identity=_endpoint_id(ep),
|
|
84
|
+
kind="endpoint_contract",
|
|
85
|
+
score=0.0,
|
|
86
|
+
attributes={k: v for k, v in ep.items() if k not in ("source_file",)},
|
|
87
|
+
)
|
|
88
|
+
for ep in matched
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
ctx.put(
|
|
92
|
+
SLOT_EVIDENCE,
|
|
93
|
+
[
|
|
94
|
+
RetrievalEvidence(
|
|
95
|
+
signal="endpoint_contract",
|
|
96
|
+
detail=_endpoint_id(ep),
|
|
97
|
+
weight=1.0,
|
|
98
|
+
strength=1.0,
|
|
99
|
+
mass=1.0,
|
|
100
|
+
contribution=1.0,
|
|
101
|
+
provider="build_validation_surface",
|
|
102
|
+
source_file=ep.get("source_file"),
|
|
103
|
+
)
|
|
104
|
+
for ep in matched
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
ctx.put(
|
|
108
|
+
SLOT_OBSERVATIONS,
|
|
109
|
+
[Observation(name="matched_endpoints", value=len(matched), detail=f"ref={ref!r}")],
|
|
110
|
+
)
|
|
111
|
+
ctx.put(SLOT_CONFIDENCE, "high" if matched else "low")
|
|
112
|
+
_add_service(ctx, "build_validation_surface")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── security (endpoint_index + run_security_audit, scoped) ───────────────────────
|
|
116
|
+
def resolve_endpoint_security(step: Step, ctx: "StepContext") -> None:
|
|
117
|
+
assert isinstance(step, ResolveEndpointSecurity)
|
|
118
|
+
ref = ctx.require(SLOT_ENDPOINT_REF)
|
|
119
|
+
index = ctx.knowledge.spring_model.endpoint_index
|
|
120
|
+
endpoints = [ep for eps in index.by_controller.values() for ep in eps]
|
|
121
|
+
matched = [ep for ep in endpoints if _canonical_endpoint_matches(ep, ref)]
|
|
122
|
+
handlers = {ep.handler_symbol for ep in matched if ep.handler_symbol}
|
|
123
|
+
findings = [f for f in _security_findings(ctx) if _finding_on_handler(f, handlers)]
|
|
124
|
+
ctx.put(
|
|
125
|
+
SLOT_ENTITIES,
|
|
126
|
+
[
|
|
127
|
+
KnowledgeEntity(
|
|
128
|
+
identity=ep.handler_symbol or f"{ep.method or ''} {ep.path or ''}".strip(),
|
|
129
|
+
kind="endpoint_security",
|
|
130
|
+
score=0.0,
|
|
131
|
+
attributes={
|
|
132
|
+
"method": ep.method or "",
|
|
133
|
+
"path": ep.path or "",
|
|
134
|
+
"security": _security_summary(ep.security),
|
|
135
|
+
"findings": [f.pattern_id for f in findings if _finding_on_handler(f, {ep.handler_symbol})],
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
for ep in matched
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
ctx.put(
|
|
142
|
+
SLOT_EVIDENCE,
|
|
143
|
+
[
|
|
144
|
+
RetrievalEvidence(
|
|
145
|
+
signal=f.pattern_id,
|
|
146
|
+
detail=f.title,
|
|
147
|
+
weight=1.0,
|
|
148
|
+
strength=1.0,
|
|
149
|
+
mass=1.0,
|
|
150
|
+
contribution=1.0,
|
|
151
|
+
provider=f"audit:{f.category}",
|
|
152
|
+
source_file=getattr(f, "source_file", "") or None,
|
|
153
|
+
)
|
|
154
|
+
for f in findings
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
ctx.put(
|
|
158
|
+
SLOT_OBSERVATIONS,
|
|
159
|
+
[
|
|
160
|
+
Observation(name="matched_endpoints", value=len(matched), detail=f"ref={ref!r}"),
|
|
161
|
+
Observation(name="security_findings", value=len(findings), detail="run_security_audit"),
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
ctx.put(SLOT_CONFIDENCE, "high" if matched else "low")
|
|
165
|
+
_add_service(ctx, "SpringSemanticModel")
|
|
166
|
+
_add_service(ctx, "run_security_audit")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ── call neighborhood (SpringSemanticModel.call_adj) ─────────────────────────────
|
|
170
|
+
def resolve_call_neighborhood(step: Step, ctx: "StepContext") -> None:
|
|
171
|
+
"""Forward callees + derived reverse callers of the target symbol. Reads the built
|
|
172
|
+
call adjacency directly; reuses the impact pack's ResolveTarget for the symbol."""
|
|
173
|
+
assert isinstance(step, ResolveCallNeighborhood)
|
|
174
|
+
target = ctx.require(SLOT_TARGET)
|
|
175
|
+
adjacency = ctx.knowledge.spring_model.call_adj.adjacency
|
|
176
|
+
matched_keys = [k for k in adjacency if _sym_matches(k, target)]
|
|
177
|
+
callees: dict[str, int] = {}
|
|
178
|
+
for k in matched_keys:
|
|
179
|
+
for callee in adjacency.get(k, []) or []:
|
|
180
|
+
callees[callee] = callees.get(callee, 0) + 1
|
|
181
|
+
callers: dict[str, int] = {}
|
|
182
|
+
for caller, callee_list in adjacency.items():
|
|
183
|
+
hits = sum(1 for c in (callee_list or []) if _sym_matches(c, target))
|
|
184
|
+
if hits and caller not in matched_keys:
|
|
185
|
+
callers[caller] = hits
|
|
186
|
+
entities = [
|
|
187
|
+
KnowledgeEntity(
|
|
188
|
+
identity=fqn, kind="callee", score=0.0, attributes={"direction": "out", "count": n}
|
|
189
|
+
)
|
|
190
|
+
for fqn, n in sorted(callees.items())
|
|
191
|
+
] + [
|
|
192
|
+
KnowledgeEntity(
|
|
193
|
+
identity=fqn, kind="caller", score=0.0, attributes={"direction": "in", "count": n}
|
|
194
|
+
)
|
|
195
|
+
for fqn, n in sorted(callers.items())
|
|
196
|
+
]
|
|
197
|
+
ctx.put(SLOT_ENTITIES, entities)
|
|
198
|
+
ctx.put(
|
|
199
|
+
SLOT_OBSERVATIONS,
|
|
200
|
+
[
|
|
201
|
+
Observation(name="matched_symbols", value=len(matched_keys), detail=f"target={target!r}"),
|
|
202
|
+
Observation(name="callees", value=len(callees), detail="forward call edges"),
|
|
203
|
+
Observation(name="callers", value=len(callers), detail="reverse call edges"),
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
ctx.put(SLOT_CONFIDENCE, "high" if matched_keys else "low")
|
|
207
|
+
_add_service(ctx, "SpringSemanticModel")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── terminal evidence (generic across the pack) ──────────────────────────────────
|
|
211
|
+
def collect_endpoint_evidence(step: Step, ctx: "StepContext") -> None:
|
|
212
|
+
"""Assemble confidence, a structured explanation (no synthesized prose), and
|
|
213
|
+
provenance from what the resolve step produced. GENERIC across the pack."""
|
|
214
|
+
assert isinstance(step, CollectEndpointEvidence)
|
|
215
|
+
confidence = ctx.get(SLOT_CONFIDENCE, "medium")
|
|
216
|
+
ctx.put(
|
|
217
|
+
SLOT_EXPLANATION,
|
|
218
|
+
Explanation(
|
|
219
|
+
winner=None,
|
|
220
|
+
score=0.0,
|
|
221
|
+
confidence=confidence,
|
|
222
|
+
top_contributions=(),
|
|
223
|
+
runner_up=None,
|
|
224
|
+
engine_explanation="",
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
ctx.put(
|
|
228
|
+
SLOT_PROVENANCE,
|
|
229
|
+
Provenance(
|
|
230
|
+
services=tuple(ctx.get(SLOT_SERVICES, [])),
|
|
231
|
+
signals_used=("endpoint_index", "validation_surface", "security_audit", "call_adj"),
|
|
232
|
+
signals_missing=(),
|
|
233
|
+
knowledge_source="SpringSemanticModel + validation/security engines",
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ── helpers ──────────────────────────────────────────────────────────────────────
|
|
239
|
+
def _security_findings(ctx: "StepContext") -> list:
|
|
240
|
+
from sourcecode.spring_security_audit import run_security_audit
|
|
241
|
+
|
|
242
|
+
return run_security_audit(
|
|
243
|
+
ctx.knowledge.context_graph.cir, root=ctx.knowledge.root, model=ctx.knowledge.spring_model
|
|
244
|
+
).findings
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _endpoint_id(ep: dict) -> str:
|
|
248
|
+
return (
|
|
249
|
+
ep.get("handler")
|
|
250
|
+
or f"{ep.get('method', '')} {ep.get('path', '')}".strip()
|
|
251
|
+
or ep.get("path", "")
|
|
252
|
+
or "?"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _ref_matches(path: str, handler: str, ref: str) -> bool:
|
|
257
|
+
"""A path ref (starts with '/') matches the endpoint's path exactly (trailing slash
|
|
258
|
+
normalized); anything else is a handler symbol match. No substring matching — that
|
|
259
|
+
would let '/' match every endpoint."""
|
|
260
|
+
if ref.startswith("/"):
|
|
261
|
+
return path.rstrip("/") == ref.rstrip("/")
|
|
262
|
+
return ref == handler or _sym_matches(handler, ref)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _endpoint_matches(ep: dict, ref: str) -> bool:
|
|
266
|
+
return _ref_matches(ep.get("path", "") or "", ep.get("handler", "") or "", ref)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _canonical_endpoint_matches(ep, ref: str) -> bool:
|
|
270
|
+
return _ref_matches(ep.path or "", ep.handler_symbol or "", ref)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _finding_on_handler(f, handlers: set) -> bool:
|
|
274
|
+
sym = getattr(f, "symbol", "") or ""
|
|
275
|
+
return any(sym == h or (h and (sym.startswith(h) or h.startswith(sym))) for h in handlers if h)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _sym_matches(fqn: str, target: str) -> bool:
|
|
279
|
+
if not fqn:
|
|
280
|
+
return False
|
|
281
|
+
if fqn == target or fqn.endswith("." + target) or fqn.endswith("#" + target):
|
|
282
|
+
return True
|
|
283
|
+
simple = fqn.rsplit("#", 1)[0].rsplit(".", 1)[-1]
|
|
284
|
+
return simple == target
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _security_summary(security) -> dict:
|
|
288
|
+
if security is None:
|
|
289
|
+
return {}
|
|
290
|
+
return {
|
|
291
|
+
"policy": getattr(security, "policy", None),
|
|
292
|
+
"source_scope": getattr(security, "source_scope", None),
|
|
293
|
+
"effective_roles": list(getattr(security, "effective_roles", []) or []),
|
|
294
|
+
"expression": getattr(security, "expression", "") or "",
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _add_service(ctx: "StepContext", name: str) -> None:
|
|
299
|
+
services = list(ctx.get(SLOT_SERVICES, []))
|
|
300
|
+
if name not in services:
|
|
301
|
+
services.append(name)
|
|
302
|
+
ctx.put(SLOT_SERVICES, services)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def register_endpoint_steps(registry: "StepRegistry") -> None:
|
|
306
|
+
"""Register the ENDPOINT & CALL deep-dive handlers (the sanctioned extension seam)."""
|
|
307
|
+
registry.register(ResolveEndpointRef.kind, resolve_endpoint_ref)
|
|
308
|
+
registry.register(ResolveEndpointContract.kind, resolve_endpoint_contract)
|
|
309
|
+
registry.register(ResolveEndpointSecurity.kind, resolve_endpoint_security)
|
|
310
|
+
registry.register(ResolveCallNeighborhood.kind, resolve_call_neighborhood)
|
|
311
|
+
registry.register(CollectEndpointEvidence.kind, collect_endpoint_evidence)
|