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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
|