codegraph-engine 3.0.0__1-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/optimizer.py ADDED
@@ -0,0 +1,1014 @@
1
+ """Global Token Budget Optimizer, Redundancy Control, Evidence Compression, and Coverage Engine.
2
+
3
+ Maximizes evidence quality, relationship coverage, freshness, and diversity of useful
4
+ context under strict deterministic budget constraints while eliminating redundant chunks
5
+ and compressing repeated evidence without weakening epistemic guarantees.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from codegraph.task import TaskSpec
13
+
14
+ # Reason codes for selected context candidates
15
+ REASON_DIRECT_TARGET = "DIRECT_TARGET"
16
+ REASON_DIRECT_CALLER = "DIRECT_CALLER"
17
+ REASON_VERIFIED_CALLEE = "VERIFIED_CALLEE"
18
+ REASON_RELEVANT_ROUTE = "RELEVANT_ROUTE"
19
+ REASON_RELATED_TEST = "RELATED_TEST"
20
+ REASON_PACKAGE_OWNER = "PACKAGE_OWNER"
21
+ REASON_DEPENDENCY_PACKAGE = "DEPENDENCY_PACKAGE"
22
+ REASON_HIGH_VALUE_EVIDENCE = "HIGH_VALUE_EVIDENCE"
23
+ REASON_COVERAGE_FILL = "COVERAGE_FILL"
24
+ REASON_AMBIGUITY_PRESERVED = "AMBIGUITY_PRESERVED"
25
+
26
+ _EVIDENCE_CLASS_RANK = {
27
+ "AST_VERIFIED": 5,
28
+ "FRAMEWORK_VERIFIED": 4,
29
+ "DATAFLOW_VERIFIED": 3,
30
+ "POSSIBLE": 2,
31
+ "UNKNOWN": 1,
32
+ }
33
+
34
+ _CONFIDENCE_RANK = {
35
+ "HIGH": 3,
36
+ "MEDIUM": 2,
37
+ "LOW": 1,
38
+ "UNKNOWN": 0,
39
+ }
40
+
41
+
42
+ # Hard upper safety limits enforced even when omitted or exceeded
43
+ HARD_MAX_TOKENS = 20_000
44
+ HARD_MAX_FILES = 40
45
+ HARD_MAX_LINES = 1_500
46
+ HARD_MAX_SYMBOLS = 50
47
+ HARD_MAX_RELATIONSHIPS = 60
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ContextBudgetSpec:
52
+ """Explicit deterministic context budget bounds."""
53
+
54
+ max_tokens: int = 20_000
55
+ max_symbols: int = 25
56
+ max_relationships: int = 30
57
+ max_files: int = 25
58
+ max_lines: int = 500
59
+ max_depth: int = 3
60
+ max_package_depth: int = 2
61
+
62
+ def as_dict(self) -> dict[str, int]:
63
+ return {
64
+ "max_tokens": self.max_tokens,
65
+ "max_symbols": self.max_symbols,
66
+ "max_relationships": self.max_relationships,
67
+ "max_files": self.max_files,
68
+ "max_lines": self.max_lines,
69
+ "max_depth": self.max_depth,
70
+ "max_package_depth": self.max_package_depth,
71
+ }
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ContextBudget:
76
+ candidate_tokens: int
77
+ selected_tokens: int
78
+ reduction_ratio: float
79
+ budget_limit: int
80
+ rejections: tuple[dict[str, object], ...] = ()
81
+ candidate_count: int = 0
82
+ selected_count: int = 0
83
+ candidate_reduction_ratio: float = 0.0
84
+ selected_reduction_ratio: float = 0.0
85
+ budget_utilization: float = 0.0
86
+ coverage_score: float = 0.0
87
+ redundancy_ratio: float = 0.0
88
+ evidence_density: float = 0.0
89
+ direct_file_reads: int = 0
90
+ direct_file_lines: int = 0
91
+ selected_files: int = 0
92
+ selected_lines: int = 0
93
+ truncated: bool = False
94
+ tool_calls: int = 1
95
+ max_symbols: int = 25
96
+ max_relationships: int = 30
97
+ max_files: int = 25
98
+ max_lines: int = 500
99
+ max_depth: int = 3
100
+ max_package_depth: int = 2
101
+ covered_dimensions: tuple[str, ...] = ()
102
+ missing_dimensions: tuple[str, ...] = ()
103
+
104
+ def as_dict(self) -> dict[str, object]:
105
+ return {
106
+ "candidate_count": self.candidate_count,
107
+ "selected_count": self.selected_count,
108
+ "candidate_tokens": self.candidate_tokens,
109
+ "selected_tokens": self.selected_tokens,
110
+ "selected_files": self.selected_files,
111
+ "selected_lines": self.selected_lines,
112
+ "truncated": self.truncated,
113
+ "reduction_ratio": round(self.reduction_ratio, 4),
114
+ "candidate_reduction_ratio": round(self.candidate_reduction_ratio, 4),
115
+ "selected_reduction_ratio": round(self.selected_reduction_ratio, 4),
116
+ "budget_limit": self.budget_limit,
117
+ "budget_utilization": round(self.budget_utilization, 4),
118
+ "coverage_score": round(self.coverage_score, 4),
119
+ "redundancy_ratio": round(self.redundancy_ratio, 4),
120
+ "evidence_density": round(self.evidence_density, 4),
121
+ "direct_file_reads": self.direct_file_reads,
122
+ "direct_file_lines": self.direct_file_lines,
123
+ "tool_calls": self.tool_calls,
124
+ "max_symbols": self.max_symbols,
125
+ "max_relationships": self.max_relationships,
126
+ "max_files": self.max_files,
127
+ "max_lines": self.max_lines,
128
+ "max_depth": self.max_depth,
129
+ "max_package_depth": self.max_package_depth,
130
+ "covered_dimensions": list(self.covered_dimensions),
131
+ "missing_dimensions": list(self.missing_dimensions),
132
+ "rejections": list(self.rejections),
133
+ }
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class ContextCandidate:
138
+ candidate_id: str
139
+ canonical_id: str | None
140
+ source_type: str
141
+ estimated_tokens: int
142
+ relevance: float
143
+ evidence_quality: float
144
+ freshness: float
145
+ relationship_value: float
146
+ coverage_value: float
147
+ redundancy_group: str | None
148
+ status: str
149
+ confidence: str
150
+ reason_code: str = ""
151
+
152
+ def as_dict(self) -> dict[str, object]:
153
+ return {
154
+ "candidate_id": self.candidate_id,
155
+ "canonical_id": self.canonical_id,
156
+ "source_type": self.source_type,
157
+ "estimated_tokens": self.estimated_tokens,
158
+ "relevance": self.relevance,
159
+ "evidence_quality": self.evidence_quality,
160
+ "freshness": self.freshness,
161
+ "relationship_value": self.relationship_value,
162
+ "coverage_value": self.coverage_value,
163
+ "redundancy_group": self.redundancy_group,
164
+ "status": self.status,
165
+ "confidence": self.confidence,
166
+ "reason_code": self.reason_code,
167
+ }
168
+
169
+
170
+ @dataclass
171
+ class CandidateContextItem:
172
+ item_id: str
173
+ file_path: str
174
+ start_line: int
175
+ end_line: int
176
+ canonical_id: str | None
177
+ estimated_tokens: int
178
+ relevance_score: float # 0.0 - 1.0
179
+ evidence_quality: float # 0.0 - 1.0
180
+ freshness: str # FRESH | PARTIALLY_STALE | STALE | UNKNOWN
181
+ coverage_layer: str # ENTRYPOINT | HANDLER | SERVICE | DATA | TEST | GIT | ARCHITECTURE | GENERAL
182
+ source_type: str # chunk | symbol | edge | test | git | route
183
+ snippet: str
184
+ confidence: str = "HIGH"
185
+ relationship_value: float = 0.5
186
+ epistemic_status: str = "FACT" # FACT | ASSUMPTION | INFERENCE | UNKNOWN | CONFLICT | POSSIBLE | AMBIGUOUS
187
+ coverage_value: float = 0.5
188
+ data: dict[str, Any] = field(default_factory=dict)
189
+ why_selected: list[str] = field(default_factory=list)
190
+ reason_code: str = ""
191
+ package_id: str | None = None
192
+ package_distance: int = 0 # 0=owner, 1=direct dep, 2=transitive, 99=unrelated
193
+ artifact_class: str = "SOURCE"
194
+ graph_distance: int = 0
195
+ task_dimension: str = ""
196
+
197
+ @property
198
+ def relevance(self) -> float:
199
+ return self.relevance_score
200
+
201
+ @property
202
+ def status(self) -> str:
203
+ return self.epistemic_status
204
+
205
+ @property
206
+ def redundancy_group(self) -> str:
207
+ return self.duplicate_key
208
+
209
+ @property
210
+ def duplicate_key(self) -> str:
211
+ """Deterministic grouping key for redundancy detection by canonical identity."""
212
+ if self.canonical_id:
213
+ return f"sym:{self.canonical_id}"
214
+ sym = self.data.get("symbol")
215
+ if sym:
216
+ return f"sym:{self.file_path}::{sym}"
217
+ return f"loc:{self.file_path}:{self.start_line}"
218
+
219
+ @property
220
+ def line_count(self) -> int:
221
+ if self.snippet:
222
+ return max(1, len(self.snippet.splitlines()))
223
+ return max(1, self.end_line - self.start_line + 1)
224
+
225
+ def infer_dimension(self) -> str:
226
+ """Determine the task coverage dimension represented by this candidate."""
227
+ if self.task_dimension:
228
+ return self.task_dimension
229
+ if self.epistemic_status in ("UNKNOWN", "CONFLICT", "POSSIBLE", "AMBIGUOUS"):
230
+ return "UNKNOWN"
231
+ rel = str(self.data.get("relationship") or "").upper()
232
+ reasons = self.data.get("reasons", [])
233
+ reason_codes = {getattr(r, "code", "") for r in reasons}
234
+ if any(c in reason_codes for c in ("EXACT_CANONICAL", "EXACT_QUALIFIED", "EXACT_SYMBOL")):
235
+ return "TARGET"
236
+ if self.coverage_layer == "ENTRYPOINT" or rel in ("HANDLED_BY", "ROUTES_TO", "MOUNTS", "ROUTE_HANDLER"):
237
+ return "ROUTE"
238
+ if self.coverage_layer == "TEST" or rel.startswith("TEST") or self.source_type == "test":
239
+ return "TEST"
240
+ if rel in ("INJECTS", "PROVIDES", "RESOLVES_DEPENDENCY", "CONFIGURES"):
241
+ return "PROVIDER"
242
+ if rel in ("REGISTERS", "DISPATCHES_TO", "EVENT_LISTENER", "TASK_HANDLER", "COMMAND_HANDLER"):
243
+ return "REGISTRATION"
244
+ if rel == "DEPENDS_ON_PACKAGE":
245
+ return "PACKAGE"
246
+ if "DIRECT_CALLER" in reason_codes or "POSSIBLE_CALLER" in reason_codes or rel in ("CALLERS", "CALLED_BY", "POSSIBLE_CALLS"):
247
+ return "CALLER"
248
+ if "DIRECT_CALLEE" in reason_codes or rel == "CALLS":
249
+ return "CALLEE"
250
+ if self.coverage_layer == "GIT":
251
+ return "GIT"
252
+ if self.coverage_layer in ("SERVICE", "DATA", "HANDLER"):
253
+ return self.coverage_layer
254
+ return "TARGET" if self.relevance_score >= 0.7 else "GENERAL"
255
+
256
+ def infer_reason_code(self, is_coverage_fill: bool = False) -> str:
257
+ """Assign an explainable reason code for selection."""
258
+ if self.reason_code:
259
+ return self.reason_code
260
+ if self.epistemic_status in ("UNKNOWN", "CONFLICT", "POSSIBLE", "AMBIGUOUS"):
261
+ return REASON_AMBIGUITY_PRESERVED
262
+ dim = self.infer_dimension()
263
+ if dim == "TARGET":
264
+ return REASON_DIRECT_TARGET
265
+ if dim == "ROUTE":
266
+ return REASON_RELEVANT_ROUTE
267
+ if dim == "TEST":
268
+ return REASON_RELATED_TEST
269
+ if dim == "CALLER":
270
+ return REASON_DIRECT_CALLER
271
+ if dim == "CALLEE":
272
+ return REASON_VERIFIED_CALLEE
273
+ if dim in ("PROVIDER", "REGISTRATION"):
274
+ return REASON_HIGH_VALUE_EVIDENCE
275
+ if self.package_id and self.package_distance == 0:
276
+ return REASON_PACKAGE_OWNER
277
+ if self.package_id and self.package_distance in (1, 2):
278
+ return REASON_DEPENDENCY_PACKAGE
279
+ if is_coverage_fill:
280
+ return REASON_COVERAGE_FILL
281
+ return REASON_HIGH_VALUE_EVIDENCE
282
+
283
+ def to_context_candidate(self) -> ContextCandidate:
284
+ f_mult = _freshness_multiplier(self.freshness)
285
+ return ContextCandidate(
286
+ candidate_id=self.item_id,
287
+ canonical_id=self.canonical_id,
288
+ source_type=self.source_type,
289
+ estimated_tokens=self.estimated_tokens,
290
+ relevance=self.relevance_score,
291
+ evidence_quality=self.evidence_quality,
292
+ freshness=f_mult,
293
+ relationship_value=self.relationship_value,
294
+ coverage_value=self.coverage_value,
295
+ redundancy_group=self.duplicate_key,
296
+ status=self.epistemic_status,
297
+ confidence=self.confidence,
298
+ reason_code=self.reason_code or self.infer_reason_code(),
299
+ )
300
+
301
+
302
+ def _freshness_multiplier(freshness: str) -> float:
303
+ if freshness == "FRESH":
304
+ return 1.0
305
+ if freshness == "PARTIALLY_STALE":
306
+ return 0.7
307
+ if freshness == "STALE":
308
+ return 0.3
309
+ return 0.5
310
+
311
+
312
+ def _confidence_penalty(confidence: str) -> float:
313
+ if confidence == "HIGH":
314
+ return 0.0
315
+ if confidence == "MEDIUM":
316
+ return 0.1
317
+ return 0.25
318
+
319
+
320
+ def _epistemic_multiplier(status: str) -> float:
321
+ if status == "FACT":
322
+ return 1.0
323
+ if status == "ASSUMPTION":
324
+ return 0.85
325
+ if status == "INFERENCE":
326
+ return 0.70
327
+ if status == "UNKNOWN":
328
+ return 0.40
329
+ if status == "CONFLICT":
330
+ return 0.30
331
+ return 0.70
332
+
333
+
334
+ def _artifact_penalty(artifact_class: str) -> float:
335
+ cat = artifact_class.upper()
336
+ if cat in ("MINIFIED", "BUNDLE", "BUILD_ARTIFACT", "BINARY"):
337
+ return 0.50
338
+ if cat == "VENDOR":
339
+ return 0.40
340
+ if cat == "GENERATED":
341
+ return 0.20
342
+ return 0.0
343
+
344
+
345
+ def _package_adjustment(package_distance: int) -> float:
346
+ if package_distance == 0:
347
+ return 0.08 # target package bonus
348
+ if package_distance == 1:
349
+ return 0.04 # direct dependency/dependent bonus
350
+ if package_distance == 2:
351
+ return 0.0
352
+ if package_distance >= 99:
353
+ return -0.15 # unrelated workspace package penalty
354
+ return -0.05 * (package_distance - 1)
355
+
356
+
357
+ def compute_item_utility(
358
+ item: CandidateContextItem,
359
+ selected_layers: set[str],
360
+ selected_duplicate_keys: set[str],
361
+ selected_dimensions: set[str] | None = None,
362
+ ) -> float:
363
+ """Calculate deterministic utility score for a candidate context item."""
364
+ fresh_factor = _freshness_multiplier(item.freshness)
365
+ conf_penalty = _confidence_penalty(item.confidence)
366
+ epistemic_factor = _epistemic_multiplier(item.epistemic_status)
367
+ art_penalty = _artifact_penalty(item.artifact_class)
368
+ pkg_adj = _package_adjustment(item.package_distance) if item.package_id else 0.0
369
+ dist_penalty = min(0.20, 0.04 * max(0, item.graph_distance))
370
+
371
+ # Coverage reward: boost item if it introduces a required layer or dimension not yet represented
372
+ dim = item.infer_dimension()
373
+ layer_unseen = item.coverage_layer not in selected_layers
374
+ dim_unseen = selected_dimensions is not None and dim not in selected_dimensions and dim != "GENERAL"
375
+ coverage_factor = 1.3 if (layer_unseen or dim_unseen) else 1.0
376
+
377
+ # Priority boost for task-critical items over tangential ones
378
+ priority_dim_boost = 0.0
379
+ if dim == "TARGET" or item.relevance_score >= 0.80:
380
+ priority_dim_boost = 0.20
381
+ elif dim in ("ROUTE", "CALLER"):
382
+ priority_dim_boost = 0.12
383
+ elif dim == "CALLEE":
384
+ priority_dim_boost = 0.08
385
+ elif dim in ("TEST", "GIT", "PACKAGE", "GENERAL") and item.relevance_score < 0.50:
386
+ priority_dim_boost = -0.08
387
+
388
+ # Redundancy penalty: heavily discount items sharing duplicate keys with already selected items
389
+ redundancy_penalty = 0.5 if item.duplicate_key in selected_duplicate_keys else 0.0
390
+
391
+ utility = (
392
+ (item.relevance_score * 0.40)
393
+ + (item.evidence_quality * 0.20)
394
+ + (item.relationship_value * 0.15)
395
+ + (fresh_factor * 0.15)
396
+ + (epistemic_factor * 0.10)
397
+ + pkg_adj
398
+ + priority_dim_boost
399
+ ) * coverage_factor
400
+
401
+ final_score = max(0.01, utility - redundancy_penalty - conf_penalty - art_penalty - dist_penalty)
402
+ return round(final_score, 4)
403
+
404
+
405
+ def _is_excluded(item: CandidateContextItem, task_spec: TaskSpec | None) -> bool:
406
+ """Return True if this candidate violates a hard exclusion constraint.
407
+
408
+ Hierarchical rules:
409
+ - "AuthService" excludes AuthService AND AuthService.login AND AuthService.*
410
+ - "src/admin_panel" excludes any file_path starting with that prefix
411
+ - Canonical-ID comparison preferred over raw string matching
412
+ """
413
+ if not task_spec or not task_spec.exclusions:
414
+ return False
415
+ item_sym = item.canonical_id or str(item.data.get("symbol") or "")
416
+ item_file = item.file_path or ""
417
+ for excl in task_spec.exclusions:
418
+ if not excl:
419
+ continue
420
+ # ── exact symbol match ───────────────────────────────────────────
421
+ if item_sym and (item_sym == excl):
422
+ return True
423
+ # ── hierarchical: AuthService excludes AuthService.anything ──────
424
+ if item_sym and (
425
+ item_sym.startswith(excl + ".") or item_sym.startswith(excl + ":")
426
+ ):
427
+ return True
428
+ # ── exact file match ─────────────────────────────────────────────
429
+ if item_file and item_file == excl:
430
+ return True
431
+ # ── file path prefix (module/directory exclusion) ────────────────
432
+ if item_file and (item_file.startswith(excl + "/") or item_file.startswith(excl + "\\")):
433
+ return True
434
+ # ── file contains the exclusion path segment ─────────────────────
435
+ if item_file and len(excl) >= 4 and excl in item_file:
436
+ return True
437
+ # ── substring match on symbol (kept for backward compat, len >= 4) ─
438
+ if item_sym and len(excl) >= 4 and excl in item_sym:
439
+ return True
440
+ return False
441
+
442
+
443
+ def select_bounded_snippet(
444
+ content: str,
445
+ start_line: int = 1,
446
+ end_line: int | None = None,
447
+ max_lines: int = 35,
448
+ max_chars: int = 800,
449
+ ) -> tuple[str, int, int]:
450
+ """Extract a deterministic, bounded source snippet without dumping entire files.
451
+
452
+ Returns:
453
+ (bounded_snippet, requested_lines, returned_lines)
454
+ """
455
+ if not content:
456
+ return "", 0, 0
457
+ lines = content.splitlines()
458
+ total_requested = len(lines)
459
+ if total_requested == 0:
460
+ return "", 0, 0
461
+
462
+ bounded_lines = lines[:max(1, max_lines)]
463
+ text = "\n".join(bounded_lines)
464
+ if len(text) > max_chars:
465
+ text = text[:max_chars]
466
+ bounded_lines = text.splitlines()
467
+
468
+ returned_lines = len(bounded_lines)
469
+ return text, total_requested, returned_lines
470
+
471
+
472
+ def compress_relationships(
473
+ relationships: list[Any],
474
+ max_relationships: int = 30,
475
+ ) -> tuple[list[Any], dict[str, Any]]:
476
+ """Deterministically compress duplicate relationship findings while preserving traceability.
477
+
478
+ Rules:
479
+ - Group strictly by (source, target, relationship). Never merge different relationship types.
480
+ - Keep the strongest evidence class and highest confidence as the primary record.
481
+ - Retain concise supporting locations when multiple locations prove the same direct fact.
482
+ - Never drop the only evidence for a claim.
483
+ - Never convert UNKNOWN into a positive relationship or POSSIBLE into verified.
484
+ """
485
+ if not relationships:
486
+ return [], {"raw_count": 0, "compressed_count": 0, "duplicates_merged": 0}
487
+
488
+ from codegraph.evidence_contract import validate_relationship_record
489
+
490
+ grouped: dict[tuple[str, str, str], list[Any]] = {}
491
+ order_keys: list[tuple[str, str, str]] = []
492
+
493
+ for r in relationships:
494
+ validated = validate_relationship_record(r)
495
+ if isinstance(r, dict):
496
+ r["relationship"] = validated.relationship
497
+ r["evidence_class"] = validated.evidence_class
498
+ r["confidence"] = validated.confidence
499
+ r["status"] = validated.status
500
+ key = (validated.source, validated.target, validated.relationship)
501
+ if key not in grouped:
502
+ grouped[key] = []
503
+ order_keys.append(key)
504
+ grouped[key].append(r)
505
+
506
+ compressed: list[Any] = []
507
+ duplicates_merged = 0
508
+
509
+ for key in order_keys:
510
+ group = grouped[key]
511
+ if len(group) == 1:
512
+ compressed.append(group[0])
513
+ continue
514
+
515
+ duplicates_merged += len(group) - 1
516
+
517
+ # Sort group to pick the strongest primary record deterministically
518
+ def _rel_rank(item: Any) -> tuple[int, int, str, int]:
519
+ conf = str(getattr(item, "confidence", "HIGH") if not isinstance(item, dict) else item.get("confidence", "HIGH")).upper()
520
+ ev_cls = str(getattr(item, "evidence_class", "") if not isinstance(item, dict) else item.get("evidence_class", "")).upper()
521
+ f_path = str(getattr(item, "file", "") or (item.get("file", "") if isinstance(item, dict) else ""))
522
+ s_line = int(getattr(item, "start_line", 0) or (item.get("start_line", 0) if isinstance(item, dict) else 0) or 0)
523
+ return (
524
+ -_EVIDENCE_CLASS_RANK.get(ev_cls, 0),
525
+ -_CONFIDENCE_RANK.get(conf, 0),
526
+ f_path,
527
+ s_line,
528
+ )
529
+
530
+ sorted_group = sorted(group, key=_rel_rank)
531
+ primary = sorted_group[0]
532
+
533
+ # Collect concise supporting locations (up to 4 additional locations)
534
+ supporting_locs: list[str] = []
535
+ for extra in sorted_group[1:]:
536
+ ef = str(getattr(extra, "file", "") or (extra.get("file", "") if isinstance(extra, dict) else ""))
537
+ el = getattr(extra, "start_line", None) if not isinstance(extra, dict) else extra.get("start_line")
538
+ loc_str = f"{ef}:{el}" if ef and el else ef
539
+ if loc_str and loc_str not in supporting_locs:
540
+ supporting_locs.append(loc_str)
541
+
542
+ if hasattr(primary, "supporting_locations"):
543
+ try:
544
+ object.__setattr__(primary, "supporting_locations", supporting_locs[:4])
545
+ object.__setattr__(primary, "occurrence_count", len(group))
546
+ except Exception:
547
+ pass
548
+ elif isinstance(primary, dict):
549
+ primary["supporting_locations"] = supporting_locs[:4]
550
+ primary["occurrence_count"] = len(group)
551
+
552
+ compressed.append(primary)
553
+
554
+ # Sort compressed relationships deterministically:
555
+ # 1. Confidence rank desc
556
+ # 2. Source, relationship, target ascending
557
+ def _final_rel_sort(item: Any) -> tuple[int, str, str, str]:
558
+ conf = str(getattr(item, "confidence", "HIGH") if not isinstance(item, dict) else item.get("confidence", "HIGH")).upper()
559
+ src = str(getattr(item, "source", "") if not isinstance(item, dict) else item.get("source", ""))
560
+ rel = str(getattr(item, "relationship", "") if not isinstance(item, dict) else item.get("relationship", ""))
561
+ tgt = str(getattr(item, "target", "") if not isinstance(item, dict) else item.get("target", ""))
562
+ return (-_CONFIDENCE_RANK.get(conf, 0), src, rel, tgt)
563
+
564
+ compressed.sort(key=_final_rel_sort)
565
+ bounded = compressed[:max(1, max_relationships)]
566
+
567
+ stats = {
568
+ "raw_count": len(relationships),
569
+ "compressed_count": len(bounded),
570
+ "duplicates_merged": duplicates_merged,
571
+ }
572
+ return bounded, stats
573
+
574
+
575
+ def optimize_context_budget(
576
+ candidates: list[CandidateContextItem],
577
+ token_budget: int,
578
+ task_spec: TaskSpec | None = None,
579
+ budget_spec: ContextBudgetSpec | None = None,
580
+ required_dimensions: tuple[str, ...] = ("TARGET", "SERVICE", "TEST"),
581
+ verified_claims_count: int = 0,
582
+ tool_calls: int = 1,
583
+ ) -> tuple[list[CandidateContextItem], ContextBudget]:
584
+ """Deterministically select the highest-utility, coverage-preserving subset of candidates."""
585
+ raw_max_symbols = budget_spec.max_symbols if budget_spec else 25
586
+ raw_max_relationships = budget_spec.max_relationships if budget_spec else 30
587
+ raw_max_files = budget_spec.max_files if budget_spec else 25
588
+ raw_max_lines = budget_spec.max_lines if budget_spec else 500
589
+ max_depth = budget_spec.max_depth if budget_spec else 3
590
+ max_package_depth = budget_spec.max_package_depth if budget_spec else 2
591
+
592
+ was_clamped = (
593
+ token_budget > HARD_MAX_TOKENS
594
+ or raw_max_symbols > HARD_MAX_SYMBOLS
595
+ or raw_max_relationships > HARD_MAX_RELATIONSHIPS
596
+ or raw_max_files > HARD_MAX_FILES
597
+ or raw_max_lines > HARD_MAX_LINES
598
+ )
599
+ token_budget = min(token_budget, HARD_MAX_TOKENS)
600
+ max_symbols = max(1, min(raw_max_symbols, HARD_MAX_SYMBOLS))
601
+ max_relationships = max(1, min(raw_max_relationships, HARD_MAX_RELATIONSHIPS))
602
+ max_files = max(1, min(raw_max_files, HARD_MAX_FILES))
603
+ max_lines = max(1, min(raw_max_lines, HARD_MAX_LINES))
604
+
605
+ if not candidates or token_budget <= 0 or (budget_spec is not None and (budget_spec.max_files <= 0 or budget_spec.max_lines <= 0)):
606
+ return [], ContextBudget(
607
+ candidate_tokens=sum(c.estimated_tokens for c in candidates) if candidates else 0,
608
+ selected_tokens=0,
609
+ reduction_ratio=1.0 if candidates else 0.0,
610
+ budget_limit=max(0, token_budget),
611
+ candidate_count=len(candidates) if candidates else 0,
612
+ selected_count=0,
613
+ candidate_reduction_ratio=1.0 if candidates else 0.0,
614
+ selected_reduction_ratio=1.0 if candidates else 0.0,
615
+ budget_utilization=0.0,
616
+ coverage_score=0.0,
617
+ redundancy_ratio=0.0,
618
+ evidence_density=0.0,
619
+ direct_file_reads=0,
620
+ direct_file_lines=0,
621
+ selected_files=0,
622
+ selected_lines=0,
623
+ truncated=bool(candidates),
624
+ tool_calls=tool_calls,
625
+ max_symbols=max_symbols,
626
+ max_relationships=max_relationships,
627
+ max_files=max_files,
628
+ max_lines=max_lines,
629
+ max_depth=max_depth,
630
+ max_package_depth=max_package_depth,
631
+ covered_dimensions=(),
632
+ missing_dimensions=required_dimensions,
633
+ )
634
+
635
+ candidate_count_total = len(candidates)
636
+ candidate_tokens_total = sum(c.estimated_tokens for c in candidates)
637
+
638
+ # Initial deterministic sort of candidates:
639
+ # 1. Relevance score descending
640
+ # 2. Evidence quality descending
641
+ # 3. Package distance ascending
642
+ # 4. Canonical ID or file path ascending (stable tie-breaker)
643
+ # 5. Start line ascending
644
+ sorted_candidates = sorted(
645
+ candidates,
646
+ key=lambda x: (
647
+ -x.relevance_score,
648
+ -x.evidence_quality,
649
+ x.package_distance,
650
+ x.canonical_id or x.file_path,
651
+ x.start_line,
652
+ ),
653
+ )
654
+
655
+ selected: list[CandidateContextItem] = []
656
+ selected_layers: set[str] = set()
657
+ selected_dimensions: set[str] = set()
658
+ selected_duplicate_keys: set[str] = set()
659
+ selected_files: set[str] = set()
660
+ selected_tokens = 0
661
+ selected_lines = 0
662
+ selected_symbols_count = 0
663
+ snippet_trimmed = False
664
+
665
+ def _is_epistemic_uncertainty(item: CandidateContextItem) -> bool:
666
+ ev_cls = str(item.data.get("evidence_class", "")).upper()
667
+ return (
668
+ item.epistemic_status in ("UNKNOWN", "CONFLICT", "POSSIBLE", "AMBIGUOUS")
669
+ or ev_cls in ("UNKNOWN", "POSSIBLE")
670
+ or item.source_type in ("uncertainty", "finding")
671
+ )
672
+
673
+ def _can_fit(item: CandidateContextItem, is_epistemic_uncertainty: bool = False) -> bool:
674
+ if item.graph_distance > max_depth:
675
+ return False
676
+ if item.package_distance != 99 and item.package_distance > max_package_depth:
677
+ return False
678
+ if selected_tokens + item.estimated_tokens > token_budget:
679
+ return False
680
+ if not is_epistemic_uncertainty and selected_symbols_count >= max_symbols:
681
+ return False
682
+ if (
683
+ not is_epistemic_uncertainty
684
+ and item.file_path
685
+ and item.file_path not in selected_files
686
+ and len(selected_files) >= max_files
687
+ ):
688
+ return False
689
+ if (
690
+ not is_epistemic_uncertainty
691
+ and selected_lines + item.line_count > max_lines
692
+ ):
693
+ return False
694
+ return True
695
+
696
+ def _trim_to_fit(item: CandidateContextItem, is_epistemic_uncertainty: bool = False) -> bool:
697
+ nonlocal snippet_trimmed
698
+ if item.graph_distance > max_depth:
699
+ return False
700
+ if item.package_distance != 99 and item.package_distance > max_package_depth:
701
+ return False
702
+ if not is_epistemic_uncertainty and selected_symbols_count >= max_symbols:
703
+ return False
704
+ if (
705
+ not is_epistemic_uncertainty
706
+ and item.file_path
707
+ and item.file_path not in selected_files
708
+ and len(selected_files) >= max_files
709
+ ):
710
+ return False
711
+ rem_tokens = token_budget - selected_tokens
712
+ rem_lines = max_lines - selected_lines if not is_epistemic_uncertainty else max(1, max_lines - selected_lines)
713
+ if rem_tokens <= 0 or rem_lines <= 0:
714
+ return False
715
+ max_chars = max(4, rem_tokens * 4)
716
+ lines = (item.snippet or "").splitlines()
717
+ bounded_lines = lines[:rem_lines] if lines else []
718
+ trimmed_text = "\n".join(bounded_lines)
719
+ if len(trimmed_text) > max_chars:
720
+ trimmed_text = trimmed_text[:max_chars]
721
+ item.snippet = trimmed_text
722
+ item.estimated_tokens = min(rem_tokens, max(1, len(trimmed_text) // 4))
723
+ snippet_trimmed = True
724
+ return _can_fit(item, is_epistemic_uncertainty=is_epistemic_uncertainty)
725
+
726
+ def _accept(item: CandidateContextItem, is_coverage_fill: bool, why: list[str]) -> None:
727
+ nonlocal selected_tokens, selected_lines, selected_symbols_count
728
+ item.reason_code = item.infer_reason_code(is_coverage_fill=is_coverage_fill)
729
+ item.why_selected = [f"Reason: {item.reason_code}"] + why
730
+ selected.append(item)
731
+ selected_tokens += item.estimated_tokens
732
+ selected_lines += item.line_count
733
+ if not _is_epistemic_uncertainty(item):
734
+ selected_symbols_count += 1
735
+ selected_layers.add(item.coverage_layer)
736
+ selected_dimensions.add(item.infer_dimension())
737
+ selected_duplicate_keys.add(item.duplicate_key)
738
+ if item.file_path:
739
+ selected_files.add(item.file_path)
740
+
741
+ # Pass 0: Preserve high-value UNKNOWN / POSSIBLE / CONFLICT / AMBIGUOUS findings
742
+ for item in sorted_candidates:
743
+ if _is_excluded(item, task_spec):
744
+ continue
745
+ if _is_epistemic_uncertainty(item) and item.relevance_score >= 0.2:
746
+ if item.duplicate_key not in selected_duplicate_keys:
747
+ if _can_fit(item, is_epistemic_uncertainty=True) or _trim_to_fit(item, is_epistemic_uncertainty=True):
748
+ _accept(
749
+ item,
750
+ is_coverage_fill=True,
751
+ why=[
752
+ f"Epistemic preservation: '{item.epistemic_status}'",
753
+ f"Relevance: {item.relevance_score:.2f}",
754
+ ],
755
+ )
756
+
757
+ # Pass 1a: Task Dimension Coverage pass — ensure required task dimensions
758
+ # (e.g. TARGET, CALLER, CALLEE, ROUTE, REGISTRATION, PROVIDER, TEST, PACKAGE)
759
+ # get represented before filling with repetitive candidates of the same dimension
760
+ for req_dim in required_dimensions:
761
+ if req_dim in selected_dimensions:
762
+ continue
763
+ for item in sorted_candidates:
764
+ if item in selected or _is_excluded(item, task_spec):
765
+ continue
766
+ if item.relevance_score < 0.2:
767
+ continue
768
+ if item.duplicate_key in selected_duplicate_keys:
769
+ continue
770
+ if item.infer_dimension() == req_dim:
771
+ if _can_fit(item) or (req_dim == "TARGET" and len(selected) == 0 and _trim_to_fit(item)):
772
+ _accept(
773
+ item,
774
+ is_coverage_fill=True,
775
+ why=[
776
+ f"Task dimension coverage guarantee: '{req_dim}'",
777
+ f"Relevance: {item.relevance_score:.2f}",
778
+ ],
779
+ )
780
+ break
781
+
782
+ # Pass 1b: Task-Critical Candidates Priority pass — ensure all direct targets,
783
+ # routes, and direct callers/callees are admitted into the file budget before
784
+ # tangential layer coverage files consume file slots.
785
+ for item in sorted_candidates:
786
+ if item in selected or _is_excluded(item, task_spec):
787
+ continue
788
+ if item.duplicate_key in selected_duplicate_keys:
789
+ continue
790
+ dim = item.infer_dimension()
791
+ is_task_critical = (
792
+ dim in ("TARGET", "ROUTE", "CALLER")
793
+ or item.relevance_score >= 0.70
794
+ or (dim == "CALLEE" and item.relevance_score >= 0.50)
795
+ )
796
+ if is_task_critical:
797
+ if _can_fit(item) or (dim == "TARGET" and len(selected) == 0 and _trim_to_fit(item)):
798
+ _accept(
799
+ item,
800
+ is_coverage_fill=False,
801
+ why=[
802
+ f"Task-critical priority candidate: dimension '{dim}'",
803
+ f"Relevance: {item.relevance_score:.2f}, Evidence: {item.evidence_quality:.2f}",
804
+ ],
805
+ )
806
+
807
+ # Pass 1c: Architectural Layer Coverage pass — ensure distinct layers
808
+ # (e.g. ENTRYPOINT, SERVICE, DATA, TEST, GIT) get represented if candidates exist
809
+ for item in sorted_candidates:
810
+ if item in selected or _is_excluded(item, task_spec):
811
+ continue
812
+ if item.relevance_score < 0.2:
813
+ continue
814
+ if item.duplicate_key in selected_duplicate_keys:
815
+ continue
816
+ if item.coverage_layer not in selected_layers and item.coverage_layer != "GENERAL":
817
+ if _can_fit(item):
818
+ _accept(
819
+ item,
820
+ is_coverage_fill=True,
821
+ why=[
822
+ f"Coverage layer guarantee: '{item.coverage_layer}'",
823
+ f"Relevance: {item.relevance_score:.2f}",
824
+ ],
825
+ )
826
+
827
+ # Pass 2: Utility-driven knapsack pass for remaining budget
828
+ remaining_candidates = [c for c in sorted_candidates if c not in selected]
829
+
830
+ scored_pool: list[tuple[float, CandidateContextItem]] = []
831
+ for item in remaining_candidates:
832
+ u = compute_item_utility(item, selected_layers, selected_duplicate_keys, selected_dimensions)
833
+ scored_pool.append((u, item))
834
+
835
+ # Sort remaining pool deterministically by utility desc, item id asc
836
+ scored_pool.sort(
837
+ key=lambda pair: (
838
+ -pair[0],
839
+ -pair[1].relevance_score,
840
+ pair[1].package_distance,
841
+ pair[1].canonical_id or pair[1].file_path,
842
+ pair[1].start_line,
843
+ )
844
+ )
845
+
846
+ for utility, item in scored_pool:
847
+ if _is_excluded(item, task_spec):
848
+ continue
849
+ # Strict duplicate elimination by canonical identity
850
+ if item.duplicate_key in selected_duplicate_keys:
851
+ continue
852
+ # Minimum relevance threshold to avoid packing irrelevant items
853
+ if item.relevance_score < 0.35 or utility < 0.25:
854
+ continue
855
+ if _can_fit(item):
856
+ _accept(
857
+ item,
858
+ is_coverage_fill=False,
859
+ why=[
860
+ f"Knapsack utility score: {utility:.2f}",
861
+ f"Relevance: {item.relevance_score:.2f}, Evidence: {item.evidence_quality:.2f}, Freshness: {item.freshness}",
862
+ ],
863
+ )
864
+
865
+ # Fallback Pass: If tight budget prevented any candidate from fitting, trim the top non-excluded candidate
866
+ if not selected:
867
+ for item in sorted_candidates:
868
+ if _is_excluded(item, task_spec):
869
+ continue
870
+ if _trim_to_fit(item):
871
+ _accept(
872
+ item,
873
+ is_coverage_fill=True,
874
+ why=[
875
+ "Primary target preserved under tight budget via bounded snippet truncation",
876
+ f"Relevance: {item.relevance_score:.2f}",
877
+ ],
878
+ )
879
+ break
880
+
881
+ # Final deterministic ordering for presentation:
882
+ # Coverage layers first (ENTRYPOINT -> HANDLER -> SERVICE -> DATA -> TEST -> GIT -> ARCHITECTURE -> GENERAL),
883
+ # then relevance descending, file path, and line number
884
+ _LAYER_ORDER = {
885
+ "ENTRYPOINT": 0,
886
+ "HANDLER": 1,
887
+ "SERVICE": 2,
888
+ "DATA": 3,
889
+ "TEST": 4,
890
+ "GIT": 5,
891
+ "ARCHITECTURE": 6,
892
+ "GENERAL": 7,
893
+ }
894
+
895
+ selected.sort(
896
+ key=lambda x: (
897
+ _LAYER_ORDER.get(x.coverage_layer, 9),
898
+ -x.relevance_score,
899
+ x.file_path,
900
+ x.start_line,
901
+ )
902
+ )
903
+
904
+ token_reduction = (
905
+ 1.0 - (selected_tokens / max(candidate_tokens_total, 1))
906
+ if candidate_tokens_total > 0
907
+ else 0.0
908
+ )
909
+ cand_reduction = (
910
+ 1.0 - (len(selected) / max(candidate_count_total, 1))
911
+ if candidate_count_total > 0
912
+ else 0.0
913
+ )
914
+ utilization = min(1.0, selected_tokens / max(token_budget, 1))
915
+
916
+ # Coverage score over available required dimensions
917
+ available_dims = {c.infer_dimension() for c in candidates if not _is_excluded(c, task_spec)}
918
+ effective_req_dims = [d for d in required_dimensions if d in available_dims] or list(required_dimensions)
919
+ covered_dims = tuple(d for d in required_dimensions if d in selected_dimensions)
920
+ missing_dims = tuple(d for d in required_dimensions if d not in selected_dimensions)
921
+ if effective_req_dims:
922
+ cov_score = sum(1 for d in effective_req_dims if d in selected_dimensions) / len(effective_req_dims)
923
+ else:
924
+ cov_score = 1.0 if selected else 0.0
925
+
926
+ # Redundancy ratio in selected items (should be 0.0 due to canonical deduplication)
927
+ seen_sel_keys: set[str] = set()
928
+ dup_sel = 0
929
+ for s in selected:
930
+ if s.duplicate_key in seen_sel_keys:
931
+ dup_sel += 1
932
+ else:
933
+ seen_sel_keys.add(s.duplicate_key)
934
+ redundancy_ratio = dup_sel / max(len(selected), 1)
935
+
936
+ # Evidence density: verified claims (selected symbols + verified relationships) per 100 selected tokens
937
+ total_claims = len(selected) + max(0, verified_claims_count)
938
+ evidence_density = (total_claims / max(selected_tokens / 100.0, 1.0)) if selected_tokens > 0 else 0.0
939
+
940
+ rejections_list: list[dict[str, object]] = []
941
+ selected_ids = {s.item_id for s in selected}
942
+ budget_rejected_count = 0
943
+ for item in candidates:
944
+ if item.item_id not in selected_ids:
945
+ if _is_excluded(item, task_spec):
946
+ reason = "Hard exclusion constraint"
947
+ elif item.duplicate_key in selected_duplicate_keys:
948
+ reason = f"Duplicate of selected symbol/chunk ({item.duplicate_key})"
949
+ elif item.graph_distance > max_depth:
950
+ reason = f"Graph depth limit reached ({max_depth} hops)"
951
+ budget_rejected_count += 1
952
+ elif item.package_distance != 99 and item.package_distance > max_package_depth:
953
+ reason = f"Package depth limit reached ({max_package_depth} hops)"
954
+ budget_rejected_count += 1
955
+ elif selected_symbols_count >= max_symbols:
956
+ reason = f"Symbol budget limit reached ({max_symbols} symbols)"
957
+ budget_rejected_count += 1
958
+ elif item.file_path not in selected_files and len(selected_files) >= max_files:
959
+ reason = f"File budget limit reached ({max_files} files)"
960
+ budget_rejected_count += 1
961
+ elif selected_lines + item.line_count > max_lines:
962
+ reason = f"Line budget limit reached ({max_lines} lines)"
963
+ budget_rejected_count += 1
964
+ elif item.estimated_tokens + selected_tokens > token_budget:
965
+ reason = f"Token budget limit reached ({token_budget} tokens)"
966
+ budget_rejected_count += 1
967
+ elif item.freshness == "STALE":
968
+ reason = "Stale source hash on disk"
969
+ else:
970
+ reason = f"Lower utility/relevance ({item.relevance_score:.2f}) compared to selected candidates"
971
+ rejections_list.append(
972
+ {
973
+ "item_id": item.item_id,
974
+ "file": item.file_path,
975
+ "start_line": item.start_line,
976
+ "symbol": item.canonical_id or item.data.get("symbol"),
977
+ "reason": reason,
978
+ "relevance": item.relevance_score,
979
+ }
980
+ )
981
+
982
+ is_truncated = bool(was_clamped or snippet_trimmed or budget_rejected_count > 0)
983
+
984
+ budget = ContextBudget(
985
+ candidate_tokens=candidate_tokens_total,
986
+ selected_tokens=selected_tokens,
987
+ reduction_ratio=round(token_reduction, 4),
988
+ budget_limit=token_budget,
989
+ rejections=tuple(rejections_list[:50]),
990
+ candidate_count=candidate_count_total,
991
+ selected_count=len(selected),
992
+ candidate_reduction_ratio=round(cand_reduction, 4),
993
+ selected_reduction_ratio=round(token_reduction, 4),
994
+ budget_utilization=round(utilization, 4),
995
+ coverage_score=round(cov_score, 4),
996
+ redundancy_ratio=round(redundancy_ratio, 4),
997
+ evidence_density=round(evidence_density, 4),
998
+ direct_file_reads=len(selected_files),
999
+ direct_file_lines=selected_lines,
1000
+ selected_files=len(selected_files),
1001
+ selected_lines=selected_lines,
1002
+ truncated=is_truncated,
1003
+ tool_calls=tool_calls,
1004
+ max_symbols=max_symbols,
1005
+ max_relationships=max_relationships,
1006
+ max_files=max_files,
1007
+ max_lines=max_lines,
1008
+ max_depth=max_depth,
1009
+ max_package_depth=max_package_depth,
1010
+ covered_dimensions=covered_dims,
1011
+ missing_dimensions=missing_dims,
1012
+ )
1013
+
1014
+ return selected, budget