CONTRACT-2 (§10-R4) arch-family registry expansion: +13 safetensors arch rows in arch_patches.yml (PhiForCausalLM — the microsoft/phi-2 STEP V1 on-rig no-arch-row anchor — Phi3Small, Gemma/Gemma3/Gemma3-CG, Starcoder2, Cohere, InternLM2, Mixtral/Qwen2Moe/Qwen3Moe MoE, Qwen2-VL). Additive data only, zero [C0]/decision-logic change. Zero false-pass by construction: each follows the established estimated-lower-bound/unverified-TRC precedent so [C0] still resolves needs-trust-remote-code-ack (fail-closed, bypassable ONLY by --trust-remote-code) — the expansion drops only the --experimental-arch requirement, never auto-passes; an arch still absent still hard-blocks no-arch-row. test-pullgate-gates.sh proves both, plus the #146-shape worked acceptance case (a hand-added awq_bf16_int4 weights variant the expanded flag schema/parity machinery absorbs cleanly). CONTRACT-2b-i chat-template attribution + behavioral drift_guard: new `chat_template` delivery class (VALID_DELIVERY_MECHANISM); froggeric (22 composes — 18 direct + 4 nvlink* via REAL Docker Compose extends: merge) and carnice (mount-only) brought under load_bearing_when + a behavioral drift_guard whose check encodes the self-contained symmetric restart+settle protocol (identical docker restart both arms, /v1/models healthy, 60s settle, >=3 bench runs/arm, grand-mean same-segment compare, flag only a 3/3 deterministic regression). Effective coverage uses REAL merge semantics: docker compose config (preferred) or a deterministic offline extends: merge applying the same rules (additive sequence merge; `!reset` removal) — never the unsound single-base text concat. .jinja artifact discovery catches an orphan vendored template. test-patch-attribution.sh adds the class checks + an H4 fixture asserting a `!reset` child AND a stopped-extending child both lose coverage (the false-negative is the dangerous direction). Generator emit kept in lock-step with reaches(). Documented as PATCH_POLICY.md §3.1. Rig-independent leak assertions added (str(abs_dir) not in shared; repo-relative-only — never a /opt|/home substring allowlist). RED-LINE: gates.py/pull.py/deriver.py/capture.py/loop_input.py/ classifier.py/dedup.py/submit_pull.py/kv-calc.py/failure_fingerprints.yml byte-unchanged; no shipped compose changed; patch_attribution.py c0_state/ is_artifact/compose_text/service_body byte-identical (additive only). Full test-*.sh suite green in the CI condition; kv-calc --calibration N/N. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
407 lines
18 KiB
Bash
Executable File
407 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
|
python3 - "$ROOT_DIR" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
sys.path.insert(0, str(root))
|
|
|
|
from scripts.lib.profiles.compose_registry import COMPOSE_REGISTRY # noqa: E402
|
|
from scripts.lib.profiles import patch_attribution as pa # noqa: E402
|
|
|
|
patches_path = root / "scripts/lib/profiles/patches.yml"
|
|
arches_path = root / "scripts/lib/profiles/arch_patches.yml"
|
|
seed_path = root / "scripts/lib/profiles/calibration_seed.yml"
|
|
|
|
errors: list[str] = []
|
|
known_gaps: list[str] = []
|
|
|
|
|
|
def load(path: Path) -> dict:
|
|
return pa.load(path, errors=errors, root=root)
|
|
|
|
|
|
patch_doc = load(patches_path)
|
|
arch_doc = load(arches_path)
|
|
seed_doc = load(seed_path)
|
|
|
|
patches = patch_doc.get("patches", [])
|
|
arches = arch_doc.get("arches", [])
|
|
seeds = seed_doc.get("anchors", [])
|
|
|
|
patch_ids: set[str] = set()
|
|
covered_files: list[Path] = []
|
|
genesis_envs: set[str] = set()
|
|
|
|
required_patch_keys = pa.REQUIRED_PATCH_KEYS
|
|
valid_patch_status = pa.VALID_PATCH_STATUS
|
|
|
|
for patch in patches:
|
|
missing = required_patch_keys - set(patch)
|
|
if missing:
|
|
errors.append(f"patch {patch.get('id', '<missing>')} missing keys: {sorted(missing)}")
|
|
pid = patch.get("id")
|
|
if not pid:
|
|
errors.append("patch entry missing id")
|
|
continue
|
|
if pid in patch_ids:
|
|
errors.append(f"duplicate patch id: {pid}")
|
|
patch_ids.add(pid)
|
|
if patch.get("status") not in valid_patch_status:
|
|
errors.append(f"{pid} has invalid status {patch.get('status')!r}")
|
|
delivery = patch.get("delivery") or {}
|
|
for key in ("dockerfile_bake", "entrypoint_invoke", "genesis"):
|
|
if key not in delivery or not isinstance(delivery.get(key), bool):
|
|
errors.append(f"{pid}.delivery.{key} must be boolean")
|
|
upstream = patch.get("upstream") or {}
|
|
for key in ("ref", "status", "drop_when"):
|
|
if not upstream.get(key):
|
|
errors.append(f"{pid}.upstream.{key} missing")
|
|
for rel in patch.get("files") or []:
|
|
target = root / rel
|
|
if not target.exists():
|
|
errors.append(f"{pid} references missing file/dir: {rel}")
|
|
else:
|
|
covered_files.append(target)
|
|
if patch.get("genesis_env"):
|
|
genesis_envs.add(patch["genesis_env"])
|
|
|
|
|
|
for artifact in sorted(p for p in (root / "models").rglob("*") if p.is_file() and pa.is_artifact(p)):
|
|
if not pa.covered(artifact, covered_files):
|
|
errors.append(f"orphan patch artifact lacks patches.yml entry: {artifact.relative_to(root)}")
|
|
|
|
compose_files = sorted((root / "models").glob("**/compose/**/*.yml"))
|
|
found_genesis = set()
|
|
for compose in compose_files:
|
|
text = compose.read_text(encoding="utf-8")
|
|
found_genesis.update(re.findall(r"GENESIS_ENABLE_[A-Z0-9_]+", text))
|
|
missing_genesis = found_genesis - genesis_envs
|
|
if missing_genesis:
|
|
errors.append(f"Genesis env flags missing patches.yml entries: {sorted(missing_genesis)}")
|
|
|
|
|
|
def compose_text(compose_name: str, seen: set[Path] | None = None) -> str:
|
|
return pa.compose_text(root, compose_name, seen)
|
|
|
|
|
|
def gap_declared(patch: dict, compose_name: str) -> bool:
|
|
return pa.gap_declared(patch, compose_name)
|
|
|
|
|
|
def reaches(patch: dict, compose_name: str) -> bool:
|
|
return pa.reaches(root, patch, compose_name)
|
|
|
|
|
|
for patch in patches:
|
|
for lb in patch.get("load_bearing_when") or []:
|
|
for compose_name in lb.get("composes") or []:
|
|
if compose_name not in COMPOSE_REGISTRY:
|
|
errors.append(f"{patch['id']} load_bearing_when references unknown compose {compose_name}")
|
|
continue
|
|
if reaches(patch, compose_name):
|
|
continue
|
|
msg = f"{patch['id']} does not reach {compose_name}"
|
|
if gap_declared(patch, compose_name):
|
|
known_gaps.append(msg)
|
|
else:
|
|
errors.append(msg)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CONTRACT-2b-i — chat_template delivery class + REAL extends merge.
|
|
# ---------------------------------------------------------------------------
|
|
# (1) Every patch's delivery_mechanism is in the v0.8.2 valid set (the
|
|
# vocabulary now includes `chat_template`).
|
|
for patch in patches:
|
|
dm = patch.get("delivery_mechanism")
|
|
if dm not in pa.VALID_DELIVERY_MECHANISM:
|
|
errors.append(
|
|
f"{patch['id']} delivery_mechanism {dm!r} not in "
|
|
f"{sorted(pa.VALID_DELIVERY_MECHANISM)}"
|
|
)
|
|
|
|
# (2) chat_template patches: spec shape + a behavioral drift_guard whose
|
|
# check encodes the SELF-CONTAINED symmetric restart+settle protocol
|
|
# (the #150 lesson — a non-symmetric guard flaps and is ignored).
|
|
chat_template_patches = [
|
|
p for p in patches if p.get("delivery_mechanism") == "chat_template"
|
|
]
|
|
for patch in chat_template_patches:
|
|
spec = patch.get("delivery_spec") or {}
|
|
for k in ("jinja", "mounted_at", "wired_at"):
|
|
if not spec.get(k):
|
|
errors.append(f"{patch['id']} chat_template delivery_spec missing {k}")
|
|
jinja_rel = spec.get("jinja")
|
|
if jinja_rel and not (root / jinja_rel).exists():
|
|
errors.append(f"{patch['id']} chat_template jinja missing on disk: {jinja_rel}")
|
|
if jinja_rel and Path(jinja_rel).suffix not in pa.CHAT_TEMPLATE_ARTIFACT_SUFFIXES:
|
|
errors.append(f"{patch['id']} chat_template jinja is not a .jinja artifact: {jinja_rel}")
|
|
dg = patch.get("drift_guard") or {}
|
|
if dg.get("kind") != "behavioral":
|
|
errors.append(f"{patch['id']} chat_template drift_guard must be kind: behavioral")
|
|
chk = (dg.get("check") or "").lower()
|
|
for token in ("symmetric", "docker restart", "settle", ">=3", "grand mean"):
|
|
if token not in chk:
|
|
errors.append(
|
|
f"{patch['id']} chat_template drift_guard.check must encode the "
|
|
f"self-contained symmetric protocol (missing {token!r})"
|
|
)
|
|
|
|
# (3) Orphan-artifact discovery for vendored `.jinja` templates: every
|
|
# chat-template `.jinja` under a model patches/ tree MUST be owned by a
|
|
# delivery_mechanism: chat_template patch (so a bad/regressed/re-vendored
|
|
# template can never ship with ZERO attribution coverage).
|
|
ct_covered_files = []
|
|
for patch in chat_template_patches:
|
|
for rel in patch.get("files") or []:
|
|
ct_covered_files.append(root / rel)
|
|
for jinja in sorted(
|
|
p for p in (root / "models").rglob("*") if p.is_file() and pa.is_chat_template_artifact(p)
|
|
):
|
|
if not pa.covered(jinja, ct_covered_files):
|
|
errors.append(
|
|
f"orphan chat-template artifact lacks a chat_template patches.yml entry: "
|
|
f"{jinja.relative_to(root)}"
|
|
)
|
|
|
|
# (4) Effective coverage MUST use REAL Docker Compose merge semantics, NOT
|
|
# declared lines. The dangerous direction is the FALSE NEGATIVE: a child
|
|
# that !reset/overrides/REMOVES the mount must be caught as a coverage
|
|
# loss. Build a synthetic base+child fixture where the child re-declares
|
|
# `volumes`/`command` WITHOUT the template and assert reaches() == False
|
|
# (the legacy single-base text-concat would still "see" the base's mount
|
|
# line and wrongly return True — that is the #377 failure mode).
|
|
import tempfile # noqa: E402
|
|
|
|
if chat_template_patches:
|
|
ctp = chat_template_patches[0]
|
|
with tempfile.TemporaryDirectory() as _td:
|
|
_tdp = Path(_td)
|
|
mounted = (ctp.get("delivery_spec") or {}).get("mounted_at")
|
|
base = _tdp / "base.yml"
|
|
keep_child = _tdp / "keep.yml"
|
|
reset_child = _tdp / "reset.yml"
|
|
noextend = _tdp / "noextend.yml"
|
|
base.write_text(
|
|
"services:\n"
|
|
" base-svc:\n"
|
|
" image: scratch\n"
|
|
" command: [--model, m, --chat-template, %s]\n"
|
|
" volumes:\n"
|
|
" - ../../patches/froggeric-chat-template/chat_template.jinja:%s:ro\n"
|
|
% (mounted, mounted),
|
|
encoding="utf-8",
|
|
)
|
|
# (a) Child that KEEPS inheritance (only overrides an env) -> still
|
|
# covered. Proves extends: IS merged (not ignored).
|
|
keep_child.write_text(
|
|
"services:\n"
|
|
" keep-svc:\n"
|
|
" extends:\n"
|
|
" file: base.yml\n"
|
|
" service: base-svc\n"
|
|
" environment:\n"
|
|
" - X=1\n",
|
|
encoding="utf-8",
|
|
)
|
|
# (b) Child that REMOVES the mount + --chat-template via the Compose
|
|
# `!reset` tag — the ONLY in-Compose removal mechanism across
|
|
# extends: (a plain re-declared `[]` does NOT drop a base
|
|
# sequence; Compose merges extends: sequences additively). The
|
|
# coverage loss MUST be caught (reaches == False). A text-concat
|
|
# would still "see" the base's mount line -> false-negative.
|
|
reset_child.write_text(
|
|
"services:\n"
|
|
" reset-svc:\n"
|
|
" extends:\n"
|
|
" file: base.yml\n"
|
|
" service: base-svc\n"
|
|
" volumes: !reset []\n"
|
|
" command: !reset [--model, m]\n",
|
|
encoding="utf-8",
|
|
)
|
|
# (c) The real #377 mode: a compose that simply STOPPED extending
|
|
# its template-bearing base — no extends: at all. Must NOT be
|
|
# reported covered. (Deterministic everywhere; no docker / tags.)
|
|
noextend.write_text(
|
|
"services:\n"
|
|
" noextend-svc:\n"
|
|
" image: scratch\n"
|
|
" command: [--model, m]\n",
|
|
encoding="utf-8",
|
|
)
|
|
if not pa.reaches(root, ctp, str(keep_child)):
|
|
errors.append(
|
|
"chat_template real-merge: a child that inherits the base "
|
|
"(extends:, no override) lost coverage — extends not merged"
|
|
)
|
|
if pa.reaches(root, ctp, str(reset_child)):
|
|
errors.append(
|
|
"chat_template real-merge FALSE-NEGATIVE: a child that "
|
|
"!reset-removed the chat-template mount/--chat-template "
|
|
"wiring was still reported covered (the #377 dangerous "
|
|
"direction — extends resolved by text concat, not real "
|
|
"Docker Compose merge)"
|
|
)
|
|
if pa.reaches(root, ctp, str(noextend)):
|
|
errors.append(
|
|
"chat_template real-merge FALSE-NEGATIVE: a compose that "
|
|
"STOPPED extending its template-bearing base was still "
|
|
"reported covered (the literal #377 silent-drift mode)"
|
|
)
|
|
|
|
# (5) Rig-independent leak-assertion convention (mandatory — the V2 on-rig
|
|
# lesson). The chat_template effective-coverage path resolves the
|
|
# merged compose via `docker compose config`, which renders the mount
|
|
# SOURCE as an ABSOLUTE host path (e.g. /opt/ai/.../patches/...jinja).
|
|
# That absolute path MUST NOT leak into any committed/shared artifact:
|
|
# the patches.yml `jinja`/`mounted_at` must be repo-relative/container
|
|
# paths, NEVER absolute. Assert with `str(abs_dir) not in shared` AND
|
|
# that only the repo-relative form appears — NEVER a bare `/opt|/home`
|
|
# substring allowlist (a sandbox path structurally defeats that; that
|
|
# exact miss shipped a real leak in V2).
|
|
abs_root = str(root.resolve())
|
|
for patch in chat_template_patches:
|
|
spec = patch.get("delivery_spec") or {}
|
|
jinja_rel = spec.get("jinja") or ""
|
|
mounted = spec.get("mounted_at") or ""
|
|
shared = f"{jinja_rel}\n{mounted}\n{patch.get('id','')}"
|
|
if abs_root in shared:
|
|
errors.append(
|
|
f"{patch['id']} chat_template delivery_spec LEAKS the absolute "
|
|
f"repo path ({abs_root!r}) — must be repo-relative/container only"
|
|
)
|
|
if jinja_rel.startswith("/") or jinja_rel.startswith(abs_root):
|
|
errors.append(
|
|
f"{patch['id']} chat_template jinja must be repo-relative, "
|
|
f"got absolute: {jinja_rel}"
|
|
)
|
|
# The repo-relative form (the ONLY acceptable shape) must be present.
|
|
if jinja_rel and not jinja_rel.startswith("models/"):
|
|
errors.append(
|
|
f"{patch['id']} chat_template jinja must be a repo-relative "
|
|
f"models/... path, got: {jinja_rel}"
|
|
)
|
|
|
|
# Every load-bearing chat_template compose's SHIPPED mount line must use
|
|
# the repo-relative `../../patches/...` form, never the absolute path the
|
|
# `docker compose config` merge renders (the merge output is internal to
|
|
# reaches() and is NEVER emitted/shared — assert that invariant on the
|
|
# committed composes directly, rig-independently).
|
|
for patch in chat_template_patches:
|
|
for lb in patch.get("load_bearing_when") or []:
|
|
for compose_name in lb.get("composes") or []:
|
|
if compose_name not in COMPOSE_REGISTRY:
|
|
continue
|
|
cpath = root / COMPOSE_REGISTRY[compose_name]["compose_path"]
|
|
ctext = cpath.read_text(encoding="utf-8")
|
|
if abs_root in ctext:
|
|
errors.append(
|
|
f"committed compose {compose_name} LEAKS the absolute "
|
|
f"repo path {abs_root!r} (must use the repo-relative "
|
|
f"../../patches/... mount form)"
|
|
)
|
|
|
|
arch_allowed_keys = pa.ARCH_ALLOWED_KEYS
|
|
arch_required = pa.ARCH_REQUIRED_KEYS
|
|
valid_trc = pa.VALID_TRC
|
|
valid_arch_status = pa.VALID_ARCH_STATUS
|
|
valid_confidence = pa.VALID_CONFIDENCE
|
|
|
|
|
|
def c0_state(row: dict, tp: int, trust_ack: bool = False) -> str:
|
|
return pa.c0_state(row, tp, trust_ack)
|
|
|
|
|
|
for row in arches:
|
|
arch = row.get("arch", "<missing>")
|
|
unknown = set(row) - arch_allowed_keys
|
|
missing = arch_required - set(row)
|
|
if unknown:
|
|
errors.append(f"arch {arch} has unknown keys: {sorted(unknown)}")
|
|
if missing:
|
|
errors.append(f"arch {arch} missing keys: {sorted(missing)}")
|
|
continue
|
|
if row["status"] not in valid_arch_status:
|
|
errors.append(f"arch {arch} invalid status {row['status']!r}")
|
|
if row["confidence"] not in valid_confidence:
|
|
errors.append(f"arch {arch} invalid confidence {row['confidence']!r}")
|
|
trc = row["requires_trust_remote_code"]
|
|
if trc not in valid_trc:
|
|
errors.append(f"arch {arch} invalid requires_trust_remote_code={trc!r}")
|
|
evidence = row.get("requires_trust_remote_code_evidence")
|
|
if not evidence:
|
|
errors.append(f"arch {arch} missing requires_trust_remote_code_evidence")
|
|
if trc == "unverified" and evidence != "none":
|
|
errors.append(f"arch {arch} has unverified trust_remote_code but evidence is not none")
|
|
if trc in {"true", "false"} and evidence == "none":
|
|
errors.append(f"arch {arch} has {trc} trust_remote_code without real evidence")
|
|
for pid in row.get("required_patches") or []:
|
|
if pid not in patch_ids:
|
|
errors.append(f"arch {arch} references unknown patch id {pid}")
|
|
valid_tp = row.get("valid_tp") or {}
|
|
divisors = valid_tp.get("tp_divisors")
|
|
if not isinstance(divisors, list) or not divisors or not all(isinstance(tp, int) for tp in divisors):
|
|
errors.append(f"arch {arch} valid_tp.tp_divisors must be a non-empty integer list")
|
|
continue
|
|
if not isinstance(valid_tp.get("marlin_alignment_required"), bool):
|
|
errors.append(f"arch {arch} valid_tp.marlin_alignment_required must be boolean")
|
|
if valid_tp.get("moe_layout") not in {"dense", "moe"}:
|
|
errors.append(f"arch {arch} valid_tp.moe_layout must be dense|moe")
|
|
states = {c0_state(row, tp) for tp in divisors}
|
|
if len(states) != 1:
|
|
errors.append(f"arch {arch} C0 declared TP states not singular: {sorted(states)}")
|
|
negative_tp = max(divisors) + 1
|
|
if c0_state({**row, "requires_trust_remote_code": "false"}, negative_tp) != "engine-support-unknown":
|
|
errors.append(f"arch {arch} negative TP did not resolve engine-support-unknown")
|
|
if trc == "unverified" and c0_state(row, divisors[0]) != "needs-trust-remote-code-ack":
|
|
errors.append(f"arch {arch} unverified trust_remote_code did not fail closed")
|
|
|
|
seed_required = pa.SEED_REQUIRED_KEYS
|
|
for seed in seeds:
|
|
label = f"{seed.get('model', '<missing>')}:{seed.get('kv_format', '<missing>')}:{seed.get('selected_ctx', '<missing>')}"
|
|
missing = seed_required - set(seed)
|
|
if missing:
|
|
errors.append(f"seed {label} missing keys: {sorted(missing)}")
|
|
continue
|
|
if seed["provenance"] != "seed-from-measured-corpus":
|
|
errors.append(f"seed {label} has invalid provenance {seed['provenance']!r}")
|
|
if seed["confidence"] == "exact" and not seed["source"].startswith("BENCHMARKS.md#"):
|
|
errors.append(f"seed {label} exact confidence lacks BENCHMARKS source")
|
|
source_file = seed["source"].split("#", 1)[0]
|
|
if not (root / source_file).exists():
|
|
errors.append(f"seed {label} source file missing: {source_file}")
|
|
measured = seed["measured"] or {}
|
|
for key in ("vram_mib_per_card", "tps_short", "tps_loaded_ctx", "soak_continuous"):
|
|
if key not in measured:
|
|
errors.append(f"seed {label} measured.{key} missing")
|
|
if measured.get("soak_continuous") not in {"pass", "fail", "not-run"}:
|
|
errors.append(f"seed {label} invalid soak_continuous {measured.get('soak_continuous')!r}")
|
|
smoked = set(seed.get("smoked_capabilities") or [])
|
|
unsmoked = set(seed.get("unsmoked_capabilities") or [])
|
|
if smoked & unsmoked:
|
|
errors.append(f"seed {label} capabilities appear in both smoked and unsmoked: {sorted(smoked & unsmoked)}")
|
|
if "tool-call-stream" in smoked:
|
|
errors.append(f"seed {label} claims tool-call-stream smoked; #145 guard forbids that without explicit working-source evidence")
|
|
|
|
if known_gaps:
|
|
print("[patch-attribution] known delivery gaps:")
|
|
for gap in sorted(known_gaps):
|
|
print(f" - {gap}")
|
|
|
|
if errors:
|
|
print("[patch-attribution] FAIL")
|
|
for err in errors:
|
|
print(f" - {err}")
|
|
sys.exit(1)
|
|
|
|
print(f"[patch-attribution] PASS: {len(patches)} patch entries, {len(arches)} arch rows, {len(seeds)} calibration seeds")
|
|
PY
|