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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user