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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""IntentResolver — bind a request's raw anchors to STRUCTURED graph identities.
|
|
2
|
+
|
|
3
|
+
Single responsibility (design §4.2): name/scope → structured identity. It plans
|
|
4
|
+
nothing and executes nothing. Resolution uses structured indexes (`types_by_simple`,
|
|
5
|
+
module ids, integration scheme vocabulary) — NEVER fuzzy text search. An anchor that
|
|
6
|
+
cannot be bound is marked `unresolved`; the resolver never guesses.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Callable
|
|
12
|
+
|
|
13
|
+
from sourcecode.retrieval.errors import UnknownIntentError
|
|
14
|
+
from sourcecode.retrieval.request import RetrievalAnchor, RetrievalIntent, RetrievalRequest
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # avoid importing the knowledge layer at module load
|
|
17
|
+
from sourcecode.retrieval.context import KnowledgeContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ResolvedAnchor:
|
|
22
|
+
"""A raw anchor bound to zero or more structured identities. Ambiguity is kept
|
|
23
|
+
(multiple identities), never silently collapsed."""
|
|
24
|
+
|
|
25
|
+
anchor: RetrievalAnchor
|
|
26
|
+
identities: tuple[str, ...]
|
|
27
|
+
unresolved_reason: str | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def resolved(self) -> bool:
|
|
31
|
+
return self.unresolved_reason is None and bool(self.identities)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ResolvedIntent:
|
|
36
|
+
"""The intent plus its bound anchors, ready for planning."""
|
|
37
|
+
|
|
38
|
+
intent: RetrievalIntent
|
|
39
|
+
anchors: tuple[ResolvedAnchor, ...] = ()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def fully_resolved(self) -> bool:
|
|
43
|
+
return all(a.resolved for a in self.anchors)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class IntentResolver:
|
|
47
|
+
"""Resolves a `RetrievalRequest` into a `ResolvedIntent`.
|
|
48
|
+
|
|
49
|
+
Per-intent strategies live in a registry so adding an intent adds a strategy
|
|
50
|
+
(often the trivial no-anchor one) without touching the resolver's core.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._strategies: dict[
|
|
55
|
+
RetrievalIntent, Callable[[RetrievalRequest, "KnowledgeContext"], ResolvedIntent]
|
|
56
|
+
] = {
|
|
57
|
+
# The archetype-dimension intents characterize the WHOLE repository — no
|
|
58
|
+
# anchor to bind. Both reuse the same (trivial) resolution strategy.
|
|
59
|
+
RetrievalIntent.ARCHITECTURAL_CORE: self._resolve_no_anchors,
|
|
60
|
+
RetrievalIntent.ARCHITECTURAL_STYLE: self._resolve_no_anchors,
|
|
61
|
+
RetrievalIntent.DEPLOYMENT_SHAPE: self._resolve_no_anchors,
|
|
62
|
+
RetrievalIntent.PRIMARY_INTERFACE: self._resolve_no_anchors,
|
|
63
|
+
# IMPACT pack: the blast engine does its own (legacy-identical) resolution of
|
|
64
|
+
# the target token, so anchors are passed through structurally (no text search,
|
|
65
|
+
# no premature ContextGraph build). ResolveTarget picks the target at runtime.
|
|
66
|
+
RetrievalIntent.BLAST_RADIUS: self._resolve_passthrough,
|
|
67
|
+
RetrievalIntent.AFFECTED_ENDPOINTS: self._resolve_passthrough,
|
|
68
|
+
RetrievalIntent.AFFECTED_INTEGRATIONS: self._resolve_passthrough,
|
|
69
|
+
RetrievalIntent.TRANSACTION_BOUNDARIES: self._resolve_passthrough,
|
|
70
|
+
RetrievalIntent.CROSS_MODULE_IMPACT: self._resolve_passthrough,
|
|
71
|
+
# TRANSACTIONS & SECURITY: whole-repo questions need no anchor; SECURITY_CHAIN
|
|
72
|
+
# scopes to a target symbol (passed through, resolved by the security engine).
|
|
73
|
+
RetrievalIntent.TRANSACTION_FLOW: self._resolve_no_anchors,
|
|
74
|
+
RetrievalIntent.TRANSACTION_ENTRYPOINTS: self._resolve_no_anchors,
|
|
75
|
+
RetrievalIntent.TRANSACTION_PROPAGATION: self._resolve_no_anchors,
|
|
76
|
+
RetrievalIntent.SECURITY_SURFACE: self._resolve_no_anchors,
|
|
77
|
+
RetrievalIntent.SECURITY_CHAIN: self._resolve_passthrough,
|
|
78
|
+
RetrievalIntent.VALIDATION_POINTS: self._resolve_no_anchors,
|
|
79
|
+
# INTERFACES & INTEGRATIONS: whole-repo inventories, no anchor to bind.
|
|
80
|
+
RetrievalIntent.ENDPOINT_INVENTORY: self._resolve_no_anchors,
|
|
81
|
+
RetrievalIntent.INTEGRATION_INVENTORY: self._resolve_no_anchors,
|
|
82
|
+
RetrievalIntent.EVENT_TOPOLOGY: self._resolve_no_anchors,
|
|
83
|
+
# SUBSYSTEMS & DEPENDENCIES: inventory is whole-repo; the scoped intents pass
|
|
84
|
+
# their subsystem/module/package anchor through (matched structurally by the
|
|
85
|
+
# projection step against the partition / graph node ids).
|
|
86
|
+
RetrievalIntent.SUBSYSTEM_INVENTORY: self._resolve_no_anchors,
|
|
87
|
+
RetrievalIntent.SUBSYSTEM_MEMBERS: self._resolve_passthrough,
|
|
88
|
+
RetrievalIntent.MODULE_DEPENDENTS: self._resolve_passthrough,
|
|
89
|
+
RetrievalIntent.MODULE_DEPENDENCIES: self._resolve_passthrough,
|
|
90
|
+
RetrievalIntent.TYPE_DEPENDENTS: self._resolve_passthrough,
|
|
91
|
+
# ENDPOINT & CALL deep-dive: all anchored (endpoint / path / symbol), passed
|
|
92
|
+
# through and matched structurally by the projection step.
|
|
93
|
+
RetrievalIntent.ENDPOINT_CONTRACT: self._resolve_passthrough,
|
|
94
|
+
RetrievalIntent.ENDPOINT_SECURITY: self._resolve_passthrough,
|
|
95
|
+
RetrievalIntent.CALL_NEIGHBORHOOD: self._resolve_passthrough,
|
|
96
|
+
# GRAPH-analysis: the two path/reachability intents pass their symbol through;
|
|
97
|
+
# subsystem-coupling is whole-repo (no anchor).
|
|
98
|
+
RetrievalIntent.EXECUTION_PATHS_THROUGH: self._resolve_passthrough,
|
|
99
|
+
RetrievalIntent.TRANSACTIONS_REACHING: self._resolve_passthrough,
|
|
100
|
+
RetrievalIntent.SUBSYSTEM_COUPLING: self._resolve_no_anchors,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def resolve(self, request: RetrievalRequest, ctx: "KnowledgeContext") -> ResolvedIntent:
|
|
104
|
+
strategy = self._strategies.get(request.intent)
|
|
105
|
+
if strategy is None:
|
|
106
|
+
raise UnknownIntentError(f"no resolver strategy for intent {request.intent!r}")
|
|
107
|
+
return strategy(request, ctx)
|
|
108
|
+
|
|
109
|
+
# ── strategies ─────────────────────────────────────────────────────────────
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _resolve_no_anchors(
|
|
112
|
+
request: RetrievalRequest, ctx: "KnowledgeContext"
|
|
113
|
+
) -> ResolvedIntent:
|
|
114
|
+
return ResolvedIntent(request.intent, ())
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _resolve_passthrough(
|
|
118
|
+
request: RetrievalRequest, ctx: "KnowledgeContext"
|
|
119
|
+
) -> ResolvedIntent:
|
|
120
|
+
"""Bind each anchor to its own raw token as the identity (structural passthrough).
|
|
121
|
+
No fuzzy search, no guessing; the downstream engine resolves the token."""
|
|
122
|
+
return ResolvedIntent(
|
|
123
|
+
request.intent,
|
|
124
|
+
tuple(ResolvedAnchor(a, (a.raw,)) for a in request.anchors),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ── structured binding helpers (for future anchored intents) ────────────────
|
|
128
|
+
# Kept here to fix the ONE sanctioned resolution mechanism: a structured index
|
|
129
|
+
# lookup, never a text scan. Not exercised by the first (anchor-free) intent.
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _bind_symbol(anchor: RetrievalAnchor, ctx: "KnowledgeContext") -> ResolvedAnchor:
|
|
132
|
+
fqns = tuple(sorted(ctx.services.types_by_simple().get(anchor.raw, ())))
|
|
133
|
+
if not fqns:
|
|
134
|
+
return ResolvedAnchor(anchor, (), unresolved_reason=f"unknown symbol {anchor.raw!r}")
|
|
135
|
+
return ResolvedAnchor(anchor, fqns)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""KnowledgeResult and its typed parts (design §4.6, §5.3).
|
|
2
|
+
|
|
3
|
+
Everything the engine returns is **structured** — no prose, no generated natural
|
|
4
|
+
language. The one free-text field, `Explanation.engine_explanation`, is passed through
|
|
5
|
+
VERBATIM from the reused engine; the retriever synthesizes no sentences of its own.
|
|
6
|
+
Every object is JSON-serializable via `to_dict()`.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RetrievalEvidence:
|
|
16
|
+
"""Structured evidence for a result, mirroring the ADR-0003 evidence contract
|
|
17
|
+
(signal / weight / strength / mass / contribution). It CAN carry a
|
|
18
|
+
``source_file:line`` grounding when the backing fact is span-anchored; for the
|
|
19
|
+
archetype signals it is ``None`` (they are mass-derived, not span-anchored)."""
|
|
20
|
+
|
|
21
|
+
signal: str
|
|
22
|
+
detail: str
|
|
23
|
+
weight: float
|
|
24
|
+
strength: float
|
|
25
|
+
mass: float
|
|
26
|
+
contribution: float
|
|
27
|
+
provider: str
|
|
28
|
+
source_file: str | None = None
|
|
29
|
+
source_line: int | None = None
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"signal": self.signal,
|
|
34
|
+
"detail": self.detail,
|
|
35
|
+
"weight": self.weight,
|
|
36
|
+
"strength": round(self.strength, 3),
|
|
37
|
+
"mass": round(self.mass, 4),
|
|
38
|
+
"contribution": round(self.contribution, 4),
|
|
39
|
+
"provider": self.provider,
|
|
40
|
+
"source_file": self.source_file,
|
|
41
|
+
"source_line": self.source_line,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class KnowledgeEntity:
|
|
47
|
+
"""One structured hit. `identity` is a graph identity (label / FQN / id), never
|
|
48
|
+
prose; `attributes` carries structured extras."""
|
|
49
|
+
|
|
50
|
+
identity: str
|
|
51
|
+
kind: str
|
|
52
|
+
score: float
|
|
53
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"identity": self.identity,
|
|
58
|
+
"kind": self.kind,
|
|
59
|
+
"score": round(self.score, 4),
|
|
60
|
+
"attributes": self.attributes,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Observation:
|
|
66
|
+
"""A structured, named measurement reused from a Knowledge-Layer provider
|
|
67
|
+
(e.g. a module-graph topology metric). Not a hit; supporting context."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
value: Any
|
|
71
|
+
detail: str = ""
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
return {"name": self.name, "value": self.value, "detail": self.detail}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class Provenance:
|
|
79
|
+
"""Which existing capabilities produced this result, and what knowledge state
|
|
80
|
+
backed it. Makes every answer auditable back to the reused engines."""
|
|
81
|
+
|
|
82
|
+
services: tuple[str, ...]
|
|
83
|
+
signals_used: tuple[str, ...]
|
|
84
|
+
signals_missing: tuple[str, ...]
|
|
85
|
+
knowledge_source: str
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"services": list(self.services),
|
|
90
|
+
"signals_used": list(self.signals_used),
|
|
91
|
+
"signals_missing": list(self.signals_missing),
|
|
92
|
+
"knowledge_source": self.knowledge_source,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class Explanation:
|
|
98
|
+
"""Structured, machine-readable rationale. `engine_explanation` is verbatim from
|
|
99
|
+
the reused engine — the retriever generates NO natural language of its own."""
|
|
100
|
+
|
|
101
|
+
winner: str | None
|
|
102
|
+
score: float
|
|
103
|
+
confidence: str
|
|
104
|
+
top_contributions: tuple[tuple[str, float], ...]
|
|
105
|
+
runner_up: str | None
|
|
106
|
+
engine_explanation: str
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> dict[str, Any]:
|
|
109
|
+
return {
|
|
110
|
+
"winner": self.winner,
|
|
111
|
+
"score": round(self.score, 4),
|
|
112
|
+
"confidence": self.confidence,
|
|
113
|
+
"top_contributions": [
|
|
114
|
+
{"signal": s, "contribution": round(c, 4)} for s, c in self.top_contributions
|
|
115
|
+
],
|
|
116
|
+
"runner_up": self.runner_up,
|
|
117
|
+
"engine_explanation": self.engine_explanation,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class KnowledgeResult:
|
|
123
|
+
"""The typed answer to a `RetrievalRequest`. Fully structured; no prose."""
|
|
124
|
+
|
|
125
|
+
intent: str
|
|
126
|
+
entities: tuple[KnowledgeEntity, ...]
|
|
127
|
+
evidence: tuple[RetrievalEvidence, ...]
|
|
128
|
+
confidence: str
|
|
129
|
+
observations: tuple[Observation, ...]
|
|
130
|
+
explanation: Explanation
|
|
131
|
+
provenance: Provenance
|
|
132
|
+
query: tuple[str, ...] # executed plan, as step names (traceability)
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict[str, Any]:
|
|
135
|
+
return {
|
|
136
|
+
"intent": self.intent,
|
|
137
|
+
"entities": [e.to_dict() for e in self.entities],
|
|
138
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
139
|
+
"confidence": self.confidence,
|
|
140
|
+
"observations": [o.to_dict() for o in self.observations],
|
|
141
|
+
"explanation": self.explanation.to_dict(),
|
|
142
|
+
"provenance": self.provenance.to_dict(),
|
|
143
|
+
"query": list(self.query),
|
|
144
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""SemanticRetriever — the façade / orchestrator (design §4.1).
|
|
2
|
+
|
|
3
|
+
The single public entry point. It wires `IntentResolver → QueryPlanner →
|
|
4
|
+
KnowledgeExecutor` over a `KnowledgeContext` and returns a typed `KnowledgeResult`. It
|
|
5
|
+
performs NO graph logic itself — it is composition only, so consumers depend on one
|
|
6
|
+
stable surface and never on the internal stages.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from sourcecode.retrieval.context import KnowledgeContext
|
|
13
|
+
from sourcecode.retrieval.executor import KnowledgeExecutor
|
|
14
|
+
from sourcecode.retrieval.planner import QueryPlanner
|
|
15
|
+
from sourcecode.retrieval.query import KnowledgeQuery
|
|
16
|
+
from sourcecode.retrieval.request import RetrievalRequest
|
|
17
|
+
from sourcecode.retrieval.resolution import IntentResolver
|
|
18
|
+
from sourcecode.retrieval.result import KnowledgeResult
|
|
19
|
+
from sourcecode.retrieval.runtime import RuntimeResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SemanticRetriever:
|
|
23
|
+
"""Orchestrates the retrieval pipeline over a bundle of already-built knowledge."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
context: KnowledgeContext,
|
|
28
|
+
*,
|
|
29
|
+
resolver: IntentResolver | None = None,
|
|
30
|
+
planner: QueryPlanner | None = None,
|
|
31
|
+
executor: KnowledgeExecutor | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._ctx = context
|
|
34
|
+
self._resolver = resolver or IntentResolver()
|
|
35
|
+
self._planner = planner or QueryPlanner()
|
|
36
|
+
self._executor = executor or KnowledgeExecutor()
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def for_repo(cls, root: Path) -> "SemanticRetriever":
|
|
40
|
+
"""Convenience constructor: acquire existing knowledge for `root` (the
|
|
41
|
+
acquisition boundary in `KnowledgeContext.for_repo`) and wire the pipeline."""
|
|
42
|
+
return cls(KnowledgeContext.for_repo(Path(root)))
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def context(self) -> KnowledgeContext:
|
|
46
|
+
return self._ctx
|
|
47
|
+
|
|
48
|
+
def plan(self, request: RetrievalRequest) -> KnowledgeQuery:
|
|
49
|
+
"""Resolve + plan WITHOUT executing — for explaining "which plan would run"."""
|
|
50
|
+
resolved = self._resolver.resolve(request, self._ctx)
|
|
51
|
+
return self._planner.plan(resolved)
|
|
52
|
+
|
|
53
|
+
def retrieve(self, request: RetrievalRequest) -> KnowledgeResult:
|
|
54
|
+
"""Run the full pipeline and return the typed result."""
|
|
55
|
+
return self.run(request).result
|
|
56
|
+
|
|
57
|
+
def run(self, request: RetrievalRequest) -> RuntimeResult:
|
|
58
|
+
"""Run the full pipeline and return the typed result plus the execution report
|
|
59
|
+
(per-step trace + profiling + Knowledge-Cache status)."""
|
|
60
|
+
resolved = self._resolver.resolve(request, self._ctx)
|
|
61
|
+
query = self._planner.plan(resolved)
|
|
62
|
+
return self._executor.run(query, self._ctx, resolved, request.options)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Query Plan Runtime — the engine that executes ANY plan of registered Steps.
|
|
2
|
+
|
|
3
|
+
This is the definitive Semantic Retrieval infrastructure. It is **generic**: it knows how
|
|
4
|
+
to sequence steps, thread a shared execution context, pass results between steps, time and
|
|
5
|
+
trace each step, and surface errors — and it knows **nothing** about any specific intent.
|
|
6
|
+
All execution logic lives in registered step **handlers** (`steps.py`); intents are
|
|
7
|
+
declarative plans (`planner.py`). Consequently:
|
|
8
|
+
|
|
9
|
+
Adding a new RetrievalIntent = declaring a new QueryPlan. The Runtime is NOT modified.
|
|
10
|
+
|
|
11
|
+
Responsibilities delivered here (design §4.5, Fase 4):
|
|
12
|
+
* sequential execution of a plan's steps;
|
|
13
|
+
* a shared `StepContext` (knowledge handles + a blackboard for inter-step results);
|
|
14
|
+
* result passing between steps via the blackboard;
|
|
15
|
+
* error handling (typed, with the failing step captured);
|
|
16
|
+
* traceability (an ordered `StepTrace` per step);
|
|
17
|
+
* profiling (per-step + total wall time);
|
|
18
|
+
* Knowledge-Cache reuse (the single `KnowledgeContext` is shared across every step, so
|
|
19
|
+
the cached CIR / SourceMap is acquired at most once — no new cache is introduced).
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from time import perf_counter
|
|
25
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from sourcecode.retrieval.context import KnowledgeContext
|
|
28
|
+
from sourcecode.retrieval.errors import (
|
|
29
|
+
IncompletePlanError,
|
|
30
|
+
RetrievalError,
|
|
31
|
+
StepContractError,
|
|
32
|
+
StepExecutionError,
|
|
33
|
+
UnknownStepError,
|
|
34
|
+
)
|
|
35
|
+
from sourcecode.retrieval.query import KnowledgeQuery, Step
|
|
36
|
+
from sourcecode.retrieval.request import RetrievalIntent, RetrievalOptions
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from sourcecode.retrieval.resolution import ResolvedAnchor, ResolvedIntent
|
|
40
|
+
from sourcecode.retrieval.result import KnowledgeResult
|
|
41
|
+
|
|
42
|
+
# A step handler is a pure procedure over (step, context). It reads/writes the shared
|
|
43
|
+
# blackboard and (for the terminal step) sets the context result. It returns nothing.
|
|
44
|
+
StepHandler = Callable[[Step, "StepContext"], None]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── shared execution context ─────────────────────────────────────────────────────
|
|
48
|
+
class StepContext:
|
|
49
|
+
"""The execution scope threaded across every step of one plan run.
|
|
50
|
+
|
|
51
|
+
* `knowledge` — read-only handles to already-built knowledge (shared → cache reuse).
|
|
52
|
+
* `anchors` — the request's resolved anchors, so anchored intents reach their target.
|
|
53
|
+
* blackboard — a keyed store steps use to pass intermediate results to later steps.
|
|
54
|
+
* `result` — the terminal `KnowledgeResult`, set by the BuildResult step.
|
|
55
|
+
* `trace` — accumulated per-step traces.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
knowledge: KnowledgeContext,
|
|
61
|
+
intent: RetrievalIntent,
|
|
62
|
+
resolved: "Optional[ResolvedIntent]" = None,
|
|
63
|
+
options: "Optional[RetrievalOptions]" = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.knowledge = knowledge
|
|
66
|
+
self.intent = intent
|
|
67
|
+
self._resolved = resolved
|
|
68
|
+
self.options = options or RetrievalOptions()
|
|
69
|
+
self._slots: dict[str, Any] = {}
|
|
70
|
+
self._result: "Optional[KnowledgeResult]" = None
|
|
71
|
+
self.trace: list["StepTrace"] = []
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def anchors(self) -> "tuple[ResolvedAnchor, ...]":
|
|
75
|
+
"""The request's resolved anchors (empty for anchor-free intents)."""
|
|
76
|
+
return self._resolved.anchors if self._resolved is not None else ()
|
|
77
|
+
|
|
78
|
+
# blackboard ------------------------------------------------------------------
|
|
79
|
+
def put(self, key: str, value: Any) -> None:
|
|
80
|
+
self._slots[key] = value
|
|
81
|
+
|
|
82
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
83
|
+
return self._slots.get(key, default)
|
|
84
|
+
|
|
85
|
+
def has(self, key: str) -> bool:
|
|
86
|
+
return key in self._slots
|
|
87
|
+
|
|
88
|
+
def require(self, key: str) -> Any:
|
|
89
|
+
"""Fetch a slot a prior step must have produced; a missing slot is a plan
|
|
90
|
+
ordering fault, not a runtime bug."""
|
|
91
|
+
if key not in self._slots:
|
|
92
|
+
raise StepContractError(f"step requires slot {key!r} which no prior step produced")
|
|
93
|
+
return self._slots[key]
|
|
94
|
+
|
|
95
|
+
# terminal result -------------------------------------------------------------
|
|
96
|
+
def set_result(self, result: "KnowledgeResult") -> None:
|
|
97
|
+
self._result = result
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def result(self) -> "Optional[KnowledgeResult]":
|
|
101
|
+
return self._result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ── trace / profiling ────────────────────────────────────────────────────────────
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class StepTrace:
|
|
107
|
+
"""One executed step's record: identity, params, status, wall time, error (if any)."""
|
|
108
|
+
|
|
109
|
+
index: int
|
|
110
|
+
kind: str
|
|
111
|
+
step: str # Python class name
|
|
112
|
+
params: dict[str, Any]
|
|
113
|
+
status: str # "ok" | "error"
|
|
114
|
+
duration_ms: float
|
|
115
|
+
error: Optional[str] = None
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
return {
|
|
119
|
+
"index": self.index,
|
|
120
|
+
"kind": self.kind,
|
|
121
|
+
"step": self.step,
|
|
122
|
+
"params": self.params,
|
|
123
|
+
"status": self.status,
|
|
124
|
+
"duration_ms": round(self.duration_ms, 3),
|
|
125
|
+
"error": self.error,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class RuntimeReport:
|
|
131
|
+
"""Inspectable execution report: the plan, per-step traces, totals, cache status, and
|
|
132
|
+
the knowledge-acquisition timings (B2)."""
|
|
133
|
+
|
|
134
|
+
intent: str
|
|
135
|
+
plan: tuple[str, ...]
|
|
136
|
+
steps: tuple[StepTrace, ...]
|
|
137
|
+
total_ms: float
|
|
138
|
+
knowledge_source: str # Knowledge-Cache status (HIT/MISS/disabled/not-acquired)
|
|
139
|
+
acquisitions: tuple[tuple[str, float], ...] = () # (name, ms) lazy acquisitions
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict[str, Any]:
|
|
142
|
+
return {
|
|
143
|
+
"intent": self.intent,
|
|
144
|
+
"plan": list(self.plan),
|
|
145
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
146
|
+
"total_ms": round(self.total_ms, 3),
|
|
147
|
+
"knowledge_source": self.knowledge_source,
|
|
148
|
+
"acquisitions": [
|
|
149
|
+
{"name": n, "ms": round(ms, 3)} for n, ms in self.acquisitions
|
|
150
|
+
],
|
|
151
|
+
"acquisition_ms": round(sum(ms for _, ms in self.acquisitions), 3),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class RuntimeResult:
|
|
157
|
+
"""A run's output: the typed result plus the execution report."""
|
|
158
|
+
|
|
159
|
+
result: "KnowledgeResult"
|
|
160
|
+
report: RuntimeReport
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── step registry ────────────────────────────────────────────────────────────────
|
|
164
|
+
class StepRegistry:
|
|
165
|
+
"""Maps a step `kind` to its handler. The ONLY place execution logic is bound to a
|
|
166
|
+
step. New capabilities register a handler here; plans then reference the kind."""
|
|
167
|
+
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
self._handlers: dict[str, StepHandler] = {}
|
|
170
|
+
|
|
171
|
+
def register(self, kind: str, handler: StepHandler) -> None:
|
|
172
|
+
if kind in self._handlers:
|
|
173
|
+
raise ValueError(f"step kind {kind!r} already registered")
|
|
174
|
+
self._handlers[kind] = handler
|
|
175
|
+
|
|
176
|
+
def handler_for(self, step: Step) -> StepHandler:
|
|
177
|
+
handler = self._handlers.get(step.kind)
|
|
178
|
+
if handler is None:
|
|
179
|
+
raise UnknownStepError(f"no handler registered for step kind {step.kind!r}")
|
|
180
|
+
return handler
|
|
181
|
+
|
|
182
|
+
def kinds(self) -> frozenset[str]:
|
|
183
|
+
return frozenset(self._handlers)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ── the runtime ──────────────────────────────────────────────────────────────────
|
|
187
|
+
class QueryPlanRuntime:
|
|
188
|
+
"""Executes any `KnowledgeQuery` composed of registered steps."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, registry: Optional[StepRegistry] = None) -> None:
|
|
191
|
+
if registry is None:
|
|
192
|
+
# Lazy import to avoid a cycle (steps import the result/context types).
|
|
193
|
+
from sourcecode.retrieval.steps import default_registry
|
|
194
|
+
|
|
195
|
+
registry = default_registry()
|
|
196
|
+
self._registry = registry
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def registry(self) -> StepRegistry:
|
|
200
|
+
return self._registry
|
|
201
|
+
|
|
202
|
+
def run(
|
|
203
|
+
self,
|
|
204
|
+
query: KnowledgeQuery,
|
|
205
|
+
knowledge: KnowledgeContext,
|
|
206
|
+
resolved: "Optional[ResolvedIntent]" = None,
|
|
207
|
+
options: "Optional[RetrievalOptions]" = None,
|
|
208
|
+
) -> RuntimeResult:
|
|
209
|
+
# `resolved` carries the request's bound anchors so anchored intents (the whole
|
|
210
|
+
# IMPACT pack) can reach their target; `options` carries the request's presentation
|
|
211
|
+
# knobs (e.g. `limit`, applied by the terminal BuildResult). Both are additive and
|
|
212
|
+
# optional — anchor-free / default-option requests pass None and behave exactly as
|
|
213
|
+
# before. Neither changes execution semantics (sequence/trace/profiling).
|
|
214
|
+
ctx = StepContext(knowledge, query.intent, resolved, options)
|
|
215
|
+
t0 = perf_counter()
|
|
216
|
+
for index, step in enumerate(query.steps):
|
|
217
|
+
handler = self._registry.handler_for(step) # UnknownStepError propagates (plan fault)
|
|
218
|
+
s0 = perf_counter()
|
|
219
|
+
try:
|
|
220
|
+
handler(step, ctx)
|
|
221
|
+
except RetrievalError as exc: # typed fault (e.g. bad slot/dimension) — keep as-is
|
|
222
|
+
self._record(ctx, index, step, s0, status="error", error=repr(exc))
|
|
223
|
+
raise
|
|
224
|
+
except Exception as exc: # unexpected fault — wrap, but preserve the trace
|
|
225
|
+
self._record(ctx, index, step, s0, status="error", error=repr(exc))
|
|
226
|
+
raise StepExecutionError(
|
|
227
|
+
f"step {type(step).__name__} ({step.kind}) failed: {exc!r}"
|
|
228
|
+
) from exc
|
|
229
|
+
self._record(ctx, index, step, s0, status="ok", error=None)
|
|
230
|
+
total_ms = (perf_counter() - t0) * 1000.0
|
|
231
|
+
|
|
232
|
+
if ctx.result is None:
|
|
233
|
+
raise IncompletePlanError("plan finished without producing a result (no BuildResult step)")
|
|
234
|
+
|
|
235
|
+
report = RuntimeReport(
|
|
236
|
+
intent=query.intent.value,
|
|
237
|
+
plan=query.describe(),
|
|
238
|
+
steps=tuple(ctx.trace),
|
|
239
|
+
total_ms=total_ms,
|
|
240
|
+
knowledge_source=knowledge.cache_render,
|
|
241
|
+
acquisitions=knowledge.acquisitions, # B2: intent-agnostic infra timings
|
|
242
|
+
)
|
|
243
|
+
return RuntimeResult(result=ctx.result, report=report)
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _record(
|
|
247
|
+
ctx: StepContext, index: int, step: Step, s0: float, *, status: str, error: Optional[str]
|
|
248
|
+
) -> None:
|
|
249
|
+
ctx.trace.append(
|
|
250
|
+
StepTrace(
|
|
251
|
+
index=index,
|
|
252
|
+
kind=step.kind,
|
|
253
|
+
step=type(step).__name__,
|
|
254
|
+
params=step.params(),
|
|
255
|
+
status=status,
|
|
256
|
+
duration_ms=(perf_counter() - s0) * 1000.0,
|
|
257
|
+
error=error,
|
|
258
|
+
)
|
|
259
|
+
)
|