Generator: pilot analysis and fixes; G0002-G0021 tasks; handover notes merged

- Static bundle checks (name length, seed type, reserved words, contract test classes,
  testclasses_file), local abaplint parser check before SAP, max_tokens, robust JSON parse
- Runner: G2 for CDS views with parameters, contract_check detail in report
- G0020 and G0013 fixed by review and revalidated (oracle 100, null 0)
- Pilot results in docs/faz1-tasarim.md 11e; handover notes merged into CLAUDE.md and docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kral
2026-10-03 05:04:23 +02:00
parent 15eb9f1bb2
commit 5de5e2851c
162 changed files with 9943 additions and 23 deletions

View File

@@ -6,6 +6,9 @@ import argparse
import json
import os
import re
import shutil
import subprocess
import tempfile
import time
import urllib.request
@@ -15,6 +18,9 @@ from .ledger import add_usage, check_budget
from .runner import Runner
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ABAPLINT = os.path.join(ROOT, "node_modules", ".bin", "abaplint")
MAX_OUT_TOKENS = 100000
ABAPLINT_VERSIONS = {"v702", "v740sp05", "v750", "v758"}
EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15"}
CATEGORIES = {
"A": "pure logic in a new class (language, OO design, Clean ABAP)",
@@ -37,6 +43,18 @@ Rules for the bundle:
The harness replaces it with a run prefix of 9 characters (for example Z005P001_).
Name length after replacement: classes, programs, function modules, CDS entities max 30;
database tables max 16; function groups max 26.
- Every other ABAP name (methods, test methods, local classes, data, types, constants) has max 30
characters. Keep test method names short, for example "rejects_zero_qty".
- 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.
- 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.
Every reference class (except exception classes) has a "testclasses_file".
- A database table seed has type "TABL" and DDL source "define table ...". Type "DDLS" is only for CDS views.
- All objects are in package $TMP. Do not use transports.
- Do not use SAP application module data (no FI, SD, MM tables). Use generic business domains and
only objects that exist in every ABAP Platform system (language, ABAP SQL, CDS, CL_ABAP_*, CL_SALV_TABLE,
@@ -84,7 +102,8 @@ def bundle_of(task_dir):
def chat(model, messages, base_url):
body = {"model": model, "messages": messages, "temperature": 0.7}
# max_tokens: one pilot call ran to 393k output tokens (reasoning) and returned no content
body = {"model": model, "messages": messages, "temperature": 0.7, "max_tokens": MAX_OUT_TOKENS}
req = urllib.request.Request(f"{base_url}/chat/completions", json.dumps(body).encode(),
{"Content-Type": "application/json", "Authorization": "Bearer none"})
for attempt in range(4):
@@ -92,7 +111,13 @@ def chat(model, messages, base_url):
with urllib.request.urlopen(req, timeout=1800) as r:
data = json.loads(r.read().decode())
add_usage(model, data.get("usage", {}), kind="generate")
return data["choices"][0]["message"].get("content") or ""
content = data["choices"][0]["message"].get("content") or ""
if content.strip():
return content
# empty content (output limit reached in reasoning): retry once more with the next attempt
if attempt == 3:
return ""
continue
except Exception as e: # noqa: BLE001
if getattr(e, "code", 500) < 500 and getattr(e, "code", 500) != 429:
raise
@@ -103,8 +128,109 @@ def chat(model, messages, base_url):
def parse_bundle(text):
text = text.strip()
text = re.sub(r"^```(json)?\s*|\s*```$", "", text)
start, end = text.find("{"), text.rfind("}")
return json.loads(text[start:end + 1])
# raw_decode stops after the first complete object; text after it (a note, a second object) is ignored
return json.JSONDecoder().raw_decode(text[text.find("{"):])[0]
NAME_RE = re.compile(r"^\s*(?:CLASS-)?(?:METHODS|METHOD|DATA|TYPES|CONSTANTS|FORM|CLASS|INTERFACE)\b:?\s+(\w+)",
re.I | re.M)
# reserved words seen in activation errors, plus common SQL keywords
RESERVED = {"HOURS", "MODE", "ORDER", "GROUP", "DATE", "TIME", "VALUE", "USER", "KEY", "COUNT", "SUM", "MIN",
"MAX", "AVG", "SELECT", "FROM", "WHERE", "TABLE", "VIEW", "UNION", "JOIN", "LEVEL", "SIZE", "TYPE",
"INDEX", "CASE", "WHEN", "THEN", "ELSE", "END", "AS", "ON", "BY", "DESC", "ASC", "DAYS", "MINUTES",
"SECONDS", "YEAR", "MONTH", "DAY", "LIMIT", "OFFSET", "PARAMETERS"}
GLOBAL_TEST_CLASS = re.compile(r"\A\s*(?:\*[^\n]*\n|\"[^\n]*\n|\s)*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING",
re.I)
DDL_KIND = re.compile(r"^\s*define\s+(?:root\s+)?(table|view|structure)\b", re.I | re.M)
def lint_files(b):
"""Static checks that need no SAP system: identifier length, seed type against DDL source."""
errs = []
for rel, src in b.get("files", {}).items():
if not rel.endswith(".abap"):
continue
for name in set(NAME_RE.findall(src.replace("{{P}}", "Z0000000_").replace("{{p}}", "z0000000_"))):
if len(name) > 30:
errs.append(f"{rel}: name {name} has {len(name)} characters (ABAP max 30)")
t = b.get("task", {})
oos = {n.upper() for n in t.get("out_of_scope", [])}
test_names = {o.get("name", "").upper() for o in t.get("hidden_tests", [])}
for o in t.get("reference", []):
name = o.get("name", "")
if name.upper() in oos:
errs.append(f"reference object {name} is also in out_of_scope; the solution must change it")
if o.get("type") == "CLAS" and GLOBAL_TEST_CLASS.search(b.get("files", {}).get(o.get("file", ""), "")):
test_names.add(name.upper())
for c in t.get("contract", []):
if c.get("name", "").upper() in test_names:
errs.append(f"contract contains the test class {c.get('name')}; only objects that the hidden "
"tests call belong in the contract")
for o in t.get("reference", []):
if (o.get("type") == "CLAS" and o.get("name", "").upper() not in test_names
and not o.get("testclasses_file")
and not re.search(r"INHERITING\s+FROM\s+\S*CX_", b.get("files", {}).get(o.get("file", ""), ""),
re.I)):
errs.append(f"reference class {o.get('name')} has no testclasses_file (local unit tests)")
for rel, src in b.get("files", {}).items():
if DDL_KIND.search(src):
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 k in ("seed", "reference"):
for o in t.get(k, []):
m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), ""))
if not m:
continue
want = {"table": "TABL", "structure": "TABL", "view": "DDLS"}[m.group(1).lower()]
if o.get("type") != want:
errs.append(f"{k}: {o.get('name')} has type {o.get('type')}, but its source is "
f"'define {m.group(1)}'; use type {want}")
return errs
def abaplint_files(b):
"""Parser errors of ABAP sources (classes, interfaces, programs) found by local abaplint.
SAP often reports only "save operation failed" for these; abaplint gives the line.
Only parser_error: check_syntax gives false errors for standard objects abaplint does not know."""
t, files = b.get("task", {}), b.get("files", {})
ext = {"CLAS": "clas", "INTF": "intf", "PROG": "prog"}
version = t.get("release_target") if t.get("release_target") in ABAPLINT_VERSIONS else "v758"
d = tempfile.mkdtemp(prefix="genlint_")
origin = {}
try:
os.makedirs(os.path.join(d, "src"))
for k in ("seed", "reference", "hidden_tests"):
for o in t.get(k, []):
if o.get("type") not in ext:
continue
name = re.sub(r"\W", "_", o.get("name", "").replace("{{P}}", "Z0000000_")).lower()
for fk, suffix in (("file", ""), ("testclasses_file", ".testclasses")):
if o.get(fk) in files:
fn = f"{name}.{ext[o['type']]}{suffix}.abap"
origin[fn] = o[fk]
src = files[o[fk]].replace("{{P}}", "Z0000000_").replace("{{p}}", "z0000000_")
open(os.path.join(d, "src", fn), "w").write(src)
if not origin:
return []
cfg = {"global": {"files": "/src/**/*.*"}, "dependencies": [],
"syntax": {"version": version, "errorNamespace": "^(Z|Y)"}, "rules": {"parser_error": True}}
json.dump(cfg, open(os.path.join(d, "abaplint.json"), "w"))
p = subprocess.run([ABAPLINT, "abaplint.json", "-f", "json"], cwd=d, capture_output=True, text=True,
timeout=300)
issues = json.loads(p.stdout or "[]")
except Exception: # noqa: BLE001 the pre-check is optional; SAP validation follows
return []
finally:
shutil.rmtree(d, ignore_errors=True)
out = []
for i in issues:
f = i.get("file", "")
fn = os.path.basename(f.get("filename", "") if isinstance(f, dict) else str(f))
if i.get("key") == "parser_error" and fn in origin:
out.append(f"{origin[fn]} line {i.get('start', {}).get('row')}: syntax error for release {version} "
f"(abaplint): {i.get('description')}")
return out[:20]
def check_bundle(b):
@@ -165,7 +291,8 @@ def write_errors(run_dir):
def failure_summary(rep):
out = {"reference_write_errors": write_errors(rep.get("_dir")),"gates": rep.get("gates"), "score": rep.get("score"), "setup": rep.get("setup"),
out = {"reference_write_errors": write_errors(rep.get("_dir")), "gates": rep.get("gates"),
"contract_check": rep.get("contract_check"), "score": rep.get("score"), "setup": rep.get("setup"),
"atc": rep.get("atc"), "abaplint": rep.get("abaplint"), "own_tests": rep.get("own_tests"),
"hidden_failed": [d for d in rep.get("hidden_tests", {}).get("detail", []) if not d["ok"]],
"hidden_install": rep.get("hidden_tests", {}).get("install")}
@@ -189,11 +316,14 @@ def generate(task_id, pool, object_type, category, difficulty, model, base_url,
messages.append({"role": "assistant", "content": text})
try:
b = parse_bundle(text)
errs = check_bundle(b)
found = {"structure": check_bundle(b), "static": lint_files(b), "abaplint": abaplint_files(b)}
except Exception as e: # noqa: BLE001
b, errs = None, [f"invalid JSON: {e}"]
b, found = None, {"json": [f"invalid JSON: {e}"]}
errs = [e for v in found.values() for e in v]
if errs:
log["attempts"].append({"stage": "bundle", "errors": errs})
# one entry per source, so a batch shows what the checks found before SAP
log["attempts"].append({"stage": "bundle", "errors": errs,
"by_check": {k: len(v) for k, v in found.items() if v}})
messages.append({"role": "user", "content": "Fix these problems and return the full bundle again:\n"
+ "\n".join(errs)})
continue