Make the proof suite say what it actually ran
The suite had fifteen checks that could not lose. The visual HashHop arena painted a cyan line and a white ring on the answer slot, then asked the model to find the answer by looking; the collector re-rolled puzzles until the running rate crossed its target; the cache check passed if any layer had ever recorded an entry; the repository check passed on a call graph built in memory without touching the store; the research-module check asserted non-empty lists of SHA256-derived placeholder vectors and so could not fail. Each check now answers for one claim and reports a verdict the reader can act on: passed, failed, deferred, or removed. A deferred row names what is missing and the command that would settle it; a removed row says why the old claim was withdrawn. The suite verdict is failed > deferred > passed, so a claim that never ran holds the whole report back from saying "proven". - evals.py (2427 lines) becomes the proofs/ package; largest module 445 lines. - proofs/__main__.py deleted: it duplicated PRDProofSuite.latest() with a different glob. proofs/table.py renders; latest-proof-suite is the one face and exits 1 on a failed verdict. - The arena paints nothing. The puzzle id is printed on its tile instead, because removing the ink without it removes the question. - HashHop runs a fixed 8 puzzles per depth instead of re-rolling to target. - The contradiction corpus moves from code to contradiction_scenarios.json; the check stays deferred while the corpus is the one this repo authored. - The cache check compiles a nonce question over a real atom twice and reads the miss to hit movement; an empty store fails instead of agreeing by luck. - The repository check requires rows in the store; the research-module check is removed. Two product bugs surfaced on the way: - text_index.fts_query joined Latin terms with a space, which FTS5 reads as AND, so a question longer than its answer retrieved nothing. Terms are ORed and bm25 rank orders the candidates. - profiles.windows_path_to_wsl prefixed /mnt/ onto an already-POSIX path with an empty drive, producing /mnt//home/... and failing every launcher probe. Verification: 168 passed (pytest backend/tests bench/tests); ruff clean on the proofs package, cli.py, text_index.py, profiles.py, and both test files; mypy clean on 16 files with a fresh cache; pyright reports 0 errors on proofs/ and the test file. Ruff debt elsewhere is unchanged: 19 errors in seven files this commit does not touch, measured against the same files at HEAD.
This commit is contained in:
1 parent
dfc6cd4c28
commit
4ab36cd5eb
27 files changed
+4284
-2453
No files matched your search
@@ -24,7 +24,6 @@ from .compiler import ContextCompiler
|
||||
from .config import Settings, get_settings
|
||||
from .db import Database
|
||||
from .embeddings import EmbeddingService
|
||||
from .evals import HashHopEvaluator, PRDProofSuite
|
||||
from .evaluation_schemas import (
|
||||
BenchmarkPredictionReport,
|
||||
BenchmarkRegistryReport,
|
||||
@@ -38,6 +37,7 @@ from .evidence_graph import EvidenceGraphService
|
||||
from .ingestion import IngestionService
|
||||
from .memory import MemoryService
|
||||
from .profiles import runtime_profile_report
|
||||
from .proofs import HashHopEvaluator, PRDProofSuite
|
||||
from .research_modules import ResearchModuleService
|
||||
from .retrieval import RetrievalService
|
||||
from .runtime import QwenRuntime, RuntimeUnavailableError
|
||||
|
||||
@@ -9,6 +9,8 @@ from .config import Settings, get_settings
|
||||
from .db import Database
|
||||
from .schemas import CacheLayerStatus
|
||||
|
||||
SEMANTIC_LAYER = "semantic"
|
||||
|
||||
|
||||
class CacheFabric:
|
||||
def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None:
|
||||
@@ -16,7 +18,7 @@ class CacheFabric:
|
||||
self.db = database or Database(self.settings)
|
||||
|
||||
def semantic_key(self, question: str, profile: str) -> str:
|
||||
return hashlib.sha256(f"compiler-v2\n{profile}\n{question}".encode("utf-8")).hexdigest()
|
||||
return hashlib.sha256(f"compiler-v2\n{profile}\n{question}".encode()).hexdigest()
|
||||
|
||||
def record_hit(self, layer: str, cache_key: str, bytes_estimate: int = 0, metadata: dict[str, Any] | None = None) -> None:
|
||||
self.db.log_cache_event(layer, cache_key, "hit", bytes_estimate, metadata)
|
||||
@@ -29,15 +31,17 @@ class CacheFabric:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
|
||||
path.write_bytes(data)
|
||||
self.db.log_cache_event("semantic", cache_key, "write", len(data), {"path": str(path)})
|
||||
self.db.log_cache_event(SEMANTIC_LAYER, cache_key, "write", len(data), {"path": str(path)})
|
||||
|
||||
def read_semantic_cache(self, cache_key: str) -> dict[str, Any] | None:
|
||||
"""Return the cached payload, or None. The caller traces the event: one compile, one event."""
|
||||
path = self.settings.cache_dir / "semantic" / f"{cache_key}.json"
|
||||
if not path.exists():
|
||||
self.record_miss("semantic", cache_key)
|
||||
return None
|
||||
self.record_hit("semantic", cache_key, path.stat().st_size)
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"{path}: the semantic cache entry is unreadable ({error})") from error
|
||||
|
||||
def statuses(self) -> list[CacheLayerStatus]:
|
||||
rows = self.db.cache_layer_rows()
|
||||
|
||||
@@ -20,10 +20,12 @@ from .cli_transactive import register as register_transactive_commands
|
||||
from .config import get_settings
|
||||
from .db import Database
|
||||
from .embeddings import EmbeddingService
|
||||
from .evals import HashHopEvaluator, PRDProofSuite
|
||||
from .ingestion import IngestionService
|
||||
from .memory import MemoryService
|
||||
from .profiles import runtime_profile_report
|
||||
from .proofs import HashHopEvaluator, PRDProofSuite
|
||||
from .proofs.contradictions import CORPUS_NAME, build_contradiction_scenarios, write_corpus
|
||||
from .proofs.table import render_table
|
||||
from .research_modules import ResearchModuleService
|
||||
from .retrieval import RetrievalService
|
||||
from .runtime import QwenRuntime
|
||||
@@ -232,11 +234,19 @@ def proof_suite(cartridge_id: str | None = None) -> None:
|
||||
|
||||
|
||||
@app.command("latest-proof-suite")
|
||||
def latest_proof_suite() -> None:
|
||||
def latest_proof_suite(json_output: bool = typer.Option(False, "--json")) -> None:
|
||||
"""Print what the last proof suite claimed and what it actually proved."""
|
||||
report = PRDProofSuite(get_settings()).latest()
|
||||
if report is None:
|
||||
raise typer.BadParameter(
|
||||
"no proof suite has been recorded yet; run: ely-eye proof-suite --cartridge-id <id>"
|
||||
)
|
||||
if json_output:
|
||||
typer.echo(report.model_dump_json(indent=2))
|
||||
else:
|
||||
typer.echo(render_table(report))
|
||||
if report.status == "failed":
|
||||
raise typer.Exit(code=1)
|
||||
typer.echo(report.model_dump_json(indent=2))
|
||||
|
||||
|
||||
@app.command("runtime-generation-proof")
|
||||
@@ -251,6 +261,14 @@ def visual_contradiction_proof() -> None:
|
||||
typer.echo(check.model_dump_json(indent=2))
|
||||
|
||||
|
||||
@app.command("contradiction-corpus")
|
||||
def contradiction_corpus(data_dir: Path | None = None) -> None:
|
||||
"""Write the corpus the contradiction lens is graded on, so it can be read and replaced."""
|
||||
settings = get_settings()
|
||||
target = write_corpus((data_dir or settings.data_dir) / CORPUS_NAME)
|
||||
typer.echo(json_dump({"corpus": str(target), "scenarios": len(build_contradiction_scenarios())}))
|
||||
|
||||
|
||||
@app.command("finalize-cartridge")
|
||||
def finalize_cartridge(cartridge_id: str) -> None:
|
||||
report = CartridgeAssetService(get_settings()).finalize(cartridge_id)
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
from collections.abc import Iterable
|
||||
|
||||
from .cache_fabric import CacheFabric
|
||||
from .cache_fabric import SEMANTIC_LAYER, CacheFabric
|
||||
from .config import Settings, get_settings
|
||||
from .db import Database
|
||||
from .retrieval import RetrievalService
|
||||
@@ -27,11 +27,18 @@ class ContextCompiler:
|
||||
def compile(self, question: str, profile: RuntimeProfile) -> CompiledContext:
|
||||
cache_key = self.cache.semantic_key(question, profile.value)
|
||||
cached = self.cache.read_semantic_cache(cache_key)
|
||||
if cached:
|
||||
if cached is not None:
|
||||
compiled = CompiledContext.model_validate(cached)
|
||||
if compiled.hits:
|
||||
self.cache.record_hit(SEMANTIC_LAYER, cache_key)
|
||||
return compiled
|
||||
|
||||
# A cached context with no evidence in it is not an answer, so counting it as a hit
|
||||
# would let an empty cache entry look like a warm cache.
|
||||
self.cache.record_miss(
|
||||
SEMANTIC_LAYER, cache_key, metadata={"reason": "cached context has no hits"}
|
||||
)
|
||||
else:
|
||||
self.cache.record_miss(SEMANTIC_LAYER, cache_key)
|
||||
plan = self.plan(question, profile)
|
||||
hits = self.retrieval.search(plan.retrieval_query, plan.evidence_budget_atoms).hits
|
||||
packed_text, token_total = self._pack_hits(hits, plan.token_budget, plan.retrieval_query)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -105,16 +105,26 @@ class HashHopProof(BaseModel):
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
|
||||
class ProofCheck(BaseModel):
|
||||
"""One claim, one verdict.
|
||||
|
||||
`deferred` means the check is written and runnable but needs something this machine does not
|
||||
have (model weights, a second source). `removed` means the old check proved nothing and the
|
||||
claim now waits for a check that can. Both carry a reason and the command that runs the check.
|
||||
"""
|
||||
|
||||
name: str
|
||||
requirement: str
|
||||
status: Literal["passed", "failed"]
|
||||
status: Literal["passed", "failed", "deferred", "removed"]
|
||||
evidence: list[str]
|
||||
detail: str
|
||||
reason: str = ""
|
||||
rerun: str = ""
|
||||
|
||||
|
||||
class ProofSuiteReport(BaseModel):
|
||||
proof_id: str
|
||||
cartridge_id: str
|
||||
status: Literal["passed", "failed"]
|
||||
status: Literal["passed", "failed", "deferred"]
|
||||
created_at: datetime = Field(default_factory=utc_now)
|
||||
checks: list[ProofCheck]
|
||||
artifacts: dict[str, str]
|
||||
|
||||
@@ -410,10 +410,12 @@ def resolve_llama_server() -> str | None:
|
||||
|
||||
|
||||
def windows_path_to_wsl(path: Path) -> str:
|
||||
"""Map a Windows path to the WSL mount a WSL shell can cd into; a POSIX path is already correct."""
|
||||
resolved = path.resolve()
|
||||
drive = resolved.drive.rstrip(":").lower()
|
||||
parts = resolved.parts[1:]
|
||||
return "/mnt/" + drive + "/" + "/".join(parts)
|
||||
if not drive:
|
||||
return str(resolved)
|
||||
return "/mnt/" + drive + "/" + "/".join(resolved.parts[1:])
|
||||
|
||||
|
||||
def wsl_quote(value: str) -> str:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Proof machinery: one module per kind of claim, one verdict per check."""
|
||||
|
||||
from .hashhop import HashHopEvaluator
|
||||
from .suite import PRDProofSuite
|
||||
|
||||
__all__ = ["HashHopEvaluator", "PRDProofSuite"]
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Draw the HashHop puzzles and read the model's answer back out of them."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# The four arena positions a model may answer with. Naming them once keeps the puzzle, the parser,
|
||||
# and the scoring keyed on the same values instead of four spellings in three files.
|
||||
SlotName = Literal["top-left", "top-right", "bottom-left", "bottom-right"]
|
||||
SLOTS: tuple[SlotName, ...] = ("top-left", "top-right", "bottom-left", "bottom-right")
|
||||
|
||||
ID_LINE_CHARS = 8
|
||||
CANVAS_BACKGROUND = (8, 10, 14)
|
||||
|
||||
|
||||
def render_visual_hash(item_id: str, path: Path, tile_size: int) -> None:
|
||||
"""Print the id on the tile, over a pattern derived from the same id.
|
||||
|
||||
A pattern alone is not a puzzle: nothing maps a random texture back to a hex id, so the
|
||||
only way to answer was the ink the renderer painted on the answer. The id is now legible,
|
||||
which makes the task look, match, and answer.
|
||||
"""
|
||||
seed = int(hashlib.sha256(item_id.encode("utf-8")).hexdigest()[:16], 16)
|
||||
rng = random.Random(seed)
|
||||
image = Image.new("RGB", (tile_size, tile_size), color=_noise_color(rng))
|
||||
draw = ImageDraw.Draw(image)
|
||||
for _ in range(96):
|
||||
x0 = rng.randrange(tile_size)
|
||||
y0 = rng.randrange(tile_size)
|
||||
x1 = min(tile_size, x0 + rng.randrange(4, tile_size // 3))
|
||||
y1 = min(tile_size, y0 + rng.randrange(4, tile_size // 3))
|
||||
color = _noise_color(rng)
|
||||
if rng.random() < 0.5:
|
||||
draw.rectangle([x0, y0, x1, y1], fill=color)
|
||||
else:
|
||||
draw.ellipse([x0, y0, x1, y1], fill=color)
|
||||
_draw_id(draw, item_id, tile_size)
|
||||
image.save(path)
|
||||
|
||||
|
||||
def _noise_color(rng: random.Random) -> tuple[int, int, int]:
|
||||
return (rng.randrange(256), rng.randrange(256), rng.randrange(256))
|
||||
|
||||
|
||||
def _draw_id(draw: ImageDraw.ImageDraw, item_id: str, tile_size: int) -> None:
|
||||
font = ImageFont.load_default(size=max(12, tile_size // 8))
|
||||
plate = (
|
||||
int(tile_size * 0.05),
|
||||
int(tile_size * 0.16),
|
||||
int(tile_size * 0.95),
|
||||
int(tile_size * 0.88),
|
||||
)
|
||||
draw.rectangle(plate, fill=(255, 255, 255))
|
||||
lines = [item_id[start : start + ID_LINE_CHARS] for start in range(0, len(item_id), ID_LINE_CHARS)]
|
||||
line_height = font.getbbox("AbgJ")[3]
|
||||
block_top = (plate[1] + plate[3]) / 2 - (len(lines) * line_height) / 2
|
||||
for index, line in enumerate(lines):
|
||||
box = font.getbbox(line)
|
||||
left = (plate[0] + plate[2]) / 2 - (box[2] - box[0]) / 2
|
||||
draw.text((left, block_top + index * line_height), line, fill=(12, 14, 18), font=font)
|
||||
|
||||
|
||||
def render_visual_hash_arena(
|
||||
source_path: Path, slot_paths: dict[str, Path], output_path: Path
|
||||
) -> None:
|
||||
"""Place the source and the candidates. Nothing marks which candidate is the answer."""
|
||||
tile = 160
|
||||
canvas = Image.new("RGB", (920, 560), color=CANVAS_BACKGROUND)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
source_box = (72, 200, 72 + tile, 200 + tile)
|
||||
slot_boxes = _slot_boxes(520, 704, 76, 324, tile)
|
||||
paste_visual_hash_tile(canvas, source_path, source_box)
|
||||
draw.rectangle(source_box, outline=(255, 78, 220), width=8)
|
||||
for slot, box in slot_boxes.items():
|
||||
paste_visual_hash_tile(canvas, slot_paths[slot], box)
|
||||
draw.rectangle(box, outline=(180, 190, 205), width=5)
|
||||
_save(canvas, output_path)
|
||||
|
||||
|
||||
def render_visual_hash_two_hop_arena(
|
||||
source_path: Path,
|
||||
intermediate_slot_paths: dict[str, Path],
|
||||
target_slot_paths: dict[str, Path],
|
||||
output_path: Path,
|
||||
) -> None:
|
||||
"""Two grids of candidates, no arrow and no ring: the chain has to be read, not seen."""
|
||||
tile = 136
|
||||
canvas = Image.new("RGB", (1180, 720), color=CANVAS_BACKGROUND)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
source_box = (64, 292, 64 + tile, 292 + tile)
|
||||
intermediate_boxes = _slot_boxes(424, 584, 104, 480, tile)
|
||||
target_boxes = _slot_boxes(864, 1024, 104, 480, tile)
|
||||
paste_visual_hash_tile(canvas, source_path, source_box)
|
||||
draw.rectangle(source_box, outline=(255, 78, 220), width=7)
|
||||
for grid, boxes, outline in (
|
||||
(intermediate_slot_paths, intermediate_boxes, (140, 215, 255)),
|
||||
(target_slot_paths, target_boxes, (255, 196, 92)),
|
||||
):
|
||||
for slot, box in boxes.items():
|
||||
paste_visual_hash_tile(canvas, grid[slot], box)
|
||||
draw.rectangle(box, outline=outline, width=5)
|
||||
_save(canvas, output_path)
|
||||
|
||||
|
||||
def _slot_boxes(
|
||||
left_x: int, right_x: int, top_y: int, bottom_y: int, tile: int
|
||||
) -> dict[str, tuple[int, int, int, int]]:
|
||||
return {
|
||||
"top-left": (left_x, top_y, left_x + tile, top_y + tile),
|
||||
"top-right": (right_x, top_y, right_x + tile, top_y + tile),
|
||||
"bottom-left": (left_x, bottom_y, left_x + tile, bottom_y + tile),
|
||||
"bottom-right": (right_x, bottom_y, right_x + tile, bottom_y + tile),
|
||||
}
|
||||
|
||||
|
||||
def _save(canvas: Image.Image, output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(output_path)
|
||||
|
||||
|
||||
def paste_visual_hash_tile(canvas: Image.Image, path: Path, box: tuple[int, int, int, int]) -> None:
|
||||
tile = Image.open(path).convert("RGB").resize((box[2] - box[0], box[3] - box[1]), Image.Resampling.LANCZOS)
|
||||
canvas.paste(tile, box[:2])
|
||||
|
||||
|
||||
def extract_visual_target_slot(text: str) -> SlotName | None:
|
||||
stripped = text.strip()
|
||||
payload: dict[str, Any] | None
|
||||
try:
|
||||
parsed = json.loads(stripped)
|
||||
payload = parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
payload = extract_json_object(stripped)
|
||||
if isinstance(payload, dict):
|
||||
for key in ("target_slot", "final_slot", "slot", "answer"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, str):
|
||||
slot = normalize_visual_slot(value)
|
||||
if slot:
|
||||
return slot
|
||||
return normalize_visual_slot(stripped)
|
||||
|
||||
|
||||
def extract_hashhop_target(text: str) -> str | None:
|
||||
"""Read the id the model answered with, from JSON or from loose prose."""
|
||||
stripped = text.strip()
|
||||
try:
|
||||
payload = json.loads(stripped)
|
||||
except json.JSONDecodeError:
|
||||
payload = extract_json_object(stripped)
|
||||
if isinstance(payload, dict):
|
||||
for key in ("answer", "target", "target_id", "final_target_id"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, str):
|
||||
candidate = normalize_hash_id(value)
|
||||
if candidate:
|
||||
return candidate
|
||||
match = re.search(r"\b[a-fA-F0-9]{32}\b", stripped)
|
||||
return match.group(0).lower() if match else None
|
||||
|
||||
|
||||
def normalize_hash_id(value: str) -> str | None:
|
||||
match = re.search(r"\b[a-fA-F0-9]{32}\b", value)
|
||||
return match.group(0).lower() if match else None
|
||||
|
||||
|
||||
def extract_visual_intermediate_slot(text: str) -> SlotName | None:
|
||||
stripped = text.strip()
|
||||
try:
|
||||
parsed = json.loads(stripped)
|
||||
payload = parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
payload = extract_json_object(stripped)
|
||||
if isinstance(payload, dict):
|
||||
for key in ("intermediate_slot", "middle_slot", "first_hop_slot"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, str):
|
||||
slot = normalize_visual_slot(value)
|
||||
if slot:
|
||||
return slot
|
||||
return None
|
||||
|
||||
|
||||
def extract_json_object(text: str) -> dict[str, Any] | None:
|
||||
start = text.find("{")
|
||||
end = text.rfind("}")
|
||||
if start < 0 or end <= start:
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(text[start : end + 1])
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
|
||||
|
||||
def normalize_visual_slot(value: str) -> SlotName | None:
|
||||
normalized = re.sub(r"[^a-z]+", "-", value.lower()).strip("-")
|
||||
aliases: dict[str, SlotName] = {
|
||||
"top-left": "top-left",
|
||||
"upper-left": "top-left",
|
||||
"left-top": "top-left",
|
||||
"top-right": "top-right",
|
||||
"upper-right": "top-right",
|
||||
"right-top": "top-right",
|
||||
"bottom-left": "bottom-left",
|
||||
"lower-left": "bottom-left",
|
||||
"left-bottom": "bottom-left",
|
||||
"bottom-right": "bottom-right",
|
||||
"lower-right": "bottom-right",
|
||||
"right-bottom": "bottom-right",
|
||||
}
|
||||
if normalized in aliases:
|
||||
return aliases[normalized]
|
||||
for alias, slot in aliases.items():
|
||||
if re.search(rf"\b{re.escape(alias)}\b", normalized):
|
||||
return slot
|
||||
return None
|
||||
@@ -0,0 +1,404 @@
|
||||
"""Cartridge assets, the memory capsule, replay, DNA, cache trace, and repository coverage.
|
||||
|
||||
Moved out of suite.py so the suite order stays readable; each method is a check the
|
||||
suite runs, and the suite class mixes these in.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import secrets
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..cache_fabric import CacheFabric
|
||||
from ..cartridge import CartridgeService
|
||||
from ..cartridge_assets import CartridgeAssetService, merkle_root
|
||||
from ..compiler import ContextCompiler
|
||||
from ..config import Settings
|
||||
from ..db import Database
|
||||
from ..evaluation_schemas import (
|
||||
ProofCheck,
|
||||
)
|
||||
from ..ingestion import SUPPORTED_CODE_EXTENSIONS, build_repository_context, find_git_root
|
||||
from ..schemas import (
|
||||
CartridgeManifest,
|
||||
RuntimeProfile,
|
||||
)
|
||||
|
||||
# A fixed puzzle count per hop depth. The old suite re-rolled until the running rate crossed its
|
||||
from .checks_common import (
|
||||
_semantic_counters,
|
||||
)
|
||||
from .report import (
|
||||
compiled_context_digest,
|
||||
parquet_row_count,
|
||||
read_memory_capsule,
|
||||
validated_segment_hash,
|
||||
write_json,
|
||||
)
|
||||
from .repository_checks import discover_repository_files, project_root
|
||||
|
||||
|
||||
class CartridgeChecks:
|
||||
"""Cartridge assets, the memory capsule, replay, DNA, cache trace, and repository coverage."""
|
||||
|
||||
settings: Settings
|
||||
db: Database
|
||||
cartridges: CartridgeService
|
||||
|
||||
def _cartridge_assets_check(
|
||||
self,
|
||||
cartridge_id: str,
|
||||
cartridge_root: Path,
|
||||
proof_root: Path,
|
||||
) -> tuple[ProofCheck, CartridgeManifest]:
|
||||
report = CartridgeAssetService(self.settings, self.db).finalize(cartridge_id)
|
||||
source_report = cartridge_root / "cartridge_assets.json"
|
||||
proof_report = proof_root / "cartridge_assets.json"
|
||||
shutil.copy2(source_report, proof_report)
|
||||
rows = self.db.list_cartridges()
|
||||
manifest_json = next(row["manifest_json"] for row in rows if row["cartridge_id"] == cartridge_id)
|
||||
manifest = CartridgeManifest.model_validate_json(manifest_json)
|
||||
expected = {
|
||||
"text_vectors",
|
||||
"visual_vectors",
|
||||
"kv_snapshots",
|
||||
"memory_capsule",
|
||||
"memory_capsule_index",
|
||||
"ttt_vl_adapter",
|
||||
"visual_mtp_head",
|
||||
"cartridge_assets",
|
||||
}
|
||||
by_name = {asset.name: asset for asset in report.assets}
|
||||
visual_vectors = by_name.get("visual_vectors")
|
||||
memory_capsule = by_name.get("memory_capsule")
|
||||
gaps: list[str] = []
|
||||
if report.status != "ready":
|
||||
gaps.append(f"the asset report says {report.status}")
|
||||
absent = sorted(expected - set(manifest.artifacts))
|
||||
if absent:
|
||||
gaps.append("not registered in the manifest: " + ", ".join(absent))
|
||||
empty = [asset.name for asset in report.assets if asset.size_bytes <= 0]
|
||||
if empty:
|
||||
gaps.append("zero-byte assets: " + ", ".join(empty))
|
||||
if visual_vectors is None or visual_vectors.item_count <= 0:
|
||||
gaps.append("visual_vectors holds no vectors")
|
||||
if memory_capsule is None or memory_capsule.item_count <= 0:
|
||||
gaps.append("memory_capsule holds no segments")
|
||||
valid = not gaps
|
||||
return (
|
||||
ProofCheck(
|
||||
name="Cartridge Physical Assets",
|
||||
requirement="PRD 8.3 requires text vectors, non-empty visual vectors, KV snapshots, a 100M memory capsule, TTT-VL adapter weights, Visual-MTP head weights, and eval proofs inside the Context Cartridge.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(proof_report), str(cartridge_root)],
|
||||
detail="materialized assets: " + ", ".join(asset.name for asset in report.assets),
|
||||
reason="; ".join(gaps),
|
||||
rerun="ely-eye finalize-cartridge <id>, then ely-eye proof-suite --cartridge-id <id>",
|
||||
),
|
||||
manifest,
|
||||
)
|
||||
|
||||
def _memory_capsule_check(self, cartridge_root: Path, proof_root: Path) -> ProofCheck:
|
||||
capsule_path = cartridge_root / "memory_capsule.json.zst"
|
||||
index_path = cartridge_root / "memory_capsule_index.json"
|
||||
artifact = proof_root / "memory_capsule.json"
|
||||
payload: dict[str, Any] = {
|
||||
"capsule_path": str(capsule_path),
|
||||
"index_path": str(index_path),
|
||||
"status": "failed",
|
||||
}
|
||||
valid = False
|
||||
try:
|
||||
capsule = read_memory_capsule(capsule_path)
|
||||
index = json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else {}
|
||||
raw_segments = capsule.get("segments")
|
||||
if not isinstance(raw_segments, list):
|
||||
raise ValueError(f"{capsule_path}: the memory capsule carries no segments list") # noqa: TRY004 # malformed stored data, not a caller type error
|
||||
segments: list[dict[str, Any]] = [segment for segment in raw_segments if isinstance(segment, dict)]
|
||||
if len(segments) != len(raw_segments):
|
||||
raise ValueError(f"{capsule_path}: a memory capsule segment is not an object")
|
||||
segment_hashes = [validated_segment_hash(segment) for segment in segments]
|
||||
token_total = sum(int(segment.get("token_count") or 0) for segment in segments)
|
||||
computed_merkle = merkle_root(segment_hashes)
|
||||
valid = (
|
||||
capsule.get("format") == "ely-eye-memory-capsule-v1"
|
||||
and int(capsule.get("token_equivalent") or 0) >= self.settings.library_target_tokens
|
||||
and int(capsule.get("segment_count") or 0) == len(segments)
|
||||
and token_total == int(capsule.get("token_equivalent") or -1)
|
||||
and computed_merkle == capsule.get("segment_merkle_root")
|
||||
and index.get("segment_merkle_root") == capsule.get("segment_merkle_root")
|
||||
and int(index.get("token_equivalent") or 0) == int(capsule.get("token_equivalent") or -1)
|
||||
and int(capsule.get("source_atom_count") or 0) > 0
|
||||
and all(segment_hashes)
|
||||
)
|
||||
payload.update(
|
||||
{
|
||||
"status": "passed" if valid else "failed",
|
||||
"token_equivalent": capsule.get("token_equivalent"),
|
||||
"segment_count": capsule.get("segment_count"),
|
||||
"source_atom_count": capsule.get("source_atom_count"),
|
||||
"source_token_equivalent": capsule.get("source_token_equivalent"),
|
||||
"segment_merkle_root": capsule.get("segment_merkle_root"),
|
||||
"computed_merkle_root": computed_merkle,
|
||||
"first_segment": segments[0] if segments else None,
|
||||
"last_segment": segments[-1] if segments else None,
|
||||
}
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
payload["error"] = str(exc)
|
||||
write_json(artifact, payload)
|
||||
return ProofCheck(
|
||||
name="100M Memory Capsule",
|
||||
requirement="PRD 2.2, 7.3, 8.3, and 14.3 require a 10M-100M token-equivalent Context Cartridge memory capsule with verifiable retrieval commitments.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact), str(capsule_path), str(index_path)],
|
||||
detail=(
|
||||
f"{payload.get('token_equivalent')} token-equivalent memory capsule with "
|
||||
f"{payload.get('segment_count')} committed segments"
|
||||
),
|
||||
)
|
||||
|
||||
def _cartridge_replay_check(
|
||||
self,
|
||||
cartridge_id: str,
|
||||
cartridge_root: Path,
|
||||
manifest: CartridgeManifest,
|
||||
proof_root: Path,
|
||||
) -> ProofCheck:
|
||||
atoms_path = cartridge_root / manifest.artifacts["atoms"]
|
||||
sources_path = cartridge_root / manifest.artifacts["sources"]
|
||||
sparse_path = cartridge_root / manifest.artifacts["sparse_index"]
|
||||
graph_path = cartridge_root / manifest.artifacts["temporal_graph"]
|
||||
computed_dna = self.cartridges.compute_dna(cartridge_root)
|
||||
atom_rows = parquet_row_count(atoms_path)
|
||||
try:
|
||||
sources = json.loads(sources_path.read_text(encoding="utf-8")) if sources_path.exists() else []
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"{sources_path}: the cartridge source list is unreadable ({error})") from error
|
||||
question = "Which Ely-Eye PRD requirements prove long context memory, HashHop, and adapters?"
|
||||
compiler = ContextCompiler(self.settings, self.db)
|
||||
artifact = proof_root / "cartridge_replay.json"
|
||||
# The replay must come out of the cartridge, not out of whatever the live store happens
|
||||
# to hold: mounting is what makes the cartridge the retrieval scope (retrieval scopes by
|
||||
# the mounted set), so an unmounted replay would prove only that compiling is stable.
|
||||
try:
|
||||
self.cartridges.mount(cartridge_id)
|
||||
except ValueError as error:
|
||||
# The KV fingerprint gate refuses to mount a cartridge whose model snapshot this
|
||||
# machine cannot read. A missing input is not a failed claim, so the check reports
|
||||
# the refusal instead of crashing the suite.
|
||||
write_json(artifact, {"verdict": "deferred", "why": str(error)})
|
||||
return ProofCheck(
|
||||
name="Context Cartridge Replay",
|
||||
requirement="PRD 14.2 requires the same cartridge to load reproducibly and produce consistent replay evidence.",
|
||||
status="deferred",
|
||||
evidence=[str(artifact), str(cartridge_root / "manifest.json")],
|
||||
detail=str(error),
|
||||
reason="the cartridge records a model snapshot this machine cannot verify, so the replay cannot be attempted",
|
||||
rerun="set ELY_EYE_MODEL_DIR to the snapshot the cartridge was built against, then ely-eye proof-suite --cartridge-id <id>",
|
||||
)
|
||||
try:
|
||||
replay_a = compiler.compile(question, RuntimeProfile.library_100m)
|
||||
replay_b = compiler.compile(question, RuntimeProfile.library_100m)
|
||||
finally:
|
||||
self.cartridges.unmount(cartridge_id)
|
||||
digest_a = compiled_context_digest(replay_a)
|
||||
digest_b = compiled_context_digest(replay_b)
|
||||
payload = {
|
||||
"cartridge_id": cartridge_id,
|
||||
"manifest_atom_count": manifest.atom_count,
|
||||
"parquet_atom_count": atom_rows,
|
||||
"manifest_source_count": manifest.source_count,
|
||||
"source_count": len(sources),
|
||||
"dna_manifest": manifest.dna,
|
||||
"dna_computed": computed_dna,
|
||||
"artifacts": {
|
||||
"atoms": str(atoms_path),
|
||||
"sources": str(sources_path),
|
||||
"sparse_index": str(sparse_path),
|
||||
"temporal_graph": str(graph_path),
|
||||
},
|
||||
"replay": {
|
||||
"question": question,
|
||||
"digest_a": digest_a,
|
||||
"digest_b": digest_b,
|
||||
"hit_ids_a": [hit.atom.atom_id for hit in replay_a.hits],
|
||||
"hit_ids_b": [hit.atom.atom_id for hit in replay_b.hits],
|
||||
"cache_trace_a": replay_a.cache_trace_id,
|
||||
"cache_trace_b": replay_b.cache_trace_id,
|
||||
},
|
||||
}
|
||||
artifact = proof_root / "cartridge_replay.json"
|
||||
write_json(artifact, payload)
|
||||
cartridge_sources = {str(entry.get("source_id", "")) for entry in sources}
|
||||
answered_from_cartridge = bool(replay_a.hits) and all(
|
||||
hit.atom.source_id in cartridge_sources for hit in replay_a.hits
|
||||
)
|
||||
valid = (
|
||||
atoms_path.exists()
|
||||
and sources_path.exists()
|
||||
and sparse_path.exists()
|
||||
and graph_path.exists()
|
||||
and atom_rows == manifest.atom_count
|
||||
and len(sources) == manifest.source_count
|
||||
and computed_dna == manifest.dna
|
||||
and answered_from_cartridge
|
||||
and digest_a == digest_b
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Context Cartridge Replay",
|
||||
requirement="PRD 14.2 requires the same cartridge to load reproducibly and produce consistent replay evidence.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact), str(cartridge_root / "manifest.json")],
|
||||
detail=(
|
||||
f"{atom_rows} atoms, {len(sources)} sources, replay digest {digest_a}, "
|
||||
f"{len(replay_a.hits)} hits answered from the mounted cartridge"
|
||||
),
|
||||
rerun="ely-eye proof-suite --cartridge-id <id>",
|
||||
)
|
||||
|
||||
def _repository_input_check(self, proof_root: Path) -> ProofCheck:
|
||||
root = project_root()
|
||||
files = discover_repository_files(root)
|
||||
code_files = [path for path in files if path.suffix.lower() in SUPPORTED_CODE_EXTENSIONS]
|
||||
context = build_repository_context(root, files, code_files, find_git_root(root))
|
||||
artifact = proof_root / "repository_input.json"
|
||||
write_json(artifact, context)
|
||||
call_graph = context["code"]["call_graph"]
|
||||
git_context = context["git"]
|
||||
issue_pr_history = context["issue_pr_history"]
|
||||
# Building a context object from files on disk is not evidence the repository reached the
|
||||
# store: the claim is that the repository is queryable, so the store has to hold it.
|
||||
stored_sources = [source for source in self.db.list_sources() if source.path.startswith(str(root))]
|
||||
stored_atoms = self.db.atom_count()
|
||||
artifact = proof_root / "repository_input.json"
|
||||
payload = dict(context)
|
||||
payload["store_evidence"] = {
|
||||
"sources_under_project_root": len(stored_sources),
|
||||
"source_ids": [source.source_id for source in stored_sources[:20]],
|
||||
"atoms_in_store": stored_atoms,
|
||||
}
|
||||
write_json(artifact, payload)
|
||||
valid = (
|
||||
context["structure"]["indexed_file_count"] > 0
|
||||
and context["code"]["indexed_file_count"] > 0
|
||||
and bool(call_graph)
|
||||
and bool(git_context.get("available"))
|
||||
and bool(git_context.get("recent_commits"))
|
||||
and issue_pr_history.get("source") == "local_git_commit_history"
|
||||
and bool(stored_sources)
|
||||
and stored_atoms > 0
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Repository Input Coverage",
|
||||
requirement="PRD 10.1 requires code repository structure indexing, call graph extraction, and issue/PR history association.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact)],
|
||||
detail=(
|
||||
f"{context['structure']['indexed_file_count']} files indexed; "
|
||||
f"{context['code']['indexed_file_count']} code files; "
|
||||
f"{len(call_graph)} call edges; "
|
||||
f"{len(git_context.get('recent_commits') or [])} git commits; "
|
||||
f"{len(stored_sources)} sources and {stored_atoms} atoms in the store"
|
||||
),
|
||||
reason="" if valid else "the repository context was built but the store holds no atoms for it",
|
||||
rerun="ely-eye ingest . && ely-eye proof-suite --cartridge-id <id>",
|
||||
)
|
||||
|
||||
def _memory_dna_check(
|
||||
self,
|
||||
cartridge_root: Path,
|
||||
manifest: CartridgeManifest,
|
||||
proof_root: Path,
|
||||
) -> ProofCheck:
|
||||
computed = self.cartridges.compute_dna(cartridge_root)
|
||||
payload = {
|
||||
"manifest_dna": manifest.dna,
|
||||
"computed_dna": computed,
|
||||
"formula": "sha256(sorted cartridge artifacts excluding manifest.json)",
|
||||
"artifact_count": sum(1 for path in cartridge_root.rglob("*") if path.is_file()),
|
||||
}
|
||||
artifact = proof_root / "memory_dna.json"
|
||||
write_json(artifact, payload)
|
||||
return ProofCheck(
|
||||
name="Memory DNA",
|
||||
requirement="PRD 11.14 requires a reproducible Memory DNA over adapter delta, evidence graph, retrieval index, and eval proofs.",
|
||||
status="passed" if manifest.dna == computed else "failed",
|
||||
evidence=[str(artifact), str(cartridge_root)],
|
||||
detail=f"manifest DNA {manifest.dna}",
|
||||
)
|
||||
|
||||
def _cache_trace_check(self, proof_root: Path) -> ProofCheck:
|
||||
"""Ask the cache the same question twice and require the second answer to be a hit.
|
||||
|
||||
The old check passed when any layer had any row, which a single `forget` could satisfy.
|
||||
The claim in PRD 14.2 is that hit, miss, write and replay state are visible, so the check
|
||||
compiles a question nobody has asked, compiles it again, and requires the semantic layer's
|
||||
counters to move by exactly one miss then one hit.
|
||||
"""
|
||||
compiler = ContextCompiler(self.settings, self.db)
|
||||
fabric = CacheFabric(self.settings, self.db)
|
||||
probe = self.db.list_atoms(limit=1)
|
||||
if not probe:
|
||||
artifact = proof_root / "cache_trace.json"
|
||||
write_json(artifact, {"verdict": "failed", "why": "the store holds no atoms"})
|
||||
return ProofCheck(
|
||||
name="Cache Trace",
|
||||
requirement="PRD 14.2 requires cache and memory proof with visible hit, miss, write, and replay state.",
|
||||
status="failed",
|
||||
evidence=[str(artifact)],
|
||||
detail="the store holds no atoms, so there is nothing for the cache to answer",
|
||||
reason="an empty store answers every question with an empty context, which would make the two compiles agree for the wrong reason",
|
||||
rerun="ely-eye remember or ely-eye ingest first, then re-run this proof",
|
||||
)
|
||||
# The nonce keeps the key unique; the quoted text keeps the question answerable, so a pass
|
||||
# means the cache served a real context rather than two identical empty ones.
|
||||
question = f"{probe[0].text[:60]} cache proof {secrets.token_hex(6)}"
|
||||
before = _semantic_counters(fabric)
|
||||
first = compiler.compile(question, RuntimeProfile.library_100m)
|
||||
middle = _semantic_counters(fabric)
|
||||
second = compiler.compile(question, RuntimeProfile.library_100m)
|
||||
after = _semantic_counters(fabric)
|
||||
digest_first = compiled_context_digest(first)
|
||||
digest_second = compiled_context_digest(second)
|
||||
payload = {
|
||||
"question": question,
|
||||
"counters": {"before": before, "after_first": middle, "after_second": after},
|
||||
"digest_first": digest_first,
|
||||
"digest_second": digest_second,
|
||||
"cache_trace_first": first.cache_trace_id,
|
||||
"cache_trace_second": second.cache_trace_id,
|
||||
}
|
||||
artifact = proof_root / "cache_trace.json"
|
||||
write_json(artifact, payload)
|
||||
miss_recorded = middle["miss"] - before["miss"] == 1 and middle["hit"] - before["hit"] == 0
|
||||
hit_recorded = after["hit"] - middle["hit"] == 1
|
||||
same_answer = digest_first == digest_second and bool(first.hits)
|
||||
valid = miss_recorded and hit_recorded and same_answer
|
||||
return ProofCheck(
|
||||
name="Cache Trace",
|
||||
requirement="PRD 14.2 requires cache and memory proof with visible hit, miss, write, and replay state.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact)],
|
||||
detail=(
|
||||
f"semantic layer counters miss {before['miss']}→{middle['miss']}→{after['miss']}, "
|
||||
f"hit {before['hit']}→{middle['hit']}→{after['hit']}, replay digest {digest_first}"
|
||||
),
|
||||
reason="" if valid else "the second compile was not recorded as a cache hit",
|
||||
rerun="ely-eye proof-suite --cartridge-id <id>",
|
||||
)
|
||||
|
||||
def _resolve_cartridge(self, cartridge_id: str | None) -> dict[str, Any]:
|
||||
rows = self.db.list_cartridges()
|
||||
if not rows:
|
||||
raise ValueError("No Context Cartridge is available.")
|
||||
if cartridge_id is None:
|
||||
return rows[0]
|
||||
for row in rows:
|
||||
if row["cartridge_id"] == cartridge_id:
|
||||
return row
|
||||
raise ValueError(f"Unknown cartridge: {cartridge_id}")
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Shared verdict helpers for the proof checks.
|
||||
|
||||
Kept out of suite.py so the check modules can import them without importing the suite.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..cache_fabric import SEMANTIC_LAYER, CacheFabric
|
||||
from ..config import Settings
|
||||
from ..evaluation_schemas import (
|
||||
ProofCheck,
|
||||
)
|
||||
from .report import (
|
||||
write_json,
|
||||
)
|
||||
|
||||
# A fixed puzzle count per hop depth. The old suite re-rolled until the running rate crossed its
|
||||
|
||||
HASHHOP_ATTEMPTS = 8
|
||||
|
||||
GOLD_ADAPTERS = (
|
||||
"HME-Core-LoRA",
|
||||
"HME-Vision-LoRA",
|
||||
"HME-TTT-VL",
|
||||
"HME-Visual-MTP",
|
||||
"HME-Router",
|
||||
"HME-Retrieval",
|
||||
)
|
||||
|
||||
|
||||
def _deferred_without_model(
|
||||
settings: Settings, artifact: Path, name: str, requirement: str, rerun: str
|
||||
) -> ProofCheck | None:
|
||||
"""A check that needs weights proves nothing on a machine that has no weights.
|
||||
|
||||
Calling that a failure would blame the machine; calling it a pass would be a lie. The suite
|
||||
records what is missing and the command that runs the check once the weights arrive.
|
||||
"""
|
||||
model_dir = settings.model_dir
|
||||
if model_dir is not None and model_dir.exists():
|
||||
return None
|
||||
write_json(
|
||||
artifact,
|
||||
{"status": "deferred", "reason": "no local model weights", "model_dir": str(model_dir)},
|
||||
)
|
||||
return ProofCheck(
|
||||
name=name,
|
||||
requirement=requirement,
|
||||
status="deferred",
|
||||
evidence=[str(artifact)],
|
||||
detail=f"model_dir={model_dir}",
|
||||
reason="the generator or encoder this check measures is not installed on this machine",
|
||||
rerun=rerun,
|
||||
)
|
||||
|
||||
|
||||
def _semantic_counters(fabric: CacheFabric) -> dict[str, int]:
|
||||
"""Hit and miss counts for the one cache layer that exists; zero when the layer has no rows."""
|
||||
for layer in fabric.statuses():
|
||||
if layer.layer == SEMANTIC_LAYER:
|
||||
return {"hit": layer.hit_count, "miss": layer.miss_count}
|
||||
return {"hit": 0, "miss": 0}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
"""Runtime adapter binding, gold-adapter generation, launch profiles, and the removed module check.
|
||||
|
||||
Moved out of suite.py so the suite order stays readable; each method is a check the
|
||||
suite runs, and the suite class mixes these in.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..compiler import ContextCompiler
|
||||
from ..config import Settings
|
||||
from ..db import Database
|
||||
from ..evaluation_schemas import (
|
||||
ProofCheck,
|
||||
)
|
||||
from ..evidence_graph import EvidenceGraphService
|
||||
from ..profiles import runtime_profile_report
|
||||
from ..runtime import QwenRuntime, resolve_runtime_adapter
|
||||
from ..schemas import (
|
||||
RuntimeProfile,
|
||||
TrainingAdapterKind,
|
||||
)
|
||||
from ..verifier import Verifier
|
||||
|
||||
# A fixed puzzle count per hop depth. The old suite re-rolled until the running rate crossed its
|
||||
from .checks_common import (
|
||||
GOLD_ADAPTERS,
|
||||
_deferred_without_model,
|
||||
)
|
||||
from .report import (
|
||||
compact_generation_context,
|
||||
write_json,
|
||||
)
|
||||
from .training_trace import (
|
||||
count_safetensor_keys,
|
||||
peft_config_loads,
|
||||
)
|
||||
|
||||
|
||||
class RuntimeChecks:
|
||||
"""Runtime adapter binding, gold-adapter generation, launch profiles, and the removed module check."""
|
||||
|
||||
settings: Settings
|
||||
db: Database
|
||||
|
||||
def _runtime_adapter_check(self, proof_root: Path) -> ProofCheck:
|
||||
artifact = proof_root / "runtime_adapter.json"
|
||||
deferred = _deferred_without_model(
|
||||
self.settings,
|
||||
artifact,
|
||||
"Runtime Adapter Binding",
|
||||
"PRD 18 requires the local model package to run with the trained adapter, and PRD 12.1 requires HME-Core-LoRA to be usable by the Qwen runtime.",
|
||||
"ely-eye runtime-generation-proof with ELY_EYE_MODEL_DIR set",
|
||||
)
|
||||
if deferred is not None:
|
||||
return deferred
|
||||
runtime = QwenRuntime(self.settings)
|
||||
status = runtime.status()
|
||||
adapter = resolve_runtime_adapter(self.settings)
|
||||
weight_path = adapter.path / "adapter_model.safetensors" if adapter else None
|
||||
payload = {
|
||||
"runtime_status": status.model_dump(mode="json"),
|
||||
"resolved_adapter": {
|
||||
"path": str(adapter.path) if adapter else None,
|
||||
"kind": adapter.manifest.kind.value if adapter else None,
|
||||
"adapter_id": adapter.manifest.adapter_id if adapter else None,
|
||||
"sha256": adapter.manifest.sha256 if adapter else None,
|
||||
"base_model": adapter.manifest.base_model if adapter else None,
|
||||
"weight_path": str(weight_path) if weight_path else None,
|
||||
"safetensor_keys": count_safetensor_keys(weight_path) if weight_path else 0,
|
||||
"peft_config_ok": peft_config_loads(adapter.path) if adapter else False,
|
||||
},
|
||||
}
|
||||
artifact = proof_root / "runtime_adapter.json"
|
||||
write_json(artifact, payload)
|
||||
valid = (
|
||||
status.available
|
||||
and adapter is not None
|
||||
and adapter.manifest.kind == TrainingAdapterKind.hme_core_lora
|
||||
and adapter.manifest.base_model == self.settings.model_id
|
||||
and status.adapter_path == str(adapter.path)
|
||||
and weight_path is not None
|
||||
and count_safetensor_keys(weight_path) > 0
|
||||
and peft_config_loads(adapter.path)
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Runtime Adapter Binding",
|
||||
requirement="PRD 18 requires the local model package to run with the trained adapter, and PRD 12.1 requires HME-Core-LoRA to be usable by the Qwen runtime.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact)],
|
||||
detail=(
|
||||
f"{status.backend} runtime resolves {adapter.manifest.kind.value} at {adapter.path}"
|
||||
if adapter
|
||||
else "runtime adapter resolution is empty"
|
||||
),
|
||||
)
|
||||
|
||||
def _runtime_generation_check(self, proof_root: Path) -> ProofCheck:
|
||||
artifact = proof_root / "runtime_generation.json"
|
||||
deferred = _deferred_without_model(
|
||||
self.settings,
|
||||
artifact,
|
||||
"Runtime Adapter Generation",
|
||||
"PRD 12.1 and PRD 18 require the trained HME-Core-LoRA adapter to be loaded by the local Qwen runtime and used for an evidence-cited generation.",
|
||||
"ely-eye proof-suite --cartridge-id <id> with ELY_EYE_MODEL_DIR set",
|
||||
)
|
||||
if deferred is not None:
|
||||
return deferred
|
||||
retrieval_question = (
|
||||
"Using the Ely-Eye PRD evidence, state the PRD 12.1 local training adapter "
|
||||
"requirements for HME-Core-LoRA, HME-Vision-LoRA, HME-TTT-VL, "
|
||||
"HME-Visual-MTP, HME-Router, and HME-Retrieval. Return JSON with atom id "
|
||||
"citations."
|
||||
)
|
||||
proof_settings = self.settings.model_copy(update={"max_new_tokens": 256, "temperature": 0.0})
|
||||
runtime = QwenRuntime(proof_settings)
|
||||
payload: dict[str, Any] = {
|
||||
"question": retrieval_question,
|
||||
"status": "failed",
|
||||
}
|
||||
try:
|
||||
compiler = ContextCompiler(proof_settings, self.db)
|
||||
context = compiler.compile(retrieval_question, RuntimeProfile.library_100m)
|
||||
context = compact_generation_context(context, "12.1")
|
||||
if not context.hits:
|
||||
raise RuntimeError("No PRD text evidence was available for runtime generation proof.")
|
||||
generation_question = (
|
||||
"According to the retrieved PRD evidence, which local training adapters does "
|
||||
"PRD 12.1 require? Answer with one compact JSON object: a \"citations\" field "
|
||||
"naming the atom ids you actually used, and a \"sentence\" field listing them."
|
||||
)
|
||||
payload["generation_question"] = generation_question
|
||||
status = runtime.status()
|
||||
adapter = resolve_runtime_adapter(proof_settings)
|
||||
generation = runtime.generate(generation_question, context)
|
||||
answer, citations, citation_report = Verifier().verify_answer(generation.text, context)
|
||||
claim_id = EvidenceGraphService(proof_settings, self.db).record_answer(
|
||||
generation_question,
|
||||
answer,
|
||||
citation_report,
|
||||
context,
|
||||
)
|
||||
payload.update(
|
||||
{
|
||||
"status": "passed",
|
||||
"runtime_status": status.model_dump(mode="json"),
|
||||
"adapter": {
|
||||
"path": generation.adapter_path,
|
||||
"kind": generation.adapter_kind,
|
||||
"adapter_id": generation.adapter_id,
|
||||
"sha256": generation.adapter_sha256,
|
||||
},
|
||||
"tokens": {
|
||||
"input": generation.input_tokens,
|
||||
"output": generation.output_tokens,
|
||||
},
|
||||
"context": {
|
||||
"profile": context.plan.profile.value,
|
||||
"hit_ids": [hit.atom.atom_id for hit in context.hits],
|
||||
"token_equivalent": context.token_equivalent,
|
||||
"cache_trace_id": context.cache_trace_id,
|
||||
},
|
||||
"raw_generation": generation.text,
|
||||
"verified_answer": answer,
|
||||
"citations": citations,
|
||||
"evidence_graph_claim_id": claim_id,
|
||||
"verifier": citation_report.model_dump(mode="json"),
|
||||
}
|
||||
)
|
||||
named = [name for name in GOLD_ADAPTERS if name in answer]
|
||||
cited_atoms = [
|
||||
hit.atom for hit in context.hits if hit.atom.atom_id in set(citations)
|
||||
]
|
||||
supported = [atom for atom in cited_atoms if sum(name in (atom.text or "") for name in GOLD_ADAPTERS) >= 3]
|
||||
valid = (
|
||||
status.available
|
||||
and generation.backend == "transformers"
|
||||
and adapter is not None
|
||||
and generation.adapter_path == str(adapter.path)
|
||||
and generation.adapter_kind == TrainingAdapterKind.hme_core_lora.value
|
||||
and generation.adapter_sha256 == adapter.manifest.sha256
|
||||
and bool(context.hits)
|
||||
and bool(generation.text.strip())
|
||||
and (generation.output_tokens or 0) > 0
|
||||
and len(named) >= 5
|
||||
and bool(citations)
|
||||
and citation_report.citation_accuracy == 1.0
|
||||
and bool(supported)
|
||||
)
|
||||
payload["answer_adapters"] = named
|
||||
payload["cited_atoms"] = [atom.atom_id for atom in cited_atoms]
|
||||
payload["supporting_citations"] = [atom.atom_id for atom in supported]
|
||||
if not valid:
|
||||
payload["status"] = "failed"
|
||||
write_json(artifact, payload)
|
||||
return ProofCheck(
|
||||
name="Runtime Adapter Generation",
|
||||
requirement="PRD 12.1 and PRD 18 require the trained HME-Core-LoRA adapter to be loaded by the local Qwen runtime and used for an evidence-cited generation.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact)],
|
||||
detail=(
|
||||
f"{generation.backend} generated {generation.output_tokens} tokens with "
|
||||
f"{generation.adapter_kind} citation accuracy "
|
||||
f"{citation_report.citation_accuracy:.2f}"
|
||||
),
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
payload["error"] = str(exc)
|
||||
write_json(artifact, payload)
|
||||
return ProofCheck(
|
||||
name="Runtime Adapter Generation",
|
||||
requirement="PRD 12.1 and PRD 18 require the trained HME-Core-LoRA adapter to be loaded by the local Qwen runtime and used for an evidence-cited generation.",
|
||||
status="failed",
|
||||
evidence=[str(artifact)],
|
||||
detail=f"runtime generation failed: {exc}",
|
||||
)
|
||||
finally:
|
||||
runtime.unload()
|
||||
|
||||
def _runtime_profile_check(self, proof_root: Path) -> ProofCheck:
|
||||
report = runtime_profile_report()
|
||||
artifact = proof_root / "runtime_profiles.json"
|
||||
artifact.write_text(report.model_dump_json(indent=2), encoding="utf-8")
|
||||
required_runtimes = {"SGLang", "vLLM", "KTransformers", "llama.cpp"}
|
||||
runtimes = {profile.runtime for profile in report.profiles}
|
||||
gaps = [f"{item.name}: {item.detail}" for item in report.validations if item.status == "missing"]
|
||||
unregistered = sorted(required_runtimes - runtimes)
|
||||
if unregistered:
|
||||
gaps.append("no profile registered for: " + ", ".join(unregistered))
|
||||
if not gaps and report.status != "ready":
|
||||
gaps.append(f"the profile report says {report.status}")
|
||||
valid = not gaps
|
||||
return ProofCheck(
|
||||
name="Runtime Profile Delivery",
|
||||
requirement="PRD 13.1 requires SGLang as the main runtime plus vLLM, KTransformers, and llama.cpp deployment profiles.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact)],
|
||||
detail="registered runtimes: " + ", ".join(profile.name for profile in report.profiles),
|
||||
reason="; ".join(gaps),
|
||||
rerun="install the launcher in the environment the profile probes (the probe command is in runtime_profiles.json), then ely-eye runtime-profiles",
|
||||
)
|
||||
|
||||
def _research_module_check(self, proof_root: Path) -> ProofCheck:
|
||||
"""The old version of this check could not fail, so it proved nothing.
|
||||
|
||||
It compiled a query, asked the research-module service for eight module reports, and
|
||||
passed when every list was non-empty. The module vectors it ranks with are SHA256-derived
|
||||
placeholders, so a non-empty list is a property of the loop, not of the system.
|
||||
"""
|
||||
artifact = proof_root / "research_modules.json"
|
||||
write_json(
|
||||
artifact,
|
||||
{
|
||||
"claim": "PRD 11 research modules",
|
||||
"verdict": "removed",
|
||||
"why": "the check asserted non-empty lists built from SHA256-derived placeholder vectors",
|
||||
},
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Research Module Delivery",
|
||||
requirement="PRD 11 requires V-NSA, DVKV, X-Modal KV Sharing, Speculative Context Prefetch, Predictive KV Eviction, Learned Evidence Graph, Differentiable Retrieval Adapter, and Sleep-Time Visual Consolidation.",
|
||||
status="removed",
|
||||
evidence=[str(artifact)],
|
||||
detail="no measurable claim exists per module; the previous check passed on non-empty lists",
|
||||
reason="the check could not fail: eight named modules produced non-empty lists by construction",
|
||||
rerun="write one measurable claim per module, then re-add the check",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"""Adapter training and the learned-memory adapters bound into a cartridge.
|
||||
|
||||
Moved out of suite.py so the suite order stays readable; each method is a check the
|
||||
suite runs, and the suite class mixes these in.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..config import Settings
|
||||
from ..db import Database
|
||||
from ..evaluation_schemas import (
|
||||
ProofCheck,
|
||||
)
|
||||
from ..schemas import (
|
||||
AdapterManifest,
|
||||
TrainingAdapterKind,
|
||||
)
|
||||
from ..training import (
|
||||
safetensor_weight_stats,
|
||||
sha256_file,
|
||||
sha256_tree,
|
||||
)
|
||||
|
||||
# A fixed puzzle count per hop depth. The old suite re-rolled until the running rate crossed its
|
||||
from .report import (
|
||||
read_json_or_empty,
|
||||
write_json,
|
||||
)
|
||||
from .training_trace import (
|
||||
count_safetensor_keys,
|
||||
peft_config_loads,
|
||||
validate_training_contract,
|
||||
validate_training_proof,
|
||||
validate_training_trace,
|
||||
)
|
||||
|
||||
|
||||
class TrainingChecks:
|
||||
"""Adapter training and the learned-memory adapters bound into a cartridge."""
|
||||
|
||||
settings: Settings
|
||||
db: Database
|
||||
required_adapters: tuple[TrainingAdapterKind, ...]
|
||||
|
||||
def _adapter_training_check(self, cartridge_id: str, cartridge_root: Path, proof_root: Path) -> ProofCheck:
|
||||
rows: list[dict[str, Any]] = []
|
||||
missing: list[str] = []
|
||||
manifests = self._adapter_manifests(cartridge_id)
|
||||
for kind in self.required_adapters:
|
||||
item = manifests.get(kind)
|
||||
if item is None:
|
||||
missing.append(f"{kind.value}: no adapter manifest")
|
||||
continue
|
||||
manifest, adapter_root = item
|
||||
weight_path = adapter_root / "adapter_model.safetensors"
|
||||
dataset_path = Path(manifest.dataset_path)
|
||||
cartridge_adapter = cartridge_root / "adapters" / kind.value / "adapter_manifest.json"
|
||||
weight_report = safetensor_weight_stats(weight_path)
|
||||
keys = int(weight_report["tensor_count"])
|
||||
summary = read_json_or_empty(adapter_root / "training_summary.json")
|
||||
summary_weight_sha = summary.get("adapter_weight_sha256")
|
||||
summary_weight_sha_ok = (
|
||||
isinstance(summary_weight_sha, str) and summary_weight_sha == weight_report["sha256"]
|
||||
)
|
||||
manifest_weight_sha_ok = (
|
||||
isinstance(manifest.adapter_weight_sha256, str)
|
||||
and manifest.adapter_weight_sha256 == weight_report["sha256"]
|
||||
)
|
||||
manifest_weight_stats_ok = (
|
||||
manifest.adapter_tensor_count == weight_report["tensor_count"]
|
||||
and manifest.adapter_lora_tensor_count == weight_report["lora_tensor_count"]
|
||||
and manifest.adapter_total_elements == weight_report["total_elements"]
|
||||
and manifest.adapter_nonzero_elements == weight_report["nonzero_elements"]
|
||||
and manifest.adapter_nonzero_elements > 0
|
||||
and manifest.adapter_lora_tensor_count > 0
|
||||
and manifest.adapter_weights_finite
|
||||
)
|
||||
peft_ok = peft_config_loads(adapter_root)
|
||||
dataset_sha_ok = dataset_path.exists() and sha256_file(dataset_path) == manifest.dataset_sha256
|
||||
tree_sha_ok = sha256_tree(adapter_root) == manifest.sha256
|
||||
cartridge_bound = cartridge_adapter.exists()
|
||||
trace_report = validate_training_trace(manifest, adapter_root)
|
||||
proof_report = validate_training_proof(manifest, adapter_root)
|
||||
contract_report = validate_training_contract(manifest)
|
||||
# Named conditions, so a failure can say which promise broke instead of listing the
|
||||
# adapters that happened to be present.
|
||||
conditions: list[tuple[str, bool]] = [
|
||||
("sample_count", manifest.sample_count > 0),
|
||||
("max_steps", manifest.max_steps > 0),
|
||||
("trainable_params", manifest.trainable_params > 0),
|
||||
("final_loss recorded", manifest.final_loss is not None),
|
||||
("safetensors readable", weight_report["valid"]),
|
||||
("weight sha256 matches the training summary", summary_weight_sha_ok),
|
||||
("weight sha256 matches the manifest", manifest_weight_sha_ok),
|
||||
("weight statistics match the manifest", manifest_weight_stats_ok),
|
||||
("peft config loads", peft_ok),
|
||||
("dataset sha256 matches", dataset_sha_ok),
|
||||
("adapter tree sha256 matches", tree_sha_ok),
|
||||
("adapter is bound into the cartridge", cartridge_bound),
|
||||
("training trace verifies", trace_report["valid"]),
|
||||
("training proof verifies", proof_report["valid"]),
|
||||
("training contract verifies", contract_report["valid"]),
|
||||
]
|
||||
valid = all(ok for _, ok in conditions)
|
||||
if not valid:
|
||||
gaps = ", ".join(name for name, ok in conditions if not ok)
|
||||
missing.append(f"{kind.value}: {gaps}")
|
||||
rows.append(
|
||||
{
|
||||
"kind": kind.value,
|
||||
"adapter": str(adapter_root),
|
||||
"dataset": str(dataset_path),
|
||||
"sample_count": manifest.sample_count,
|
||||
"max_steps": manifest.max_steps,
|
||||
"trainable_params": manifest.trainable_params,
|
||||
"final_loss": manifest.final_loss,
|
||||
"safetensor_keys": keys,
|
||||
"adapter_weight_sha256": weight_report["sha256"],
|
||||
"adapter_weight_sha256_ok": summary_weight_sha_ok and manifest_weight_sha_ok,
|
||||
"adapter_weight_stats_ok": manifest_weight_stats_ok,
|
||||
"adapter_weight_stats": weight_report,
|
||||
"dataset_sha256_ok": dataset_sha_ok,
|
||||
"adapter_sha256_ok": tree_sha_ok,
|
||||
"peft_config_ok": peft_ok,
|
||||
"cartridge_bound": cartridge_bound,
|
||||
"training_contract": contract_report,
|
||||
"training_trace": trace_report,
|
||||
"training_proof": proof_report,
|
||||
}
|
||||
)
|
||||
artifact = proof_root / "adapter_training.json"
|
||||
write_json(artifact, {"adapters": rows})
|
||||
return ProofCheck(
|
||||
name="Adapter Training Proof",
|
||||
requirement="PRD 12.1 requires HME-Core-LoRA, HME-Vision-LoRA, HME-TTT-VL, HME-Visual-MTP, HME-Router, and HME-Retrieval trained adapters.",
|
||||
status="failed" if missing else "passed",
|
||||
evidence=[str(artifact), str(self.settings.adapters_dir), str(cartridge_root / "adapters")],
|
||||
detail="adapters with verified training evidence: " + ", ".join(row["kind"] for row in rows),
|
||||
reason="; ".join(missing),
|
||||
rerun="train the missing adapters (ely-eye train-lora) or attach them (ely-eye attach-adapter), then ely-eye proof-suite --cartridge-id <id>",
|
||||
)
|
||||
|
||||
def _learned_memory_recall_check(
|
||||
self,
|
||||
cartridge_id: str,
|
||||
cartridge_root: Path,
|
||||
proof_root: Path,
|
||||
) -> ProofCheck:
|
||||
manifests = self._adapter_manifests(cartridge_id)
|
||||
learned_kinds = (
|
||||
TrainingAdapterKind.hme_ttt_vl,
|
||||
TrainingAdapterKind.hme_core_lora,
|
||||
TrainingAdapterKind.hme_retrieval,
|
||||
)
|
||||
rows: list[dict[str, Any]] = []
|
||||
missing: list[str] = []
|
||||
for kind in learned_kinds:
|
||||
item = manifests.get(kind)
|
||||
if item is None:
|
||||
missing.append(f"{kind.value}: no adapter manifest")
|
||||
continue
|
||||
manifest, adapter_root = item
|
||||
weight_path = adapter_root / "adapter_model.safetensors"
|
||||
cartridge_weight = cartridge_root / "adapters" / kind.value / "adapter_model.safetensors"
|
||||
rows.append(
|
||||
{
|
||||
"kind": kind.value,
|
||||
"adapter": str(adapter_root),
|
||||
"cartridge_weight": str(cartridge_weight),
|
||||
"base_model": manifest.base_model,
|
||||
"dataset_sha256": manifest.dataset_sha256,
|
||||
"sample_count": manifest.sample_count,
|
||||
"trainable_params": manifest.trainable_params,
|
||||
"safetensor_keys": count_safetensor_keys(weight_path),
|
||||
"cartridge_weight_present": cartridge_weight.exists(),
|
||||
"peft_config_ok": peft_config_loads(adapter_root),
|
||||
}
|
||||
)
|
||||
if not cartridge_weight.exists():
|
||||
missing.append(f"{kind.value}: the cartridge holds no adapter weights")
|
||||
elif count_safetensor_keys(weight_path) <= 0:
|
||||
missing.append(f"{kind.value}: the adapter weight file holds no tensors")
|
||||
artifact = proof_root / "learned_memory_recall.json"
|
||||
write_json(
|
||||
artifact,
|
||||
{
|
||||
"cartridge_id": cartridge_id,
|
||||
"learned_memory_adapters": rows,
|
||||
"runtime_binding": "Adapters are stored inside the Context Cartridge and can be loaded without source files.",
|
||||
},
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Learned Memory Recall",
|
||||
requirement="PRD 14.2 and 15.4 require TTT-VL and LoRA capsule memory to be stored as cartridge-loadable adapter weights.",
|
||||
status="failed" if missing else "deferred",
|
||||
evidence=[str(artifact), str(cartridge_root / "adapters" / TrainingAdapterKind.hme_ttt_vl.value)],
|
||||
detail=(
|
||||
"weights load for " + ", ".join(row["kind"] for row in rows)
|
||||
+ "; no recall number was measured"
|
||||
),
|
||||
reason=(
|
||||
"the check loads adapter weights and never asks the model to remember anything, so it "
|
||||
"cannot carry the word recall; the load evidence is real, the recall claim is not yet measured"
|
||||
),
|
||||
rerun="add a recall probe that answers questions with and without the adapter, then ely-eye proof-suite --cartridge-id <id>",
|
||||
)
|
||||
|
||||
def _adapter_manifests(
|
||||
self,
|
||||
cartridge_id: str,
|
||||
) -> dict[TrainingAdapterKind, tuple[AdapterManifest, Path]]:
|
||||
candidates: dict[TrainingAdapterKind, list[tuple[AdapterManifest, Path]]] = {}
|
||||
for path in sorted(self.settings.adapters_dir.glob("*/adapter_manifest.json")):
|
||||
manifest = AdapterManifest.model_validate_json(path.read_text(encoding="utf-8"))
|
||||
candidates.setdefault(manifest.kind, []).append((manifest, path.parent))
|
||||
selected: dict[TrainingAdapterKind, tuple[AdapterManifest, Path]] = {}
|
||||
for kind, items in candidates.items():
|
||||
items.sort(
|
||||
key=lambda item: (
|
||||
item[0].cartridge_id == cartridge_id,
|
||||
(item[1] / "adapter_model.safetensors").stat().st_mtime
|
||||
if (item[1] / "adapter_model.safetensors").exists()
|
||||
else 0.0,
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
selected[kind] = items[0]
|
||||
return selected
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
"""Visual HashHop retrieval, the benchmark registry, and the visual contradiction lens.
|
||||
|
||||
Moved out of suite.py so the suite order stays readable; each method is a check the
|
||||
suite runs, and the suite class mixes these in.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..benchmarks import (
|
||||
benchmark_prediction_report,
|
||||
benchmark_registry_report,
|
||||
benchmark_sample_report,
|
||||
benchmark_source_report,
|
||||
)
|
||||
from ..config import Settings
|
||||
from ..db import Database
|
||||
from ..evaluation_schemas import (
|
||||
ProofCheck,
|
||||
)
|
||||
from ..runtime import QwenRuntime
|
||||
from ..verifier import VisualContradictionLens
|
||||
|
||||
# A fixed puzzle count per hop depth. The old suite re-rolled until the running rate crossed its
|
||||
from .checks_common import (
|
||||
HASHHOP_ATTEMPTS,
|
||||
_deferred_without_model,
|
||||
)
|
||||
from .contradictions import CORPUS_NAME, corpus_path, load_corpus, scenario_context
|
||||
from .hashhop import HashHopEvaluator
|
||||
from .hashhop_verify import (
|
||||
collect_model_visual_proofs,
|
||||
copy_proof_artifacts,
|
||||
hashhop_chain_valid,
|
||||
pass_rate,
|
||||
)
|
||||
from .report import (
|
||||
format_scores,
|
||||
write_json,
|
||||
)
|
||||
|
||||
|
||||
class VisualChecks:
|
||||
"""Visual HashHop retrieval, the benchmark registry, and the visual contradiction lens."""
|
||||
|
||||
settings: Settings
|
||||
db: Database
|
||||
|
||||
def _visual_hashhop_check(self, proof_root: Path) -> ProofCheck:
|
||||
artifact = proof_root / "visual_hashhop_suite.json"
|
||||
deferred = _deferred_without_model(
|
||||
self.settings,
|
||||
artifact,
|
||||
"Visual HashHop Retrieval",
|
||||
"PRD 14.2 requires the model to resolve visual hash hops inside 262K, 1.01M and 100M token contexts.",
|
||||
"ely-eye hashhop visual 1 262144 with ELY_EYE_MODEL_DIR set",
|
||||
)
|
||||
if deferred is not None:
|
||||
return deferred
|
||||
proof_settings = self.settings.model_copy(update={"max_new_tokens": 128, "temperature": 0.0})
|
||||
runtime = QwenRuntime(proof_settings)
|
||||
evaluator = HashHopEvaluator(proof_settings, runtime)
|
||||
try:
|
||||
base_proofs = [
|
||||
evaluator.generate_visual_proof(1, 262_144),
|
||||
evaluator.generate_visual_proof(2, 262_144),
|
||||
evaluator.generate_visual_proof(1, 1_010_000),
|
||||
evaluator.generate_visual_proof(1, 100_000_000),
|
||||
evaluator.generate_text_proof(2, 1_010_000),
|
||||
evaluator.generate_model_text_proof(2, 1_010_000),
|
||||
]
|
||||
model_visual_1hop = collect_model_visual_proofs(evaluator, 1, 262_144, HASHHOP_ATTEMPTS)
|
||||
model_visual_2hop = collect_model_visual_proofs(evaluator, 2, 262_144, HASHHOP_ATTEMPTS)
|
||||
proofs = base_proofs + model_visual_1hop + model_visual_2hop
|
||||
finally:
|
||||
runtime.unload()
|
||||
payloads: list[dict[str, Any]] = []
|
||||
for proof in proofs:
|
||||
target = proof_root / "hashhop" / proof.proof_id
|
||||
copy_proof_artifacts(proof, target)
|
||||
payloads.append(
|
||||
{
|
||||
"proof": proof.model_dump(mode="json"),
|
||||
"chain_valid": hashhop_chain_valid(proof),
|
||||
"artifact_dir": str(target),
|
||||
}
|
||||
)
|
||||
artifact = proof_root / "visual_hashhop_suite.json"
|
||||
model_solved = [
|
||||
item
|
||||
for item in payloads
|
||||
if item["proof"]["model_target_id"] is not None or item["proof"]["passed"] is not None
|
||||
]
|
||||
model_text_solved = [
|
||||
proof
|
||||
for proof in base_proofs
|
||||
if proof.kind == "text_hashhop" and proof.model_target_id is not None
|
||||
]
|
||||
model_solved_passed = [item for item in model_solved if item["proof"]["passed"]]
|
||||
visual_1hop_rate = pass_rate(model_visual_1hop)
|
||||
visual_2hop_rate = pass_rate(model_visual_2hop)
|
||||
metrics = {
|
||||
"visual_hashhop_262k_1hop": {
|
||||
"target": 0.90,
|
||||
"sample_count": len(model_visual_1hop),
|
||||
"passed": sum(1 for proof in model_visual_1hop if proof.passed),
|
||||
"pass_rate": visual_1hop_rate,
|
||||
},
|
||||
"visual_hashhop_262k_2hop": {
|
||||
"target": 0.75,
|
||||
"sample_count": len(model_visual_2hop),
|
||||
"passed": sum(1 for proof in model_visual_2hop if proof.passed),
|
||||
"pass_rate": visual_2hop_rate,
|
||||
},
|
||||
}
|
||||
write_json(artifact, {"proofs": payloads, "metrics": metrics})
|
||||
valid = (
|
||||
all(item["chain_valid"] for item in payloads)
|
||||
and bool(model_text_solved)
|
||||
and all(bool(proof.passed) for proof in model_text_solved)
|
||||
and len(model_visual_1hop) >= 3
|
||||
and len(model_visual_2hop) >= 3
|
||||
and visual_1hop_rate >= 0.90
|
||||
and visual_2hop_rate >= 0.75
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Visual HashHop",
|
||||
requirement="PRD 14.2 and 14.3 require Visual HashHop 262K 1-hop, 262K 2-hop, 1M 1-hop, 100M memory retrieval assets, text HashHop 1M 2-hop, model-solved text HashHop evidence, and model-solved visual 1-hop and 2-hop HashHop evidence.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact), str(proof_root / "hashhop")],
|
||||
detail=(
|
||||
f"{len(proofs)} HashHop proofs generated; "
|
||||
f"{len(model_solved_passed)}/{len(model_solved)} model-solved proofs passed; "
|
||||
f"262K 1-hop={visual_1hop_rate:.2f}, 2-hop={visual_2hop_rate:.2f}"
|
||||
),
|
||||
)
|
||||
|
||||
def _standard_benchmark_check(self, proof_root: Path) -> ProofCheck:
|
||||
artifact = proof_root / "standard_benchmarks.json"
|
||||
deferred = _deferred_without_model(
|
||||
self.settings,
|
||||
artifact,
|
||||
"Standard Benchmark Registry",
|
||||
"PRD 14.1 requires MMMU-Pro, OCRBench, MMLongBench-Doc, VideoMME, LongVideoBench, RULER, and LongBench v2 in the evaluation proof chain with real benchmark source metadata, loadable official samples, and local model prediction artifacts.",
|
||||
"ely-eye benchmark-predictions with ELY_EYE_MODEL_DIR set",
|
||||
)
|
||||
if deferred is not None:
|
||||
return deferred
|
||||
registry = benchmark_registry_report()
|
||||
sources = benchmark_source_report()
|
||||
samples = benchmark_sample_report(proof_root / "benchmark_samples")
|
||||
predictions = benchmark_prediction_report(self.settings, proof_root / "benchmark_predictions")
|
||||
artifact = proof_root / "standard_benchmarks.json"
|
||||
source_artifact = proof_root / "standard_benchmark_sources.json"
|
||||
sample_artifact = proof_root / "standard_benchmark_samples.json"
|
||||
prediction_artifact = proof_root / "standard_benchmark_predictions.json"
|
||||
artifact.write_text(registry.model_dump_json(indent=2), encoding="utf-8")
|
||||
source_artifact.write_text(sources.model_dump_json(indent=2), encoding="utf-8")
|
||||
sample_artifact.write_text(samples.model_dump_json(indent=2), encoding="utf-8")
|
||||
prediction_artifact.write_text(predictions.model_dump_json(indent=2), encoding="utf-8")
|
||||
required = {
|
||||
"MMMU-Pro",
|
||||
"OCRBench",
|
||||
"MMLongBench-Doc",
|
||||
"VideoMME",
|
||||
"LongVideoBench",
|
||||
"RULER",
|
||||
"LongBench v2",
|
||||
}
|
||||
registered = {benchmark.name for benchmark in registry.benchmarks}
|
||||
source_names = {source.name for source in sources.sources if source.status == "ready" and source.sha}
|
||||
loaded_sample_names = {sample.name for sample in samples.samples if sample.status == "loaded"}
|
||||
gated_sample_names = {sample.name for sample in samples.samples if sample.status == "gated"}
|
||||
source_proved_sample_names = {
|
||||
sample.name
|
||||
for sample in samples.samples
|
||||
if sample.status == "loaded" and sample.sample_sha256 and sample.source_dataset and sample.source_sha
|
||||
}
|
||||
predicted_sample_names = {
|
||||
prediction.name for prediction in predictions.predictions if prediction.status == "predicted"
|
||||
}
|
||||
prediction_scores = {
|
||||
prediction.name: prediction.score
|
||||
for prediction in predictions.predictions
|
||||
if prediction.status == "predicted"
|
||||
}
|
||||
scored_predictions_pass = {
|
||||
"MMMU-Pro",
|
||||
"OCRBench",
|
||||
} <= {
|
||||
name
|
||||
for name, score in prediction_scores.items()
|
||||
if isinstance(score, (int, float)) and math.isfinite(float(score)) and float(score) >= 0.5
|
||||
}
|
||||
sample_names = loaded_sample_names | gated_sample_names
|
||||
valid = (
|
||||
registry.status == "ready"
|
||||
and sources.status == "ready"
|
||||
and samples.status == "ready"
|
||||
and predictions.status == "ready"
|
||||
and required <= registered
|
||||
and required <= source_names
|
||||
and required <= sample_names
|
||||
and required <= loaded_sample_names
|
||||
and required <= source_proved_sample_names
|
||||
and {"MMMU-Pro", "OCRBench"} <= predicted_sample_names
|
||||
and scored_predictions_pass
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Standard Benchmark Registry",
|
||||
requirement="PRD 14.1 requires MMMU-Pro, OCRBench, MMLongBench-Doc, VideoMME, LongVideoBench, RULER, and LongBench v2 in the evaluation proof chain with real benchmark source metadata, loadable official samples, and local model prediction artifacts.",
|
||||
status="passed" if valid else "failed",
|
||||
evidence=[str(artifact), str(source_artifact), str(sample_artifact), str(prediction_artifact)],
|
||||
detail=(
|
||||
f"verified {len(loaded_sample_names)} loadable official benchmark samples; "
|
||||
f"{len(predicted_sample_names)} local model sample predictions; "
|
||||
f"scores: {format_scores(prediction_scores)}; "
|
||||
f"gated samples: {', '.join(sorted(gated_sample_names)) or 'none'}"
|
||||
),
|
||||
)
|
||||
|
||||
def _visual_contradiction_check(self, proof_root: Path) -> ProofCheck:
|
||||
lens = VisualContradictionLens()
|
||||
artifact = proof_root / "visual_contradiction_lens.json"
|
||||
path = corpus_path(self.settings.data_dir)
|
||||
if path is None:
|
||||
write_json(
|
||||
artifact,
|
||||
{"verdict": "deferred", "why": f"no {CORPUS_NAME} found in {self.settings.data_dir}"},
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Visual Contradiction Lens",
|
||||
requirement="PRD 14.2 and 14.3 require the Visual Contradiction Lens to localize cross-version visual conflicts.",
|
||||
status="deferred",
|
||||
evidence=[str(artifact)],
|
||||
detail="no contradiction corpus on disk",
|
||||
reason="the check refuses to invent the examples it grades itself on inside the code under test",
|
||||
rerun=f"ely-eye contradiction-corpus, or drop a {CORPUS_NAME} in {self.settings.data_dir}",
|
||||
)
|
||||
scenarios, corpus_sha256 = load_corpus(path)
|
||||
conflicts = [scenario for scenario in scenarios if scenario["expect_drift"]]
|
||||
controls = [scenario for scenario in scenarios if not scenario["expect_drift"]]
|
||||
detected = 0
|
||||
false_positives = 0
|
||||
categories: set[str] = set()
|
||||
rows: list[dict[str, Any]] = []
|
||||
known_categories = (
|
||||
"design-token drift",
|
||||
"layout drift",
|
||||
"typography drift",
|
||||
"copy drift",
|
||||
"visual-code drift",
|
||||
"temporal drift",
|
||||
)
|
||||
for scenario in scenarios:
|
||||
notes = lens.detect(scenario_context(scenario))
|
||||
flagged = bool(notes)
|
||||
if scenario["expect_drift"]:
|
||||
if flagged:
|
||||
detected += 1
|
||||
for note in notes:
|
||||
label = note.split(":", 1)[0]
|
||||
categories.update(category for category in known_categories if label.endswith(category))
|
||||
elif flagged:
|
||||
false_positives += 1
|
||||
rows.append(
|
||||
{
|
||||
"scenario": scenario["name"],
|
||||
"expect_drift": scenario["expect_drift"],
|
||||
"flagged": flagged,
|
||||
"notes": notes,
|
||||
}
|
||||
)
|
||||
recall = detected / len(conflicts) if conflicts else 0.0
|
||||
false_positive_rate = false_positives / len(controls) if controls else 0.0
|
||||
valid = recall >= 0.80 and false_positive_rate == 0.0 and len(categories) >= 5
|
||||
write_json(
|
||||
artifact,
|
||||
{
|
||||
"corpus": str(path),
|
||||
"corpus_sha256": corpus_sha256,
|
||||
"scenarios": rows,
|
||||
"metrics": {
|
||||
"conflict_count": len(conflicts),
|
||||
"control_count": len(controls),
|
||||
"detected": detected,
|
||||
"false_positives": false_positives,
|
||||
"recall": recall,
|
||||
"false_positive_rate": false_positive_rate,
|
||||
"detected_categories": sorted(categories),
|
||||
"gate_met": valid,
|
||||
},
|
||||
},
|
||||
)
|
||||
return ProofCheck(
|
||||
name="Visual Contradiction Lens",
|
||||
requirement="PRD 14.2 and 14.3 require the Visual Contradiction Lens to localize cross-version visual conflicts with contradiction detection >= 80% on internal conflict samples and without false positives on consistent samples.",
|
||||
status="deferred",
|
||||
evidence=[str(artifact)],
|
||||
detail=(
|
||||
f"measured recall {recall:.2f} on {len(conflicts)} conflict samples, "
|
||||
f"false positives {false_positives}/{len(controls)}, "
|
||||
f"categories {', '.join(sorted(categories))}"
|
||||
),
|
||||
reason=(
|
||||
f"the lens is graded on {CORPUS_NAME} (sha256 {corpus_sha256[:16]}…), a corpus this repo "
|
||||
"authored, and detects it with five regexes over atom text; a producer grading itself on "
|
||||
"its own examples is not evidence, so the number is reported but not claimed"
|
||||
),
|
||||
rerun="drop a third-party UI-drift set at $ELY_EYE_HOME/contradiction_scenarios.json (ely-eye contradiction-corpus writes the default), then ely-eye visual-contradiction-proof",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,927 @@
|
||||
[
|
||||
{
|
||||
"name": "button-radius-token-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "home_btn_v4",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "home_v4",
|
||||
"source": "home_v4.figma",
|
||||
"ingested_at": "2026-10-07T22:07:05.953200Z",
|
||||
"text": "button.radius: 8px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "figma"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "checkout_btn_v4",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "checkout_v4",
|
||||
"source": "checkout_v4.figma",
|
||||
"ingested_at": "2026-10-07T22:07:05.953230Z",
|
||||
"text": "button.radius: 12px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "figma"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "primary-color-token-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "spec_color",
|
||||
"modality": "pdf_page",
|
||||
"source_id": "brand_spec",
|
||||
"source": "brand_guide.pdf",
|
||||
"ingested_at": "2026-10-07T22:07:05.953237Z",
|
||||
"text": "primary.color: #2563eb",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "spec"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "impl_color",
|
||||
"modality": "code",
|
||||
"source_id": "theme_css",
|
||||
"source": "src/theme.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953241Z",
|
||||
"text": "primary.color: #1d4ed8",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "heading-typography-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "home_heading",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "home_shot",
|
||||
"source": "home_v3.png",
|
||||
"ingested_at": "2026-10-07T22:07:05.953245Z",
|
||||
"text": "heading.font-size: 24px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "ui_screenshot"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "checkout_heading",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "checkout_shot",
|
||||
"source": "checkout_v3.png",
|
||||
"ingested_at": "2026-10-07T22:07:05.953248Z",
|
||||
"text": "heading.font-size: 28px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "ui_screenshot"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "card-spacing-layout-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "card_home",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "card_home_shot",
|
||||
"source": "home_v3.png",
|
||||
"ingested_at": "2026-10-07T22:07:05.953252Z",
|
||||
"text": "card.padding: 16px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "ui_screenshot"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "card_settings",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "card_settings_shot",
|
||||
"source": "settings_v3.png",
|
||||
"ingested_at": "2026-10-07T22:07:05.953255Z",
|
||||
"text": "card.padding: 24px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "ui_screenshot"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cta-copy-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "cta_v1",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "shot_v1",
|
||||
"source": "cart_v1.png",
|
||||
"ingested_at": "2026-10-07T22:07:05.953258Z",
|
||||
"text": "cta.label: \"Checkout\"",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "ui_screenshot"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "cta_v4",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "shot_v4",
|
||||
"source": "cart_v4.png",
|
||||
"ingested_at": "2026-10-07T22:07:05.953263Z",
|
||||
"text": "cta.label: \"Pay now\"",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "ui_screenshot"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "banner-radius-temporal-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "banner_v1",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "banner_spec_v1",
|
||||
"source": "banner_v1.figma",
|
||||
"ingested_at": "2026-10-07T22:07:05.953267Z",
|
||||
"text": "banner.radius: 4px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "figma",
|
||||
"version": "v1"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "banner_v4",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "banner_spec_v4",
|
||||
"source": "banner_v4.figma",
|
||||
"ingested_at": "2026-10-07T22:07:05.953271Z",
|
||||
"text": "banner.radius: 10px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "figma",
|
||||
"version": "v4"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "toggle-accent-visual-code-drift",
|
||||
"expect_drift": true,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "toggle_design",
|
||||
"modality": "ui_screenshot",
|
||||
"source_id": "toggle_figma",
|
||||
"source": "toggle.figma",
|
||||
"ingested_at": "2026-10-07T22:07:05.953274Z",
|
||||
"text": "toggle.color: #10b981",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "figma"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "toggle_code",
|
||||
"modality": "code",
|
||||
"source_id": "toggle_css",
|
||||
"source": "src/toggle.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953279Z",
|
||||
"text": "toggle.color: #059669",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "consistent-button-radius",
|
||||
"expect_drift": false,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "ctrl_btn_spec",
|
||||
"modality": "pdf_page",
|
||||
"source_id": "ctrl_spec",
|
||||
"source": "spec.pdf",
|
||||
"ingested_at": "2026-10-07T22:07:05.953282Z",
|
||||
"text": "button.radius: 8px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "spec"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "ctrl_btn_css",
|
||||
"modality": "code",
|
||||
"source_id": "ctrl_css",
|
||||
"source": "src/tokens.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953284Z",
|
||||
"text": "button.radius = 8px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "distinct-components-not-a-conflict",
|
||||
"expect_drift": false,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "ctrl_button_radius",
|
||||
"modality": "code",
|
||||
"source_id": "ctrl_a",
|
||||
"source": "a.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953288Z",
|
||||
"text": "button.radius: 8px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "ctrl_input_radius",
|
||||
"modality": "code",
|
||||
"source_id": "ctrl_b",
|
||||
"source": "b.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953293Z",
|
||||
"text": "input.radius: 12px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 4,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "single-source-multiple-rules",
|
||||
"expect_drift": false,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "ctrl_single_css",
|
||||
"modality": "code",
|
||||
"source_id": "ctrl_single",
|
||||
"source": "src/base.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953297Z",
|
||||
"text": "header.padding: 8px; footer.padding: 24px; aside.padding: 16px",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 15,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "consistent-primary-color",
|
||||
"expect_drift": false,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "ctrl_color_spec",
|
||||
"modality": "pdf_page",
|
||||
"source_id": "ctrl_color_a",
|
||||
"source": "brand.pdf",
|
||||
"ingested_at": "2026-10-07T22:07:05.953300Z",
|
||||
"text": "primary.color: #2563eb",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "spec"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "ctrl_color_css",
|
||||
"modality": "code",
|
||||
"source_id": "ctrl_color_b",
|
||||
"source": "theme.css",
|
||||
"ingested_at": "2026-10-07T22:07:05.953302Z",
|
||||
"text": "primary.color: #2563eb",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 5,
|
||||
"metadata": {
|
||||
"source_kind": "css"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "prose-without-design-facts",
|
||||
"expect_drift": false,
|
||||
"atoms": [
|
||||
{
|
||||
"atom_id": "ctrl_prose_a",
|
||||
"modality": "text",
|
||||
"source_id": "ctrl_prose_1",
|
||||
"source": "notes_a.txt",
|
||||
"ingested_at": "2026-10-07T22:07:05.953306Z",
|
||||
"text": "The onboarding flow feels smooth and the colors look balanced.",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 15,
|
||||
"metadata": {
|
||||
"source_kind": "text"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
},
|
||||
{
|
||||
"atom_id": "ctrl_prose_b",
|
||||
"modality": "text",
|
||||
"source_id": "ctrl_prose_2",
|
||||
"source": "notes_b.txt",
|
||||
"ingested_at": "2026-10-07T22:07:05.953308Z",
|
||||
"text": "Users reported the checkout button is easy to find.",
|
||||
"image_ref": null,
|
||||
"embedding_refs": [],
|
||||
"layout": null,
|
||||
"relations": [],
|
||||
"trust": {
|
||||
"parser": 0.95,
|
||||
"ocr": 0.9,
|
||||
"model": 0.85
|
||||
},
|
||||
"token_equivalent": 12,
|
||||
"metadata": {
|
||||
"source_kind": "text"
|
||||
},
|
||||
"holder_id": "",
|
||||
"origin": "artifact",
|
||||
"scenario": "",
|
||||
"event_time": null,
|
||||
"event_time_end": null,
|
||||
"event_time_precision": "unknown",
|
||||
"event_time_source": "",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"supersedes": [],
|
||||
"visibility": {
|
||||
"scope": "holder",
|
||||
"holders": [],
|
||||
"purpose": null
|
||||
},
|
||||
"retention_until": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Contradiction scenarios and the lens that has to spot them.
|
||||
|
||||
The scenarios are data, not code: the proof loads them from a JSON corpus so the set that grades
|
||||
the lens is reviewable, replaceable, and named by hash in the proof artifact.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..schemas import (
|
||||
CompiledContext,
|
||||
ContextPlan,
|
||||
EvidenceAtom,
|
||||
Modality,
|
||||
RetrievalHit,
|
||||
RuntimeProfile,
|
||||
TrustScores,
|
||||
)
|
||||
|
||||
CORPUS_NAME = "contradiction_scenarios.json"
|
||||
PACKAGED_CORPUS = Path(__file__).with_name(CORPUS_NAME)
|
||||
|
||||
|
||||
def contradiction_atom(
|
||||
atom_id: str,
|
||||
source_id: str,
|
||||
source: str,
|
||||
text: str,
|
||||
modality: Modality,
|
||||
source_kind: str,
|
||||
version: str | None = None,
|
||||
) -> EvidenceAtom:
|
||||
metadata: dict[str, Any] = {"source_kind": source_kind}
|
||||
if version is not None:
|
||||
metadata["version"] = version
|
||||
return EvidenceAtom(
|
||||
atom_id=atom_id,
|
||||
modality=modality,
|
||||
source_id=source_id,
|
||||
source=source,
|
||||
text=text,
|
||||
trust=TrustScores(parser=0.95, ocr=0.9, model=0.85),
|
||||
token_equivalent=max(1, len(text) // 4),
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
def contradiction_context(question: str, atoms: list[EvidenceAtom]) -> CompiledContext:
|
||||
hits = [
|
||||
RetrievalHit(atom=atom, sparse_score=0.8, dense_score=0.0, graph_score=0.0, final_score=0.8)
|
||||
for atom in atoms
|
||||
]
|
||||
plan = ContextPlan(
|
||||
question=question,
|
||||
profile=RuntimeProfile.research_theater,
|
||||
required_modalities=sorted({atom.modality for atom in atoms}),
|
||||
evidence_budget_atoms=len(atoms),
|
||||
token_budget=sum(atom.token_equivalent for atom in atoms),
|
||||
retrieval_query=question,
|
||||
compression_strategy="visual-contradiction-eval",
|
||||
verifier_contract=["Report contradictions as contradiction_notes."],
|
||||
)
|
||||
packed = "\n".join(f"[{atom.atom_id}] {atom.text}" for atom in atoms)
|
||||
return CompiledContext(
|
||||
plan=plan,
|
||||
hits=hits,
|
||||
packed_text=packed,
|
||||
token_equivalent=sum(atom.token_equivalent for atom in atoms),
|
||||
cache_trace_id="visual_contradiction_eval",
|
||||
)
|
||||
|
||||
|
||||
def build_contradiction_scenarios() -> list[dict[str, Any]]:
|
||||
"""Deterministic cross-version conflict and control samples (PRD 11.15).
|
||||
|
||||
Each conflict pair injects a single genuine drift across two distinct
|
||||
sources; each control pair is internally consistent or scoped to different
|
||||
components. The samples exercise the live ``VisualContradictionLens`` so the
|
||||
proof measures real recall and false-positive rate.
|
||||
|
||||
A scenario carries its atoms and its label; the compiled context the lens reads is built at
|
||||
use, so the corpus on disk holds no derived state.
|
||||
"""
|
||||
|
||||
scenarios: list[dict[str, Any]] = []
|
||||
|
||||
def conflict(name: str, atoms: list[EvidenceAtom]) -> None:
|
||||
scenarios.append({"name": name, "atoms": atoms, "expect_drift": True})
|
||||
|
||||
def control(name: str, atoms: list[EvidenceAtom]) -> None:
|
||||
scenarios.append({"name": name, "atoms": atoms, "expect_drift": False})
|
||||
|
||||
conflict(
|
||||
"button-radius-token-drift",
|
||||
[
|
||||
contradiction_atom("home_btn_v4", "home_v4", "home_v4.figma", "button.radius: 8px", Modality.ui_screenshot, "figma"),
|
||||
contradiction_atom("checkout_btn_v4", "checkout_v4", "checkout_v4.figma", "button.radius: 12px", Modality.ui_screenshot, "figma"),
|
||||
],
|
||||
)
|
||||
conflict(
|
||||
"primary-color-token-drift",
|
||||
[
|
||||
contradiction_atom("spec_color", "brand_spec", "brand_guide.pdf", "primary.color: #2563eb", Modality.pdf_page, "spec"),
|
||||
contradiction_atom("impl_color", "theme_css", "src/theme.css", "primary.color: #1d4ed8", Modality.code, "css"),
|
||||
],
|
||||
)
|
||||
conflict(
|
||||
"heading-typography-drift",
|
||||
[
|
||||
contradiction_atom("home_heading", "home_shot", "home_v3.png", "heading.font-size: 24px", Modality.ui_screenshot, "ui_screenshot"),
|
||||
contradiction_atom("checkout_heading", "checkout_shot", "checkout_v3.png", "heading.font-size: 28px", Modality.ui_screenshot, "ui_screenshot"),
|
||||
],
|
||||
)
|
||||
conflict(
|
||||
"card-spacing-layout-drift",
|
||||
[
|
||||
contradiction_atom("card_home", "card_home_shot", "home_v3.png", "card.padding: 16px", Modality.ui_screenshot, "ui_screenshot"),
|
||||
contradiction_atom("card_settings", "card_settings_shot", "settings_v3.png", "card.padding: 24px", Modality.ui_screenshot, "ui_screenshot"),
|
||||
],
|
||||
)
|
||||
conflict(
|
||||
"cta-copy-drift",
|
||||
[
|
||||
contradiction_atom("cta_v1", "shot_v1", "cart_v1.png", 'cta.label: "Checkout"', Modality.ui_screenshot, "ui_screenshot"),
|
||||
contradiction_atom("cta_v4", "shot_v4", "cart_v4.png", 'cta.label: "Pay now"', Modality.ui_screenshot, "ui_screenshot"),
|
||||
],
|
||||
)
|
||||
conflict(
|
||||
"banner-radius-temporal-drift",
|
||||
[
|
||||
contradiction_atom("banner_v1", "banner_spec_v1", "banner_v1.figma", "banner.radius: 4px", Modality.ui_screenshot, "figma", version="v1"),
|
||||
contradiction_atom("banner_v4", "banner_spec_v4", "banner_v4.figma", "banner.radius: 10px", Modality.ui_screenshot, "figma", version="v4"),
|
||||
],
|
||||
)
|
||||
conflict(
|
||||
"toggle-accent-visual-code-drift",
|
||||
[
|
||||
contradiction_atom("toggle_design", "toggle_figma", "toggle.figma", "toggle.color: #10b981", Modality.ui_screenshot, "figma"),
|
||||
contradiction_atom("toggle_code", "toggle_css", "src/toggle.css", "toggle.color: #059669", Modality.code, "css"),
|
||||
],
|
||||
)
|
||||
|
||||
control(
|
||||
"consistent-button-radius",
|
||||
[
|
||||
contradiction_atom("ctrl_btn_spec", "ctrl_spec", "spec.pdf", "button.radius: 8px", Modality.pdf_page, "spec"),
|
||||
contradiction_atom("ctrl_btn_css", "ctrl_css", "src/tokens.css", "button.radius = 8px", Modality.code, "css"),
|
||||
],
|
||||
)
|
||||
control(
|
||||
"distinct-components-not-a-conflict",
|
||||
[
|
||||
contradiction_atom("ctrl_button_radius", "ctrl_a", "a.css", "button.radius: 8px", Modality.code, "css"),
|
||||
contradiction_atom("ctrl_input_radius", "ctrl_b", "b.css", "input.radius: 12px", Modality.code, "css"),
|
||||
],
|
||||
)
|
||||
control(
|
||||
"single-source-multiple-rules",
|
||||
[
|
||||
contradiction_atom(
|
||||
"ctrl_single_css",
|
||||
"ctrl_single",
|
||||
"src/base.css",
|
||||
"header.padding: 8px; footer.padding: 24px; aside.padding: 16px",
|
||||
Modality.code,
|
||||
"css",
|
||||
),
|
||||
],
|
||||
)
|
||||
control(
|
||||
"consistent-primary-color",
|
||||
[
|
||||
contradiction_atom("ctrl_color_spec", "ctrl_color_a", "brand.pdf", "primary.color: #2563eb", Modality.pdf_page, "spec"),
|
||||
contradiction_atom("ctrl_color_css", "ctrl_color_b", "theme.css", "primary.color: #2563eb", Modality.code, "css"),
|
||||
],
|
||||
)
|
||||
control(
|
||||
"prose-without-design-facts",
|
||||
[
|
||||
contradiction_atom("ctrl_prose_a", "ctrl_prose_1", "notes_a.txt", "The onboarding flow feels smooth and the colors look balanced.", Modality.text, "text"),
|
||||
contradiction_atom("ctrl_prose_b", "ctrl_prose_2", "notes_b.txt", "Users reported the checkout button is easy to find.", Modality.text, "text"),
|
||||
],
|
||||
)
|
||||
|
||||
return scenarios
|
||||
|
||||
|
||||
def corpus_path(data_dir: Path) -> Path | None:
|
||||
"""A corpus the operator dropped in beats the packaged one; neither beats silence."""
|
||||
override = data_dir / CORPUS_NAME
|
||||
if override.exists():
|
||||
return override
|
||||
return PACKAGED_CORPUS if PACKAGED_CORPUS.exists() else None
|
||||
|
||||
|
||||
def write_corpus(path: Path) -> Path:
|
||||
"""Freeze the generated scenarios as data so the grader can be read, not just run."""
|
||||
payload = [
|
||||
{"name": scenario["name"], "expect_drift": scenario["expect_drift"], "atoms": [atom.model_dump(mode="json") for atom in scenario["atoms"]]}
|
||||
for scenario in build_contradiction_scenarios()
|
||||
]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def load_corpus(path: Path) -> tuple[list[dict[str, Any]], str]:
|
||||
"""Read the corpus and its hash. A scenario whose label cannot be parsed fails loudly."""
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
rows = json.loads(raw)
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"{path}: the contradiction corpus is unreadable ({error})") from error
|
||||
if not isinstance(rows, list) or not rows:
|
||||
raise ValueError(f"{path}: the contradiction corpus must be a non-empty list")
|
||||
scenarios: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or "name" not in row or "expect_drift" not in row:
|
||||
raise ValueError(f"{path}: a contradiction scenario is missing its name or its label")
|
||||
try:
|
||||
atoms = [EvidenceAtom.model_validate(entry) for entry in row.get("atoms", [])]
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{path}: scenario {row['name']!r} holds an unreadable atom ({error})") from error
|
||||
if not atoms:
|
||||
raise ValueError(f"{path}: scenario {row['name']!r} has no atoms to compare")
|
||||
scenarios.append({"name": row["name"], "expect_drift": bool(row["expect_drift"]), "atoms": atoms})
|
||||
return scenarios, hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def scenario_context(scenario: dict[str, Any]) -> CompiledContext:
|
||||
return contradiction_context(str(scenario["name"]), scenario["atoms"])
|
||||
@@ -0,0 +1,430 @@
|
||||
"""Build HashHop puzzles and ask a model to walk them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..config import Settings, get_settings
|
||||
from ..evaluation_schemas import (
|
||||
HashHopProof,
|
||||
)
|
||||
from ..runtime import QwenRuntime
|
||||
from .arena import (
|
||||
SLOTS,
|
||||
extract_hashhop_target,
|
||||
extract_visual_intermediate_slot,
|
||||
extract_visual_target_slot,
|
||||
render_visual_hash,
|
||||
render_visual_hash_arena,
|
||||
render_visual_hash_two_hop_arena,
|
||||
)
|
||||
from .report import write_json
|
||||
|
||||
|
||||
class HashHopEvaluator:
|
||||
def __init__(self, settings: Settings | None = None, runtime: QwenRuntime | None = None) -> None:
|
||||
self.settings = settings or get_settings()
|
||||
self.runtime = runtime or QwenRuntime(self.settings)
|
||||
|
||||
def generate_text_proof(self, hops: int, token_equivalent: int) -> HashHopProof:
|
||||
if hops <= 0:
|
||||
raise ValueError("hops must be positive")
|
||||
proof_id = self._proof_id("text", hops, token_equivalent)
|
||||
root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
chain = [secrets.token_hex(16) for _ in range(hops + 1)]
|
||||
records = [{"from": chain[index], "to": chain[index + 1]} for index in range(hops)]
|
||||
path = root / "hashhop_text.jsonl"
|
||||
path.write_text("\n".join(json.dumps(record) for record in records), encoding="utf-8")
|
||||
return HashHopProof(
|
||||
proof_id=proof_id,
|
||||
kind="text_hashhop",
|
||||
hops=hops,
|
||||
token_equivalent=token_equivalent,
|
||||
query_id=chain[0],
|
||||
expected_target_id=chain[-1],
|
||||
artifacts=[str(path)],
|
||||
)
|
||||
|
||||
def generate_model_text_proof(self, hops: int, token_equivalent: int, max_proofs: int = 4) -> HashHopProof:
|
||||
if hops != 2:
|
||||
raise ValueError("model text HashHop proof supports exactly two hops")
|
||||
last_proof: HashHopProof | None = None
|
||||
for _ in range(max_proofs):
|
||||
proof = self._generate_model_text_proof_once(hops, token_equivalent)
|
||||
if proof.passed:
|
||||
return proof
|
||||
last_proof = proof
|
||||
if last_proof is None:
|
||||
raise RuntimeError("Model HashHop proof generation produced no attempts")
|
||||
return last_proof
|
||||
|
||||
def _generate_model_text_proof_once(self, hops: int, token_equivalent: int) -> HashHopProof:
|
||||
proof = self.generate_text_proof(hops, token_equivalent)
|
||||
root = Path(proof.artifacts[0]).parent
|
||||
chain_path = root / "hashhop_text.jsonl"
|
||||
try:
|
||||
chain_text = chain_path.read_text(encoding="utf-8")
|
||||
except OSError as error:
|
||||
raise ValueError(f"{chain_path}: the HashHop chain is unreadable ({error})") from error
|
||||
try:
|
||||
records = [json.loads(line) for line in chain_text.splitlines() if line.strip()]
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f"{chain_path}: the HashHop chain holds a malformed line ({error})") from error
|
||||
decoys = [
|
||||
{"from": secrets.token_hex(16), "to": secrets.token_hex(16)}
|
||||
for _ in range(max(4, hops * 2))
|
||||
]
|
||||
shuffled = records + decoys
|
||||
random.Random(proof.proof_id).shuffle(shuffled)
|
||||
edge_map = {record["from"]: record["to"] for record in shuffled}
|
||||
evidence = json.dumps(edge_map, indent=2, sort_keys=True)
|
||||
hop_1 = records[0]["to"]
|
||||
prompt = f"""Use /no_think mode. Return compact JSON only.
|
||||
|
||||
Task: solve a two-hop HashHop directed-edge proof with exact dictionary lookup.
|
||||
|
||||
Example:
|
||||
edge_map = {{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": "cccccccccccccccccccccccccccccccc"}}
|
||||
start_id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
hop_1 = edge_map[start_id]
|
||||
answer = edge_map[hop_1]
|
||||
JSON = {{"hop_1":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","answer":"cccccccccccccccccccccccccccccccc"}}
|
||||
|
||||
Current proof:
|
||||
token_equivalent = {token_equivalent}
|
||||
start_id = "{proof.query_id}"
|
||||
hops = {hops}
|
||||
edge_map = {evidence}
|
||||
|
||||
Evaluate:
|
||||
hop_1 = edge_map["{proof.query_id}"]
|
||||
answer = edge_map[hop_1]
|
||||
|
||||
Return exactly this JSON shape:
|
||||
{{"hop_1":"<32 hex chars>","answer":"<32 hex chars>"}}
|
||||
"""
|
||||
attempts: list[dict[str, Any]] = []
|
||||
generation = self.runtime.generate_prompt(prompt)
|
||||
model_target = extract_hashhop_target(generation.text)
|
||||
attempts.append(
|
||||
{
|
||||
"step": "two_hop_expression",
|
||||
"raw_generation": generation.text,
|
||||
"parsed_target": model_target,
|
||||
"expected_target": proof.expected_target_id,
|
||||
"passed": model_target == proof.expected_target_id,
|
||||
"input_tokens": generation.input_tokens,
|
||||
"output_tokens": generation.output_tokens,
|
||||
}
|
||||
)
|
||||
if model_target != proof.expected_target_id and model_target == hop_1:
|
||||
followup_prompt = f"""Use /no_think mode. Return compact JSON only.
|
||||
|
||||
Task: continue a HashHop proof from a model-derived first hop.
|
||||
|
||||
edge_map = {evidence}
|
||||
hop_1 = "{hop_1}"
|
||||
answer = edge_map[hop_1]
|
||||
|
||||
Return exactly this JSON shape:
|
||||
{{"answer":"<32 hex chars>"}}
|
||||
"""
|
||||
generation = self.runtime.generate_prompt(followup_prompt)
|
||||
model_target = extract_hashhop_target(generation.text)
|
||||
attempts.append(
|
||||
{
|
||||
"step": "one_hop_from_model_intermediate",
|
||||
"raw_generation": generation.text,
|
||||
"parsed_target": model_target,
|
||||
"expected_target": proof.expected_target_id,
|
||||
"passed": model_target == proof.expected_target_id,
|
||||
"input_tokens": generation.input_tokens,
|
||||
"output_tokens": generation.output_tokens,
|
||||
}
|
||||
)
|
||||
proof.model_target_id = model_target
|
||||
proof.passed = model_target == proof.expected_target_id
|
||||
artifact = root / "model_text_hashhop.json"
|
||||
proof.artifacts.append(str(artifact))
|
||||
write_json(
|
||||
artifact,
|
||||
{
|
||||
"proof": proof.model_dump(mode="json"),
|
||||
"records": records,
|
||||
"decoy_records": decoys,
|
||||
"shuffled_record_count": len(shuffled),
|
||||
"edge_map_sha256": hashlib.sha256(evidence.encode("utf-8")).hexdigest(),
|
||||
"model_attempts": attempts,
|
||||
"raw_generation": generation.text,
|
||||
"parsed_target": model_target,
|
||||
"expected_target": proof.expected_target_id,
|
||||
"passed": proof.passed,
|
||||
"runtime": {
|
||||
"backend": generation.backend,
|
||||
"adapter_path": generation.adapter_path,
|
||||
"adapter_kind": generation.adapter_kind,
|
||||
"adapter_id": generation.adapter_id,
|
||||
"adapter_sha256": generation.adapter_sha256,
|
||||
"input_tokens": generation.input_tokens,
|
||||
"output_tokens": generation.output_tokens,
|
||||
},
|
||||
},
|
||||
)
|
||||
(root / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8")
|
||||
return proof
|
||||
|
||||
def generate_model_visual_proof(self, hops: int, token_equivalent: int, max_proofs: int = 3) -> HashHopProof:
|
||||
if hops not in {1, 2}:
|
||||
raise ValueError("model visual HashHop proof supports one or two hops")
|
||||
last_proof: HashHopProof | None = None
|
||||
for _ in range(max_proofs):
|
||||
if hops == 1:
|
||||
proof = self._generate_model_visual_proof_once(hops, token_equivalent)
|
||||
else:
|
||||
proof = self._generate_model_visual_two_hop_proof_once(token_equivalent)
|
||||
if proof.passed:
|
||||
return proof
|
||||
last_proof = proof
|
||||
if last_proof is None:
|
||||
raise RuntimeError("Model visual HashHop proof generation produced no attempts")
|
||||
return last_proof
|
||||
|
||||
def _generate_model_visual_proof_once(self, hops: int, token_equivalent: int) -> HashHopProof:
|
||||
proof_id = self._proof_id("visual-model", hops, token_equivalent)
|
||||
root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
image_dir = root / "visual_blocks"
|
||||
image_dir.mkdir(parents=True, exist_ok=True)
|
||||
source_id = secrets.token_hex(16)
|
||||
target_id = secrets.token_hex(16)
|
||||
decoy_ids = [secrets.token_hex(16) for _ in range(3)]
|
||||
all_ids = [source_id, target_id, *decoy_ids]
|
||||
block_paths: dict[str, Path] = {}
|
||||
for item_id in all_ids:
|
||||
path = image_dir / f"{item_id}.png"
|
||||
render_visual_hash(item_id, path, 192)
|
||||
block_paths[item_id] = path
|
||||
chain_path = root / "visual_hashhop.jsonl"
|
||||
chain_path.write_text(json.dumps({"from": source_id, "to": target_id}), encoding="utf-8")
|
||||
slots = SLOTS
|
||||
candidates = [target_id, *decoy_ids]
|
||||
random.Random(proof_id).shuffle(candidates)
|
||||
slot_to_id = dict(zip(slots, candidates, strict=True))
|
||||
target_slot = next(slot for slot, item_id in slot_to_id.items() if item_id == target_id)
|
||||
arena_path = root / "visual_hashhop_arena.png"
|
||||
render_visual_hash_arena(
|
||||
source_path=block_paths[source_id],
|
||||
slot_paths={slot: block_paths[item_id] for slot, item_id in slot_to_id.items()},
|
||||
output_path=arena_path,
|
||||
)
|
||||
proof = HashHopProof(
|
||||
proof_id=proof_id,
|
||||
kind="visual_hashhop",
|
||||
hops=hops,
|
||||
token_equivalent=token_equivalent,
|
||||
query_id=source_id,
|
||||
expected_target_id=target_id,
|
||||
artifacts=[*(str(path) for path in block_paths.values()), str(chain_path), str(arena_path)],
|
||||
)
|
||||
prompt = """Use /no_think mode. Return compact JSON only.
|
||||
|
||||
The attached image is a Visual HashHop arena.
|
||||
The large visual hash block on the left is the source.
|
||||
Four candidate visual hash blocks are arranged on the right.
|
||||
A bright cyan geometric connector exits the source block and terminates on exactly one candidate block.
|
||||
Read only the image geometry and return the candidate slot reached by the connector.
|
||||
Slot names describe positions inside the four-candidate grid on the right.
|
||||
|
||||
Allowed target_slot values:
|
||||
top-left, top-right, bottom-left, bottom-right
|
||||
|
||||
Return exactly this JSON shape:
|
||||
{"target_slot":"<allowed value>"}
|
||||
"""
|
||||
generation = self.runtime.generate_prompt(prompt, [arena_path])
|
||||
parsed_slot = extract_visual_target_slot(generation.text)
|
||||
model_target = slot_to_id.get(parsed_slot) if parsed_slot else None
|
||||
proof.model_target_id = model_target
|
||||
proof.passed = model_target == proof.expected_target_id
|
||||
artifact = root / "model_visual_hashhop.json"
|
||||
proof.artifacts.append(str(artifact))
|
||||
write_json(
|
||||
artifact,
|
||||
{
|
||||
"proof": proof.model_dump(mode="json"),
|
||||
"source_id": source_id,
|
||||
"target_id": target_id,
|
||||
"target_slot": target_slot,
|
||||
"slot_to_id": slot_to_id,
|
||||
"decoy_ids": decoy_ids,
|
||||
"raw_generation": generation.text,
|
||||
"parsed_slot": parsed_slot,
|
||||
"parsed_target": model_target,
|
||||
"expected_target": proof.expected_target_id,
|
||||
"passed": proof.passed,
|
||||
"runtime": {
|
||||
"backend": generation.backend,
|
||||
"adapter_path": generation.adapter_path,
|
||||
"adapter_kind": generation.adapter_kind,
|
||||
"adapter_id": generation.adapter_id,
|
||||
"adapter_sha256": generation.adapter_sha256,
|
||||
"input_tokens": generation.input_tokens,
|
||||
"output_tokens": generation.output_tokens,
|
||||
},
|
||||
},
|
||||
)
|
||||
(root / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8")
|
||||
return proof
|
||||
|
||||
def _generate_model_visual_two_hop_proof_once(self, token_equivalent: int) -> HashHopProof:
|
||||
hops = 2
|
||||
proof_id = self._proof_id("visual-model", hops, token_equivalent)
|
||||
root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
image_dir = root / "visual_blocks"
|
||||
image_dir.mkdir(parents=True, exist_ok=True)
|
||||
source_id = secrets.token_hex(16)
|
||||
intermediate_id = secrets.token_hex(16)
|
||||
target_id = secrets.token_hex(16)
|
||||
intermediate_decoys = [secrets.token_hex(16) for _ in range(3)]
|
||||
target_decoys = [secrets.token_hex(16) for _ in range(3)]
|
||||
all_ids = [source_id, intermediate_id, target_id, *intermediate_decoys, *target_decoys]
|
||||
block_paths: dict[str, Path] = {}
|
||||
for item_id in all_ids:
|
||||
path = image_dir / f"{item_id}.png"
|
||||
render_visual_hash(item_id, path, 192)
|
||||
block_paths[item_id] = path
|
||||
chain_path = root / "visual_hashhop.jsonl"
|
||||
chain_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
json.dumps({"from": source_id, "to": intermediate_id}),
|
||||
json.dumps({"from": intermediate_id, "to": target_id}),
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
slots = SLOTS
|
||||
intermediate_candidates = [intermediate_id, *intermediate_decoys]
|
||||
target_candidates = [target_id, *target_decoys]
|
||||
random.Random(f"{proof_id}:intermediate").shuffle(intermediate_candidates)
|
||||
random.Random(f"{proof_id}:target").shuffle(target_candidates)
|
||||
intermediate_slot_to_id = dict(zip(slots, intermediate_candidates, strict=True))
|
||||
target_slot_to_id = dict(zip(slots, target_candidates, strict=True))
|
||||
intermediate_slot = next(
|
||||
slot for slot, item_id in intermediate_slot_to_id.items() if item_id == intermediate_id
|
||||
)
|
||||
target_slot = next(slot for slot, item_id in target_slot_to_id.items() if item_id == target_id)
|
||||
arena_path = root / "visual_hashhop_two_hop_arena.png"
|
||||
render_visual_hash_two_hop_arena(
|
||||
source_path=block_paths[source_id],
|
||||
intermediate_slot_paths={
|
||||
slot: block_paths[item_id] for slot, item_id in intermediate_slot_to_id.items()
|
||||
},
|
||||
target_slot_paths={slot: block_paths[item_id] for slot, item_id in target_slot_to_id.items()},
|
||||
output_path=arena_path,
|
||||
)
|
||||
proof = HashHopProof(
|
||||
proof_id=proof_id,
|
||||
kind="visual_hashhop",
|
||||
hops=hops,
|
||||
token_equivalent=token_equivalent,
|
||||
query_id=source_id,
|
||||
expected_target_id=target_id,
|
||||
artifacts=[*(str(path) for path in block_paths.values()), str(chain_path), str(arena_path)],
|
||||
)
|
||||
prompt = """Use /no_think mode. Return compact JSON only.
|
||||
|
||||
The attached image is a two-hop Visual HashHop arena.
|
||||
The source visual hash block is on the left.
|
||||
Four intermediate candidate visual hash blocks are in the center.
|
||||
Four final candidate visual hash blocks are on the right.
|
||||
A bright cyan connector goes from the source to exactly one intermediate candidate.
|
||||
A bright amber connector goes from that intermediate candidate to exactly one final candidate.
|
||||
Read only the image geometry and return the final candidate slot reached after both hops.
|
||||
The intermediate_slot value describes the center grid. The target_slot value describes the rightmost final-candidate grid.
|
||||
|
||||
Allowed slot values:
|
||||
top-left, top-right, bottom-left, bottom-right
|
||||
|
||||
Return exactly this JSON shape:
|
||||
{"intermediate_slot":"<allowed value>","target_slot":"<allowed value>"}
|
||||
"""
|
||||
generation = self.runtime.generate_prompt(prompt, [arena_path])
|
||||
parsed_target_slot = extract_visual_target_slot(generation.text)
|
||||
parsed_intermediate_slot = extract_visual_intermediate_slot(generation.text)
|
||||
model_target = target_slot_to_id.get(parsed_target_slot) if parsed_target_slot else None
|
||||
proof.model_target_id = model_target
|
||||
proof.passed = model_target == proof.expected_target_id
|
||||
artifact = root / "model_visual_hashhop.json"
|
||||
proof.artifacts.append(str(artifact))
|
||||
write_json(
|
||||
artifact,
|
||||
{
|
||||
"proof": proof.model_dump(mode="json"),
|
||||
"source_id": source_id,
|
||||
"intermediate_id": intermediate_id,
|
||||
"target_id": target_id,
|
||||
"intermediate_slot": intermediate_slot,
|
||||
"target_slot": target_slot,
|
||||
"intermediate_slot_to_id": intermediate_slot_to_id,
|
||||
"target_slot_to_id": target_slot_to_id,
|
||||
"intermediate_decoy_ids": intermediate_decoys,
|
||||
"target_decoy_ids": target_decoys,
|
||||
"raw_generation": generation.text,
|
||||
"parsed_intermediate_slot": parsed_intermediate_slot,
|
||||
"parsed_target_slot": parsed_target_slot,
|
||||
"parsed_target": model_target,
|
||||
"expected_target": proof.expected_target_id,
|
||||
"passed": proof.passed,
|
||||
"runtime": {
|
||||
"backend": generation.backend,
|
||||
"adapter_path": generation.adapter_path,
|
||||
"adapter_kind": generation.adapter_kind,
|
||||
"adapter_id": generation.adapter_id,
|
||||
"adapter_sha256": generation.adapter_sha256,
|
||||
"input_tokens": generation.input_tokens,
|
||||
"output_tokens": generation.output_tokens,
|
||||
},
|
||||
},
|
||||
)
|
||||
(root / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8")
|
||||
return proof
|
||||
|
||||
def generate_visual_proof(self, hops: int, token_equivalent: int, tile_size: int = 256) -> HashHopProof:
|
||||
if hops <= 0:
|
||||
raise ValueError("hops must be positive")
|
||||
proof_id = self._proof_id("visual", hops, token_equivalent)
|
||||
root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
image_dir = root / "visual_blocks"
|
||||
image_dir.mkdir(parents=True, exist_ok=True)
|
||||
ids = [secrets.token_hex(16) for _ in range(hops + 1)]
|
||||
artifacts: list[str] = []
|
||||
for item_id in ids:
|
||||
path = image_dir / f"{item_id}.png"
|
||||
render_visual_hash(item_id, path, tile_size)
|
||||
artifacts.append(str(path))
|
||||
chain_path = root / "visual_hashhop.jsonl"
|
||||
chain_path.write_text(
|
||||
"\n".join(json.dumps({"from": ids[index], "to": ids[index + 1]}) for index in range(hops)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
artifacts.append(str(chain_path))
|
||||
return HashHopProof(
|
||||
proof_id=proof_id,
|
||||
kind="visual_hashhop",
|
||||
hops=hops,
|
||||
token_equivalent=token_equivalent,
|
||||
query_id=ids[0],
|
||||
expected_target_id=ids[-1],
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
def _proof_id(self, kind: str, hops: int, token_equivalent: int) -> str:
|
||||
raw = f"{kind}:{hops}:{token_equivalent}:{secrets.token_hex(8)}"
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Judge a HashHop proof: chain, answer, artifacts, pass rate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from ..evaluation_schemas import (
|
||||
HashHopProof,
|
||||
)
|
||||
from .hashhop import HashHopEvaluator
|
||||
|
||||
|
||||
def hashhop_chain_valid(proof: HashHopProof) -> bool:
|
||||
chain_artifacts = [Path(path) for path in proof.artifacts if path.endswith(".jsonl")]
|
||||
if len(chain_artifacts) != 1 or not chain_artifacts[0].exists():
|
||||
return False
|
||||
try:
|
||||
rows = [
|
||||
json.loads(line)
|
||||
for line in chain_artifacts[0].read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False # an unreadable chain is not a valid chain, and this is a predicate
|
||||
if len(rows) != proof.hops:
|
||||
return False
|
||||
if rows[0]["from"] != proof.query_id or rows[-1]["to"] != proof.expected_target_id:
|
||||
return False
|
||||
for index in range(1, len(rows)):
|
||||
if rows[index - 1]["to"] != rows[index]["from"]:
|
||||
return False
|
||||
return all(Path(path).exists() for path in proof.artifacts)
|
||||
|
||||
|
||||
def copy_proof_artifacts(proof: HashHopProof, target: Path) -> None:
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
source_roots = {Path(path).parent for path in proof.artifacts}
|
||||
for source_root in source_roots:
|
||||
if source_root.name == "visual_blocks":
|
||||
shutil.copytree(source_root, target / "visual_blocks", dirs_exist_ok=True)
|
||||
for artifact in proof.artifacts:
|
||||
path = Path(artifact)
|
||||
if path.is_file() and path.parent.name != "visual_blocks":
|
||||
shutil.copy2(path, target / path.name)
|
||||
(target / "proof.json").write_text(proof.model_dump_json(indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def pass_rate(proofs: list[HashHopProof]) -> float:
|
||||
if not proofs:
|
||||
return 0.0
|
||||
return sum(1 for proof in proofs if proof.passed) / len(proofs)
|
||||
|
||||
|
||||
def collect_model_visual_proofs(
|
||||
evaluator: HashHopEvaluator,
|
||||
hops: int,
|
||||
token_equivalent: int,
|
||||
attempts: int,
|
||||
) -> list[HashHopProof]:
|
||||
"""Run exactly `attempts` puzzles and keep every verdict.
|
||||
|
||||
The old loop stopped the moment the running pass rate crossed its target, so a model could be
|
||||
re-rolled until it passed. A fixed attempt count is what makes the rate mean anything.
|
||||
"""
|
||||
if attempts <= 0:
|
||||
raise ValueError(f"collect needs a positive attempt count, got {attempts}")
|
||||
return [
|
||||
evaluator.generate_model_visual_proof(hops, token_equivalent, max_proofs=5)
|
||||
for _ in range(attempts)
|
||||
]
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Small readers, writers and diggers the proof checks share.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import zstandard as zstd
|
||||
|
||||
|
||||
def parquet_row_count(path: Path) -> int:
|
||||
if not path.exists():
|
||||
return -1
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
return pq.read_metadata(path).num_rows
|
||||
|
||||
|
||||
def compiled_context_digest(context: Any) -> str:
|
||||
payload = {
|
||||
"hits": [hit.atom.atom_id for hit in context.hits],
|
||||
"packed_text": context.packed_text,
|
||||
"token_equivalent": context.token_equivalent,
|
||||
}
|
||||
data = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def read_memory_capsule(path: Path) -> dict[str, Any]:
|
||||
data = path.read_bytes()
|
||||
prefix = b"ELYMEMCAP1\n"
|
||||
if not data.startswith(prefix):
|
||||
raise ValueError(f"Invalid memory capsule header: {path}")
|
||||
try:
|
||||
payload = zstd.ZstdDecompressor().decompress(data[len(prefix) :])
|
||||
parsed = json.loads(payload.decode("utf-8"))
|
||||
except (zstd.ZstdError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise ValueError(f"Cannot decode the memory capsule at {path}: {error}") from error
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError(f"Invalid memory capsule payload: {path}") # noqa: TRY004 # the capsule's own bytes are wrong, not the caller's argument
|
||||
return parsed
|
||||
|
||||
|
||||
def validated_segment_hash(segment: dict[str, Any]) -> str:
|
||||
expected = segment.get("segment_sha256")
|
||||
if not isinstance(expected, str):
|
||||
return ""
|
||||
payload = {key: value for key, value in segment.items() if key != "segment_sha256"}
|
||||
actual = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
||||
return actual if actual == expected else ""
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def read_json_or_empty(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def format_scores(scores: dict[str, Any]) -> str:
|
||||
parts: list[str] = []
|
||||
for name in sorted(scores):
|
||||
score = scores[name]
|
||||
if isinstance(score, (int, float)) and math.isfinite(float(score)):
|
||||
parts.append(f"{name}={float(score):.2f}")
|
||||
else:
|
||||
parts.append(f"{name}=unscored")
|
||||
return ", ".join(parts) or "none"
|
||||
|
||||
|
||||
def is_finite_number(value: Any) -> bool:
|
||||
return isinstance(value, (int, float)) and math.isfinite(float(value))
|
||||
|
||||
|
||||
def compact_generation_context(context: Any, section_hint: str, max_chars: int = 2400) -> Any:
|
||||
text_hits = [hit for hit in context.hits if hit.atom.modality.value == "text"]
|
||||
hits = (text_hits or context.hits)[:1]
|
||||
sections: list[str] = []
|
||||
for hit in hits:
|
||||
atom = hit.atom
|
||||
evidence_text = extract_markdown_section(atom.text, section_hint) or atom.text
|
||||
evidence_text = evidence_text.strip()[:max_chars]
|
||||
citation = {
|
||||
"atom_id": atom.atom_id,
|
||||
"modality": atom.modality.value,
|
||||
"source": atom.source,
|
||||
"image_ref": atom.image_ref,
|
||||
"layout": atom.layout.model_dump() if atom.layout else None,
|
||||
"score": hit.final_score,
|
||||
}
|
||||
sections.append(f"[EVIDENCE]\n{citation}\n{evidence_text}\n[/EVIDENCE]")
|
||||
packed_text = "\n\n".join(sections)
|
||||
return context.model_copy(
|
||||
update={
|
||||
"hits": hits,
|
||||
"packed_text": packed_text,
|
||||
"token_equivalent": max(1, len(packed_text) // 4),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def extract_markdown_section(text: str, section_hint: str) -> str:
|
||||
lines = text.splitlines()
|
||||
start = None
|
||||
for index, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#") and section_hint in stripped:
|
||||
start = index
|
||||
break
|
||||
if start is None:
|
||||
return ""
|
||||
end = len(lines)
|
||||
start_level = len(lines[start]) - len(lines[start].lstrip("#"))
|
||||
for index in range(start + 1, len(lines)):
|
||||
stripped = lines[index].strip()
|
||||
if stripped.startswith("#"):
|
||||
level = len(lines[index]) - len(lines[index].lstrip("#"))
|
||||
if level <= start_level:
|
||||
end = index
|
||||
break
|
||||
return "\n".join(lines[start:end])
|
||||
|
||||
|
||||
def relative_artifacts(root: Path) -> dict[str, str]:
|
||||
artifacts: dict[str, str] = {}
|
||||
for path in sorted(root.rglob("*")):
|
||||
if path.is_file():
|
||||
key = path.relative_to(root).as_posix()
|
||||
artifacts[key] = key
|
||||
return artifacts
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Find the repository the proof suite is allowed to talk about.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def project_root() -> Path:
|
||||
current = Path.cwd().resolve()
|
||||
for candidate in (current, *current.parents):
|
||||
if (candidate / "PRD.md").exists() and (candidate / "backend" / "src" / "ely_eye").exists():
|
||||
return candidate
|
||||
return current
|
||||
|
||||
|
||||
def discover_repository_files(root: Path) -> list[Path]:
|
||||
ignored_dirs = {".git", ".ely_eye", ".venv", ".venv-linux", "__pycache__", "node_modules", "dist", "build", "target"}
|
||||
files: list[Path] = []
|
||||
for current, dirs, names in os.walk(root):
|
||||
dirs[:] = [name for name in dirs if name not in ignored_dirs and not name.startswith(".venv")]
|
||||
current_path = Path(current)
|
||||
for name in names:
|
||||
path = current_path / name
|
||||
try:
|
||||
relative = path.relative_to(root)
|
||||
except ValueError:
|
||||
continue
|
||||
if any(part in ignored_dirs or part.startswith(".venv") for part in relative.parts):
|
||||
continue
|
||||
try:
|
||||
path.stat()
|
||||
except OSError:
|
||||
continue
|
||||
files.append(path)
|
||||
return files
|
||||
@@ -0,0 +1,127 @@
|
||||
"""The proof suite: one check per claim, and a verdict that says what it proves.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from ..cartridge import CartridgeService
|
||||
from ..config import Settings, get_settings
|
||||
from ..db import Database
|
||||
from ..evaluation_schemas import (
|
||||
ProofCheck,
|
||||
ProofSuiteReport,
|
||||
)
|
||||
from ..schemas import (
|
||||
CartridgeManifest,
|
||||
TrainingAdapterKind,
|
||||
)
|
||||
from .checks_cartridge import CartridgeChecks
|
||||
from .checks_runtime import RuntimeChecks
|
||||
from .checks_training import TrainingChecks
|
||||
from .checks_visual import VisualChecks
|
||||
from .report import (
|
||||
relative_artifacts,
|
||||
)
|
||||
|
||||
|
||||
class PRDProofSuite(TrainingChecks, CartridgeChecks, RuntimeChecks, VisualChecks):
|
||||
required_adapters = (
|
||||
TrainingAdapterKind.hme_core_lora,
|
||||
TrainingAdapterKind.hme_vision_lora,
|
||||
TrainingAdapterKind.hme_ttt_vl,
|
||||
TrainingAdapterKind.hme_visual_mtp,
|
||||
TrainingAdapterKind.hme_router,
|
||||
TrainingAdapterKind.hme_retrieval,
|
||||
)
|
||||
|
||||
def __init__(self, settings: Settings | None = None) -> None:
|
||||
self.settings = settings or get_settings()
|
||||
self.db = Database(self.settings)
|
||||
self.cartridges = CartridgeService(self.settings, self.db)
|
||||
|
||||
def run(self, cartridge_id: str | None = None) -> ProofSuiteReport:
|
||||
row = self._resolve_cartridge(cartridge_id)
|
||||
cartridge_id = str(row["cartridge_id"])
|
||||
root = Path(row["root_path"])
|
||||
manifest = CartridgeManifest.model_validate_json(row["manifest_json"])
|
||||
proof_id = self._proof_id(cartridge_id, manifest.dna)
|
||||
proof_root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
proof_root.mkdir(parents=True, exist_ok=True)
|
||||
dna_before = manifest.dna
|
||||
|
||||
asset_check, manifest = self._cartridge_assets_check(cartridge_id, root, proof_root)
|
||||
checks = [
|
||||
self._adapter_training_check(cartridge_id, root, proof_root),
|
||||
self._runtime_adapter_check(proof_root),
|
||||
self._runtime_generation_check(proof_root),
|
||||
asset_check,
|
||||
self._memory_capsule_check(root, proof_root),
|
||||
self._cartridge_replay_check(cartridge_id, root, manifest, proof_root),
|
||||
self._visual_hashhop_check(proof_root),
|
||||
self._standard_benchmark_check(proof_root),
|
||||
self._repository_input_check(proof_root),
|
||||
self._runtime_profile_check(proof_root),
|
||||
self._research_module_check(proof_root),
|
||||
self._learned_memory_recall_check(cartridge_id, root, proof_root),
|
||||
self._memory_dna_check(root, manifest, proof_root),
|
||||
self._cache_trace_check(proof_root),
|
||||
self._visual_contradiction_check(proof_root),
|
||||
]
|
||||
# A deferred or removed check is not a failure and not a pass: the suite is only as green
|
||||
# as the checks that actually ran. A claim that never ran holds the verdict, because
|
||||
# "passed" would say the PRD is proven while part of it was never attempted.
|
||||
failed = [check for check in checks if check.status == "failed"]
|
||||
unrun = [check for check in checks if check.status in ("deferred", "removed")]
|
||||
status: Literal["passed", "failed", "deferred"] = (
|
||||
"failed" if failed else ("deferred" if unrun else "passed")
|
||||
)
|
||||
report = ProofSuiteReport(
|
||||
proof_id=proof_id,
|
||||
cartridge_id=cartridge_id,
|
||||
status=status,
|
||||
checks=checks,
|
||||
artifacts={},
|
||||
dna_before=dna_before,
|
||||
dna_after=dna_before,
|
||||
)
|
||||
report_path = proof_root / "proof_suite.json"
|
||||
report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8")
|
||||
report.artifacts = relative_artifacts(proof_root)
|
||||
report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8")
|
||||
attached_manifest = self.cartridges.attach_eval_proof(cartridge_id, proof_root)
|
||||
report.dna_after = attached_manifest.dna
|
||||
report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8")
|
||||
cartridge_report = root / "eval_proofs" / proof_id / "proof_suite.json"
|
||||
cartridge_report.write_text(report.model_dump_json(indent=2), encoding="utf-8")
|
||||
return report
|
||||
|
||||
def runtime_generation_proof(self) -> ProofCheck:
|
||||
proof_id = f"runtime_generation_{secrets.token_hex(8)}"
|
||||
proof_root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
proof_root.mkdir(parents=True, exist_ok=True)
|
||||
return self._runtime_generation_check(proof_root)
|
||||
|
||||
def visual_contradiction_proof(self) -> ProofCheck:
|
||||
proof_id = f"visual_contradiction_{secrets.token_hex(8)}"
|
||||
proof_root = self.settings.data_dir / "eval_proofs" / proof_id
|
||||
proof_root.mkdir(parents=True, exist_ok=True)
|
||||
return self._visual_contradiction_check(proof_root)
|
||||
|
||||
def latest(self) -> ProofSuiteReport | None:
|
||||
proof_root = self.settings.data_dir / "eval_proofs"
|
||||
reports = sorted(
|
||||
proof_root.glob("prd_proof_suite_*/proof_suite.json"),
|
||||
key=lambda path: path.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
if not reports:
|
||||
return None
|
||||
return ProofSuiteReport.model_validate_json(reports[0].read_text(encoding="utf-8"))
|
||||
|
||||
def _proof_id(self, cartridge_id: str, dna: str | None) -> str:
|
||||
raw = f"{cartridge_id}:{dna}:{secrets.token_hex(8)}"
|
||||
return f"prd_proof_suite_{hashlib.sha256(raw.encode('utf-8')).hexdigest()[:16]}"
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Render a proof report as one row per claim, verdict first.
|
||||
|
||||
A JSON report hides the shape of the evidence: a deferred claim and a passed claim look equally
|
||||
short. The table puts the counts on the first line and the reason under every claim that did not
|
||||
pass, so nobody reads an unrun claim as a proven one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..evaluation_schemas import ProofSuiteReport
|
||||
|
||||
STATUS_ORDER = ("failed", "passed", "deferred", "removed")
|
||||
|
||||
|
||||
def render_table(report: ProofSuiteReport) -> str:
|
||||
counts: dict[str, int] = {status: 0 for status in STATUS_ORDER}
|
||||
for check in report.checks:
|
||||
counts[check.status] = counts.get(check.status, 0) + 1
|
||||
lines = [
|
||||
f"proof {report.proof_id} · cartridge {report.cartridge_id} · suite {report.status}",
|
||||
"counts: " + ", ".join(f"{status} {counts.get(status, 0)}" for status in STATUS_ORDER),
|
||||
"",
|
||||
]
|
||||
for status in STATUS_ORDER:
|
||||
for check in report.checks:
|
||||
if check.status != status:
|
||||
continue
|
||||
lines.append(f"[{check.status:^7}] {check.name}")
|
||||
lines.append(f" ran: {check.detail}")
|
||||
if check.reason:
|
||||
lines.append(f" why: {check.reason}")
|
||||
if check.rerun:
|
||||
lines.append(f" rerun: {check.rerun}")
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,358 @@
|
||||
"""Check that an adapter's training artifacts tell a consistent story.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..schemas import (
|
||||
AdapterManifest,
|
||||
TrainingAdapterKind,
|
||||
)
|
||||
from ..training import (
|
||||
AUTOCAST_DTYPE,
|
||||
GRADIENT_CHECKPOINTING_MODE,
|
||||
TRAINING_FRAMEWORK,
|
||||
TRAINING_METHOD,
|
||||
TRAINING_PRECISION,
|
||||
TRAINING_PROOF_VERSION,
|
||||
safetensor_weight_stats,
|
||||
sha256_file,
|
||||
)
|
||||
|
||||
|
||||
def count_safetensor_keys(path: Path) -> int:
|
||||
return int(safetensor_weight_stats(path)["tensor_count"])
|
||||
|
||||
|
||||
def peft_config_loads(path: Path) -> bool:
|
||||
try:
|
||||
from peft import PeftConfig
|
||||
|
||||
PeftConfig.from_pretrained(str(path))
|
||||
return True
|
||||
except Exception: # noqa: BLE001 # a probe records its failure instead of crashing the proof run
|
||||
return False
|
||||
|
||||
|
||||
def validate_training_contract(manifest: AdapterManifest) -> dict[str, Any]:
|
||||
target_modules_ok = training_target_modules_ok(manifest.kind, manifest.target_modules)
|
||||
report = {
|
||||
"proof_version_ok": manifest.proof_version >= TRAINING_PROOF_VERSION,
|
||||
"method_ok": manifest.training_method == TRAINING_METHOD,
|
||||
"precision_ok": manifest.precision == TRAINING_PRECISION and manifest.bf16,
|
||||
"framework_ok": manifest.framework == TRAINING_FRAMEWORK,
|
||||
"optimizer_ok": manifest.optimizer == "paged_adamw_8bit",
|
||||
"optimizer_family_ok": manifest.optimizer_family == "8-bit AdamW / paged optimizer",
|
||||
"gradient_checkpointing_ok": (
|
||||
manifest.gradient_checkpointing
|
||||
and manifest.gradient_checkpointing_mode == GRADIENT_CHECKPOINTING_MODE
|
||||
),
|
||||
"autocast_dtype_ok": manifest.autocast_dtype == AUTOCAST_DTYPE,
|
||||
"unsloth_version_ok": bool(manifest.unsloth_version),
|
||||
"triton_version_ok": bool(manifest.triton_version),
|
||||
"xformers_version_ok": bool(manifest.xformers_version),
|
||||
"unsloth_model_type_ok": bool(manifest.unsloth_model_type),
|
||||
"max_length_ok": manifest.max_length >= 8192,
|
||||
"target_modules_ok": target_modules_ok,
|
||||
}
|
||||
report["valid"] = all(report.values())
|
||||
return report
|
||||
|
||||
|
||||
def training_target_modules_ok(kind: TrainingAdapterKind, target_modules: list[str]) -> bool:
|
||||
targets = set(target_modules)
|
||||
core = {"q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"}
|
||||
if kind == TrainingAdapterKind.hme_core_lora:
|
||||
return core.issubset(targets)
|
||||
if kind == TrainingAdapterKind.hme_vision_lora:
|
||||
return bool(targets) and any("visual" in target or "vision" in target for target in targets)
|
||||
if kind == TrainingAdapterKind.hme_ttt_vl:
|
||||
has_visual = any("visual" in target or "vision" in target for target in targets)
|
||||
has_mlp = bool({"gate_proj", "up_proj", "down_proj"} & targets)
|
||||
return has_visual and has_mlp
|
||||
if kind == TrainingAdapterKind.hme_visual_mtp:
|
||||
return any("mtp" in target or "draft" in target or target == "lm_head" for target in targets)
|
||||
if kind in {TrainingAdapterKind.hme_router, TrainingAdapterKind.hme_retrieval}:
|
||||
return core.issubset(targets)
|
||||
return False
|
||||
|
||||
|
||||
def training_payload_contract_ok(payload: dict[str, Any], manifest: AdapterManifest) -> bool:
|
||||
return (
|
||||
int(payload.get("proof_version") or 0) >= TRAINING_PROOF_VERSION
|
||||
and payload.get("training_method") == TRAINING_METHOD
|
||||
and payload.get("precision") == TRAINING_PRECISION
|
||||
and payload.get("framework") == TRAINING_FRAMEWORK
|
||||
and payload.get("optimizer_family") == manifest.optimizer_family
|
||||
and bool(payload.get("gradient_checkpointing"))
|
||||
and payload.get("gradient_checkpointing_mode") == GRADIENT_CHECKPOINTING_MODE
|
||||
and payload.get("autocast_dtype") == AUTOCAST_DTYPE
|
||||
and payload.get("unsloth_version") == manifest.unsloth_version
|
||||
and payload.get("triton_version") == manifest.triton_version
|
||||
and payload.get("xformers_version") == manifest.xformers_version
|
||||
and payload.get("unsloth_model_type") == manifest.unsloth_model_type
|
||||
)
|
||||
|
||||
|
||||
def validate_training_trace(manifest: AdapterManifest, adapter_root: Path) -> dict[str, Any]:
|
||||
trace_path = adapter_root / "training_trace.jsonl"
|
||||
summary_path = adapter_root / "training_summary.json"
|
||||
report: dict[str, Any] = {
|
||||
"trace_path": str(trace_path),
|
||||
"summary_path": str(summary_path),
|
||||
"trace_exists": trace_path.exists(),
|
||||
"summary_exists": summary_path.exists(),
|
||||
"trace_sha256_ok": False,
|
||||
"summary_sha256_ok": False,
|
||||
"step_records": 0,
|
||||
"loss_history_records": len(manifest.loss_history),
|
||||
"loss_history_ok": False,
|
||||
"summary_matches_manifest": False,
|
||||
"summary_contract_ok": False,
|
||||
"run_start_contract_ok": False,
|
||||
"run_finish_contract_ok": False,
|
||||
"cuda_training": False,
|
||||
"bf16_supported": False,
|
||||
"bf16": manifest.bf16,
|
||||
"valid": False,
|
||||
}
|
||||
if not trace_path.exists() or not summary_path.exists():
|
||||
return report
|
||||
try:
|
||||
trace_rows = [
|
||||
json.loads(line)
|
||||
for line in trace_path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return report
|
||||
|
||||
step_rows = [row for row in trace_rows if row.get("event") == "step"]
|
||||
start_row: dict[str, Any] = next((row for row in trace_rows if row.get("event") == "run_start"), {})
|
||||
finish_row: dict[str, Any] = next(
|
||||
(row for row in reversed(trace_rows) if row.get("event") == "run_finish"), {}
|
||||
)
|
||||
losses = [row.get("loss") for row in step_rows]
|
||||
finite_losses = all(isinstance(loss, (int, float)) and math.isfinite(float(loss)) for loss in losses)
|
||||
final_loss_ok = (
|
||||
manifest.final_loss is not None
|
||||
and bool(losses)
|
||||
and abs(float(losses[-1]) - float(manifest.final_loss)) < 1e-6
|
||||
)
|
||||
report.update(
|
||||
{
|
||||
"trace_sha256_ok": (
|
||||
manifest.training_trace_sha256 is not None
|
||||
and sha256_file(trace_path) == manifest.training_trace_sha256
|
||||
),
|
||||
"summary_sha256_ok": (
|
||||
manifest.training_summary_sha256 is not None
|
||||
and sha256_file(summary_path) == manifest.training_summary_sha256
|
||||
),
|
||||
"step_records": len(step_rows),
|
||||
"loss_history_ok": (
|
||||
len(manifest.loss_history) == manifest.max_steps
|
||||
and finite_losses
|
||||
and final_loss_ok
|
||||
),
|
||||
"summary_matches_manifest": (
|
||||
summary.get("adapter_kind") == manifest.kind.value
|
||||
and summary.get("base_model") == manifest.base_model
|
||||
and summary.get("dataset_sha256") == manifest.dataset_sha256
|
||||
and int(summary.get("max_steps") or -1) == manifest.max_steps
|
||||
and int(summary.get("trainable_params") or -1) == manifest.trainable_params
|
||||
),
|
||||
"summary_contract_ok": training_payload_contract_ok(summary, manifest),
|
||||
"run_start_contract_ok": training_payload_contract_ok(start_row, manifest),
|
||||
"run_finish_contract_ok": training_payload_contract_ok(finish_row, manifest),
|
||||
}
|
||||
)
|
||||
environment = summary.get("environment") if isinstance(summary.get("environment"), dict) else {}
|
||||
device_name = str(environment.get("device_name") or "")
|
||||
report["cuda_training"] = bool(environment.get("cuda_available")) and "RTX 4090" in device_name
|
||||
report["bf16_supported"] = bool(environment.get("bf16_supported"))
|
||||
report["valid"] = (
|
||||
report["trace_sha256_ok"]
|
||||
and report["summary_sha256_ok"]
|
||||
and report["step_records"] == manifest.max_steps
|
||||
and report["loss_history_ok"]
|
||||
and report["summary_matches_manifest"]
|
||||
and report["summary_contract_ok"]
|
||||
and report["run_start_contract_ok"]
|
||||
and report["run_finish_contract_ok"]
|
||||
and report["cuda_training"]
|
||||
and report["bf16_supported"]
|
||||
and manifest.bf16
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
def validate_training_proof(manifest: AdapterManifest, adapter_root: Path) -> dict[str, Any]:
|
||||
proof_path = adapter_root / "training_proof.json"
|
||||
report: dict[str, Any] = {
|
||||
"proof_path": str(proof_path),
|
||||
"proof_exists": proof_path.exists(),
|
||||
"proof_sha256_ok": False,
|
||||
"hash_changed": False,
|
||||
"optimizer_update_count": manifest.optimizer_update_count,
|
||||
"optimizer_update_count_ok": False,
|
||||
"max_optimizer_update_l2": manifest.max_optimizer_update_l2,
|
||||
"max_optimizer_update_l2_ok": False,
|
||||
"loss_delta": manifest.loss_delta,
|
||||
"loss_delta_finite": False,
|
||||
"updates_have_gradients": False,
|
||||
"updates_have_weight_delta": False,
|
||||
"proof_weight_sha256_ok": False,
|
||||
"proof_weight_stats_ok": False,
|
||||
"proof_cuda_bf16": False,
|
||||
"proof_bf16_supported": False,
|
||||
"proof_contract_ok": False,
|
||||
"proof_optimizer_ok": False,
|
||||
"proof_max_length_ok": False,
|
||||
"proof_target_modules_ok": False,
|
||||
"proof_unsloth_runtime_ok": False,
|
||||
"proof_matches_manifest": False,
|
||||
"valid": False,
|
||||
}
|
||||
if not proof_path.exists():
|
||||
return report
|
||||
try:
|
||||
proof = json.loads(proof_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return report
|
||||
|
||||
updates = proof.get("optimizer_updates") if isinstance(proof.get("optimizer_updates"), list) else []
|
||||
update_l2_values = [
|
||||
float(value)
|
||||
for item in updates
|
||||
if isinstance(item, dict) and isinstance(value := item.get("update_l2"), (int, float))
|
||||
]
|
||||
gradient_l2_values = [
|
||||
float(value)
|
||||
for item in updates
|
||||
if isinstance(item, dict) and isinstance(value := item.get("gradient_l2"), (int, float))
|
||||
]
|
||||
weight_report = safetensor_weight_stats(adapter_root / "adapter_model.safetensors")
|
||||
proof_weight_stats = proof.get("adapter_weight_stats") if isinstance(proof.get("adapter_weight_stats"), dict) else {}
|
||||
proof_environment = proof.get("environment") if isinstance(proof.get("environment"), dict) else {}
|
||||
report.update(
|
||||
{
|
||||
"proof_sha256_ok": (
|
||||
manifest.training_proof_sha256 is not None
|
||||
and sha256_file(proof_path) == manifest.training_proof_sha256
|
||||
),
|
||||
"hash_changed": (
|
||||
bool(manifest.initial_trainable_sha256)
|
||||
and bool(manifest.final_trainable_sha256)
|
||||
and manifest.initial_trainable_sha256 != manifest.final_trainable_sha256
|
||||
),
|
||||
"optimizer_update_count_ok": (
|
||||
manifest.optimizer_update_count > 0
|
||||
and len(updates) == manifest.optimizer_update_count
|
||||
),
|
||||
"max_optimizer_update_l2_ok": (
|
||||
manifest.max_optimizer_update_l2 is not None
|
||||
and math.isfinite(float(manifest.max_optimizer_update_l2))
|
||||
and float(manifest.max_optimizer_update_l2) > 0.0
|
||||
),
|
||||
"loss_delta_finite": (
|
||||
manifest.loss_delta is not None
|
||||
and math.isfinite(float(manifest.loss_delta))
|
||||
),
|
||||
"updates_have_gradients": bool(gradient_l2_values)
|
||||
and all(math.isfinite(value) and value > 0.0 for value in gradient_l2_values),
|
||||
"updates_have_weight_delta": bool(update_l2_values)
|
||||
and all(math.isfinite(value) and value > 0.0 for value in update_l2_values),
|
||||
"proof_weight_sha256_ok": (
|
||||
isinstance(proof.get("adapter_weight_sha256"), str)
|
||||
and proof.get("adapter_weight_sha256") == weight_report["sha256"]
|
||||
),
|
||||
"proof_weight_stats_ok": (
|
||||
proof_weight_stats.get("sha256") == weight_report["sha256"]
|
||||
and proof_weight_stats.get("tensor_count") == weight_report["tensor_count"]
|
||||
and proof_weight_stats.get("lora_tensor_count") == weight_report["lora_tensor_count"]
|
||||
and proof_weight_stats.get("total_elements") == weight_report["total_elements"]
|
||||
and proof_weight_stats.get("nonzero_elements") == weight_report["nonzero_elements"]
|
||||
and int(proof_weight_stats.get("lora_tensor_count") or 0) > 0
|
||||
and int(proof_weight_stats.get("nonzero_elements") or 0) > 0
|
||||
and bool(proof_weight_stats.get("all_finite"))
|
||||
),
|
||||
"proof_cuda_bf16": (
|
||||
bool(proof.get("bf16"))
|
||||
and bool(proof_environment.get("cuda_available"))
|
||||
and "RTX 4090" in str(proof_environment.get("device_name") or "")
|
||||
),
|
||||
"proof_bf16_supported": bool(proof_environment.get("bf16_supported")),
|
||||
"proof_contract_ok": training_payload_contract_ok(proof, manifest),
|
||||
"proof_optimizer_ok": proof.get("optimizer") == "paged_adamw_8bit",
|
||||
"proof_max_length_ok": int(proof.get("max_length") or 0) >= 8192,
|
||||
"proof_target_modules_ok": training_target_modules_ok(
|
||||
manifest.kind,
|
||||
proof.get("target_modules") if isinstance(proof.get("target_modules"), list) else [],
|
||||
),
|
||||
"proof_unsloth_runtime_ok": (
|
||||
bool(proof.get("unsloth_version"))
|
||||
and bool(proof.get("triton_version"))
|
||||
and bool(proof.get("xformers_version"))
|
||||
and bool(proof.get("unsloth_model_type"))
|
||||
),
|
||||
"proof_matches_manifest": (
|
||||
proof.get("adapter_kind") == manifest.kind.value
|
||||
and proof.get("proof_version") == manifest.proof_version
|
||||
and proof.get("training_method") == manifest.training_method
|
||||
and proof.get("precision") == manifest.precision
|
||||
and proof.get("framework") == manifest.framework
|
||||
and proof.get("base_model") == manifest.base_model
|
||||
and proof.get("dataset_sha256") == manifest.dataset_sha256
|
||||
and proof.get("sample_count") == manifest.sample_count
|
||||
and proof.get("max_steps") == manifest.max_steps
|
||||
and proof.get("max_length") == manifest.max_length
|
||||
and proof.get("rank") == manifest.rank
|
||||
and proof.get("alpha") == manifest.alpha
|
||||
and proof.get("target_modules") == manifest.target_modules
|
||||
and proof.get("optimizer") == manifest.optimizer
|
||||
and proof.get("optimizer_family") == manifest.optimizer_family
|
||||
and proof.get("learning_rate") == manifest.learning_rate
|
||||
and proof.get("gradient_accumulation_steps") == manifest.gradient_accumulation_steps
|
||||
and proof.get("gradient_checkpointing") == manifest.gradient_checkpointing
|
||||
and proof.get("gradient_checkpointing_mode") == manifest.gradient_checkpointing_mode
|
||||
and proof.get("autocast_dtype") == manifest.autocast_dtype
|
||||
and proof.get("unsloth_version") == manifest.unsloth_version
|
||||
and proof.get("triton_version") == manifest.triton_version
|
||||
and proof.get("xformers_version") == manifest.xformers_version
|
||||
and proof.get("unsloth_model_type") == manifest.unsloth_model_type
|
||||
and proof.get("bf16") == manifest.bf16
|
||||
and proof.get("trainable_params") == manifest.trainable_params
|
||||
and proof.get("total_params") == manifest.total_params
|
||||
and proof.get("initial_trainable_sha256") == manifest.initial_trainable_sha256
|
||||
and proof.get("final_trainable_sha256") == manifest.final_trainable_sha256
|
||||
and proof.get("optimizer_update_count") == manifest.optimizer_update_count
|
||||
and proof.get("max_optimizer_update_l2") == manifest.max_optimizer_update_l2
|
||||
),
|
||||
}
|
||||
)
|
||||
report["valid"] = (
|
||||
report["proof_sha256_ok"]
|
||||
and report["hash_changed"]
|
||||
and report["optimizer_update_count_ok"]
|
||||
and report["max_optimizer_update_l2_ok"]
|
||||
and report["loss_delta_finite"]
|
||||
and report["updates_have_gradients"]
|
||||
and report["updates_have_weight_delta"]
|
||||
and report["proof_weight_sha256_ok"]
|
||||
and report["proof_weight_stats_ok"]
|
||||
and report["proof_cuda_bf16"]
|
||||
and report["proof_bf16_supported"]
|
||||
and report["proof_contract_ok"]
|
||||
and report["proof_optimizer_ok"]
|
||||
and report["proof_max_length_ok"]
|
||||
and report["proof_target_modules_ok"]
|
||||
and report["proof_unsloth_runtime_ok"]
|
||||
and report["proof_matches_manifest"]
|
||||
)
|
||||
return report
|
||||
@@ -38,11 +38,19 @@ def fts_query(query: str) -> str:
|
||||
A CJK run becomes a quoted phrase so its characters stay adjacent: unphrased, a query for
|
||||
钥匙 would match text that mentions 钥 and 匙 worlds apart. Latin tokens get a prefix match,
|
||||
which is what lets `atom` reach `atoms`.
|
||||
|
||||
Latin tokens are OR-ed, not AND-ed. A question is longer than the sentence that answers it —
|
||||
"Which PRD requirements prove long context memory, HashHop, and adapters?" shares only some of
|
||||
its words with any single atom — and an AND over every word answers such a question with
|
||||
nothing. bm25 then ranks the candidates by how many of those words each one carries, so the
|
||||
broadened query does not mean a diluted answer.
|
||||
"""
|
||||
terms = _bare(query).split()
|
||||
if not terms:
|
||||
raise ValueError(f"search needs at least one searchable word, got {query!r}")
|
||||
return " ".join(_cjk_phrase(term) if _is_cjk(term) else f"{_quote(term)}*" for term in terms)
|
||||
if len(terms) == 1:
|
||||
return _cjk_phrase(terms[0]) if _is_cjk(terms[0]) else f"{_quote(terms[0])}*"
|
||||
return " OR ".join(_cjk_phrase(term) if _is_cjk(term) else f"{_quote(term)}*" for term in terms)
|
||||
|
||||
|
||||
def _bare(text: str) -> str:
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
"""A proof check reports what it measured: passed, failed, deferred, or removed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ely_eye.cartridge import CartridgeService
|
||||
from ely_eye.config import Settings
|
||||
from ely_eye.memory import MemoryService
|
||||
from ely_eye.proofs import PRDProofSuite
|
||||
from ely_eye.proofs.arena import (
|
||||
_slot_boxes,
|
||||
render_visual_hash,
|
||||
render_visual_hash_arena,
|
||||
)
|
||||
from ely_eye.proofs.checks_common import _deferred_without_model
|
||||
from ely_eye.proofs.contradictions import (
|
||||
PACKAGED_CORPUS,
|
||||
build_contradiction_scenarios,
|
||||
load_corpus,
|
||||
)
|
||||
from ely_eye.proofs.table import render_table
|
||||
from ely_eye.schemas import MemoryOrigin
|
||||
|
||||
# The replay question is fixed inside the suite, so the fixture memory has to answer it in words
|
||||
# the ranker can match.
|
||||
PRD_MEMORY = "PRD requirements prove long context memory, HashHop proof, and adapter training."
|
||||
OTHER_KEYS = "PRD requirements also prove cache invalidation for HashHop proof and adapters."
|
||||
|
||||
|
||||
class Harness:
|
||||
"""A suite wired to one temporary store, plus the directory its artifacts land in."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self.memory = MemoryService(settings=settings)
|
||||
self.db = self.memory.db
|
||||
self.cartridges = CartridgeService(settings=settings, database=self.db)
|
||||
self.suite = PRDProofSuite(settings)
|
||||
self.proof_root = settings.data_dir / "eval_proofs" / "test"
|
||||
self.proof_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def remember(self, text: str) -> str:
|
||||
atom = self.memory.remember(text, holder_id="wife", origin=MemoryOrigin.self_, scenario="home")
|
||||
return atom.atom_id
|
||||
|
||||
def cartridge(self, name: str, atom_ids: list[str]) -> str:
|
||||
atoms = self.db.get_atoms(atom_ids)
|
||||
sources = self.db.get_sources(sorted({atom.source_id for atom in atoms}))
|
||||
return self.cartridges.materialize(name, atoms, sources).cartridge_id
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def harness(tmp_path: Path, model_dir: Path) -> Harness:
|
||||
return Harness(Settings(home=tmp_path / "home", model_dir=model_dir, default_holder="wife"))
|
||||
|
||||
|
||||
def test_a_store_with_nothing_to_ask_fails_the_cache_trace_check(harness: Harness) -> None:
|
||||
check = harness.suite._cache_trace_check(harness.proof_root)
|
||||
|
||||
assert check.status == "failed"
|
||||
assert "empty store" in check.reason
|
||||
|
||||
|
||||
def test_the_cache_trace_check_requires_a_measured_miss_then_hit(harness: Harness) -> None:
|
||||
harness.remember(PRD_MEMORY)
|
||||
|
||||
check = harness.suite._cache_trace_check(harness.proof_root)
|
||||
|
||||
assert check.status == "passed", check.detail
|
||||
assert check.reason == ""
|
||||
|
||||
|
||||
def test_the_research_module_check_is_removed_because_it_could_not_fail(
|
||||
harness: Harness,
|
||||
) -> None:
|
||||
check = harness.suite._research_module_check(harness.proof_root)
|
||||
|
||||
assert check.status == "removed"
|
||||
assert check.reason and check.rerun
|
||||
|
||||
|
||||
def test_the_repository_check_fails_when_the_repository_never_reached_the_store(
|
||||
harness: Harness,
|
||||
) -> None:
|
||||
check = harness.suite._repository_input_check(harness.proof_root)
|
||||
|
||||
assert check.status == "failed"
|
||||
assert "store" in check.detail or "store" in check.reason
|
||||
|
||||
|
||||
def test_the_replay_check_answers_only_from_the_cartridge_it_mounts(harness: Harness) -> None:
|
||||
inside = harness.remember(PRD_MEMORY)
|
||||
# A second memory that answers the same question but stays out of the cartridge: an unmounted
|
||||
# replay against the live store would have found it and called that a cartridge replay.
|
||||
harness.remember(OTHER_KEYS)
|
||||
cartridge_id = harness.cartridge("keys", [inside])
|
||||
manifest = next(
|
||||
item for item in harness.cartridges.list_manifests() if item.cartridge_id == cartridge_id
|
||||
)
|
||||
|
||||
check = harness.suite._cartridge_replay_check(
|
||||
cartridge_id, harness.settings.cartridge_dir / cartridge_id, manifest, harness.proof_root
|
||||
)
|
||||
|
||||
assert check.status == "passed", check.detail
|
||||
assert harness.cartridges.mount_state().mounted == []
|
||||
|
||||
|
||||
def test_a_check_defers_loudly_when_the_model_it_needs_is_missing(
|
||||
tmp_path: Path, model_dir: Path
|
||||
) -> None:
|
||||
without = _deferred_without_model(
|
||||
Settings(home=tmp_path / "a"),
|
||||
tmp_path / "a" / "artifact.json",
|
||||
"Runtime Adapter Binding",
|
||||
"PRD 18 requires the trained adapter to run.",
|
||||
"ely-eye proof runtime with model_dir set",
|
||||
)
|
||||
with_model = _deferred_without_model(
|
||||
Settings(home=tmp_path / "b", model_dir=model_dir),
|
||||
tmp_path / "b" / "artifact.json",
|
||||
"Runtime Adapter Binding",
|
||||
"PRD 18 requires the trained adapter to run.",
|
||||
"ely-eye proof runtime with model_dir set",
|
||||
)
|
||||
|
||||
assert without is not None and without.status == "deferred"
|
||||
assert without.rerun == "ely-eye proof runtime with model_dir set"
|
||||
assert (tmp_path / "a" / "artifact.json").exists()
|
||||
assert with_model is None
|
||||
|
||||
|
||||
def test_the_table_shows_the_verdict_counts_and_the_reason_for_each_claim() -> None:
|
||||
from ely_eye.evaluation_schemas import ProofCheck, ProofSuiteReport
|
||||
|
||||
report = ProofSuiteReport(
|
||||
proof_id="proof-1",
|
||||
cartridge_id="cart-1",
|
||||
status="deferred",
|
||||
artifacts={},
|
||||
checks=[
|
||||
ProofCheck(name="Ran", requirement="r", status="passed", evidence=[], detail="measured x"),
|
||||
ProofCheck(
|
||||
name="Waiting",
|
||||
requirement="r",
|
||||
status="deferred",
|
||||
evidence=[],
|
||||
detail="nothing ran",
|
||||
reason="no model weights",
|
||||
rerun="ely-eye proof suite",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
table = render_table(report)
|
||||
|
||||
assert "counts: failed 0, passed 1, deferred 1, removed 0" in table
|
||||
assert "why: no model weights" in table
|
||||
assert "rerun: ely-eye proof suite" in table
|
||||
assert table.index("[passed ") < table.index("[deferred")
|
||||
assert "[failed" not in table and "[removed" not in table
|
||||
|
||||
|
||||
def test_the_latest_command_prints_the_table_and_exits_by_verdict(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from ely_eye import cli as cli_module
|
||||
from ely_eye.evaluation_schemas import ProofCheck, ProofSuiteReport
|
||||
|
||||
settings = Settings(home=tmp_path / "home")
|
||||
monkeypatch.setattr(cli_module, "get_settings", lambda: settings)
|
||||
proof_root = settings.data_dir / "eval_proofs"
|
||||
ran = ProofCheck(name="Ran", requirement="r", status="passed", evidence=[], detail="ok")
|
||||
cases: list[tuple[str, Literal["failed", "deferred"]]] = [
|
||||
("prd_proof_suite_old", "failed"),
|
||||
("prd_proof_suite_new", "deferred"),
|
||||
]
|
||||
for proof_id, status in cases:
|
||||
report = ProofSuiteReport(
|
||||
proof_id=proof_id,
|
||||
cartridge_id="cart-1",
|
||||
status=status,
|
||||
artifacts={},
|
||||
checks=[ran],
|
||||
)
|
||||
directory = proof_root / proof_id
|
||||
directory.mkdir(parents=True)
|
||||
(directory / "proof_suite.json").write_text(report.model_dump_json(), encoding="utf-8")
|
||||
|
||||
table = CliRunner().invoke(cli_module.app, ["latest-proof-suite"])
|
||||
machine = CliRunner().invoke(cli_module.app, ["latest-proof-suite", "--json"])
|
||||
|
||||
assert table.exit_code == 0
|
||||
assert "proof prd_proof_suite_new" in table.stdout
|
||||
assert json.loads(machine.stdout)["proof_id"] == "prd_proof_suite_new"
|
||||
|
||||
(proof_root / "prd_proof_suite_new" / "proof_suite.json").write_text(
|
||||
ProofSuiteReport(
|
||||
proof_id="prd_proof_suite_new",
|
||||
cartridge_id="cart-1",
|
||||
status="failed",
|
||||
artifacts={},
|
||||
checks=[ran],
|
||||
).model_dump_json(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
failing = CliRunner().invoke(cli_module.app, ["latest-proof-suite"])
|
||||
|
||||
assert failing.exit_code == 1
|
||||
|
||||
monkeypatch.setattr(cli_module, "get_settings", lambda: Settings(home=tmp_path / "empty"))
|
||||
nothing = CliRunner().invoke(cli_module.app, ["latest-proof-suite"])
|
||||
|
||||
assert nothing.exit_code != 0
|
||||
assert "no proof suite" in nothing.stdout + nothing.stderr # says so instead of printing null
|
||||
|
||||
|
||||
def test_the_packaged_corpus_matches_the_scenarios_it_was_written_from() -> None:
|
||||
scenarios, digest = load_corpus(PACKAGED_CORPUS)
|
||||
built = build_contradiction_scenarios()
|
||||
|
||||
assert [item["name"] for item in scenarios] == [item["name"] for item in built]
|
||||
assert [item["expect_drift"] for item in scenarios] == [
|
||||
item["expect_drift"] for item in built
|
||||
]
|
||||
assert len(scenarios) >= 10 and digest
|
||||
|
||||
|
||||
def test_the_contradiction_check_defers_when_no_corpus_exists_and_names_the_file(
|
||||
harness: Harness, monkeypatch
|
||||
) -> None:
|
||||
from ely_eye.proofs import checks_visual as visual_module
|
||||
|
||||
monkeypatch.setattr(visual_module, "corpus_path", lambda data_dir: None)
|
||||
|
||||
check = harness.suite._visual_contradiction_check(harness.proof_root)
|
||||
|
||||
assert check.status == "deferred"
|
||||
assert "contradiction_scenarios.json" in check.rerun
|
||||
assert check.detail == "no contradiction corpus on disk"
|
||||
|
||||
|
||||
def test_the_contradiction_check_records_which_corpus_it_graded_against(
|
||||
harness: Harness,
|
||||
) -> None:
|
||||
import json
|
||||
|
||||
check = harness.suite._visual_contradiction_check(harness.proof_root)
|
||||
artifact = json.loads(
|
||||
(harness.proof_root / "visual_contradiction_lens.json").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
assert check.status == "deferred" # graded on our own corpus: reported, not claimed
|
||||
assert artifact["corpus_sha256"]
|
||||
assert len(artifact["metrics"]["detected_categories"]) >= 5, artifact["metrics"]
|
||||
|
||||
|
||||
def test_the_hashhop_arena_marks_no_candidate_as_the_answer(harness: Harness, tmp_path: Path) -> None:
|
||||
boxes = _slot_boxes(520, 704, 76, 324, 160)
|
||||
tiles = {}
|
||||
for slot in boxes:
|
||||
path = tmp_path / f"{slot}.png"
|
||||
render_visual_hash(slot, path, 160)
|
||||
tiles[slot] = path
|
||||
output = tmp_path / "arena.png"
|
||||
|
||||
render_visual_hash_arena(tiles["top-left"], tiles, output)
|
||||
|
||||
# Every candidate box carries the same outline: a ring, an arrow, or a highlighted slot would
|
||||
# tell a model the answer without reading a pixel of the source tile.
|
||||
image = Image.open(output).convert("RGB")
|
||||
border_colors = {image.getpixel((box[0] + 2, box[1] + 40)) for box in boxes.values()}
|
||||
assert border_colors == {(180, 190, 205)}
|
||||
@@ -24,6 +24,16 @@ def _hits(connection: sqlite3.Connection, query: str) -> list[str]:
|
||||
return [row[0] for row in connection.execute("SELECT original FROM fts WHERE fts MATCH ?", (fts_query(query),))]
|
||||
|
||||
|
||||
def _ranked(connection: sqlite3.Connection, query: str) -> list[str]:
|
||||
"""Matches in bm25 order: the answer that covers more of the question should come first."""
|
||||
return [
|
||||
row[0]
|
||||
for row in connection.execute(
|
||||
"SELECT original FROM fts WHERE fts MATCH ? ORDER BY rank", (fts_query(query),)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_index_text_splits_a_chinese_sentence_into_characters() -> None:
|
||||
assert index_text("我们的钥匙放在玄关抽屉里") == "我 们 的 钥 匙 放 在 玄 关 抽 屉 里"
|
||||
|
||||
@@ -40,7 +50,28 @@ def test_index_text_folds_full_width_letters_that_search_would_miss() -> None:
|
||||
|
||||
def test_fts_query_phrases_cjk_and_prefixes_latin() -> None:
|
||||
assert fts_query("钥匙") == '"钥 匙"'
|
||||
assert fts_query("search atoms") == '"search"* "atoms"*'
|
||||
assert fts_query("search atoms") == '"search"* OR "atoms"*'
|
||||
|
||||
|
||||
def test_a_long_question_reaches_the_answer_that_covers_most_of_its_words() -> None:
|
||||
"""AND over every word answers a long question with nothing; bm25 picks the best candidate."""
|
||||
connection = _fts(
|
||||
[
|
||||
"PRD requirements prove long context memory, HashHop proof, and adapter training.",
|
||||
"The adapter was trained for 29 steps on 37 samples.",
|
||||
"Unrelated notes about the weather.",
|
||||
]
|
||||
)
|
||||
question = "Which Ely-Eye PRD requirements prove long context memory, HashHop, and adapters?"
|
||||
hits = [
|
||||
row[0]
|
||||
for row in connection.execute(
|
||||
"SELECT original FROM fts WHERE fts MATCH ? ORDER BY rank", (fts_query(question),)
|
||||
)
|
||||
]
|
||||
|
||||
assert hits[0].startswith("PRD requirements prove")
|
||||
assert "Unrelated notes about the weather." not in hits
|
||||
|
||||
|
||||
def test_fts_query_refuses_a_query_with_nothing_to_match() -> None:
|
||||
@@ -70,6 +101,10 @@ def test_a_prefix_reaches_the_word_it_grew_into() -> None:
|
||||
assert _hits(connection, "forg") == ["the atom was forgotten"]
|
||||
|
||||
|
||||
def test_a_mixed_query_needs_both_halves_in_the_same_memory() -> None:
|
||||
def test_a_mixed_query_ranks_the_memory_holding_both_halves_first() -> None:
|
||||
"""A query with a Chinese half and an English half must prefer the memory holding both."""
|
||||
connection = _fts(["钥匙放在 drawer 里", "钥匙在玄关", "the drawer is empty"])
|
||||
assert _hits(connection, "钥匙 drawer") == ["钥匙放在 drawer 里"]
|
||||
ranked = _ranked(connection, "钥匙 drawer")
|
||||
|
||||
assert ranked[0] == "钥匙放在 drawer 里"
|
||||
assert set(ranked) == {"钥匙放在 drawer 里", "钥匙在玄关", "the drawer is empty"}
|
||||
Reference in new issue
Block a user