Files
club-3090/scripts/lib/profiles/capture.py
T
noonghunnaandClaude Opus 4.7 20f1557d29 feat(pull): v0.8.2 STEP V1 — capture-on-hard-block pt1-gate emitter + BaseCaptureBundle protocol lift
CONTRACT-1.1 capture-on-hard-block (additive only; zero v0.8.0 decision-logic
change — the safetensors/GGUF paths are byte-unchanged):

- capture.py: new SEPARATE emit_gate_capture() (the emit_override_capture
  byte-preserving precedent — NOT invoked by emit_capture()) writing a
  pt1-gate.json + schema:2 manifest.json (outcome:hard-block, exact shipped
  abort_reason, failure_class:null) per the per-abort-stratum key table
  (model/arch/quant null pre-deriver; topology best-effort/nullable, capture-
  only resolve; post-C0 always null). New shared write_last_marker() helper
  (atomic tmp+os.replace) called from BOTH emit_capture() and the gate
  emitter (centralization mandate — gate-only is the commonest failure).
- pull.py: pass-through capture on the 7 terminal hard-block return paths
  (deriver / profile-like / hardware-sm-undetermined / C0 / C2a / no-fit-model
  / C1) — emits a bundle before the existing `return res`; the decision is
  byte-unchanged; injectable gate_capture_fn; never raises.
- loop_input.py: BaseCaptureBundle typing.Protocol (Optional[dict] pt2-5);
  FInput satisfies it by construction (verified: no isinstance(finput,FInput)
  anywhere in F2/F5 — pure static retype, schema==1 byte-identical incl.
  dedup_hash); new FInputGate + read_gate_bundle() (schema==2; validates ONLY
  the always-present row + outcome==hard-block + failure_class is None — does
  NOT reuse the 22-key validator); FInputGate.dedup_tuple() uses .get(k,None)
  (behaviour-neutral schema-1, crash-safe schema-2, deterministic null-topo).
- classifier.py / dedup.py: F2+F5 parameter annotations retyped FInput ->
  BaseCaptureBundle. The dedup.py FInput.dedup_hash(_EffProxy()) unbound-class
  idiom is FENCED (unchanged — not tidied). Additive gate_abort_reason
  _match_condition kind (reads pt1_gate.abort_reason; bool like sibling kinds;
  no enum/routing change).
- failure_fingerprints.yml: seeded gate_abort_reason rules keyed on the
  verified shipped abort_reason strings — only engine-support-unknown/
  no-arch-row -> kernel-unsupported (public-filed); runtime-incompatible /
  disk-short / hard-block / catch-alls -> unknown (review-queued, not filed).
- tests: extended test-{pull,pullemit-capture,loop-input,classifier,dedup}.sh
  with the V1 RED-LINE proofs (emit_capture() still writes ONLY pt1-4; a
  schema==1 bundle yields byte-identical FInput / ClassificationResult /
  dedup_hash / effective_dedup_hash pre/post the protocol lift; the dedup
  fence holds) + gate-emitter / read_gate_bundle / gate_abort_reason routing
  / shared .last marker coverage; all 22 shipped test-*.sh green in the CI
  condition (gitignored .pull-captures absent), kv-calc --calibration 11/11.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-18 16:43:28 +00:00

1153 lines
50 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""v0.8.0 Pull-Emit-Derived `[E]` — STEP E3: capability-aware derived smoke
+ the 4 §6 capture-point artifact emitters + the §6.2/§6.3 manifest.
CONTRACT-4 (the brief's locked E3 spec, capture half). `[E]` **emits** these;
`[F]` (the Loop — §6.1 classifier / §6.2 inbound-trust / §6.3 dedup /
consensus / promotion) **consumes** them and is explicitly OUT of scope here.
This module owns ONLY:
* `smoke_derived()` — the capability-aware DERIVED smoke prober
(CONTRACT-4 "Capability-smoke set for DERIVED
models" — the conservative floor);
* `emit_capture()` — write the 4 §6 capture-point artifacts (pt1 gate /
pt2 download / pt3 boot / pt4 smoke) + a top-level
`manifest.json`, schema **v1**, redacted via the
`report.sh --redact` convention.
CAPTURE-POINT 5 (override-accepted force-capture) is the ONE additive E4
extension to this module (`emit_override_capture()` — §5.3 / CONTRACT-4
pt5). It is emitted ONLY on the post-`[C1]` override-accepted path E4
wires; the E3 pt1-4 + manifest emitters below are byte-behaviour-preserving
(`test-pullemit-capture.sh` stays green — it asserts pt5 is NOT written by
`emit_capture()`; pt5 is a SEPARATE function E4 invokes only when
`einput.is_override_accepted`). NO `run_pull()` wiring here (that is E4).
NO §6.1 failure classification (`failure_class` is left null — that is
`[F]`'s job). NO docs (E5). NO real on-rig boot (E5).
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import subprocess
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
from .downloader import sanitize_slug
SCHEMA = 1
# v0.8.2 CONTRACT-1.1 — the gate-only (capture-on-hard-block) bundle schema.
# A DISTINCT schema version so F1's strict schema==1 reader is byte-
# unchanged; the gate-only bundle is consumed by a SEPARATE
# `read_gate_bundle()` (schema==2) that validates only the always-present
# key row (NOT the 22-key schema-1 validator).
GATE_SCHEMA = 2
# ---------------------------------------------------------------------------
# CONTRACT-4 capability-smoke set for DERIVED models.
#
# A derived generic profile declares no capabilities, so the conservative
# floor is: plain-chat ALWAYS + streaming ALWAYS (cheap; catches the #145
# class — a model can boot + answer plain chat while streaming/tools/etc are
# silently dead). The remaining capabilities are smoked ONLY if `der`'s
# config.json POSITIVELY declares them; otherwise recorded "unsmoked" and
# the anchor is `partial` (per §6.2: an anchor with un-smoked capabilities
# is `partial` and cannot graduate to Tier-1 for those capabilities).
# ---------------------------------------------------------------------------
FLOOR_CAPS = ("plain-chat", "streaming")
OPTIONAL_CAPS = (
"tool-call",
"reasoning-streaming",
"structured-output",
"vision",
"long-context",
)
@dataclass
class SmokeResult:
smoke_capability_set: list[str] = field(default_factory=list)
# {<cap>: "green" | "red" | "unsmoked"}
results: dict[str, str] = field(default_factory=dict)
partial: bool = False
# ADDITIVE (E3-fix): per-capability failure diagnostic for `red` probes
# — {<cap>: {"status": int|None, "error": str}}. Populated ONLY for
# capabilities that probed `red`; absent for green/unsmoked caps. This
# is consumed by the §6.1 classifier `[F]` will build + on-rig E5
# diagnosis. It does NOT alter the locked `results`/`partial`/
# `smoke_capability_set` shape (CONTRACT-4 + [F] recon depend on those).
results_detail: dict[str, dict] = field(default_factory=dict)
def _config_declares(der: Any, cap: str) -> bool:
"""Does `der`'s config.json POSITIVELY declare `cap`?
A derived generic-dense model surfaces config.json signals on
`der.profile`. We read ONLY positive declarations (never infer a
capability from absence). The recognised positive signals, conservative
by design (unknown -> not declared -> "unsmoked" -> partial):
tool-call config.json declares a tool/function-calling
chat-template or `tool_use`/`tools` support, OR
the deriver surfaced a positive tool flag.
reasoning-streaming config declares a reasoning/thinking parser
(`reasoning`/`thinking` config block).
structured-output config declares guided/structured-output support.
vision an image/vision config block, a *VL/*Vision
architecture, or a positive vision flag.
long-context config declares a context window beyond the
plain-floor (e.g. `max_position_embeddings` /
`rope_scaling`) AND the runtime selected a large
max_model_len — derived defers this unless the
model itself positively declares it.
`der.profile` may carry a raw config dict under `config`/`_config`
(whatever the deriver/orchestrator attaches); we look there + at the
surfaced `arch`. We NEVER mutate the deriver and NEVER guess.
"""
prof = getattr(der, "profile", None) or {}
cfg = prof.get("config") or prof.get("_config") or {}
arch = str(prof.get("arch") or "").lower()
def has(*keys: str) -> bool:
return any(k in cfg and cfg.get(k) for k in keys)
if cap == "tool-call":
return bool(
prof.get("supports_tool_call") is True
or has("tool_use", "tools", "function_calling")
or (isinstance(cfg.get("chat_template"), str)
and "tool" in cfg["chat_template"].lower())
)
if cap == "reasoning-streaming":
return bool(
prof.get("supports_reasoning") is True
or has("reasoning", "thinking", "reasoning_parser")
)
if cap == "structured-output":
return bool(
prof.get("supports_structured_output") is True
or has("guided_decoding", "structured_outputs", "grammar")
)
if cap == "vision":
return bool(
prof.get("supports_vision") is True
or has("vision_config", "image_token_id", "vision_tower")
or arch.endswith(("vl", "vision"))
or "vl" in arch
or "vision" in arch
)
if cap == "long-context":
return bool(
prof.get("supports_long_context") is True
or has("rope_scaling")
)
return False
def _resolve_served_model_name(einput, compose_meta: Optional[dict]) -> str:
"""The model name the probe MUST send in the OpenAI `model` field — it
has to be the EXACT value `generate_from_profile` emitted for
`--served-model-name`, or vLLM 404s the request (the on-rig E5 defect:
a healthy booted server, but `model:"derived"` is an unknown served
name -> HTTP 404 -> every floor probe `red`).
Authoritative source priority (CONTRACT-4 / brief):
(a) `compose_meta['served_model_name']` — the literal value the
generator emitted (preferred when the smoke path has it);
(b) `sanitize_slug(einput.slug)` — the SAME function
`generate_compose._sanitize_slug` mirrors, so it is identical to
what `--served-model-name` carries. No third derivation.
"""
if compose_meta:
smn = compose_meta.get("served_model_name")
if isinstance(smn, str) and smn:
return smn
return sanitize_slug(einput.slug)
def smoke_derived(
einput,
endpoint: str,
*,
client: Optional[Any] = None,
compose_meta: Optional[dict] = None,
) -> SmokeResult:
"""Capability-aware DERIVED smoke prober (CONTRACT-4).
The DERIVED floor: **plain-chat ALWAYS + streaming ALWAYS** (cheap;
catches the #145 class). `tool-call` / `reasoning-streaming` /
`structured-output` / `vision` / `long-context` are probed ONLY if
`der`'s config.json positively declares them; otherwise recorded
`"unsmoked"`. `partial = any(v == "unsmoked")` — per §6.2 an anchor
with un-smoked capabilities is `partial` and cannot graduate to Tier-1
for those capabilities.
The OpenAI `model` field sent to the server is the RESOLVED
served-model-name (see `_resolve_served_model_name`) — NOT the literal
`"derived"` (which vLLM 404s; the on-rig E5 red-smoke-on-healthy-boot
defect). `compose_meta` (optional; the dict
`generate_from_profile` returns, carrying `served_model_name`) is used
when available, else `sanitize_slug(einput.slug)` (identical to the
emitted `--served-model-name`).
`client` is INJECTABLE: default = the real OpenAI-compatible probe
against `endpoint`; E3 tests pass a fixture client so there is NO live
server in CI. A client must provide:
.probe(capability, endpoint, model_name) -> bool
| (bool, status:int|None, error:str)
(truthy / a True first element == green; the optional 2nd/3rd
carry the HTTP status + a short error snippet for the additive
`results_detail` failure capture). A legacy bare-bool client is
still accepted (no detail recorded for it).
"""
if client is None:
client = _HttpSmokeClient()
model_name = _resolve_served_model_name(einput, compose_meta)
der = einput.der
probe_set: list[str] = list(FLOOR_CAPS)
for cap in OPTIONAL_CAPS:
if _config_declares(der, cap):
probe_set.append(cap)
results: dict[str, str] = {}
results_detail: dict[str, dict] = {}
# FLOOR + declared caps -> actually probed; everything in OPTIONAL_CAPS
# not declared -> "unsmoked" (recorded, drives `partial`).
for cap in FLOOR_CAPS + OPTIONAL_CAPS:
if cap not in probe_set:
results[cap] = "unsmoked"
continue
status: Optional[int] = None
error: str = ""
try:
raw = client.probe(cap, endpoint, model_name)
except TypeError:
# Legacy fixture/client with the old 2-arg signature — keep the
# injected-fixture seam working (E3 tests inject a fake client).
try:
raw = client.probe(cap, endpoint)
except Exception as exc:
raw, status, error = False, None, repr(exc)
except Exception as exc:
raw, status, error = False, None, repr(exc)
if isinstance(raw, tuple):
ok = bool(raw[0])
if len(raw) > 1 and raw[1] is not None:
status = int(raw[1])
if len(raw) > 2 and raw[2]:
error = str(raw[2])
else:
ok = bool(raw)
if ok:
results[cap] = "green"
else:
results[cap] = "red"
# ADDITIVE: a `red` carries the HTTP status + a short, redacted
# error snippet so [F]'s §6.1 classifier + on-rig E5 diagnosis
# have something to reason over (today pt4 records only the
# bare verdict).
results_detail[cap] = {
"status": status,
"error": _redact_text(error)[:240] if error else "",
}
partial = any(v == "unsmoked" for v in results.values())
return SmokeResult(
smoke_capability_set=sorted(probe_set),
results=results,
partial=partial,
results_detail=results_detail,
)
class _HttpSmokeClient:
"""Real probe client (NOT exercised in E3 CI — a fixture client is
injected; the live server is E5 on-rig). Codifies the minimal
OpenAI-compatible probe per capability so E5 has nothing to invent."""
def _build_body(self, capability: str, model_name: str) -> dict:
"""Construct the OpenAI /chat/completions probe body. The `model`
field MUST be the resolved served-model-name (NOT the literal
`"derived"` — vLLM validates it against `--served-model-name` and
404s an unknown name; that was the on-rig E5 defect). Factored out
so a UNIT test can assert the request shape with NO network."""
body: dict = {
"model": model_name,
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 8,
}
if capability == "streaming":
body["stream"] = True
return body
def probe( # pragma: no cover - E5
self, capability: str, endpoint: str, model_name: str
) -> tuple[bool, Optional[int], str]:
import urllib.error
import urllib.request
url = f"{endpoint}/chat/completions"
body = self._build_body(capability, model_name)
try:
req = urllib.request.Request(
url,
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
ok = resp.status == 200
return (ok, resp.status, "" if ok else f"HTTP {resp.status}")
except urllib.error.HTTPError as exc:
snippet = ""
try:
snippet = exc.read().decode("utf-8", "replace")[:240]
except Exception:
snippet = str(exc)
return (False, exc.code, f"HTTP {exc.code}: {snippet}")
except (urllib.error.URLError, OSError) as exc:
return (False, None, repr(exc))
# ---------------------------------------------------------------------------
# Redaction — REUSE the `report.sh --redact` convention (do NOT reinvent).
#
# report.sh's `redact()` (scripts/report.sh:66-80) is a bash sed pipeline; it
# is not independently importable, so E3 reuses the SAME convention by
# applying the IDENTICAL sed expression set, driven by the SAME env keys
# (USER / hostname / HF token). Kept in lock-step with report.sh:66-80 — if
# that block changes, this must change with it. No path/token/host leak.
# ---------------------------------------------------------------------------
def _redact_text(text: str) -> str:
user = os.environ.get("USER") or ""
try:
host = subprocess.run(
["hostname", "-s"], capture_output=True, text=True, check=False
).stdout.strip()
except Exception: # pragma: no cover - hostname always present on rig
host = ""
# The EXACT report.sh:66-80 expression set (verbatim convention reuse).
sed_exprs: list[str] = []
if user:
sed_exprs += ["-e", f"s|/home/{re.escape(user)}|~|g"]
sed_exprs += ["-e", "s|/root|~|g"]
if host:
sed_exprs += ["-e", f"s|{re.escape(host)}|<HOST>|g"]
if user:
sed_exprs += ["-e", f"s|{re.escape(user)}|<USER>|g"]
sed_exprs += [
"-e", 's|HF_TOKEN=[^ "]*|HF_TOKEN=<REDACTED>|g',
"-e", 's|HUGGING_FACE_HUB_TOKEN=[^ "]*|HUGGING_FACE_HUB_TOKEN=<REDACTED>|g',
"-e", 's|api_key=[^ "]*|api_key=<REDACTED>|gi',
"-e", r's|hf_[A-Za-z0-9]\{30,\}|hf_<REDACTED>|g',
]
# Hardening BEYOND report.sh's home/root convention (additive, never
# weaker): the CONTRACT-4 schema carries only slugs / verdicts /
# relative filenames — it must NEVER carry an absolute internal host
# path. report.sh only collapses ~/ + /root; a capture artifact is
# consumed by [F]/cross-rig so ANY absolute internal mount path
# (/opt/* /mnt/* /data/*) is scrubbed to <PATH> as a defence in depth
# (the "don't leak internal paths in public artifacts" stack rule).
sed_exprs += [
"-e", r's|/opt/[A-Za-z0-9._/-]*|<PATH>|g',
"-e", r's|/mnt/[A-Za-z0-9._/-]*|<PATH>|g',
"-e", r's|/data/[A-Za-z0-9._/-]*|<PATH>|g',
]
try:
proc = subprocess.run(
["sed", *sed_exprs],
input=text, capture_output=True, text=True, check=True,
)
return proc.stdout
except Exception: # pragma: no cover - sed is POSIX-ubiquitous
return text
# ---------------------------------------------------------------------------
# F3/G6-A-ii — the `pt3.actual` parse helper (capture-time, [E]-owned).
#
# Both inputs are regexed out of the SAME already-captured + redacted
# `failure_log_excerpt` (the booter's bounded `docker compose logs
# --no-color` text). `--no-color` is MANDATORY upstream (ANSI escapes
# corrupt these regexes) — see booter.capture_failure_log.
#
# attempted_alloc_mib — the torch.cuda.OutOfMemoryError traceback's
# "Tried to allocate <N> {MiB|GiB|KiB|B}" figure
# (the size the allocation that OOM'd asked for).
# gpu_worker_reported_mib — the gpu_worker.py measured-peak line
# ("... peak ... <N> {MiB|GiB} ..." emitted by
# vLLM's GPU worker memory profiler).
#
# Either is `None` when not found — NEVER fabricate a number (the §1
# confidently-wrong rule; the F3 routing gate then honest-degrades). This
# helper is `[E]`-owned (CONTRACT-1: classifier.py only READS pt3.actual).
# ---------------------------------------------------------------------------
_ALLOC_UNIT_TO_MIB = {
"b": 1.0 / (1024.0 * 1024.0),
"kib": 1.0 / 1024.0,
"kb": 1.0 / 1024.0,
"mib": 1.0,
"mb": 1.0,
"gib": 1024.0,
"gb": 1024.0,
}
# "Tried to allocate 512.00 MiB" / "Tried to allocate 2.50 GiB" etc.
# (the CLASSIC torch.cuda.OutOfMemoryError traceback phrasing).
_RE_ATTEMPTED_ALLOC = re.compile(
r"tried to allocate\s+([0-9]+(?:\.[0-9]+)?)\s*(B|KiB|KB|MiB|MB|GiB|GB)",
re.IGNORECASE,
)
# vLLM gpu_worker peak line, tolerant of phrasing:
# "gpu_worker.py ... peak memory ... 22880.00 MiB"
# "GPU memory peak: 22.34 GiB"
# "Maximum memory usage ... 22880 MiB"
_RE_GPU_WORKER_PEAK = re.compile(
r"(?:gpu[_ ]?worker|peak memory|memory peak|maximum memory|"
r"peak gpu memory|max memory)[^0-9\n]*?"
r"([0-9]+(?:\.[0-9]+)?)\s*(MiB|MB|GiB|GB)",
re.IGNORECASE,
)
# ── F8-fix: real modern vLLM v0.21.0+ KV-cache-too-large phrasing ──────────
# The on-rig F8 validator induced a GENUINE vLLM KV-cache-too-large failure;
# the captured nightly (bf610c2f / v0.20.2rc1.dev371, the v0.21.0+ memory-
# profiler regime) does NOT raise `torch.cuda.OutOfMemoryError` for this very
# common kv-calc-relevant case. It raises a CLEAN `ValueError` from
# `_check_enough_kv_cache_memory` with this VERBATIM shape (captured
# from a real vLLM KV-OOM):
#
# ValueError: To serve at least one request with the models's max seq len
# (2000000), (22.89 GiB KV cache is needed, which is larger than the
# available KV cache memory (20.89 GiB). ...
# INFO ... [gpu_worker.py:462] Available KV cache memory: 20.89 GiB
#
# The CLASSIC `_RE_ATTEMPTED_ALLOC` ("tried to allocate") + `_RE_GPU_WORKER_
# PEAK` ("peak memory") regexes match NEITHER line, so this real, common
# failure was silently parsed `{None, None}` and Tier-1 never routed.
#
# These NEW sibling regexes are tried AFTER the classic ones (first match
# wins; classic torch OOM is never regressed). Mapping (F8-fix spec):
# attempted_alloc_mib <- the "(N GiB|MiB) KV cache is needed" figure
# (the FIRST number in the ValueError; the KV the
# engine actually needed -> 22.89 GiB).
# gpu_worker_reported_mib <- the standalone gpu_worker.py line
# "Available KV cache memory: N GiB" (preferred),
# ELSE the ValueError parenthetical
# "available KV cache memory (N GiB)" (the SECOND
# number -> 20.89 GiB). Both are the SAME measured-
# available figure; prefer the explicit gpu_worker
# line when present.
#
# "<N> GiB KV cache is needed" — the engine-needed KV size (attempted side).
_RE_KV_NEEDED = re.compile(
r"([0-9]+(?:\.[0-9]+)?)\s*(B|KiB|KB|MiB|MB|GiB|GB)\s+kv cache is needed",
re.IGNORECASE,
)
# The explicit gpu_worker.py measured-available line (PREFERRED source for
# gpu_worker_reported_mib): "[gpu_worker.py:462] Available KV cache memory:
# 20.89 GiB".
_RE_KV_AVAILABLE_GPU_WORKER = re.compile(
r"gpu_worker[^\n]*?available kv cache memory[:\s]+"
r"([0-9]+(?:\.[0-9]+)?)\s*(B|KiB|KB|MiB|MB|GiB|GB)",
re.IGNORECASE,
)
# Fallback: the available figure inside the ValueError parenthetical —
# "available KV cache memory (20.89 GiB)".
_RE_KV_AVAILABLE_VALUEERROR = re.compile(
r"available kv cache memory\s*\(\s*"
r"([0-9]+(?:\.[0-9]+)?)\s*(B|KiB|KB|MiB|MB|GiB|GB)\s*\)",
re.IGNORECASE,
)
def _to_mib(value: str, unit: str) -> Optional[int]:
try:
factor = _ALLOC_UNIT_TO_MIB.get(unit.lower())
if factor is None:
return None
return int(round(float(value) * factor))
except (TypeError, ValueError):
return None
def _parse_pt3_actual(excerpt: Optional[str]) -> dict:
"""Regex the two Tier-1 actual-side inputs out of the redacted excerpt.
Returns the structured first-class `{attempted_alloc_mib:int|None,
gpu_worker_reported_mib:int|None}` object (both `None` when not found —
never fabricated). The OOM-signature gate itself is `[F]`'s job
(classifier.py Tier-1); this only extracts the numbers `[E]` measured.
"""
attempted: Optional[int] = None
gpu_worker: Optional[int] = None
if excerpt:
# --- attempted_alloc_mib: classic torch OOM FIRST, then the real
# modern vLLM v0.21.0+ "<N> GiB KV cache is needed" phrasing
# (first match wins; classic path never regressed). ----------
m = _RE_ATTEMPTED_ALLOC.search(excerpt)
if m:
attempted = _to_mib(m.group(1), m.group(2))
else:
mk = _RE_KV_NEEDED.search(excerpt)
if mk:
attempted = _to_mib(mk.group(1), mk.group(2))
# --- gpu_worker_reported_mib: classic "peak memory" FIRST; else
# the explicit gpu_worker.py "Available KV cache memory: N GiB"
# line (PREFERRED), else the ValueError's parenthetical
# "available KV cache memory (N GiB)" fallback. ---------------
g = _RE_GPU_WORKER_PEAK.search(excerpt)
if g:
gpu_worker = _to_mib(g.group(1), g.group(2))
else:
ga = _RE_KV_AVAILABLE_GPU_WORKER.search(excerpt)
if ga:
gpu_worker = _to_mib(ga.group(1), ga.group(2))
else:
gv = _RE_KV_AVAILABLE_VALUEERROR.search(excerpt)
if gv:
gpu_worker = _to_mib(gv.group(1), gv.group(2))
return {
"attempted_alloc_mib": attempted,
"gpu_worker_reported_mib": gpu_worker,
}
def _write_redacted_json(path: Path, obj: dict) -> None:
raw = json.dumps(obj, indent=2, sort_keys=True, ensure_ascii=False)
path.write_text(_redact_text(raw) + "\n", encoding="utf-8")
# ---------------------------------------------------------------------------
# v0.8.2 CONTRACT-1.3 — the shared `.last`-marker write helper.
#
# `scripts/pull.sh --submit-last` (CONTRACT-1.3, V2) resolves "the most-recent
# capture" from a single marker file `.pull-captures/.last` (one line: the
# bundle dir RELATIVE to `.pull-captures/`). Centralization is MANDATORY
# (design rule): `emit_capture()` (`[E]` path) and the new
# `emit_gate_capture()` (the commonest — pre-download — failure) are SEPARATE
# functions on SEPARATE terminal paths. If `.last` were written only by
# `emit_capture()`, gate-only captures (exactly the §10-R9 volume the on-ramp
# exists to harvest) would never update `.last` and `--submit-last` would
# silently miss them. So BOTH emitters call this ONE helper.
#
# Atomic (`tmp` + `os.replace`) so a racing reader never sees a torn marker;
# last-writer-wins by design (a serial user workflow — V2's submit re-reads
# + re-shows the resolved bundle identity before the consent prompt). NEVER
# raises — a marker-write failure must not break a capture (CONTRACT-1: the
# gate path stays I/O-light + never blocks).
# ---------------------------------------------------------------------------
def write_last_marker(repo_root: Path, bundle_dir: Path) -> None:
"""Atomically record `bundle_dir` (made relative to `.pull-captures/`)
as the most-recent capture for `--submit-last`. Shared by BOTH
`emit_capture()` and `emit_gate_capture()`. Never raises.
"""
try:
pull_captures = Path(repo_root) / ".pull-captures"
pull_captures.mkdir(parents=True, exist_ok=True)
try:
rel = str(Path(bundle_dir).relative_to(pull_captures))
except ValueError:
# Defensive: a bundle dir outside .pull-captures/ (should not
# happen — both emitters write under it) — store as given.
rel = str(bundle_dir)
marker = pull_captures / ".last"
tmp = pull_captures / ".last.tmp"
tmp.write_text(rel + "\n", encoding="utf-8")
os.replace(tmp, marker)
except Exception: # pragma: no cover - marker write is best-effort.
pass
# ---------------------------------------------------------------------------
# §6.2 submission_fingerprint + manifest helpers.
# ---------------------------------------------------------------------------
def _fingerprint(parts: list[str]) -> str:
h = hashlib.sha256()
h.update("\x1f".join(str(p) for p in parts).encode("utf-8"))
return h.hexdigest()
def _arch_family(der: Any) -> Optional[str]:
prof = getattr(der, "profile", None) or {}
return prof.get("arch") or prof.get("family")
def _quant_label(der: Any) -> Optional[str]:
prof = getattr(der, "profile", None) or {}
return prof.get("weight_format")
def _topology_class(einput) -> str:
"""A coarse, deterministic class for §6.2/§6.3 (NOT the canonical
summary — that is `topology_summary_canonical`). N GPUs × VRAM-bucket."""
n = len(einput.selected_gpu_vram_mib or [])
vram = min(einput.selected_gpu_vram_mib) if einput.selected_gpu_vram_mib else 0
return f"{n}x{vram}MiB"
# ---------------------------------------------------------------------------
# THE 4 §6 capture-point emitters + manifest.
# ---------------------------------------------------------------------------
def emit_capture(
einput,
*,
confidence,
raw_verdict,
profile_like: str,
download_result,
boot_result,
smoke_result: SmokeResult,
compose_meta: dict,
kv_calc_version: str,
repo_root: Path,
ts: Optional[str] = None,
predicted_b_breakdown=None,
failure_log_excerpt: Optional[str] = None,
) -> dict:
"""Write the 4 §6 capture-point artifacts (pt1 gate / pt2 download /
pt3 boot / pt4 smoke) + a top-level `manifest.json`, schema v1, JSON,
redacted via the `report.sh --redact` convention, under:
<repo>/.pull-captures/<slug-sanitized>/<utc-ts>/
Returns `{paths:{...}, dir:str, manifest:{...}}`. CONTRACT-4 EXACTLY:
pt1 gate {schema, point, slug, confidence, raw_verdict, terminal,
profile_like, hardware_sm, predicted_b_breakdown}
pt2 download {point, ok, files, bytes, sha_verified, failure}
pt3 boot {point, ok, seconds, failure
[, failure_log_excerpt, actual] -- only when NOT ok}
pt4 smoke {point, smoke_capability_set, results, partial}
CAPTURE-POINT 5 (override-accepted force-capture) is OUT of E3 scope —
NOT emitted here (E4 wires it). `failure_class` is left **null** — E3
must NOT classify (§6.1 = `[F]`'s job).
F3/G6-A — the 3-part additive `[E]` touch (CONTRACT-2 G6-(A); all
additive, every existing key byte-preserved):
* A-i `predicted_b_breakdown` (kw-only, default `None`): the `[B]`
kv-calc GB breakdown that produced the verdict
(`res.diagnostics["b_breakdown"]` == `raw_verdict()["breakdown_gb"]`,
`pull.py:638`). Persisted into **pt1** for ALL post-`[B]` captures
(C1: previously on disk only in pt5/override). The key is ALWAYS
present in pt1 (value `None` when the caller passes nothing) — pt1
already emits a fixed key set, so an always-present additive key
keeps that shape; no EXISTING pt1 key's value or bytes change
(JSON is `sort_keys=True`: a new key inserts its own line, it never
mutates an existing key's serialization). F1's `read_capture_bundle`
+ F2's classifier were built to tolerate this exact key
absent-or-present.
* A-ii `failure_log_excerpt` (kw-only, default `None`): the bounded,
`_redact_text`-scrubbed `docker compose logs --no-color` excerpt the
booter captured BEFORE teardown (C2: the in-container vLLM OOM
traceback + the gpu_worker peak line live there, NOT in compose-up
stderr). Written into **pt3 ONLY when `not ok`** — so the success
path pt3 is byte-identical to today (NO new keys at all when boot
ok); a failure pt3 gains `failure_log_excerpt`.
* A-ii `pt3.actual`: emitted HERE at capture time (NOT in
classifier.py — `[F]` stays a pure bundle reader, CONTRACT-1). When
pt3 is `not ok` and we have an excerpt, `_parse_pt3_actual()`
regexes `attempted_alloc_mib` (the OOM traceback "Tried to allocate"
line) and `gpu_worker_reported_mib` (the gpu_worker peak line) out
of that SAME captured excerpt into the structured first-class
`pt3.actual = {attempted_alloc_mib:int|None,
gpu_worker_reported_mib:int|None}`. classifier.py only READS this.
"""
san = sanitize_slug(einput.slug)
stamp = ts or datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
out_dir = Path(repo_root) / ".pull-captures" / san / stamp
out_dir.mkdir(parents=True, exist_ok=True)
# ---- pt1: pre-download gate verdict --------------------------------
# F3/G6-A-i: `predicted_b_breakdown` is ALWAYS present (None when the
# caller passes nothing) — pt1 emits a fixed key set, so this additive
# key keeps that shape. No EXISTING key value/bytes change.
pt1 = {
"schema": SCHEMA,
"point": "gate",
"slug": einput.slug,
"confidence": str(getattr(confidence, "name", confidence)),
"raw_verdict": raw_verdict,
"terminal": einput.terminal,
"profile_like": profile_like,
"hardware_sm": einput.hardware_sm,
"predicted_b_breakdown": predicted_b_breakdown,
}
# ---- pt2: download (the E2 DownloadResult shape) -------------------
pt2 = {
"point": "download",
"ok": bool(getattr(download_result, "ok", False)),
"files": list(getattr(download_result, "files", []) or []),
"bytes": int(getattr(download_result, "bytes", 0) or 0),
"sha_verified": bool(getattr(download_result, "sha_verified", False)),
"failure": getattr(download_result, "failure", None),
}
# ---- pt3: boot -----------------------------------------------------
# The 4 LOCKED keys are byte-behaviour-unchanged. F3/G6-A-ii + A-ii:
# `failure_log_excerpt` (the booter's bounded redacted
# `docker compose logs --no-color` excerpt) and the structured
# `actual.{attempted_alloc_mib,gpu_worker_reported_mib}` are added
# **ONLY when boot is NOT ok** — so the success-path pt3 is byte-
# identical to today (no new keys at all); a failure pt3 gains them.
pt3 = {
"point": "boot",
"ok": bool(getattr(boot_result, "ok", False)),
"seconds": float(getattr(boot_result, "seconds", 0.0) or 0.0),
"failure": getattr(boot_result, "failure", None),
}
if not pt3["ok"]:
# Prefer the explicitly-passed excerpt; else the booter may have
# stashed it on the BootResult (additive attribute, default None).
excerpt = failure_log_excerpt
if excerpt is None:
excerpt = getattr(boot_result, "failure_log_excerpt", None)
excerpt_red = (
_redact_text(str(excerpt)) if excerpt else None
)
pt3["failure_log_excerpt"] = excerpt_red
pt3["actual"] = _parse_pt3_actual(excerpt_red)
# ---- pt4: post-boot capability-aware smoke ------------------------
# `point` / `smoke_capability_set` / `results` / `partial` are the
# LOCKED CONTRACT-4 shape (byte-behaviour-unchanged — [F] §6.1 recon
# depends on them). `results_detail` is ADDITIVE only: per-`red`-cap
# HTTP-status + redacted error snippet so [F]'s classifier + on-rig E5
# diagnosis are not blind to WHY a probe went red (the on-rig defect:
# only `red` was recorded, no 404 status).
pt4 = {
"point": "smoke",
"smoke_capability_set": list(smoke_result.smoke_capability_set),
"results": dict(smoke_result.results),
"partial": bool(smoke_result.partial),
"results_detail": {
k: dict(v)
for k, v in (
getattr(smoke_result, "results_detail", {}) or {}
).items()
},
}
# ---- manifest: §6.2 consensus-key inputs as FIRST-CLASS fields -----
# (Codex-r5 High-2 — [F] must reason OVER them; a hash is opaque.)
model = einput.slug
quant_label = _quant_label(einput.der)
arch_family = _arch_family(einput.der)
topology_class = _topology_class(einput)
engine_pin = compose_meta.get("resolved_image") or compose_meta.get(
"engine_pin"
)
engine_version = engine_pin
selected_ctx = compose_meta.get("max_model_len")
kv_format = compose_meta.get("kv_format")
smoke_capability_set = list(smoke_result.smoke_capability_set)
topology_summary_canonical = einput.topology_summary
# Honest 3-state manifest outcome, derived PURELY from structured truth
# already in scope (pt2 download `ok`, pt3 boot `ok`, the locked
# `smoke_result.results`/`.partial`) — no new fields, no re-derivation,
# no model/network. Precedence is strict: failed > partial > ok.
# - "failed" : a REAL stage failure — download not ok, OR boot not ok,
# OR ANY *smoked* capability went "red". (A stage
# hard-fail dominates even when smoke is absent.)
# - "partial" : NOT failed AND `smoke_result.partial` is True — every
# smoked cap green but ≥1 cap "unsmoked". Per §6.2 this
# is a capability-scoped SUCCESS (it merely cannot
# graduate to Tier-1 for those caps); it is NOT a
# failure. The floor-green/optionals-unsmoked
# generic-dense case (e.g. Qwen2.5-0.5B) lands HERE.
# - "ok" : NOT failed AND NOT partial — everything attempted, all
# green, nothing unsmoked.
# NOTE: this 3-state is the honest INTERIM only — the final anchor-status
# taxonomy is owned by the future `[F]` Loop phase (§6.1 classifier /
# §6.2 consensus). `[E]` emits honest structured truth; `[F]` classifies.
_failed = (
not pt2["ok"]
or not pt3["ok"]
or any(v == "red" for v in smoke_result.results.values())
)
if _failed:
outcome = "failed"
elif smoke_result.partial:
outcome = "partial"
else:
outcome = "ok"
submission_fingerprint = _fingerprint([
model,
einput.club3090_commit,
topology_summary_canonical,
str(quant_label),
kv_calc_version,
str(engine_version),
stamp,
outcome,
])
manifest = {
"schema": SCHEMA,
"slug": einput.slug,
"utc_ts": stamp,
# §6.2 stage-2 hash (quick correlation).
"submission_fingerprint": submission_fingerprint,
# §6.2 consensus-key inputs — FIRST-CLASS (not only hashed).
"model": model,
"quant_label": quant_label,
"arch_family": arch_family,
"topology_class": topology_class,
"engine_pin": engine_pin,
"engine_version": engine_version,
"kv_calc_version": kv_calc_version,
"selected_ctx": selected_ctx,
"kv_format": kv_format,
"smoke_capability_set": smoke_capability_set,
# §6.2 verbatim — sorted (gpu_name, vram_mib) serialization.
"topology_summary_canonical": topology_summary_canonical,
# §6.3 dedup-key inputs — FIRST-CLASS too. `[E]` emits the inputs;
# `[F]` computes/uses the key. `failure_class` is left **null**:
# that is §6.1 classifier = `[F]`'s job; E3 must NOT classify.
"model_id": model,
"failure_class": None,
"club3090_commit": einput.club3090_commit,
"outcome": outcome,
"capture_points": ["gate", "download", "boot", "smoke"],
}
paths = {
"gate": str(out_dir / "pt1-gate.json"),
"download": str(out_dir / "pt2-download.json"),
"boot": str(out_dir / "pt3-boot.json"),
"smoke": str(out_dir / "pt4-smoke.json"),
"manifest": str(out_dir / "manifest.json"),
}
_write_redacted_json(Path(paths["gate"]), pt1)
_write_redacted_json(Path(paths["download"]), pt2)
_write_redacted_json(Path(paths["boot"]), pt3)
_write_redacted_json(Path(paths["smoke"]), pt4)
_write_redacted_json(Path(paths["manifest"]), manifest)
# v0.8.2 CONTRACT-1.3 — the SHARED `.last` marker (centralized: invoked
# from BOTH `emit_capture()` here AND `emit_gate_capture()` below; never
# raises). This is purely additive: it writes ONE extra marker file and
# changes NONE of the pt1-4 / manifest artifacts — `test-pullemit-
# capture.sh`'s "emit_capture() writes ONLY pt1-4 + manifest" assertion
# is unaffected (`.last` lives at `.pull-captures/.last`, NOT inside the
# `<slug>/<ts>/` bundle dir it enumerates).
write_last_marker(Path(repo_root), out_dir)
return {"paths": paths, "dir": str(out_dir), "manifest": manifest}
# ---------------------------------------------------------------------------
# CAPTURE-POINT 5 — override-accepted force-capture (CONTRACT-4 pt5 / §5.3).
#
# ADDITIVE E4 extension (NOT invoked by emit_capture(); a SEPARATE function
# E4 calls ONLY on the post-`[C1]` override-accepted path, i.e. when
# `einput.is_override_accepted` is True). E3's pt1-4 + manifest emitters
# above are byte-behaviour-preserving — `test-pullemit-capture.sh` continues
# to assert `emit_capture()` writes ONLY pt1-4 + manifest and NO override
# artifact. pt5 is written into the SAME `<repo>/.pull-captures/<slug>/<ts>/`
# directory, redacted via the SAME convention, schema-less per CONTRACT-4
# pt5's literal field list.
#
# CONTRACT-4 pt5 / §5.3 — emit EXACTLY:
# { point:"override_capture",
# predicted_b_breakdown:{ the full [B] kv-calc GB breakdown that
# produced the verdict },
# actual:{ boot_peak_mib:int|null, gpu_worker_reported_mib:int|null },
# predicted_vs_actual_delta_mib:int|null,
# exit_error_summary:str|null,
# calibration_signal_not_validated:true }
# The `true` flag is MANDATORY + LITERAL — §5.3: a forced low-confidence
# download is a calibration SIGNAL, never recorded as fit-validated. `actual`
# may be null (boot never reached allocation) — then `exit_error_summary`
# carries why; the artifact is STILL emitted regardless.
# ---------------------------------------------------------------------------
def emit_override_capture(
einput,
*,
predicted_b_breakdown,
boot_peak_mib: Optional[int] = None,
gpu_worker_reported_mib: Optional[int] = None,
exit_error_summary: Optional[str] = None,
repo_root: Path,
ts: str,
) -> str:
"""Write the §5.3 / CONTRACT-4 pt5 override-accepted force-capture
artifact into the SAME capture directory `emit_capture()` used (keyed by
the SAME sanitized-slug + `ts`). Returns the written path.
`predicted_b_breakdown` is the full `[B]` kv-calc GB breakdown that
produced the verdict (E4 passes `res.diagnostics['b_breakdown']`).
`actual` is `null` iff BOTH `boot_peak_mib` and
`gpu_worker_reported_mib` are None (boot never reached allocation) — in
that case `predicted_vs_actual_delta_mib` is also `null` and
`exit_error_summary` carries why. `calibration_signal_not_validated` is
ALWAYS the literal `True` (mandatory; §5.3).
"""
san = sanitize_slug(einput.slug)
out_dir = Path(repo_root) / ".pull-captures" / san / ts
out_dir.mkdir(parents=True, exist_ok=True)
have_actual = (
boot_peak_mib is not None or gpu_worker_reported_mib is not None
)
if have_actual:
actual: Optional[dict] = {
"boot_peak_mib": (
int(boot_peak_mib) if boot_peak_mib is not None else None
),
"gpu_worker_reported_mib": (
int(gpu_worker_reported_mib)
if gpu_worker_reported_mib is not None
else None
),
}
else:
# boot never reached allocation -> actual is null; the WHY lives in
# exit_error_summary (CONTRACT-4 pt5).
actual = None
# predicted_vs_actual_delta_mib: only computable when we have a measured
# peak AND the prediction carries a comparable MiB figure; else null.
delta: Optional[int] = None
if actual is not None and boot_peak_mib is not None:
pred_mib = _predicted_total_mib(predicted_b_breakdown)
if pred_mib is not None:
delta = int(boot_peak_mib) - int(pred_mib)
pt5 = {
"point": "override_capture",
"predicted_b_breakdown": predicted_b_breakdown,
"actual": actual,
"predicted_vs_actual_delta_mib": delta,
"exit_error_summary": exit_error_summary,
# MANDATORY + LITERAL — never "fit validated" (§5.3).
"calibration_signal_not_validated": True,
}
path = out_dir / "pt5-override-capture.json"
_write_redacted_json(path, pt5)
return str(path)
def _predicted_total_mib(breakdown) -> Optional[int]:
"""Best-effort MiB total of the `[B]` GB breakdown (for the
predicted-vs-actual delta). The `[B]` breakdown is a `{component: GB}`
dict (`kv.raw_verdict()['breakdown_gb']`); sum numeric components and
convert GB -> MiB. Returns None if it is not a usable numeric mapping
(delta stays null — never fabricate a number)."""
if not isinstance(breakdown, dict) or not breakdown:
return None
total_gb = 0.0
saw = False
for v in breakdown.values():
if isinstance(v, (int, float)):
total_gb += float(v)
saw = True
if not saw:
return None
return int(round(total_gb * 1024.0))
# ---------------------------------------------------------------------------
# v0.8.2 CONTRACT-1.1 — capture-on-hard-block: the pt1-gate-only emitter.
#
# A SEPARATE, ADDITIVE function — the byte-preserving `emit_override_capture`
# precedent (a separate fn NOT invoked by `emit_capture()`; same
# `.pull-captures/<slug>/<ts>/` dir + the SAME `_redact_text` redaction).
# `emit_capture()`'s pt1-4 + manifest emitters are byte-behaviour-unchanged
# (`test-pullemit-capture.sh` still asserts `emit_capture()` writes ONLY
# pt1-4 + manifest). `pull.py` wires this on its terminal hard-block
# `return res` paths as a pure PASS-THROUGH capture — it emits a bundle
# BEFORE the existing return; the `return res` decision is UNCHANGED (zero
# §1-§6/§4.1/§5.x decision-logic change).
#
# Schema-2 bundle: a `pt1-gate.json` + `manifest.json` with `schema:2`,
# `outcome:"hard-block"`, the EXACT shipped `res.abort_reason` (NOT a
# semantic alias), `failure_class:null`. The manifest carries the
# per-abort-stratum key subset (the CONTRACT-1.1 enumerated table): the
# always-present row is guaranteed; model/arch/quant are `null` pre-deriver;
# topology is best-effort/nullable at every gate stratum (resolved here
# capture-only, mirroring `pull.py:853-855`, `null` when unavailable —
# GUARANTEED `null` for `hardware-sm-undetermined`); the post-C0 fields are
# always `null` (a gate-only bundle never reached them).
#
# F1's `read_gate_bundle()` (schema==2) consumes this; it validates ONLY the
# always-present row + `outcome=="hard-block"` + `failure_class is None` (it
# does NOT reuse the 22-key schema-1 validator). The raw `abort_reason` is
# carried through redaction into the bundle so a maintainer can distinguish
# a registry gap from a kernel bug (the H2 distinguishability mandate).
# ---------------------------------------------------------------------------
def _gate_topology_best_effort(
gpu_topology: Optional[tuple],
) -> tuple[Optional[str], Optional[str]]:
"""Capture-only best-effort `(topology_class, topology_summary_canonical)`
for a gate-only bundle. Mirrors `pull.py:853-855`: prefer an injected
`gpu_topology` (the `--hardware-gpus` override) else `detect_gpu_topology`
(needs nvidia-smi). Returns `(None, None)` when neither is available —
NOT a `pull.py` decision-logic change (capture-only). Never raises.
"""
topo = gpu_topology
if topo is None:
try: # late import — avoids a capture.py -> pull.py import cycle.
from scripts.lib.profiles.pull import ( # noqa: E402
detect_gpu_topology,
)
topo = detect_gpu_topology()
except Exception: # pragma: no cover - defensive
topo = None
if not topo:
return None, None
try:
_count, vram_mib, names = topo
if not vram_mib:
return None, None
n = len(vram_mib)
vram = min(int(v) for v in vram_mib)
topo_class = f"{n}x{vram}MiB"
tuples = sorted(
(str(nm), int(v)) for nm, v in zip(names, vram_mib)
)
topo_summary = "[" + ", ".join(
f"({nm}, {v})" for nm, v in tuples
) + "]"
return topo_class, topo_summary
except Exception: # pragma: no cover - defensive
return None, None
def emit_gate_capture(
*,
slug: str,
profile_like: str,
abort_reason: str,
confidence,
raw_verdict: Optional[str],
detail: str,
der=None,
hardware_sm=None,
gpu_topology: Optional[tuple] = None,
club3090_commit: str = "unknown",
kv_calc_version: Optional[str] = None,
predicted_b_breakdown=None,
repo_root: Path,
ts: Optional[str] = None,
) -> dict:
"""v0.8.2 CONTRACT-1.1 — write the pt1-gate-only redacted bundle on a
terminal hard-block. Writes `pt1-gate.json` + `manifest.json`
(`schema:2`, `outcome:"hard-block"`, the EXACT shipped `abort_reason`,
`failure_class:null`) under `<repo>/.pull-captures/<slug>/<ts>/`, then
updates the SHARED `.last` marker. Returns
`{paths:{gate,manifest}, dir:str, manifest:{...}}`. Never classifies
(§6.1 = `[F]`'s job) — `failure_class` is authoritatively `null`.
A SEPARATE function (NOT invoked by `emit_capture()`), cloning the
`emit_override_capture` byte-preserving precedent: same capture dir +
the SAME `_redact_text` redaction. Best-effort topology resolve is
capture-only (mirrors `pull.py:853-855`); `null` when unavailable —
GUARANTEED `null` for `hardware-sm-undetermined` (nvidia-smi absent).
"""
san = sanitize_slug(slug)
stamp = ts or datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
out_dir = Path(repo_root) / ".pull-captures" / san / stamp
out_dir.mkdir(parents=True, exist_ok=True)
# Best-effort model/arch/quant: `null` pre-deriver (a deriver-error /
# profile-like / repo-not-found terminal has no `der.profile`). We read
# the SAME shipped accessors `emit_capture()` uses (`_arch_family` /
# `_quant_label`); both already tolerate a missing profile -> None.
arch_family = _arch_family(der) if der is not None else None
quant_label = _quant_label(der) if der is not None else None
# Topology is NOT guaranteed at any gate stratum (CONTRACT-1.1):
# capture-only best-effort resolve, `null` when unavailable.
topology_class, topology_summary_canonical = _gate_topology_best_effort(
gpu_topology
)
# ---- pt1-gate.json (gate-only): the pre-download verdict snapshot ----
pt1 = {
"schema": GATE_SCHEMA,
"point": "gate",
"slug": slug,
"confidence": (
str(getattr(confidence, "name", confidence))
if confidence is not None
else None
),
"raw_verdict": raw_verdict,
"profile_like": profile_like,
"hardware_sm": hardware_sm,
"predicted_b_breakdown": predicted_b_breakdown,
# The EXACT shipped abort sub-token (NOT a semantic alias) — the
# §6.1 `gate_abort_reason` matcher keys on THIS. Carried through
# `_redact_text` so a maintainer can distinguish a registry gap
# from a kernel bug (the H2 distinguishability mandate).
"abort_reason": abort_reason,
"detail": detail,
"is_gate_only": True,
}
# ---- manifest.json schema:2 — per-abort-stratum key subset ----------
# ALWAYS-present row (guaranteed at every stratum). model/arch/quant are
# `null` pre-deriver. topology is best-effort/nullable. The post-C0
# fields are ALWAYS `null` (a gate-only bundle never reached them).
manifest = {
"schema": GATE_SCHEMA,
"slug": slug,
"utc_ts": stamp,
"club3090_commit": club3090_commit,
"outcome": "hard-block",
"abort_reason": abort_reason,
"failure_class": None,
# null if pre-deriver (profile-like / repo-not-found may have none).
"model": slug,
"model_id": slug,
"arch_family": arch_family,
"quant_label": quant_label,
# best-effort / nullable at every gate stratum (guaranteed null for
# hardware-sm-undetermined).
"topology_class": topology_class,
"topology_summary_canonical": topology_summary_canonical,
# post-C0 — NEVER reached on a gate-only bundle -> always null.
"selected_ctx": None,
"kv_format": None,
"smoke_capability_set": None,
"engine_pin": None,
"engine_version": None,
"kv_calc_version": kv_calc_version,
"submission_fingerprint": None,
# gate-only marker so F1 / F2 can branch without re-parsing.
"is_gate_only": True,
"capture_points": ["gate"],
}
paths = {
"gate": str(out_dir / "pt1-gate.json"),
"manifest": str(out_dir / "manifest.json"),
}
_write_redacted_json(Path(paths["gate"]), pt1)
_write_redacted_json(Path(paths["manifest"]), manifest)
# SHARED `.last` marker — the SAME helper `emit_capture()` calls (the
# CONTRACT-1.1 centralization mandate: gate-only is the commonest
# failure; if `.last` were `[E]`-only, `--submit-last` would miss it).
write_last_marker(Path(repo_root), out_dir)
return {"paths": paths, "dir": str(out_dir), "manifest": manifest}