sourcecode 2.3.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 +491 -4
- sourcecode/context_cache.py +637 -0
- sourcecode/context_graph.py +32 -24
- sourcecode/explain.py +24 -0
- 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.3.0.dist-info → sourcecode-2.5.0.dist-info}/METADATA +4 -4
- {sourcecode-2.3.0.dist-info → sourcecode-2.5.0.dist-info}/RECORD +31 -12
- {sourcecode-2.3.0.dist-info → sourcecode-2.5.0.dist-info}/WHEEL +0 -0
- {sourcecode-2.3.0.dist-info → sourcecode-2.5.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.3.0.dist-info → sourcecode-2.5.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/context_graph.py
CHANGED
|
@@ -407,7 +407,7 @@ class ContextGraph:
|
|
|
407
407
|
__slots__ = (
|
|
408
408
|
"_cir", "_nodes_by_fqn", "_nodes", "_build_ms",
|
|
409
409
|
"_body_index", "_literal_index", "_guard_index", "_span_index",
|
|
410
|
-
"_class_type_index",
|
|
410
|
+
"_class_type_index", "_relations_all", "_symbols_sorted",
|
|
411
411
|
)
|
|
412
412
|
|
|
413
413
|
def __init__(self, cir: CanonicalRepositoryIR, *, build_ms: float = 0.0) -> None:
|
|
@@ -431,6 +431,14 @@ class ContextGraph:
|
|
|
431
431
|
_node_to_symbol(n) for n in raw_nodes
|
|
432
432
|
)
|
|
433
433
|
self._nodes_by_fqn: dict[str, Symbol] = {s.fqn: s for s in self._nodes}
|
|
434
|
+
# Memoized full projections (the CIR is immutable for this graph's lifetime).
|
|
435
|
+
# `relations()`/`symbols()` are called once PER FILE by per-file consumers such as
|
|
436
|
+
# `_java_contract_from_graph`; without these caches each call re-converted every
|
|
437
|
+
# edge (`_edge_to_relation`) and re-sorted every node — an O(files × edges) blow-up
|
|
438
|
+
# that made cold `ask` / `ask --agent` take minutes on large repos. Built once,
|
|
439
|
+
# filtered cheaply thereafter.
|
|
440
|
+
self._relations_all: Optional[tuple[Relation, ...]] = None
|
|
441
|
+
self._symbols_sorted: Optional[tuple[Symbol, ...]] = None
|
|
434
442
|
|
|
435
443
|
# -- construction -------------------------------------------------------
|
|
436
444
|
|
|
@@ -497,19 +505,18 @@ class ContextGraph:
|
|
|
497
505
|
set. Results are sorted by FQN for determinism. This is the primitive
|
|
498
506
|
the role-convenience methods below are thin sugar over.
|
|
499
507
|
"""
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return out
|
|
508
|
+
if self._symbols_sorted is None:
|
|
509
|
+
self._symbols_sorted = tuple(sorted(self._nodes, key=lambda s: s.fqn))
|
|
510
|
+
# Filter over the memoized fqn-sorted set (filtering preserves that order, so the
|
|
511
|
+
# result is identical to sorting after filtering — but the sort happens once).
|
|
512
|
+
return [
|
|
513
|
+
s
|
|
514
|
+
for s in self._symbols_sorted
|
|
515
|
+
if (kind is None or s.kind == kind)
|
|
516
|
+
and (role is None or s.role == role)
|
|
517
|
+
and (annotated_with is None or s.has_annotation(annotated_with))
|
|
518
|
+
and (name_contains is None or name_contains in s.fqn)
|
|
519
|
+
]
|
|
513
520
|
|
|
514
521
|
def types(self) -> list[Symbol]:
|
|
515
522
|
"""All class/interface/enum/annotation symbols, sorted by FQN."""
|
|
@@ -575,16 +582,17 @@ class ContextGraph:
|
|
|
575
582
|
) -> list[Relation]:
|
|
576
583
|
"""All relations matching the given filters (AND-combined), in the IR's
|
|
577
584
|
stable edge order (already sorted from → type → to)."""
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
585
|
+
if self._relations_all is None:
|
|
586
|
+
# Convert every edge ONCE; filtered calls then scan the cached Relation
|
|
587
|
+
# objects without re-running `_edge_to_relation` (the cold-path hotspot).
|
|
588
|
+
self._relations_all = tuple(_edge_to_relation(e) for e in self._cir.call_graph)
|
|
589
|
+
return [
|
|
590
|
+
r
|
|
591
|
+
for r in self._relations_all
|
|
592
|
+
if (kind is None or r.kind == kind)
|
|
593
|
+
and (source is None or r.source == source)
|
|
594
|
+
and (target is None or r.target == target)
|
|
595
|
+
]
|
|
588
596
|
|
|
589
597
|
# -- inheritance / implementation (reuses cir.implementation_graph) ------
|
|
590
598
|
|
sourcecode/explain.py
CHANGED
|
@@ -133,6 +133,30 @@ class ClassExplanation:
|
|
|
133
133
|
"found": self.found,
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_dict(cls, d: dict) -> "ClassExplanation":
|
|
138
|
+
"""Reconstruct from :meth:`to_dict` output (lossless round-trip).
|
|
139
|
+
|
|
140
|
+
The informational ``incoming_callers_note`` key is derived, not stored
|
|
141
|
+
on the instance, so it is dropped on the way back in.
|
|
142
|
+
"""
|
|
143
|
+
return cls(
|
|
144
|
+
class_name=d.get("class_name", ""),
|
|
145
|
+
class_fqn=d.get("class_fqn", ""),
|
|
146
|
+
stereotype=d.get("stereotype", ""),
|
|
147
|
+
purpose=d.get("purpose", ""),
|
|
148
|
+
public_methods=list(d.get("public_methods", [])),
|
|
149
|
+
incoming_callers=list(d.get("incoming_callers", [])),
|
|
150
|
+
outgoing_deps=list(d.get("outgoing_deps", [])),
|
|
151
|
+
events_published=list(d.get("events_published", [])),
|
|
152
|
+
events_consumed=list(d.get("events_consumed", [])),
|
|
153
|
+
transactions=list(d.get("transactions", [])),
|
|
154
|
+
security_constraints=list(d.get("security_constraints", [])),
|
|
155
|
+
rest_endpoints=list(d.get("rest_endpoints", [])),
|
|
156
|
+
warnings=list(d.get("warnings", [])),
|
|
157
|
+
found=bool(d.get("found", True)),
|
|
158
|
+
)
|
|
159
|
+
|
|
136
160
|
|
|
137
161
|
# ---------------------------------------------------------------------------
|
|
138
162
|
# FQN resolution
|
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).
|
|
@@ -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
|