Files
club-3090/docs/PATCH_POLICY.md
noonghunna 999c93fe8c feat(pull): v0.8.2 STEP V3 — arch-registry expansion + chat-template attribution/drift_guard
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) <noreply@anthropic.com>
2026-05-18 18:29:58 +00:00

15 KiB
Raw Blame History

Patch Policy — the contributor contract for load-bearing patches (v0.8.0, #141)

This is the durable contract for anyone (human or agent) adding, modifying, or removing a patch that the v0.8.0 compose generator (scripts/generate-compose.sh) consumes. It is prescriptive and self-contained: follow it exactly, and scripts/tests/test-patch-attribution.sh + scripts/tests/test-generate-compose.sh will gate your PR cleanly.

It exists for one reason, stated up front so it frames every rule below:

The #145 lesson

A patch can be present in the repo, declared "load-bearing", and still never reach the running engine — silently. That is the #72 / #145-class failure: a load-bearing fix that the compose does not actually deliver, with no error, just a quietly-wrong server. Every mechanism in this document exists to make a silently-undelivered load-bearing patch impossible to introduce without it failing loud.

For the data model these rules sit on, see docs/PATCH_ATTRIBUTION.md. For how the generator uses this metadata, see docs/COMPOSE_GENERATOR.md.


1. Where patches and metadata live

File Role
models/<model>/<engine>/patches/<patch-dir>/ the patch artifact itself (overlay file(s), sidecar .py, or install.sh)
scripts/lib/profiles/patches.yml the patch entry — what it fixes, how it is delivered, where it is load-bearing, its drift-guard
scripts/lib/profiles/arch_patches.yml declarative arch → engine-pin / valid-TP / trc gate (C0 authority; strict closed key-set — do not add keys)
scripts/lib/profiles/profile_runtime.yml per-profile captured template + genesis_equipped; arch_model_xref trc evidence ledger

Add the patches.yml entry in the same change as the patch artifact. Never land a patch file without its entry, and never land an entry that points at a missing artifact.


2. Required per-patch metadata

A load-bearing patch entry in patches.yml MUST carry all of:

Field Requirement
id unique, stable, descriptive
model list of model slugs the patch applies to
files repo-relative path(s) to the patch artifact
load_bearing_when[].composes the exact list of profile keys this patch is load-bearing for. This is the only thing the generator keys patch selection off (X ∈ P.load_bearing_when[].composes). Each entry also carries reason + evidence.
delivery_mechanism one of python_sidecar, site_package_overlay, install_script, chat_template (vendored .jinja override — see §3.1), or none (diagnostics / negative-result / Genesis-env patches)
delivery_spec the concrete wiring: mount target(s) (overlay_files[].dest, overlay_dir+dest_root, mounted_at), sidecar/script path, invoke command, and wired_at (volumes, entrypoint, or both). This is the single source both the generator's emit and reaches()'s reachability validation read — they must agree by construction.
drift_guard mandatory for any load-bearing patch (kind, check, on_fail). See §4.
capability the capability the patch delivers (e.g. tp-weight-load, tool-choice-required, tool-call-stream, dflash-spec-decode)
foundational true if weights/boot cannot proceed without it; false if absence merely loses a capability. Drives the §4.1 grade.
upstream ref + status + drop_when — the condition under which the patch should be removed
status verified, unverified, or suspect. Use unverified honestly — never claim verified without evidence. The load_bearing_when[].evidence string must point at a real doc/README anchor.
delivery_gaps[] declare here, do not silently fix, any profile where the patch is load-bearing but cannot reach the compose today (the #145-class boundary). Each gap carries composes + an issue string.

The legacy delivery: boolean block (dockerfile_bake/entrypoint_invoke/genesis) is DEPRECATED and READ-ONLY — retained verbatim only because test-patch-attribution.sh still reads it. Do not put new wiring decisions there; use delivery_mechanism / delivery_spec.

Diagnostics / no-op / negative-local-result / Genesis-env patches set delivery_mechanism: none and have no delivery_spec.


3. Hard rule: guarded python_sidecar is the default delivery mechanism

Choose the delivery mechanism in this order of preference:

  1. python_sidecar (DEFAULT). A guarded sidecar .py invoked from the entrypoint before vllm imports/serves. This is the locked default for any new patch unless there is a concrete reason it cannot work.
  2. install_script. A mounted install.sh invoked from the entrypoint (invoked_before: vllm-import). Use when the change is a multi-step install rather than a single Python shim.
  3. site_package_overlay (LAST RESORT). A bind-mount over a dist-packages/... file. Permitted only for an isolated, single-file emergency. It carries the #145-class risk (a stale/conflicting overlay silently shadows upstream — overlays of speculative.py / gpu_model_runner.py etc. conflict with each other; only one variant can run at a time) and must be re-homed to a sidecar or upstream fix as soon as the emergency is over. A multi-file overlay must record its file_count and a note flagging the #145-risk and any conflicting overlay it is mutually exclusive with.

If you reach for site_package_overlay, justify it in the PR and file the re-home follow-up.

3.1 The chat_template delivery class

A model chat-template override (a vendored .jinja mounted into the container) is its own delivery class — delivery_mechanism: chat_template. A bad/regressed/re-vendored template is exactly the #145 silent-break class (tool-call XML / reasoning delimiters / streaming), so it MUST be attributed and drift-guarded like any other load-bearing patch, not left invisible to the generator and test-patch-attribution.

delivery_spec for a chat_template patch carries:

  • jinja — repo-relative path to the vendored .jinja (it is also the patch's files[] entry; an orphan .jinja under a model patches/ tree with no chat_template patch is a hard test failure);
  • mounted_at — the container path the .jinja is bind-mounted to;
  • mount_mode — typically ro;
  • invoke — the serving wiring. Two in-tree styles:
    • explicit --chat-template <mounted_at> (e.g. froggeric): wired_at: [volumes, entrypoint];
    • mount-only (e.g. carnice mounts over the model dir's chat_template.jinja and vLLM auto-loads it): wired_at: [volumes], no --chat-template arg;
  • wired_at — as above.

Effective coverage is computed from the REAL merged compose graph. A chat_template patch's reachability is resolved with Docker Compose extends: merge semantics (docker compose -f <child> config, or a deterministic offline merge that applies the same rules), never a raw single-base text concat. This is mandatory because a child compose can !reset the base's mount/command or simply stop extending a template-bearing base — a text concat would still "see" the base's mount line and report coverage that no longer exists (a false negative — the dangerous direction; the #377 silent-drift mode). Note Compose merges extends: sequences additively: a plain re-declared volumes: [] does not drop a base mount; only the !reset tag (or not extending the base at all) removes it. test-patch-attribution.sh carries a fixture asserting a !reset child and a stopped-extending child both lose coverage.

The drift_guard for a chat_template patch is kind: behavioral and its check MUST encode the self-contained symmetric restart+settle protocol: identical docker restart <container> on BOTH arms → wait for /v1/models healthy → fixed 60 s settle → ≥3 bench.sh runs/arm → compare the grand mean of the SAME canonical bench segment (NARRATIVE 800-word essay + CODE; never mix segments, never a single run); flag ONLY a deterministic regression reproduced across all 3 runs. A non-symmetric guard fabricates phantom regressions (the asymmetric-restart "7%" artifact) and gets ignored. The same guard must clear the next template re-vendor.


4. Hard rule: no drift_guard ⇒ not load-bearing in a generated compose

A patch with no drift_guard is, by definition, not load-bearing in a generated compose. The generator only wires a patch whose state is "applies cleanly" — and "applies cleanly" means drift-guard-tested (a locked decision). If you believe a patch is load-bearing, you MUST give it a drift_guard; if you cannot articulate a guard, the patch is not eligible to be wired by the generator.

drift_guard fields:

  • kindimport-and-boot or behavioral.
  • check — a concrete, testable statement of what "still applies" means on the selected nightly.
  • on_fail — the §4.1 grade (see below).

4.1 Drift fails loud — never silently repaired

The drift-guard is a runtime probe. The generator cannot run it at generation time (no engine container), so it wires the maintainer-tested patch and surfaces the guard for the boot leg / operator to re-run. When a guard fails, the outcome is graded — and the generator never repairs:

on_fail / foundational Outcome Rationale
capability-degraded (foundational: false) patch OMITTED, compose flagged DEGRADED; run needs --accept-degraded (else exit 3). Patch is never wired. The model still serves, minus one capability. Honest degradation beats a silently-broken capability.
hard-refuse / foundational: true hard-refuse (exit 2), even with --accept-degraded. Weights/boot depend on it; there is nothing safe to emit.

Gap-before-guard ordering: a declared delivery_gaps[] entry covering the profile is evaluated before the drift-guard — the patch is selected-but-undelivered (wiring omitted, header WARNING) and the guard is skipped. This is the structural acknowledgement of a known coverage boundary; it is loud (header category [2]) by construction, never silent.

The contract: drift never gets repaired, only flagged. A failed capability-scoped guard degrades; a failed foundational guard refuses; neither edits the patch.


5. Hard rule: Genesis patches are arch-data only, never generator-emitted

The compose generator is non-Genesis, vLLM-only (a permanently locked v0.8.x scope decision). Genesis patches:

  • live in patches.yml/arch_patches.yml as data only (so the attribution audit stays complete);
  • set delivery_mechanism: none (they are env-gated, not generator-wired);
  • are never emitted by the generator — any profile that is genesis_equipped: true (compose has a GENESIS_* / _genesis token, or kv_format starts turboquant) is a clean refusal.

Do not add a Genesis patch expecting the generator to wire it. It will not, and that is intentional.


6. Contributor workflow

  1. Land the artifact + entry together. Add the patch under models/<model>/<engine>/patches/<patch-dir>/ and its patches.yml entry (all §2 fields) in the same commit.
  2. Pick the delivery mechanism per §3 (default python_sidecar). Write the delivery_spec so wired_at and the mount/invoke markers describe the actual wiring in the compose(s) you list under load_bearing_when[].composes.
  3. Write a real drift_guard (§4). No guard ⇒ the generator will not wire it (§4 hard rule) — that is a design signal, not a workaround.
  4. Wire it into the shipped compose(s) you declared, OR — if you cannot reach a listed compose today — declare a delivery_gaps[] entry with an honest issue string. Do not silently fix runtime files instead of declaring the gap (that is the #145 failure mode).
  5. If the patch gates an arch/engine path, extend the matching arch_patches.yml row (required_patches, engine_pin, valid_tp, requires_trust_remote_code). Do not add new top-level keys — arch_patches.yml is a strict closed key-set; arch-scoped fold-ins go in profile_runtime.yml's arch_model_xref.
  6. Run the gates (next section). Both must be green; the patch-attribution summary must stay exactly 61 patch / 11 arch / 18 calibration plus your delta (the audit re-counts — keep the known-gaps list intentional).

7. How the tests gate a patch PR

Two tests are the contract. They are the contract; patches are fixed to them, never the reverse.

scripts/tests/test-patch-attribution.sh

Imports scripts/lib/profiles/patch_attribution.py and audits every patches.yml / arch_patches.yml / calibration_seed.yml entry. It checks: schema (all §2 required keys present, valid status/confidence/trc tri-state), the closed arch key-set, artifact coverage, and reachability:

reaches(root, patch, name_or_path) parses the comment-stripped service body only (everything from the top-level services: key onward, header banner and # comments removed) and validates the patch's actual delivery_spec wiring — the declared mount target(s) and/or entrypoint invoke at the wired_at insertion point(s) — not a substring of the patch ID.

Consequences for a contributor:

  • A patch ID merely named in a comment/header does not count as reached — you must wire the real delivery_spec artifact.
  • A patch declared load-bearing for a compose it does not actually reach fails the audit unless you declared the delivery_gaps[] entry. The audit prints the gap list explicitly — a silently-undelivered load-bearing patch cannot pass.

scripts/tests/test-generate-compose.sh

Generates the five golden triples and asserts the golden-parity invariant (semantic diff confined to the two insertion points; constants + image expression verbatim), that wired patches reaches() the generated compose, that selected-but-undelivered patches are not reachable, that no --trust-remote-code is emitted, and the full refusal/degraded matrix.

If your patch is load-bearing for a golden triple, it must wire cleanly into the generated compose (or be a correctly-declared gap). If it changes the captured service body of a golden profile, update the shipped compose so the parity invariant holds — the generator captures, it never synthesizes.

Both tests must be RC=0 before a patch PR merges. A silently-undelivered load-bearing patch is precisely the failure these gates exist to prevent — if either test goes red, the contract has caught exactly what it is for.