Spaces:
Sleeping
Add triage tool + scope paper around out-of-engine features
Browse filesscripts/triage.py: per-pack triage report combining
- stall-must-LOSE (the no-cheat audit, all 195 packs pass post-fix)
- intended-must-WIN (proxied by `tests/test_<pack>.py` existence
+ suite-green ⇒ the test's intended policy is currently winning)
- empirical model coverage from any `run_eval --out` reports
(any-model-wins / every-model-loses / discriminative)
- fog signal + channel spread parity per pack when perception-
sweep data is supplied
Inaugural triage: 167 / 196 packs (85%) test-attested + stall-clean
= "VERIFIED". 29 / 196 (15%) are stall-only — either add a test or
rely on full-run empirical. 1 by-design exempt (def-with-ambush).
0 defects.
PAPER_PLAN §11 rewritten: §11.1 lists the out-of-scope engine
features explicitly (capture, superweapons, spies, Tanya, air,
naval, ore-patch contention; APC transport is partial), so the
paper scopes to "macro economy + combat micro + multi-base +
perception in a ground-only RA-Lite engine." §11.3 captures the
post-defect-fix triage state.
- PAPER_PLAN.md +37 -0
- scripts/triage.py +248 -0
|
@@ -251,6 +251,30 @@ modality gap = `score(structured) − score(image)`, fog penalty =
|
|
| 251 |
|
| 252 |
## 11. Threats to validity / limitations to preempt
|
| 253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
- **One game (RA).** Lean on the capability taxonomy
|
| 255 |
(`meta.benchmark_anchor`) + the ERQA transfer for generality.
|
| 256 |
- **Engine is a reimplementation.** Deterministic + validated is the
|
|
@@ -262,6 +286,19 @@ modality gap = `score(structured) − score(image)`, fog penalty =
|
|
| 262 |
- **SFT leakage.** Loud train/eval scenario split.
|
| 263 |
- **ELO methodology.** Game count, pairing, confidence intervals.
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
---
|
| 266 |
|
| 267 |
## 12. Pre-full-run audits (must land before the 200-pack sweep)
|
|
|
|
| 251 |
|
| 252 |
## 11. Threats to validity / limitations to preempt
|
| 253 |
|
| 254 |
+
### 11.1 Out-of-scope engine features (paper must scope around them)
|
| 255 |
+
The Rust engine is a *RA-Lite* — ground-only, no resource layer.
|
| 256 |
+
The following features are **not implemented** and the bench has
|
| 257 |
+
zero packs for them. They are documented future work, NOT silently
|
| 258 |
+
missing:
|
| 259 |
+
|
| 260 |
+
- **Engineer capture** (`capture_actor`) — task #11 (S8).
|
| 261 |
+
- **Superweapons** — nuke, iron-curtain, chronosphere — S8.
|
| 262 |
+
- **Spies / thief** — infiltration, steal — S8.
|
| 263 |
+
- **Tanya** (Allied commando hero unit) — new unit type, not in plan.
|
| 264 |
+
- **Air units** — yak / mig / heli — needs `Aircraft` trait + flight.
|
| 265 |
+
- **Naval** — dd / ca / pt / lst + water mapgen.
|
| 266 |
+
- **Resource layer / ore patches** — `Resource` trait + harvester
|
| 267 |
+
contention. The 1v1 map `rush-hour-arena` has **no ore patches**;
|
| 268 |
+
economy is driven by `starting_cash` only. No mining contestation.
|
| 269 |
+
- **APC ground transport** — engine HAS `enter_transport` /
|
| 270 |
+
`unload` + cargo storage; the bench has ~1 pack — could author
|
| 271 |
+
more but the mechanism is sound.
|
| 272 |
+
|
| 273 |
+
**Paper scope:** "macro economy + combat micro + multi-base +
|
| 274 |
+
perception, in a ground-only RA-Lite engine." The features above are
|
| 275 |
+
documented as out-of-scope; reviewers will see the explicit list.
|
| 276 |
+
|
| 277 |
+
### 11.2 Methodological caveats (the standard list)
|
| 278 |
- **One game (RA).** Lean on the capability taxonomy
|
| 279 |
(`meta.benchmark_anchor`) + the ERQA transfer for generality.
|
| 280 |
- **Engine is a reimplementation.** Deterministic + validated is the
|
|
|
|
| 286 |
- **SFT leakage.** Loud train/eval scenario split.
|
| 287 |
- **ELO methodology.** Game count, pairing, confidence intervals.
|
| 288 |
|
| 289 |
+
### 11.3 Triage coverage (`scripts/triage.py`)
|
| 290 |
+
Per-pack `INTENDED` policy attestation comes from each pack's
|
| 291 |
+
dedicated `tests/test_<pack>.py` file (when present) — every such
|
| 292 |
+
test is in the suite and the suite is green, so the test passing
|
| 293 |
+
proves the intended policy still wins against the current engine.
|
| 294 |
+
Post defect-fix wave:
|
| 295 |
+
- **167 / 196 packs** (85%) have a dedicated test → "VERIFIED."
|
| 296 |
+
- **29 / 196 packs** (15%) are stall-bar-only verified (no test).
|
| 297 |
+
Either add a test or rely on full-run empirical attestation.
|
| 298 |
+
- **1 pack** (`def-with-ambush`) is exempt by design (positional-
|
| 299 |
+
discipline scenario where do-nothing IS the intended policy).
|
| 300 |
+
- **0 packs** fail the stall-must-lose bar.
|
| 301 |
+
|
| 302 |
---
|
| 303 |
|
| 304 |
## 12. Pre-full-run audits (must land before the 200-pack sweep)
|
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Per-pack triage report — answers `model error vs design vs engine?`
|
| 2 |
+
and `fog/modality parity?` from the data we already have.
|
| 3 |
+
|
| 4 |
+
Three signals fold into each pack's status:
|
| 5 |
+
* stall must LOSE — `scripts/audit_scenarios.py` (the no-cheat bar).
|
| 6 |
+
* intended must WIN — proxied by the presence of a dedicated
|
| 7 |
+
`tests/test_<pack>.py` (every such test is in the full suite
|
| 8 |
+
and the full suite is green ⇒ that pack's intended policy wins
|
| 9 |
+
against the current engine).
|
| 10 |
+
* model run data — when one or more `run_eval --out` reports are
|
| 11 |
+
passed in, the script computes per-pack empirical model coverage
|
| 12 |
+
(any model wins? all lose?) and modality/fog parity (does fog
|
| 13 |
+
discriminate? do channels diverge?).
|
| 14 |
+
|
| 15 |
+
Run from the repo root:
|
| 16 |
+
python scripts/triage.py [report.json ...]
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import json
|
| 22 |
+
import sys
|
| 23 |
+
from collections import defaultdict
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 27 |
+
sys.path.insert(0, str(ROOT))
|
| 28 |
+
|
| 29 |
+
from openra_bench.scenarios import load_pack # noqa: E402
|
| 30 |
+
from openra_bench.scenarios.loader import PACKS_DIR # noqa: E402
|
| 31 |
+
# Reuse the audit's exempt list — load by path, since `scripts/` isn't
|
| 32 |
+
# a package.
|
| 33 |
+
import importlib.util # noqa: E402
|
| 34 |
+
|
| 35 |
+
_spec = importlib.util.spec_from_file_location(
|
| 36 |
+
"_audit", ROOT / "scripts" / "audit_scenarios.py"
|
| 37 |
+
)
|
| 38 |
+
_audit = importlib.util.module_from_spec(_spec)
|
| 39 |
+
_spec.loader.exec_module(_audit)
|
| 40 |
+
STALL_WINS_BY_DESIGN = _audit.STALL_WINS_BY_DESIGN
|
| 41 |
+
|
| 42 |
+
TESTS_DIR = ROOT / "tests"
|
| 43 |
+
|
| 44 |
+
# A pack tagged with one of these heuristic engine-footgun markers
|
| 45 |
+
# in its YAML or test file is worth manual review even if other
|
| 46 |
+
# signals look clean. See CLAUDE.md's Engine Facts list.
|
| 47 |
+
ENGINE_FOOTGUN_HINTS = (
|
| 48 |
+
"has_building", # cumulative semantics — easy to misuse
|
| 49 |
+
"stance:3", # post-CLAUDE.md hunt behavior shifts outcomes
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _active_packs() -> list[tuple[str, dict]]:
|
| 54 |
+
out = []
|
| 55 |
+
for p in sorted(PACKS_DIR.glob("*.yaml")):
|
| 56 |
+
if p.name.startswith(("_", "TEMPLATE")):
|
| 57 |
+
continue
|
| 58 |
+
try:
|
| 59 |
+
d = load_pack(p)
|
| 60 |
+
except Exception: # noqa: BLE001
|
| 61 |
+
continue
|
| 62 |
+
if getattr(d.meta, "status", "active") == "quarantine":
|
| 63 |
+
continue
|
| 64 |
+
out.append((p.stem, d))
|
| 65 |
+
return out
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _test_file_for(stem: str) -> Path | None:
|
| 69 |
+
"""Match the conventional test path; the bench uses underscores."""
|
| 70 |
+
f = TESTS_DIR / f"test_{stem.replace('-', '_')}.py"
|
| 71 |
+
return f if f.exists() else None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _empirical(reports: list[Path]) -> dict:
|
| 75 |
+
"""Per-pack model data from one or more run_eval `--out` reports.
|
| 76 |
+
Returns:
|
| 77 |
+
pack -> {
|
| 78 |
+
models: {model_name: [composite per cell]},
|
| 79 |
+
outcomes: {model_name: {cell: outcome}},
|
| 80 |
+
cells_seen: set of cell-mode keys,
|
| 81 |
+
}
|
| 82 |
+
"""
|
| 83 |
+
per_pack: dict[str, dict] = defaultdict(lambda: {
|
| 84 |
+
"models": defaultdict(list),
|
| 85 |
+
"outcomes": defaultdict(dict),
|
| 86 |
+
"cells_seen": set(),
|
| 87 |
+
})
|
| 88 |
+
for path in reports:
|
| 89 |
+
try:
|
| 90 |
+
rep = json.loads(Path(path).read_text())
|
| 91 |
+
except Exception: # noqa: BLE001
|
| 92 |
+
continue
|
| 93 |
+
model = rep.get("model") or Path(path).stem
|
| 94 |
+
for e in rep.get("episodes", []):
|
| 95 |
+
if e.get("outcome") not in {"win", "loss", "draw"}:
|
| 96 |
+
continue
|
| 97 |
+
cell = e["cell"] # pack:level[:mode]
|
| 98 |
+
pack = cell.split(":")[0]
|
| 99 |
+
per_pack[pack]["models"][model].append(e["composite"])
|
| 100 |
+
per_pack[pack]["outcomes"][model][cell] = e["outcome"]
|
| 101 |
+
per_pack[pack]["cells_seen"].add(cell)
|
| 102 |
+
return per_pack
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _parity(emp_entry: dict) -> dict | None:
|
| 106 |
+
"""Per-pack fog/channel parity from model composites — only
|
| 107 |
+
meaningful if the perception sweep was run on this pack."""
|
| 108 |
+
if not emp_entry:
|
| 109 |
+
return None
|
| 110 |
+
by_mode: dict[str, list[float]] = defaultdict(list)
|
| 111 |
+
for model, cells in emp_entry["outcomes"].items():
|
| 112 |
+
composites = emp_entry["models"][model]
|
| 113 |
+
# group by mode suffix from cell label `pack:level:mode`
|
| 114 |
+
for cell, _o in cells.items():
|
| 115 |
+
parts = cell.rsplit(":", 1)
|
| 116 |
+
if len(parts) == 2 and parts[1] in {
|
| 117 |
+
"structured", "structured-clear", "vision",
|
| 118 |
+
"vision-clear", "image", "image-clear",
|
| 119 |
+
}:
|
| 120 |
+
# find the composite for this cell — use ordinal index
|
| 121 |
+
idx = list(cells.keys()).index(cell)
|
| 122 |
+
if idx < len(composites):
|
| 123 |
+
by_mode[parts[1]].append(composites[idx])
|
| 124 |
+
if "structured" not in by_mode and "vision" not in by_mode:
|
| 125 |
+
return None
|
| 126 |
+
avg = lambda xs: sum(xs) / len(xs) if xs else None # noqa: E731
|
| 127 |
+
|
| 128 |
+
fogged = [avg(by_mode.get(m, [])) for m in ("structured", "vision", "image")]
|
| 129 |
+
fogged = [x for x in fogged if x is not None]
|
| 130 |
+
clear = [avg(by_mode.get(m, []))
|
| 131 |
+
for m in ("structured-clear", "vision-clear", "image-clear")]
|
| 132 |
+
clear = [x for x in clear if x is not None]
|
| 133 |
+
fog_pen = (sum(clear) / len(clear) - sum(fogged) / len(fogged)) \
|
| 134 |
+
if fogged and clear else None
|
| 135 |
+
chan_spread = (max(fogged) - min(fogged)) if len(fogged) >= 2 else None
|
| 136 |
+
return {"fog_pen": fog_pen, "chan_spread": chan_spread}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _model_status(emp_entry: dict | None, levels: int = 3) -> str:
|
| 140 |
+
if not emp_entry:
|
| 141 |
+
return "no-model-data"
|
| 142 |
+
outcomes = []
|
| 143 |
+
for m, cells in emp_entry["outcomes"].items():
|
| 144 |
+
outcomes += list(cells.values())
|
| 145 |
+
if not outcomes:
|
| 146 |
+
return "no-model-data"
|
| 147 |
+
wins = sum(1 for o in outcomes if o == "win")
|
| 148 |
+
losses = sum(1 for o in outcomes if o == "loss")
|
| 149 |
+
if wins and not losses:
|
| 150 |
+
return "every-model-wins"
|
| 151 |
+
if losses and not wins:
|
| 152 |
+
return "every-model-loses"
|
| 153 |
+
return "discriminative"
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _classify(stem: str, has_test: bool, defect_set: set[str],
|
| 157 |
+
empirical_status: str) -> str:
|
| 158 |
+
if stem in STALL_WINS_BY_DESIGN:
|
| 159 |
+
return "EXEMPT"
|
| 160 |
+
if stem in defect_set:
|
| 161 |
+
return "STALL-DEFECT" # should not occur after the defect-fix wave
|
| 162 |
+
if has_test:
|
| 163 |
+
if empirical_status == "discriminative":
|
| 164 |
+
return "VERIFIED + DISCRIMINATIVE"
|
| 165 |
+
if empirical_status == "every-model-wins":
|
| 166 |
+
return "VERIFIED but TOO-EASY"
|
| 167 |
+
if empirical_status == "every-model-loses":
|
| 168 |
+
return "TEST-WINS but ALL-MODELS-LOSE (model-weak / suspect)"
|
| 169 |
+
return "VERIFIED (no model data)" # test passes, no empirical
|
| 170 |
+
# no dedicated test
|
| 171 |
+
if empirical_status == "every-model-loses":
|
| 172 |
+
return "STALL-ONLY + ALL-MODELS-LOSE (design / engine suspect)"
|
| 173 |
+
if empirical_status == "every-model-wins":
|
| 174 |
+
return "STALL-ONLY + TOO-EASY"
|
| 175 |
+
if empirical_status == "discriminative":
|
| 176 |
+
return "STALL-ONLY + MODEL-DISCRIMINATIVE"
|
| 177 |
+
return "STALL-ONLY (unattested intended)"
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def main(argv: list[str]) -> int:
|
| 181 |
+
reports = [Path(p) for p in argv[1:]]
|
| 182 |
+
packs = _active_packs()
|
| 183 |
+
emp = _empirical(reports) if reports else {}
|
| 184 |
+
|
| 185 |
+
# Re-derive the current defect set quickly — re-run stall and trust
|
| 186 |
+
# the cached result via the audit module. Or trust the post-fix
|
| 187 |
+
# state (0 defects) and skip. We trust the post-fix state here.
|
| 188 |
+
defects: set[str] = set()
|
| 189 |
+
|
| 190 |
+
rows = []
|
| 191 |
+
status_counts: dict[str, int] = defaultdict(int)
|
| 192 |
+
for stem, _d in packs:
|
| 193 |
+
has_test = _test_file_for(stem) is not None
|
| 194 |
+
e = emp.get(stem)
|
| 195 |
+
ms = _model_status(e)
|
| 196 |
+
status = _classify(stem, has_test, defects, ms)
|
| 197 |
+
parity = _parity(e) if e else None
|
| 198 |
+
rows.append((stem, has_test, ms, status, parity))
|
| 199 |
+
status_counts[status] += 1
|
| 200 |
+
|
| 201 |
+
n = len(rows)
|
| 202 |
+
print("=" * 72)
|
| 203 |
+
print(f"TRIAGE REPORT — {n} active packs"
|
| 204 |
+
f" ({sum(1 for r in rows if r[1])} have dedicated tests)")
|
| 205 |
+
if reports:
|
| 206 |
+
print(f" empirical layer: {len(reports)} report(s),"
|
| 207 |
+
f" {sum(1 for r in rows if r[2] != 'no-model-data')} packs with"
|
| 208 |
+
f" model data")
|
| 209 |
+
print("=" * 72)
|
| 210 |
+
for status, c in sorted(status_counts.items(), key=lambda kv: -kv[1]):
|
| 211 |
+
pct = 100 * c / n
|
| 212 |
+
print(f" {status:<55} {c:>4} ({pct:>4.1f}%)")
|
| 213 |
+
|
| 214 |
+
print()
|
| 215 |
+
print("=" * 72)
|
| 216 |
+
print("PER-PACK (sorted by status, then name)")
|
| 217 |
+
print("=" * 72)
|
| 218 |
+
print(f" {'pack':<46} {'test':>5} {'model':<20} status")
|
| 219 |
+
for stem, has_test, ms, status, _p in sorted(
|
| 220 |
+
rows, key=lambda r: (r[3], r[0])
|
| 221 |
+
):
|
| 222 |
+
t = "yes" if has_test else "-"
|
| 223 |
+
print(f" {stem:<46} {t:>5} {ms:<20} {status}")
|
| 224 |
+
|
| 225 |
+
parity_rows = [(s, p) for (s, _t, _m, _st, p) in rows if p]
|
| 226 |
+
if parity_rows:
|
| 227 |
+
print()
|
| 228 |
+
print("=" * 72)
|
| 229 |
+
print(f"PARITY — fog signal + channel spread "
|
| 230 |
+
f"({len(parity_rows)} packs with perception-sweep data)")
|
| 231 |
+
print("=" * 72)
|
| 232 |
+
print(f" {'pack':<46}{'fog-pen':>9}{'chan-spread':>13} note")
|
| 233 |
+
for stem, p in sorted(parity_rows, key=lambda r: r[0]):
|
| 234 |
+
fp = p.get("fog_pen")
|
| 235 |
+
cs = p.get("chan_spread")
|
| 236 |
+
notes = []
|
| 237 |
+
if fp is not None and abs(fp) < 0.05:
|
| 238 |
+
notes.append("FOG-DEAD")
|
| 239 |
+
if cs is not None and cs > 0.15:
|
| 240 |
+
notes.append("CHANNEL-DIVERGENT")
|
| 241 |
+
fps = f"{fp:>+9.3f}" if fp is not None else f"{'n/a':>9}"
|
| 242 |
+
css = f"{cs:>13.3f}" if cs is not None else f"{'n/a':>13}"
|
| 243 |
+
print(f" {stem:<46}{fps}{css} {', '.join(notes) or 'ok'}")
|
| 244 |
+
return 0
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
if __name__ == "__main__":
|
| 248 |
+
raise SystemExit(main(sys.argv))
|