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
@@ -0,0 +1,166 @@
1
+ """
2
+ Stage 5: Graph Linearizer.
3
+ Serializes the sliced subgraph and patch metadata into a compact Domain-Specific Language (< 400 tokens)
4
+ for Laya ModernBERT decision head ingestion.
5
+ """
6
+
7
+ import re
8
+ from typing import List, Optional
9
+
10
+ from code_oracle.models import GateResult, PatchResult, SlicedGraph
11
+
12
+
13
+ def estimate_tokens(text: str) -> int:
14
+ """
15
+ Conservative token estimator matching BPE / WordPiece tokenizers.
16
+ Counts word/identifier chunks and individual punctuation symbols.
17
+ """
18
+ if not text:
19
+ return 0
20
+ tokens = re.findall(r"[A-Za-z0-9_]+|[^\w\s]", text)
21
+ return len(tokens)
22
+
23
+
24
+ def linearize_subgraph(
25
+ patch_result: PatchResult,
26
+ slice_graph: SlicedGraph,
27
+ gate_result: GateResult,
28
+ max_tokens: int = 400,
29
+ ) -> str:
30
+ """
31
+ Convert patch metadata, sliced subgraph, and gate verdict into a compact DSL.
32
+ Guarantees output size <= max_tokens (default 400 tokens).
33
+ """
34
+ # 1. Diff target header
35
+ if patch_result.affected_symbols:
36
+ seed_names = [s.qualname for s in patch_result.affected_symbols]
37
+ seed_str = ", ".join(seed_names)
38
+ target_header = f"[DIFF_TARGET] {patch_result.file_path}::{seed_str} (MODIFIED)"
39
+ elif patch_result.deleted_symbols:
40
+ del_names = [s.qualname for s in patch_result.deleted_symbols]
41
+ seed_str = ", ".join(del_names)
42
+ target_header = f"[DIFF_TARGET] {patch_result.file_path}::{seed_str} (DELETED)"
43
+ elif patch_result.added_symbols:
44
+ add_names = [s.qualname for s in patch_result.added_symbols]
45
+ seed_str = ", ".join(add_names)
46
+ target_header = f"[DIFF_TARGET] {patch_result.file_path}::{seed_str} (ADDED)"
47
+ else:
48
+ target_header = f"[DIFF_TARGET] {patch_result.file_path}::<module> (MODIFIED)"
49
+
50
+ def format_line_numbers(line_set: set) -> str:
51
+ if not line_set:
52
+ return "[]"
53
+ sorted_lines = sorted(list(line_set))
54
+ if len(sorted_lines) <= 6:
55
+ return str(sorted_lines)
56
+ return f"[{sorted_lines[0]}..{sorted_lines[-1]}] ({len(sorted_lines)} lines)"
57
+
58
+ lines_str = f"OldLines: {format_line_numbers(patch_result.modified_old_lines)} | NewLines: {format_line_numbers(patch_result.modified_new_lines)}"
59
+ meta_header = f"[METADATA] File: {patch_result.file_path} | {lines_str} | Nodes: {len(slice_graph.nodes)} | Edges: {len(slice_graph.edges)}"
60
+
61
+ # 2. Gate Section
62
+ status_str = f"STATUS: {gate_result.status} (conf: {round(gate_result.confidence, 2)})"
63
+ cycles_str = f"CYCLES: {len(gate_result.cycles)}"
64
+ if gate_result.violations:
65
+ violations_str = "VIOLATIONS:\n - " + "\n - ".join(gate_result.violations[:3])
66
+ if len(gate_result.violations) > 3:
67
+ violations_str += f"\n - ... ({len(gate_result.violations) - 3} more)"
68
+ else:
69
+ violations_str = "VIOLATIONS: NONE"
70
+
71
+ gate_section = f"[GATE]\n{status_str}\n{cycles_str}\n{violations_str}"
72
+
73
+ # 3. Nodes and Edges preparation
74
+ node_id_map = {}
75
+ ordered_nodes = []
76
+
77
+ # Put seeds first
78
+ for n_id, node in slice_graph.nodes.items():
79
+ if node.is_seed:
80
+ node_id_map[n_id] = f"N{len(node_id_map)}"
81
+ ordered_nodes.append((n_id, node))
82
+
83
+ # Then non-seed nodes
84
+ for n_id, node in slice_graph.nodes.items():
85
+ if not node.is_seed:
86
+ node_id_map[n_id] = f"N{len(node_id_map)}"
87
+ ordered_nodes.append((n_id, node))
88
+
89
+ def build_dsl(active_node_pairs: List[tuple]) -> str:
90
+ active_ids = {n_id for n_id, _ in active_node_pairs}
91
+
92
+ node_lines = []
93
+ for n_id, node in active_node_pairs:
94
+ short_id = node_id_map[n_id]
95
+ flags = []
96
+ if node.is_seed:
97
+ flags.append("SEED")
98
+ if node.is_modified:
99
+ flags.append("MODIFIED")
100
+ if node.truncated:
101
+ flags.append("TRUNCATED")
102
+ flag_str = f" ({', '.join(flags)})" if flags else ""
103
+ node_lines.append(f"{short_id}: {node.file_path}::{node.name} [{node.signature}]{flag_str}")
104
+
105
+ edge_lines = []
106
+ for edge in slice_graph.edges:
107
+ if edge.source in active_ids and edge.target in active_ids:
108
+ src_short = node_id_map.get(edge.source, edge.source.split("::")[-1])
109
+ tgt_short = node_id_map.get(edge.target, edge.target.split("::")[-1])
110
+ edge_lines.append(f"{src_short} -> {tgt_short} ({edge.relation})")
111
+
112
+ nodes_section = "[NODES]\n" + ("\n".join(node_lines) if node_lines else "(none)")
113
+ edges_section = "[EDGES]\n" + ("\n".join(edge_lines) if edge_lines else "(none)")
114
+
115
+ return f"{target_header}\n{meta_header}\n{nodes_section}\n{edges_section}\n{gate_section}"
116
+
117
+ # Try full graph first
118
+ dsl = build_dsl(ordered_nodes)
119
+ current_tokens = estimate_tokens(dsl)
120
+
121
+ # If exceeding max_tokens, prune non-seed nodes from tail until under budget
122
+ if current_tokens > max_tokens:
123
+ seed_nodes = [(n_id, n) for n_id, n in ordered_nodes if n.is_seed]
124
+ non_seed_nodes = [(n_id, n) for n_id, n in ordered_nodes if not n.is_seed]
125
+
126
+ pruned = False
127
+ while non_seed_nodes:
128
+ non_seed_nodes.pop()
129
+ candidate_dsl = build_dsl(seed_nodes + non_seed_nodes)
130
+ candidate_dsl += f"\n[TRUNCATED: {len(ordered_nodes) - len(seed_nodes) - len(non_seed_nodes)} peripheral nodes pruned for token budget]"
131
+ if estimate_tokens(candidate_dsl) <= max_tokens:
132
+ dsl = candidate_dsl
133
+ pruned = True
134
+ break
135
+
136
+ if not pruned:
137
+ candidate_dsl = build_dsl(seed_nodes)
138
+ candidate_dsl += f"\n[TRUNCATED: all non-seed nodes pruned]"
139
+ if estimate_tokens(candidate_dsl) <= max_tokens:
140
+ dsl = candidate_dsl
141
+ else:
142
+ # Even seed nodes exceed max_tokens; prune excess seeds
143
+ while len(seed_nodes) > 1:
144
+ seed_nodes.pop()
145
+ cand = build_dsl(seed_nodes) + f"\n[TRUNCATED: excess seeds pruned]"
146
+ if estimate_tokens(cand) <= max_tokens:
147
+ dsl = cand
148
+ break
149
+ else:
150
+ dsl = candidate_dsl
151
+
152
+ # Hard guarantee: strictly under or equal to max_tokens budget
153
+ if estimate_tokens(dsl) > max_tokens:
154
+ dsl_lines = dsl.splitlines()
155
+ min_lines = min(2, len(dsl_lines))
156
+ while len(dsl_lines) > min_lines and estimate_tokens("\n".join(dsl_lines)) > max_tokens:
157
+ dsl_lines.pop()
158
+ dsl = "\n".join(dsl_lines)
159
+
160
+ if estimate_tokens(dsl) > max_tokens:
161
+ tokens_list = dsl.split()
162
+ while tokens_list and estimate_tokens(" ".join(tokens_list)) > max_tokens:
163
+ tokens_list.pop()
164
+ dsl = " ".join(tokens_list)
165
+
166
+ return dsl
code_oracle/locator.py ADDED
@@ -0,0 +1,301 @@
1
+ """
2
+ Stage 1: Diff Boundary Locator.
3
+ Maps patch diff line changes to AST symbol spans (functions, methods, classes).
4
+ """
5
+
6
+ import ast
7
+ import difflib
8
+ import re
9
+ from pathlib import Path
10
+ from typing import List, Optional, Set, Tuple
11
+
12
+ from code_oracle.languages import extract_imports, extract_symbols, validate_syntax
13
+ from code_oracle.models import CallReference, DiffHunk, ImportReference, Parameter, PatchResult, Symbol
14
+
15
+
16
+ def parse_unified_diff(diff_text: str) -> List[DiffHunk]:
17
+ """Parse unified diff text into structured DiffHunk objects."""
18
+ hunk_pattern = re.compile(r"^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@")
19
+ hunks: List[DiffHunk] = []
20
+ current_hunk: Optional[DiffHunk] = None
21
+
22
+ for line in diff_text.splitlines():
23
+ match = hunk_pattern.match(line)
24
+ if match:
25
+ old_start = int(match.group(1))
26
+ old_count = int(match.group(2)) if match.group(2) else 1
27
+ new_start = int(match.group(3)) if match.group(3) else 1
28
+ new_count = int(match.group(4)) if match.group(4) else 1
29
+ current_hunk = DiffHunk(
30
+ old_start=old_start,
31
+ old_count=old_count,
32
+ new_start=new_start,
33
+ new_count=new_count,
34
+ lines=[],
35
+ )
36
+ hunks.append(current_hunk)
37
+ elif current_hunk is not None:
38
+ if line.startswith(("--- ", "+++ ", "diff --git", "index ")):
39
+ current_hunk = None
40
+ elif line.startswith(("+", "-", " ", "\\")):
41
+ current_hunk.lines.append(line)
42
+ elif line == "":
43
+ current_hunk.lines.append(" ")
44
+
45
+ return hunks
46
+
47
+
48
+ def apply_patch(
49
+ original_text: str,
50
+ patch_text: str,
51
+ is_replacement: bool = False,
52
+ ) -> Tuple[str, Set[int], Set[int]]:
53
+ """
54
+ Applies unified diff or replacement text in memory.
55
+ Returns:
56
+ (patched_text, modified_old_lines, modified_new_lines)
57
+ """
58
+ hunks = [] if is_replacement else parse_unified_diff(patch_text)
59
+
60
+ # If no diff hunks found or explicitly replacement, treat patch_text as full replacement content
61
+ if is_replacement or not hunks:
62
+ orig_lines = original_text.splitlines(keepends=True)
63
+ patched_lines = patch_text.splitlines(keepends=True)
64
+
65
+ if not original_text.strip():
66
+ # Brand new file
67
+ new_lines = set(range(1, len(patched_lines) + 1))
68
+ return patch_text, set(), new_lines
69
+
70
+ # Calculate line differences via difflib
71
+ matcher = difflib.SequenceMatcher(None, orig_lines, patched_lines)
72
+ old_modified = set()
73
+ new_modified = set()
74
+ for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
75
+ if tag != "equal":
76
+ for l in range(alo + 1, ahi + 1):
77
+ old_modified.add(l)
78
+ for l in range(blo + 1, bhi + 1):
79
+ new_modified.add(l)
80
+ return patch_text, old_modified, new_modified
81
+
82
+ # Apply unified diff hunks line by line
83
+ orig_lines = original_text.splitlines(keepends=True)
84
+ out_lines: List[str] = []
85
+ orig_idx = 0 # 0-indexed cursor into orig_lines
86
+ old_modified = set()
87
+ new_modified = set()
88
+ current_new_line = 1
89
+
90
+ for hunk in hunks:
91
+ target_orig_idx = max(0, hunk.old_start - 1) if hunk.old_start > 0 else 0
92
+ while orig_idx < target_orig_idx and orig_idx < len(orig_lines):
93
+ out_lines.append(orig_lines[orig_idx])
94
+ orig_idx += 1
95
+ current_new_line += 1
96
+
97
+ cur_old_line = hunk.old_start
98
+ for line in hunk.lines:
99
+ if line.startswith("+"):
100
+ content = line[1:]
101
+ if not content.endswith("\n"):
102
+ content += "\n"
103
+ out_lines.append(content)
104
+ new_modified.add(current_new_line)
105
+ current_new_line += 1
106
+ elif line.startswith("-"):
107
+ old_modified.add(cur_old_line)
108
+ cur_old_line += 1
109
+ orig_idx += 1
110
+ elif line.startswith(" "):
111
+ content = line[1:]
112
+ if not content.endswith("\n"):
113
+ content += "\n"
114
+ out_lines.append(content)
115
+ cur_old_line += 1
116
+ orig_idx += 1
117
+ current_new_line += 1
118
+ # Ignore ''
119
+
120
+ while orig_idx < len(orig_lines):
121
+ out_lines.append(orig_lines[orig_idx])
122
+ orig_idx += 1
123
+ current_new_line += 1
124
+
125
+ return "".join(out_lines), old_modified, new_modified
126
+
127
+
128
+ def _extract_calls_from_node(node: ast.AST, caller_id: Optional[str] = None) -> List[CallReference]:
129
+ """Extract all function/method calls inside an AST node."""
130
+ calls: List[CallReference] = []
131
+ for child in ast.walk(node):
132
+ if isinstance(child, ast.Call):
133
+ try:
134
+ callee_name = ast.unparse(child.func)
135
+ except Exception:
136
+ callee_name = "<unknown>"
137
+ kwargs = [kw.arg for kw in child.keywords if kw.arg is not None]
138
+ has_vararg = any(isinstance(a, ast.Starred) for a in child.args)
139
+ has_kwarg = any(kw.arg is None for kw in child.keywords)
140
+ calls.append(
141
+ CallReference(
142
+ callee=callee_name,
143
+ args_count=len(child.args),
144
+ kwargs=kwargs,
145
+ lineno=getattr(child, "lineno", 0),
146
+ caller=caller_id,
147
+ has_vararg=has_vararg,
148
+ has_kwarg=has_kwarg,
149
+ )
150
+ )
151
+ return calls
152
+
153
+
154
+ def extract_imports_from_ast(source: str, file_path: str = "") -> List[ImportReference]:
155
+ """Extract all import statements from source for any supported language."""
156
+ return extract_imports(source, file_path=file_path)
157
+
158
+
159
+ def extract_symbols_from_ast(source: str, file_path: str = "") -> List[Symbol]:
160
+ """Parse source into AST and extract symbol entities with detailed metadata."""
161
+ if not source.strip():
162
+ return []
163
+ return extract_symbols(source, file_path=file_path)
164
+
165
+
166
+
167
+ def locate_affected_symbols(
168
+ file_path: str,
169
+ patch_content: str,
170
+ workspace_root: Optional[Path] = None,
171
+ original_content: Optional[str] = None,
172
+ is_replacement: bool = False,
173
+ ) -> PatchResult:
174
+ """
175
+ Stage 1 Diff Boundary Locator:
176
+ Maps patch changes to precise AST symbol spans.
177
+ """
178
+ full_path = Path(file_path)
179
+ if workspace_root:
180
+ if not full_path.is_absolute():
181
+ full_path = (workspace_root / file_path).resolve()
182
+ else:
183
+ full_path = full_path.resolve()
184
+ try:
185
+ clean_path = str(full_path.relative_to(workspace_root.resolve())).replace("\\", "/")
186
+ except ValueError:
187
+ clean_path = str(file_path).replace("\\", "/")
188
+ else:
189
+ clean_path = str(file_path).replace("\\", "/")
190
+
191
+ # Read original file if not supplied
192
+ if original_content is None:
193
+ if full_path.exists() and full_path.is_file():
194
+ try:
195
+ original_content = full_path.read_text(encoding="utf-8")
196
+ except Exception:
197
+ original_content = ""
198
+ else:
199
+ original_content = ""
200
+
201
+ # Apply patch
202
+ patched_content, old_lines, new_lines = apply_patch(
203
+ original_content, patch_content, is_replacement=is_replacement
204
+ )
205
+
206
+ # Check syntax of patched content
207
+ syntax_error = validate_syntax(patched_content, file_path=clean_path)
208
+ if syntax_error:
209
+ return PatchResult(
210
+ file_path=clean_path,
211
+ original_content=original_content,
212
+ patched_content=patched_content,
213
+ modified_old_lines=old_lines,
214
+ modified_new_lines=new_lines,
215
+ syntax_error=syntax_error,
216
+ )
217
+
218
+ orig_symbols = extract_symbols_from_ast(original_content, file_path=clean_path)
219
+ patched_symbols = extract_symbols_from_ast(patched_content, file_path=clean_path)
220
+ patched_imports = extract_imports_from_ast(patched_content, file_path=clean_path)
221
+
222
+ orig_map = {s.qualname: s for s in orig_symbols}
223
+ patched_map = {s.qualname: s for s in patched_symbols}
224
+
225
+ # Identify deleted and added symbols
226
+ deleted_symbols = [s for q, s in orig_map.items() if q not in patched_map]
227
+ added_symbols = [s for q, s in patched_map.items() if q not in orig_map]
228
+
229
+ # Identify affected symbols (modified or newly added)
230
+ affected: List[Symbol] = []
231
+ seen_qualnames = set()
232
+
233
+ for sym in patched_symbols:
234
+ if sym.kind == "module":
235
+ continue
236
+ # Check if any new line overlaps with symbol span
237
+ sym_lines = set(range(sym.lineno, sym.end_lineno + 1))
238
+ if sym_lines.intersection(new_lines):
239
+ affected.append(sym)
240
+ seen_qualnames.add(sym.qualname)
241
+
242
+ # Also check if old symbols were modified/removed and not yet caught
243
+ for sym in orig_symbols:
244
+ if sym.kind == "module":
245
+ continue
246
+ sym_lines = set(range(sym.lineno, sym.end_lineno + 1))
247
+ if sym_lines.intersection(old_lines):
248
+ if sym.qualname in patched_map and sym.qualname not in seen_qualnames:
249
+ affected.append(patched_map[sym.qualname])
250
+ seen_qualnames.add(sym.qualname)
251
+
252
+ # Check if lines outside any function/class symbol were modified
253
+ non_module_patched = [s for s in patched_symbols if s.kind != "module"]
254
+ non_module_orig = [s for s in orig_symbols if s.kind != "module"]
255
+ has_module_level_change = (
256
+ any(not any(s.lineno <= l <= s.end_lineno for s in non_module_patched) for l in new_lines)
257
+ or any(not any(s.lineno <= l <= s.end_lineno for s in non_module_orig) for l in old_lines)
258
+ )
259
+
260
+ if has_module_level_change or (not affected and (old_lines or new_lines)):
261
+ module_sym = next((s for s in patched_symbols if s.qualname == "<module>"), None)
262
+ if not module_sym:
263
+ line_count = len(patched_content.splitlines()) or 1
264
+ module_calls: List[CallReference] = []
265
+ if clean_path.endswith(".py") or not clean_path:
266
+ try:
267
+ tree = ast.parse(patched_content)
268
+ module_calls = [
269
+ c for c in _extract_calls_from_node(tree, caller_id=f"{clean_path}::<module>")
270
+ if not any(s.lineno <= c.lineno <= s.end_lineno for s in non_module_patched)
271
+ ]
272
+ except Exception:
273
+ pass
274
+
275
+ module_sym = Symbol(
276
+ name="<module>",
277
+ qualname="<module>",
278
+ file_path=clean_path,
279
+ kind="module",
280
+ lineno=1,
281
+ end_lineno=line_count,
282
+ signature=f"# module {clean_path}",
283
+ calls=module_calls,
284
+ )
285
+ patched_symbols.append(module_sym)
286
+
287
+ if module_sym not in affected:
288
+ affected.append(module_sym)
289
+
290
+ return PatchResult(
291
+ file_path=clean_path,
292
+ original_content=original_content,
293
+ patched_content=patched_content,
294
+ modified_old_lines=old_lines,
295
+ modified_new_lines=new_lines,
296
+ affected_symbols=affected,
297
+ deleted_symbols=deleted_symbols,
298
+ added_symbols=added_symbols,
299
+ all_patched_symbols=patched_symbols,
300
+ imports=patched_imports,
301
+ )
code_oracle/models.py ADDED
@@ -0,0 +1,237 @@
1
+ """
2
+ Core data structures for the TopoSlice neuro-symbolic verification engine.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import List, Dict, Optional, Set, Any
7
+
8
+
9
+ @dataclass
10
+ class CallReference:
11
+ """Represents a function or method call site."""
12
+ callee: str
13
+ args_count: int
14
+ kwargs: List[str] = field(default_factory=list)
15
+ lineno: int = 0
16
+ caller: Optional[str] = None
17
+ has_vararg: bool = False
18
+ has_kwarg: bool = False
19
+
20
+
21
+ @dataclass
22
+ class ImportReference:
23
+ """Represents an imported module or symbol reference."""
24
+ module: Optional[str]
25
+ name: str
26
+ asname: Optional[str] = None
27
+ lineno: int = 0
28
+ file_path: str = ""
29
+ level: int = 0
30
+
31
+
32
+ @dataclass
33
+ class Parameter:
34
+ """Represents a function parameter in an AST."""
35
+ name: str
36
+ annotation: Optional[str] = None
37
+ default: Optional[str] = None
38
+ has_default: bool = False
39
+ is_vararg: bool = False
40
+ is_kwarg: bool = False
41
+ is_kwonly: bool = False
42
+ is_posonly: bool = False
43
+
44
+
45
+ @dataclass
46
+ class Symbol:
47
+ """Represents a code symbol (function, method, class, module)."""
48
+ name: str
49
+ qualname: str
50
+ file_path: str
51
+ kind: str # 'function', 'async_function', 'class', 'method', 'module'
52
+ lineno: int
53
+ end_lineno: int
54
+ signature: str = ""
55
+ params: List[Parameter] = field(default_factory=list)
56
+ min_args: int = 0
57
+ max_args: Optional[int] = None # None indicates *args allowed
58
+ accepted_kwargs: Optional[Set[str]] = None # None indicates **kwargs allowed
59
+ required_kwargs: Set[str] = field(default_factory=set)
60
+ return_type: Optional[str] = None
61
+ calls: List[CallReference] = field(default_factory=list)
62
+ is_method: bool = False
63
+ is_static: bool = False
64
+ bases: List[str] = field(default_factory=list)
65
+ docstring: Optional[str] = None
66
+ is_exported: bool = False
67
+ visibility: str = "internal"
68
+
69
+ @property
70
+ def id(self) -> str:
71
+ return f"{self.file_path}::{self.qualname}"
72
+
73
+
74
+ @dataclass
75
+ class DiffHunk:
76
+ """Represents a unified diff hunk."""
77
+ old_start: int
78
+ old_count: int
79
+ new_start: int
80
+ new_count: int
81
+ lines: List[str] = field(default_factory=list)
82
+
83
+
84
+ @dataclass
85
+ class PatchResult:
86
+ """Represents the output of the Diff Boundary Locator."""
87
+ file_path: str
88
+ original_content: str
89
+ patched_content: str
90
+ modified_old_lines: Set[int] = field(default_factory=set)
91
+ modified_new_lines: Set[int] = field(default_factory=set)
92
+ affected_symbols: List[Symbol] = field(default_factory=list)
93
+ deleted_symbols: List[Symbol] = field(default_factory=list)
94
+ added_symbols: List[Symbol] = field(default_factory=list)
95
+ all_patched_symbols: List[Symbol] = field(default_factory=list)
96
+ imports: List[ImportReference] = field(default_factory=list)
97
+ syntax_error: Optional[str] = None
98
+
99
+
100
+ @dataclass
101
+ class SliceNode:
102
+ """Node in the k-hop sliced subgraph."""
103
+ id: str
104
+ name: str
105
+ file_path: str
106
+ kind: str
107
+ signature: str
108
+ is_seed: bool = False
109
+ is_modified: bool = False
110
+ truncated: bool = False
111
+ symbol: Optional[Symbol] = None
112
+
113
+
114
+ @dataclass
115
+ class SliceEdge:
116
+ """Directed edge in the k-hop sliced subgraph."""
117
+ source: str
118
+ target: str
119
+ relation: str # 'CALLS', 'IMPORTS', 'INHERITS'
120
+
121
+
122
+ @dataclass
123
+ class SlicedGraph:
124
+ """Compact directed subgraph representing the affected neighborhood."""
125
+ nodes: Dict[str, SliceNode] = field(default_factory=dict)
126
+ edges: List[SliceEdge] = field(default_factory=list)
127
+ seed_ids: Set[str] = field(default_factory=set)
128
+ truncated: bool = False
129
+
130
+
131
+ @dataclass
132
+ class GateResult:
133
+ """Outcome of the Deterministic Symbolic Gate."""
134
+ status: str # 'APPROVED', 'REJECTED'
135
+ confidence: float
136
+ cycles: List[List[str]] = field(default_factory=list)
137
+ violations: List[str] = field(default_factory=list)
138
+ details: Dict[str, Any] = field(default_factory=dict)
139
+
140
+
141
+ @dataclass
142
+ class VerificationReport:
143
+ """Complete verification response for agent consumption."""
144
+ status: str
145
+ confidence: float
146
+ cycles_detected: List[List[str]]
147
+ invariant_violations: List[str]
148
+ linearized_subgraph: str
149
+ affected_symbols: List[str]
150
+ latency_ms: float
151
+ risk_score: float = 0.05
152
+
153
+ def to_dict(self) -> Dict[str, Any]:
154
+ return {
155
+ "status": self.status,
156
+ "confidence": round(self.confidence, 4),
157
+ "risk_score": round(self.risk_score, 4),
158
+ "cycles_detected": self.cycles_detected,
159
+ "invariant_violations": self.invariant_violations,
160
+ "linearized_subgraph": self.linearized_subgraph,
161
+ "affected_symbols": self.affected_symbols,
162
+ "latency_ms": round(self.latency_ms, 2),
163
+ }
164
+
165
+
166
+ @dataclass
167
+ class RiskTaxonomyScores:
168
+ """Multi-label risk taxonomy probabilities."""
169
+ breaking_public_api: float = 0.0
170
+ security_surface: float = 0.0
171
+ concurrency_hazard: float = 0.0
172
+ performance_regression: float = 0.0
173
+ silent_logic_drift: float = 0.0
174
+
175
+ def active_categories(self, threshold: float = 0.5) -> List[str]:
176
+ """Return list of active risk categories exceeding threshold."""
177
+ categories = []
178
+ if self.breaking_public_api >= threshold:
179
+ categories.append("BreakingPublicAPI")
180
+ if self.security_surface >= threshold:
181
+ categories.append("SecuritySurface")
182
+ if self.concurrency_hazard >= threshold:
183
+ categories.append("ConcurrencyHazard")
184
+ if self.performance_regression >= threshold:
185
+ categories.append("PerformanceRegression")
186
+ if self.silent_logic_drift >= threshold:
187
+ categories.append("SilentLogicDrift")
188
+ return categories
189
+
190
+ def to_dict(self) -> Dict[str, float]:
191
+ return {
192
+ "breaking_public_api": round(self.breaking_public_api, 4),
193
+ "security_surface": round(self.security_surface, 4),
194
+ "concurrency_hazard": round(self.concurrency_hazard, 4),
195
+ "performance_regression": round(self.performance_regression, 4),
196
+ "silent_logic_drift": round(self.silent_logic_drift, 4),
197
+ }
198
+
199
+
200
+ @dataclass
201
+ class EnhancedVerificationReport:
202
+ """
203
+ Backward-compatible verification report featuring Multi-Task Risk Taxonomy
204
+ and Epistemic Uncertainty Estimation.
205
+ """
206
+ status: str # "APPROVED" | "REJECTED"
207
+ confidence: float # Calibrated epistemic confidence [0.0 - 1.0]
208
+ risk_score: float # Continuous calibrated risk [0.0 - 1.0]
209
+ epistemic_uncertainty: float # Predicted variance sigma^2
210
+ risk_taxonomy: RiskTaxonomyScores
211
+ active_risk_categories: List[str]
212
+ cycles_detected: List[List[str]]
213
+ invariant_violations: List[str]
214
+ linearized_subgraph: str
215
+ affected_symbols: List[str]
216
+ latency_ms: float
217
+ is_neural_calibrated: bool = False
218
+ engine_mode: str = "heuristic"
219
+
220
+ def to_dict(self) -> Dict[str, Any]:
221
+ """Convert report to JSON-serializable dictionary with backward compatibility."""
222
+ return {
223
+ "status": self.status,
224
+ "confidence": round(self.confidence, 4),
225
+ "risk_score": round(self.risk_score, 4),
226
+ "epistemic_uncertainty": round(self.epistemic_uncertainty, 4),
227
+ "risk_taxonomy": self.risk_taxonomy.to_dict(),
228
+ "active_risk_categories": self.active_risk_categories,
229
+ "cycles_detected": self.cycles_detected,
230
+ "invariant_violations": self.invariant_violations,
231
+ "linearized_subgraph": self.linearized_subgraph,
232
+ "affected_symbols": self.affected_symbols,
233
+ "latency_ms": round(self.latency_ms, 2),
234
+ "is_neural_calibrated": self.is_neural_calibrated,
235
+ "engine_mode": self.engine_mode,
236
+ }
237
+