Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ experiment-priority-check:
calibration-performance-check:
openamp-foundry calibration-performance-check --entry-json '{"summary_id":"CPS-001","pipeline_version":"0.9.9","evaluation_date":"2026-07-10","batch_ids_evaluated":["BATCH-001","BATCH-002"],"total_candidates_evaluated":20,"true_positive_count":10,"false_positive_count":2,"true_negative_count":6,"false_negative_count":2,"brier_score":0.12,"calibration_notes":"Calibration looks reasonable.","reviewer":"test@example.com","dry_lab_only":false}' --format text

.PHONY: prediction-drift-check
.PHONY: calibration-readiness-check prediction-drift-check
prediction-drift-check:
openamp-foundry prediction-drift-check --entry-json '{"monitor_id":"DRM-TEST","pipeline_version":"v1.0","reference_batch_id":"A","evaluation_batch_id":"B","reference_mean_score":0.6,"reference_std_score":0.1,"evaluation_mean_score":0.65,"evaluation_std_score":0.12,"mean_shift_magnitude":0.05,"population_size_reference":50,"population_size_evaluation":50,"drift_flag":false,"drift_notes":"","reviewer":"test"}'

Expand All @@ -975,6 +975,10 @@ dataset-release-check:
--entry-json '{"release_id":"DSR-001","dataset_name":"OpenAMP Wave 0.5 Candidates","dataset_version":"1.0.0","release_date":"2026-07-09","license_identifier":"CC-BY-4.0","data_sources":["UniProt AMP database","APD3","in-house pipeline"],"contains_sequences":true,"contains_activity_data":false,"dual_use_assessed":true,"usage_policy_url":"https://openamp.example.org/data-policy","contact_email":"data@openamp.example.org","release_approved":true,"dry_lab_only":true}' \
--format text

.PHONY: calibration-readiness-check
calibration-readiness-check:
openamp-foundry calibration-readiness-check --entry-json '{"gate_id":"CRG-TEST","pipeline_version":"v1.0","aggregator_id":"CBA-001","mean_brier_score":0.15,"trend":"stable","total_batches_evaluated":3,"gate_passed":true,"failure_reasons":[],"gate_notes":"","reviewer":"test"}'

.PHONY: calibration-improvement-check
calibration-improvement-check:
openamp-foundry calibration-improvement-check --entry-json '{"improvement_id":"CIR-TEST","pipeline_version_before":"v1.0","pipeline_version_after":"v1.1","trigger_ids":["CPS-001"],"action_taken":"threshold adjustment","action_category":"threshold_adjustment","brier_score_before":0.3,"brier_score_after":0.2,"improvement_confirmed":true,"reviewer":"test"}'
Expand Down
50 changes: 50 additions & 0 deletions docs/evidence/CALIBRATION_READINESS_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Calibration Readiness Gate Guide

Schema: `CalibrationReadinessEntry` — binary pass/fail gate before releasing the next candidate batch.

## Purpose

Before releasing a new candidate batch for wet-lab testing, the calibration readiness gate
checks that recent performance is good enough. It consumes a cross-batch aggregator record
(CBA-) and applies threshold checks to produce a pass/fail verdict. Failing batches are held
until recalibration is complete.

## Fields

| Field | Type | Description |
|-------|------|-------------|
| `gate_id` | str | Unique ID, must start with "CRG-" |
| `pipeline_version` | str | Pipeline version being evaluated |
| `aggregator_id` | str | CBA- record being used as input (must start with "CBA-") |
| `mean_brier_score` | float | Mean Brier score from aggregator (0.0–1.0) |
| `trend` | str | Trend from aggregator: improving, stable, or degrading |
| `total_batches_evaluated` | int | Number of batches in the aggregation (>= 2) |
| `gate_passed` | bool | True if all threshold checks pass |
| `failure_reasons` | List[str] | Empty if passed; non-empty if failed |
| `gate_notes` | str | Max 400 chars |
| `reviewer` | str | Who signed off on this gate decision |
| `dry_lab_only` | bool | True (computational gate) |

## Validation Rules

1. `gate_id` must start with "CRG-"
2. `aggregator_id` must start with "CBA-"
3. `mean_brier_score` must be in [0.0, 1.0]
4. `trend` must be one of: improving, stable, degrading
5. `total_batches_evaluated` must be >= 2
6. `gate_notes` must not exceed 400 chars
7. `gate_passed` must be consistent: True iff failure_reasons is empty
8. If gate_passed=False, failure_reasons must be non-empty

## Warning Conditions

- trend == "degrading" and gate_passed=True (passed despite degrading trend)
- mean_brier_score >= WARN_BRIER_THRESHOLD (0.2) and gate_passed=True (marginal pass)

## Constants

- `GATE_NOTES_MAX_LENGTH = 400`
- `VALID_TRENDS = {"improving", "stable", "degrading"}`
- `PASS_BRIER_THRESHOLD = 0.25` (Brier > this fails the gate)
- `WARN_BRIER_THRESHOLD = 0.2` (Brier >= this but < 0.25 triggers warning)
- `MIN_BATCHES = 2`
3 changes: 2 additions & 1 deletion docs/evidence/METRICS_CURRENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak
> **Purpose:** One authoritative table of current pipeline metrics. If any doc disagrees
> with this file, this file wins. Updated whenever benchmark/benchmark config changes.
>
> **Last updated:** 2026-07-10 (Phase O O4 — cross-batch performance aggregator — v0.10.2)
> **Last updated:** 2026-07-10 (Phase O O5 — calibration readiness gate — v0.10.3)
> **New in v0.10.3 (Phase O O5):** CalibrationReadinessEntry schema (11 fields, 8 validation rules, 2 warnings). Added validate_calibration_readiness() and validate_calibration_readiness_dict(). Added 68 tests. CLI: openamp-foundry calibration-readiness-check. Phase O (Calibration Quality Assurance) COMPLETE.
> **New in v0.10.2 (Phase O O4):** CrossBatchAggregatorEntry schema (12 fields, 9 validation rules, 3 warnings). Added validate_cross_batch_aggregator() and validate_cross_batch_aggregator_dict(). Added 68 tests. CLI: openamp-foundry cross-batch-aggregator-check.
> **New in v0.10.0 (Phase O O2):** PredictionDriftEntry schema (15 fields: monitor_id, pipeline_version, reference_batch_id, evaluation_batch_id, reference_mean_score, reference_std_score, evaluation_mean_score, evaluation_std_score, mean_shift_magnitude, population_size_reference, population_size_evaluation, drift_flag, drift_notes, reviewer, dry_lab_only). 8 validation rules (DRM- prefix, batch id must differ, scores 0.0-1.0, std >= 0.0, mean shift tolerance ±0.001, population >=1, notes <=400 chars, drift_flag=True requires non-empty notes). 4 warning conditions (unreported large shift >=0.1, small population <10, variance explosion >2x). 5 constants (DRIFT_NOTES_MAX_LENGTH=400, MEAN_SHIFT_TOLERANCE=0.001, SIGNIFICANT_DRIFT_THRESHOLD=0.1, MIN_POPULATION_FOR_RELIABLE_DRIFT=10, VARIANCE_EXPLOSION_RATIO=2.0). PredictionDriftEntry (15 fields), PredictionDriftResult (8 fields: monitor_id, pipeline_version, mean_shift_magnitude, drift_flag, passed, errors, warnings, dry_lab_only). validate_prediction_drift() (12+ error checks, 4 warning conditions). validate_prediction_drift_dict() (14 required fields guard). CLI (openamp-foundry prediction-drift-check) with --entry-json, --format text|json. make prediction-drift-check target. 68 tests. Phase O O2: prediction drift monitor is the earliest warning signal for pipeline score distribution shifts before wet-lab validation data arrives.
> **New in v0.9.9:** Calibration performance summary schema (O1 — Loop 138) — docs/evidence/CALIBRATION_PERFORMANCE_GUIDE.md with purpose, schema fields table (13 fields: summary_id, pipeline_version, evaluation_date, batch_ids_evaluated, total_candidates_evaluated, true_positive_count, false_positive_count, true_negative_count, false_negative_count, brier_score, calibration_notes, reviewer, dry_lab_only), 4 derived metrics (precision, recall, specificity, F1), 9 validation rules (CPS- prefix, >=1 batch, >=1 candidates, all counts >=0, total == TP+FP+TN+FN, Brier 0.0-1.0, notes<=400 chars, non-empty reviewer, dry_lab_only must be False), 4 warning conditions (high FP rate >0.5, low recall <0.3, poor Brier >0.25, small sample <10), honest-use boundary. CalibrationPerformanceEntry dataclass (13 fields, dry_lab_only=False), CalibrationPerformanceResult dataclass (8 fields: summary_id, pipeline_version, total_candidates_evaluated, brier_score, passed, errors, warnings, dry_lab_only=False), MAX_CALIBRATION_NOTES_LENGTH=400, MIN_CANDIDATES_FOR_RELIABLE_ESTIMATE=10, HIGH_FP_RATE_THRESHOLD=0.5, LOW_RECALL_THRESHOLD=0.3, POOR_BRIER_SCORE_THRESHOLD=0.25. validate_calibration_performance() (9+ error checks: CPS- prefix, >=1 batch, >=1 candidates, >=0 counts, confusion sum match, Brier bounds, notes<=400, non-empty reviewer, dry_lab_only must be False; 4 warnings: high FP, low recall, poor Brier, small sample). validate_calibration_performance_dict() (12 required fields guard). CLI (openamp-foundry calibration-performance-check) with --entry-json, --format text|json. make calibration-performance-check target. 68 tests. Phase O O1: calibration performance summaries now measure how well predictions match experimental outcomes. Phase O (Calibration Quality Assurance) started.
Expand Down
2 changes: 1 addition & 1 deletion docs/research/NEXT_100_PR_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Track how well predictions match experimental outcomes over time. Detect drift,
| O2 | Add prediction drift monitor schema (complete — Loop 139). | Detects when pipeline predictions are systematically shifting. | B/D |
| O3 | Add calibration improvement record schema. | Documents what was changed to improve calibration and by how much. | B/D |
| O4 | Add cross-batch performance aggregator schema (complete — Loop 140). | Aggregates results across batches for trend analysis. | B/D |
| O5 | Add calibration readiness gate schema. | Validates calibration quality is sufficient before releasing the next batch. | B/D |
| [x] O5 | Add calibration readiness gate schema. | Validates calibration quality is sufficient before releasing the next batch. | B/D |

## Prioritization rule

Expand Down
5 changes: 5 additions & 0 deletions docs/research/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Roadmap

### v0.10.3 - Phase O O5: Calibration Readiness Gate (COMPLETES PHASE O)
- CalibrationReadinessEntry: binary gate consuming CBA- aggregators
- gate_passed consistent with failure_reasons; degrading-trend and marginal-Brier warnings
- Phase O (O1-O5 Calibration QA) fully complete

### v0.10.2 - Phase O O4: Cross-Batch Performance Aggregator
- CrossBatchAggregatorEntry aggregates CalibrationPerformanceEntry results across N>=2 batches
- trend field (improving/stable/degrading) feeds calibration readiness gate
Expand Down
28 changes: 28 additions & 0 deletions src/openamp_foundry/cli/commands/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3490,6 +3490,34 @@ def _run_calibration_improvement_check(args) -> int:
return 0 if result.passed else 1


def _run_calibration_readiness_check(args) -> int:
import json
from openamp_foundry.evidence.calibration_readiness_gate import (
validate_calibration_readiness_dict,
)
if args.entry_json:
try:
data = json.loads(args.entry_json)
except json.JSONDecodeError as exc:
print(f"Error: invalid JSON: {exc}", file=__import__("sys").stderr)
return 1
else:
data = json.load(__import__("sys").stdin)
result = validate_calibration_readiness_dict(data)
if args.format == "json":
import dataclasses
print(json.dumps(dataclasses.asdict(result), indent=2))
else:
status = "PASS" if result.passed else "FAIL"
gate = "GATE-PASS" if result.gate_passed else "GATE-FAIL"
print(f"[{status}] Calibration Readiness Gate: {result.gate_id} [{gate}]")
for err in result.errors:
print(f" ERROR: {err}")
for warn in result.warnings:
print(f" WARN: {warn}")
return 0 if result.passed else 1


def _run_cross_batch_aggregator_check(args) -> int:
import json
from openamp_foundry.evidence.cross_batch_aggregator import (
Expand Down
12 changes: 12 additions & 0 deletions src/openamp_foundry/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
_run_experiment_priority_check, _run_calibration_performance_check, _run_prediction_drift_check,
_run_calibration_improvement_check,
_run_cross_batch_aggregator_check,
_run_calibration_readiness_check,
)
from openamp_foundry.cli.commands.gates import _run_gate_check, _run_release_gate_check
from openamp_foundry.cli.commands.reports import _run_contribution_check
Expand Down Expand Up @@ -2347,6 +2348,14 @@ def build_parser() -> argparse.ArgumentParser:
p_cba.add_argument("--format", choices=["text", "json"], default="text")
p_cba.set_defaults(func=_run_cross_batch_aggregator_check)

p_crg = sub.add_parser(
"calibration-readiness-check",
help="Validate calibration readiness gate entry",
)
p_crg.add_argument("--entry-json", default=None)
p_crg.add_argument("--format", choices=["text", "json"], default="text")
p_crg.set_defaults(func=_run_calibration_readiness_check)

# ── Selection rationale check (Phase K K1) ───────────────────────
src2 = sub.add_parser(
"selection-rationale-check",
Expand Down Expand Up @@ -2719,6 +2728,9 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "cross-batch-aggregator-check":
return _run_cross_batch_aggregator_check(args)

if args.command == "calibration-readiness-check":
return _run_calibration_readiness_check(args)

parser.error("unknown command")
return 2

Expand Down
154 changes: 154 additions & 0 deletions src/openamp_foundry/evidence/calibration_readiness_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Set

GATE_NOTES_MAX_LENGTH: int = 400
VALID_TRENDS: Set[str] = {"improving", "stable", "degrading"}
PASS_BRIER_THRESHOLD: float = 0.25
WARN_BRIER_THRESHOLD: float = 0.2
MIN_BATCHES: int = 2


@dataclass
class CalibrationReadinessEntry:
gate_id: str
pipeline_version: str
aggregator_id: str
mean_brier_score: float
trend: str
total_batches_evaluated: int
gate_passed: bool
failure_reasons: List[str]
gate_notes: str
reviewer: str
dry_lab_only: bool = True


@dataclass
class CalibrationReadinessResult:
gate_id: str
pipeline_version: str
mean_brier_score: float
trend: str
gate_passed: bool
passed: bool
errors: List[str] = field(default_factory=list)
warnings: List[str] = field(default_factory=list)
dry_lab_only: bool = True


def validate_calibration_readiness(
entry: CalibrationReadinessEntry,
) -> CalibrationReadinessResult:
errors: List[str] = []
warnings: List[str] = []

if not entry.gate_id.startswith("CRG-"):
errors.append(
f"gate_id must start with 'CRG-', got '{entry.gate_id}'"
)

if not entry.aggregator_id.startswith("CBA-"):
errors.append(
f"aggregator_id must start with 'CBA-', got '{entry.aggregator_id}'"
)

if not (0.0 <= entry.mean_brier_score <= 1.0):
errors.append(
f"mean_brier_score must be in [0.0, 1.0], got {entry.mean_brier_score}"
)

if entry.trend not in VALID_TRENDS:
errors.append(
f"trend must be one of {sorted(VALID_TRENDS)}, got '{entry.trend}'"
)

if entry.total_batches_evaluated < MIN_BATCHES:
errors.append(
f"total_batches_evaluated must be >= {MIN_BATCHES}, "
f"got {entry.total_batches_evaluated}"
)

if len(entry.gate_notes) > GATE_NOTES_MAX_LENGTH:
errors.append(
f"gate_notes exceeds {GATE_NOTES_MAX_LENGTH} chars "
f"(got {len(entry.gate_notes)})"
)

expected_passed = len(entry.failure_reasons) == 0
if entry.gate_passed != expected_passed:
errors.append(
f"gate_passed ({entry.gate_passed}) is inconsistent with "
f"failure_reasons (length={len(entry.failure_reasons)}): "
f"gate_passed must be True iff failure_reasons is empty"
)

if not entry.gate_passed and not entry.failure_reasons:
errors.append(
"failure_reasons must be non-empty when gate_passed=False"
)

# Warnings
if entry.trend == "degrading" and entry.gate_passed:
warnings.append(
"gate_passed=True despite trend=degrading -- monitor closely"
)

if (
WARN_BRIER_THRESHOLD <= entry.mean_brier_score < PASS_BRIER_THRESHOLD
and entry.gate_passed
):
warnings.append(
f"mean_brier_score ({entry.mean_brier_score}) >= {WARN_BRIER_THRESHOLD} "
"-- marginal pass, consider recalibration"
)

return CalibrationReadinessResult(
gate_id=entry.gate_id,
pipeline_version=entry.pipeline_version,
mean_brier_score=entry.mean_brier_score,
trend=entry.trend,
gate_passed=entry.gate_passed,
passed=len(errors) == 0,
errors=errors,
warnings=warnings,
dry_lab_only=entry.dry_lab_only,
)


def validate_calibration_readiness_dict(data: dict) -> CalibrationReadinessResult:
required = [
"gate_id", "pipeline_version", "aggregator_id", "mean_brier_score",
"trend", "total_batches_evaluated", "gate_passed", "failure_reasons",
"gate_notes", "reviewer",
]
errors: List[str] = []
for key in required:
if key not in data:
errors.append(f"Missing required field: {key}")
if errors:
return CalibrationReadinessResult(
gate_id=data.get("gate_id", ""),
pipeline_version=data.get("pipeline_version", ""),
mean_brier_score=float(data.get("mean_brier_score", 0.0)),
trend=data.get("trend", ""),
gate_passed=bool(data.get("gate_passed", False)),
passed=False,
errors=errors,
warnings=[],
)
entry = CalibrationReadinessEntry(
gate_id=str(data["gate_id"]),
pipeline_version=str(data["pipeline_version"]),
aggregator_id=str(data["aggregator_id"]),
mean_brier_score=float(data["mean_brier_score"]),
trend=str(data["trend"]),
total_batches_evaluated=int(data["total_batches_evaluated"]),
gate_passed=bool(data["gate_passed"]),
failure_reasons=list(data["failure_reasons"]),
gate_notes=str(data["gate_notes"]),
reviewer=str(data["reviewer"]),
dry_lab_only=bool(data.get("dry_lab_only", True)),
)
return validate_calibration_readiness(entry)
Loading
Loading