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
65 changes: 65 additions & 0 deletions docs/evidence/CERTIFICATE_CLAIM_BOUNDARY_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CertificateClaimBoundary (CCB-) Guide

## Purpose

`CertificateClaimBoundary` documents what a certificate does NOT prove. It prevents
score-to-proof drift by requiring explicit enumeration of unsupported claim classes
before a certificate can be shared externally.

This is the negative complement of `ProofLadderLevelCertificate` (PLC-): while PLC-
asserts what level IS supported, CCB- explicitly asserts what is NOT.

## Claim Classes (8 total)

| Class | Description |
|-------|-------------|
| `biological_activity` | Actual in-vitro antimicrobial activity |
| `human_safety` | Safety for human use or administration |
| `clinical_utility` | Usefulness in clinical settings |
| `animal_efficacy` | Efficacy in animal models |
| `therapeutic_indication` | Indication for therapeutic use |
| `regulatory_approval` | Any regulatory or market approval |
| `mechanism_proof` | Proven mechanism of action |
| `resistance_profile` | Actual resistance/susceptibility profile |

## Schema Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ccb_id` | str | yes | Unique identifier, must start with `CCB-` |
| `pipeline_version` | str | yes | Pipeline version |
| `certificate_id` | str | yes | Source certificate ID |
| `candidate_id` | str | yes | Candidate being assessed |
| `boundary_date` | str | yes | ISO date (YYYY-MM-DD) |
| `unsupported_claim_classes` | list | yes | ≥3 claim classes from controlled vocabulary |
| `boundary_statement` | str | yes | Human-readable statement (≤600 chars) |
| `dry_lab_only` | bool | yes | Must be True |
| `all_listed_classes_unsupported` | bool | yes | Must be True |
| `notes` | str | no | Additional context (≤300 chars) |

## Validation Rules

1. `ccb_id` starts with `CCB-`
2. `pipeline_version` non-empty
3. `certificate_id` non-empty
4. `candidate_id` non-empty
5. `boundary_date` is ISO date (YYYY-MM-DD)
6. `unsupported_claim_classes` non-empty
7. All listed classes in controlled vocabulary (8 values)
8. At least 3 valid claim classes listed
9. `boundary_statement` non-empty and ≤600 chars
10. `dry_lab_only` must be True
11. `all_listed_classes_unsupported` must be True
12. No duplicate claim classes
13. `notes` ≤300 chars

## Warnings

- Not all 8 claim classes listed: reports how many are missing
- `notes` empty: consider documenting context

## Integration

- Pairs with: `ProofLadderLevelCertificate` (PLC-) as negative complement
- Required for: external review packets, partner sharing
- Closes: Phase B B2 — score-to-proof drift prevention
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.27 — Phase B B2: CertificateClaimBoundary schema
- Added `CertificateClaimBoundary` (CCB-) schema
- 10 fields: ccb_id, pipeline_version, certificate_id, candidate_id, boundary_date, unsupported_claim_classes (8-value vocab), boundary_statement, dry_lab_only, all_listed_classes_unsupported, notes
- 13 validation rules; 2 warnings; 63 tests
- BASELINE 6268→6331
- CLI: openamp-foundry certificate-claim-boundary-check
- Closes Phase B B2 — negative complement of PLC-; prevents score-to-proof drift by requiring explicit enumeration of unsupported claims

### 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
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 @@ -42,7 +42,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` (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 |
| B2 | Add `unsupported_claims` field to certificates (complete). | CCB- schema: 10 fields, 13 validation rules, 8 claim class vocabulary, ≥3 classes required, dry_lab_only+all_listed_classes_unsupported enforced, no-duplicate check; negative complement of PLC- closes score-to-proof drift gap. | B |
| B3 | Add `baseline_caveat` field. | Forces cheap-explanation visibility. | B |
| B4 | Add `release_status` field. | Supports staged release. | C/D |
| B5 | Add certificate quality-tier validator. | External-review readiness becomes measurable. | B |
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 @@ -2614,6 +2614,23 @@ def _run_rotation_plan_check(args: argparse.Namespace) -> int:
return 0 if result.passed else 3


def _run_certificate_claim_boundary_check(args):
"""CLI handler for certificate-claim-boundary-check."""
import json, sys
from openamp_foundry.evidence.certificate_claim_boundary 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: CertificateClaimBoundary is valid.")


def _run_security_report_check(args: argparse.Namespace) -> int:
"""Validate a security vulnerability report."""
import json as _json
Expand Down
5 changes: 5 additions & 0 deletions src/openamp_foundry/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
_run_synthetic_boundary_audit_record_check,
_run_expert_review_example_package_check,
_run_proof_ladder_level_certificate_check,
_run_certificate_claim_boundary_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 @@ -2553,6 +2554,10 @@ def build_parser() -> argparse.ArgumentParser:
p_plc.add_argument("input", help="Path to JSON file")
p_plc.set_defaults(func=_run_proof_ladder_level_certificate_check)

p_ccb = subparsers.add_parser("certificate-claim-boundary-check", help="Validate a CertificateClaimBoundary JSON")
p_ccb.add_argument("input", help="Path to JSON file")
p_ccb.set_defaults(func=_run_certificate_claim_boundary_check)

# ── Selection rationale check (Phase K K1) ───────────────────────
src2 = sub.add_parser(
"selection-rationale-check",
Expand Down
178 changes: 178 additions & 0 deletions src/openamp_foundry/evidence/certificate_claim_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Certificate claim boundary schema — Phase B B2.

Documents what a certificate's scores do NOT prove. Prevents score-to-proof
drift by requiring explicit enumeration of unsupported claim classes before
a certificate can be shared externally.

This schema is the negative complement of ProofLadderLevelCertificate (PLC-):
while PLC- asserts what level IS supported, CCB- explicitly asserts what is NOT.

Claim classes (the things a dry-lab certificate cannot support):
biological_activity — actual in-vitro antimicrobial activity
human_safety — safety for human use or administration
clinical_utility — usefulness in clinical settings
animal_efficacy — efficacy in animal models
therapeutic_indication — indication for therapeutic use
regulatory_approval — any regulatory or market approval
mechanism_proof — proven mechanism of action
resistance_profile — actual resistance/susceptibility profile
"""

from __future__ import annotations

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

CCB_PREFIX = "CCB-"

ALL_CLAIM_CLASSES = frozenset({
"biological_activity",
"human_safety",
"clinical_utility",
"animal_efficacy",
"therapeutic_indication",
"regulatory_approval",
"mechanism_proof",
"resistance_profile",
})

MINIMUM_UNSUPPORTED_CLASSES = 3
BOUNDARY_STATEMENT_MAX_LENGTH = 600
NOTES_MAX_LENGTH = 300
_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


@dataclass
class CertificateClaimBoundary:
ccb_id: str
pipeline_version: str
certificate_id: str
candidate_id: str
boundary_date: str
unsupported_claim_classes: List[str]
boundary_statement: str
dry_lab_only: bool
all_listed_classes_unsupported: bool
notes: str = ""


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

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

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

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

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

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

# Rule 6: unsupported_claim_classes must be non-empty list
if not boundary.unsupported_claim_classes:
errors.append(
"unsupported_claim_classes must not be empty — "
"at least one claim class must be listed as unsupported"
)

# Rule 7: each claim class must be in ALL_CLAIM_CLASSES
invalid_classes = [
c for c in boundary.unsupported_claim_classes
if c not in ALL_CLAIM_CLASSES
]
if invalid_classes:
errors.append(
f"unsupported_claim_classes contains invalid values: {invalid_classes}; "
f"valid classes are {sorted(ALL_CLAIM_CLASSES)}"
)

# Rule 8: minimum unsupported classes
valid_listed = [c for c in boundary.unsupported_claim_classes if c in ALL_CLAIM_CLASSES]
if len(valid_listed) < MINIMUM_UNSUPPORTED_CLASSES:
errors.append(
f"unsupported_claim_classes must list at least "
f"{MINIMUM_UNSUPPORTED_CLASSES} valid claim classes; "
f"got {len(valid_listed)}"
)

# Rule 9: boundary_statement non-empty and length
if not boundary.boundary_statement.strip():
errors.append("boundary_statement must not be empty")
elif len(boundary.boundary_statement) > BOUNDARY_STATEMENT_MAX_LENGTH:
errors.append(
f"boundary_statement exceeds {BOUNDARY_STATEMENT_MAX_LENGTH} chars "
f"(got {len(boundary.boundary_statement)})"
)

# Rule 10: dry_lab_only must be True for pipeline-generated boundaries
if not boundary.dry_lab_only:
errors.append(
"dry_lab_only must be True — certificate claim boundaries are "
"issued for dry-lab certificates only"
)

# Rule 11: all_listed_classes_unsupported must be True
if not boundary.all_listed_classes_unsupported:
errors.append(
"all_listed_classes_unsupported must be True — all listed claim "
"classes are verified as unsupported by this certificate"
)

# Rule 12: no duplicate claim classes
seen = set()
duplicates = []
for c in boundary.unsupported_claim_classes:
if c in seen:
duplicates.append(c)
seen.add(c)
if duplicates:
errors.append(
f"unsupported_claim_classes contains duplicates: {duplicates}"
)

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

# Warnings
if len(valid_listed) < len(ALL_CLAIM_CLASSES):
missing = sorted(ALL_CLAIM_CLASSES - set(boundary.unsupported_claim_classes))
errors.append(
f"WARNING: {len(missing)} claim class(es) not listed as unsupported: "
f"{missing} — consider whether these are truly supported by this certificate"
)
if not boundary.notes.strip():
errors.append(
"WARNING: notes is empty — consider documenting why specific "
"claim classes are not listed"
)

return errors


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