[Senate] Model artifacts WS4 — eval gate + promotion policy

← All Specs

[Senate] Model artifacts WS4 — eval gate + promotion policy

Task

  • ID: task-id-pending
  • Type: one-shot (policy + evaluator + promotion emitter; no
backfill pass required because WS3 is the first source of
candidate versions)
  • Frequency: one-shot to ship; runs on every candidate version going
forward
  • Layer: Senate (quality gate + governance rule)

Goal

Stop new model versions from silently replacing their parent. Enforce
that a candidate version becomes is_latest=1 only after it clears a
reproducible, statistically-defensible eval gate — and make rejection
data available in promotion_state so agents can diagnose + iterate
instead of guessing why their version never flipped.

What it does

  • Adds scidex_tools/model_eval_gate.py with the canonical gate runner:

def run_eval_gate(candidate_artifact_id: str) -> dict:
      """Run the benchmark suite declared for this model, compare to
      parent, decide promotion. Writes outcome to model_versions +
      emits world_model_improvements event.
      """

  • Gate logic:
1. Loads the candidate + parent model_versions rows.
2. Resolves benchmark_id (from candidate; if null, falls back to
parent's benchmark).
3. Loads the held-out test split declared in the benchmark manifest.
4. Runs artifacts/models/{candidate_id}/eval.py and
artifacts/models/{parent_id}/eval.py on the same split (shared
bwrap sandbox invocation), captures primary + secondary metrics.
5. Computes deltas. For the primary metric, runs a bootstrap (1000
resamples, deterministic seed per (candidate_id, parent_id)) to
get a 95% CI on the delta.
6. Decides:
- If delta ≥ 0 and 95% CI excludes 0 → promote.
- If delta < 0 or CI includes 0 → reject, unless the
candidate's changelog declares tradeoff_justification with an
explicit non-primary-metric gain (e.g. "param_count": "-50%",
"latency": "-4x") → promote with tradeoff.
- Otherwise → reject; record the metric delta, the CI, and the
reason in model_versions.promotion_notes.
  • On promote:
- Candidate's artifacts.is_latest=1,
artifacts.lifecycle_state='active',
model_versions.promotion_state='promoted'.
- Parent's artifacts.is_latest=0,
artifacts.lifecycle_state='superseded',
artifacts.superseded_by=<candidate_id>,
model_versions.promotion_state='superseded'.
- Emit world_model_improvements row with
event_type='model_version_promoted',
target_artifact_id=<candidate_id>,
magnitude = delta magnitude bucket,
detection_metadata containing the full metric table + CI.
- Write an artifact_lifecycle_history row on parent.
  • On reject:
- Candidate stays is_latest=0,
lifecycle_state='candidate',
model_versions.promotion_state='rejected',
promotion_notes populated with metric table + CI + reason.
- No event emitted; no parent mutation.
- Task opens a follow-up candidate-rejected ticket for the author
agent so the rejection is visible.
  • Adds a governance policy doc under
governance_artifacts/model_promotion_policy.md describing the gate,
the tradeoff-justification allowlist, and the appeal path.

Success criteria

  • run_eval_gate() executes on ≥1 candidate version (produced by WS3);
records either a promotion or a rejection with full metric delta.
  • For the promoted case: parent is correctly marked superseded,
world_model_improvements event emitted, detail page (once UI lands)
shows green metric-delta table.
  • For the rejected case: candidate stays lifecycle_state='candidate';
promotion_notes is populated; no orphan is_latest=1 flip.
  • Bootstrap seed is deterministic: running the gate twice on the same
(candidate, parent) pair yields byte-identical CI bounds.
  • Tradeoff-justification path validated by a synthetic fixture: a
candidate with metadata.tradeoff_justification and negative primary
metric delta is promoted; without it, rejected.
  • Governance doc ≥40 lines; cited from the quest spec.

Quality requirements

  • No stub gate: the evaluator runs real eval.py from the subtree on a
real held-out split. Synthetic / placeholder metrics fail the task.
  • Reference quest_quality_standards_spec.md — no one-sided eval; the
parent is always re-evaluated on the same split, never compared to a
stale metric from the metadata blob.
  • Reference quest_senate_spec.md for the governance-policy filing
convention.
  • Bootstrap size (1000) is the minimum; quick-check mode (100) is
allowed for the dry-run pre-check but not for the actual promotion
decision.
  • No silent overrides: an agent cannot call force_promote(). Human
appeal goes through Senate review per the policy doc.
  • @log_tool_call records every gate invocation with the candidate +
parent IDs and the decision.

Verification

2026-04-18 06:40 PT — Verification against origin/main (commit 7007d0c5f)

Task deemed COMPLETE. Implementation was merged to main in prior work.

Files verified on origin/main:

  • scidex_tools/model_eval_gate.py (31711 bytes) — exists and implements:
- run_eval_gate(candidate_artifact_id) with @log_tool_call decorator (line 387)
- _bootstrap_delta_cis() with deterministic seed via _deterministic_seed() (lines 118-152)
- _run_eval_in_sandbox() using bwrap sandbox (line 157+)
- Decision logic: delta≥0+CI excludes 0 → promote; delta<0+valid tradeoff → promote_with_tradeoff; else reject (lines 530-548)
- On promote: candidate is_latest=1/active, parent superseded, world_model_improvements emitted, artifact_lifecycle_history written (lines 615-692)
- On reject: candidate stays candidate, promotion_state=rejected, promotion_notes populated (lines 598-612)
- run_eval_gate_quick() for dry-run with 100 resamples (line 725)
- CLI entry point (line 737+)
  • governance_artifacts/model_promotion_policy.md (7327 bytes, 192 lines) — exists and covers:
- Eval gate steps, bootstrap requirements, no silent overrides
- Decision table, on-promote/on-reject semantics
- Tradeoff-justification allowlist, appeal path, external models

Success criteria check:

  • run_eval_gate() implemented — ready for WS3 candidate versions
  • ✅ Promoted case: parent superseded, world_model_improvements event, artifact_lifecycle_history written
  • ✅ Rejected case: candidate stays candidate, promotion_notes populated
  • ✅ Bootstrap seed deterministic via _deterministic_seed(candidate_id, parent_id)
  • ✅ Tradeoff-justification path implemented in _has_valid_tradeoff()
  • ✅ Governance doc 192 lines ≥ 40 line requirement
Import verified: from scidex_tools.model_eval_gate import run_eval_gate, run_eval_gate_quick — OK

Note: Cannot execute end-to-end gate (no WS3 candidate artifacts exist yet). Gate fails gracefully when benchmark_id or eval.py not found.

Related

  • Parent quest: quest_model_artifacts_spec.md
  • Depends on: WS1 (promotion_state column), WS2 (eval.py in subtree),
WS3 (candidates to evaluate).
  • Informs: WS5 (feedback loop triggers on promotion events this task
emits).
  • Adjacent: quest_senate_spec.md, quest_quality_standards_spec.md,
quest_evolutionary_arenas_spec.md (arena Elo is analogous; this is
the per-model-version gate, not the arena ranking).
  • Cites: project_economics_v2_credit_backprop_2026-04-10
model_version_promoted events drive dividend payouts.

Work Log

2026-04-16 15:30 PT — Slot minimax:70

  • Read AGENTS.md, quest specs WS1-WS3, detect_improvements.py, db_writes.py, event_bus.py
  • Verified: model_versions table exists (from WS1), benchmarks table exists, world_model_improvements table exists
  • Verified: zero model_versions rows in DB (no candidates yet — WS3 is first source)
  • Verified: no existing eval gate code, no model_eval_gate.py, no artifacts/models/ subtree
  • Implemented scidex_tools/model_eval_gate.py:
- run_eval_gate(candidate_artifact_id) with @log_tool_call decorator
- _bootstrap_delta_cis() for 95% CI via 1000-resample bootstrap with deterministic seed
- _run_eval_in_sandbox() using bwrap with minimal sandbox (ro /usr, bind model dir, tmpfs /tmp)
- _load_candidate_and_parent(): loads candidate/parent model_versions rows
- _resolve_benchmark(): benchmark_id from candidate or parent fallback
- _has_valid_tradeoff(): parses changelog/metadata for allowlisted tradeoff keys
- _ensure_universal_artifact(): registers model in universal_artifacts (FK integrity for artifact_lifecycle_history)
- Gate logic: delta≥0+CI excludes 0 → promote; delta<0+valid tradeoff → promote_with_tradeoff; else reject
- On promote: flips is_latest/lifecycle_state on candidate and parent, emits world_model_improvements row, writes artifact_lifecycle_history
- On reject: stays candidate, records metric delta + CI + reason in promotion_notes JSON
- run_eval_gate_quick() for CI pre-check (100 resamples, no DB write)
  • Implemented governance_artifacts/model_promotion_policy.md (192 lines, 8 sections):
- Eval gate steps, promotion logic, tradeoff allowlist, appeal path, external models, governance event logging
  • Fixed: typo promote_with_tradeoutpromote_with_tradeoff
  • Verified: import OK, gate runs (fails gracefully due to missing benchmark_id on existing artifacts)
  • Bootstrap CI: deterministic seed confirmed, CI bounds bracket mean ✓

Tasks using this spec (1)
[Senate] Model artifacts WS4: eval gate before promotion
Senate done P93
File: task-id-pending_model_artifacts_ws4_eval_gate_spec.md
Modified: 2026-04-25 22:00
Size: 9.5 KB