codegraph-engine 2.1.2__py3-none-any.whl → 2.1.7__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 (61) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +1961 -0
  3. codegraph/agent_capabilities.py +3500 -0
  4. codegraph/agent_rules.py +530 -0
  5. codegraph/architecture.py +48 -5
  6. codegraph/binding_resolver.py +616 -0
  7. codegraph/cli.py +345 -5
  8. codegraph/config.py +2 -0
  9. codegraph/context.py +845 -93
  10. codegraph/database/__init__.py +58 -0
  11. codegraph/database/extractor.py +2956 -0
  12. codegraph/database/interrogation.py +1085 -0
  13. codegraph/database/models.py +310 -0
  14. codegraph/epistemic.py +95 -0
  15. codegraph/errors.py +7 -0
  16. codegraph/evidence/citations.py +22 -1
  17. codegraph/evidence_contract.py +455 -0
  18. codegraph/frameworks.py +372 -11
  19. codegraph/git.py +92 -10
  20. codegraph/graph/models.py +15 -0
  21. codegraph/graph/traversal.py +680 -121
  22. codegraph/indexing/__init__.py +15 -1
  23. codegraph/indexing/classifier.py +343 -58
  24. codegraph/indexing/indexer.py +1515 -141
  25. codegraph/indexing/models.py +75 -0
  26. codegraph/indexing/parser.py +1843 -70
  27. codegraph/indexing/scanner.py +4 -1
  28. codegraph/indexing/telemetry.py +326 -0
  29. codegraph/installer.py +1652 -0
  30. codegraph/interrogation.py +708 -122
  31. codegraph/mcp/server.py +525 -107
  32. codegraph/mcp_diagnostics.py +879 -0
  33. codegraph/models.py +14 -0
  34. codegraph/monorepo.py +922 -0
  35. codegraph/observability.py +113 -2
  36. codegraph/optimizer.py +647 -42
  37. codegraph/ranking.py +61 -16
  38. codegraph/resolver.py +2006 -77
  39. codegraph/resources/monitor.py +1 -1
  40. codegraph/retrieval_policy.py +96 -17
  41. codegraph/route_composer.py +363 -0
  42. codegraph/runtime/__init__.py +32 -0
  43. codegraph/runtime/ingestor.py +672 -0
  44. codegraph/runtime/models.py +130 -0
  45. codegraph/runtime/reconciliation.py +492 -0
  46. codegraph/search/__init__.py +2 -0
  47. codegraph/search/hybrid.py +572 -53
  48. codegraph/security/__init__.py +50 -2
  49. codegraph/security/paths.py +162 -8
  50. codegraph/security/redaction.py +633 -0
  51. codegraph/semantic_decorators.py +341 -0
  52. codegraph/target_resolver.py +6 -6
  53. codegraph/tool_selection_eval.py +1158 -0
  54. codegraph_engine-2.1.7.dist-info/METADATA +617 -0
  55. codegraph_engine-2.1.7.dist-info/RECORD +84 -0
  56. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/licenses/LICENSE +1 -1
  57. codegraph_engine-2.1.2.dist-info/METADATA +0 -336
  58. codegraph_engine-2.1.2.dist-info/RECORD +0 -63
  59. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/WHEEL +0 -0
  60. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/entry_points.txt +0 -0
  61. {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/top_level.txt +0 -0
codegraph/context.py CHANGED
@@ -44,8 +44,12 @@ from codegraph.observability import (
44
44
  get_global_metrics,
45
45
  )
46
46
  from codegraph.optimizer import (
47
+ HARD_MAX_LINES,
47
48
  CandidateContextItem,
49
+ ContextBudgetSpec,
50
+ compress_relationships,
48
51
  optimize_context_budget,
52
+ select_bounded_snippet,
49
53
  )
50
54
  from codegraph.planner import RetrievalPlan, build_retrieval_plan
51
55
  from codegraph.query_expansion import QueryExpansion, expand_query_terms, get_search_queries
@@ -92,6 +96,8 @@ class SymbolRef(BaseModel):
92
96
  score: float
93
97
  reasons: list[RankingReasonModel] = []
94
98
  canonical_id: str | None = None
99
+ reason_code: str = ""
100
+ package_id: str | None = None
95
101
 
96
102
 
97
103
  class Relationship(BaseModel):
@@ -102,7 +108,19 @@ class Relationship(BaseModel):
102
108
  file: str | None = None
103
109
  start_line: int | None = None
104
110
  evidence: str = ""
105
- status: str = "FACT" # FACT | ASSUMPTION | INFERENCE | UNKNOWN | CONFLICT
111
+ status: str = "" # FACT | ASSUMPTION | INFERENCE | UNKNOWN | CONFLICT
112
+ evidence_class: str = ""
113
+ supporting_locations: list[str] = []
114
+ occurrence_count: int = 1
115
+
116
+ def model_post_init(self, __context: Any) -> None:
117
+ from codegraph.evidence_contract import validate_relationship_record
118
+
119
+ validated = validate_relationship_record(self)
120
+ object.__setattr__(self, "relationship", validated.relationship)
121
+ object.__setattr__(self, "evidence_class", validated.evidence_class)
122
+ object.__setattr__(self, "confidence", validated.confidence)
123
+ object.__setattr__(self, "status", validated.status)
106
124
 
107
125
 
108
126
  class TestRef(BaseModel):
@@ -158,6 +176,18 @@ class ContextPacket(BaseModel):
158
176
  unknowns: list[str] = []
159
177
  conflicts: list[str] = []
160
178
 
179
+ # Phase 10 & Phase 14 structured ContextPacket sections
180
+ metadata: dict[str, object] = {}
181
+ target: dict[str, object] = {}
182
+ routes: list[dict[str, object]] = []
183
+ packages: list[dict[str, object]] = []
184
+ git_impact: dict[str, object] = {}
185
+ findings: list[dict[str, object]] = []
186
+ coverage: dict[str, object] = {}
187
+ database: list[dict[str, object]] = []
188
+ runtime: list[dict[str, object]] = []
189
+ reconciliation: list[dict[str, object]] = []
190
+
161
191
  # Knowledge
162
192
  facts: list[dict[str, object]] = field(default_factory=list)
163
193
  symbols: list[SymbolRef] = []
@@ -167,7 +197,12 @@ class ContextPacket(BaseModel):
167
197
  evidence: list[EvidenceRef] = []
168
198
  uncertainties: list[str] = []
169
199
 
170
- # Token budget
200
+ # Token budget & explicit boundary metadata
201
+ candidate_tokens: int = 0
202
+ selected_tokens: int = 0
203
+ selected_lines: int = 0
204
+ coverage_score: float = 0.0
205
+ truncated: bool = False
171
206
  candidate_token_estimate: int = 0
172
207
  selected_token_estimate: int = 0
173
208
  context_reduction_pct: float = 0.0
@@ -179,6 +214,9 @@ class ContextPacket(BaseModel):
179
214
  def as_dict(self) -> dict[str, object]:
180
215
  return self.model_dump()
181
216
 
217
+ def to_dict(self) -> dict[str, object]:
218
+ return self.model_dump()
219
+
182
220
 
183
221
  # ---------------------------------------------------------------------------
184
222
  # Token estimation helper
@@ -191,6 +229,67 @@ def _token_estimate(text: str) -> int:
191
229
  return max(1, len(text) // _CHARS_PER_TOKEN)
192
230
 
193
231
 
232
+ def _resolve_task_or_query_input(
233
+ task: str | TaskSpec | dict[str, Any] | None = "",
234
+ query: str | TaskSpec | dict[str, Any] | None = "",
235
+ ) -> str | TaskSpec | dict[str, Any]:
236
+ """Resolve canonical `query` / `task` inputs with strict conflict validation."""
237
+ def _is_nonempty(val: Any) -> bool:
238
+ if val is None:
239
+ return False
240
+ if isinstance(val, str):
241
+ return bool(val.strip())
242
+ if isinstance(val, dict):
243
+ return bool(val)
244
+ return True
245
+
246
+ has_task = _is_nonempty(task)
247
+ has_query = _is_nonempty(query)
248
+
249
+ if not has_task and not has_query:
250
+ raise ValueError("Either 'query' or 'task' must be provided to get_context (non-empty string or TaskSpec).")
251
+
252
+ if has_task and has_query:
253
+ if isinstance(task, str) and isinstance(query, str):
254
+ if task.strip() != query.strip():
255
+ raise ValueError(
256
+ f"Conflicting 'query' ({query.strip()!r}) and 'task' ({task.strip()!r}) provided; "
257
+ "pass one or identical values."
258
+ )
259
+ return query.strip()
260
+ if isinstance(task, dict) and isinstance(query, str):
261
+ dict_prompt = str(task.get("raw_prompt") or task.get("goal") or task.get("task") or task.get("query") or "").strip()
262
+ if dict_prompt and dict_prompt != query.strip():
263
+ raise ValueError(
264
+ f"Conflicting 'query' ({query.strip()!r}) and 'task' prompt ({dict_prompt!r}) provided."
265
+ )
266
+ if not dict_prompt:
267
+ merged = dict(task)
268
+ merged["raw_prompt"] = query.strip()
269
+ return merged
270
+ return task
271
+ if isinstance(query, dict) and isinstance(task, str):
272
+ dict_prompt = str(query.get("raw_prompt") or query.get("goal") or query.get("task") or query.get("query") or "").strip()
273
+ if dict_prompt and dict_prompt != task.strip():
274
+ raise ValueError(
275
+ f"Conflicting 'query' prompt ({dict_prompt!r}) and 'task' ({task.strip()!r}) provided."
276
+ )
277
+ if not dict_prompt:
278
+ merged = dict(query)
279
+ merged["raw_prompt"] = task.strip()
280
+ return merged
281
+ return query
282
+ if task != query:
283
+ raise ValueError("Conflicting 'query' and 'task' arguments provided to get_context.")
284
+ return task # type: ignore[return-value]
285
+
286
+ resolved = query if has_query else task
287
+ if isinstance(resolved, str):
288
+ return resolved.strip()
289
+ assert resolved is not None
290
+ return resolved
291
+
292
+
194
293
  # ---------------------------------------------------------------------------
195
294
  # Main Context Compiler
196
295
  # ---------------------------------------------------------------------------
@@ -199,25 +298,43 @@ def _token_estimate(text: str) -> int:
199
298
  def get_context(
200
299
  con: sqlite3.Connection,
201
300
  repository: Path,
202
- task: str | TaskSpec | dict[str, Any],
301
+ task: str | TaskSpec | dict[str, Any] = "",
203
302
  intent: Intent | str | None = None,
204
303
  max_tokens: int = 20_000,
205
304
  top_k: int = 15,
206
305
  plan: RetrievalPlan | dict[str, Any] | None = None,
207
306
  mode: str = "BALANCED", # FAST | BALANCED | DEEP
208
307
  explain: bool = False,
308
+ include_generated: bool = False,
309
+ include_vendor: bool = False,
310
+ include_bundles: bool = False,
311
+ include_build_artifacts: bool = False,
312
+ max_symbols: int = 25,
313
+ max_relationships: int = 30,
314
+ max_files: int = 15,
315
+ max_lines: int = 500,
316
+ max_depth: int | None = None,
317
+ max_package_depth: int | None = None,
318
+ query: str | TaskSpec | dict[str, Any] = "",
209
319
  ) -> ContextPacket:
210
- """Compile a deterministic, verified ContextPacket for the given task and intent."""
320
+ """Compile a deterministic, verified ContextPacket for the given query/task and intent."""
321
+ effective_task = _resolve_task_or_query_input(task=task, query=query)
322
+ if max_tokens <= 0 or max_files <= 0 or max_lines <= 0:
323
+ raise ValueError("max_tokens, max_files, and max_lines must be positive integers (> 0).")
211
324
  governor = get_global_governor()
212
325
  coalescer = get_global_coalescer()
213
- flight_key = f"{repository}:{task}:{intent}:{max_tokens}:{top_k}:{mode}:{explain}"
326
+ flight_key = (
327
+ f"{repository}:{effective_task}:{intent}:{max_tokens}:{top_k}:{mode}:{explain}:"
328
+ f"{include_generated}:{include_vendor}:{include_bundles}:{include_build_artifacts}:"
329
+ f"{max_symbols}:{max_relationships}:{max_files}:{max_lines}:{max_depth}:{max_package_depth}"
330
+ )
214
331
 
215
332
  def _execute() -> ContextPacket:
216
333
  with governor.task_scope(TaskPriority.INTERACTIVE_HIGH):
217
334
  return _get_context_impl(
218
335
  con=con,
219
336
  repository=repository,
220
- task=task,
337
+ task=effective_task,
221
338
  intent=intent,
222
339
  max_tokens=max_tokens,
223
340
  top_k=top_k,
@@ -225,6 +342,16 @@ def get_context(
225
342
  mode=mode,
226
343
  explain=explain,
227
344
  governor=governor,
345
+ include_generated=include_generated,
346
+ include_vendor=include_vendor,
347
+ include_bundles=include_bundles,
348
+ include_build_artifacts=include_build_artifacts,
349
+ max_symbols=max_symbols,
350
+ max_relationships=max_relationships,
351
+ max_files=max_files,
352
+ max_lines=max_lines,
353
+ max_depth=max_depth,
354
+ max_package_depth=max_package_depth,
228
355
  )
229
356
 
230
357
  return coalescer.coalesce(flight_key, _execute)
@@ -241,6 +368,16 @@ def _get_context_impl(
241
368
  mode: str = "BALANCED", # FAST | BALANCED | DEEP
242
369
  explain: bool = False,
243
370
  governor: Any = None,
371
+ include_generated: bool = False,
372
+ include_vendor: bool = False,
373
+ include_bundles: bool = False,
374
+ include_build_artifacts: bool = False,
375
+ max_symbols: int = 25,
376
+ max_relationships: int = 30,
377
+ max_files: int = 15,
378
+ max_lines: int = 500,
379
+ max_depth: int | None = None,
380
+ max_package_depth: int | None = None,
244
381
  ) -> ContextPacket:
245
382
  if governor is None:
246
383
  governor = get_global_governor()
@@ -307,11 +444,22 @@ def _get_context_impl(
307
444
  )
308
445
 
309
446
  gen = index_generation(con)
447
+ runtime_edge_count = 0
448
+ try:
449
+ row_rt = con.execute("SELECT count(*), COALESCE(MAX(runtime_generation), 0) FROM runtime_edges").fetchone()
450
+ if row_rt:
451
+ runtime_edge_count = int(row_rt[0] or 0) + int(row_rt[1] or 0) * 1000
452
+ except Exception:
453
+ pass
310
454
  cache_key = compute_context_cache_key(
311
455
  task_spec=task_spec,
312
456
  repository_generation=gen,
313
457
  max_tokens=max_tokens,
314
- extra_config=f"{caller_raw_intent or ''}:{mode_upper}",
458
+ extra_config=(
459
+ f"{caller_raw_intent or ''}:{mode_upper}:{top_k}:"
460
+ f"{include_generated}:{include_vendor}:{include_bundles}:{include_build_artifacts}:"
461
+ f"{max_symbols}:{max_relationships}:{max_files}:{max_lines}:{max_depth}:{max_package_depth}:{runtime_edge_count}"
462
+ ),
315
463
  )
316
464
 
317
465
  # 2. Check Cache
@@ -329,7 +477,13 @@ def _get_context_impl(
329
477
  mode=mode_upper,
330
478
  timing=timing,
331
479
  )
480
+ old_exec = cached_data.get("execution", {}) if isinstance(cached_data, dict) else {}
332
481
  packet.execution = exec_meta.as_dict()
482
+ packet.execution["excluded_count"] = old_exec.get("excluded_count", 0)
483
+ packet.execution["excluded_artifact_classes"] = old_exec.get("excluded_artifact_classes", [])
484
+ packet.execution["artifact_inclusions"] = old_exec.get("artifact_inclusions", [])
485
+ packet.execution["package_context"] = old_exec.get("package_context")
486
+ packet.execution["tool_efficiency"] = old_exec.get("tool_efficiency", {"tool_calls": 1, "cache_hit": True})
333
487
  packet.execution["resource"] = {
334
488
  "profile": governor.policy.profile.value,
335
489
  "pressure": governor.get_pressure().value,
@@ -442,13 +596,31 @@ def _get_context_impl(
442
596
  c.row_factory = sqlite3.Row
443
597
  try:
444
598
  for query in queries:
445
- for item in search(c, query, top_k=effective_top_k):
599
+ for item in search(
600
+ c,
601
+ query,
602
+ top_k=effective_top_k,
603
+ include_generated=include_generated,
604
+ include_vendor=include_vendor,
605
+ include_bundles=include_bundles,
606
+ include_build_artifacts=include_build_artifacts,
607
+ include_text_files=False,
608
+ ):
446
609
  items.append(item.as_dict())
447
610
  finally:
448
611
  c.close()
449
612
  else:
450
613
  for query in queries:
451
- for item in search(con, query, top_k=effective_top_k):
614
+ for item in search(
615
+ con,
616
+ query,
617
+ top_k=effective_top_k,
618
+ include_generated=include_generated,
619
+ include_vendor=include_vendor,
620
+ include_bundles=include_bundles,
621
+ include_build_artifacts=include_build_artifacts,
622
+ include_text_files=False,
623
+ ):
452
624
  items.append(item.as_dict())
453
625
  return items
454
626
 
@@ -564,6 +736,7 @@ def _get_context_impl(
564
736
  "start_line": r_line,
565
737
  "end_line": r_line,
566
738
  "score": 0.95,
739
+ "relationship": "HANDLED_BY",
567
740
  "snippet": f"Endpoint: {ep_id} handled by {r['handler_name']}",
568
741
  }
569
742
  )
@@ -576,6 +749,7 @@ def _get_context_impl(
576
749
  start_line=r_line,
577
750
  evidence=f"Route definition for {ep_id}",
578
751
  status="FACT",
752
+ evidence_class="FRAMEWORK_VERIFIED",
579
753
  )
580
754
  route_relationships.append(route_rel)
581
755
  entry_points_out.append(
@@ -665,20 +839,121 @@ def _get_context_impl(
665
839
  _seen_seeds.add(s_sym)
666
840
  ordered_seeds.append(s_sym)
667
841
 
842
+ effective_max_depth = max_depth if max_depth is not None else _retrieval_policy.max_graph_depth
843
+ effective_pkg_depth = max_package_depth if max_package_depth is not None else _retrieval_policy.max_package_depth
844
+
668
845
  # Bounded seed policy
669
846
  graph_seed_policy = GraphSeedPolicy(
670
847
  source_type="EXPLICIT_TARGET",
671
848
  min_confidence="HIGH",
672
849
  max_seeds=max_graph_syms,
673
- max_depth=_retrieval_policy.max_graph_depth,
850
+ max_depth=effective_max_depth,
674
851
  allowed_edges=_retrieval_policy.allowed_relationship_types,
675
852
  )
676
853
 
677
- for sym in ordered_seeds[:graph_seed_policy.max_seeds]:
854
+ # Monorepo / Package boundary detection early for package-aware limiting
855
+ from codegraph.monorepo import detect_workspace
856
+ ws_info = detect_workspace(repository, con=con)
857
+ target_pkg = None
858
+ for t in task_spec.targets:
859
+ pkg = ws_info.get_package_for_file(t)
860
+ if pkg and pkg.root_path != "":
861
+ target_pkg = pkg
862
+ break
863
+ if not target_pkg:
864
+ for t in task_spec.targets:
865
+ pkg = ws_info.get_package_for_file(t)
866
+ if pkg:
867
+ target_pkg = pkg
868
+ break
869
+ if not target_pkg and ordered_seeds:
870
+ for s in ordered_seeds:
871
+ s_row = con.execute(
872
+ "SELECT path FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? LIMIT 1",
873
+ (s, s, s),
874
+ ).fetchone()
875
+ if s_row:
876
+ pkg = ws_info.get_package_for_file(str(s_row[0]))
877
+ if pkg:
878
+ target_pkg = pkg
879
+ break
880
+
881
+ # Compute bounded package distance map (0=target, 1=direct dep/dependent, ..d=transitive, 99=unrelated)
882
+ pkg_distances: dict[str, int] = {}
883
+ if target_pkg:
884
+ pkg_distances[target_pkg.package_id] = 0
885
+ frontier = [target_pkg.package_id]
886
+ for dist in range(1, max(1, effective_pkg_depth) + 1):
887
+ next_frontier: list[str] = []
888
+ for curr_pid in frontier:
889
+ curr_pkg = ws_info.packages.get(curr_pid)
890
+ if not curr_pkg:
891
+ continue
892
+ for dep_pid in curr_pkg.dependency_package_ids:
893
+ if dep_pid in ws_info.packages and dep_pid not in pkg_distances:
894
+ pkg_distances[dep_pid] = dist
895
+ next_frontier.append(dep_pid)
896
+ for dep_pkg in ws_info.get_dependent_packages(curr_pkg.package_id):
897
+ if dep_pkg.package_id not in pkg_distances:
898
+ pkg_distances[dep_pkg.package_id] = dist
899
+ next_frontier.append(dep_pkg.package_id)
900
+ frontier = next_frontier
901
+ for pid in ws_info.packages:
902
+ if pid not in pkg_distances:
903
+ pkg_distances[pid] = 99
904
+
905
+ # Artifact-aware seed policy:
906
+ # SOURCE: full participation
907
+ # TEST: participation for test-aware tasks
908
+ # GENERATED: deprioritized (placed after handwritten source)
909
+ # BUILD_ARTIFACT, MINIFIED, BUNDLE, BINARY: suppressed from normal seeds
910
+ # VENDOR: suppressed from normal seeds unless explicit opt-in
911
+ filtered_seeds: list[str] = []
912
+ deprioritized_seeds: list[str] = []
913
+ excluded_artifacts_count = 0
914
+ excluded_artifact_classes: set[str] = set()
915
+ artifact_inclusions: list[dict[str, object]] = []
916
+
917
+ explicit_targets = set(task_spec.targets) | set(task_spec.priority_targets or ())
918
+
919
+ for s_candidate in ordered_seeds:
920
+ if s_candidate in explicit_targets:
921
+ filtered_seeds.append(s_candidate)
922
+ continue
923
+ cat_row = con.execute(
924
+ "SELECT f.category FROM symbols s JOIN files f ON s.path = f.path "
925
+ "WHERE s.canonical_id = ? OR s.qualified_name = ? OR s.name = ? LIMIT 1",
926
+ (s_candidate, s_candidate, s_candidate),
927
+ ).fetchone()
928
+ if cat_row:
929
+ s_cat = str(cat_row[0])
930
+ if s_cat in ("MINIFIED", "BUNDLE") and not include_bundles:
931
+ excluded_artifacts_count += 1
932
+ excluded_artifact_classes.add(s_cat)
933
+ continue
934
+ if s_cat in ("BUILD_ARTIFACT", "BINARY") and not include_build_artifacts:
935
+ excluded_artifacts_count += 1
936
+ excluded_artifact_classes.add(s_cat)
937
+ continue
938
+ if s_cat == "VENDOR" and not include_vendor:
939
+ excluded_artifacts_count += 1
940
+ excluded_artifact_classes.add(s_cat)
941
+ continue
942
+ if s_cat == "TEST" and task_spec.intent not in ("TEST", "DEBUG", "REVIEW"):
943
+ continue
944
+ if s_cat == "GENERATED" and not include_generated:
945
+ deprioritized_seeds.append(s_candidate)
946
+ continue
947
+ filtered_seeds.append(s_candidate)
948
+
949
+ active_seeds = filtered_seeds + deprioritized_seeds
950
+ findings_out: list[dict[str, object]] = []
951
+
952
+ for sym in active_seeds[:graph_seed_policy.max_seeds]:
678
953
  short = sym.split(".")[-1]
679
954
  if _retrieval_policy.include_callers and (
680
955
  retrieval_plan.entry_point_strategy in ("TRACE_FROM_ENDPOINT", "TRACE_HIERARCHY")
681
- or task_spec.intent in ("DEBUG", "TRACE", "IMPACT", "CHANGE", "REVIEW")
956
+ or task_spec.intent in ("DEBUG", "TRACE", "IMPACT", "CHANGE", "REVIEW", "UNDERSTAND")
682
957
  ):
683
958
  callers = find_callers(con, short, max_results=max_graph_results)
684
959
  for c in callers:
@@ -725,7 +1000,7 @@ def _get_context_impl(
725
1000
  }
726
1001
  )
727
1002
  # Depth 2 callee and parallel discovery
728
- if _retrieval_policy.max_graph_depth >= 2 and callee_sym:
1003
+ if effective_max_depth >= 2 and callee_sym:
729
1004
  c2_list = find_callees(con, str(callee_sym), max_results=max_graph_results)
730
1005
  for c2 in c2_list:
731
1006
  c2_sym = c2.get("callee")
@@ -789,6 +1064,62 @@ def _get_context_impl(
789
1064
  }
790
1065
  )
791
1066
 
1067
+ # Phase 10: Semantic edge candidate discovery (DI providers, registrations, events, routes, tests)
1068
+ # Ensures coverage-aware optimization discovers high-value semantic neighbors even when lexically distinct
1069
+ try:
1070
+ sem_rows = con.execute(
1071
+ """
1072
+ SELECT source, target, relationship, confidence, evidence_class, file, start_line, evidence
1073
+ FROM graph_edges
1074
+ WHERE source = ? OR target = ? OR source LIKE ? OR target LIKE ?
1075
+ LIMIT 30
1076
+ """,
1077
+ (sym, sym, f"%::{short}", f"%::{short}"),
1078
+ ).fetchall()
1079
+ for srow in sem_rows:
1080
+ s_rel = str(srow["relationship"] or "")
1081
+ s_conf = str(srow["confidence"] or "HIGH").upper()
1082
+ s_ev_cls = str(srow["evidence_class"] or "AST_VERIFIED").upper()
1083
+ if s_ev_cls in ("UNKNOWN", "POSSIBLE") or s_conf == "LOW":
1084
+ findings_out.append({
1085
+ "type": "EPISTEMIC_EDGE",
1086
+ "source": srow["source"],
1087
+ "target": srow["target"],
1088
+ "relationship": s_rel,
1089
+ "evidence_class": s_ev_cls,
1090
+ "confidence": s_conf,
1091
+ "file": srow["file"],
1092
+ "line": srow["start_line"],
1093
+ "evidence": srow["evidence"] or "",
1094
+ "status": "UNKNOWN" if s_ev_cls == "UNKNOWN" else "INFERENCE",
1095
+ })
1096
+ if not _retrieval_policy.allows(s_rel):
1097
+ continue
1098
+ other_id = str(srow["target"]) if str(srow["source"]).endswith(short) or str(srow["source"]) == sym else str(srow["source"])
1099
+ target_symbols.add(other_id)
1100
+ sym_lookup = con.execute(
1101
+ "SELECT canonical_id, qualified_name, name, path, start_line, end_line FROM symbols "
1102
+ "WHERE canonical_id = ? OR qualified_name = ? OR name = ? LIMIT 1",
1103
+ (other_id, other_id, other_id.split("::")[-1].split(".")[-1]),
1104
+ ).fetchone()
1105
+ if sym_lookup:
1106
+ o_file = str(sym_lookup["path"])
1107
+ o_sym = str(sym_lookup["qualified_name"] or sym_lookup["name"])
1108
+ if _guard.allows_symbol(o_sym) and _guard.allows_file(o_file):
1109
+ candidate_dicts.append({
1110
+ "file": o_file,
1111
+ "symbol": o_sym,
1112
+ "canonical_id": str(sym_lookup["canonical_id"]),
1113
+ "start_line": int(sym_lookup["start_line"] or 1),
1114
+ "end_line": int(sym_lookup["end_line"] or 1),
1115
+ "score": 0.65,
1116
+ "snippet": "",
1117
+ "relationship": s_rel,
1118
+ "confidence": s_conf,
1119
+ })
1120
+ except Exception:
1121
+ pass
1122
+
792
1123
  # Also discover parallel implementations for top search results
793
1124
  if _retrieval_policy.allows("PARALLEL_IMPLEMENTATION"):
794
1125
  for s_item in search_res[:4]:
@@ -819,7 +1150,8 @@ def _get_context_impl(
819
1150
  # Related tests
820
1151
  t_tests_start = time.perf_counter()
821
1152
  tests_out: list[TestRef] = []
822
- if retrieval_plan.include_tests:
1153
+ seen_test_keys: set[tuple[str, str | None, str]] = set()
1154
+ if retrieval_plan.include_tests or _retrieval_policy.include_tests:
823
1155
  test_sym_targets = list(task_spec.priority_targets or task_spec.targets)
824
1156
  if not test_sym_targets:
825
1157
  test_sym_targets = list(target_symbols)
@@ -828,27 +1160,91 @@ def _get_context_impl(
828
1160
  for test_item in test_results:
829
1161
  if "result" in test_item:
830
1162
  continue
1163
+ t_file = str(test_item.get("file", ""))
1164
+ t_sym = str(test_item["symbol"]) if test_item.get("symbol") is not None else None
1165
+ t_rel = str(test_item.get("relationship", "TESTS"))
1166
+ t_key = (t_file, t_sym, t_rel)
1167
+ if t_key in seen_test_keys:
1168
+ continue
1169
+ seen_test_keys.add(t_key)
831
1170
  candidate_dicts.append(
832
1171
  {
833
- "file": test_item.get("file"),
834
- "symbol": test_item.get("symbol"),
1172
+ "file": t_file,
1173
+ "symbol": t_sym,
835
1174
  "start_line": test_item.get("start_line", 1),
836
1175
  "end_line": test_item.get("start_line", 1),
837
- "score": 0.55,
1176
+ "score": 0.60,
838
1177
  "snippet": "",
1178
+ "relationship": t_rel,
839
1179
  }
840
1180
  )
841
1181
  tests_out.append(
842
1182
  TestRef(
843
- file=str(test_item.get("file", "")),
844
- symbol=str(test_item["symbol"]) if test_item.get("symbol") is not None else None,
845
- relationship=str(test_item.get("relationship", "")),
1183
+ file=t_file,
1184
+ symbol=t_sym,
1185
+ relationship=t_rel,
846
1186
  confidence=str(test_item.get("confidence", "LOW")),
847
1187
  evidence=str(test_item.get("evidence", "")),
848
1188
  )
849
1189
  )
850
1190
  timing.tests_ms = (time.perf_counter() - t_tests_start) * 1000
851
1191
 
1192
+ # Pre-ranking artifact & package annotation on candidate_dicts
1193
+ from codegraph.indexing.classifier import classify_file
1194
+ from codegraph.security import is_sensitive_path
1195
+ filtered_candidate_dicts: list[dict[str, object]] = []
1196
+ for cd in candidate_dicts:
1197
+ c_file = str(cd.get("file") or "")
1198
+ if c_file and is_sensitive_path(c_file):
1199
+ continue
1200
+ c_sym = str(cd.get("symbol") or "")
1201
+ c_cat = str(cd.get("category") or "")
1202
+ if not c_cat and c_file:
1203
+ frow = con.execute("SELECT category FROM files WHERE path=? LIMIT 1", (c_file,)).fetchone()
1204
+ c_cat = str(frow[0]) if frow and frow[0] else str(classify_file(c_file).value)
1205
+ cd["category"] = c_cat
1206
+
1207
+ is_explicit = c_file in explicit_targets or c_sym in explicit_targets
1208
+ if not is_explicit:
1209
+ if c_cat in ("MINIFIED", "BUNDLE") and not include_bundles:
1210
+ excluded_artifacts_count += 1
1211
+ excluded_artifact_classes.add(c_cat)
1212
+ continue
1213
+ if c_cat in ("BUILD_ARTIFACT", "BINARY") and not include_build_artifacts:
1214
+ excluded_artifacts_count += 1
1215
+ excluded_artifact_classes.add(c_cat)
1216
+ continue
1217
+ if c_cat == "VENDOR" and not include_vendor:
1218
+ excluded_artifacts_count += 1
1219
+ excluded_artifact_classes.add(c_cat)
1220
+ continue
1221
+ else:
1222
+ if c_cat in ("MINIFIED", "BUNDLE", "BUILD_ARTIFACT", "VENDOR", "GENERATED"):
1223
+ artifact_inclusions.append({
1224
+ "file": c_file,
1225
+ "artifact_class": c_cat,
1226
+ "reason": "Explicitly targeted by task",
1227
+ })
1228
+
1229
+ if c_cat in ("MINIFIED", "BUNDLE") and include_bundles:
1230
+ artifact_inclusions.append({"file": c_file, "artifact_class": c_cat, "reason": "Explicit opt-in: include_bundles=True"})
1231
+ elif c_cat in ("BUILD_ARTIFACT", "BINARY") and include_build_artifacts:
1232
+ artifact_inclusions.append({"file": c_file, "artifact_class": c_cat, "reason": "Explicit opt-in: include_build_artifacts=True"})
1233
+ elif c_cat == "VENDOR" and include_vendor:
1234
+ artifact_inclusions.append({"file": c_file, "artifact_class": c_cat, "reason": "Explicit opt-in: include_vendor=True"})
1235
+ elif c_cat == "GENERATED" and include_generated:
1236
+ artifact_inclusions.append({"file": c_file, "artifact_class": c_cat, "reason": "Explicit opt-in: include_generated=True"})
1237
+
1238
+ if target_pkg and c_file:
1239
+ c_pkg = ws_info.get_package_for_file(c_file)
1240
+ if c_pkg:
1241
+ cd["package_id"] = c_pkg.package_id
1242
+ cd["package_distance"] = pkg_distances.get(c_pkg.package_id, 99)
1243
+
1244
+ filtered_candidate_dicts.append(cd)
1245
+
1246
+ candidate_dicts = filtered_candidate_dicts
1247
+
852
1248
  # 6. Rank candidates
853
1249
  t_rank_start = time.perf_counter()
854
1250
  legacy_intent_str = str(caller_raw_intent or task_spec.intent.lower())
@@ -863,20 +1259,40 @@ def _get_context_impl(
863
1259
  )
864
1260
  timing.ranking_ms = (time.perf_counter() - t_rank_start) * 1000
865
1261
 
866
- # 7. Snippets and Evidence
1262
+ # 7. Bounded Snippets and Evidence
1263
+ from codegraph.security import redact_secrets
867
1264
  t_evidence_start = time.perf_counter()
1265
+ per_snippet_max_lines = max(1, min(35, min(max_lines, HARD_MAX_LINES)))
868
1266
  for item in ranked:
869
1267
  if not item.snippet and item.symbol:
870
1268
  row = con.execute(
871
1269
  "SELECT content FROM chunks WHERE symbol=? LIMIT 1", (item.symbol,)
872
1270
  ).fetchone()
873
1271
  if row:
874
- snip = row["content"][:800]
1272
+ snip, _, _ = select_bounded_snippet(
1273
+ redact_secrets(str(row["content"])),
1274
+ start_line=item.start_line,
1275
+ end_line=item.end_line,
1276
+ max_lines=per_snippet_max_lines,
1277
+ max_chars=800,
1278
+ )
1279
+ snip = redact_secrets(snip)
875
1280
  object.__setattr__(item, "snippet", snip)
876
1281
  object.__setattr__(item, "token_estimate", _token_estimate(snip))
1282
+ elif item.snippet:
1283
+ snip, _, _ = select_bounded_snippet(
1284
+ redact_secrets(item.snippet),
1285
+ start_line=item.start_line,
1286
+ end_line=item.end_line,
1287
+ max_lines=per_snippet_max_lines,
1288
+ max_chars=800,
1289
+ )
1290
+ snip = redact_secrets(snip)
1291
+ object.__setattr__(item, "snippet", snip)
1292
+ object.__setattr__(item, "token_estimate", _token_estimate(snip))
877
1293
  timing.evidence_ms = (time.perf_counter() - t_evidence_start) * 1000
878
1294
 
879
- # 8. Token Budget Optimization
1295
+ # 8. Token Budget & Coverage-Aware Optimization
880
1296
  t_compile_start = time.perf_counter()
881
1297
  candidate_items: list[CandidateContextItem] = []
882
1298
  for idx, item in enumerate(ranked):
@@ -892,15 +1308,45 @@ def _get_context_impl(
892
1308
  elif "model" in item.file.lower() or "db" in item.file.lower() or "schema" in item.file.lower():
893
1309
  layer = "DATA"
894
1310
 
895
- canon = None
896
- if item.symbol:
1311
+ canon = item.canonical_id
1312
+ if not canon and item.symbol:
897
1313
  srow = con.execute(
898
- "SELECT canonical_id FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? LIMIT 1",
899
- (item.symbol, item.symbol, item.symbol),
1314
+ "SELECT canonical_id FROM symbols WHERE (canonical_id=? OR qualified_name=? OR name=?) AND path=? LIMIT 1",
1315
+ (item.symbol, item.symbol, item.symbol, item.file),
900
1316
  ).fetchone()
1317
+ if not srow:
1318
+ srow = con.execute(
1319
+ "SELECT canonical_id FROM symbols WHERE canonical_id=? OR qualified_name=? OR name=? LIMIT 1",
1320
+ (item.symbol, item.symbol, item.symbol),
1321
+ ).fetchone()
901
1322
  if srow:
902
1323
  canon = srow["canonical_id"]
903
1324
 
1325
+ item_pkg = ws_info.get_package_for_file(item.file) if item.file else None
1326
+ item_pkg_id = item_pkg.package_id if item_pkg else None
1327
+ item_pkg_dist = pkg_distances.get(item_pkg_id, 0) if (target_pkg and item_pkg_id) else 0
1328
+
1329
+ frow = con.execute("SELECT category FROM files WHERE path=? LIMIT 1", (item.file,)).fetchone() if item.file else None
1330
+ item_art_cls = str(frow[0]) if frow and frow[0] else str(classify_file(item.file).value)
1331
+
1332
+ # Determine relationship tag from reasons if present
1333
+ rel_tag = ""
1334
+ for rs in item.reasons:
1335
+ if rs.code == "DI_PROVIDER":
1336
+ rel_tag = "PROVIDES"
1337
+ elif rs.code == "SEMANTIC_DISPATCH":
1338
+ rel_tag = "REGISTERS"
1339
+ elif rs.code == "ENDPOINT_HANDLER":
1340
+ rel_tag = "HANDLED_BY"
1341
+ elif rs.code in ("DIRECT_CALLER", "POSSIBLE_CALLER"):
1342
+ rel_tag = "CALLED_BY"
1343
+ elif rs.code == "DIRECT_CALLEE":
1344
+ rel_tag = "CALLS"
1345
+ elif rs.code == "TEST_RELATION":
1346
+ rel_tag = "TESTS"
1347
+ elif rs.code == "PACKAGE_DEPENDENCY":
1348
+ rel_tag = "DEPENDS_ON_PACKAGE"
1349
+
904
1350
  candidate_items.append(
905
1351
  CandidateContextItem(
906
1352
  item_id=f"c_{idx}_{item.file}_{item.start_line}",
@@ -916,33 +1362,45 @@ def _get_context_impl(
916
1362
  source_type="symbol" if item.symbol else "chunk",
917
1363
  snippet=item.snippet,
918
1364
  confidence="HIGH" if item.file not in stale_paths else "LOW",
919
- relationship_value=0.8 if layer in ("ENTRYPOINT", "SERVICE", "TEST") else 0.4,
1365
+ relationship_value=0.8 if layer in ("ENTRYPOINT", "SERVICE", "TEST") or rel_tag else 0.4,
920
1366
  epistemic_status="FACT" if item.file not in stale_paths else "UNKNOWN",
921
- data={"reasons": item.reasons, "symbol": item.symbol},
1367
+ data={"reasons": item.reasons, "symbol": item.symbol, "relationship": rel_tag},
1368
+ package_id=item_pkg_id,
1369
+ package_distance=item_pkg_dist,
1370
+ artifact_class=item_art_cls,
922
1371
  )
923
1372
  )
924
1373
 
1374
+ budget_spec = ContextBudgetSpec(
1375
+ max_tokens=max_tokens,
1376
+ max_symbols=max_symbols,
1377
+ max_relationships=max_relationships,
1378
+ max_files=max_files,
1379
+ max_lines=max_lines,
1380
+ max_depth=effective_max_depth,
1381
+ )
1382
+
925
1383
  selected_items, budget = optimize_context_budget(
926
1384
  candidates=candidate_items,
927
1385
  token_budget=max_tokens,
928
1386
  task_spec=task_spec,
1387
+ budget_spec=budget_spec,
1388
+ required_dimensions=_retrieval_policy.required_dimensions,
1389
+ verified_claims_count=len(route_relationships) + len(tests_out),
1390
+ tool_calls=1,
929
1391
  )
930
1392
  timing.compilation_ms = (time.perf_counter() - t_compile_start) * 1000
931
1393
 
932
- # 9. Assembly & Serialization
1394
+ # 9. Assembly, Deduplication & Evidence Compression
933
1395
  t_serial_start = time.perf_counter()
934
- selected_paths = {item.file_path for item in selected_items}
935
- discarded_paths = {item.file_path for item in candidate_items if item.file_path not in selected_paths}
1396
+ selected_paths = {item.file_path for item in selected_items if item.file_path}
1397
+ discarded_paths = {item.file_path for item in candidate_items if item.file_path and item.file_path not in selected_paths}
936
1398
 
937
1399
  symbols_out: list[SymbolRef] = []
938
- seen_sym_keys: set[tuple[str, str]] = set()
1400
+ seen_canonical_syms: set[str] = set()
939
1401
  for it in selected_items:
940
1402
  sym_name = it.data.get("symbol")
941
1403
  if sym_name:
942
- sym_key = (sym_name, it.file_path)
943
- if sym_key in seen_sym_keys:
944
- continue
945
- seen_sym_keys.add(sym_key)
946
1404
  known_cid = it.canonical_id or it.data.get("canonical_id")
947
1405
  if known_cid:
948
1406
  row = con.execute(
@@ -960,6 +1418,10 @@ def _get_context_impl(
960
1418
  (sym_name, sym_name),
961
1419
  ).fetchone()
962
1420
  cid = known_cid or (row["canonical_id"] if row and "canonical_id" in row.keys() else None)
1421
+ dedup_id = str(cid) if cid else f"{it.file_path}::{sym_name}"
1422
+ if dedup_id in seen_canonical_syms:
1423
+ continue
1424
+ seen_canonical_syms.add(dedup_id)
963
1425
  kind = row["kind"] if row else "unknown"
964
1426
  reasons = it.data.get("reasons", [])
965
1427
  symbols_out.append(
@@ -975,44 +1437,55 @@ def _get_context_impl(
975
1437
  for rs in reasons
976
1438
  ],
977
1439
  canonical_id=cid or it.canonical_id,
1440
+ reason_code=it.reason_code,
1441
+ package_id=it.package_id,
978
1442
  )
979
1443
  )
980
1444
 
981
- # ── Relationships extraction ────────────────────────────────────────
1445
+ # ── Relationships extraction & compression ──────────────────────────
982
1446
  relationships_raw: list[Relationship] = list(route_relationships)
983
- seen_rel: set[tuple[str, str, str]] = {
984
- (r.source, r.target, r.relationship) for r in route_relationships
985
- }
986
1447
 
987
1448
  try:
988
- all_syms = list(target_symbols) + [s.split(".")[-1] for s in target_symbols]
1449
+ all_syms = (
1450
+ list(target_symbols)
1451
+ + [s.split(".")[-1] for s in target_symbols]
1452
+ + [s.canonical_id for s in symbols_out if s.canonical_id]
1453
+ + [s.symbol for s in symbols_out if s.symbol]
1454
+ )
1455
+ if target_pkg:
1456
+ all_syms.append(target_pkg.package_id)
1457
+ all_syms.append(target_pkg.name)
1458
+ all_syms = list(dict.fromkeys(all_syms))
989
1459
  if all_syms:
990
1460
  placeholders = ",".join("?" for _ in all_syms)
991
1461
  edge_rows = con.execute(
992
1462
  f"""
993
- SELECT source, target, relationship, confidence, file, start_line, evidence
1463
+ SELECT source, target, relationship, confidence, evidence_class, file, start_line, evidence
994
1464
  FROM graph_edges
995
1465
  WHERE source IN ({placeholders}) OR target IN ({placeholders})
996
- LIMIT 50
1466
+ LIMIT 100
997
1467
  """,
998
1468
  all_syms + all_syms,
999
1469
  ).fetchall()
1000
1470
  for er in edge_rows:
1001
- key = (er["source"], er["target"], er["relationship"])
1002
- if key not in seen_rel:
1003
- seen_rel.add(key)
1004
- relationships_raw.append(
1005
- Relationship(
1006
- source=er["source"],
1007
- target=er["target"],
1008
- relationship=er["relationship"],
1009
- confidence=er["confidence"] or "HIGH",
1010
- file=er["file"],
1011
- start_line=er["start_line"],
1012
- evidence=er["evidence"] or "",
1013
- status="FACT" if (er["confidence"] or "HIGH").upper() == "HIGH" else "INFERENCE",
1014
- )
1471
+ ev_cls = str(er["evidence_class"] if "evidence_class" in er.keys() and er["evidence_class"] else "AST_VERIFIED")
1472
+ conf_val = str(er["confidence"] or "HIGH").upper()
1473
+ status_val = "FACT" if conf_val == "HIGH" and ev_cls not in ("POSSIBLE", "UNKNOWN") else (
1474
+ "UNKNOWN" if ev_cls == "UNKNOWN" else "INFERENCE"
1475
+ )
1476
+ relationships_raw.append(
1477
+ Relationship(
1478
+ source=er["source"],
1479
+ target=er["target"],
1480
+ relationship=er["relationship"],
1481
+ confidence=conf_val,
1482
+ file=er["file"],
1483
+ start_line=er["start_line"],
1484
+ evidence=er["evidence"] or "",
1485
+ status=status_val,
1486
+ evidence_class=ev_cls,
1015
1487
  )
1488
+ )
1016
1489
  except Exception:
1017
1490
  pass
1018
1491
 
@@ -1023,63 +1496,79 @@ def _get_context_impl(
1023
1496
  for c in callers:
1024
1497
  c_file = str(c.get("file", ""))
1025
1498
  if c_file in selected_paths:
1026
- key = (c_file, sym, "POSSIBLE_CALLS")
1027
- if key not in seen_rel:
1028
- seen_rel.add(key)
1029
- relationships_raw.append(
1030
- Relationship(
1031
- source=c_file,
1032
- target=sym,
1033
- relationship="POSSIBLE_CALLS",
1034
- confidence=str(c.get("confidence", "LOW")),
1035
- file=c_file,
1036
- evidence=str(c.get("evidence", "")),
1037
- status="INFERENCE",
1038
- )
1499
+ raw_c_conf = str(c.get("confidence", "LOW")).upper()
1500
+ c_conf = "MEDIUM" if raw_c_conf == "HIGH" else raw_c_conf
1501
+ relationships_raw.append(
1502
+ Relationship(
1503
+ source=c_file,
1504
+ target=sym,
1505
+ relationship="POSSIBLE_CALLS",
1506
+ confidence=c_conf,
1507
+ file=c_file,
1508
+ evidence=str(c.get("evidence", "")),
1509
+ status="INFERENCE",
1510
+ evidence_class="POSSIBLE",
1039
1511
  )
1512
+ )
1040
1513
 
1041
- # ── v2.1 HARD SAFETY & INTEGRITY INVARIANT ──────────────────────────
1042
- # Every channel must strictly conform to ConstraintGuard and RetrievalPolicy:
1514
+ # ── v2.1 & Phase 10 HARD SAFETY & INTEGRITY INVARIANT ───────────────
1043
1515
  # 1. Symbols
1044
1516
  symbols_out = [
1045
1517
  s for s in symbols_out
1046
1518
  if _guard.allows_symbol(s.symbol) and _guard.allows_canonical_id(s.canonical_id) and _guard.allows_file(s.file)
1047
- ]
1519
+ ][: budget.max_symbols]
1048
1520
 
1049
- # 2. Relationships
1050
- relationships_out: list[Relationship] = [
1521
+ # 2. Relationships (filtered + deterministically compressed)
1522
+ filtered_rels: list[Relationship] = [
1051
1523
  r for r in relationships_raw
1052
1524
  if _guard.allows_relationship(r.source, r.relationship, r.target)
1053
1525
  and (_guard.allows_file(r.file) if r.file else True)
1054
1526
  and _retrieval_policy.allows(r.relationship)
1055
1527
  ]
1528
+ relationships_out, compression_stats = compress_relationships(
1529
+ filtered_rels, max_relationships=budget.max_relationships
1530
+ )
1056
1531
 
1057
- # 3. Tests
1532
+ # 3. Tests (deduplicated)
1058
1533
  tests_out = [
1059
1534
  t for t in tests_out
1060
1535
  if _guard.allows_test(t.file, t.symbol)
1061
1536
  ]
1062
1537
 
1063
- # 4. Entry points & framework facts
1064
- entry_points_out = [
1065
- ep for ep in entry_points_out
1066
- if _guard.allows_endpoint(
1538
+ # 4. Entry points & framework facts (deduplicated by endpoint_id)
1539
+ seen_ep_ids: set[str] = set()
1540
+ dedup_entry_points: list[dict[str, object]] = []
1541
+ for ep in entry_points_out:
1542
+ if not _guard.allows_endpoint(
1067
1543
  str(ep.get("route_path")),
1068
1544
  str(ep.get("handler")),
1069
1545
  str(ep.get("file")),
1070
- )
1071
- ]
1546
+ ):
1547
+ continue
1548
+ eid = str(ep.get("endpoint_id") or "")
1549
+ if eid and eid in seen_ep_ids:
1550
+ continue
1551
+ if eid:
1552
+ seen_ep_ids.add(eid)
1553
+ dedup_entry_points.append(ep)
1554
+ entry_points_out = dedup_entry_points
1555
+
1072
1556
  framework_facts_out = [
1073
1557
  ff for ff in framework_facts_out
1074
1558
  if _guard.allows_symbol(str(ff.get("handler"))) and _guard.allows_file(str(ff.get("file")))
1075
1559
  ]
1076
1560
 
1077
- # 5. Evidence
1561
+ # 5. Evidence (bounded & deduplicated by canonical location)
1078
1562
  evidence_out: list[EvidenceRef] = []
1563
+ seen_ev_keys: set[tuple[str, int, str | None]] = set()
1079
1564
  for it in selected_items[:12]:
1080
1565
  if it.snippet:
1081
1566
  if not _guard.allows_file(it.file_path) or not _guard.allows_symbol(it.data.get("symbol")):
1082
1567
  continue
1568
+ ev_key = (it.file_path, it.start_line, it.data.get("symbol"))
1569
+ if ev_key in seen_ev_keys:
1570
+ continue
1571
+ seen_ev_keys.add(ev_key)
1083
1572
  src_hash: str | None = None
1084
1573
  try:
1085
1574
  row = con.execute("SELECT content_hash FROM files WHERE path=?", (it.file_path,)).fetchone()
@@ -1101,15 +1590,20 @@ def _get_context_impl(
1101
1590
  )
1102
1591
  )
1103
1592
 
1104
- # 6. Files
1593
+ # 6. Files (bounded by budget.max_files, aggregating token_estimate across selected items in each file)
1105
1594
  files_out: list[FileRef] = []
1106
- seen_files: set[str] = set()
1595
+ file_index_map: dict[str, int] = {}
1107
1596
  for it in selected_items:
1108
- if not _guard.allows_file(it.file_path):
1597
+ if not it.file_path or not _guard.allows_file(it.file_path):
1598
+ continue
1599
+ if it.file_path in file_index_map:
1600
+ existing_idx = file_index_map[it.file_path]
1601
+ existing_ref = files_out[existing_idx]
1602
+ existing_ref.token_estimate += it.estimated_tokens
1109
1603
  continue
1110
- if it.file_path in seen_files:
1604
+ if len(files_out) >= budget.max_files:
1111
1605
  continue
1112
- seen_files.add(it.file_path)
1606
+ file_index_map[it.file_path] = len(files_out)
1113
1607
  reasons = it.data.get("reasons", [])
1114
1608
  files_out.append(
1115
1609
  FileRef(
@@ -1124,6 +1618,184 @@ def _get_context_impl(
1124
1618
  )
1125
1619
  )
1126
1620
 
1621
+ # 7. Package-aware bounded packages list & unresolved package dependency preservation
1622
+ packages_out: list[dict[str, object]] = []
1623
+ seen_pkg_ids: set[str] = set()
1624
+ for pid in sorted(ws_info.packages.keys()):
1625
+ p_info = ws_info.packages[pid]
1626
+ if p_info.root_path == "" and len(ws_info.packages) > 1:
1627
+ continue
1628
+ p_dist = pkg_distances.get(pid, 0)
1629
+ if target_pkg and p_dist > effective_pkg_depth and task_spec.intent != "ARCHITECTURE":
1630
+ continue
1631
+ if pid not in seen_pkg_ids:
1632
+ seen_pkg_ids.add(pid)
1633
+ p_dict = p_info.as_dict()
1634
+ p_dict["distance"] = p_dist
1635
+ packages_out.append(p_dict)
1636
+
1637
+ for unres in ws_info.unresolved_dependencies:
1638
+ src_pid = str(unres.get("source_package_id") or "")
1639
+ if not target_pkg or src_pid == target_pkg.package_id:
1640
+ findings_out.append({
1641
+ "type": "UNRESOLVED_PACKAGE_DEPENDENCY",
1642
+ "source": src_pid,
1643
+ "target": str(unres.get("target_name") or ""),
1644
+ "specifier": str(unres.get("specifier") or ""),
1645
+ "status": "UNKNOWN",
1646
+ "reason": f"Dependency '{unres.get('target_name')}' is external or unresolved in workspace",
1647
+ })
1648
+
1649
+ # Check unresolved target resolutions and preserve as UNKNOWN findings
1650
+ assumptions_out = list(task_spec.assumptions)
1651
+ unknowns_out = list(task_spec.unknowns)
1652
+ conflicts_out = list(task_spec.conflicts) if hasattr(task_spec, "conflicts") else []
1653
+
1654
+ for raw_t, t_res in _target_resolutions.items():
1655
+ if t_res.ambiguity_state == "AMBIGUOUS":
1656
+ findings_out.append({
1657
+ "type": "AMBIGUOUS_TARGET",
1658
+ "target": raw_t,
1659
+ "status": "CONFLICT",
1660
+ "alternatives": list(t_res.alternatives),
1661
+ })
1662
+ msg = f"Target '{raw_t}' is ambiguous across multiple candidates."
1663
+ if msg not in conflicts_out:
1664
+ conflicts_out.append(msg)
1665
+ elif t_res.target_type.value == "UNKNOWN":
1666
+ findings_out.append({
1667
+ "type": "UNRESOLVED_TARGET",
1668
+ "target": raw_t,
1669
+ "status": "UNKNOWN",
1670
+ "reason": f"Target '{raw_t}' could not be statically resolved in repository.",
1671
+ })
1672
+ msg = f"Target '{raw_t}' could not be statically resolved."
1673
+ if msg not in unknowns_out:
1674
+ unknowns_out.append(msg)
1675
+
1676
+ # 8. Phase 14: Database, Runtime & Reconciliation Context Assembly
1677
+ database_out: list[dict[str, object]] = []
1678
+ runtime_out: list[dict[str, object]] = []
1679
+ reconciliation_out: list[dict[str, object]] = []
1680
+
1681
+ try:
1682
+ relevant_names: set[str] = set()
1683
+ for t in task_spec.targets:
1684
+ if t:
1685
+ relevant_names.add(t.lower())
1686
+ relevant_names.add(t.split(".")[-1].lower())
1687
+ for s_ref in symbols_out:
1688
+ relevant_names.add(s_ref.symbol.lower())
1689
+ relevant_names.add(s_ref.symbol.split(".")[-1].lower())
1690
+ if s_ref.canonical_id:
1691
+ relevant_names.add(s_ref.canonical_id.lower())
1692
+ for word in task_display_str.lower().replace(",", " ").replace("(", " ").replace(")", " ").split():
1693
+ if len(word) >= 3:
1694
+ relevant_names.add(word)
1695
+
1696
+ db_ent_rows = con.execute(
1697
+ "SELECT canonical_id, entity_type, name, dialect, schema_name, table_name, data_type, "
1698
+ "nullable, primary_key, foreign_key_target, path, start_line, end_line, confidence, evidence_class "
1699
+ "FROM db_entities ORDER BY table_name ASC, entity_type ASC, name ASC LIMIT 80"
1700
+ ).fetchall()
1701
+ for drow in db_ent_rows:
1702
+ t_name = str(drow["table_name"] or drow["name"] or "").lower()
1703
+ e_name = str(drow["name"] or "").lower()
1704
+ d_path = str(drow["path"] or "")
1705
+ if (
1706
+ not _guard.allows_file(d_path)
1707
+ or not _guard.allows_symbol(str(drow["name"] or ""))
1708
+ ):
1709
+ continue
1710
+ if (
1711
+ t_name in relevant_names
1712
+ or e_name in relevant_names
1713
+ or d_path in selected_paths
1714
+ or any(rn and (rn in t_name or t_name in rn) for rn in relevant_names if len(rn) >= 4)
1715
+ ):
1716
+ database_out.append({
1717
+ "record_type": "entity",
1718
+ "canonical_id": str(drow["canonical_id"]),
1719
+ "entity_type": str(drow["entity_type"]),
1720
+ "name": str(drow["name"]),
1721
+ "dialect": str(drow["dialect"]),
1722
+ "schema": str(drow["schema_name"]),
1723
+ "table": str(drow["table_name"] or ""),
1724
+ "data_type": drow["data_type"],
1725
+ "primary_key": bool(drow["primary_key"]),
1726
+ "foreign_key_target": drow["foreign_key_target"],
1727
+ "file": d_path,
1728
+ "start_line": int(drow["start_line"] or 1),
1729
+ "end_line": int(drow["end_line"] or 1),
1730
+ "confidence": str(drow["confidence"]),
1731
+ "evidence_class": str(drow["evidence_class"]),
1732
+ })
1733
+ if len(database_out) >= 20:
1734
+ break
1735
+
1736
+ db_q_rows = con.execute(
1737
+ "SELECT source_symbol, target_table, operation, relationship, path, start_line, end_line, "
1738
+ "confidence, evidence_class, snippet, framework FROM db_queries "
1739
+ "ORDER BY path ASC, start_line ASC LIMIT 50"
1740
+ ).fetchall()
1741
+ for qrow in db_q_rows:
1742
+ q_path = str(qrow["path"] or "")
1743
+ q_sym = str(qrow["source_symbol"] or "")
1744
+ q_tbl = str(qrow["target_table"] or "").lower()
1745
+ if not _guard.allows_file(q_path) or not _guard.allows_symbol(q_sym):
1746
+ continue
1747
+ if (
1748
+ q_path in selected_paths
1749
+ or q_tbl in relevant_names
1750
+ or q_sym.lower() in relevant_names
1751
+ or q_sym.split("::")[-1].split(".")[-1].lower() in relevant_names
1752
+ ):
1753
+ database_out.append({
1754
+ "record_type": "query",
1755
+ "source_symbol": q_sym,
1756
+ "target_table": str(qrow["target_table"]),
1757
+ "operation": str(qrow["operation"]),
1758
+ "relationship": str(qrow["relationship"]),
1759
+ "framework": str(qrow["framework"] or ""),
1760
+ "file": q_path,
1761
+ "start_line": int(qrow["start_line"] or 1),
1762
+ "end_line": int(qrow["end_line"] or 1),
1763
+ "confidence": str(qrow["confidence"]),
1764
+ "evidence_class": str(qrow["evidence_class"]),
1765
+ "snippet": redact_secrets(str(qrow["snippet"] or "")[:240]),
1766
+ })
1767
+ if len(database_out) >= 30:
1768
+ break
1769
+ except Exception:
1770
+ pass
1771
+
1772
+ try:
1773
+ rt_rows = con.execute(
1774
+ "SELECT source, target, relationship, observation_count, first_seen, last_seen, "
1775
+ "runtime_generation, confidence, evidence_class, sample_trace_id "
1776
+ "FROM runtime_edges ORDER BY observation_count DESC, source ASC, target ASC LIMIT 25"
1777
+ ).fetchall()
1778
+ for rrow in rt_rows:
1779
+ runtime_out.append({
1780
+ "source": str(rrow["source"]),
1781
+ "target": str(rrow["target"]),
1782
+ "relationship": str(rrow["relationship"]),
1783
+ "observation_count": int(rrow["observation_count"] or 1),
1784
+ "first_seen": str(rrow["first_seen"] or ""),
1785
+ "last_seen": str(rrow["last_seen"] or ""),
1786
+ "runtime_generation": int(rrow["runtime_generation"] or 1),
1787
+ "confidence": str(rrow["confidence"] or "HIGH"),
1788
+ "evidence_class": "RUNTIME_OBSERVED",
1789
+ "sample_trace_id": str(rrow["sample_trace_id"] or ""),
1790
+ })
1791
+ if runtime_out:
1792
+ from codegraph.runtime.reconciliation import reconcile_static_runtime
1793
+ rec_res = reconcile_static_runtime(con, repository)
1794
+ for item_rec in rec_res.get("items", [])[:20]:
1795
+ reconciliation_out.append(dict(item_rec))
1796
+ except Exception:
1797
+ pass
1798
+
1127
1799
  uncertainties: list[str] = []
1128
1800
  if freshness_report.status != FreshnessStatus.FRESH:
1129
1801
  uncertainties.append(f"Index is {freshness_report.status.value}: {freshness_report.detail}")
@@ -1132,9 +1804,6 @@ def _get_context_impl(
1132
1804
  if not candidate_dicts:
1133
1805
  uncertainties.append("No lexical or graph matches found. Results may be incomplete.")
1134
1806
 
1135
- assumptions_out = list(task_spec.assumptions)
1136
- unknowns_out = list(task_spec.unknowns)
1137
-
1138
1807
  timing.serialization_ms = (time.perf_counter() - t_serial_start) * 1000
1139
1808
  timing.total_ms = timer.elapsed_ms
1140
1809
 
@@ -1148,6 +1817,26 @@ def _get_context_impl(
1148
1817
  timing=timing,
1149
1818
  )
1150
1819
  exec_dict = exec_meta.as_dict()
1820
+ exec_dict["excluded_count"] = excluded_artifacts_count
1821
+ exec_dict["excluded_artifact_classes"] = sorted(excluded_artifact_classes)
1822
+ exec_dict["artifact_inclusions"] = artifact_inclusions
1823
+ exec_dict["package_context"] = target_pkg.as_dict() if target_pkg else None
1824
+ exec_dict["evidence_compression"] = compression_stats
1825
+ exec_dict["tool_efficiency"] = {
1826
+ "tool_calls": 1,
1827
+ "internal_queries_executed": len(queries_to_run),
1828
+ "consolidated_operations": [
1829
+ "target_resolution",
1830
+ "hybrid_search",
1831
+ "graph_traversal",
1832
+ "route_matching",
1833
+ "database_intelligence",
1834
+ "runtime_reconciliation",
1835
+ "test_discovery",
1836
+ "package_boundary_filtering",
1837
+ "budget_optimization",
1838
+ ],
1839
+ }
1151
1840
  exec_dict["resource"] = {
1152
1841
  "profile": governor.policy.profile.value,
1153
1842
  "pressure": governor.get_pressure().value,
@@ -1172,6 +1861,54 @@ def _get_context_impl(
1172
1861
  "rejections": list(budget.rejections),
1173
1862
  }
1174
1863
 
1864
+ coverage_dict: dict[str, object] = {
1865
+ "coverage_score": round(budget.coverage_score, 4),
1866
+ "required_dimensions": list(_retrieval_policy.required_dimensions),
1867
+ "covered_dimensions": list(budget.covered_dimensions),
1868
+ "missing_dimensions": list(budget.missing_dimensions),
1869
+ "reason_codes": {
1870
+ it.canonical_id or str(it.data.get("symbol") or it.file_path): it.reason_code
1871
+ for it in selected_items
1872
+ },
1873
+ }
1874
+
1875
+ metadata_dict: dict[str, object] = {
1876
+ "schema_version": SCHEMA_VERSION,
1877
+ "repository": str(repository),
1878
+ "repository_generation": gen,
1879
+ "freshness": freshness_report.status.value,
1880
+ "mode": mode_upper,
1881
+ "intent": str(caller_raw_intent) if caller_raw_intent else task_spec.intent.lower(),
1882
+ "selected_tokens": budget.selected_tokens,
1883
+ "candidate_tokens": budget.candidate_tokens,
1884
+ "selected_files": len(files_out),
1885
+ "selected_lines": budget.selected_lines,
1886
+ "coverage_score": round(budget.coverage_score, 4),
1887
+ "truncated": budget.truncated,
1888
+ }
1889
+
1890
+ target_dict: dict[str, object] = {
1891
+ "raw_targets": list(task_spec.targets),
1892
+ "resolutions": {
1893
+ k: {
1894
+ "target_type": v.target_type.value,
1895
+ "canonical_id": v.canonical_id,
1896
+ "qualified_name": v.qualified_name,
1897
+ "file_path": v.file_path,
1898
+ "confidence": v.confidence,
1899
+ "ambiguity_state": v.ambiguity_state,
1900
+ }
1901
+ for k, v in _target_resolutions.items()
1902
+ },
1903
+ "package": target_pkg.as_dict() if target_pkg else None,
1904
+ }
1905
+
1906
+ git_impact_dict: dict[str, object] = {
1907
+ "recent_changes": git_changes_out,
1908
+ "modified_files_count": len(recent_paths),
1909
+ "affected_package": target_pkg.package_id if target_pkg else None,
1910
+ }
1911
+
1175
1912
  packet = ContextPacket(
1176
1913
  schema_version=SCHEMA_VERSION,
1177
1914
  task=task_display_str,
@@ -1194,13 +1931,28 @@ def _get_context_impl(
1194
1931
  git_facts=git_changes_out,
1195
1932
  assumptions=assumptions_out,
1196
1933
  unknowns=unknowns_out,
1197
- conflicts=list(task_spec.conflicts) if hasattr(task_spec, "conflicts") else [],
1934
+ conflicts=conflicts_out,
1935
+ metadata=metadata_dict,
1936
+ target=target_dict,
1937
+ routes=entry_points_out,
1938
+ packages=packages_out,
1939
+ git_impact=git_impact_dict,
1940
+ findings=findings_out,
1941
+ coverage=coverage_dict,
1942
+ database=database_out,
1943
+ runtime=runtime_out,
1944
+ reconciliation=reconciliation_out,
1198
1945
  symbols=symbols_out,
1199
1946
  relationships=relationships_out,
1200
1947
  tests=tests_out,
1201
1948
  files=files_out,
1202
1949
  evidence=evidence_out,
1203
1950
  uncertainties=uncertainties,
1951
+ candidate_tokens=budget.candidate_tokens,
1952
+ selected_tokens=budget.selected_tokens,
1953
+ selected_lines=budget.selected_lines,
1954
+ coverage_score=round(budget.coverage_score, 4),
1955
+ truncated=budget.truncated,
1204
1956
  candidate_token_estimate=budget.candidate_tokens,
1205
1957
  selected_token_estimate=budget.selected_tokens,
1206
1958
  context_reduction_pct=round(budget.reduction_ratio * 100, 1),