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.
@@ -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
- "@M3FiltroSeguridad") whose argument lists must be stored in annotation_values
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("*") 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