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
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,18 @@ ip-report:
--novelty-csv outputs/novelty_audit_full.csv \
--out outputs/ip_report.md

benchmark-card: validate-scoring
@echo "Benchmark card is a static document at docs/BENCHMARK_CARD.md"
@echo "Run 'make validate-scoring' to update outputs/validate_scoring_report.json"
benchmark-card:
.venv/bin/openamp-foundry benchmark-card \
--benchmark-id bench-auroc-001 \
--benchmark-name "AMP vs Decoy AUROC" \
--metric AUROC \
--metric-value 0.82 \
--baseline-name "random" \
--baseline-value 0.50 \
--dataset APD3 \
--dataset-size 500 \
--validate
@echo "Benchmark card complete."

pilot-confident:
@if [ -z "$(KEEP)" ]; then \
Expand Down
5 changes: 3 additions & 2 deletions 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-09 (Phase I I2 — candidate manifest schema — v0.6.0)
> **Last updated:** 2026-07-09 (Phase I I3 — benchmark card schema — v0.6.1)
> **New in v0.6.1:** Benchmark card schema (I3 — Loop 101) — `schemas/benchmark_card.schema.json` (Draft 2020-12, 15 required fields: benchmark_id, benchmark_name, version, date, metric, metric_value, baseline_name, baseline_value, delta, beats_baseline, dataset, dataset_size, scope, caveats, dry_lab_only). `$schema`, `$id`, `title`, `additionalProperties: false`. `src/openamp_foundry/benchmarks/` module with `BenchmarkCard` dataclass (15 fields), `make_benchmark_card()` (auto-computes delta, beats_baseline), `validate_benchmark_card()` (10 checks: non-empty benchmark_id, non-empty benchmark_name, non-empty metric, non-empty dataset, non-empty baseline_name, dataset_size >= 1, delta matches metric_value - baseline_value within 1e-9, beats_baseline matches delta > 0, dry_lab_only must be True), `benchmark_card_summary()` (total, beats_baseline_count, fails_baseline_count, dry_lab_only). CLI (`openamp-foundry benchmark-card`) with `--benchmark-id`, `--benchmark-name`, `--metric`, `--metric-value`, `--baseline-name`, `--baseline-value`, `--dataset`, `--dataset-size`, `--validate`, `--format text|json`. `make benchmark-card` target. 19 tests. **3329 total.** A benchmark card is the standard format for describing external benchmark results — what was benchmarked, what the baseline was, what the result was, and what claims are supported.
> **New in v0.6.0:** Candidate manifest schema (I2 — Loop 100) — `schemas/candidate_manifest.schema.json` (Draft 2020-12, 14 required fields: candidate_id, sequence, evidence_level, scopes, scores, uncertainty, source_modules, calibration_set, safety_flags, provenance_run_id, dry_lab_only, version, created_at, notes). `$schema`, `$id`, `title`, `description`, `additionalProperties: false`. `src/openamp_foundry/manifests/` module with `CandidateManifest` dataclass (14 fields), `make_candidate_manifest()`, `validate_candidate_manifest()` (8 checks: non-empty candidate_id, non-empty sequence, evidence_level 1-6, non-empty scopes, uncertainty 0.0-1.0, non-empty source_modules, dry_lab_only must be True, version MAJOR.MINOR.PATCH), `manifest_to_dict()`, `manifest_summary()` (total, by_evidence_level, with_safety_flags, dry_lab_only). CLI (`openamp-foundry candidate-manifest`) with `--candidate-id`, `--sequence`, `--evidence-level`, `--scopes`, `--scores-json`, `--uncertainty`, `--source-modules`, `--validate`, `--format text|json`. `make candidate-manifest` target. 19 tests. **3310 total.** A candidate manifest is the core interoperable artifact — it describes a dry-lab candidate (sequence, scores, evidence level, scopes, safety flags, provenance) in a machine-readable format that external tools can consume without the full OpenAMP stack.
> **New in v0.5.99:** Artifact versioning policy (I1 — starts Phase I) — `docs/engineering/ARTIFACT_VERSIONING_POLICY.md` with structured versioning policy covering scope, SemVer format, breaking/non-breaking change definitions, deprecation timeline (at least one minor version warning), schema `$id` policy, changelog requirement, and three stability tiers (stable/experimental/internal). `src/openamp_foundry/versioning/` module with `ArtifactVersionInfo` dataclass (7 fields), `STABILITY_TIERS` dict, `VERSIONED_ARTIFACTS` list (6 entries covering candidate, lab_result, run_manifest, external_review_packet, safety_release_decision, simulation_result at v1.0.0), `get_artifact_version()`, `list_versioned_artifacts()`, `validate_version_format()`, `artifact_version_summary()`. CLI (`openamp-foundry artifact-version`) with `--list`, `--show`, `--tier`, `--format text|json`. `make artifact-version` target. 19 tests. **3291 total.** Starts Phase I (interoperability) — external users now have stability guarantees for schemas, evidence certificates, and candidate manifests.
> **New in v0.5.97:** Simulation-scope coverage checker (H9) — `ScopeCoverageResult` dataclass (8 fields: module_id, requested_scopes, module_scopes, covered, uncovered, coverage_fraction, is_fully_covered, dry_lab_only). `check_scope_coverage()` looks up module in registry, computes covered (intersection) and uncovered (requested scopes not in module scopes), coverage_fraction = len(covered)/len(requested_scopes) if requested else 1.0. `check_result_scope()` uses conservative intersection of registry scopes and result.scope as effective module_scopes. `scope_coverage_report()` returns full dict. CLI (`openamp-foundry simulation-scope-check`) with `--module-id`, `--requested-scopes`, `--format text|json`. `make simulation-scope-check` target. 17 tests. **3255 total.** A simulation module may cover only some biological scopes. If a candidate is evaluated for a scope the module doesn't cover, that result must be flagged as out-of-scope rather than silently trusted.
Expand Down Expand Up @@ -90,7 +91,7 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak
> **New in v0.5.31:** Added dipeptide-order features for sequence-order awareness. `dipeptide_order_score` achieves AUROC 0.7861 on AMP-vs-scrambled discrimination — the strongest order-dependent feature in the pipeline. Only 7/31 features survive scrambling (amphipathicity/helix-wheel + dipeptide). All composition features are purely position-independent (exactly 0.5000 AUROC on scrambled test).
> **New in v0.5.30:** Easy baseline benchmark added — charge density alone (AUROC 0.8166) outperforms the full pipeline ensemble (0.7792) on AMP-vs-Swiss-Prot-decoy discrimination. Honest finding documented: expected because pipeline optimizes for safety, not raw discrimination.
> **New in v0.5.29:** Expanded benchmark to 500 AMPs + 500 composition-matched decoys (n=1000). AUROC 0.7792 (CI₉₅: 0.7505–0.8065) confirms signal generalizes. Cluster-aware CI: 0.746–0.8102. Representative AUROC: 0.778. Standard benchmark (n=191) retained for backward comparison.
> **Pipeline version:** v0.6.0 (I2 — candidate manifest schema)
> **Pipeline version:** v0.6.1 (I3 — benchmark card schema)
> **Branch:** main

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

## v0.6.1 — Loop 101: Phase I I3 — Benchmark Card Schema ✓ (2026-07-09)

`schemas/benchmark_card.schema.json` (Draft 2020-12, 15 required fields:
benchmark_id, benchmark_name, version, date, metric, metric_value, baseline_name,
baseline_value, delta, beats_baseline, dataset, dataset_size, scope, caveats,
dry_lab_only). `$schema`, `$id`, `title`, `additionalProperties: false`.

`src/openamp_foundry/benchmarks/` module with `BenchmarkCard` dataclass
(15 fields), `make_benchmark_card()` (auto-computes delta, beats_baseline),
`validate_benchmark_card()` (10 checks: non-empty benchmark_id, non-empty
benchmark_name, non-empty metric, non-empty dataset, non-empty baseline_name,
dataset_size >= 1, delta matches metric_value - baseline_value within 1e-9,
beats_baseline matches delta > 0, dry_lab_only must be True),
`benchmark_card_summary()` (total, beats_baseline_count, fails_baseline_count,
dry_lab_only).

CLI (`openamp-foundry benchmark-card`) with `--benchmark-id`, `--benchmark-name`,
`--metric`, `--metric-value`, `--baseline-name`, `--baseline-value`, `--dataset`,
`--dataset-size`, `--validate`, `--format text|json`.
`make benchmark-card` target.

19 tests. **3329 total.** A benchmark card is the standard format for describing
external benchmark results — what was benchmarked, what the baseline was, what
the result was, and what claims are supported. This is Loop 101 — the 101st PR
in the NEXT_100_PR_MAP series.

Changes:
- `schemas/benchmark_card.schema.json` (I3) — Draft 2020-12 schema with
15 required fields, `$id`, `title`, `additionalProperties: false`.
- `src/openamp_foundry/benchmarks/__init__.py` (I3) — Package init, exports
`BenchmarkCard`, `make_benchmark_card`, `validate_benchmark_card`,
`benchmark_card_summary`.
- `src/openamp_foundry/benchmarks/benchmark_card.py` (I3) — Core module
with `BenchmarkCard` dataclass (15 fields), `make_benchmark_card()`,
`validate_benchmark_card()`, `benchmark_card_summary()`.
- `src/openamp_foundry/cli/main.py` (I3) — Registered `benchmark-card`
subcommand with `--benchmark-id`, `--benchmark-name`, `--metric`,
`--metric-value`, `--baseline-name`, `--baseline-value`, `--dataset`,
`--dataset-size`, `--validate`, `--format` flags. Added import and dispatch.
- `src/openamp_foundry/cli/commands/reports.py` (I3) — Added
`_run_benchmark_card()` CLI handler with JSON and text output,
card creation, optional validation, and error handling.
- `Makefile` (I3) — Updated `benchmark-card` target with demo invocation
using bench-auroc-001. Added to `.PHONY`.
- `tests/benchmarks/__init__.py` (I3) — Empty package init.
- `tests/benchmarks/test_benchmark_card.py` (I3) — 19 tests covering:
make_benchmark_card returns BenchmarkCard, delta computed correctly,
beats_baseline True/False, dry_lab_only always True, valid card passes
validation, catches empty benchmark_id, empty benchmark_name, empty metric,
empty dataset, empty baseline_name, dataset_size < 1, wrong delta,
wrong beats_baseline, dry_lab_only=False, summary total, summary
beats_baseline_count, dry_lab_only, schema file exists.
- `docs/evidence/METRICS_CURRENT.md` — v0.6.1 I3 changelog. Test count: 3329.
- `tests/test_test_count_regression.py` — baseline updated to 3329.

Honest boundaries:
- Benchmark cards describe computational benchmark results only — they do not
measure biological activity, safety, or clinical value.
- Delta and beats_baseline are computed from metric_value and baseline_value,
not independently measured.
- The schema is versioned (currently 1.0.0) and should be updated when fields
are added or changed.
- Validation checks structural correctness and internal consistency only — it
does not verify that the benchmark was correctly designed or that the results
generalize.
- `dry_lab_only: true` is a const field — all benchmark cards are inherently
dry-lab and must never be presented as validated biological findings.

## v0.6.0 — Loop 100: Phase I I2 — Candidate Manifest Schema ✓ (2026-07-09)

`schemas/candidate_manifest.schema.json` (Draft 2020-12, 14 required fields:
Expand Down
99 changes: 99 additions & 0 deletions schemas/benchmark_card.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openamp-foundry.org/schemas/benchmark_card.schema.json",
"title": "BenchmarkCard",
"description": "Schema for a benchmark card — a machine-readable artifact describing what was benchmarked, what the baseline was, what the result was, and what claims are supported. Dry-lab only — does not measure biological activity.",
"type": "object",
"properties": {
"benchmark_id": {
"description": "Unique identifier for the benchmark",
"type": "string",
"minLength": 1
},
"benchmark_name": {
"description": "Human-readable benchmark name",
"type": "string",
"minLength": 1
},
"version": {
"description": "Schema version (MAJOR.MINOR.PATCH)",
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"date": {
"description": "Date the benchmark was run (YYYY-MM-DD)",
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"metric": {
"description": "Metric name (e.g. AUROC, AUPRC, recall@k)",
"type": "string",
"minLength": 1
},
"metric_value": {
"description": "Observed metric value",
"type": "number"
},
"baseline_name": {
"description": "Name of the comparison baseline",
"type": "string",
"minLength": 1
},
"baseline_value": {
"description": "Baseline metric value",
"type": "number"
},
"delta": {
"description": "metric_value - baseline_value",
"type": "number"
},
"beats_baseline": {
"description": "True if metric_value > baseline_value",
"type": "boolean"
},
"dataset": {
"description": "Name or identifier of the dataset used",
"type": "string",
"minLength": 1
},
"dataset_size": {
"description": "Number of samples in the dataset",
"type": "integer",
"minimum": 1
},
"scope": {
"description": "List of biological scopes this benchmark applies to",
"type": "array",
"items": { "type": "string" },
"minItems": 1
},
"caveats": {
"description": "List of caveats and limitations",
"type": "array",
"items": { "type": "string" }
},
"dry_lab_only": {
"description": "Must be true — benchmark cards are computational artifacts, not biological proof",
"type": "boolean",
"const": true
}
},
"required": [
"benchmark_id",
"benchmark_name",
"version",
"date",
"metric",
"metric_value",
"baseline_name",
"baseline_value",
"delta",
"beats_baseline",
"dataset",
"dataset_size",
"scope",
"caveats",
"dry_lab_only"
],
"additionalProperties": false
}
13 changes: 13 additions & 0 deletions src/openamp_foundry/benchmarks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from openamp_foundry.benchmarks.benchmark_card import (
BenchmarkCard,
make_benchmark_card,
validate_benchmark_card,
benchmark_card_summary,
)

__all__ = [
"BenchmarkCard",
"make_benchmark_card",
"validate_benchmark_card",
"benchmark_card_summary",
]
101 changes: 101 additions & 0 deletions src/openamp_foundry/benchmarks/benchmark_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from __future__ import annotations

import math
from dataclasses import dataclass, field
from datetime import datetime, timezone


@dataclass
class BenchmarkCard:
benchmark_id: str
benchmark_name: str
version: str
date: str
metric: str
metric_value: float
baseline_name: str
baseline_value: float
delta: float
beats_baseline: bool
dataset: str
dataset_size: int
scope: list[str]
caveats: list[str]
dry_lab_only: bool = True


def make_benchmark_card(
benchmark_id: str,
benchmark_name: str,
metric: str,
metric_value: float,
baseline_name: str,
baseline_value: float,
dataset: str,
dataset_size: int,
*,
version: str = "1.0.0",
date: str = "",
scope: list[str] | None = None,
caveats: list[str] | None = None,
) -> BenchmarkCard:
delta = metric_value - baseline_value
beats_baseline = delta > 0
return BenchmarkCard(
benchmark_id=benchmark_id,
benchmark_name=benchmark_name,
version=version,
date=date or datetime.now(timezone.utc).strftime("%Y-%m-%d"),
metric=metric,
metric_value=metric_value,
baseline_name=baseline_name,
baseline_value=baseline_value,
delta=delta,
beats_baseline=beats_baseline,
dataset=dataset,
dataset_size=dataset_size,
scope=scope or [],
caveats=caveats or [],
)


def validate_benchmark_card(card: BenchmarkCard) -> list[str]:
errors: list[str] = []
if not card.benchmark_id:
errors.append("benchmark_id must be non-empty")
if not card.benchmark_name:
errors.append("benchmark_name must be non-empty")
if not card.metric:
errors.append("metric must be non-empty")
if not card.dataset:
errors.append("dataset must be non-empty")
if not card.baseline_name:
errors.append("baseline_name must be non-empty")
if card.dataset_size < 1:
errors.append(f"dataset_size must be >= 1, got {card.dataset_size}")
expected_delta = card.metric_value - card.baseline_value
if abs(card.delta - expected_delta) > 1e-9:
errors.append(
f"delta {card.delta} does not match metric_value - baseline_value "
f"({expected_delta})"
)
expected_beats = expected_delta > 0
if card.beats_baseline != expected_beats:
errors.append(
f"beats_baseline={card.beats_baseline} does not match delta > 0 "
f"({expected_beats})"
)
if not card.dry_lab_only:
errors.append("dry_lab_only must be True")
return errors


def benchmark_card_summary(cards: list[BenchmarkCard]) -> dict:
beats_count = sum(1 for c in cards if c.beats_baseline)
fails_count = sum(1 for c in cards if not c.beats_baseline)
return {
"total": len(cards),
"beats_baseline_count": beats_count,
"fails_baseline_count": fails_count,
"dry_lab_only": True,
}
Loading
Loading