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,247 @@
|
|
|
1
|
+
"""INTERFACES & INTEGRATIONS pack step handlers (Fase 7).
|
|
2
|
+
|
|
3
|
+
Three whole-repo inventory intents (ENDPOINT_INVENTORY, INTEGRATION_INVENTORY,
|
|
4
|
+
EVENT_TOPOLOGY) expressed purely as compositions of these projection steps. NO new
|
|
5
|
+
analyzer: every step reuses an engine already wired into the shared `KnowledgeContext`
|
|
6
|
+
and projects its already-computed output.
|
|
7
|
+
|
|
8
|
+
* Endpoints → `SpringSemanticModel.endpoint_index` (the SAME index the `endpoints` /
|
|
9
|
+
SEC audit path uses).
|
|
10
|
+
* Integrations → `SemanticIntegrationEngine.detect()` — the whole-repo counterpart of the
|
|
11
|
+
impact pack's cone-scoped `ResolveIntegrations` (same engine, no cone filter).
|
|
12
|
+
* Events → `SpringSemanticModel.event_graph` (publishers / listeners per event).
|
|
13
|
+
|
|
14
|
+
No step opens source or parses: the ContextGraph / SpringSemanticModel are acquired once by
|
|
15
|
+
the shared `KnowledgeContext`; steps only read them. Ordering is deterministic (by
|
|
16
|
+
identity). The terminal `CollectInterfaceEvidence` is generic across the pack.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from sourcecode.retrieval.query import (
|
|
23
|
+
CollectInterfaceEvidence,
|
|
24
|
+
ResolveEndpointInventory,
|
|
25
|
+
ResolveEventTopology,
|
|
26
|
+
ResolveIntegrationInventory,
|
|
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 _integration_evidence
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from sourcecode.retrieval.runtime import StepContext, StepRegistry
|
|
48
|
+
|
|
49
|
+
SLOT_SERVICES = "intf_services"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── endpoints (SpringSemanticModel.endpoint_index) ───────────────────────────────
|
|
53
|
+
def resolve_endpoint_inventory(step: Step, ctx: "StepContext") -> None:
|
|
54
|
+
assert isinstance(step, ResolveEndpointInventory)
|
|
55
|
+
index = ctx.knowledge.spring_model.endpoint_index
|
|
56
|
+
endpoints = [ep for eps in index.by_controller.values() for ep in eps]
|
|
57
|
+
endpoints.sort(key=_endpoint_id)
|
|
58
|
+
ctx.put(SLOT_ENTITIES, [_endpoint_entity(ep) for ep in endpoints])
|
|
59
|
+
ctx.put(SLOT_EVIDENCE, [_endpoint_evidence(ep) for ep in endpoints])
|
|
60
|
+
ctx.put(
|
|
61
|
+
SLOT_OBSERVATIONS,
|
|
62
|
+
[
|
|
63
|
+
Observation(name="endpoints", value=len(endpoints), detail="endpoint_index"),
|
|
64
|
+
Observation(
|
|
65
|
+
name="controllers",
|
|
66
|
+
value=len(index.by_controller),
|
|
67
|
+
detail="endpoint_index controllers",
|
|
68
|
+
),
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
ctx.put(SLOT_CONFIDENCE, "high" if endpoints else "medium")
|
|
72
|
+
_add_service(ctx, "SpringSemanticModel")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── integrations (SemanticIntegrationEngine — whole repo, no cone) ───────────────
|
|
76
|
+
def resolve_integration_inventory(step: Step, ctx: "StepContext") -> None:
|
|
77
|
+
assert isinstance(step, ResolveIntegrationInventory)
|
|
78
|
+
from sourcecode.semantic_integration_engine import SemanticIntegrationEngine
|
|
79
|
+
|
|
80
|
+
integrations = SemanticIntegrationEngine(ctx.knowledge.context_graph).detect()
|
|
81
|
+
integrations = sorted(integrations, key=lambda i: (i.kind, i.client, i.target or ""))
|
|
82
|
+
ctx.put(
|
|
83
|
+
SLOT_ENTITIES,
|
|
84
|
+
[
|
|
85
|
+
KnowledgeEntity(
|
|
86
|
+
identity=f"{i.kind}:{i.target or i.client}",
|
|
87
|
+
kind="integration",
|
|
88
|
+
score=0.0,
|
|
89
|
+
attributes={
|
|
90
|
+
"integration_kind": i.kind,
|
|
91
|
+
"client": i.client,
|
|
92
|
+
"target": i.target,
|
|
93
|
+
"basis": i.basis,
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
for i in integrations
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
ctx.put(SLOT_EVIDENCE, [_integration_evidence(i) for i in integrations])
|
|
100
|
+
ctx.put(SLOT_OBSERVATIONS, _kind_histogram(integrations, key=lambda i: i.kind, label="integration"))
|
|
101
|
+
ctx.put(SLOT_CONFIDENCE, "high" if integrations else "medium")
|
|
102
|
+
_add_service(ctx, "SemanticIntegrationEngine")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── events (SpringSemanticModel.event_graph) ─────────────────────────────────────
|
|
106
|
+
def resolve_event_topology(step: Step, ctx: "StepContext") -> None:
|
|
107
|
+
assert isinstance(step, ResolveEventTopology)
|
|
108
|
+
eg = ctx.knowledge.spring_model.event_graph
|
|
109
|
+
event_types = sorted(eg.event_types)
|
|
110
|
+
entities = [
|
|
111
|
+
KnowledgeEntity(
|
|
112
|
+
identity=et,
|
|
113
|
+
kind="event_type",
|
|
114
|
+
score=0.0,
|
|
115
|
+
attributes={
|
|
116
|
+
"publishers": list(eg.publishers_of(et)),
|
|
117
|
+
"listeners": list(eg.listeners_of(et)),
|
|
118
|
+
"publisher_count": len(eg.publishers_of(et)),
|
|
119
|
+
"listener_count": len(eg.listeners_of(et)),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
for et in event_types
|
|
123
|
+
]
|
|
124
|
+
ctx.put(SLOT_ENTITIES, entities)
|
|
125
|
+
ctx.put(
|
|
126
|
+
SLOT_EVIDENCE,
|
|
127
|
+
[
|
|
128
|
+
RetrievalEvidence(
|
|
129
|
+
signal="event",
|
|
130
|
+
detail=f"{et}: {len(eg.publishers_of(et))} pub / {len(eg.listeners_of(et))} sub",
|
|
131
|
+
weight=1.0,
|
|
132
|
+
strength=1.0,
|
|
133
|
+
mass=1.0,
|
|
134
|
+
contribution=1.0,
|
|
135
|
+
provider="SpringSemanticModel.event_graph",
|
|
136
|
+
)
|
|
137
|
+
for et in event_types
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
ctx.put(
|
|
141
|
+
SLOT_OBSERVATIONS,
|
|
142
|
+
[
|
|
143
|
+
Observation(name="event_types", value=len(event_types), detail="event_graph"),
|
|
144
|
+
Observation(name="event_edges", value=eg.total_edges, detail="event_graph total edges"),
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
ctx.put(SLOT_CONFIDENCE, "high" if event_types else "medium")
|
|
148
|
+
_add_service(ctx, "SpringSemanticModel")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── terminal evidence (generic across the pack) ──────────────────────────────────
|
|
152
|
+
def collect_interface_evidence(step: Step, ctx: "StepContext") -> None:
|
|
153
|
+
"""Assemble confidence, a structured explanation (no synthesized prose), and
|
|
154
|
+
provenance from what the resolve step produced. GENERIC across the pack."""
|
|
155
|
+
assert isinstance(step, CollectInterfaceEvidence)
|
|
156
|
+
confidence = ctx.get(SLOT_CONFIDENCE, "medium")
|
|
157
|
+
ctx.put(
|
|
158
|
+
SLOT_EXPLANATION,
|
|
159
|
+
Explanation(
|
|
160
|
+
winner=None,
|
|
161
|
+
score=0.0,
|
|
162
|
+
confidence=confidence,
|
|
163
|
+
top_contributions=(),
|
|
164
|
+
runner_up=None,
|
|
165
|
+
engine_explanation="",
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
ctx.put(
|
|
169
|
+
SLOT_PROVENANCE,
|
|
170
|
+
Provenance(
|
|
171
|
+
services=tuple(ctx.get(SLOT_SERVICES, [])),
|
|
172
|
+
signals_used=("endpoint_index", "integration_engine", "event_graph"),
|
|
173
|
+
signals_missing=(),
|
|
174
|
+
knowledge_source="SpringSemanticModel + SemanticIntegrationEngine",
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── helpers ──────────────────────────────────────────────────────────────────────
|
|
180
|
+
def _endpoint_id(ep) -> str:
|
|
181
|
+
return ep.handler_symbol or f"{ep.method or ''} {ep.path or ''}".strip()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _endpoint_entity(ep) -> KnowledgeEntity:
|
|
185
|
+
return KnowledgeEntity(
|
|
186
|
+
identity=_endpoint_id(ep),
|
|
187
|
+
kind="endpoint",
|
|
188
|
+
score=0.0,
|
|
189
|
+
attributes={
|
|
190
|
+
"method": ep.method or "",
|
|
191
|
+
"path": ep.path or "",
|
|
192
|
+
"controller_class": ep.controller_class or "",
|
|
193
|
+
"handler_symbol": ep.handler_symbol or "",
|
|
194
|
+
"security": _security_summary(ep.security),
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _endpoint_evidence(ep) -> RetrievalEvidence:
|
|
200
|
+
return RetrievalEvidence(
|
|
201
|
+
signal="endpoint",
|
|
202
|
+
detail=f"{ep.method or ''} {ep.path or ''}".strip(),
|
|
203
|
+
weight=1.0,
|
|
204
|
+
strength=1.0,
|
|
205
|
+
mass=1.0,
|
|
206
|
+
contribution=1.0,
|
|
207
|
+
provider="SpringSemanticModel.endpoint_index",
|
|
208
|
+
source_file=ep.source_file or None,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _security_summary(security) -> dict:
|
|
213
|
+
"""Compact, JSON-safe projection of the endpoint's CanonicalSecurity (no new
|
|
214
|
+
analysis — just the engine's own fields)."""
|
|
215
|
+
if security is None:
|
|
216
|
+
return {}
|
|
217
|
+
return {
|
|
218
|
+
"policy": getattr(security, "policy", None),
|
|
219
|
+
"source_scope": getattr(security, "source_scope", None),
|
|
220
|
+
"effective_roles": list(getattr(security, "effective_roles", []) or []),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _kind_histogram(items, *, key, label: str) -> list:
|
|
225
|
+
hist: dict[str, int] = {}
|
|
226
|
+
for it in items:
|
|
227
|
+
k = key(it)
|
|
228
|
+
hist[k] = hist.get(k, 0) + 1
|
|
229
|
+
return [
|
|
230
|
+
Observation(name=f"{label}_{k}", value=n, detail=f"{label} count")
|
|
231
|
+
for k, n in sorted(hist.items())
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _add_service(ctx: "StepContext", name: str) -> None:
|
|
236
|
+
services = list(ctx.get(SLOT_SERVICES, []))
|
|
237
|
+
if name not in services:
|
|
238
|
+
services.append(name)
|
|
239
|
+
ctx.put(SLOT_SERVICES, services)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def register_intf_steps(registry: "StepRegistry") -> None:
|
|
243
|
+
"""Register the INTERFACES & INTEGRATIONS handlers (the sanctioned extension seam)."""
|
|
244
|
+
registry.register(ResolveEndpointInventory.kind, resolve_endpoint_inventory)
|
|
245
|
+
registry.register(ResolveIntegrationInventory.kind, resolve_integration_inventory)
|
|
246
|
+
registry.register(ResolveEventTopology.kind, resolve_event_topology)
|
|
247
|
+
registry.register(CollectInterfaceEvidence.kind, collect_interface_evidence)
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""SUBSYSTEMS & DEPENDENCIES pack step handlers (Fase 8).
|
|
2
|
+
|
|
3
|
+
Four intents (SUBSYSTEM_INVENTORY, SUBSYSTEM_MEMBERS, MODULE_DEPENDENTS,
|
|
4
|
+
MODULE_DEPENDENCIES) expressed purely as compositions of these projection steps. NO new
|
|
5
|
+
analyzer: every step reuses knowledge already built into the shared `KnowledgeContext`.
|
|
6
|
+
|
|
7
|
+
* Subsystems → `repo_ir['subsystems']` (the SAME subsystem partition the `impact` /
|
|
8
|
+
cross-module engine uses).
|
|
9
|
+
* Dependencies → the module graph (`SourceMap.module_graph`, built by `GraphAnalyzer` —
|
|
10
|
+
the SAME graph the `archetype` / graph commands use). Dependents/dependencies are the
|
|
11
|
+
reverse/forward edges of a scope, read directly off the built graph.
|
|
12
|
+
|
|
13
|
+
The two dependency intents are ANCHORED: a shared `ResolveScope` step binds the request's
|
|
14
|
+
subsystem / module / package anchor to a scope token, which the projection step matches
|
|
15
|
+
against the graph's OWN node ids (a structural membership filter over the built index —
|
|
16
|
+
never a fuzzy text scan of source). Cycle *enumeration* is intentionally NOT here: the
|
|
17
|
+
graph engine only counts import cycles, so surfacing the members would require new
|
|
18
|
+
analysis — out of scope for a reuse-only pack.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from sourcecode.retrieval.errors import StepContractError
|
|
25
|
+
from sourcecode.retrieval.query import (
|
|
26
|
+
CollectStructureEvidence,
|
|
27
|
+
ResolveModuleDependencies,
|
|
28
|
+
ResolveModuleDependents,
|
|
29
|
+
ResolveScope,
|
|
30
|
+
ResolveSubsystemInventory,
|
|
31
|
+
ResolveSubsystemMembers,
|
|
32
|
+
ResolveTypeDependents,
|
|
33
|
+
Step,
|
|
34
|
+
)
|
|
35
|
+
from sourcecode.retrieval.result import (
|
|
36
|
+
Explanation,
|
|
37
|
+
KnowledgeEntity,
|
|
38
|
+
Observation,
|
|
39
|
+
Provenance,
|
|
40
|
+
RetrievalEvidence,
|
|
41
|
+
)
|
|
42
|
+
from sourcecode.retrieval.steps import (
|
|
43
|
+
SLOT_CONFIDENCE,
|
|
44
|
+
SLOT_ENTITIES,
|
|
45
|
+
SLOT_EVIDENCE,
|
|
46
|
+
SLOT_EXPLANATION,
|
|
47
|
+
SLOT_OBSERVATIONS,
|
|
48
|
+
SLOT_PROVENANCE,
|
|
49
|
+
)
|
|
50
|
+
from sourcecode.retrieval.steps_impact import SLOT_TARGET
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from sourcecode.retrieval.runtime import StepContext, StepRegistry
|
|
54
|
+
|
|
55
|
+
SLOT_SCOPE = "structure_scope"
|
|
56
|
+
SLOT_SERVICES = "structure_services"
|
|
57
|
+
_SCOPE_ANCHOR_KINDS = ("subsystem", "module", "package")
|
|
58
|
+
_MODULE_PREFIX = "module:"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── shared scope-binding step (anchored intents) ─────────────────────────────────
|
|
62
|
+
def resolve_scope(step: Step, ctx: "StepContext") -> None:
|
|
63
|
+
"""Bind the request's subsystem/module/package anchor to a scope token. The token is
|
|
64
|
+
matched structurally against the subsystem partition / graph node ids downstream."""
|
|
65
|
+
assert isinstance(step, ResolveScope)
|
|
66
|
+
anchor = next((a for a in ctx.anchors if a.anchor.kind in _SCOPE_ANCHOR_KINDS), None)
|
|
67
|
+
if anchor is None:
|
|
68
|
+
raise StepContractError(
|
|
69
|
+
"subsystem/dependency intents require a 'subsystem', 'module', or 'package' anchor"
|
|
70
|
+
)
|
|
71
|
+
ctx.put(SLOT_SCOPE, anchor.identities[0] if anchor.identities else anchor.anchor.raw)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── subsystems (repo_ir['subsystems']) ───────────────────────────────────────────
|
|
75
|
+
def resolve_subsystem_inventory(step: Step, ctx: "StepContext") -> None:
|
|
76
|
+
assert isinstance(step, ResolveSubsystemInventory)
|
|
77
|
+
subsystems = ctx.knowledge.repo_ir.get("subsystems", []) or []
|
|
78
|
+
ordered = sorted(subsystems, key=lambda s: (-int(s.get("member_count", 0)), s.get("label", "")))
|
|
79
|
+
ctx.put(
|
|
80
|
+
SLOT_ENTITIES,
|
|
81
|
+
[
|
|
82
|
+
KnowledgeEntity(
|
|
83
|
+
identity=s.get("label", "") or s.get("package_prefix", ""),
|
|
84
|
+
kind="subsystem",
|
|
85
|
+
score=0.0,
|
|
86
|
+
attributes={
|
|
87
|
+
"package_prefix": s.get("package_prefix", ""),
|
|
88
|
+
"member_count": s.get("member_count", 0),
|
|
89
|
+
"summary": s.get("summary", ""),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
for s in ordered
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
ctx.put(
|
|
96
|
+
SLOT_OBSERVATIONS,
|
|
97
|
+
[Observation(name="subsystems", value=len(ordered), detail="repo_ir subsystem partition")],
|
|
98
|
+
)
|
|
99
|
+
ctx.put(SLOT_CONFIDENCE, "high" if ordered else "medium")
|
|
100
|
+
_add_service(ctx, "build_repo_ir(subsystems)")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def resolve_subsystem_members(step: Step, ctx: "StepContext") -> None:
|
|
104
|
+
"""The member symbols of the subsystem named by the scope anchor. Reuses the repo_ir
|
|
105
|
+
subsystem partition; matches the scope against the subsystem label / package prefix."""
|
|
106
|
+
assert isinstance(step, ResolveSubsystemMembers)
|
|
107
|
+
scope = ctx.require(SLOT_SCOPE)
|
|
108
|
+
subsystems = ctx.knowledge.repo_ir.get("subsystems", []) or []
|
|
109
|
+
matched = [s for s in subsystems if _subsystem_matches(s, scope)]
|
|
110
|
+
members = [(s, m) for s in matched for m in (s.get("members", []) or [])]
|
|
111
|
+
ctx.put(
|
|
112
|
+
SLOT_ENTITIES,
|
|
113
|
+
[
|
|
114
|
+
KnowledgeEntity(
|
|
115
|
+
identity=m,
|
|
116
|
+
kind="subsystem_member",
|
|
117
|
+
score=0.0,
|
|
118
|
+
attributes={"subsystem": s.get("label", ""), "package_prefix": s.get("package_prefix", "")},
|
|
119
|
+
)
|
|
120
|
+
for s, m in members
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
ctx.put(
|
|
124
|
+
SLOT_OBSERVATIONS,
|
|
125
|
+
[
|
|
126
|
+
Observation(name="matched_subsystems", value=len(matched), detail=f"scope={scope!r}"),
|
|
127
|
+
Observation(name="members", value=len(members), detail="subsystem members"),
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
ctx.put(SLOT_CONFIDENCE, "high" if members else "low")
|
|
131
|
+
_add_service(ctx, "build_repo_ir(subsystems)")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── dependencies (SourceMap.module_graph) ────────────────────────────────────────
|
|
135
|
+
def resolve_module_dependents(step: Step, ctx: "StepContext") -> None:
|
|
136
|
+
"""Modules that depend ON the scope: sources of edges whose target is in the scope.
|
|
137
|
+
Reads the built module graph directly (reverse edges)."""
|
|
138
|
+
assert isinstance(step, ResolveModuleDependents)
|
|
139
|
+
_project_edges(ctx, incoming=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def resolve_module_dependencies(step: Step, ctx: "StepContext") -> None:
|
|
143
|
+
"""Modules the scope DEPENDS ON: targets of edges whose source is in the scope.
|
|
144
|
+
Reads the built module graph directly (forward edges)."""
|
|
145
|
+
assert isinstance(step, ResolveModuleDependencies)
|
|
146
|
+
_project_edges(ctx, incoming=False)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _project_edges(ctx: "StepContext", *, incoming: bool) -> None:
|
|
150
|
+
scope = ctx.require(SLOT_SCOPE)
|
|
151
|
+
mg = ctx.knowledge.module_graph
|
|
152
|
+
if mg is None:
|
|
153
|
+
raise StepContractError("module graph unavailable (name-mass-only acquisition)")
|
|
154
|
+
scope_nodes = _match_nodes(mg, scope)
|
|
155
|
+
relation = "dependent" if incoming else "dependency"
|
|
156
|
+
hits: dict[str, set] = {}
|
|
157
|
+
for e in mg.edges:
|
|
158
|
+
source, target = getattr(e, "source", None), getattr(e, "target", None)
|
|
159
|
+
kind = getattr(e, "kind", "") or ""
|
|
160
|
+
if incoming and target in scope_nodes and source not in scope_nodes:
|
|
161
|
+
hits.setdefault(source, set()).add(kind)
|
|
162
|
+
elif not incoming and source in scope_nodes and target not in scope_nodes:
|
|
163
|
+
hits.setdefault(target, set()).add(kind)
|
|
164
|
+
ordered = sorted(hits.items(), key=lambda kv: _node_path(kv[0]))
|
|
165
|
+
ctx.put(
|
|
166
|
+
SLOT_ENTITIES,
|
|
167
|
+
[
|
|
168
|
+
KnowledgeEntity(
|
|
169
|
+
identity=_node_path(node),
|
|
170
|
+
kind="module",
|
|
171
|
+
score=0.0,
|
|
172
|
+
attributes={"relation": relation, "via": sorted(k for k in kinds if k)},
|
|
173
|
+
)
|
|
174
|
+
for node, kinds in ordered
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
ctx.put(
|
|
178
|
+
SLOT_EVIDENCE,
|
|
179
|
+
[
|
|
180
|
+
RetrievalEvidence(
|
|
181
|
+
signal=f"module_{relation}",
|
|
182
|
+
detail=f"{_node_path(node)} ({', '.join(sorted(k for k in kinds if k))})",
|
|
183
|
+
weight=1.0,
|
|
184
|
+
strength=1.0,
|
|
185
|
+
mass=1.0,
|
|
186
|
+
contribution=1.0,
|
|
187
|
+
provider="GraphAnalyzer(module_graph)",
|
|
188
|
+
source_file=_node_path(node),
|
|
189
|
+
)
|
|
190
|
+
for node, kinds in ordered
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
ctx.put(
|
|
194
|
+
SLOT_OBSERVATIONS,
|
|
195
|
+
[
|
|
196
|
+
Observation(name="scope_nodes", value=len(scope_nodes), detail=f"scope={scope!r}"),
|
|
197
|
+
Observation(name=f"{relation}s", value=len(ordered), detail="module graph edges"),
|
|
198
|
+
],
|
|
199
|
+
)
|
|
200
|
+
ctx.put(SLOT_CONFIDENCE, "high" if scope_nodes else "low")
|
|
201
|
+
_add_service(ctx, "GraphAnalyzer(module_graph)")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── type dependents (repo_ir reverse graph) ──────────────────────────────────────
|
|
205
|
+
def resolve_type_dependents(step: Step, ctx: "StepContext") -> None:
|
|
206
|
+
"""The type-level dependents of the target symbol: the reverse-graph edges
|
|
207
|
+
(imports / implements / injects / contained_in) that point AT the target. Reuses the
|
|
208
|
+
repo_ir reverse graph and the impact pack's ResolveTarget (cross-pack step reuse)."""
|
|
209
|
+
assert isinstance(step, ResolveTypeDependents)
|
|
210
|
+
target = ctx.require(SLOT_TARGET)
|
|
211
|
+
reverse = ctx.knowledge.repo_ir.get("reverse_graph", {}) or {}
|
|
212
|
+
matched = {k: v for k, v in reverse.items() if _type_matches(k, target)}
|
|
213
|
+
dependents: dict[str, set] = {}
|
|
214
|
+
for edges in matched.values():
|
|
215
|
+
for edge_kind, fqns in (edges or {}).items():
|
|
216
|
+
for fqn in fqns or []:
|
|
217
|
+
dependents.setdefault(fqn, set()).add(edge_kind)
|
|
218
|
+
ordered = sorted(dependents.items(), key=lambda kv: kv[0])
|
|
219
|
+
ctx.put(
|
|
220
|
+
SLOT_ENTITIES,
|
|
221
|
+
[
|
|
222
|
+
KnowledgeEntity(
|
|
223
|
+
identity=fqn,
|
|
224
|
+
kind="type_dependent",
|
|
225
|
+
score=0.0,
|
|
226
|
+
attributes={"relation": "dependent", "via": sorted(kinds)},
|
|
227
|
+
)
|
|
228
|
+
for fqn, kinds in ordered
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
ctx.put(
|
|
232
|
+
SLOT_EVIDENCE,
|
|
233
|
+
[
|
|
234
|
+
RetrievalEvidence(
|
|
235
|
+
signal="type_dependent",
|
|
236
|
+
detail=f"{fqn} ({', '.join(sorted(kinds))})",
|
|
237
|
+
weight=1.0,
|
|
238
|
+
strength=1.0,
|
|
239
|
+
mass=1.0,
|
|
240
|
+
contribution=1.0,
|
|
241
|
+
provider="build_repo_ir(reverse_graph)",
|
|
242
|
+
)
|
|
243
|
+
for fqn, kinds in ordered
|
|
244
|
+
],
|
|
245
|
+
)
|
|
246
|
+
ctx.put(
|
|
247
|
+
SLOT_OBSERVATIONS,
|
|
248
|
+
[
|
|
249
|
+
Observation(name="matched_types", value=len(matched), detail=f"target={target!r}"),
|
|
250
|
+
Observation(name="dependents", value=len(ordered), detail="reverse graph edges"),
|
|
251
|
+
],
|
|
252
|
+
)
|
|
253
|
+
ctx.put(SLOT_CONFIDENCE, "high" if matched else "low")
|
|
254
|
+
_add_service(ctx, "build_repo_ir(reverse_graph)")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ── terminal evidence (generic across the pack) ──────────────────────────────────
|
|
258
|
+
def collect_structure_evidence(step: Step, ctx: "StepContext") -> None:
|
|
259
|
+
"""Assemble confidence, a structured explanation (no synthesized prose), and
|
|
260
|
+
provenance from what the resolve step produced. GENERIC across the pack."""
|
|
261
|
+
assert isinstance(step, CollectStructureEvidence)
|
|
262
|
+
confidence = ctx.get(SLOT_CONFIDENCE, "medium")
|
|
263
|
+
ctx.put(
|
|
264
|
+
SLOT_EXPLANATION,
|
|
265
|
+
Explanation(
|
|
266
|
+
winner=None,
|
|
267
|
+
score=0.0,
|
|
268
|
+
confidence=confidence,
|
|
269
|
+
top_contributions=(),
|
|
270
|
+
runner_up=None,
|
|
271
|
+
engine_explanation="",
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
ctx.put(
|
|
275
|
+
SLOT_PROVENANCE,
|
|
276
|
+
Provenance(
|
|
277
|
+
services=tuple(ctx.get(SLOT_SERVICES, [])),
|
|
278
|
+
signals_used=("subsystems", "module_graph"),
|
|
279
|
+
signals_missing=(),
|
|
280
|
+
knowledge_source="repo IR (subsystems) + module graph (GraphAnalyzer)",
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── helpers ──────────────────────────────────────────────────────────────────────
|
|
286
|
+
def _subsystem_matches(subsystem: dict, scope: str) -> bool:
|
|
287
|
+
label = (subsystem.get("label", "") or "").lower()
|
|
288
|
+
prefix = (subsystem.get("package_prefix", "") or "").lower()
|
|
289
|
+
s = scope.lower()
|
|
290
|
+
return s == label or s == prefix or s in label or s in prefix
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _type_matches(key: str, target: str) -> bool:
|
|
294
|
+
"""Match a reverse-graph type key against the target symbol: exact FQN, or the target
|
|
295
|
+
is the key's simple name / a dotted suffix. Structural, over the built index."""
|
|
296
|
+
if key == target:
|
|
297
|
+
return True
|
|
298
|
+
simple = key.rsplit(".", 1)[-1]
|
|
299
|
+
return target == simple or key.endswith("." + target)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _normalize_scope(scope: str) -> str:
|
|
303
|
+
"""A package token (dotted) becomes a path fragment so it matches the graph's
|
|
304
|
+
file-path node ids; a raw path fragment is used as-is."""
|
|
305
|
+
return scope.replace(".", "/") if "/" not in scope and "." in scope else scope
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _match_nodes(mg, scope: str) -> set:
|
|
309
|
+
frag = _normalize_scope(scope).lower()
|
|
310
|
+
return {
|
|
311
|
+
getattr(n, "id", None)
|
|
312
|
+
for n in mg.nodes
|
|
313
|
+
if frag in _node_path(getattr(n, "id", None)).lower()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _node_path(node_id) -> str:
|
|
318
|
+
if not node_id:
|
|
319
|
+
return ""
|
|
320
|
+
return node_id[len(_MODULE_PREFIX):] if node_id.startswith(_MODULE_PREFIX) else node_id
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _add_service(ctx: "StepContext", name: str) -> None:
|
|
324
|
+
services = list(ctx.get(SLOT_SERVICES, []))
|
|
325
|
+
if name not in services:
|
|
326
|
+
services.append(name)
|
|
327
|
+
ctx.put(SLOT_SERVICES, services)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def register_struct_steps(registry: "StepRegistry") -> None:
|
|
331
|
+
"""Register the SUBSYSTEMS & DEPENDENCIES handlers (the sanctioned extension seam)."""
|
|
332
|
+
registry.register(ResolveScope.kind, resolve_scope)
|
|
333
|
+
registry.register(ResolveSubsystemInventory.kind, resolve_subsystem_inventory)
|
|
334
|
+
registry.register(ResolveSubsystemMembers.kind, resolve_subsystem_members)
|
|
335
|
+
registry.register(ResolveModuleDependents.kind, resolve_module_dependents)
|
|
336
|
+
registry.register(ResolveModuleDependencies.kind, resolve_module_dependencies)
|
|
337
|
+
registry.register(ResolveTypeDependents.kind, resolve_type_dependents)
|
|
338
|
+
registry.register(CollectStructureEvidence.kind, collect_structure_evidence)
|