Files
club-3090/scripts/lib/profiles/loop_input.py
T

375 lines
16 KiB
Python

"""v0.8.0 Loop `[F]` — STEP F1: the `FInput` capture-bundle reader.
CONTRACT-1 (the LOCKED brief's F1 spec — `/opt/ai/docs/v0.8.0-loop-brief.md`
"## CONTRACT 1"). `[E]` (`capture.py`) is the **producer**; this module is
the **consumer**: it parses ONE on-disk capture directory into a validated
`FInput` object that the later `[F]` STEPs (F2 classifier / F4 trust pipeline
/ F5 dedup) consume. F1 is a STRICT boundary validator — any schema/shape
violation raises a clear typed `CaptureBundleError` (never a silent partial
parse; downstream STEPs trust this object).
This module owns ONLY:
* `FInput` — the parsed-bundle dataclass (CONTRACT-1 shape);
* `read_capture_bundle()`— parse + hard-validate one capture dir;
* the CONTRACT-1 key-normalization helpers + the §6.2 consensus 9-tuple
and §6.3 dedup 7-tuple builders (pure functions over the manifest —
`[F]` owns the canonical construction; `[E]` does NOT normalize).
It is PURE-PYTHON, stdlib only (json / pathlib / dataclasses / hashlib),
matching the no-external-deps style of its `scripts/lib/profiles/` siblings
(`capture.py`, `deriver.py`). It touches NO shipped code — F1 is purely
additive (two NEW files only).
Binding rules enforced in code + comment (CONTRACT-1):
* F1 MUST NOT treat `manifest["outcome"]` as the §6.1 class enum — it is
`[E]`'s interim honest 3-state (`failed > partial > ok`,
`capture.py:524-534`). There is deliberately NO accessor here that
claims `outcome` is the §6.1 class; `[F]` derives `failure_class` itself
in a later STEP (F2/F3). The raw value is surfaced (`outcome` property)
but never re-interpreted as a class.
* `failure_class` is authoritatively `null` in EVERY `[E]` manifest
(`capture.py:569`) — F1 just SURFACES that null; it never expects
`[E]` to have pre-classified.
* Key normalization is `[F]`'s job: `quant_label` is the raw
`weight_format` case-as-emitted (`capture.py:389-391`) and is
LOWERCASED for keying here. `arch_family` is used VERBATIM — it is
`config.json["architectures"][0]` (`deriver.py:679-680`), already an
exact identifier, NOT re-normalized.
* `model_id ≡ manifest["model"]` and `engine_version ≡
manifest["engine_pin"]` are shipped aliases (`capture.py:553/568`,
`497-500`) — exposed here as the canonical accessors.
* `topology_class` (coarse `NxVRAMMiB`) and `topology_summary_canonical`
(full sorted `(name,vram)` list) are deliberately two resolutions:
keys use `topology_class`; `submission_fingerprint` uses
`topology_summary_canonical`. Not interchangeable.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
# The schema version `[E]` stamps (`capture.py:42` `SCHEMA = 1`). F1 hard-
# asserts the bundle is this exact schema — a strict boundary refuses an
# unrecognised schema rather than mis-parsing a future shape.
EXPECTED_SCHEMA = 1
class CaptureBundleError(Exception):
"""Raised on ANY capture-bundle schema/shape violation.
F1 is a strict boundary validator: a malformed / wrong-schema / shape-
violating bundle MUST raise this (never a silent partial `FInput`) so
the downstream `[F]` STEPs can trust every field on a returned object.
"""
# Required per-point capture artifacts and the `point` value each must
# carry (`capture.py:426-430`, `442-489`). pt5 is intentionally NOT here —
# it is conditional (override-accepted only, `pull.py:1082`).
_REQUIRED_POINTS = {
"pt1-gate.json": "gate",
"pt2-download.json": "download",
"pt3-boot.json": "boot",
"pt4-smoke.json": "smoke",
}
_PT5_FILENAME = "pt5-override-capture.json"
_MANIFEST_FILENAME = "manifest.json"
@dataclass
class FInput:
"""Parsed from a SINGLE capture dir (CONTRACT-1).
`manifest` — manifest.json (schema==1 hard-asserted). All
consensus/dedup inputs are FIRST-CLASS keys here.
`pt1_gate` ... `pt4_smoke` — the 4 §6 capture-point artifacts (required).
`pt5_override` — pt5-override-capture.json as a dict, or None when the
file is absent (present iff override-accepted path).
`raw_bundle_path` — the OPTIONAL `report.sh --redact` human-triage
attachment. **NOT produced by `[E]`** (`capture.py`
writes no such file — CONTRACT-1 / Kimi-r1 L1); it is
an externally-supplied maintainer attachment only.
F1 never synthesizes it: it defaults to None and is
only ever the explicit value the caller passed.
"""
manifest: dict
pt1_gate: dict
pt2_download: dict
pt3_boot: dict
pt4_smoke: dict
pt5_override: Optional[dict] = None
# NOT produced by [E] — optional externally-supplied attachment only.
raw_bundle_path: Optional[Path] = None
# ---- CONTRACT-1 canonical accessors -------------------------------
# These are the SHIPPED aliases (`capture.py:553/568`, `497-500`);
# exposed as the canonical names so downstream STEPs do not re-derive.
@property
def model_id(self) -> str:
"""`model_id ≡ manifest["model"]` (shipped alias)."""
return self.manifest["model"]
@property
def engine_version(self) -> str:
"""`engine_version ≡ manifest["engine_pin"]` (shipped alias)."""
return self.manifest["engine_pin"]
@property
def quant_label(self) -> str:
"""Normalized (LOWERCASED) `quant_label` for keying.
Raw `manifest["quant_label"]` is `weight_format` case-as-emitted
(`capture.py:389-391`); CONTRACT-1 binds `[F]` to lowercase it for
every key it builds. Use this — never the raw manifest value — when
constructing consensus / dedup keys.
"""
return _norm_quant(self.manifest["quant_label"])
@property
def arch_family(self) -> str:
"""`arch_family` used VERBATIM (CONTRACT-1 G3 RESOLVED).
It is `config.json["architectures"][0]` (`deriver.py:679-680`) —
already an exact identifier, NOT a normalized family. F1 must NOT
re-normalize it.
"""
return self.manifest["arch_family"]
@property
def failure_class(self) -> None:
"""Authoritatively `None` in every `[E]` manifest (`capture.py:569`).
F1 only SURFACES this null — `[F]` computes the real
`failure_class` in a later STEP (F2/F3). Never expect `[E]` to have
pre-classified.
"""
return self.manifest["failure_class"]
@property
def outcome(self) -> str:
"""The RAW `[E]` interim 3-state `outcome` (`failed|partial|ok`).
BINDING RULE (CONTRACT-1): this is `[E]`'s honest interim signal
ONLY (`capture.py:521-534`) — it is **NOT** the §6.1 class enum.
There is deliberately NO accessor on `FInput` that claims `outcome`
is the §6.1 class; `[F]` derives `failure_class` itself downstream.
Surfaced here so STEPs can read the raw value, never re-interpret.
"""
return self.manifest["outcome"]
# ---- CONTRACT-3 / CONTRACT-4 canonical key builders ---------------
def consensus_key(self) -> tuple:
"""The §6.2 consensus 9-tuple (CONTRACT-3, verbatim order).
`(model, quant_label, arch_family, topology_class,
engine_version/pin, kv_calc_version, selected_ctx, kv_format,
smoke_capability_set)` — normalization applied (`quant_label`
lowercased; `arch_family` verbatim; `engine_version≡engine_pin`,
`model≡model_id` per CONTRACT-1). A DIFFERENT key from §6.3 by
design (Codex-r4 M2) — it validates *success* anchors, carrying
`selected_ctx`/`kv_format`/`smoke_capability_set` and NO
`failure_class`. `smoke_capability_set` is a tuple (hashable /
order-stable: it ships sorted, `capture.py:259`).
"""
m = self.manifest
scs = m["smoke_capability_set"]
return (
m["model"],
_norm_quant(m["quant_label"]),
m["arch_family"],
m["topology_class"],
m["engine_pin"],
m["kv_calc_version"],
m["selected_ctx"],
m["kv_format"],
tuple(scs) if scs is not None else (),
)
def dedup_tuple(self) -> tuple:
"""The §6.3 dedup 7-tuple (CONTRACT-4, verbatim order).
`(model_id, quant_label, arch_family, kv_calc_version,
engine_version, failure_class, topology_class)` — normalized
(`quant_label` lowercased; `arch_family` verbatim;
`engine_version≡engine_pin`, `model_id≡model`). A DIFFERENT key
from §6.2 by design — it dedups *failure* issues, carrying
`failure_class` and NO ctx/KV/smoke. `failure_class` is `None`
here on every `[E]` manifest; `[F]` fills it in a later STEP and
re-builds this tuple — its presence IN the tuple is the §6.1
mislabel safeguard (a misclassification yields a different tuple,
can't silently merge with a real OOM).
"""
m = self.manifest
return (
m["model"],
_norm_quant(m["quant_label"]),
m["arch_family"],
m["kv_calc_version"],
m["engine_pin"],
m["failure_class"],
m["topology_class"],
)
def dedup_hash(self) -> str:
"""`sha256("\\x1f".join(dedup_tuple))[:12]` (CONTRACT-4).
Same `\\x1f`-join + sha256 convention as `[E]`'s
`submission_fingerprint` (`capture.py:378-381`), truncated to 12
hex chars — the bounded, collision-safe `loop:dedup-<hash>` label
primitive. F5 owns the issue-tracker side; F1 owns this canonical
deterministic serialization so every STEP hashes identically.
"""
joined = "\x1f".join(str(p) for p in self.dedup_tuple())
return hashlib.sha256(joined.encode("utf-8")).hexdigest()[:12]
# ---------------------------------------------------------------------------
# Normalization helpers (CONTRACT-1 binding rules — `[F]`'s job, not `[E]`'s).
# ---------------------------------------------------------------------------
def _norm_quant(value) -> str:
"""`quant_label` -> lowercased for keying (CONTRACT-1).
Raw value is `weight_format` case-as-emitted (`capture.py:389-391`).
A `None` (theoretically possible if the deriver surfaced no
`weight_format`) is normalized to the literal string ``"none"`` so the
key is always a deterministic string (same defensive `str()` discipline
as `submission_fingerprint`, `capture.py:539`).
"""
if value is None:
return "none"
return str(value).lower()
# ---------------------------------------------------------------------------
# Strict bundle validation + load.
# ---------------------------------------------------------------------------
def _load_json(path: Path) -> dict:
if not path.is_file():
raise CaptureBundleError(f"missing required artifact: {path.name}")
try:
obj = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise CaptureBundleError(
f"unreadable/invalid JSON in {path.name}: {exc}"
) from exc
if not isinstance(obj, dict):
raise CaptureBundleError(
f"{path.name} must be a JSON object, got {type(obj).__name__}"
)
return obj
def _require_keys(obj: dict, keys: tuple, where: str) -> None:
missing = [k for k in keys if k not in obj]
if missing:
raise CaptureBundleError(
f"{where} missing required key(s): {', '.join(sorted(missing))}"
)
def read_capture_bundle(
capture_dir,
*,
raw_bundle_path=None,
) -> FInput:
"""Parse + STRICTLY validate ONE `[E]` capture directory into `FInput`.
`capture_dir` is `<repo>/.pull-captures/<sanitize_slug(slug)>/<utc_ts>/`
(`capture.py:436-439`). Required: pt1-pt4 + manifest. Optional: pt5
(None when absent — present iff override-accepted, `pull.py:1082`).
`raw_bundle_path` is the OPTIONAL `report.sh --redact` attachment —
**NOT produced by `[E]`** (CONTRACT-1 / Kimi-r1 L1); F1 never
synthesizes it. It is only ever the explicit value the caller passes
(default None).
Forward-compat: future `[F]` STEPs (F3/G6-A) ADD `predicted_b_breakdown`
to pt1 and `failure_log_excerpt` + `actual.{...}` to pt3. F1 MUST
tolerate those being present OR absent — it validates only the
`[E]`-shipped required shape and never rejects an additive future key.
Raises `CaptureBundleError` on ANY schema/shape violation (schema != 1,
a missing required artifact, a wrong `point`, malformed JSON).
"""
cdir = Path(capture_dir)
if not cdir.is_dir():
raise CaptureBundleError(f"capture dir does not exist: {cdir}")
# ---- manifest (required; schema==1 hard-asserted) ------------------
manifest = _load_json(cdir / _MANIFEST_FILENAME)
if manifest.get("schema") != EXPECTED_SCHEMA:
raise CaptureBundleError(
f"manifest schema must be {EXPECTED_SCHEMA}, "
f"got {manifest.get('schema')!r}"
)
# All CONTRACT-1 first-class consensus/dedup inputs MUST be present —
# F1 is the boundary that guarantees them to downstream STEPs.
_require_keys(
manifest,
(
"schema", "slug", "utc_ts", "submission_fingerprint", "model",
"quant_label", "arch_family", "topology_class", "engine_pin",
"engine_version", "kv_calc_version", "selected_ctx",
"kv_format", "smoke_capability_set",
"topology_summary_canonical", "model_id", "failure_class",
"club3090_commit", "outcome", "capture_points",
),
"manifest.json",
)
# BINDING RULE: failure_class is authoritatively null in every [E]
# manifest (capture.py:569) — F1 enforces that invariant (a non-null
# value means [E] wrongly classified, which it must never do).
if manifest["failure_class"] is not None:
raise CaptureBundleError(
"manifest.failure_class must be null in an [E] bundle "
f"([F] classifies, not [E]); got {manifest['failure_class']!r}"
)
# ---- pt1-pt4 (required); each schema/point hard-asserted -----------
pts: dict = {}
for fname, expected_point in _REQUIRED_POINTS.items():
obj = _load_json(cdir / fname)
# pt1 carries `schema` (`capture.py:443`); pt2-4 do not ship a
# `schema` key (`capture.py:454/464/478`) — assert it ONLY where
# `[E]` actually emits it (assert-where-present, never invent a
# constraint `[E]` does not satisfy).
if "schema" in obj and obj["schema"] != EXPECTED_SCHEMA:
raise CaptureBundleError(
f"{fname} schema must be {EXPECTED_SCHEMA}, "
f"got {obj['schema']!r}"
)
if obj.get("point") != expected_point:
raise CaptureBundleError(
f"{fname} point must be {expected_point!r}, "
f"got {obj.get('point')!r}"
)
pts[fname] = obj
# ---- pt5 (optional — present iff override-accepted) ---------------
pt5_path = cdir / _PT5_FILENAME
pt5_override: Optional[dict] = None
if pt5_path.is_file():
pt5_override = _load_json(pt5_path)
if pt5_override.get("point") != "override_capture":
raise CaptureBundleError(
f"{_PT5_FILENAME} point must be 'override_capture', "
f"got {pt5_override.get('point')!r}"
)
return FInput(
manifest=manifest,
pt1_gate=pts["pt1-gate.json"],
pt2_download=pts["pt2-download.json"],
pt3_boot=pts["pt3-boot.json"],
pt4_smoke=pts["pt4-smoke.json"],
pt5_override=pt5_override,
raw_bundle_path=(
Path(raw_bundle_path) if raw_bundle_path is not None else None
),
)