Hard batch G0022-G0026: 5/5 accepted; budget floor, CDS checks, mutation check

- generator: budget floor (2x oracle activations, 3x calls), static checks for CDS
  $parameters and UNION annotation
- harness/mutation.py: deterministic mutants of the reference; hidden tests must fail
- G0022 revalidated with harness fixes: oracle 100, null 0
- results in docs/faz1-tasarim.md 11f

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kral
2026-10-03 05:16:30 +02:00
parent 3570d7ef9b
commit 32f03fcad4
53 changed files with 2931 additions and 14 deletions

View File

@@ -49,8 +49,10 @@ Rules for the bundle:
- Method parameters need a complete type: "TYPE c LENGTH 4" is not allowed in a signature.
Declare a type first (TYPES ty_zone TYPE c LENGTH 4) and use it.
- Field names in tables and CDS views are not SQL reserved words (no HOURS, MODE, ORDER, DATE, COUNT ...).
- CDS: use "define view entity". Parameters have no default value. In a UNION, all branches have the same
key elements and element names. Hidden tests for a view with parameters pass all parameters.
- CDS: use "define view entity". Parameters have no default value. Use a parameter as
$parameters.p_name (not :p_name). In a UNION, all branches have the same
key elements and element names, and the view
has the annotation @Metadata.ignorePropagatedAnnotations: true. Hidden tests for a view with parameters pass all parameters.
- Legacy seed code (categories E, I) must activate on SAP_BASIS 816: do not mix old and new syntax in one
SQL statement (with new syntax, every host variable needs "@").
- The contract lists only the objects that the hidden tests call. Test classes are never in the contract.
@@ -178,6 +180,14 @@ def lint_files(b):
for f in re.findall(r"^\s*(?:key\s+)?(\w+)\s*:", src, re.I | re.M):
if f.upper() in RESERVED:
errs.append(f"{rel}: field name {f} is a reserved word; choose another name")
for rel, src in b.get("files", {}).items():
if rel.endswith(".asddls") and re.search(r"define\s+(root\s+)?view\s+entity", src, re.I):
if re.search(r"\bunion\b", src, re.I) and not re.search(r"@Metadata\.ignorePropagatedAnnotations\s*:\s*true",
src, re.I):
errs.append(f"{rel}: a view entity with UNION needs @Metadata.ignorePropagatedAnnotations: true")
for p in sorted(set(re.findall(r"[=<>(,]\s*:(\w+)|\bbetween\s+:(\w+)|\band\s+:(\w+)", src, re.I))):
name = next(x for x in p if x)
errs.append(f"{rel}: parameter :{name}; a view entity needs $parameters.{name}")
for k in ("seed", "reference"):
for o in t.get(k, []):
m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), ""))
@@ -279,11 +289,23 @@ def dependency_order(objs, files):
return out
def floor_budget(task):
"""The model needs more calls than the oracle (reads, repairs). G0022: budget 12 activations,
reference alone 13. Minimum: 2 x oracle activations, 3 x oracle tool calls."""
ref = task.get("reference", [])
pushes = sum(bool(o.get("file")) + bool(o.get("testclasses_file")) for o in ref)
calls = len(ref) + pushes
bud = task.setdefault("budget", {})
bud["max_activations"] = max(int(bud.get("max_activations", 0)), 2 * pushes)
bud["max_tool_calls"] = max(int(bud.get("max_tool_calls", 0)), 3 * calls)
def write_bundle(b, task_dir, task_id):
os.makedirs(task_dir, exist_ok=True)
b["task"]["id"] = task_id
for k in ("seed", "reference"):
b["task"][k] = dependency_order(b["task"].get(k, []), b["files"])
floor_budget(b["task"])
json.dump(b["task"], open(os.path.join(task_dir, "task.json"), "w"), indent=2)
for rel, content in b["files"].items():
path = os.path.join(task_dir, rel)

188
harness/mutation.py Normal file
View File

@@ -0,0 +1,188 @@
"""Mutation check (step D): the hidden tests must fail on a broken reference.
Small deterministic changes (mutants) go into the main source of the contract objects of the reference.
For each mutant the oracle writes the mutated reference and the harness runs the hidden tests.
killed = reference active and at least one hidden test fails
survived = reference active and all hidden tests pass (a test gap or an equivalent mutant)
invalid = the mutant does not activate (not counted)
python3 -m harness.mutation G0002 G0003 --pool eval --run-base 5000
"""
import argparse
import json
import os
import random
import re
import shutil
from .adt_client import load_env
from .agents import OracleAgent
from .runner import Runner
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAX_MUTANTS = 5
MIN_VALID = 3
MIN_KILL_RATE = 0.75
# (kind, pattern, replacement); patterns match only outside comments and literals
ABAP_OPS = [
("rel", r"(?<=\s)>=(?=\s)", "<"), ("rel", r"(?<=\s)<=(?=\s)", ">"),
("rel", r"(?<=\s)>(?=\s)", ">="), ("rel", r"(?<=\s)<(?=\s)", "<="),
("rel", r"(?<=\s)<>(?=\s)", "="),
("rel", r"\bGE\b", "LT"), ("rel", r"\bLE\b", "GT"), ("rel", r"\bGT\b", "GE"), ("rel", r"\bLT\b", "LE"),
("rel", r"\bNE\b", "EQ"),
("logic", r"(?<=\s)AND(?=\s)", "OR"), ("logic", r"(?<=\s)OR(?=\s)", "AND"),
("arith", r"(?<=\s)\+(?=\s)", "-"), ("arith", r"(?<=\s)-(?=\s)", "+"), ("arith", r"(?<=\s)\*(?=\s)", "/"),
("bool", r"\babap_true\b", "abap_false"), ("bool", r"\babap_false\b", "abap_true"),
("const", r"(?<![\w.'-])([1-9]\d{0,5})(?![\w.'])", None), # integer literal + 1
]
CDS_OPS = [
("rel", r"(?<=\s)>=(?=\s)", "<"), ("rel", r"(?<=\s)<=(?=\s)", ">"),
("rel", r"(?<=\s)>(?=\s)", ">="), ("rel", r"(?<=\s)<(?=\s)", "<="), ("rel", r"(?<=\s)<>(?=\s)", "="),
("logic", r"(?<=\s)and(?=\s)", "or"),
("arith", r"(?<=\s)\+(?=\s)", "-"), ("arith", r"(?<=\s)-(?=\s)", "+"),
("agg", r"\bsum\s*\(", "max("), ("agg", r"\bcount\s*\(", "max("), ("agg", r"\bavg\s*\(", "max("),
("join", r"\binner\s+join\b", "left outer join"),
("const", r"(?<![\w.'-])([1-9]\d{0,5})(?![\w.'])", None),
]
def _mask(src, cds):
"""True for each character inside a comment or a literal."""
m = [False] * len(src)
pats = ([r"//[^\n]*", r"/\*.*?\*/", r"'[^']*'", r"@[^\n]*"] if cds else
[r'"[^\n]*', r"^\*[^\n]*", r"'(?:[^']|'')*'", r"`[^`]*`", r"\|(?:[^|\\]|\\.)*\|"])
for p in pats:
for x in re.finditer(p, src, re.M | re.S):
for i in range(x.start(), x.end()):
m[i] = True
return m
def _region(src, otype):
"""Start and end of the part that may change: the implementation, not declarations or test classes."""
start, end = 0, len(src)
if otype == "CLAS":
x = re.search(r"^\s*CLASS\s+\S+\s+IMPLEMENTATION", src, re.I | re.M)
start = x.end() if x else len(src)
if otype == "PROG":
x = re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING", src, re.I | re.M)
end = x.start() if x else len(src)
if otype == "DDLS":
x = re.search(r"\bas\s+select\b|\bas\s+projection\b", src, re.I)
start = x.start() if x else 0
return start, end
def _skip_line(line):
"""Declarations and signatures: a change there gives syntax errors or no behavior change."""
return re.match(r"\s*(DATA|TYPES|CONSTANTS|METHODS|CLASS-METHODS|PARAMETERS|SELECT-OPTIONS|"
r"IMPORTING|EXPORTING|RETURNING|RAISING|TABLES|FIELD-SYMBOLS)\b", line, re.I)
def mutants(src, otype, seed, n=MAX_MUTANTS):
"""Up to n mutants as (description, source). Different kinds first, spread over the source."""
cds = otype == "DDLS"
mask = _mask(src, cds)
lo, hi = _region(src, otype)
sites = []
for kind, pat, rep in (CDS_OPS if cds else ABAP_OPS):
for x in re.finditer(pat, src, re.I):
if not lo <= x.start() < hi or mask[x.start()]:
continue
line_start = src.rfind("\n", 0, x.start()) + 1
line_end = src.find("\n", x.start())
line = src[line_start:line_end if line_end >= 0 else len(src)]
if _skip_line(line) or (kind == "const" and re.search(r"\bLENGTH\b|\bDECIMALS\b|\(\s*\d+\s*,", line, 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)))
rnd = random.Random(seed)
rnd.shuffle(sites)
chosen, kinds, lines = [], set(), set()
for prefer_new in (True, False):
for s in sites:
if len(chosen) >= n:
break
if s in chosen or s[4] in lines or (prefer_new and s[0] in kinds):
continue
chosen.append(s)
kinds.add(s[0])
lines.add(s[4])
out = []
for kind, a, b, new, line, old in sorted(chosen, key=lambda s: s[1]):
out.append((f"line {line}: {old.strip()} -> {new} ({kind})", src[:a] + new + src[b:]))
return out
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")]
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"))
# candidates per object, then round robin: objects without mutation sites (exception classes) give their share
cand = [[(o, d, m) for d, m in mutants(open(os.path.join(task_dir, o["file"])).read(), o["type"],
f"{task_id}:{o['name']}", n)] for o in targets]
plan = []
while len(plan) < n and any(cand):
for c in cand:
if c and len(plan) < n:
plan.append(c.pop(0))
results = []
for k, (o, desc, msrc) in enumerate(plan):
mdir = os.path.join(work, "pool", task_id)
shutil.rmtree(mdir, ignore_errors=True)
shutil.copytree(task_dir, mdir)
open(os.path.join(mdir, o["file"]), "w").write(msrc)
rep, _ = runner.run(task_id, OracleAgent(), run_base + k)
h = rep.get("hidden_tests") or {}
g = rep.get("gates") or {}
if not g.get("G1_active") or not h.get("total"):
status = "invalid"
elif h["passed"] < h["total"]:
status = "killed"
else:
status = "survived"
results.append({"object": o["name"], "mutant": desc, "status": status,
"hidden": f"{h.get('passed')}/{h.get('total')}",
"failed_tests": [d["method"] for d in h.get("detail", []) if not d["ok"]]})
if keep and status == "killed": # candidate faulty reference for own-test scoring
fdir = os.path.join(task_dir, "faulty")
os.makedirs(fdir, exist_ok=True)
open(os.path.join(fdir, f"m{k}_{os.path.basename(o['file'])}"), "w").write(msrc)
shutil.rmtree(os.path.join(work, "pool", task_id), ignore_errors=True)
valid = [r for r in results if r["status"] != "invalid"]
killed = [r for r in valid if r["status"] == "killed"]
summary = {"task": task_id, "mutants": results, "valid": len(valid), "killed": len(killed),
"kill_rate": round(len(killed) / len(valid), 2) if valid else None,
"ok": len(valid) >= MIN_VALID and len(killed) / max(1, len(valid)) >= MIN_KILL_RATE}
if len(valid) < MIN_VALID:
summary["note"] = f"fewer than {MIN_VALID} valid mutants; check by review"
json.dump(summary, open(os.path.join(task_dir, "mutation.json"), "w"), indent=1)
return summary
def main():
load_env(os.path.join(ROOT, ".env"))
ap = argparse.ArgumentParser()
ap.add_argument("tasks", nargs="+")
ap.add_argument("--pool", default="eval")
ap.add_argument("--run-base", type=int, required=True)
ap.add_argument("-n", type=int, default=MAX_MUTANTS)
ap.add_argument("--keep", action="store_true", help="store killed mutants in <task>/faulty/")
a = ap.parse_args()
root = os.path.join(ROOT, "tasks_gen", a.pool) if a.pool != "tasks" else os.path.join(ROOT, "tasks")
for i, t in enumerate(a.tasks):
s = check_task(root, t, a.run_base + 10 * i, a.n, a.keep)
print(json.dumps({k: v for k, v in s.items() if k != "mutants"}), flush=True)
for m in s["mutants"]:
print(" ", m["status"], m["object"], m["mutant"], m["hidden"], flush=True)
if __name__ == "__main__":
main()