sourcecode 0.42.0__py3-none-any.whl → 0.44.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/cli.py +30 -0
- sourcecode/context_scorer.py +404 -0
- sourcecode/contract_model.py +1 -0
- sourcecode/contract_pipeline.py +59 -25
- sourcecode/prepare_context.py +27 -1
- sourcecode/ranking_engine.py +29 -7
- sourcecode/serializer.py +49 -5
- {sourcecode-0.42.0.dist-info → sourcecode-0.44.0.dist-info}/METADATA +1 -1
- {sourcecode-0.42.0.dist-info → sourcecode-0.44.0.dist-info}/RECORD +13 -12
- {sourcecode-0.42.0.dist-info → sourcecode-0.44.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.42.0.dist-info → sourcecode-0.44.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-0.42.0.dist-info → sourcecode-0.44.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -181,6 +181,7 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
|
|
|
181
181
|
"--dependency-depth",
|
|
182
182
|
"--rank-by",
|
|
183
183
|
"--symbol",
|
|
184
|
+
"--max-importers",
|
|
184
185
|
})
|
|
185
186
|
|
|
186
187
|
|
|
@@ -594,6 +595,17 @@ def main(
|
|
|
594
595
|
"--symbol",
|
|
595
596
|
help="Contract mode: extract localized context for a specific symbol name. Returns defining file + all importers.",
|
|
596
597
|
),
|
|
598
|
+
max_importers: int = typer.Option(
|
|
599
|
+
50,
|
|
600
|
+
"--max-importers",
|
|
601
|
+
help=(
|
|
602
|
+
"Maximum importer files returned by --symbol (default: 50). "
|
|
603
|
+
"Popular symbols can have hundreds of importers — this prevents output explosion. "
|
|
604
|
+
"Defining files are never truncated. Override: --symbol Foo --max-importers 200."
|
|
605
|
+
),
|
|
606
|
+
min=1,
|
|
607
|
+
max=10000,
|
|
608
|
+
),
|
|
597
609
|
copy: bool = typer.Option(
|
|
598
610
|
False,
|
|
599
611
|
"--copy",
|
|
@@ -770,6 +782,21 @@ def main(
|
|
|
770
782
|
code_notes = True
|
|
771
783
|
no_tree = True # agents never need the raw file tree
|
|
772
784
|
typer.echo("[agent] dependencies env-map code-notes (no-tree)", err=True)
|
|
785
|
+
# Warn about flags that are computed but excluded from agent_view output
|
|
786
|
+
_agent_suppressed: list[str] = []
|
|
787
|
+
if full_metrics:
|
|
788
|
+
_agent_suppressed.append("--full-metrics")
|
|
789
|
+
if graph_modules:
|
|
790
|
+
_agent_suppressed.append("--graph-modules")
|
|
791
|
+
if docs:
|
|
792
|
+
_agent_suppressed.append("--docs")
|
|
793
|
+
if _agent_suppressed:
|
|
794
|
+
typer.echo(
|
|
795
|
+
f"[agent] warning: {', '.join(_agent_suppressed)} computed but excluded "
|
|
796
|
+
"from --agent output — agent_view does not include these sections. "
|
|
797
|
+
"Remove these flags to skip unnecessary computation.",
|
|
798
|
+
err=True,
|
|
799
|
+
)
|
|
773
800
|
|
|
774
801
|
scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth)
|
|
775
802
|
raw_tree = scanner.scan_tree()
|
|
@@ -1343,6 +1370,9 @@ def main(
|
|
|
1343
1370
|
changed_only=changed_only,
|
|
1344
1371
|
symbol=symbol,
|
|
1345
1372
|
compress_types=compress_types,
|
|
1373
|
+
max_importers=max_importers,
|
|
1374
|
+
semantic_calls=sm.semantic_calls or None,
|
|
1375
|
+
code_notes=sm.code_notes or None,
|
|
1346
1376
|
)
|
|
1347
1377
|
sm = _replace(sm, file_contracts=_contracts, contract_summary=_contract_summary)
|
|
1348
1378
|
if symbol is not None and len(_contracts) == 0:
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""context_scorer.py — Unified node scoring and minimum-sufficient subgraph selection.
|
|
2
|
+
|
|
3
|
+
Aggregates all available signals (structural, semantic, git, annotations, proximity)
|
|
4
|
+
into a NodeScore per file, then uses greedy selection to produce the minimum-sufficient
|
|
5
|
+
subgraph that maximises explanatory value within a context budget.
|
|
6
|
+
|
|
7
|
+
Design invariants:
|
|
8
|
+
- Deterministic: sort key is always (-score, path). Path breaks all ties.
|
|
9
|
+
- No LLMs, no randomness, no external I/O.
|
|
10
|
+
- All signals optional: degrades gracefully when data is absent.
|
|
11
|
+
- SCORER_VERSION: bump on any formula change so callers can detect drift.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections import Counter, deque
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
SCORER_VERSION = "1"
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Edge weight tables
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_EDGE_BASE_WEIGHTS: dict[str, float] = {
|
|
27
|
+
"imports": 1.00, # structural dependency — strongest signal
|
|
28
|
+
"extends": 0.90, # inheritance / implementation — tight coupling
|
|
29
|
+
"calls": 0.80, # behavioral dependency
|
|
30
|
+
"contains": 0.30, # membership — low marginal information
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_CONFIDENCE_MULT: dict[str, float] = {
|
|
34
|
+
"high": 1.0,
|
|
35
|
+
"medium": 0.7,
|
|
36
|
+
"low": 0.3,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Annotation kinds weighted at 2× (actionable defects vs informational notes)
|
|
40
|
+
_HIGH_SEVERITY_NOTES: frozenset[str] = frozenset({"BUG", "FIXME", "HACK", "XXX"})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Data model
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class NodeScore:
|
|
49
|
+
"""Unified scoring breakdown for a single file node.
|
|
50
|
+
|
|
51
|
+
score / display_score drive all ranking and selection decisions.
|
|
52
|
+
The component fields (structural, semantic, annotation, proximity) allow
|
|
53
|
+
callers to inspect which signals dominated the final score.
|
|
54
|
+
"""
|
|
55
|
+
path: str
|
|
56
|
+
score: float # final weighted score (higher = more relevant)
|
|
57
|
+
display_score: float # clamped [0.0, 1.0] for output fields
|
|
58
|
+
structural: float # contribution from RankingEngine
|
|
59
|
+
semantic: float # call graph centrality [0.0, 1.0]
|
|
60
|
+
annotation: float # code note density [0.0, 1.0]
|
|
61
|
+
proximity: float # BFS closeness to focus [0.0, 1.0]
|
|
62
|
+
reasons: list[str]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Core scorer
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
class ContextScorer:
|
|
70
|
+
"""Unified file scoring and minimum-sufficient subgraph selection.
|
|
71
|
+
|
|
72
|
+
Stateless once constructed. Thread-safe (no mutable state after __init__).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
monorepo_packages: Optional[list] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
from sourcecode.ranking_engine import RankingEngine
|
|
80
|
+
self._engine = RankingEngine(monorepo_packages or [])
|
|
81
|
+
|
|
82
|
+
def score_nodes(
|
|
83
|
+
self,
|
|
84
|
+
contracts: list[Any],
|
|
85
|
+
*,
|
|
86
|
+
semantic_calls: Optional[list] = None,
|
|
87
|
+
git_hotspots: Optional[dict[str, int]] = None,
|
|
88
|
+
code_notes: Optional[list] = None,
|
|
89
|
+
focus_path: Optional[str] = None,
|
|
90
|
+
task: str = "default",
|
|
91
|
+
) -> dict[str, NodeScore]:
|
|
92
|
+
"""Compute a NodeScore for every contract.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
contracts FileContract list. fan_in, fan_out, is_entrypoint,
|
|
97
|
+
is_changed, and exports must be set before calling.
|
|
98
|
+
semantic_calls list[CallRecord] from --semantics (optional).
|
|
99
|
+
git_hotspots {path: commit_count} from git analysis (optional).
|
|
100
|
+
code_notes list[CodeNote] from --code-notes (optional).
|
|
101
|
+
focus_path Anchor file for proximity BFS (optional).
|
|
102
|
+
task Task profile: fix-bug | refactor | explain | …
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
dict mapping path → NodeScore for every contract path.
|
|
107
|
+
"""
|
|
108
|
+
from sourcecode.ranking_engine import TASK_WEIGHTS
|
|
109
|
+
|
|
110
|
+
w = TASK_WEIGHTS.get(task, TASK_WEIGHTS["default"])
|
|
111
|
+
_hotspots = git_hotspots or {}
|
|
112
|
+
max_fan_in = max((c.fan_in for c in contracts), default=1)
|
|
113
|
+
max_churn = max(_hotspots.values(), default=1)
|
|
114
|
+
|
|
115
|
+
# Pre-compute optional signal maps
|
|
116
|
+
sem_centrality: dict[str, float] = {}
|
|
117
|
+
if semantic_calls:
|
|
118
|
+
sem_centrality = _semantic_centrality(semantic_calls, contracts)
|
|
119
|
+
max_semantic = max(sem_centrality.values(), default=1.0) or 1.0
|
|
120
|
+
|
|
121
|
+
ann_density: dict[str, float] = {}
|
|
122
|
+
if code_notes:
|
|
123
|
+
ann_density = _annotation_density(code_notes, contracts)
|
|
124
|
+
|
|
125
|
+
prox_scores: dict[str, float] = {}
|
|
126
|
+
if focus_path:
|
|
127
|
+
prox_scores = _proximity_bfs(focus_path, contracts, semantic_calls or [])
|
|
128
|
+
|
|
129
|
+
result: dict[str, NodeScore] = {}
|
|
130
|
+
for c in contracts:
|
|
131
|
+
sem = sem_centrality.get(c.path, 0.0)
|
|
132
|
+
ann = ann_density.get(c.path, 0.0)
|
|
133
|
+
prox = prox_scores.get(c.path, 0.0)
|
|
134
|
+
|
|
135
|
+
# Structural + git + annotation + semantic centrality via unified engine
|
|
136
|
+
fs = self._engine.score(
|
|
137
|
+
c.path,
|
|
138
|
+
fan_in=c.fan_in,
|
|
139
|
+
fan_out=c.fan_out,
|
|
140
|
+
max_fan_in=max_fan_in,
|
|
141
|
+
git_churn=_hotspots.get(c.path, 0),
|
|
142
|
+
max_churn=max_churn,
|
|
143
|
+
is_entrypoint=c.is_entrypoint,
|
|
144
|
+
is_changed=c.is_changed,
|
|
145
|
+
export_count=len(c.exports),
|
|
146
|
+
task=task,
|
|
147
|
+
semantic_centrality=sem,
|
|
148
|
+
max_semantic=max_semantic,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Proximity is a graph operation, computed here and added on top
|
|
152
|
+
prox_contrib = prox * 0.50 * w.proximity
|
|
153
|
+
|
|
154
|
+
final = fs.score + prox_contrib
|
|
155
|
+
|
|
156
|
+
reasons = list(fs.reasons)
|
|
157
|
+
if prox >= 0.80 and prox_contrib > 0:
|
|
158
|
+
reasons.append("close to focus")
|
|
159
|
+
elif prox >= 0.50 and prox_contrib > 0:
|
|
160
|
+
reasons.append("near focus")
|
|
161
|
+
|
|
162
|
+
result[c.path] = NodeScore(
|
|
163
|
+
path=c.path,
|
|
164
|
+
score=final,
|
|
165
|
+
display_score=max(0.0, min(1.0, final)),
|
|
166
|
+
structural=fs.score,
|
|
167
|
+
semantic=sem,
|
|
168
|
+
annotation=ann,
|
|
169
|
+
proximity=prox,
|
|
170
|
+
reasons=reasons,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
def select_subgraph(
|
|
176
|
+
self,
|
|
177
|
+
node_scores: dict[str, NodeScore],
|
|
178
|
+
contracts: list[Any],
|
|
179
|
+
*,
|
|
180
|
+
budget: int = 30,
|
|
181
|
+
min_score: float = 0.05,
|
|
182
|
+
) -> list[str]:
|
|
183
|
+
"""Greedy minimum-sufficient subgraph selection with diversity re-ranking.
|
|
184
|
+
|
|
185
|
+
At each round, recomputes effective scores for all remaining candidates
|
|
186
|
+
(raw_score × (1 - redundancy_penalty)), then picks the highest. This
|
|
187
|
+
allows a file from a new directory to beat a clustered sibling even if
|
|
188
|
+
the sibling has a higher raw score — the selection actively prefers
|
|
189
|
+
coverage over concentration.
|
|
190
|
+
|
|
191
|
+
Stops when the budget is exhausted or no remaining candidate has an
|
|
192
|
+
effective score above min_score.
|
|
193
|
+
|
|
194
|
+
O(n × budget) — negligible for typical budgets (15-30) and file counts.
|
|
195
|
+
Deterministic: tie-break by path on every round.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
node_scores output of score_nodes()
|
|
200
|
+
contracts same FileContract list passed to score_nodes()
|
|
201
|
+
(used for directory-based redundancy; may be empty)
|
|
202
|
+
budget maximum number of nodes to select
|
|
203
|
+
min_score discard candidates whose effective score is below this
|
|
204
|
+
"""
|
|
205
|
+
contract_map = {c.path: c for c in contracts}
|
|
206
|
+
remaining: dict[str, NodeScore] = dict(node_scores)
|
|
207
|
+
selected: list[str] = []
|
|
208
|
+
selected_set: set[str] = set()
|
|
209
|
+
|
|
210
|
+
while len(selected) < budget and remaining:
|
|
211
|
+
best_path: str | None = None
|
|
212
|
+
best_effective: float = -1.0
|
|
213
|
+
|
|
214
|
+
for path, ns in remaining.items():
|
|
215
|
+
if ns.score < min_score:
|
|
216
|
+
continue
|
|
217
|
+
penalty = _redundancy_penalty(path, selected_set, contract_map)
|
|
218
|
+
effective = ns.score * (1.0 - penalty)
|
|
219
|
+
# Strict tie-break by path ensures determinism
|
|
220
|
+
if effective > best_effective or (
|
|
221
|
+
effective == best_effective
|
|
222
|
+
and best_path is not None
|
|
223
|
+
and path < best_path
|
|
224
|
+
):
|
|
225
|
+
best_effective = effective
|
|
226
|
+
best_path = path
|
|
227
|
+
|
|
228
|
+
if best_path is None or best_effective < min_score:
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
selected.append(best_path)
|
|
232
|
+
selected_set.add(best_path)
|
|
233
|
+
del remaining[best_path]
|
|
234
|
+
|
|
235
|
+
return selected
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def edge_weight(kind: str, confidence: str) -> float:
|
|
239
|
+
"""Scalar weight for a graph edge based on relationship type and confidence.
|
|
240
|
+
|
|
241
|
+
Higher weight = stronger information dependency between the connected nodes.
|
|
242
|
+
"""
|
|
243
|
+
base = _EDGE_BASE_WEIGHTS.get(kind, 0.50)
|
|
244
|
+
mult = _CONFIDENCE_MULT.get(confidence, 0.50)
|
|
245
|
+
return base * mult
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Signal computers (module-level, pure functions)
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def _semantic_centrality(
|
|
253
|
+
semantic_calls: list,
|
|
254
|
+
contracts: list,
|
|
255
|
+
) -> dict[str, float]:
|
|
256
|
+
"""Per-file centrality from the call graph.
|
|
257
|
+
|
|
258
|
+
centrality(path) = (weighted_fan_in × 2 + weighted_fan_out) / max
|
|
259
|
+
where weight = confidence multiplier (high=1.0, medium=0.7, low=0.3).
|
|
260
|
+
|
|
261
|
+
Returns a dict normalised to [0.0, 1.0] across the contract set.
|
|
262
|
+
"""
|
|
263
|
+
path_set = {c.path for c in contracts}
|
|
264
|
+
fan_in: Counter[str] = Counter()
|
|
265
|
+
fan_out: Counter[str] = Counter()
|
|
266
|
+
|
|
267
|
+
for call in semantic_calls:
|
|
268
|
+
w = _CONFIDENCE_MULT.get(getattr(call, "confidence", "medium"), 0.7)
|
|
269
|
+
callee = getattr(call, "callee_path", None)
|
|
270
|
+
caller = getattr(call, "caller_path", None)
|
|
271
|
+
if callee and callee in path_set:
|
|
272
|
+
fan_in[callee] += w
|
|
273
|
+
if caller and caller in path_set:
|
|
274
|
+
fan_out[caller] += w
|
|
275
|
+
|
|
276
|
+
raw = {p: fan_in[p] * 2.0 + fan_out[p] for p in path_set}
|
|
277
|
+
max_val = max(raw.values(), default=0.0)
|
|
278
|
+
if max_val <= 0.0:
|
|
279
|
+
return {p: 0.0 for p in path_set}
|
|
280
|
+
return {p: v / max_val for p, v in raw.items()}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _proximity_bfs(
|
|
284
|
+
focus_path: str,
|
|
285
|
+
contracts: list,
|
|
286
|
+
semantic_calls: list,
|
|
287
|
+
) -> dict[str, float]:
|
|
288
|
+
"""BFS from focus_path through import + call edges.
|
|
289
|
+
|
|
290
|
+
Traversal is bidirectional (imports and calls traversed in both directions)
|
|
291
|
+
so the proximity score reflects reachability in any direction from the focus.
|
|
292
|
+
|
|
293
|
+
proximity(path) = 1.0 / (2 ** distance)
|
|
294
|
+
distance=0 → 1.00 (the focus itself)
|
|
295
|
+
distance=1 → 0.50
|
|
296
|
+
distance=2 → 0.25
|
|
297
|
+
distance=3 → 0.125
|
|
298
|
+
distance=4 → 0.0625 (max depth)
|
|
299
|
+
|
|
300
|
+
BFS neighbours are sorted before enqueuing to ensure determinism.
|
|
301
|
+
"""
|
|
302
|
+
path_set = {c.path for c in contracts}
|
|
303
|
+
|
|
304
|
+
# Build bidirectional adjacency from import graph
|
|
305
|
+
adj: dict[str, set[str]] = {p: set() for p in path_set}
|
|
306
|
+
for c in contracts:
|
|
307
|
+
base_dir = str(Path(c.path).parent).replace("\\", "/")
|
|
308
|
+
for imp in c.imports:
|
|
309
|
+
src = getattr(imp, "source", "")
|
|
310
|
+
if not src.startswith("."):
|
|
311
|
+
continue
|
|
312
|
+
for t in _resolve_import(base_dir, src, path_set):
|
|
313
|
+
adj[c.path].add(t)
|
|
314
|
+
adj[t].add(c.path)
|
|
315
|
+
|
|
316
|
+
# Augment with call graph edges
|
|
317
|
+
for call in semantic_calls:
|
|
318
|
+
caller = getattr(call, "caller_path", None)
|
|
319
|
+
callee = getattr(call, "callee_path", None)
|
|
320
|
+
if caller in adj and callee in adj:
|
|
321
|
+
adj[caller].add(callee)
|
|
322
|
+
adj[callee].add(caller)
|
|
323
|
+
|
|
324
|
+
if focus_path not in adj:
|
|
325
|
+
return {}
|
|
326
|
+
|
|
327
|
+
distances: dict[str, int] = {focus_path: 0}
|
|
328
|
+
queue: deque[str] = deque([focus_path])
|
|
329
|
+
while queue:
|
|
330
|
+
node = queue.popleft()
|
|
331
|
+
d = distances[node]
|
|
332
|
+
if d >= 4:
|
|
333
|
+
continue
|
|
334
|
+
for neighbor in sorted(adj.get(node, set())):
|
|
335
|
+
if neighbor not in distances:
|
|
336
|
+
distances[neighbor] = d + 1
|
|
337
|
+
queue.append(neighbor)
|
|
338
|
+
|
|
339
|
+
return {p: 1.0 / (2 ** d) for p, d in distances.items()}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _annotation_density(
|
|
343
|
+
code_notes: list,
|
|
344
|
+
contracts: list,
|
|
345
|
+
) -> dict[str, float]:
|
|
346
|
+
"""Severity-weighted annotation density per file, normalised [0.0, 1.0].
|
|
347
|
+
|
|
348
|
+
BUG / FIXME / HACK / XXX count 2×; all other kinds count 1×.
|
|
349
|
+
"""
|
|
350
|
+
path_set = {c.path for c in contracts}
|
|
351
|
+
weighted: Counter[str] = Counter()
|
|
352
|
+
for note in code_notes:
|
|
353
|
+
path = getattr(note, "path", None)
|
|
354
|
+
if path not in path_set:
|
|
355
|
+
continue
|
|
356
|
+
kind = getattr(note, "kind", "").upper()
|
|
357
|
+
weighted[path] += 2.0 if kind in _HIGH_SEVERITY_NOTES else 1.0
|
|
358
|
+
|
|
359
|
+
max_val = max(weighted.values(), default=1.0)
|
|
360
|
+
return {p: min(weighted.get(p, 0.0) / max_val, 1.0) for p in path_set}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _redundancy_penalty(
|
|
364
|
+
path: str,
|
|
365
|
+
selected_set: set[str],
|
|
366
|
+
contract_map: dict,
|
|
367
|
+
) -> float:
|
|
368
|
+
"""Penalty for adding a file from the same directory as already-selected files.
|
|
369
|
+
|
|
370
|
+
Rationale: files in the same directory address the same concern; the
|
|
371
|
+
marginal explanatory gain of the n-th file from a directory is lower than
|
|
372
|
+
that of the first file from a new directory.
|
|
373
|
+
|
|
374
|
+
Penalty grows by 0.10 per same-directory sibling, capped at 0.40.
|
|
375
|
+
The 0.40 cap ensures no node is ever fully excluded by proximity alone.
|
|
376
|
+
"""
|
|
377
|
+
if not selected_set:
|
|
378
|
+
return 0.0
|
|
379
|
+
path_dir = str(Path(path).parent)
|
|
380
|
+
same_dir_count = sum(
|
|
381
|
+
1 for s in selected_set
|
|
382
|
+
if str(Path(s).parent) == path_dir
|
|
383
|
+
)
|
|
384
|
+
return min(same_dir_count * 0.10, 0.40)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _resolve_import(base_dir: str, src: str, path_set: set[str]) -> list[str]:
|
|
388
|
+
"""Approximate resolution of a relative import specifier to known paths.
|
|
389
|
+
|
|
390
|
+
Mirrors the logic in contract_pipeline._resolve_relative without importing
|
|
391
|
+
from that module (avoids circular import).
|
|
392
|
+
"""
|
|
393
|
+
src = src.lstrip("./")
|
|
394
|
+
if not src:
|
|
395
|
+
return []
|
|
396
|
+
exts = (".ts", ".tsx", ".js", ".jsx", ".py", "/index.ts", "/index.js", "/index.tsx")
|
|
397
|
+
for ext in exts:
|
|
398
|
+
candidate = f"{base_dir}/{src}{ext}".replace("//", "/")
|
|
399
|
+
if candidate in path_set:
|
|
400
|
+
return [candidate]
|
|
401
|
+
candidate = f"{base_dir}/{src}".replace("//", "/")
|
|
402
|
+
if candidate in path_set:
|
|
403
|
+
return [candidate]
|
|
404
|
+
return []
|
sourcecode/contract_model.py
CHANGED
sourcecode/contract_pipeline.py
CHANGED
|
@@ -175,6 +175,9 @@ class ContractPipeline:
|
|
|
175
175
|
changed_only: bool = False,
|
|
176
176
|
symbol: Optional[str] = None,
|
|
177
177
|
compress_types: bool = False,
|
|
178
|
+
max_importers: int = 50,
|
|
179
|
+
semantic_calls: Optional[list] = None,
|
|
180
|
+
code_notes: Optional[list] = None,
|
|
178
181
|
) -> tuple[list[FileContract], ContractSummary]:
|
|
179
182
|
"""Run the full extraction pipeline.
|
|
180
183
|
|
|
@@ -256,40 +259,42 @@ class ContractPipeline:
|
|
|
256
259
|
if rank_by == "git-churn":
|
|
257
260
|
churn = _get_git_churn(root, [c.path for c in contracts])
|
|
258
261
|
|
|
259
|
-
# 6. Compute relevance scores via unified
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
# 6. Compute relevance scores via unified scoring engine.
|
|
263
|
+
# ContextScorer wraps RankingEngine and enriches scores with semantic
|
|
264
|
+
# centrality (when semantic_calls available) and annotation density
|
|
265
|
+
# (when code_notes available). Falls back to structural signals only
|
|
266
|
+
# when neither is present — identical to the old behaviour.
|
|
267
|
+
from sourcecode.context_scorer import ContextScorer
|
|
268
|
+
_ctx_scorer = ContextScorer(monorepo_packages)
|
|
269
|
+
_node_scores = _ctx_scorer.score_nodes(
|
|
270
|
+
contracts,
|
|
271
|
+
semantic_calls=semantic_calls,
|
|
272
|
+
code_notes=code_notes,
|
|
273
|
+
git_hotspots=churn,
|
|
274
|
+
task="default",
|
|
275
|
+
)
|
|
262
276
|
for c in contracts:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
fan_out=c.fan_out,
|
|
267
|
-
max_fan_in=max_fan_in,
|
|
268
|
-
git_churn=churn.get(c.path, 0),
|
|
269
|
-
max_churn=max_churn_val,
|
|
270
|
-
is_entrypoint=c.is_entrypoint,
|
|
271
|
-
is_changed=c.is_changed,
|
|
272
|
-
export_count=len(c.exports),
|
|
273
|
-
task="default",
|
|
274
|
-
)
|
|
275
|
-
c.relevance_score = fs.display_score
|
|
276
|
-
c.ranking_reasons = fs.reasons
|
|
277
|
+
ns = _node_scores[c.path]
|
|
278
|
+
c.relevance_score = ns.display_score
|
|
279
|
+
c.ranking_reasons = ns.reasons
|
|
277
280
|
|
|
278
281
|
# 7. Rank
|
|
279
282
|
contracts = self._rank(contracts, rank_by)
|
|
280
283
|
|
|
281
284
|
# 8. Symbol filter — keep files that define or import the symbol
|
|
285
|
+
_symbol_truncation: Optional[dict] = None
|
|
282
286
|
if symbol:
|
|
283
|
-
contracts = _filter_by_symbol(contracts, symbol)
|
|
287
|
+
contracts, _symbol_truncation = _filter_by_symbol(contracts, symbol, max_importers=max_importers)
|
|
284
288
|
# When shallow scan missed the defining file (deep monorepo), fall back
|
|
285
289
|
# to a grep-based filesystem search over the full directory tree.
|
|
286
290
|
if not contracts:
|
|
287
|
-
contracts = self._symbol_deep_scan(
|
|
291
|
+
contracts, _symbol_truncation = self._symbol_deep_scan(
|
|
288
292
|
root, symbol,
|
|
289
293
|
known_paths=set(src_paths),
|
|
290
294
|
entry_paths=entry_paths,
|
|
291
295
|
changed_files=changed_files,
|
|
292
296
|
engine=engine,
|
|
297
|
+
max_importers=max_importers,
|
|
293
298
|
)
|
|
294
299
|
|
|
295
300
|
# 9. Entrypoints-only filter
|
|
@@ -313,6 +318,7 @@ class ContractPipeline:
|
|
|
313
318
|
method_breakdown=dict(method_counts),
|
|
314
319
|
ranked_by=rank_by,
|
|
315
320
|
limitations=limitations,
|
|
321
|
+
symbol_truncation=_symbol_truncation,
|
|
316
322
|
)
|
|
317
323
|
return contracts, summary
|
|
318
324
|
|
|
@@ -332,7 +338,8 @@ class ContractPipeline:
|
|
|
332
338
|
entry_paths: set[str],
|
|
333
339
|
changed_files: set[str],
|
|
334
340
|
engine: RankingEngine,
|
|
335
|
-
|
|
341
|
+
max_importers: int = 50,
|
|
342
|
+
) -> tuple[list[FileContract], dict]:
|
|
336
343
|
"""Grep-based fallback when the shallow scan missed the defining files.
|
|
337
344
|
|
|
338
345
|
Searches the full directory tree for source files containing *symbol*,
|
|
@@ -356,7 +363,7 @@ class ContractPipeline:
|
|
|
356
363
|
contract.ranking_reasons = fs.reasons
|
|
357
364
|
extra.append(contract)
|
|
358
365
|
|
|
359
|
-
return _filter_by_symbol(extra, symbol)
|
|
366
|
+
return _filter_by_symbol(extra, symbol, max_importers=max_importers)
|
|
360
367
|
|
|
361
368
|
|
|
362
369
|
# ---------------------------------------------------------------------------
|
|
@@ -412,7 +419,11 @@ def _limit_symbols(contracts: list[FileContract], max_symbols: int) -> list[File
|
|
|
412
419
|
# Symbol-aware filter
|
|
413
420
|
# ---------------------------------------------------------------------------
|
|
414
421
|
|
|
415
|
-
def _filter_by_symbol(
|
|
422
|
+
def _filter_by_symbol(
|
|
423
|
+
contracts: list[FileContract],
|
|
424
|
+
symbol: str,
|
|
425
|
+
max_importers: int = 50,
|
|
426
|
+
) -> tuple[list[FileContract], dict]:
|
|
416
427
|
"""Return contracts that define, import, or structurally reference *symbol*.
|
|
417
428
|
|
|
418
429
|
Four tiers applied in order:
|
|
@@ -423,6 +434,8 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
|
|
|
423
434
|
function signatures (word-boundary). Only used when tiers 1-3 fail.
|
|
424
435
|
|
|
425
436
|
Defining contracts are ranked first; importers and references follow.
|
|
437
|
+
max_importers caps tier 3 results to prevent output explosion on popular symbols.
|
|
438
|
+
Returns (contracts, truncation_metadata).
|
|
426
439
|
"""
|
|
427
440
|
sym_l = symbol.lower()
|
|
428
441
|
word_re = re.compile(
|
|
@@ -466,8 +479,14 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
|
|
|
466
479
|
|
|
467
480
|
# Tier 3: import matching (case-insensitive when no definers found)
|
|
468
481
|
ci_imports = len(defining) == 0
|
|
469
|
-
|
|
470
|
-
|
|
482
|
+
all_importer_paths = {c.path for c in contracts if _imports_sym(c, case=ci_imports)}
|
|
483
|
+
all_importers = [c for c in contracts if c.path in all_importer_paths and c.path not in defining_paths]
|
|
484
|
+
|
|
485
|
+
# Apply importer cap — definers are never truncated
|
|
486
|
+
total_importers = len(all_importers)
|
|
487
|
+
truncated = total_importers > max_importers
|
|
488
|
+
importers = all_importers[:max_importers] if truncated else all_importers
|
|
489
|
+
importer_paths = {c.path for c in importers}
|
|
471
490
|
|
|
472
491
|
# Tier 4: type-reference matching (only when tiers 1-3 yield nothing)
|
|
473
492
|
references: list[FileContract] = []
|
|
@@ -483,12 +502,27 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
|
|
|
483
502
|
seen.add(c.path)
|
|
484
503
|
merged.append(c)
|
|
485
504
|
|
|
486
|
-
|
|
505
|
+
result = sorted(merged, key=lambda c: (
|
|
487
506
|
c.path not in defining_paths,
|
|
488
507
|
c.path not in importer_paths,
|
|
489
508
|
-c.relevance_score,
|
|
490
509
|
))
|
|
491
510
|
|
|
511
|
+
truncation: dict = {
|
|
512
|
+
"symbol": symbol,
|
|
513
|
+
"definers_found": len(defining),
|
|
514
|
+
"importers_found": total_importers,
|
|
515
|
+
"importers_returned": len(importers),
|
|
516
|
+
"references_found": len(references),
|
|
517
|
+
"total_returned": len(result),
|
|
518
|
+
"truncated": truncated,
|
|
519
|
+
}
|
|
520
|
+
if truncated:
|
|
521
|
+
truncation["truncation_reason"] = "max_importers_limit"
|
|
522
|
+
truncation["override_hint"] = f"--symbol {symbol} --max-importers {total_importers}"
|
|
523
|
+
|
|
524
|
+
return result, truncation
|
|
525
|
+
|
|
492
526
|
|
|
493
527
|
# ---------------------------------------------------------------------------
|
|
494
528
|
# Deep symbol scan — grep-based fallback for shallow-scanned repos
|
sourcecode/prepare_context.py
CHANGED
|
@@ -701,7 +701,33 @@ class TaskContextBuilder:
|
|
|
701
701
|
|
|
702
702
|
# Deterministic: score desc, then path asc as tiebreaker
|
|
703
703
|
scored.sort(key=lambda x: (-x[0], x[1]))
|
|
704
|
-
|
|
704
|
+
|
|
705
|
+
# Apply directory-diversity selection via ContextScorer.
|
|
706
|
+
# Files from the same directory share the same concern; the scorer
|
|
707
|
+
# applies a small redundancy penalty so the final set spans more of
|
|
708
|
+
# the codebase rather than clustering inside a single directory.
|
|
709
|
+
# Falls back to top-15 slice when scorer is unavailable.
|
|
710
|
+
try:
|
|
711
|
+
from sourcecode.context_scorer import ContextScorer, NodeScore
|
|
712
|
+
_ctx = ContextScorer()
|
|
713
|
+
_ns: dict[str, NodeScore] = {
|
|
714
|
+
path: NodeScore(
|
|
715
|
+
path=path,
|
|
716
|
+
score=total,
|
|
717
|
+
display_score=min(total / 3.0, 1.0),
|
|
718
|
+
structural=total,
|
|
719
|
+
semantic=0.0,
|
|
720
|
+
annotation=0.0,
|
|
721
|
+
proximity=0.0,
|
|
722
|
+
reasons=[rf.reason] if rf.reason else ["source file"],
|
|
723
|
+
)
|
|
724
|
+
for total, path, rf in scored
|
|
725
|
+
}
|
|
726
|
+
_selected = _ctx.select_subgraph(_ns, contracts=[], budget=15, min_score=0.05)
|
|
727
|
+
_rf_map = {path: rf for _, path, rf in scored}
|
|
728
|
+
return [_rf_map[p] for p in _selected if p in _rf_map]
|
|
729
|
+
except Exception:
|
|
730
|
+
return [f for _, _, f in scored[:15]]
|
|
705
731
|
|
|
706
732
|
def _is_test(self, path: str) -> bool:
|
|
707
733
|
name = Path(path).name.lower()
|
sourcecode/ranking_engine.py
CHANGED
|
@@ -45,60 +45,71 @@ class TaskWeights:
|
|
|
45
45
|
code_notes: float = 0.5
|
|
46
46
|
exports: float = 0.3
|
|
47
47
|
is_changed: float = 0.8
|
|
48
|
+
# Call graph centrality from --semantics (ContextScorer feeds this in)
|
|
49
|
+
semantic_centrality: float = 0.5
|
|
50
|
+
# BFS proximity to a focus symbol/file (added by ContextScorer on top)
|
|
51
|
+
proximity: float = 1.0
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
# Task profiles: each emphasizes different signals for different agent goals.
|
|
51
55
|
# The contrast between profiles is intentional — fix-bug and explain must
|
|
52
56
|
# produce meaningfully different ranked sets from the same codebase.
|
|
53
57
|
TASK_WEIGHTS: dict[str, TaskWeights] = {
|
|
54
|
-
# fix-bug:
|
|
58
|
+
# fix-bug: bug annotations, recent churn, changed files, proximity to focus
|
|
55
59
|
"fix-bug": TaskWeights(
|
|
56
60
|
path_relevance=0.5, entrypoint=0.5,
|
|
57
61
|
fan_in=0.8, fan_out=0.3,
|
|
58
62
|
git_churn=1.5, code_notes=3.0,
|
|
59
63
|
exports=0.2, is_changed=2.0,
|
|
64
|
+
semantic_centrality=0.5, proximity=2.0,
|
|
60
65
|
),
|
|
61
|
-
# refactor:
|
|
66
|
+
# refactor: hub modules, coupling, technical debt, call graph hubs
|
|
62
67
|
"refactor": TaskWeights(
|
|
63
68
|
path_relevance=0.8, entrypoint=0.3,
|
|
64
69
|
fan_in=2.0, fan_out=2.0,
|
|
65
70
|
git_churn=0.3, code_notes=2.0,
|
|
66
71
|
exports=1.0, is_changed=0.3,
|
|
72
|
+
semantic_centrality=1.5, proximity=0.5,
|
|
67
73
|
),
|
|
68
|
-
# explain: stable core, entrypoints,
|
|
74
|
+
# explain: stable core, entrypoints, call graph backbone — ignore churn
|
|
69
75
|
"explain": TaskWeights(
|
|
70
76
|
path_relevance=2.0, entrypoint=3.0,
|
|
71
77
|
fan_in=0.8, fan_out=0.3,
|
|
72
78
|
git_churn=0.0, code_notes=0.0,
|
|
73
79
|
exports=0.5, is_changed=0.0,
|
|
80
|
+
semantic_centrality=1.0, proximity=0.3,
|
|
74
81
|
),
|
|
75
|
-
# onboard:
|
|
82
|
+
# onboard: entrypoints + hub modules + call graph backbone
|
|
76
83
|
"onboard": TaskWeights(
|
|
77
84
|
path_relevance=2.0, entrypoint=3.0,
|
|
78
85
|
fan_in=1.2, fan_out=0.5,
|
|
79
86
|
git_churn=0.0, code_notes=0.0,
|
|
80
87
|
exports=1.0, is_changed=0.0,
|
|
88
|
+
semantic_centrality=1.2, proximity=0.3,
|
|
81
89
|
),
|
|
82
|
-
# generate-tests:
|
|
90
|
+
# generate-tests: large public API, call graph reachability
|
|
83
91
|
"generate-tests": TaskWeights(
|
|
84
92
|
path_relevance=0.8, entrypoint=0.3,
|
|
85
93
|
fan_in=1.5, fan_out=0.8,
|
|
86
94
|
git_churn=0.5, code_notes=0.5,
|
|
87
95
|
exports=2.5, is_changed=0.5,
|
|
96
|
+
semantic_centrality=0.8, proximity=0.5,
|
|
88
97
|
),
|
|
89
|
-
# review-pr: changed files
|
|
98
|
+
# review-pr: changed files, their importers, impact radius
|
|
90
99
|
"review-pr": TaskWeights(
|
|
91
100
|
path_relevance=0.5, entrypoint=0.5,
|
|
92
101
|
fan_in=1.5, fan_out=0.5,
|
|
93
102
|
git_churn=0.5, code_notes=0.8,
|
|
94
103
|
exports=0.3, is_changed=3.0,
|
|
104
|
+
semantic_centrality=1.0, proximity=1.5,
|
|
95
105
|
),
|
|
96
|
-
# delta: changed files
|
|
106
|
+
# delta: changed files, dependency impact, call graph proximity
|
|
97
107
|
"delta": TaskWeights(
|
|
98
108
|
path_relevance=0.5, entrypoint=0.5,
|
|
99
109
|
fan_in=1.5, fan_out=0.5,
|
|
100
110
|
git_churn=0.5, code_notes=0.5,
|
|
101
111
|
exports=0.3, is_changed=3.0,
|
|
112
|
+
semantic_centrality=1.0, proximity=1.0,
|
|
102
113
|
),
|
|
103
114
|
# default: balanced, no task bias
|
|
104
115
|
"default": TaskWeights(),
|
|
@@ -139,6 +150,8 @@ class RankingEngine:
|
|
|
139
150
|
code_note_count: int = 0,
|
|
140
151
|
export_count: int = 0,
|
|
141
152
|
task: str = "default",
|
|
153
|
+
semantic_centrality: float = 0.0,
|
|
154
|
+
max_semantic: float = 1.0,
|
|
142
155
|
) -> FileScore:
|
|
143
156
|
"""Compute a scored, explained ranking for a single file.
|
|
144
157
|
|
|
@@ -203,6 +216,15 @@ class RankingEngine:
|
|
|
203
216
|
raw += 0.2 * w.is_changed
|
|
204
217
|
reasons.append("uncommitted changes")
|
|
205
218
|
|
|
219
|
+
# 9. Semantic call-graph centrality (fed by ContextScorer from --semantics)
|
|
220
|
+
if semantic_centrality > 0 and w.semantic_centrality > 0:
|
|
221
|
+
sc_norm = min(semantic_centrality / max(max_semantic, 1e-9), 1.0)
|
|
222
|
+
raw += sc_norm * 0.25 * w.semantic_centrality
|
|
223
|
+
if sc_norm >= 0.60:
|
|
224
|
+
reasons.append("call graph hub")
|
|
225
|
+
elif sc_norm >= 0.25:
|
|
226
|
+
reasons.append("call graph contributor")
|
|
227
|
+
|
|
206
228
|
# Monorepo package role
|
|
207
229
|
pkg_role = self._scorer.package_role(norm)
|
|
208
230
|
if pkg_role in _WORKSPACE_CORE_ROLES:
|
sourcecode/serializer.py
CHANGED
|
@@ -186,6 +186,21 @@ def _file_relevance(sm: SourceMap, *, limit: int = 15) -> list[dict[str, Any]]:
|
|
|
186
186
|
git_churn = {h.file: h.commit_count for h in gc.change_hotspots}
|
|
187
187
|
max_churn = max(git_churn.values(), default=1)
|
|
188
188
|
|
|
189
|
+
# Incorporate semantic hotspots from --semantics when available.
|
|
190
|
+
# Hotspots rank files by call-graph centrality (fan_in×2 + fan_out),
|
|
191
|
+
# normalised across the analysed files.
|
|
192
|
+
semantic_hub_scores: dict[str, float] = {}
|
|
193
|
+
ss = sm.semantic_summary
|
|
194
|
+
if ss and getattr(ss, "requested", False) and ss.hotspots:
|
|
195
|
+
max_importance = max(
|
|
196
|
+
(h.get("importance_score", 0.0) for h in ss.hotspots),
|
|
197
|
+
default=1.0,
|
|
198
|
+
) or 1.0
|
|
199
|
+
for h in ss.hotspots:
|
|
200
|
+
p = h.get("path", "")
|
|
201
|
+
if p:
|
|
202
|
+
semantic_hub_scores[p] = h.get("importance_score", 0.0) / max_importance
|
|
203
|
+
|
|
189
204
|
entry_paths = {ep.path for ep in sm.entry_points}
|
|
190
205
|
scored: list[tuple[float, dict[str, Any]]] = []
|
|
191
206
|
|
|
@@ -202,7 +217,9 @@ def _file_relevance(sm: SourceMap, *, limit: int = 15) -> list[dict[str, Any]]:
|
|
|
202
217
|
continue
|
|
203
218
|
|
|
204
219
|
content_rel = file_class.relevance if file_class else 0.0
|
|
205
|
-
|
|
220
|
+
# Semantic hub bonus: normalised call-graph centrality adds up to +0.30
|
|
221
|
+
sem_hub = semantic_hub_scores.get(path, 0.0) * 0.30
|
|
222
|
+
combined = fs.score + content_rel + sem_hub
|
|
206
223
|
|
|
207
224
|
if combined <= 0 and not (file_class and file_class.relevance > 0.3):
|
|
208
225
|
continue
|
|
@@ -217,6 +234,8 @@ def _file_relevance(sm: SourceMap, *, limit: int = 15) -> list[dict[str, Any]]:
|
|
|
217
234
|
}
|
|
218
235
|
|
|
219
236
|
ranking_reasons = [r for r in fs.reasons if r != "source file"]
|
|
237
|
+
if sem_hub >= 0.15:
|
|
238
|
+
ranking_reasons.append("call graph hub")
|
|
220
239
|
if ranking_reasons:
|
|
221
240
|
item["ranking_reasons"] = ranking_reasons
|
|
222
241
|
|
|
@@ -722,8 +741,10 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
722
741
|
# production runtime is represented as entry_points=[], never by fallback.
|
|
723
742
|
ep_groups = _entry_point_groups(sm.entry_points)
|
|
724
743
|
result["entry_points"] = ep_groups["production"]
|
|
725
|
-
|
|
726
|
-
|
|
744
|
+
if ep_groups["development"]:
|
|
745
|
+
result["development_entry_points"] = ep_groups["development"]
|
|
746
|
+
if ep_groups["auxiliary"]:
|
|
747
|
+
result["auxiliary_entry_points"] = ep_groups["auxiliary"]
|
|
727
748
|
|
|
728
749
|
# ── 3. Architecture ───────────────────────────────────────────────────────
|
|
729
750
|
result["architecture"] = _architecture_context(sm)
|
|
@@ -888,6 +909,23 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
888
909
|
if analysis_gaps:
|
|
889
910
|
result["analysis_gaps"] = analysis_gaps
|
|
890
911
|
|
|
912
|
+
# ── 8. Agent mode metadata — explicit transparency about auto-enabled/suppressed flags ──
|
|
913
|
+
_auto_enabled: list[str] = ["--dependencies", "--env-map", "--code-notes"]
|
|
914
|
+
_suppressed: list[str] = []
|
|
915
|
+
if sm.metrics_summary is not None and sm.metrics_summary.requested:
|
|
916
|
+
_suppressed.append("--full-metrics")
|
|
917
|
+
if sm.module_graph is not None and sm.module_graph.summary.requested:
|
|
918
|
+
_suppressed.append("--graph-modules")
|
|
919
|
+
if sm.doc_summary is not None and sm.doc_summary.requested:
|
|
920
|
+
_suppressed.append("--docs")
|
|
921
|
+
agent_mode_meta: dict[str, Any] = {
|
|
922
|
+
"auto_enabled": _auto_enabled,
|
|
923
|
+
}
|
|
924
|
+
if _suppressed:
|
|
925
|
+
agent_mode_meta["suppressed_flags"] = _suppressed
|
|
926
|
+
agent_mode_meta["suppressed_note"] = "computed but excluded from agent_view"
|
|
927
|
+
result["agent_mode"] = agent_mode_meta
|
|
928
|
+
|
|
891
929
|
return result
|
|
892
930
|
|
|
893
931
|
|
|
@@ -918,9 +956,11 @@ def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any
|
|
|
918
956
|
"architecture_summary": sm.architecture_summary,
|
|
919
957
|
"stacks": [asdict(s) for s in sm.stacks],
|
|
920
958
|
"entry_points": ep_groups["production"],
|
|
921
|
-
"development_entry_points": ep_groups["development"],
|
|
922
|
-
"auxiliary_entry_points": ep_groups["auxiliary"],
|
|
923
959
|
}
|
|
960
|
+
if ep_groups["development"]:
|
|
961
|
+
result["development_entry_points"] = ep_groups["development"]
|
|
962
|
+
if ep_groups["auxiliary"]:
|
|
963
|
+
result["auxiliary_entry_points"] = ep_groups["auxiliary"]
|
|
924
964
|
|
|
925
965
|
# Layer B — signals (only when the corresponding analyzer ran)
|
|
926
966
|
if sm.dependency_summary is not None and sm.dependency_summary.requested:
|
|
@@ -1125,6 +1165,8 @@ def _contract_view_minimal(
|
|
|
1125
1165
|
summary["degraded"] = True
|
|
1126
1166
|
summary["degraded_hint"] = "install sourcecode[ast] for full TS/JS extraction"
|
|
1127
1167
|
result["summary"] = summary
|
|
1168
|
+
if cs.symbol_truncation:
|
|
1169
|
+
result["symbol_query"] = cs.symbol_truncation
|
|
1128
1170
|
|
|
1129
1171
|
return result
|
|
1130
1172
|
|
|
@@ -1404,6 +1446,8 @@ def _contract_view_standard(
|
|
|
1404
1446
|
}
|
|
1405
1447
|
if cs.limitations:
|
|
1406
1448
|
result["contract_summary"]["limitations"] = cs.limitations
|
|
1449
|
+
if cs.symbol_truncation:
|
|
1450
|
+
result["symbol_query"] = cs.symbol_truncation
|
|
1407
1451
|
|
|
1408
1452
|
return result
|
|
1409
1453
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=2Lz--0KUPsKNbk6_eT1ko8NRsFtoDHnAHBo9uNtCIDo,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=6dh34C2qZXyRbw-8xBhbEwDdXanM6CRFRWayVoYITnA,10190
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=O4AXc7l_WTzIXrcAzstqZy-TGKNaFa6p3MzpgVjaO8g,27749
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=rSY5MRiaz4N1YdG0pqDTDuFjSN7PO_Zplx-dtNzv2Yo,19985
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=0OHQwTUBBc9lmqPLryVeB1z8dGIC6NhLlar800CD9oI,41129
|
|
6
6
|
sourcecode/classifier.py,sha256=GKTMN8qKZX7ponSwDJfN08RrasI4CVpq1_gFBgEopps,7093
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
7
|
+
sourcecode/cli.py,sha256=ON1YG8WzJ0Nb-zmlf65AzAgyCqkL1iiprJo2jwz6Pqk,68885
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
9
9
|
sourcecode/confidence_analyzer.py,sha256=HxJMPLI5ulqtkncnv98W4iVO6yMbpQo87VuxiuNbDmY,12167
|
|
10
|
+
sourcecode/context_scorer.py,sha256=nhppAo80fblAqcB9Ns0iQd21TZUrl2mQMo_xzPgavRE,14679
|
|
10
11
|
sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
|
|
11
|
-
sourcecode/contract_model.py,sha256=
|
|
12
|
-
sourcecode/contract_pipeline.py,sha256=
|
|
12
|
+
sourcecode/contract_model.py,sha256=gCf9-Kj0G7l0lvRTAcRfFAfMgs1Rpizv4mKovQLYUkw,3434
|
|
13
|
+
sourcecode/contract_pipeline.py,sha256=C3TJycL7pMRku7HQ5YbNFXxEZywtPQm8YaASRbYjs2g,24454
|
|
13
14
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
14
15
|
sourcecode/dependency_analyzer.py,sha256=Exq0BfInvfS5iAg9xAr6WI2uPNuotkIudTKcYJcRhB8,52757
|
|
15
16
|
sourcecode/doc_analyzer.py,sha256=TttdS7mndKQhyJCfJnnAsyGCJrf-TIL7oXxDlTLUFKE,21248
|
|
@@ -19,8 +20,8 @@ sourcecode/file_classifier.py,sha256=_KfFIIolharaIxbSTrCkaWauQIqNHCyor_n47RGyDh8
|
|
|
19
20
|
sourcecode/git_analyzer.py,sha256=PD3eNWydznQ6KLNpxGzBqizIHoPIKevfwz9Xyf_pDt4,11600
|
|
20
21
|
sourcecode/graph_analyzer.py,sha256=hMOsLLz9B0UnQ4xwbHdgr3bFvqpw0bQ8kN-xmEn3Krk,64156
|
|
21
22
|
sourcecode/metrics_analyzer.py,sha256=e2cFwB9XubFq_dIVsP2PLjpr4wX0N6ulb3ol3sGDUeo,20777
|
|
22
|
-
sourcecode/prepare_context.py,sha256=
|
|
23
|
-
sourcecode/ranking_engine.py,sha256=
|
|
23
|
+
sourcecode/prepare_context.py,sha256=qmxMvTlteeEGwDaNDRoRj0iGY1D7goVD_yV1MVOeQkM,32261
|
|
24
|
+
sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
|
|
24
25
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
25
26
|
sourcecode/relevance_scorer.py,sha256=E74w7nlsNVobO3LqKHiMtBd84ONwGp8uDpwXJEjRtLA,8330
|
|
26
27
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
@@ -28,7 +29,7 @@ sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBG
|
|
|
28
29
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
29
30
|
sourcecode/schema.py,sha256=ofEge9hTWHOTjeWt7ceCDQWzP-uhhenrYX2usjW2KVU,22759
|
|
30
31
|
sourcecode/semantic_analyzer.py,sha256=16EFTgM7ooW0m5gNUKOlTSn7IEMLSzKmzQn-cWaSqjs,82604
|
|
31
|
-
sourcecode/serializer.py,sha256=
|
|
32
|
+
sourcecode/serializer.py,sha256=uDYSGjNjyrI2Qqvq23dl0owfi7zUVo8bwHBJ2RlGdz8,58975
|
|
32
33
|
sourcecode/summarizer.py,sha256=ZuzIdm3t8A-d5MuQL0TSNLrd-L0IQIuguIxeNXMNJf8,16070
|
|
33
34
|
sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
|
|
34
35
|
sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
|
|
@@ -59,8 +60,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
59
60
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
60
61
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
61
62
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
62
|
-
sourcecode-0.
|
|
63
|
-
sourcecode-0.
|
|
64
|
-
sourcecode-0.
|
|
65
|
-
sourcecode-0.
|
|
66
|
-
sourcecode-0.
|
|
63
|
+
sourcecode-0.44.0.dist-info/METADATA,sha256=pxp0MxePWfJw419_NjcL3P8C2Rk4yjH9JqIKMyDuVqo,25209
|
|
64
|
+
sourcecode-0.44.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
65
|
+
sourcecode-0.44.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
66
|
+
sourcecode-0.44.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
67
|
+
sourcecode-0.44.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|