code-oracle 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
code_oracle/slicer.py ADDED
@@ -0,0 +1,225 @@
1
+ """
2
+ Stage 3: k-Hop Neighborhood Slicer.
3
+ Isolates a compact directed subgraph (10 to 50 nodes) representing immediate callers,
4
+ callees, importers, and inheritance (k=1 or k=2), capped by a fan-out threshold to prevent graph explosion.
5
+ """
6
+
7
+ from typing import Dict, List, Optional, Set
8
+
9
+ from code_oracle.indexer import WorkspaceIndexer
10
+ from code_oracle.models import SlicedGraph, SliceEdge, SliceNode, Symbol
11
+
12
+
13
+ def slice_neighborhood(
14
+ seeds: List[Symbol],
15
+ indexer: WorkspaceIndexer,
16
+ k: int = 1,
17
+ max_fanout: int = 20,
18
+ max_nodes: int = 50,
19
+ ) -> SlicedGraph:
20
+ """
21
+ Extract a directed neighborhood subgraph around seed symbols.
22
+ Expands forward (callees, base classes) and backward (callers, subclasses, importers) up to k hops,
23
+ enforcing a strict degree cutoff (max_fanout) per node and computing the induced subgraph.
24
+ """
25
+ nodes: Dict[str, SliceNode] = {}
26
+ edges: List[SliceEdge] = []
27
+ seen_edges: Set[tuple] = set()
28
+ seed_ids: Set[str] = set()
29
+ graph_truncated = False
30
+
31
+ # Initialize seed nodes (capped by max_nodes)
32
+ if len(seeds) > max_nodes:
33
+ seeds = seeds[:max_nodes]
34
+ graph_truncated = True
35
+
36
+ current_symbols: List[Symbol] = []
37
+ for s in seeds:
38
+ node = SliceNode(
39
+ id=s.id,
40
+ name=s.name,
41
+ file_path=s.file_path,
42
+ kind=s.kind,
43
+ signature=s.signature,
44
+ is_seed=True,
45
+ is_modified=True,
46
+ symbol=s,
47
+ )
48
+ nodes[s.id] = node
49
+ seed_ids.add(s.id)
50
+ current_symbols.append(s)
51
+
52
+ visited_symbol_ids: Set[str] = set(seed_ids)
53
+
54
+ # Breadth-first expansion up to k hops
55
+ for hop in range(1, k + 1):
56
+ next_symbols: List[Symbol] = []
57
+
58
+ for curr_sym in current_symbols:
59
+ u_id = curr_sym.id
60
+ node_degree = 0
61
+
62
+ # 1. Forward edges: Callees called by curr_sym
63
+ for call in curr_sym.calls:
64
+ callee_def = indexer.resolve_callee(call, caller_sym=curr_sym)
65
+ if callee_def:
66
+ v_id = callee_def.id
67
+ if node_degree >= max_fanout:
68
+ nodes[u_id].truncated = True
69
+ graph_truncated = True
70
+ break
71
+
72
+ edge_key = (u_id, v_id, "CALLS")
73
+ if edge_key not in seen_edges:
74
+ seen_edges.add(edge_key)
75
+ edges.append(SliceEdge(source=u_id, target=v_id, relation="CALLS"))
76
+ node_degree += 1
77
+
78
+ if v_id not in nodes and len(nodes) < max_nodes:
79
+ nodes[v_id] = SliceNode(
80
+ id=v_id,
81
+ name=callee_def.name,
82
+ file_path=callee_def.file_path,
83
+ kind=callee_def.kind,
84
+ signature=callee_def.signature,
85
+ symbol=callee_def,
86
+ )
87
+ if v_id not in visited_symbol_ids:
88
+ visited_symbol_ids.add(v_id)
89
+ next_symbols.append(callee_def)
90
+
91
+ # 2. Forward edges: Base classes (INHERITS)
92
+ for base_name in getattr(curr_sym, "bases", []):
93
+ base_def = indexer.get_definition(base_name)
94
+ if base_def:
95
+ v_id = base_def.id
96
+ if node_degree >= max_fanout:
97
+ nodes[u_id].truncated = True
98
+ graph_truncated = True
99
+ break
100
+
101
+ edge_key = (u_id, v_id, "INHERITS")
102
+ if edge_key not in seen_edges:
103
+ seen_edges.add(edge_key)
104
+ edges.append(SliceEdge(source=u_id, target=v_id, relation="INHERITS"))
105
+ node_degree += 1
106
+
107
+ if v_id not in nodes and len(nodes) < max_nodes:
108
+ nodes[v_id] = SliceNode(
109
+ id=v_id,
110
+ name=base_def.name,
111
+ file_path=base_def.file_path,
112
+ kind=base_def.kind,
113
+ signature=base_def.signature,
114
+ symbol=base_def,
115
+ )
116
+ if v_id not in visited_symbol_ids:
117
+ visited_symbol_ids.add(v_id)
118
+ next_symbols.append(base_def)
119
+
120
+ # 3. Backward edges: Callers that call curr_sym
121
+ callers = indexer.get_callers(curr_sym.qualname)
122
+ if curr_sym.name != curr_sym.qualname:
123
+ for c in indexer.get_callers(curr_sym.name):
124
+ if c not in callers:
125
+ callers.append(c)
126
+
127
+ for call in callers:
128
+ if not call.caller:
129
+ continue
130
+
131
+ caller_def = indexer.get_definition(call.caller)
132
+ v_id = caller_def.id if caller_def else call.caller
133
+
134
+ if node_degree >= max_fanout:
135
+ nodes[u_id].truncated = True
136
+ graph_truncated = True
137
+ break
138
+
139
+ edge_key = (v_id, u_id, "CALLS")
140
+ if edge_key not in seen_edges:
141
+ seen_edges.add(edge_key)
142
+ edges.append(SliceEdge(source=v_id, target=u_id, relation="CALLS"))
143
+ node_degree += 1
144
+
145
+ if v_id not in nodes and len(nodes) < max_nodes:
146
+ node_name = caller_def.name if caller_def else call.caller.split("::")[-1]
147
+ file_path = caller_def.file_path if caller_def else (
148
+ call.caller.split("::")[0] if "::" in call.caller else ""
149
+ )
150
+ signature = caller_def.signature if caller_def else f"def {node_name}(...)"
151
+ kind = caller_def.kind if caller_def else "function"
152
+
153
+ nodes[v_id] = SliceNode(
154
+ id=v_id,
155
+ name=node_name,
156
+ file_path=file_path,
157
+ kind=kind,
158
+ signature=signature,
159
+ symbol=caller_def,
160
+ )
161
+ if caller_def and v_id not in visited_symbol_ids:
162
+ visited_symbol_ids.add(v_id)
163
+ next_symbols.append(caller_def)
164
+
165
+ # 4. Backward edges: Subclasses (INHERITS)
166
+ subclasses = indexer.get_subclasses(curr_sym.name)
167
+ if curr_sym.name != curr_sym.qualname:
168
+ subclasses.extend(indexer.get_subclasses(curr_sym.qualname))
169
+ for sub in subclasses:
170
+ v_id = sub.id
171
+ if node_degree >= max_fanout:
172
+ nodes[u_id].truncated = True
173
+ graph_truncated = True
174
+ break
175
+
176
+ edge_key = (v_id, u_id, "INHERITS")
177
+ if edge_key not in seen_edges:
178
+ seen_edges.add(edge_key)
179
+ edges.append(SliceEdge(source=v_id, target=u_id, relation="INHERITS"))
180
+ node_degree += 1
181
+
182
+ if v_id not in nodes and len(nodes) < max_nodes:
183
+ nodes[v_id] = SliceNode(
184
+ id=v_id,
185
+ name=sub.name,
186
+ file_path=sub.file_path,
187
+ kind=sub.kind,
188
+ signature=sub.signature,
189
+ symbol=sub,
190
+ )
191
+ if v_id not in visited_symbol_ids:
192
+ visited_symbol_ids.add(v_id)
193
+ next_symbols.append(sub)
194
+
195
+ current_symbols = next_symbols
196
+ if not current_symbols or len(nodes) >= max_nodes:
197
+ break
198
+
199
+ # 5. Induced Subgraph Completion: Connect any calls/inheritance between nodes already in the slice
200
+ for u_id, u_node in list(nodes.items()):
201
+ if not u_node.symbol:
202
+ continue
203
+ # Check calls
204
+ for call in u_node.symbol.calls:
205
+ target_def = indexer.resolve_callee(call, caller_sym=u_node.symbol)
206
+ if target_def and target_def.id in nodes:
207
+ edge_key = (u_id, target_def.id, "CALLS")
208
+ if edge_key not in seen_edges:
209
+ seen_edges.add(edge_key)
210
+ edges.append(SliceEdge(source=u_id, target=target_def.id, relation="CALLS"))
211
+ # Check base classes
212
+ for base_name in getattr(u_node.symbol, "bases", []):
213
+ base_def = indexer.get_definition(base_name)
214
+ if base_def and base_def.id in nodes:
215
+ edge_key = (u_id, base_def.id, "INHERITS")
216
+ if edge_key not in seen_edges:
217
+ seen_edges.add(edge_key)
218
+ edges.append(SliceEdge(source=u_id, target=base_def.id, relation="INHERITS"))
219
+
220
+ return SlicedGraph(
221
+ nodes=nodes,
222
+ edges=edges,
223
+ seed_ids=seed_ids,
224
+ truncated=graph_truncated,
225
+ )
@@ -0,0 +1,459 @@
1
+ """
2
+ Deterministic Symbolic Gate: Graph Cycle Detection and Invariant Checks.
3
+ """
4
+
5
+ import ast
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Set
8
+
9
+ from code_oracle.indexer import WorkspaceIndexer
10
+ from code_oracle.models import CallReference, GateResult, PatchResult, SlicedGraph, Symbol
11
+
12
+
13
+ def find_cycles_tarjan(
14
+ graph: Dict[str, List[str]], include_self_loops: bool = False
15
+ ) -> List[List[str]]:
16
+ """
17
+ Find strongly connected components with size > 1 (cycles) using Tarjan's algorithm.
18
+ Deterministic O(V + E) runtime with sorted node traversal.
19
+ """
20
+ index = 0
21
+ indices: Dict[str, int] = {}
22
+ lowlink: Dict[str, int] = {}
23
+ on_stack: Set[str] = set()
24
+ stack: List[str] = []
25
+ cycles: List[List[str]] = []
26
+
27
+ def strongconnect(node: str):
28
+ nonlocal index
29
+ indices[node] = index
30
+ lowlink[node] = index
31
+ index += 1
32
+ stack.append(node)
33
+ on_stack.add(node)
34
+
35
+ for neighbor in sorted(graph.get(node, [])):
36
+ if neighbor not in indices:
37
+ strongconnect(neighbor)
38
+ lowlink[node] = min(lowlink[node], lowlink[neighbor])
39
+ elif neighbor in on_stack:
40
+ lowlink[node] = min(lowlink[node], indices[neighbor])
41
+
42
+ if lowlink[node] == indices[node]:
43
+ component = []
44
+ while True:
45
+ w = stack.pop()
46
+ on_stack.remove(w)
47
+ component.append(w)
48
+ if w == node:
49
+ break
50
+
51
+ if len(component) > 1:
52
+ # Rotate cycle so minimum element is first for determinism
53
+ min_idx = component.index(min(component))
54
+ rotated = component[min_idx:] + component[:min_idx]
55
+ cycles.append(rotated)
56
+ elif include_self_loops and len(component) == 1:
57
+ if node in graph.get(node, []):
58
+ cycles.append(component)
59
+
60
+ for node in sorted(graph.keys()):
61
+ if node not in indices:
62
+ strongconnect(node)
63
+
64
+ # Sort cycles by their starting element
65
+ cycles.sort(key=lambda c: c[0] if c else "")
66
+ return cycles
67
+
68
+
69
+ def validate_call_site(
70
+ call: CallReference,
71
+ callee_sym: Symbol,
72
+ caller_label: str,
73
+ ) -> List[str]:
74
+ """
75
+ Validate a single call site against the target symbol's signature.
76
+ Returns a list of violation messages.
77
+ """
78
+ violations: List[str] = []
79
+
80
+ # If method (not @staticmethod), the first param ('self'/'cls') is bound at runtime
81
+ if callee_sym.is_method:
82
+ formal_params = callee_sym.params[1:] if len(callee_sym.params) > 0 else []
83
+ else:
84
+ formal_params = callee_sym.params[:]
85
+
86
+ pos_params = [
87
+ p for p in formal_params if not p.is_kwonly and not p.is_vararg and not p.is_kwarg
88
+ ]
89
+ kwonly_params = [p for p in formal_params if p.is_kwonly]
90
+ has_vararg = any(p.is_vararg for p in formal_params)
91
+ has_kwarg = any(p.is_kwarg for p in formal_params)
92
+
93
+ effective_max_args = None if has_vararg else len(pos_params)
94
+ effective_min_args = len([p for p in pos_params if not p.has_default])
95
+
96
+ # Check max positional args
97
+ if not call.has_vararg and effective_max_args is not None and call.args_count > effective_max_args:
98
+ violations.append(
99
+ f"ARITY_MISMATCH: Caller '{caller_label}' (line {call.lineno}) calls "
100
+ f"'{callee_sym.qualname}' with {call.args_count} positional arguments, "
101
+ f"but '{callee_sym.qualname}' accepts at most {effective_max_args} positional arguments."
102
+ )
103
+
104
+ # Check which positional arguments were supplied
105
+ supplied_pos_params = pos_params[: min(call.args_count, len(pos_params))]
106
+ unsatisfied_pos_params = pos_params[min(call.args_count, len(pos_params)) :]
107
+
108
+ # Check duplicate arguments (passed positionally and by keyword)
109
+ supplied_pos_names = {p.name for p in supplied_pos_params}
110
+ for kw in call.kwargs:
111
+ if kw in supplied_pos_names:
112
+ violations.append(
113
+ f"DUPLICATE_ARGUMENT: Caller '{caller_label}' (line {call.lineno}) provides "
114
+ f"multiple values for argument '{kw}' when calling '{callee_sym.qualname}'."
115
+ )
116
+
117
+ # Check positional-only parameter called as keyword
118
+ posonly_names = {p.name for p in pos_params if p.is_posonly}
119
+ for p in pos_params:
120
+ if p.is_posonly and p.name in call.kwargs:
121
+ violations.append(
122
+ f"KEYWORD_MISMATCH: Caller '{caller_label}' (line {call.lineno}) passed "
123
+ f"positional-only argument '{p.name}' as keyword when calling '{callee_sym.qualname}'."
124
+ )
125
+
126
+ # Check unexpected keyword arguments
127
+ if not has_kwarg:
128
+ accepted_names = {p.name for p in pos_params if not p.is_posonly} | {
129
+ p.name for p in kwonly_params
130
+ }
131
+ for kw in call.kwargs:
132
+ if kw not in accepted_names and kw not in posonly_names:
133
+ violations.append(
134
+ f"KEYWORD_MISMATCH: Caller '{caller_label}' (line {call.lineno}) calls "
135
+ f"'{callee_sym.qualname}' with unexpected keyword argument '{kw}'."
136
+ )
137
+
138
+ # Check missing required positional arguments (if not satisfied via kwargs or dynamic unpacking)
139
+ if not call.has_vararg:
140
+ for p in unsatisfied_pos_params:
141
+ if not p.has_default and p.name not in call.kwargs:
142
+ if not (call.has_kwarg and not p.is_posonly):
143
+ violations.append(
144
+ f"ARITY_MISMATCH: Caller '{caller_label}' (line {call.lineno}) missing "
145
+ f"required argument '{p.name}' when calling '{callee_sym.qualname}' "
146
+ f"(requires at least {effective_min_args} arguments)."
147
+ )
148
+
149
+ # Check missing required keyword-only arguments
150
+ if not call.has_kwarg:
151
+ for p in kwonly_params:
152
+ if not p.has_default and p.name not in call.kwargs:
153
+ violations.append(
154
+ f"KEYWORD_MISMATCH: Caller '{caller_label}' (line {call.lineno}) missing "
155
+ f"required keyword argument '{p.name}' when calling '{callee_sym.qualname}'."
156
+ )
157
+
158
+ return violations
159
+
160
+
161
+ def verify_symbolic_gate(
162
+ patch_result: PatchResult,
163
+ slice_graph: SlicedGraph,
164
+ indexer: WorkspaceIndexer,
165
+ ) -> GateResult:
166
+ """
167
+ Deterministic Symbolic Gate (Stage 4):
168
+ Verifies:
169
+ 1. Syntax validity of patched content.
170
+ 2. Absence of cyclic dependency / circular calls and imports (Tarjan SCC).
171
+ 3. Parameter arity and keyword invariants across callers and callees.
172
+ 4. Deleted symbol references (callers and importers) and dangling imports.
173
+ Runs deterministically in sub-2ms.
174
+ """
175
+ violations: List[str] = []
176
+ cycles_detected: List[List[str]] = []
177
+
178
+ # 1. Syntax check
179
+ if patch_result.syntax_error:
180
+ violations.append(f"SYNTAX_ERROR: {patch_result.syntax_error}")
181
+ return GateResult(
182
+ status="REJECTED",
183
+ confidence=1.0,
184
+ cycles=[],
185
+ violations=violations,
186
+ details={"error_type": "SYNTAX_ERROR"},
187
+ )
188
+
189
+ # 2. Cycle detection via Tarjan's SCC
190
+ # 2A. Call graph cycle detection
191
+ call_graph: Dict[str, List[str]] = {}
192
+ for node_id in slice_graph.nodes:
193
+ call_graph.setdefault(node_id, [])
194
+
195
+ for edge in slice_graph.edges:
196
+ if edge.relation == "CALLS":
197
+ call_graph.setdefault(edge.source, []).append(edge.target)
198
+
199
+ # Ensure calls inside modified/added symbols are included in graph
200
+ for sym in patch_result.affected_symbols + patch_result.added_symbols:
201
+ u_id = sym.id
202
+ call_graph.setdefault(u_id, [])
203
+ for call in sym.calls:
204
+ callee_def = indexer.resolve_callee(call, caller_sym=sym)
205
+ if callee_def:
206
+ v_id = callee_def.id
207
+ if v_id not in call_graph[u_id]:
208
+ call_graph[u_id].append(v_id)
209
+
210
+ raw_call_cycles = find_cycles_tarjan(call_graph)
211
+ if raw_call_cycles:
212
+ cycles_detected.extend(raw_call_cycles)
213
+ for cycle in raw_call_cycles:
214
+ cycle_repr = " -> ".join([c.split("::")[-1] for c in cycle] + [cycle[0].split("::")[-1]])
215
+ violations.append(f"CIRCULAR_DEPENDENCY: Detected call cycle: {cycle_repr}")
216
+
217
+ # 2B. Import cycle detection (direct and multi-hop across workspace)
218
+ if hasattr(indexer, "_import_graph") and indexer._import_graph:
219
+ import_graph: Dict[str, List[str]] = {k: list(v) for k, v in indexer._import_graph.items()}
220
+ else:
221
+ import_graph = {}
222
+ for f_path, f_data in indexer._file_cache.items():
223
+ import_graph.setdefault(f_path, [])
224
+ for imp_data in f_data.get("imports", []):
225
+ imp_obj = indexer._deserialize_import(imp_data)
226
+ target_f = indexer.resolve_import_to_file(imp_obj, f_path)
227
+ if target_f and target_f != f_path:
228
+ if target_f not in import_graph[f_path]:
229
+ import_graph[f_path].append(target_f)
230
+
231
+ raw_import_cycles = find_cycles_tarjan(import_graph)
232
+ for cycle in raw_import_cycles:
233
+ if patch_result.file_path in cycle:
234
+ if cycle not in cycles_detected:
235
+ cycles_detected.append(cycle)
236
+ cycle_repr = " -> ".join(cycle + [cycle[0]])
237
+ violations.append(f"CIRCULAR_DEPENDENCY: Detected import cycle: {cycle_repr}")
238
+
239
+ # 3. Contract / Arity Invariant Checks (Bidirectional)
240
+ seen_call_sites: Set[tuple] = set()
241
+
242
+ # 3A. Existing callers calling modified/added symbols
243
+ for sym in patch_result.affected_symbols + patch_result.added_symbols:
244
+ if sym.kind not in ("function", "async_function", "method"):
245
+ continue
246
+
247
+ callers = indexer.get_callers(sym.qualname)
248
+ if sym.qualname != sym.name:
249
+ for c in indexer.get_callers(sym.name):
250
+ if c not in callers:
251
+ callers.append(c)
252
+
253
+ # If __init__ or constructor method of a class, callers might be instantiating the class by its class name
254
+ if sym.name in ("__init__", "constructor") and "." in sym.qualname:
255
+ class_qualname = sym.qualname.rsplit(".", 1)[0]
256
+ for c in indexer.get_callers(class_qualname):
257
+ if c not in callers:
258
+ callers.append(c)
259
+ class_simple = class_qualname.split(".")[-1]
260
+ if class_simple != class_qualname:
261
+ for c in indexer.get_callers(class_simple):
262
+ if c not in callers:
263
+ callers.append(c)
264
+
265
+ for call in callers:
266
+ caller_label = call.caller or "unknown_caller"
267
+ call_key = (caller_label, call.lineno, sym.id, call.args_count, tuple(sorted(call.kwargs)))
268
+ if call_key in seen_call_sites:
269
+ continue
270
+ seen_call_sites.add(call_key)
271
+
272
+ violations.extend(validate_call_site(call, sym, caller_label))
273
+
274
+ # 3B. Calls MADE BY modified/added symbols (or module level)
275
+ for caller_sym in patch_result.affected_symbols + patch_result.added_symbols:
276
+ caller_label = caller_sym.id
277
+ for call in caller_sym.calls:
278
+ callee_def = indexer.resolve_callee(call, caller_sym=caller_sym)
279
+ if not callee_def:
280
+ continue
281
+
282
+ target_sym = callee_def
283
+ if callee_def.kind == "class":
284
+ # Class instantiation invokes __init__
285
+ init_def = indexer.resolve_class_init(callee_def)
286
+ if init_def:
287
+ target_sym = init_def
288
+ else:
289
+ # Class without custom __init__ accepts 0 arguments
290
+ call_key = (caller_label, call.lineno, callee_def.id, call.args_count, tuple(sorted(call.kwargs)))
291
+ if call_key not in seen_call_sites:
292
+ seen_call_sites.add(call_key)
293
+ if not call.has_vararg and call.args_count > 0:
294
+ violations.append(
295
+ f"ARITY_MISMATCH: Caller '{caller_label}' (line {call.lineno}) calls "
296
+ f"'{callee_def.qualname}' with {call.args_count} positional arguments, "
297
+ f"but '{callee_def.qualname}' accepts at most 0 positional arguments."
298
+ )
299
+ if not call.has_kwarg and call.kwargs:
300
+ for kw in call.kwargs:
301
+ violations.append(
302
+ f"KEYWORD_MISMATCH: Caller '{caller_label}' (line {call.lineno}) calls "
303
+ f"'{callee_def.qualname}' with unexpected keyword argument '{kw}'."
304
+ )
305
+ continue
306
+
307
+ if target_sym.kind in ("function", "async_function", "method"):
308
+ call_key = (caller_label, call.lineno, target_sym.id, call.args_count, tuple(sorted(call.kwargs)))
309
+ if call_key in seen_call_sites:
310
+ continue
311
+ seen_call_sites.add(call_key)
312
+
313
+ violations.extend(validate_call_site(call, target_sym, caller_label))
314
+
315
+ # 4. Deleted Symbol & Broken Reference Invariants
316
+ active_caller_ids = {s.id for s in patch_result.all_patched_symbols}
317
+ for del_sym in patch_result.deleted_symbols:
318
+ # Check callers
319
+ callers = indexer.get_callers(del_sym.name)
320
+ if del_sym.qualname != del_sym.name:
321
+ callers.extend(indexer.get_callers(del_sym.qualname))
322
+
323
+ active_callers = []
324
+ for c in callers:
325
+ if not c.caller:
326
+ continue
327
+ if c.caller.startswith(f"{patch_result.file_path}::"):
328
+ if c.caller in active_caller_ids:
329
+ active_callers.append(c)
330
+ else:
331
+ active_callers.append(c)
332
+
333
+ for c in active_callers:
334
+ violations.append(
335
+ f"BROKEN_REFERENCE: Symbol '{del_sym.qualname}' was deleted in patch, "
336
+ f"but is called by '{c.caller or 'unknown'}' at line {c.lineno}."
337
+ )
338
+
339
+ # Check importers
340
+ importers = indexer.get_importers(del_sym.name)
341
+ if del_sym.qualname != del_sym.name:
342
+ importers.extend(indexer.get_importers(del_sym.qualname))
343
+
344
+ active_importers = [
345
+ imp for imp in importers
346
+ if imp.file_path != patch_result.file_path
347
+ ]
348
+ for imp in active_importers:
349
+ violations.append(
350
+ f"BROKEN_REFERENCE: Symbol '{del_sym.qualname}' was deleted in patch, "
351
+ f"but is imported by '{imp.file_path}' at line {imp.lineno}."
352
+ )
353
+
354
+ # 4B. Check if any symbol imported by the patch does not exist in workspace module
355
+ for imp in patch_result.imports:
356
+ if (imp.module or imp.level > 0) and imp.name != "*":
357
+ target_f = indexer.resolve_import_to_file(imp, patch_result.file_path)
358
+ if not target_f:
359
+ mod_str = imp.module or ""
360
+ if mod_str.startswith("./") or mod_str.startswith("../") or imp.level > 0:
361
+ violations.append(
362
+ f"BROKEN_REFERENCE: Cannot resolve relative import '{mod_str}' in '{patch_result.file_path}' (line {imp.lineno}) - file does not exist."
363
+ )
364
+ continue
365
+
366
+ if target_f in indexer._file_cache:
367
+ cached = indexer._file_cache[target_f]
368
+ defined_names = {s["name"] for s in cached.get("symbols", [])}
369
+ imported_names = {i.get("asname") or i["name"] for i in cached.get("imports", [])}
370
+
371
+ # If target module re-exports with star import, allow dynamic symbols
372
+ if "*" in {i["name"] for i in cached.get("imports", [])}:
373
+ continue
374
+
375
+ if imp.name not in defined_names and imp.name not in imported_names:
376
+ target_full = indexer.workspace_root / target_f
377
+ target_dir = target_full.parent
378
+ submod_cands = [
379
+ target_dir / f"{imp.name}.py",
380
+ target_dir / imp.name / "__init__.py",
381
+ target_dir / f"{imp.name}.ts",
382
+ target_dir / f"{imp.name}.tsx",
383
+ target_dir / f"{imp.name}.js",
384
+ target_dir / imp.name / "index.ts",
385
+ target_dir / imp.name / "index.js",
386
+ target_dir / f"{imp.name}.rs",
387
+ target_dir / imp.name / "mod.rs",
388
+ target_dir / f"{imp.name}.go",
389
+ ]
390
+ has_symbol = any(c.exists() for c in submod_cands)
391
+ if not has_symbol and target_f.endswith(".go"):
392
+ # In Go, imports are package-level (e.g. import "scorp-agent/agent").
393
+ # The identifier is the package namespace, which is valid if the package dir has Go files.
394
+ if target_dir.is_dir() and any(target_dir.glob("*.go")):
395
+ has_symbol = True
396
+ if not has_symbol and target_full.exists():
397
+ if target_f.endswith(".py"):
398
+ try:
399
+ src = target_full.read_text(encoding="utf-8", errors="ignore")
400
+ tree = ast.parse(src)
401
+ for node in ast.walk(tree):
402
+ if isinstance(node, (ast.Assign, ast.AnnAssign)):
403
+ target_list = node.targets if isinstance(node, ast.Assign) else [node.target]
404
+ for t in target_list:
405
+ for child in ast.walk(t):
406
+ if isinstance(child, ast.Name) and child.id == imp.name:
407
+ has_symbol = True
408
+ break
409
+ if has_symbol:
410
+ break
411
+ elif isinstance(node, ast.NamedExpr):
412
+ if isinstance(node.target, ast.Name) and node.target.id == imp.name:
413
+ has_symbol = True
414
+ break
415
+ elif isinstance(node, getattr(ast, "TypeAlias", ())):
416
+ if isinstance(node.name, ast.Name) and node.name.id == imp.name:
417
+ has_symbol = True
418
+ break
419
+ if has_symbol:
420
+ break
421
+ except Exception:
422
+ has_symbol = True
423
+ else:
424
+ # For TypeScript, Go, Rust: check extracted symbols from target file
425
+ try:
426
+ src = target_full.read_text(encoding="utf-8", errors="ignore")
427
+ from code_oracle.languages import extract_symbols
428
+ syms = extract_symbols(src, file_path=target_f)
429
+ if any(s.name == imp.name for s in syms):
430
+ has_symbol = True
431
+ except Exception:
432
+ pass
433
+ if not has_symbol:
434
+ module_label = imp.module if imp.module else ("." * imp.level)
435
+ violations.append(
436
+ f"BROKEN_REFERENCE: Symbol '{imp.name}' imported from '{module_label}' "
437
+ f"does not exist in '{target_f}' (line {imp.lineno})."
438
+ )
439
+
440
+ # 5. Verdict
441
+ if violations or cycles_detected:
442
+ status = "REJECTED"
443
+ confidence = 0.95
444
+ else:
445
+ status = "APPROVED"
446
+ confidence = 0.98
447
+
448
+ return GateResult(
449
+ status=status,
450
+ confidence=confidence,
451
+ cycles=cycles_detected,
452
+ violations=violations,
453
+ details={
454
+ "nodes_checked": len(slice_graph.nodes),
455
+ "edges_checked": len(slice_graph.edges),
456
+ "cycles_count": len(cycles_detected),
457
+ "violations_count": len(violations),
458
+ },
459
+ )