@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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """Minimal Python mutation helper for the P-1 proof spike.
2
+ """Fail-closed Python mutation helper for dynamic proof.
3
3
 
4
- This intentionally supports only the first safe shape: exactly one function or
5
- method named --func in the target file, with a block suite that can be replaced
6
- by a simple return sentinel. Unsupported or ambiguous shapes fail closed.
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
- for node in ast.walk(tree):
49
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == args.func:
50
- candidates.append(node)
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(n, (ast.Yield, ast.YieldFrom)) for n in ast.walk(fn)):
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 0\n"]
74
- target.write_text("".join(lines), encoding="utf8")
75
- print(json.dumps({"ok": True, "changed": True, "start_line": start, "end_line": end}))
76
- return
77
-
78
- indent = leading_ws(lines[start - 1])
79
- if not indent or len(indent.replace("\t", " ")) <= len(leading_ws(lines[fn.lineno - 1]).replace("\t", " ")):
80
- fail("unsupported_suite_indent")
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({"ok": True, "changed": True, "start_line": start, "end_line": end}))
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__":