@orangepro/orangepro-mcp 0.2.43 → 0.2.45
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.
- package/dist/local/analyze/analyzer.js +331 -17
- package/dist/local/analyze/treeSitter/engine.js +1157 -13
- package/dist/local/autoProve.js +377 -97
- package/dist/local/operations.js +65 -16
- package/dist/local/proofDoctor.js +27 -5
- package/dist/local/provenance.js +2 -2
- package/dist/local/score/risk.js +284 -104
- package/dist/local/score/riskConfig.js +17 -1
- package/dist/local/viz/behaviorReportData.js +54 -18
- package/dist/local/viz/behaviorReportHtml.js +12 -1
- package/package.json +1 -1
- package/scripts/spikes/python-dynamic-proof-spike.mjs +399 -103
- package/scripts/spikes/python-mutate.py +72 -24
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Fail-closed Python mutation helper for dynamic proof.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
A sentinel is selected from the declared return annotation. An unannotated function
|
|
5
|
+
may use only a direct, unambiguous observed test oracle supplied by the spike; all
|
|
6
|
+
other shapes are refused rather than guessing a type-incompatible mutation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -24,65 +24,113 @@ def leading_ws(line: str) -> str:
|
|
|
24
24
|
return line[: len(line) - len(line.lstrip(" \t"))]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def annotation_name(annotation: ast.expr | None) -> str | None:
|
|
28
|
+
if annotation is None:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
return ast.unparse(annotation).replace(" ", "")
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def sentinel_for(annotation: ast.expr | None, oracle_type: str | None) -> tuple[str, str] | None:
|
|
37
|
+
"""Return (expression, provenance), never infer an unsupported return type."""
|
|
38
|
+
name = annotation_name(annotation)
|
|
39
|
+
if name:
|
|
40
|
+
lowered = name.lower().replace("typing.", "").replace("collections.abc.", "")
|
|
41
|
+
if lowered == "bool":
|
|
42
|
+
return "False", "annotation:bool"
|
|
43
|
+
if lowered in {"str", "builtins.str"}:
|
|
44
|
+
return "''", "annotation:str"
|
|
45
|
+
if lowered in {"none", "nonetype"} or lowered.startswith(("optional[", "union[none,")) or ",none]" in lowered or "|none" in lowered:
|
|
46
|
+
return "None", "annotation:optional"
|
|
47
|
+
if lowered in {"int", "float", "complex", "decimal", "fraction", "number", "numbers.number"} or lowered.startswith(("int[", "float[")):
|
|
48
|
+
return "0", "annotation:scalar"
|
|
49
|
+
if lowered.startswith(("dict[", "mapping[")) or lowered in {"dict", "mapping"}:
|
|
50
|
+
return "{}", "annotation:mapping"
|
|
51
|
+
if lowered.startswith(("list[", "set[", "tuple[", "sequence[", "iterable[", "collection[", "frozenset[", "deque[")) or lowered in {"list", "set", "tuple", "sequence", "iterable", "collection"}:
|
|
52
|
+
# [] is intentionally type-compatible enough for abstract collection return types;
|
|
53
|
+
# the proof oracle still requires an assertion kill, never this heuristic alone.
|
|
54
|
+
return "[]", "annotation:collection"
|
|
55
|
+
return None
|
|
56
|
+
observed = {
|
|
57
|
+
"bool": "False",
|
|
58
|
+
"str": "''",
|
|
59
|
+
"collection": "[]",
|
|
60
|
+
"optional": "None",
|
|
61
|
+
"scalar": "0",
|
|
62
|
+
}.get(oracle_type or "")
|
|
63
|
+
return (observed, f"observed_oracle:{oracle_type}") if observed else None
|
|
64
|
+
|
|
65
|
+
|
|
27
66
|
def main() -> None:
|
|
28
67
|
parser = argparse.ArgumentParser()
|
|
29
68
|
parser.add_argument("--file", required=True)
|
|
30
69
|
parser.add_argument("--func", required=True)
|
|
31
70
|
parser.add_argument("--mode", choices=["sentinel", "equivalent"], default="sentinel")
|
|
71
|
+
parser.add_argument("--oracle-type", choices=["bool", "str", "collection", "optional", "scalar"])
|
|
32
72
|
args = parser.parse_args()
|
|
33
73
|
|
|
34
74
|
if args.mode == "equivalent":
|
|
35
|
-
print(json.dumps({"ok": True, "changed": False}))
|
|
75
|
+
print(json.dumps({"ok": True, "changed": False, "sentinel": None, "sentinel_source": "equivalent"}))
|
|
36
76
|
return
|
|
37
77
|
|
|
38
78
|
target = Path(args.file)
|
|
39
79
|
text = target.read_text(encoding="utf8")
|
|
40
80
|
lines = text.splitlines(keepends=True)
|
|
41
|
-
|
|
42
81
|
try:
|
|
43
82
|
tree = ast.parse(text, filename=str(target))
|
|
44
83
|
except SyntaxError as exc:
|
|
45
84
|
fail(f"syntax_error:{exc.lineno}")
|
|
46
85
|
|
|
47
|
-
candidates: list[ast.FunctionDef | ast.AsyncFunctionDef] = [
|
|
48
|
-
|
|
49
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == args.func
|
|
50
|
-
|
|
51
|
-
|
|
86
|
+
candidates: list[ast.FunctionDef | ast.AsyncFunctionDef] = [
|
|
87
|
+
node for node in ast.walk(tree)
|
|
88
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == args.func
|
|
89
|
+
]
|
|
52
90
|
if len(candidates) != 1:
|
|
53
91
|
fail("ambiguous_function" if candidates else "function_not_found")
|
|
54
|
-
|
|
55
92
|
fn = candidates[0]
|
|
56
93
|
if fn.end_lineno is None or not fn.body:
|
|
57
94
|
fail("unsupported_function_shape")
|
|
58
|
-
if any(isinstance(
|
|
95
|
+
if any(isinstance(node, (ast.Yield, ast.YieldFrom)) for node in ast.walk(fn)):
|
|
59
96
|
fail("unsupported_generator")
|
|
60
97
|
|
|
98
|
+
sentinel = sentinel_for(fn.returns, args.oracle_type)
|
|
99
|
+
if sentinel is None:
|
|
100
|
+
fail("return_annotation_or_observed_oracle_required")
|
|
101
|
+
expression, sentinel_source = sentinel
|
|
102
|
+
|
|
61
103
|
first_body = fn.body[0]
|
|
62
104
|
start = first_body.lineno
|
|
63
105
|
end = fn.end_lineno
|
|
64
106
|
if start < 1 or end < start or end > len(lines):
|
|
65
107
|
fail("unsupported_function_range")
|
|
66
108
|
|
|
109
|
+
replacement: str
|
|
67
110
|
if start == fn.lineno and end == fn.lineno:
|
|
68
111
|
header_line = lines[fn.lineno - 1]
|
|
69
112
|
match = re.match(r"^(\s*(?:async\s+)?def\s+[A-Za-z_][A-Za-z0-9_]*\([^)]*\)(?:\s*->\s*[^:]+)?):\s*.+$", header_line)
|
|
70
113
|
if not match:
|
|
71
114
|
fail("unsupported_inline_suite")
|
|
72
115
|
body_indent = leading_ws(header_line) + " "
|
|
73
|
-
lines[start - 1 : end] = [f"{match.group(1)}:\n", f"{body_indent}return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
lines[start - 1 : end] = [f"{match.group(1)}:\n", f"{body_indent}return {expression}\n"]
|
|
117
|
+
else:
|
|
118
|
+
indent = leading_ws(lines[start - 1])
|
|
119
|
+
header_indent = leading_ws(lines[fn.lineno - 1])
|
|
120
|
+
if not indent or len(indent.replace("\t", " ")) <= len(header_indent.replace("\t", " ")):
|
|
121
|
+
fail("unsupported_suite_indent")
|
|
122
|
+
replacement = f"{indent}return {expression}\n"
|
|
123
|
+
lines[start - 1 : end] = [replacement]
|
|
81
124
|
|
|
82
|
-
replacement = f"{indent}return 0\n"
|
|
83
|
-
lines[start - 1 : end] = [replacement]
|
|
84
125
|
target.write_text("".join(lines), encoding="utf8")
|
|
85
|
-
print(json.dumps({
|
|
126
|
+
print(json.dumps({
|
|
127
|
+
"ok": True,
|
|
128
|
+
"changed": True,
|
|
129
|
+
"start_line": start,
|
|
130
|
+
"end_line": end,
|
|
131
|
+
"sentinel": expression,
|
|
132
|
+
"sentinel_source": sentinel_source,
|
|
133
|
+
}))
|
|
86
134
|
|
|
87
135
|
|
|
88
136
|
if __name__ == "__main__":
|