Gate A1 and related cognition-path changes shifted the showcase determinism fingerprint (86db25b6…) while the lane pin still reflected the pre-drift artifact (e323adb3…). Re-pin to the CI-verified SHA 2895df08… and refresh v1_dev.json + CLAIMS.md. Hermetic demo hygiene: use no_load_state for register-tour, learning- loop, and multi-hop showcase adapters, and isolate CORE_ENGINE_STATE_DIR in the public_demo lane runner so local engine_state/ pollution cannot affect eval output or wall-clock.
251 lines
8.1 KiB
Python
251 lines
8.1 KiB
Python
"""Runner for evals/public_demo/ (ADR-0099).
|
|
|
|
Verifies the four ADR-0099 invariants in one pass:
|
|
|
|
- All claims supported on a single fresh run.
|
|
- Two runs produce byte-identical JSON (excluding ``total_runtime_ms``).
|
|
- Total runtime ≤ 30 seconds.
|
|
- Showcase imports only from already-shipped modules (no new mechanism).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
SHOWCASE_PATH = REPO_ROOT / "core" / "demos" / "showcase.py"
|
|
|
|
# ADR-0099 §"Hard constraints" grep gate: showcase must compose only
|
|
# from these top-level packages plus its own sub-modules.
|
|
ALLOWED_IMPORT_PREFIXES: tuple[str, ...] = (
|
|
"core.",
|
|
"chat.",
|
|
"generate.",
|
|
"language_packs.",
|
|
"teaching.",
|
|
"evals.",
|
|
)
|
|
ALLOWED_STDLIB: tuple[str, ...] = (
|
|
"__future__",
|
|
"subprocess",
|
|
"time",
|
|
"pathlib",
|
|
"typing",
|
|
"dataclasses",
|
|
"html",
|
|
"re",
|
|
"hashlib",
|
|
"json",
|
|
"os",
|
|
"sys",
|
|
)
|
|
|
|
|
|
def _case_all_claims_supported(payload: dict[str, Any]) -> dict[str, Any]:
|
|
if not payload.get("all_claims_supported", False):
|
|
return _fail("all_claims_supported", "showcase reports all_claims_supported=False")
|
|
bad_scenes = [
|
|
s["scene_id"] for s in payload["scenes"] if not s["all_claims_supported"]
|
|
]
|
|
if bad_scenes:
|
|
return _fail("all_claims_supported", f"failing scenes: {bad_scenes}")
|
|
return _pass(
|
|
"all_claims_supported",
|
|
{"scene_count": len(payload["scenes"])},
|
|
)
|
|
|
|
|
|
_VOLATILE_KEYS: frozenset[str] = frozenset(
|
|
{
|
|
"total_runtime_ms", # wall-clock
|
|
"json_path", # adapter output paths (per-run temp dirs)
|
|
"transient_corpus", # learning-loop embeds its temp corpus path
|
|
# ``generated_at_revision`` advances every commit; the lane's
|
|
# invariant is "same code → same SHA," not "same HEAD → same
|
|
# SHA." Stripping this here keeps the pinned lane SHA stable
|
|
# across commits unless the underlying demos' content changes.
|
|
"generated_at_revision",
|
|
}
|
|
)
|
|
|
|
|
|
def _strip_volatile(node: Any) -> Any:
|
|
"""Recursively drop volatile keys so two runs can be compared.
|
|
|
|
The deterministic content sits in: claims, evidence (sha256 prefixes),
|
|
trace_features (each adapter's report_sha256), and the per-scene
|
|
statement. Wall-clock and absolute paths are excluded.
|
|
"""
|
|
if isinstance(node, dict):
|
|
return {
|
|
k: _strip_volatile(v)
|
|
for k, v in node.items()
|
|
if k not in _VOLATILE_KEYS
|
|
}
|
|
if isinstance(node, list):
|
|
return [_strip_volatile(v) for v in node]
|
|
return node
|
|
|
|
|
|
def _case_byte_equality(payload_a: dict, payload_b: dict) -> dict[str, Any]:
|
|
a_stripped = _strip_volatile(payload_a)
|
|
b_stripped = _strip_volatile(payload_b)
|
|
a = json.dumps(a_stripped, sort_keys=True, indent=2).encode()
|
|
b = json.dumps(b_stripped, sort_keys=True, indent=2).encode()
|
|
if a != b:
|
|
return _fail(
|
|
"determinism_run_to_run_byte_equality",
|
|
"showcase JSON bytes differ across two runs (after volatile-key strip)",
|
|
)
|
|
return _pass(
|
|
"determinism_run_to_run_byte_equality",
|
|
{"sha256": hashlib.sha256(a).hexdigest()},
|
|
)
|
|
|
|
|
|
def _case_runtime_under_budget(payload: dict[str, Any]) -> dict[str, Any]:
|
|
runtime_ms = payload.get("total_runtime_ms")
|
|
budget_ms = payload.get("max_runtime_seconds", 30) * 1000
|
|
if runtime_ms is None:
|
|
return _fail("runtime_under_budget", "payload missing total_runtime_ms")
|
|
if runtime_ms > budget_ms:
|
|
return _fail(
|
|
"runtime_under_budget",
|
|
f"{runtime_ms} ms > budget {budget_ms} ms",
|
|
)
|
|
# Don't include the exact runtime_ms in details — it varies per
|
|
# run and would break the lane report's byte-equality even at
|
|
# one-second bucket granularity for near-boundary runs. The case
|
|
# passing already proves runtime ≤ budget; the exact ms is in the
|
|
# showcase's own JSON for callers who want it.
|
|
return _pass(
|
|
"runtime_under_budget",
|
|
{"budget_seconds": budget_ms // 1000},
|
|
)
|
|
|
|
|
|
_IMPORT_RE = re.compile(r"^\s*(?:from\s+([\w\.]+)\s+import|import\s+([\w\.]+))")
|
|
|
|
|
|
def _case_pure_composition() -> dict[str, Any]:
|
|
"""Grep gate: showcase imports only from shipped packages + stdlib."""
|
|
sources = (
|
|
SHOWCASE_PATH,
|
|
REPO_ROOT / "core" / "demos" / "showcase_adapters.py",
|
|
REPO_ROOT / "core" / "demos" / "learning_loop_adapter.py",
|
|
)
|
|
forbidden: list[str] = []
|
|
for source in sources:
|
|
if not source.exists():
|
|
continue
|
|
for line in source.read_text(encoding="utf-8").splitlines():
|
|
match = _IMPORT_RE.match(line)
|
|
if not match:
|
|
continue
|
|
mod = (match.group(1) or match.group(2)).strip()
|
|
head = mod.split(".", 1)[0]
|
|
if mod.startswith(ALLOWED_IMPORT_PREFIXES):
|
|
continue
|
|
if head in ALLOWED_STDLIB:
|
|
continue
|
|
if mod in ALLOWED_STDLIB:
|
|
continue
|
|
forbidden.append(f"{source.relative_to(REPO_ROOT)}: {mod}")
|
|
if forbidden:
|
|
return _fail(
|
|
"pure_composition_no_new_mechanism",
|
|
f"forbidden imports: {forbidden}",
|
|
)
|
|
return _pass(
|
|
"pure_composition_no_new_mechanism",
|
|
{"sources_checked": len(sources)},
|
|
)
|
|
|
|
|
|
def _pass(case_id: str, details: dict[str, Any]) -> dict[str, Any]:
|
|
return {"case_id": case_id, "passed": True, "details": details, "divergence": None}
|
|
|
|
|
|
def _fail(case_id: str, divergence: str) -> dict[str, Any]:
|
|
return {"case_id": case_id, "passed": False, "details": {}, "divergence": divergence}
|
|
|
|
|
|
def run() -> dict[str, Any]:
|
|
import os
|
|
|
|
from core.demos.showcase import run_showcase
|
|
|
|
# Hermetic lane: demos must not load the developer's lived engine_state/.
|
|
os.environ.setdefault(
|
|
"CORE_ENGINE_STATE_DIR",
|
|
tempfile.mkdtemp(prefix="public_demo_engine_state_"),
|
|
)
|
|
|
|
tmp_root = Path(tempfile.mkdtemp(prefix="public_demo_lane_"))
|
|
try:
|
|
run_a = run_showcase(output_dir=tmp_root / "run_a")
|
|
run_b = run_showcase(output_dir=tmp_root / "run_b")
|
|
finally:
|
|
shutil.rmtree(tmp_root, ignore_errors=True)
|
|
|
|
cases = [
|
|
_case_all_claims_supported(run_a),
|
|
_case_byte_equality(run_a, run_b),
|
|
_case_runtime_under_budget(run_a),
|
|
_case_pure_composition(),
|
|
]
|
|
return {
|
|
"lane": "public_demo",
|
|
"lane_version": "v1",
|
|
"split": "dev",
|
|
"adr": "ADR-0099",
|
|
"invariants": [
|
|
"public_showcase_pure_composition",
|
|
"public_showcase_all_claims_supported",
|
|
"public_showcase_json_byte_equality",
|
|
],
|
|
"total_cases": len(cases),
|
|
"passed_cases": sum(1 for c in cases if c["passed"]),
|
|
"failed_cases": sum(1 for c in cases if not c["passed"]),
|
|
"all_passed": all(c["passed"] for c in cases),
|
|
"cases": cases,
|
|
}
|
|
|
|
|
|
def _canonical_json(payload: dict[str, Any]) -> bytes:
|
|
return json.dumps(payload, sort_keys=True, indent=2).encode("utf-8") + b"\n"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="public_demo lane runner")
|
|
parser.add_argument("--report", type=Path, default=None)
|
|
args = parser.parse_args(argv)
|
|
|
|
summary = run()
|
|
lane_dir = Path(__file__).resolve().parent
|
|
report_path = args.report or (lane_dir / "results" / "v1_dev.json")
|
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload_bytes = _canonical_json(summary)
|
|
report_path.write_bytes(payload_bytes)
|
|
|
|
print(f"report: {report_path}")
|
|
print(f"sha256: {hashlib.sha256(payload_bytes).hexdigest()}")
|
|
print(f"passed: {summary['passed_cases']}/{summary['total_cases']}")
|
|
if not summary["all_passed"]:
|
|
for c in summary["cases"]:
|
|
if not c["passed"]:
|
|
print(f" FAIL {c['case_id']}: {c['divergence']}")
|
|
return 0 if summary["all_passed"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|