Mutation check done on all accepted tasks; mutant fixes; dump filter in proxy

- skip sy-subrc lines and CDS type lengths; CDS literal mutants; mutate helper classes
- proxy: sap_short_dumps shows only dumps of the current run
- 26/26 tasks pass; results in docs/faz1-tasarim.md 11g

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aUaQeLnwbb1zTpN7kHeat
This commit is contained in:
Kral
2026-10-03 06:02:59 +02:00
parent 0ba25faeea
commit f907351c0d
108 changed files with 7384 additions and 8 deletions

View File

@@ -115,10 +115,21 @@ def mutants(src, otype, seed, n=MAX_MUTANTS):
continue
if kind == "arith" and re.search(r"(\(|\bSELECT)\s*$", src[line_start:x.start()], re.I):
continue # COUNT( * ), SELECT *
if _skip_line(line) or (kind == "const" and re.search(r"\bLENGTH\b|\bDECIMALS\b|\(\s*\d+\s*,", line, re.I)):
if re.search(r"\bsy-subrc\b|\bsy-tabix\b", line, re.I):
continue # mostly gives a short dump (unassigned field symbol), not a behavior change
if _skip_line(line) or (kind == "const" and (re.search(r"\bLENGTH\b|\bDECIMALS\b|\(\s*\d+\s*,", line, re.I)
or re.search(r"abap\.\w+\(\s*[\d\s,]*$", src[line_start:x.start()], re.I))):
continue
new = str(int(x.group(1)) + 1) if rep is None else rep
sites.append((kind, x.start(), x.end(), new, src.count("\n", 0, x.start()) + 1, x.group(0)))
if cds: # literal values in the select list (G0025: source 'A' / 'B' was the only logic)
for x in re.finditer(r"'([^'\n]{1,20})'", src):
line_start = src.rfind("\n", 0, x.start()) + 1
if not lo <= x.start() < hi or src[line_start:x.start()].lstrip().startswith(("@", "//")):
continue
v = x.group(1)
new = "'" + ("Z" if v[0] != "Z" else "Y") + v[1:] + "'"
sites.append(("lit", x.start(), x.end(), new, src.count("\n", 0, x.start()) + 1, x.group(0)))
rnd = random.Random(seed)
rnd.shuffle(sites)
chosen, kinds, lines = [], set(), set()
@@ -141,9 +152,12 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
"""Run the mutants of one task. Returns a summary dict; writes it to <task>/mutation.json."""
task_dir = os.path.join(pool_root, task_id)
meta = json.load(open(os.path.join(task_dir, "task.json")))
contract = {c["name"].upper() for c in meta["contract"]}
targets = [o for o in meta["reference"] if o["name"].upper() in contract and o.get("file")
and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS")]
# every reference object except test classes: the logic can sit in a helper (G0026: FM calls a class)
def is_test(o):
return o["type"] == "CLAS" and re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING",
open(os.path.join(task_dir, o["file"])).read(), re.I | re.M)
targets = [o for o in meta["reference"] if o.get("file") and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS")
and not is_test(o)]
work = os.path.join(ROOT, "runs", "gen", "mut")
os.makedirs(work, exist_ok=True)
runner = Runner(os.path.join(work, "pool"), os.path.join(work, "runs"))

View File

@@ -41,6 +41,7 @@ class ToolProxy:
def __init__(self, mcp, prefix, budget, log_path, tool_schema=None):
self.mcp = mcp
self.variant = VARIANTS.get(tool_schema) if tool_schema else None
self.started = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) # dump times are UTC
self.prefix = prefix.upper()
self.max_calls = budget.get("max_tool_calls", 60)
self.max_activations = budget.get("max_activations", 15)
@@ -82,6 +83,16 @@ class ToolProxy:
return bool(RUN_PREFIX.match(n)) and not n.startswith(self.prefix)
def _filter(self, tool, text):
if tool == "sap_short_dumps": # only dumps of this run: other runs (and mutants) also write dumps
try:
data = json.loads(text)
except ValueError:
return text
if isinstance(data, dict) and isinstance(data.get("dumps"), list):
data["dumps"] = [d for d in data["dumps"] if str(d.get("updated", "")) >= self.started]
data["count"] = len(data["dumps"])
return json.dumps(data)
return text
if tool not in ("sap_search_object", "sap_usage_references"):
return text
try: