arborist/bench/es_delta.py
russell@unturf.com 2c98fc964e
feat: cross-language Q&A (Operation Sandwich) + Windows quickstart — all default-OFF
Three workstreams, full suite 2482 passed, experimental paths default-OFF.

#000055 — Windows quickstart without make
  tasks.py (pure-stdlib runner) + make.bat shim + .gitattributes;
  README Windows section rewritten. Quickstart needs only Python
  3.10+ (no make/bzip2/curl/bash). Mirrors the Makefile quickstart
  subset; drift-pinned by tests/test_tasks_runner.py.

#000001 §7 Phase 0 — deterministic cross-language guard
  arborist/qa/crosslang.py: non-English signal (¿/¡/non-ASCII) + an
  es function-word stoppack. Fail-closed to UNGROUNDED before
  retrieval/LLM (mirrors the quantifier reject-DAG) when no content
  token survives, else strips es stopwords from the retrieval query
  only. English path byte-identical by construction. Default OFF
  (crosslang_guard_enabled). Measured: the anarcocapitalismo field
  case 10.4s -> 1.6s.

#000056 — Operation Sandwich (cross-language grounding)
  arborist/qa/mt/: opus-mt es/fr/ru<->en, lazy per-pair memoised
  singleton (fixes the 88%-engine-error concurrency defect),
  manifest-pinned, [mt] extra; entity_mask wrapper. Sandwich =
  translate query in (retrieval + LLM prompt) -> English answer ->
  UNTOUCHED verifier grounds English-vs-English -> translate the
  verified answer out as display-only (banner-labelled, zero
  grounding). question_hash + verifier_policy_hash invariant; MT
  engine identity binds into RetrievalPlan, not governance. CLI
  --crosslang-translate / make XLANG_MT=1. Default OFF; entity_mask
  default OFF (measured net-negative at bench scale). Fan-out bench
  (bench/*.py): Spanish ~0% -> 71% grounded vs the real no-support
  baseline; the round-trip predictor was tried and refuted; the
  entity-mask lever failed at scale (corpus-title anchoring untried).

CLAUDE.md: cross-language bright-line convention + module map.
Pre-existing modified diagram files are intentionally excluded.
2026-05-18 12:12:23 -04:00

86 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""The real metric: per-question English-baseline → Spanish+sandwich
transition. Absolute es grounding % is uninterpretable on an
adversarial bench; what the sandwich *costs* is the transition.
usage: es_delta.py <en_baseline.jsonl> <es_sandwich.jsonl>
Joins on the en↔es map. Buckets each question:
PRESERVED en grounded (S/H) & es grounded
DOWNGRADE en STRICT & es HYBRID (partial cost)
LOST en grounded & es UNGROUNDED (the real cost)
N/A en UNGROUNDED (sandwich not at fault)
GAINED en UNGROUNDED & es grounded (noise/curio)
Then cross-tabs LOST vs the round-trip bucket — does drift predict
the *loss* transition (even though it didn't predict absolute %)?
"""
from __future__ import annotations
import json
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RES = ROOT / "bench" / "qa_results"
GROUNDED = {"STRICT", "HYBRID"}
def load(p):
return [json.loads(x) for x in Path(p).read_text().splitlines() if x.strip()]
def main() -> int:
en_rows = load(sys.argv[1])
es_rows = load(sys.argv[2])
pairs = json.loads((ROOT / "bench" / "qa_questions_es_map.json").read_text())
rt = {r["es"]: r["bucket"] for r in
json.loads((RES / "es_roundtrip.json").read_text())}
en_am = {r["question"]: r.get("audit_mode") for r in en_rows}
es_am = {r["question"]: r.get("audit_mode") for r in es_rows}
cls = Counter()
lost_by_bucket = Counter()
lost_list, downgrade_list = [], []
for p in pairs:
en, es = p["en"], p["es"]
a, b = en_am.get(en), es_am.get(es)
if a is None or b is None:
cls["MISSING"] += 1
continue
if a not in GROUNDED:
cls["N/A (en ungrounded)"] += 1
if b in GROUNDED:
cls[" └ of which GAINED"] += 1
continue
if b not in GROUNDED:
cls["LOST"] += 1
lost_by_bucket[rt.get(es, "?")] += 1
lost_list.append((en, es, a, b))
elif a == "STRICT" and b == "HYBRID":
cls["DOWNGRADE (S→H)"] += 1
downgrade_list.append((en, es))
else:
cls["PRESERVED"] += 1
n = len(pairs)
en_g = sum(1 for p in pairs if en_am.get(p["en"]) in GROUNDED)
es_g = sum(1 for p in pairs if es_am.get(p["es"]) in GROUNDED)
print(f"n={n} EN-baseline grounded={en_g} ({en_g/n:.0%}) "
f"ES+sandwich grounded={es_g} ({es_g/n:.0%}) "
f"net Δ={es_g-en_g:+d}")
print("\ntransition (only en-grounded questions can be 'LOST'):")
for k, v in cls.most_common():
print(f" {k:<24} {v}")
print("\nLOST × round-trip bucket (does drift predict the LOSS?):")
for bk in ("CLEAN", "DRIFT", "COLLAPSE", "?"):
if lost_by_bucket.get(bk):
print(f" {bk:<9} {lost_by_bucket[bk]}")
print("\n-- LOST questions (en grounded, es+sandwich UNGROUNDED) --")
for en, es, a, b in lost_list:
print(f" [{a}{b}] en={en}\n es={es} rt={rt.get(es)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())