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
sourcecode/repository_ir.py
CHANGED
|
@@ -742,7 +742,7 @@ def _extract_symbols(
|
|
|
742
742
|
"""Phase 1: Extract symbols from a Java source file.
|
|
743
743
|
|
|
744
744
|
extra_capture: extra annotation tokens (e.g. custom security annotations like
|
|
745
|
-
"@
|
|
745
|
+
"@CustomSecurityAnnotation") whose argument lists must be stored in annotation_values
|
|
746
746
|
even though they are not in the built-in _CAPTURE_ANN_ARGS set.
|
|
747
747
|
|
|
748
748
|
Returns (package, symbols, raw_imports).
|
|
@@ -1967,6 +1967,22 @@ def _extract_class_type_refs(symbols: list[SymbolRecord]) -> dict[str, list[dict
|
|
|
1967
1967
|
return out
|
|
1968
1968
|
|
|
1969
1969
|
|
|
1970
|
+
_LEADING_ANNOT_RE = re.compile(r'^\s*@\w+(?:\s*\([^()]*\))?\s*')
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _strip_leading_annotations(line: str) -> str:
|
|
1974
|
+
"""Strip leading inline annotations from a declaration line so the residual
|
|
1975
|
+
declaration is still detected (`@Autowired private Foo f;` -> `private Foo f;`).
|
|
1976
|
+
A pure annotation line (`@Override`, `@GetMapping("/x")`) strips to empty.
|
|
1977
|
+
Nested-paren annotation args (rare on fields) are left intact — the residual
|
|
1978
|
+
just won't match a decl regex, which is safe (a miss, never a wrong entry)."""
|
|
1979
|
+
prev: Optional[str] = None
|
|
1980
|
+
while line != prev:
|
|
1981
|
+
prev = line
|
|
1982
|
+
line = _LEADING_ANNOT_RE.sub('', line, count=1)
|
|
1983
|
+
return line.strip()
|
|
1984
|
+
|
|
1985
|
+
|
|
1970
1986
|
def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
|
|
1971
1987
|
"""{class_fqn -> {field_name -> declared_type_simple_name}} for EVERY field
|
|
1972
1988
|
(annotated or plain), the fact that binds an invocation receiver to a typed
|
|
@@ -1994,8 +2010,17 @@ def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
|
|
|
1994
2010
|
if "*/" not in stripped:
|
|
1995
2011
|
in_block = True
|
|
1996
2012
|
continue
|
|
1997
|
-
if not stripped or stripped.startswith("//") or stripped.startswith("*")
|
|
2013
|
+
if not stripped or stripped.startswith("//") or stripped.startswith("*"):
|
|
1998
2014
|
continue
|
|
2015
|
+
if stripped.startswith("@"):
|
|
2016
|
+
# A field/type decl may carry leading inline annotations
|
|
2017
|
+
# (`@Autowired private Foo f;`). Strip them so the residual declaration is
|
|
2018
|
+
# still detected; a pure annotation line strips to empty and is skipped as
|
|
2019
|
+
# before. Brace counting below runs on the stripped residual, so annotation
|
|
2020
|
+
# string args containing braces (`@GetMapping("/a/{id}")`) never skew depth.
|
|
2021
|
+
stripped = _strip_leading_annotations(stripped)
|
|
2022
|
+
if not stripped:
|
|
2023
|
+
continue
|
|
1999
2024
|
if stripped.startswith("package "):
|
|
2000
2025
|
package = stripped[8:].rstrip(";").strip()
|
|
2001
2026
|
continue
|
|
@@ -4351,6 +4376,84 @@ def extract_file_ir(
|
|
|
4351
4376
|
return _assemble(symbols, relations, changed_symbols, spring_summary, route_diffs)
|
|
4352
4377
|
|
|
4353
4378
|
|
|
4379
|
+
def _receiver_typed_call_edges(
|
|
4380
|
+
body_facts: dict[str, list[dict]],
|
|
4381
|
+
field_types: dict[str, dict[str, str]],
|
|
4382
|
+
symbols: list[SymbolRecord],
|
|
4383
|
+
) -> list[RelationEdge]:
|
|
4384
|
+
"""Method-level `calls` edges for instance invocations through a typed field
|
|
4385
|
+
receiver (`field.method(...)`).
|
|
4386
|
+
|
|
4387
|
+
The per-file scans in `_build_relations` resolve intra-class (`m()`/`this.m()`)
|
|
4388
|
+
and static (`Type.method()`, uppercase receiver) calls, but a call through a
|
|
4389
|
+
field-injected or plainly-typed dependency goes through a lowercase receiver
|
|
4390
|
+
variable (`nominasCalculoService.aplicarMejora(...)`) and matched none of them.
|
|
4391
|
+
A service reached only via an injected sibling therefore had NO inbound `calls`
|
|
4392
|
+
edge: impact-chain found 0 callers and reported a false-confident "no blast
|
|
4393
|
+
radius / low risk isolated change" — a dangerous false negative.
|
|
4394
|
+
|
|
4395
|
+
Joins facts already extracted (no new parse — Prime Directive):
|
|
4396
|
+
- invocation atom (owner method → {receiver, callee}) [body_facts]
|
|
4397
|
+
- declared type of `receiver` on the owner's class [field_types]
|
|
4398
|
+
- target type resolved to a SINGLE in-repo class FQN [symbols]
|
|
4399
|
+
- callee resolved to that class's method(s) of that name [symbols]
|
|
4400
|
+
|
|
4401
|
+
Emits `owner_method --calls--> target_class#callee` (every overload of the name —
|
|
4402
|
+
arity is not resolved by name alone, matching `_intra_class_call_edges`). Anything
|
|
4403
|
+
unresolved — a local variable (no field entry), a JDK/library type, an ambiguous
|
|
4404
|
+
simple name, or a callee the target class does not declare — yields nothing: an
|
|
4405
|
+
unresolved fact stays unresolved rather than being guessed (INV honesty). Runs as
|
|
4406
|
+
a repo-wide post-pass because the target type + its methods live in other files.
|
|
4407
|
+
"""
|
|
4408
|
+
if not body_facts or not field_types:
|
|
4409
|
+
return []
|
|
4410
|
+
|
|
4411
|
+
# type simple name → in-repo class FQN(s); class FQN → {method name → [method FQN]}
|
|
4412
|
+
type_simple_to_fqn: dict[str, list[str]] = {}
|
|
4413
|
+
class_methods: dict[str, dict[str, list[str]]] = {}
|
|
4414
|
+
for s in symbols:
|
|
4415
|
+
if s.type in ("class", "interface", "enum", "record") and "#" not in s.symbol:
|
|
4416
|
+
type_simple_to_fqn.setdefault(s.symbol.rsplit(".", 1)[-1], []).append(s.symbol)
|
|
4417
|
+
elif s.symbol_kind == "method" and "#" in s.symbol:
|
|
4418
|
+
cls = _enclosing_class(s.symbol)
|
|
4419
|
+
name = s.symbol.rsplit("#", 1)[1]
|
|
4420
|
+
class_methods.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
|
|
4421
|
+
|
|
4422
|
+
edges: list[RelationEdge] = []
|
|
4423
|
+
for owner_fqn in sorted(body_facts):
|
|
4424
|
+
owner_cls = _enclosing_class(owner_fqn)
|
|
4425
|
+
fmap = field_types.get(owner_cls)
|
|
4426
|
+
if not fmap:
|
|
4427
|
+
continue
|
|
4428
|
+
for atom in body_facts[owner_fqn]:
|
|
4429
|
+
if atom.get("fact") != "invocation":
|
|
4430
|
+
continue
|
|
4431
|
+
receiver, callee = atom.get("receiver"), atom.get("callee")
|
|
4432
|
+
if not receiver or receiver == "this" or not callee:
|
|
4433
|
+
continue
|
|
4434
|
+
tsimple = fmap.get(receiver)
|
|
4435
|
+
if not tsimple:
|
|
4436
|
+
continue # not a typed field (local / unknown)
|
|
4437
|
+
tfqns = type_simple_to_fqn.get(tsimple)
|
|
4438
|
+
if not tfqns or len(tfqns) != 1:
|
|
4439
|
+
continue # unresolved / ambiguous type → skip (honest)
|
|
4440
|
+
target_cls = tfqns[0]
|
|
4441
|
+
if target_cls == owner_cls:
|
|
4442
|
+
continue # intra-class handled by _intra_class_call_edges
|
|
4443
|
+
target_methods = class_methods.get(target_cls, {}).get(callee)
|
|
4444
|
+
if not target_methods:
|
|
4445
|
+
continue # callee not declared on the target type
|
|
4446
|
+
for mfqn in target_methods:
|
|
4447
|
+
edges.append(RelationEdge(
|
|
4448
|
+
from_symbol=owner_fqn,
|
|
4449
|
+
to_symbol=mfqn,
|
|
4450
|
+
type="calls",
|
|
4451
|
+
confidence="medium",
|
|
4452
|
+
evidence={"type": "method_call", "value": f"{receiver}.{callee}(...)"},
|
|
4453
|
+
))
|
|
4454
|
+
return edges
|
|
4455
|
+
|
|
4456
|
+
|
|
4354
4457
|
def build_repo_ir(
|
|
4355
4458
|
file_paths: list[str],
|
|
4356
4459
|
root: Path,
|
|
@@ -4495,6 +4598,17 @@ def build_repo_ir(
|
|
|
4495
4598
|
_annotation_reference_edges(_ann_ref_sources, all_symbols, all_relations, _same_pkg_map)
|
|
4496
4599
|
)
|
|
4497
4600
|
|
|
4601
|
+
# Instance calls through a typed field receiver (`field.method(...)`). The per-file
|
|
4602
|
+
# scans resolve intra-class and static (`Type.method()`) calls but never a call
|
|
4603
|
+
# through a lowercase field-injected dependency, so a service reached only via an
|
|
4604
|
+
# injected sibling had 0 inbound `calls` edges and impact-chain reported a
|
|
4605
|
+
# false-confident "no blast radius". Repo-wide post-pass (target type + methods are
|
|
4606
|
+
# cross-file). Gated on body facts + field types being present (emit_body_facts).
|
|
4607
|
+
if emit_body_facts:
|
|
4608
|
+
all_relations.extend(
|
|
4609
|
+
_receiver_typed_call_edges(_all_body_facts, _all_field_types, all_symbols)
|
|
4610
|
+
)
|
|
4611
|
+
|
|
4498
4612
|
spring_summary = _build_spring_summary(all_symbols)
|
|
4499
4613
|
|
|
4500
4614
|
# Deduplicate relations
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Semantic Retrieval Engine — Layer 3.5 (ADR-0001 / DESIGN-semantic-retrieval.md).
|
|
2
|
+
|
|
3
|
+
Turns "ask a typed question of the repository's knowledge" into a first-class,
|
|
4
|
+
reusable operation. This layer **queries already-built structured knowledge**; it
|
|
5
|
+
does NOT extract, does NOT analyze source, does NOT build the Semantic IR or the
|
|
6
|
+
ContextGraph, and does NOT compute new Evidence. Every answer is a *composition* of
|
|
7
|
+
existing Knowledge-Layer capabilities (SemanticServices, ContextGraph, the derived
|
|
8
|
+
engines, the Evidence Providers) acquired through the Knowledge Artifact Store.
|
|
9
|
+
|
|
10
|
+
Public pipeline (design §3.1):
|
|
11
|
+
|
|
12
|
+
RetrievalRequest → IntentResolver → QueryPlanner → KnowledgeQuery
|
|
13
|
+
→ KnowledgeExecutor → KnowledgeResult (+ RetrievalEvidence)
|
|
14
|
+
|
|
15
|
+
Phase 2 delivers the complete skeleton plus exactly one intent — ARCHITECTURAL_CORE —
|
|
16
|
+
proven end-to-end over the existing ArchetypeClassifier / GraphEvidenceProvider. Adding
|
|
17
|
+
a further intent is: (1) a new `RetrievalIntent` member, (2) a plan in `QueryPlanner`,
|
|
18
|
+
(3) a (usually trivial) resolver strategy — the executor is unchanged when the plan
|
|
19
|
+
composes existing steps.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from sourcecode.retrieval.context import KnowledgeContext
|
|
24
|
+
from sourcecode.retrieval.errors import (
|
|
25
|
+
IncompletePlanError,
|
|
26
|
+
KnowledgeUnavailableError,
|
|
27
|
+
RetrievalError,
|
|
28
|
+
StepContractError,
|
|
29
|
+
StepExecutionError,
|
|
30
|
+
UnknownDimensionError,
|
|
31
|
+
UnknownIntentError,
|
|
32
|
+
UnknownStepError,
|
|
33
|
+
)
|
|
34
|
+
from sourcecode.retrieval.executor import KnowledgeExecutor
|
|
35
|
+
from sourcecode.retrieval.planner import QueryPlanner
|
|
36
|
+
from sourcecode.retrieval.query import (
|
|
37
|
+
BuildResult,
|
|
38
|
+
Characterize,
|
|
39
|
+
CollectEvidence,
|
|
40
|
+
CollectImpactEvidence,
|
|
41
|
+
ComputeBlastRadius,
|
|
42
|
+
KnowledgeQuery,
|
|
43
|
+
RankByRisk,
|
|
44
|
+
RankByScore,
|
|
45
|
+
ResolveDependents,
|
|
46
|
+
ResolveEndpoints,
|
|
47
|
+
ResolveIntegrations,
|
|
48
|
+
ResolveModules,
|
|
49
|
+
ResolveTarget,
|
|
50
|
+
ResolveTransactions,
|
|
51
|
+
ResolveTransactionFlow,
|
|
52
|
+
ResolveTransactionEntrypoints,
|
|
53
|
+
ResolvePropagation,
|
|
54
|
+
ResolveSecuritySurface,
|
|
55
|
+
ResolveSecurityChain,
|
|
56
|
+
ResolveValidationPoints,
|
|
57
|
+
RankByCriticality,
|
|
58
|
+
CollectSecurityEvidence,
|
|
59
|
+
ResolveEndpointInventory,
|
|
60
|
+
ResolveIntegrationInventory,
|
|
61
|
+
ResolveEventTopology,
|
|
62
|
+
CollectInterfaceEvidence,
|
|
63
|
+
ResolveScope,
|
|
64
|
+
ResolveSubsystemInventory,
|
|
65
|
+
ResolveSubsystemMembers,
|
|
66
|
+
ResolveModuleDependents,
|
|
67
|
+
ResolveModuleDependencies,
|
|
68
|
+
ResolveTypeDependents,
|
|
69
|
+
CollectStructureEvidence,
|
|
70
|
+
ResolveEndpointRef,
|
|
71
|
+
ResolveEndpointContract,
|
|
72
|
+
ResolveEndpointSecurity,
|
|
73
|
+
ResolveCallNeighborhood,
|
|
74
|
+
CollectEndpointEvidence,
|
|
75
|
+
ResolveExecutionPaths,
|
|
76
|
+
ResolveTransactionsReaching,
|
|
77
|
+
ResolveSubsystemCoupling,
|
|
78
|
+
CollectGraphEvidence,
|
|
79
|
+
Step,
|
|
80
|
+
step_from_dict,
|
|
81
|
+
)
|
|
82
|
+
from sourcecode.retrieval.runtime import (
|
|
83
|
+
QueryPlanRuntime,
|
|
84
|
+
RuntimeReport,
|
|
85
|
+
RuntimeResult,
|
|
86
|
+
StepContext,
|
|
87
|
+
StepRegistry,
|
|
88
|
+
StepTrace,
|
|
89
|
+
)
|
|
90
|
+
from sourcecode.retrieval.steps import default_registry
|
|
91
|
+
from sourcecode.retrieval.request import (
|
|
92
|
+
RetrievalAnchor,
|
|
93
|
+
RetrievalIntent,
|
|
94
|
+
RetrievalOptions,
|
|
95
|
+
RetrievalRequest,
|
|
96
|
+
)
|
|
97
|
+
from sourcecode.retrieval.resolution import IntentResolver, ResolvedAnchor, ResolvedIntent
|
|
98
|
+
from sourcecode.retrieval.result import (
|
|
99
|
+
Explanation,
|
|
100
|
+
KnowledgeEntity,
|
|
101
|
+
KnowledgeResult,
|
|
102
|
+
Observation,
|
|
103
|
+
Provenance,
|
|
104
|
+
RetrievalEvidence,
|
|
105
|
+
)
|
|
106
|
+
from sourcecode.retrieval.retriever import SemanticRetriever
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
# façade
|
|
110
|
+
"SemanticRetriever",
|
|
111
|
+
# request
|
|
112
|
+
"RetrievalRequest",
|
|
113
|
+
"RetrievalIntent",
|
|
114
|
+
"RetrievalAnchor",
|
|
115
|
+
"RetrievalOptions",
|
|
116
|
+
# resolution
|
|
117
|
+
"IntentResolver",
|
|
118
|
+
"ResolvedIntent",
|
|
119
|
+
"ResolvedAnchor",
|
|
120
|
+
# plan
|
|
121
|
+
"QueryPlanner",
|
|
122
|
+
"KnowledgeQuery",
|
|
123
|
+
"Step",
|
|
124
|
+
"Characterize",
|
|
125
|
+
"RankByScore",
|
|
126
|
+
"CollectEvidence",
|
|
127
|
+
"BuildResult",
|
|
128
|
+
"ResolveTarget",
|
|
129
|
+
"ComputeBlastRadius",
|
|
130
|
+
"ResolveDependents",
|
|
131
|
+
"ResolveEndpoints",
|
|
132
|
+
"ResolveIntegrations",
|
|
133
|
+
"ResolveTransactions",
|
|
134
|
+
"ResolveModules",
|
|
135
|
+
"RankByRisk",
|
|
136
|
+
"CollectImpactEvidence",
|
|
137
|
+
"ResolveTransactionFlow",
|
|
138
|
+
"ResolveTransactionEntrypoints",
|
|
139
|
+
"ResolvePropagation",
|
|
140
|
+
"ResolveSecuritySurface",
|
|
141
|
+
"ResolveSecurityChain",
|
|
142
|
+
"ResolveValidationPoints",
|
|
143
|
+
"RankByCriticality",
|
|
144
|
+
"CollectSecurityEvidence",
|
|
145
|
+
"ResolveEndpointInventory",
|
|
146
|
+
"ResolveIntegrationInventory",
|
|
147
|
+
"ResolveEventTopology",
|
|
148
|
+
"CollectInterfaceEvidence",
|
|
149
|
+
"ResolveScope",
|
|
150
|
+
"ResolveSubsystemInventory",
|
|
151
|
+
"ResolveSubsystemMembers",
|
|
152
|
+
"ResolveModuleDependents",
|
|
153
|
+
"ResolveModuleDependencies",
|
|
154
|
+
"ResolveTypeDependents",
|
|
155
|
+
"CollectStructureEvidence",
|
|
156
|
+
"ResolveEndpointRef",
|
|
157
|
+
"ResolveEndpointContract",
|
|
158
|
+
"ResolveEndpointSecurity",
|
|
159
|
+
"ResolveCallNeighborhood",
|
|
160
|
+
"CollectEndpointEvidence",
|
|
161
|
+
"ResolveExecutionPaths",
|
|
162
|
+
"ResolveTransactionsReaching",
|
|
163
|
+
"ResolveSubsystemCoupling",
|
|
164
|
+
"CollectGraphEvidence",
|
|
165
|
+
# execution / runtime
|
|
166
|
+
"KnowledgeExecutor",
|
|
167
|
+
"KnowledgeContext",
|
|
168
|
+
"QueryPlanRuntime",
|
|
169
|
+
"StepRegistry",
|
|
170
|
+
"StepContext",
|
|
171
|
+
"StepTrace",
|
|
172
|
+
"RuntimeReport",
|
|
173
|
+
"RuntimeResult",
|
|
174
|
+
"default_registry",
|
|
175
|
+
"step_from_dict",
|
|
176
|
+
# result
|
|
177
|
+
"KnowledgeResult",
|
|
178
|
+
"KnowledgeEntity",
|
|
179
|
+
"RetrievalEvidence",
|
|
180
|
+
"Observation",
|
|
181
|
+
"Explanation",
|
|
182
|
+
"Provenance",
|
|
183
|
+
# errors
|
|
184
|
+
"RetrievalError",
|
|
185
|
+
"UnknownIntentError",
|
|
186
|
+
"UnknownStepError",
|
|
187
|
+
"UnknownDimensionError",
|
|
188
|
+
"IncompletePlanError",
|
|
189
|
+
"KnowledgeUnavailableError",
|
|
190
|
+
"StepExecutionError",
|
|
191
|
+
"StepContractError",
|
|
192
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""KnowledgeContext — the read-only bundle of ALREADY-BUILT knowledge (design §3.2, §4.1).
|
|
2
|
+
|
|
3
|
+
The retrieval layer never extracts. It receives knowledge here and only queries it.
|
|
4
|
+
Two ways to obtain a context:
|
|
5
|
+
|
|
6
|
+
* **Injected** — construct with pre-built handles (a `SourceMap`, a `ContextGraph`).
|
|
7
|
+
This is how the pipeline is unit-tested: no scan, no parse, provably no source access.
|
|
8
|
+
* **Acquired** — `KnowledgeContext.for_repo(root)` obtains existing knowledge using the
|
|
9
|
+
engine's OWN builders (the same AdaptiveScanner/ProjectDetector/GraphAnalyzer path the
|
|
10
|
+
`archetype` command uses, and the `context_cache` Knowledge Artifact Store for the
|
|
11
|
+
CIR). This is the acquisition BOUNDARY — it delegates to existing extractors; it adds
|
|
12
|
+
no retrieval-specific parser. The pipeline stages (resolver/planner/executor) never
|
|
13
|
+
call acquisition; they only read the handles exposed here.
|
|
14
|
+
|
|
15
|
+
`ContextGraph` / `SemanticServices` are acquired **lazily** via the Knowledge Artifact
|
|
16
|
+
Store so an intent that does not need them (like ARCHITECTURAL_CORE) pays nothing, while
|
|
17
|
+
the wiring is present and exercised for future anchored intents.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from time import perf_counter
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
24
|
+
|
|
25
|
+
from sourcecode.retrieval.errors import KnowledgeUnavailableError
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from sourcecode.context_graph import ContextGraph
|
|
29
|
+
from sourcecode.schema import SourceMap
|
|
30
|
+
from sourcecode.semantic_services import SemanticServices
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class KnowledgeContext:
|
|
34
|
+
"""Handles to already-built knowledge. Read-only from the pipeline's view."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
root: Optional[Path] = None,
|
|
40
|
+
repo_root: Optional[Path] = None,
|
|
41
|
+
source_map: "Optional[SourceMap]" = None,
|
|
42
|
+
module_graph: Any = None,
|
|
43
|
+
context_graph: "Optional[ContextGraph]" = None,
|
|
44
|
+
file_list: Optional[list[str]] = None,
|
|
45
|
+
repo_ir: Optional[dict] = None,
|
|
46
|
+
spring_model: object = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._root = root
|
|
49
|
+
self._repo_root = repo_root or root
|
|
50
|
+
self._source_map = source_map
|
|
51
|
+
self._module_graph = module_graph
|
|
52
|
+
# True once the SourceMap/module_graph pair has been acquired (or injected), so a
|
|
53
|
+
# legitimately-None module_graph is not re-acquired on every access.
|
|
54
|
+
self._source_map_ready = source_map is not None
|
|
55
|
+
self._context_graph = context_graph
|
|
56
|
+
self._file_list = file_list
|
|
57
|
+
self._repo_ir = repo_ir
|
|
58
|
+
self._spring_model = spring_model
|
|
59
|
+
self._services: "Optional[SemanticServices]" = None
|
|
60
|
+
self._cache_render: Optional[str] = None
|
|
61
|
+
# B2: acquisition instrumentation — ordered (name, wall-time-ms) records of every
|
|
62
|
+
# lazy knowledge acquisition this context performed. Cross-cutting infra: it lets
|
|
63
|
+
# the runtime report attribute the real cost of acquiring knowledge separately from
|
|
64
|
+
# the step that happened to trigger it, without the runtime knowing any intent.
|
|
65
|
+
self._acquisitions: list[tuple[str, float]] = []
|
|
66
|
+
|
|
67
|
+
# ── read handles ────────────────────────────────────────────────────────────
|
|
68
|
+
@property
|
|
69
|
+
def root(self) -> Optional[Path]:
|
|
70
|
+
return self._root
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def source_map(self) -> "SourceMap":
|
|
74
|
+
"""The built `SourceMap`, acquired lazily from the same AdaptiveScanner /
|
|
75
|
+
ProjectDetector / GraphAnalyzer path the `archetype` command uses. Acquired only
|
|
76
|
+
when an intent actually needs it (e.g. ARCHITECTURAL_*); tx / security / impact
|
|
77
|
+
intents never touch it and pay nothing for its ~7s construction (design B1)."""
|
|
78
|
+
if not self._source_map_ready:
|
|
79
|
+
self._acquire_source_map()
|
|
80
|
+
if self._source_map is None:
|
|
81
|
+
raise KnowledgeUnavailableError("no SourceMap was injected or acquired")
|
|
82
|
+
return self._source_map
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def module_graph(self) -> Any:
|
|
86
|
+
if not self._source_map_ready:
|
|
87
|
+
self._acquire_source_map()
|
|
88
|
+
return self._module_graph
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def context_graph(self) -> "ContextGraph":
|
|
92
|
+
"""The built ContextGraph, fetched lazily from the Knowledge Artifact Store
|
|
93
|
+
(cached CIR — no re-parse on a hit). Never built by the retrieval steps."""
|
|
94
|
+
if self._context_graph is None:
|
|
95
|
+
self._acquire_context_graph()
|
|
96
|
+
assert self._context_graph is not None
|
|
97
|
+
return self._context_graph
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def services(self) -> "SemanticServices":
|
|
101
|
+
if self._services is None:
|
|
102
|
+
from sourcecode.semantic_services import SemanticServices
|
|
103
|
+
|
|
104
|
+
t0 = perf_counter()
|
|
105
|
+
self._services = SemanticServices(self.context_graph)
|
|
106
|
+
self._record_acquisition("services", t0)
|
|
107
|
+
return self._services
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def repo_ir(self) -> dict:
|
|
111
|
+
"""The structural repo IR dict (from `build_repo_ir`), acquired lazily and shared
|
|
112
|
+
across steps. Same builder the legacy `impact` command uses — the retrieval layer
|
|
113
|
+
introduces no parser; it reuses this one. Built at most once per context."""
|
|
114
|
+
if self._repo_ir is None:
|
|
115
|
+
self._acquire_repo_ir()
|
|
116
|
+
assert self._repo_ir is not None
|
|
117
|
+
return self._repo_ir
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def spring_model(self) -> object:
|
|
121
|
+
"""The `SpringSemanticModel` (tx index, endpoint index, call adjacency, bean/event
|
|
122
|
+
graphs), built lazily over the acquired ContextGraph's CIR — the SAME builder the
|
|
123
|
+
`spring-audit` command uses. Reused, never re-derived. Built at most once."""
|
|
124
|
+
if self._spring_model is None:
|
|
125
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
126
|
+
|
|
127
|
+
t0 = perf_counter()
|
|
128
|
+
self._spring_model = SpringSemanticModel.build(self.context_graph.cir)
|
|
129
|
+
self._record_acquisition("spring_model", t0)
|
|
130
|
+
return self._spring_model
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def cache_render(self) -> str:
|
|
134
|
+
"""Human-free provenance of how the CIR was obtained (cache HIT/MISS/disabled),
|
|
135
|
+
or "not acquired" when the ContextGraph was never needed."""
|
|
136
|
+
return self._cache_render or "not acquired"
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def acquisitions(self) -> tuple[tuple[str, float], ...]:
|
|
140
|
+
"""Ordered (name, wall-time-ms) records of the knowledge acquisitions performed
|
|
141
|
+
(B2). Empty when everything was injected (no acquisition happened)."""
|
|
142
|
+
return tuple(self._acquisitions)
|
|
143
|
+
|
|
144
|
+
def _record_acquisition(self, name: str, t0: float) -> None:
|
|
145
|
+
self._acquisitions.append((name, (perf_counter() - t0) * 1000.0))
|
|
146
|
+
|
|
147
|
+
# ── lazy ContextGraph acquisition (Knowledge Artifact Store) ────────────────
|
|
148
|
+
def _acquire_context_graph(self) -> None:
|
|
149
|
+
if self._root is None:
|
|
150
|
+
raise KnowledgeUnavailableError(
|
|
151
|
+
"cannot acquire a ContextGraph without a repository root"
|
|
152
|
+
)
|
|
153
|
+
from sourcecode.context_cache import get_or_build_cir
|
|
154
|
+
from sourcecode.context_graph import ContextGraph
|
|
155
|
+
from sourcecode.repository_ir import find_java_files
|
|
156
|
+
|
|
157
|
+
t0 = perf_counter()
|
|
158
|
+
files = self._file_list if self._file_list is not None else find_java_files(self._root)
|
|
159
|
+
cir, lookup = get_or_build_cir(self._repo_root or self._root, self._root, files)
|
|
160
|
+
self._context_graph = ContextGraph.from_cir(cir)
|
|
161
|
+
self._cache_render = lookup.render()
|
|
162
|
+
self._record_acquisition("context_graph", t0)
|
|
163
|
+
|
|
164
|
+
def _acquire_source_map(self) -> None:
|
|
165
|
+
if self._root is None:
|
|
166
|
+
raise KnowledgeUnavailableError(
|
|
167
|
+
"cannot acquire a SourceMap without a repository root"
|
|
168
|
+
)
|
|
169
|
+
t0 = perf_counter()
|
|
170
|
+
self._source_map, self._module_graph = _acquire_source_map(self._root)
|
|
171
|
+
self._source_map_ready = True
|
|
172
|
+
self._record_acquisition("source_map", t0)
|
|
173
|
+
|
|
174
|
+
def _acquire_repo_ir(self) -> None:
|
|
175
|
+
if self._root is None:
|
|
176
|
+
raise KnowledgeUnavailableError("cannot acquire a repo IR without a repository root")
|
|
177
|
+
from sourcecode.repository_ir import build_repo_ir, find_java_files
|
|
178
|
+
|
|
179
|
+
t0 = perf_counter()
|
|
180
|
+
files = self._file_list if self._file_list is not None else find_java_files(self._root)
|
|
181
|
+
self._repo_ir = build_repo_ir(files, self._root)
|
|
182
|
+
self._record_acquisition("repo_ir", t0)
|
|
183
|
+
|
|
184
|
+
# ── acquisition boundary ────────────────────────────────────────────────────
|
|
185
|
+
@classmethod
|
|
186
|
+
def for_repo(cls, root: Path) -> "KnowledgeContext":
|
|
187
|
+
"""Obtain existing knowledge for a repository via the engine's own builders.
|
|
188
|
+
|
|
189
|
+
This mirrors the `archetype` command's SourceMap construction exactly — it
|
|
190
|
+
reuses AdaptiveScanner / ProjectDetector / GraphAnalyzer; it does not introduce
|
|
191
|
+
a retrieval-specific parser. Both the SourceMap and the ContextGraph are left for
|
|
192
|
+
lazy acquisition — built only if an intent actually needs them (design B1).
|
|
193
|
+
"""
|
|
194
|
+
target = Path(root).resolve()
|
|
195
|
+
return cls(root=target, repo_root=target)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _acquire_source_map(target: Path) -> "tuple[SourceMap, Any]":
|
|
199
|
+
"""Build a SourceMap + module graph using the existing analyzers (acquisition
|
|
200
|
+
boundary). Best-effort on the module graph, exactly like the `archetype` command:
|
|
201
|
+
on any failure it degrades to name-mass-only (module_graph=None)."""
|
|
202
|
+
from sourcecode.adaptive_scanner import AdaptiveScanner
|
|
203
|
+
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
204
|
+
from sourcecode.repo_classifier import RepoClassifier
|
|
205
|
+
from sourcecode.schema import SourceMap
|
|
206
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
207
|
+
|
|
208
|
+
topology = RepoClassifier().classify(target)
|
|
209
|
+
scanner = AdaptiveScanner(target, topology=topology, base_depth=12)
|
|
210
|
+
file_tree = scanner.scan_tree()
|
|
211
|
+
manifests = scanner.find_manifests()
|
|
212
|
+
|
|
213
|
+
detector = ProjectDetector(build_default_detectors())
|
|
214
|
+
stacks, entry_points, _ = detector.detect(target, file_tree, manifests)
|
|
215
|
+
stacks, project_type = detector.classify_results(file_tree, stacks, entry_points)
|
|
216
|
+
|
|
217
|
+
sm = SourceMap(
|
|
218
|
+
file_tree=file_tree,
|
|
219
|
+
stacks=stacks,
|
|
220
|
+
project_type=project_type,
|
|
221
|
+
entry_points=entry_points,
|
|
222
|
+
)
|
|
223
|
+
sm.file_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
|
|
224
|
+
java = next((s for s in stacks if s.stack == "java"), None)
|
|
225
|
+
if java is not None:
|
|
226
|
+
sm.packaging = getattr(java, "packaging", None)
|
|
227
|
+
|
|
228
|
+
module_graph = None
|
|
229
|
+
try:
|
|
230
|
+
from sourcecode.graph_analyzer import GraphAnalyzer
|
|
231
|
+
|
|
232
|
+
module_graph = GraphAnalyzer(max_nodes=1200, max_edges=4000).analyze(
|
|
233
|
+
target, file_tree, detail="full", entry_points=entry_points,
|
|
234
|
+
)
|
|
235
|
+
if module_graph is not None:
|
|
236
|
+
sm.module_graph = module_graph
|
|
237
|
+
sm.module_graph_summary = module_graph.summary
|
|
238
|
+
except Exception:
|
|
239
|
+
module_graph = None
|
|
240
|
+
|
|
241
|
+
return sm, module_graph
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Typed errors for the Semantic Retrieval Engine.
|
|
2
|
+
|
|
3
|
+
All are programming/wiring faults (an unknown intent, a plan the executor cannot
|
|
4
|
+
interpret) — never a "no result" signal. Absence of knowledge is expressed in the
|
|
5
|
+
typed KnowledgeResult (empty entities, `signals_missing`), not by raising.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RetrievalError(Exception):
|
|
11
|
+
"""Base class for every retrieval-layer fault."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnknownIntentError(RetrievalError):
|
|
15
|
+
"""No resolver/planner strategy is registered for the requested intent."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnknownStepError(RetrievalError):
|
|
19
|
+
"""The executor has no handler for a step type in the plan (plan/executor drift)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnknownDimensionError(RetrievalError):
|
|
23
|
+
"""A Characterize step named an archetype dimension the engine does not produce."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IncompletePlanError(RetrievalError):
|
|
27
|
+
"""A plan finished without producing a KnowledgeResult (missing BuildResult step)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KnowledgeUnavailableError(RetrievalError):
|
|
31
|
+
"""A required knowledge handle was neither injected nor acquirable."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StepExecutionError(RetrievalError):
|
|
35
|
+
"""A step handler raised an unexpected (non-retrieval) error while running. Wraps
|
|
36
|
+
the original cause; the failing step is recorded in the runtime trace."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StepContractError(RetrievalError):
|
|
40
|
+
"""A step required a shared-context slot that a prior step did not produce (a plan
|
|
41
|
+
composed steps in an invalid order)."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""KnowledgeExecutor — thin façade over the Query Plan Runtime (design §4.5).
|
|
2
|
+
|
|
3
|
+
Fase 4 moved all execution logic into `QueryPlanRuntime` + the step handlers in
|
|
4
|
+
`steps.py`. This façade preserves the stable public surface: `execute()` returns just the
|
|
5
|
+
typed `KnowledgeResult` (as consumers and Phase-2 tests expect), while `run()` exposes the
|
|
6
|
+
full `RuntimeResult` (result + execution report with per-step trace and profiling).
|
|
7
|
+
|
|
8
|
+
The executor owns no execution logic — it delegates to the Runtime. That is why adding an
|
|
9
|
+
intent never touches this file.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from sourcecode.retrieval.context import KnowledgeContext
|
|
16
|
+
from sourcecode.retrieval.query import KnowledgeQuery
|
|
17
|
+
from sourcecode.retrieval.request import RetrievalOptions
|
|
18
|
+
from sourcecode.retrieval.resolution import ResolvedIntent
|
|
19
|
+
from sourcecode.retrieval.result import KnowledgeResult
|
|
20
|
+
from sourcecode.retrieval.runtime import QueryPlanRuntime, RuntimeResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KnowledgeExecutor:
|
|
24
|
+
"""Runs a `KnowledgeQuery` over a `KnowledgeContext` via the Query Plan Runtime."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, runtime: Optional[QueryPlanRuntime] = None) -> None:
|
|
27
|
+
self._runtime = runtime or QueryPlanRuntime()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def runtime(self) -> QueryPlanRuntime:
|
|
31
|
+
return self._runtime
|
|
32
|
+
|
|
33
|
+
def run(
|
|
34
|
+
self,
|
|
35
|
+
query: KnowledgeQuery,
|
|
36
|
+
ctx: KnowledgeContext,
|
|
37
|
+
resolved: Optional[ResolvedIntent] = None,
|
|
38
|
+
options: Optional[RetrievalOptions] = None,
|
|
39
|
+
) -> RuntimeResult:
|
|
40
|
+
"""Execute and return the full runtime result (typed result + execution report)."""
|
|
41
|
+
return self._runtime.run(query, ctx, resolved, options)
|
|
42
|
+
|
|
43
|
+
def execute(
|
|
44
|
+
self,
|
|
45
|
+
query: KnowledgeQuery,
|
|
46
|
+
ctx: KnowledgeContext,
|
|
47
|
+
resolved: Optional[ResolvedIntent] = None,
|
|
48
|
+
options: Optional[RetrievalOptions] = None,
|
|
49
|
+
) -> KnowledgeResult:
|
|
50
|
+
"""Execute and return only the typed `KnowledgeResult`."""
|
|
51
|
+
return self._runtime.run(query, ctx, resolved, options).result
|