5bd148a607
Three-cohort harness against IONOS-hosted Qwen3-Coder-Next, testing
whether application-head tag (`app` vs `apply` vs `call`) affects
LLM authoring success. Cohorts share lambda/constructor/typecon tag
renamings (the non-discriminator changes); only the head differs.
Result: 100 % cohort-treue — Qwen writes whatever the spec teaches,
no measurable natural preference. Pipeline pass-rate (classic 1/8,
apply 0/8, call 1/8) is cohort-independent. The four task failures
are general Form-A friction, not naming-related. Refactor not
justified per the feature-acceptance gate.
Tracked harness:
- 2026-05-12-cross-model-authoring/rename-spec.py — regex tag
rewriter for the two non-classic spec variants
- 2026-05-21-naming-ab/run.py, reprocess.py — three-cohort runner
+ post-processing with disjoint-discriminator counting and
module-name-matched temp dirs
Generated artefacts (runs/, rendered/ail-{renamed,call}.md) are
gitignored — regeneratable from rename-spec.py + run.py.
Side-effects filed against current spec/schema bugs surfaced during
the run:
- refs #28 spec teaches (ctor X) for term position; parser requires
(term-ctor X) — 100 % t4 failure across cohorts
- refs #29 io/print_str appends newline (de facto println);
spec now documents the behavior, code question still open
- refs #30 schema camelCase outlier: paramTypes/retType in Term::Lam
Spec patch in rendered/ail.md:
- replaces stale io/print_int references with io/print_str (bitrot
from the print-builtin consolidation)
- documents io/print_str's trailing-newline behavior
- corrects the prelude claim about int_to_str (IS in prelude)
141 lines
5.6 KiB
Python
141 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Re-process the raw responses from a finished run with fixes:
|
|
- Per-call temp dir so file stem matches module name
|
|
- Tag-disjoint counting (only count tags unique to a cohort,
|
|
ignore overlapping ones like `lambda` shared by apply+call)
|
|
"""
|
|
from __future__ import annotations
|
|
import json, re, subprocess, sys
|
|
from pathlib import Path
|
|
|
|
RUN_DIR = Path(sys.argv[1])
|
|
RESULTS = json.loads((RUN_DIR / "results.json").read_text())
|
|
|
|
REVERSE_APPLY = [("tail-apply","tail-app"),("term-constructor","term-ctor"),
|
|
("pat-constructor","pat-ctor"),("constructor","ctor"),
|
|
("apply","app"),("lambda","lam"),("typecon","con")]
|
|
REVERSE_CALL = [("tail-call","tail-app"),("term-constructor","term-ctor"),
|
|
("pat-constructor","pat-ctor"),("constructor","ctor"),
|
|
("call","app"),("lambda","lam"),("typecon","con")]
|
|
|
|
def reverse_sub(src, pairs):
|
|
out = src
|
|
for n,o in pairs:
|
|
out = re.sub(rf"\({re.escape(n)}(?=[\s)])", f"({o}", out)
|
|
return out
|
|
|
|
# Disjoint discriminator tags only — the head of function application
|
|
# (which IS the test variable). Ignore lambda/constructor/typecon since
|
|
# apply and call share those.
|
|
DISCRIM = {
|
|
"classic": ["app","tail-app"],
|
|
"apply": ["apply","tail-apply"],
|
|
"call": ["call","tail-call"],
|
|
}
|
|
|
|
def count_tags(src, tags):
|
|
return {t: len(re.findall(rf"\({re.escape(t)}(?=[\s)])", src)) for t in tags}
|
|
|
|
def extract_module_name(code):
|
|
m = re.search(r"\(module\s+([A-Za-z_][A-Za-z0-9_]*)", code)
|
|
return m.group(1) if m else None
|
|
|
|
def run_pipeline_fixed(code, module_name, expected_stdout, base):
|
|
work = base / module_name
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
src = work / f"{module_name}.ail"
|
|
src.write_text(code)
|
|
out = {"check_ok": False, "check_stderr": "", "build_ok": False,
|
|
"run_ok": False, "run_stdout": "", "matches_expected": False}
|
|
c = subprocess.run(["ail","check",str(src)], capture_output=True, text=True, timeout=30)
|
|
out["check_stderr"] = (c.stderr or c.stdout)[:2000]
|
|
if c.returncode != 0:
|
|
return out
|
|
out["check_ok"] = True
|
|
b = subprocess.run(["ail","build",str(src),"-o",str(work/"bin")],
|
|
capture_output=True, text=True, timeout=60)
|
|
if b.returncode != 0:
|
|
out["check_stderr"] = b.stderr[:2000]
|
|
return out
|
|
out["build_ok"] = True
|
|
r = subprocess.run([str(work/"bin")], capture_output=True, text=True, timeout=10)
|
|
if r.returncode == 0:
|
|
out["run_ok"] = True
|
|
out["run_stdout"] = r.stdout
|
|
out["matches_expected"] = (r.stdout == expected_stdout)
|
|
return out
|
|
|
|
# need expected stdout per task
|
|
TASKS = {}
|
|
CMA_TASKS = Path("/home/brummel/dev/ailang/experiments/2026-05-12-cross-model-authoring/master/tasks")
|
|
for f in CMA_TASKS.glob("*.task.json"):
|
|
t = json.loads(f.read_text())
|
|
TASKS[t["id"]] = t["expected_stdout"]
|
|
|
|
base = RUN_DIR / "work2"
|
|
base.mkdir(exist_ok=True)
|
|
|
|
new_rows = []
|
|
for r in RESULTS:
|
|
code = r["extracted_code"]
|
|
cohort = r["cohort"]
|
|
pairs = {"classic":None,"apply":REVERSE_APPLY,"call":REVERSE_CALL}[cohort]
|
|
pipeline_input = reverse_sub(code, pairs) if pairs else code
|
|
mod_name = extract_module_name(pipeline_input) or r["task_id"]
|
|
pipe = run_pipeline_fixed(pipeline_input, mod_name, TASKS[r["task_id"]], base)
|
|
|
|
own_disc = count_tags(code, DISCRIM[cohort])
|
|
foreign_disc = {}
|
|
for c, tags in DISCRIM.items():
|
|
if c == cohort: continue
|
|
foreign_disc[c] = count_tags(code, tags)
|
|
|
|
new_rows.append({
|
|
"cohort": cohort, "task_id": r["task_id"], "trial": r["trial"],
|
|
"module_name_in_output": mod_name,
|
|
"module_name_expected": r["task_id"],
|
|
"module_match": (mod_name == r["task_id"]),
|
|
"own_disc": own_disc,
|
|
"foreign_disc": foreign_disc,
|
|
"check_ok": pipe["check_ok"],
|
|
"build_ok": pipe["build_ok"],
|
|
"run_ok": pipe["run_ok"],
|
|
"matches_expected": pipe["matches_expected"],
|
|
"run_stdout": pipe["run_stdout"],
|
|
"first_err_line": pipe["check_stderr"].split("\n")[0][:140],
|
|
})
|
|
|
|
(RUN_DIR / "results2.json").write_text(json.dumps(new_rows, indent=2))
|
|
|
|
print("\n=== final pipeline pass per cohort ===")
|
|
by_cohort = {}
|
|
for r in new_rows:
|
|
c = by_cohort.setdefault(r["cohort"], {"n":0,"mod":0,"check":0,"build":0,"run":0,"correct":0})
|
|
c["n"] += 1
|
|
if r["module_match"]: c["mod"] += 1
|
|
if r["check_ok"]: c["check"] += 1
|
|
if r["build_ok"]: c["build"] += 1
|
|
if r["run_ok"]: c["run"] += 1
|
|
if r["matches_expected"]: c["correct"] += 1
|
|
for cohort, c in by_cohort.items():
|
|
print(f"{cohort:8s} n={c['n']} mod-name-OK={c['mod']} check={c['check']} build={c['build']} run={c['run']} stdout-correct={c['correct']}")
|
|
|
|
print("\n=== discriminator tag totals (head of function application only) ===")
|
|
disc_totals = {}
|
|
for r in new_rows:
|
|
c = r["cohort"]
|
|
own_sum = sum(r["own_disc"].values())
|
|
foreign_sum = sum(sum(v.values()) for v in r["foreign_disc"].values())
|
|
t = disc_totals.setdefault(c, {"own":0,"foreign":0,"by_foreign":{}})
|
|
t["own"] += own_sum
|
|
for fc, fv in r["foreign_disc"].items():
|
|
t["by_foreign"][fc] = t["by_foreign"].get(fc,0) + sum(fv.values())
|
|
t["foreign"] += foreign_sum
|
|
for cohort, t in disc_totals.items():
|
|
print(f"{cohort:8s} own={t['own']:3d} foreign={t['foreign']:3d} breakdown={t['by_foreign']}")
|
|
|
|
print("\n=== per-row failure mode ===")
|
|
for r in new_rows:
|
|
flag = "OK" if r["matches_expected"] else ("CORRECT-NOT" if r["run_ok"] else ("RUN" if r["build_ok"] else ("BUILD" if r["check_ok"] else "CHECK")))
|
|
print(f"[{r['cohort']:8s}] {r['task_id']:18s} t{r['trial']} {flag:12s} {r['first_err_line']}")
|