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
8 changes: 8 additions & 0 deletions docs/evidence/METRICS_CURRENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak

## Changelog

### v0.10.26 — Phase B B1: ProofLadderLevelCertificate schema
- Added `ProofLadderLevelCertificate` (PLC-) schema
- 14 fields: plc_id, pipeline_version, candidate_id, certificate_id, claimed_level, evidence_type, verifier_type, verification_date, supporting_artifact_ids, unsupported_claims, human_review_required, human_review_completed, dry_lab_only, notes
- 14 validation rules; 3 warnings; 63 tests
- BASELINE 6205→6268
- CLI: openamp-foundry proof-ladder-level-certificate-check
- Closes Phase B B1 — proof-ladder level claim is now a machine-checkable structured assertion

### v0.10.25 — Phase B B9: Certificate claim discipline CI gate
- Added `tests/test_certificate_claim_discipline.py` — 35-test CI gate
- Scans all certificate text fields against RISKY_PATTERNS (check_claims.py) and FORBIDDEN_CLAIM_PATTERNS (wave0_5_gate_checker.py)
Expand Down
65 changes: 65 additions & 0 deletions docs/evidence/PROOF_LADDER_LEVEL_CERTIFICATE_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# ProofLadderLevelCertificate (PLC-) Guide

## Purpose

`ProofLadderLevelCertificate` makes the proof-ladder level claim machine-checkable.
Instead of a bare string in a certificate, a PLC- record creates a structured assertion
tying a specific candidate to a specific level with explicit evidence references and
unsupported-claims documentation.

## Proof-Ladder Levels (ordered by evidence strength)

| Level | Description | Dry-lab only? |
|-------|-------------|---------------|
| `valid_input` | Passes format validation | Yes |
| `reproducible_dry_lab_features` | Features reproducibly computed | Yes |
| `baseline_triaged` | Beats cheap heuristic baseline | Yes |
| `leakage_aware_benchmark` | Benchmark is split-clean | Yes |
| `multi_signal_candidate_evidence` | Multiple scorers agree (**dry-lab max**) | Yes |
| `expert_reviewed_assay_proposal` | Domain expert reviewed | No — human review required |
| `initial_qualified_assay_result` | First wet-lab result | No — human review required |
| `safety_adjusted_follow_up_signal` | Follow-up with safety adjustment | No |
| `independent_replication` | Independently replicated | No |
| `reusable_discovery_loop` | Reusable pipeline | No |

## Schema Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `plc_id` | str | yes | Unique identifier, must start with `PLC-` |
| `pipeline_version` | str | yes | Pipeline version |
| `candidate_id` | str | yes | Candidate being assessed |
| `certificate_id` | str | yes | Source certificate ID |
| `claimed_level` | str | yes | Claimed proof-ladder level |
| `evidence_type` | str | yes | Evidence type (controlled vocabulary) |
| `verifier_type` | str | yes | Who verified (controlled vocabulary) |
| `verification_date` | str | yes | ISO date (YYYY-MM-DD) |
| `supporting_artifact_ids` | list | yes | Non-empty list of supporting artifact IDs |
| `unsupported_claims` | str | yes | Explicit anti-overclaim statement (≤500 chars) |
| `human_review_required` | bool | yes | Required for levels ≥ expert_reviewed_assay_proposal |
| `human_review_completed` | bool | yes | Tracks completion status |
| `dry_lab_only` | bool | yes | Caps level at multi_signal_candidate_evidence |
| `notes` | str | no | Additional context (≤300 chars) |

## Validation Rules

1. `plc_id` starts with `PLC-`
2. `pipeline_version` non-empty
3. `candidate_id` non-empty
4. `certificate_id` non-empty
5. `claimed_level` in valid proof-ladder vocabulary (10 levels)
6. `evidence_type` in controlled vocabulary (4 values)
7. `verifier_type` in controlled vocabulary (5 values)
8. `verification_date` is ISO date (YYYY-MM-DD)
9. `supporting_artifact_ids` non-empty
10. `unsupported_claims` non-empty and ≤500 chars
11. `dry_lab_only=True` caps `claimed_level` at `multi_signal_candidate_evidence`
12. Levels ≥ `expert_reviewed_assay_proposal` require `human_review_required=True`
13. `human_review_required=True` but `human_review_completed=False` → warning (pending)
14. `notes` ≤300 chars

## Integration

- Source: built from `build_certificate()` output
- Feeds into: external review packets, release gates, audit trails
- Supersedes: bare `proof_ladder_level` string with structured evidence record
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 @@ -41,7 +41,7 @@ Make candidate evidence packages impossible to confuse with biological proof.

| PR | Task | Why it matters | Review class |
|---:|---|---|---|
| B1 | Add certificate field for `proof_ladder_level`. | Claims become machine-checkable. | B |
| B1 | Add certificate field for `proof_ladder_level` (complete). | PLC- schema: 14 fields, 14 validation rules, dry_lab_only caps level at multi_signal_candidate_evidence, human_review_required enforced for levels ≥expert_reviewed_assay_proposal, unsupported_claims required as anti-overclaim guard; makes level claim machine-verifiable. | B |
| B2 | Add `unsupported_claims` field to certificates. | Prevents score-to-proof drift. | B |
| B3 | Add `baseline_caveat` field. | Forces cheap-explanation visibility. | B |
| B4 | Add `release_status` field. | Supports staged release. | C/D |
Expand Down
17 changes: 17 additions & 0 deletions src/openamp_foundry/cli/commands/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,23 @@ def _run_recalibration_rejection_summary_check(args):
print("OK: RecalibrationRejectionSummary is valid.")


def _run_proof_ladder_level_certificate_check(args):
"""CLI handler for proof-ladder-level-certificate-check."""
import json, sys
from openamp_foundry.evidence.proof_ladder_level_certificate import validate_dict
data = json.load(open(args.input))
issues = validate_dict(data)
errors = [i for i in issues if not i.startswith("WARNING:")]
warnings = [i for i in issues if i.startswith("WARNING:")]
for w in warnings:
print(w)
if errors:
for e in errors:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
print("OK: ProofLadderLevelCertificate is valid.")


def _run_domain_review_outcome_check(args):
from openamp_foundry.evidence.domain_review_outcome import (
validate_domain_review_outcome_dict,
Expand Down
11 changes: 11 additions & 0 deletions src/openamp_foundry/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
_run_recalibration_rejection_summary_check,
_run_synthetic_boundary_audit_record_check,
_run_expert_review_example_package_check,
_run_proof_ladder_level_certificate_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 @@ -2548,6 +2549,10 @@ def build_parser() -> argparse.ArgumentParser:
p_erp.add_argument("input", help="Path to JSON file")
p_erp.set_defaults(func=_run_expert_review_example_package_check)

p_plc = subparsers.add_parser("proof-ladder-level-certificate-check", help="Validate a ProofLadderLevelCertificate JSON")
p_plc.add_argument("input", help="Path to JSON file")
p_plc.set_defaults(func=_run_proof_ladder_level_certificate_check)

# ── Selection rationale check (Phase K K1) ───────────────────────
src2 = sub.add_parser(
"selection-rationale-check",
Expand Down Expand Up @@ -2996,6 +3001,12 @@ def main(argv: list[str] | None = None) -> int:
elif args.command == "expert-review-example-package-check":
_run_expert_review_example_package_check(args)

elif args.command == "recalibration-rejection-summary-check":
_run_recalibration_rejection_summary_check(args)

elif args.command == "proof-ladder-level-certificate-check":
_run_proof_ladder_level_certificate_check(args)

else:
parser.error("unknown command")
return 2
Expand Down
209 changes: 209 additions & 0 deletions src/openamp_foundry/evidence/proof_ladder_level_certificate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""Proof-ladder level certificate schema — Phase B B1.

Makes the proof-ladder level claim machine-checkable. Instead of a bare string
field in a certificate, a PLC- record asserts:
- which candidate is being assessed
- what level is claimed
- what the maximum permissible level is (capped by evidence type)
- which artifacts support the claim
- what is explicitly NOT supported at this level

Proof-ladder levels (ordered by evidence strength):
valid_input — passes format validation only
reproducible_dry_lab_features — features are reproducibly computed
baseline_triaged — beats or ties a cheap heuristic baseline
leakage_aware_benchmark — benchmark is split-clean
multi_signal_candidate_evidence — multiple independent scorers agree
expert_reviewed_assay_proposal — domain expert reviewed and proposed assay
initial_qualified_assay_result — first wet-lab result obtained (qualified lab)
safety_adjusted_follow_up_signal — follow-up with safety adjustment
independent_replication — independently replicated wet-lab result
reusable_discovery_loop — pipeline produces reusable discovery loop

Levels >= expert_reviewed_assay_proposal require human review before issuance.
"""

from __future__ import annotations

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

PLC_PREFIX = "PLC-"

VALID_PROOF_LADDER_LEVELS = (
"valid_input",
"reproducible_dry_lab_features",
"baseline_triaged",
"leakage_aware_benchmark",
"multi_signal_candidate_evidence",
"expert_reviewed_assay_proposal",
"initial_qualified_assay_result",
"safety_adjusted_follow_up_signal",
"independent_replication",
"reusable_discovery_loop",
)

# Max level a dry-lab-only pipeline can issue without human review
DRY_LAB_MAX_LEVEL = "multi_signal_candidate_evidence"
DRY_LAB_MAX_INDEX = VALID_PROOF_LADDER_LEVELS.index(DRY_LAB_MAX_LEVEL)

VALID_EVIDENCE_TYPES = frozenset({
"dry_lab_only",
"dry_lab_plus_expert_review",
"wet_lab_preliminary",
"wet_lab_replicated",
})

VALID_VERIFIER_TYPES = frozenset({
"automated_pipeline",
"pipeline_owner",
"external_domain_expert",
"safety_officer",
"independent_lab",
})

UNSUPPORTED_CLAIMS_MAX_LENGTH = 500
NOTES_MAX_LENGTH = 300
_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


@dataclass
class ProofLadderLevelCertificate:
plc_id: str
pipeline_version: str
candidate_id: str
certificate_id: str
claimed_level: str
evidence_type: str
verifier_type: str
verification_date: str
supporting_artifact_ids: List[str]
unsupported_claims: str
human_review_required: bool
human_review_completed: bool
dry_lab_only: bool
notes: str = ""


def validate(plc: ProofLadderLevelCertificate) -> List[str]:
"""Return list of error strings; empty list means valid."""
errors: List[str] = []

# Rule 1: ID prefix
if not plc.plc_id.startswith(PLC_PREFIX):
errors.append(
f"plc_id must start with '{PLC_PREFIX}', got: {plc.plc_id!r}"
)

# Rule 2: pipeline_version non-empty
if not plc.pipeline_version.strip():
errors.append("pipeline_version must not be empty")

# Rule 3: candidate_id non-empty
if not plc.candidate_id.strip():
errors.append("candidate_id must not be empty")

# Rule 4: certificate_id non-empty
if not plc.certificate_id.strip():
errors.append("certificate_id must not be empty")

# Rule 5: claimed_level is a valid proof-ladder level
if plc.claimed_level not in VALID_PROOF_LADDER_LEVELS:
errors.append(
f"claimed_level must be one of {list(VALID_PROOF_LADDER_LEVELS)}, "
f"got: {plc.claimed_level!r}"
)

# Rule 6: evidence_type vocabulary
if plc.evidence_type not in VALID_EVIDENCE_TYPES:
errors.append(
f"evidence_type must be one of {sorted(VALID_EVIDENCE_TYPES)}, "
f"got: {plc.evidence_type!r}"
)

# Rule 7: verifier_type vocabulary
if plc.verifier_type not in VALID_VERIFIER_TYPES:
errors.append(
f"verifier_type must be one of {sorted(VALID_VERIFIER_TYPES)}, "
f"got: {plc.verifier_type!r}"
)

# Rule 8: verification_date ISO format
if not _ISO_DATE_RE.match(plc.verification_date):
errors.append(
f"verification_date must be YYYY-MM-DD, got: {plc.verification_date!r}"
)

# Rule 9: supporting_artifact_ids must be non-empty list
if not plc.supporting_artifact_ids:
errors.append(
"supporting_artifact_ids must be non-empty — "
"at least one artifact must support the claimed level"
)

# Rule 10: unsupported_claims non-empty (explicitly documenting what is NOT supported)
if not plc.unsupported_claims.strip():
errors.append(
"unsupported_claims must not be empty — explicitly document what "
"this level does NOT support (anti-overclaim guard)"
)
elif len(plc.unsupported_claims) > UNSUPPORTED_CLAIMS_MAX_LENGTH:
errors.append(
f"unsupported_claims exceeds {UNSUPPORTED_CLAIMS_MAX_LENGTH} chars "
f"(got {len(plc.unsupported_claims)})"
)

# Rule 11: dry_lab_only=True caps level at DRY_LAB_MAX_LEVEL
if plc.dry_lab_only and plc.claimed_level in VALID_PROOF_LADDER_LEVELS:
claimed_index = VALID_PROOF_LADDER_LEVELS.index(plc.claimed_level)
if claimed_index > DRY_LAB_MAX_INDEX:
errors.append(
f"dry_lab_only=True caps claimed_level at '{DRY_LAB_MAX_LEVEL}'; "
f"got '{plc.claimed_level}' which requires wet-lab evidence"
)

# Rule 12: wet_lab levels require human_review_required=True
if plc.claimed_level in VALID_PROOF_LADDER_LEVELS:
claimed_index = VALID_PROOF_LADDER_LEVELS.index(plc.claimed_level)
if claimed_index >= VALID_PROOF_LADDER_LEVELS.index("expert_reviewed_assay_proposal"):
if not plc.human_review_required:
errors.append(
f"claimed_level '{plc.claimed_level}' requires "
"human_review_required=True"
)

# Rule 13: if human_review_required then human_review_completed must track status
if plc.human_review_required and not plc.human_review_completed:
errors.append(
"WARNING: human_review_required=True but human_review_completed=False "
"— this certificate is pending human review"
)

# Rule 14: notes length
if len(plc.notes) > NOTES_MAX_LENGTH:
errors.append(
f"notes exceeds {NOTES_MAX_LENGTH} chars (got {len(plc.notes)})"
)

# Warnings
if len(plc.supporting_artifact_ids) == 1:
errors.append(
"WARNING: only 1 supporting artifact — consider adding additional "
"corroborating artifacts for stronger evidence"
)
if not plc.notes.strip():
errors.append(
"WARNING: notes is empty — consider documenting verification context"
)

return errors


def validate_dict(data: dict) -> List[str]:
"""Validate a plain dict by constructing ProofLadderLevelCertificate first."""
try:
plc = ProofLadderLevelCertificate(**data)
except TypeError as exc:
return [f"Schema construction error: {exc}"]
return validate(plc)
Loading
Loading