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

## Changelog

### v0.10.30 — Phase B B6: Human-readable certificate report
- Added `src/openamp_foundry/evidence/certificate_report.py` — build_certificate_report() function
- Converts cert dict to formatted text with labelled sections: CANDIDATE, PROOF LADDER, SCORES (with not-biological-proof notice), CHEAP-EXPLANATION CHECK, SELECTION REASON, KNOWN FAILURE MODES, RECOMMENDED NEXT STEPS, REFERENCES CHECKED, optional QUALITY TIER
- Footer: DRY-LAB COMPUTATIONAL OUTPUTS ONLY notice on every report
- Optional quality_report parameter integrates with assess_certificate_quality()
- 63 tests in `tests/test_certificate_report.py`
- BASELINE 6457→6520
- Closes Phase B B6 — domain experts can now inspect candidate certificates as readable text

### v0.10.29 — Phase B B5: Certificate quality-tier validator
- Added `src/openamp_foundry/evidence/certificate_quality.py` — assess_certificate_quality() function
- Three tiers: draft (candidate_id+sequence+scores) → internal_review (+selection_reason+known_failure_modes+proof_ladder_level+baseline_caveat+pipeline_version, no forbidden claims) → external_review_ready (+recommended_next_steps+references_checked+config_hash, no warnings)
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 @@ -46,7 +46,7 @@ Make candidate evidence packages impossible to confuse with biological proof.
| B3 | Add `baseline_caveat` field (complete). | Added to build_certificate(): auto-computes charge/length/hydrophobicity flags and warns when all three cheap baselines pass; forces cheap-explanation visibility at certificate issue time. 63 tests in tests/test_certificate_baseline_caveat.py. | B |
| B4 | Add `release_status` field. | Supports staged release. | C/D |
| B5 | Add certificate quality-tier validator (complete). | assess_certificate_quality() computes draft/internal_review/external_review_ready tier; missing-field detection, forbidden-claim violation check, external-review gate; 63 tests in tests/test_certificate_quality_validator.py. | B |
| B6 | Add human-readable certificate report. | Domain experts can inspect faster. | B |
| B6 | Add human-readable certificate report (complete). | build_certificate_report() in certificate_report.py converts cert dict to formatted text with sections: identity, proof ladder, scores, cheap-explanation check, selection reason, failure modes, next steps, references, optional quality tier; 63 tests in tests/test_certificate_report.py. | B |
| B7 | Add candidate rejection certificate support. | Failures become artifacts. | B |
| B8 | Link certificates to run manifest hashes. | Stronger reproducibility. | B |
| B9 | Add test that dry-lab certificates cannot include forbidden claims (complete). | CI gate: tests/test_certificate_claim_discipline.py scans all cert text fields against RISKY_PATTERNS and FORBIDDEN_CLAIM_PATTERNS; default cert proven clean; forbidden phrases confirmed detectable. 35 tests total. | B/C |
Expand Down
170 changes: 170 additions & 0 deletions src/openamp_foundry/evidence/certificate_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""Human-readable certificate report generator — Phase B B6.

Converts a certificate dict (from build_certificate()) into a formatted text
report that domain experts can read quickly without parsing JSON.

Usage:
from openamp_foundry.evidence.certificate import build_certificate
from openamp_foundry.evidence.certificate_report import build_certificate_report

cert = build_certificate(scored, config, references)
report = build_certificate_report(cert)
print(report)
"""

from __future__ import annotations

from typing import Any

_SEPARATOR = "=" * 72
_SECTION_SEP = "-" * 72


def _fmt_score(val: Any) -> str:
if isinstance(val, float):
return f"{val:.3f}"
return str(val)


def _fmt_list(items: Any, indent: int = 2) -> str:
prefix = " " * indent
if not items:
return f"{prefix}(none)"
if isinstance(items, list):
return "\n".join(f"{prefix}• {item}" for item in items)
return f"{prefix}{items}"


def build_certificate_report(
cert: dict[str, Any],
quality_report: dict[str, Any] | None = None,
) -> str:
"""Build a human-readable text report from a certificate dict.

Args:
cert: Certificate dict from build_certificate().
quality_report: Optional output of assess_certificate_quality(cert).
If provided, quality tier and readiness are included.

Returns:
A multi-line string formatted for domain expert review.
"""
lines: list[str] = []

# Header
lines.append(_SEPARATOR)
lines.append("OPENAMP FOUNDRY — CANDIDATE EVIDENCE CERTIFICATE")
lines.append(_SEPARATOR)
lines.append("")

# Identity
lines.append("CANDIDATE")
lines.append(_SECTION_SEP)
lines.append(f" ID: {cert.get('candidate_id', '(unknown)')}")
lines.append(f" Sequence: {cert.get('sequence', '(none)')}")
lines.append(f" Source: {cert.get('source', '(unknown)')}")
lines.append(f" Version: {cert.get('pipeline_version', '(unknown)')}")
lines.append(f" Generated:{cert.get('generated_at', '(unknown)')}")
lines.append("")

# Proof ladder
ladder_level = cert.get("proof_ladder_level", "(not set)")
lines.append("PROOF LADDER")
lines.append(_SECTION_SEP)
lines.append(f" Level: {ladder_level}")
lines.append("")

# Scores
lines.append("SCORES [dry-lab computational — not biological proof]")
lines.append(_SECTION_SEP)
scores = cert.get("scores", {})
if isinstance(scores, dict):
for key, val in sorted(scores.items()):
lines.append(f" {key:<20} {_fmt_score(val)}")
else:
lines.append(" (none)")
lines.append("")

# Baseline caveat
lines.append("CHEAP-EXPLANATION CHECK")
lines.append(_SECTION_SEP)
caveat = cert.get("baseline_caveat", "(not computed)")
for chunk in _wrap(caveat, width=68, indent=2):
lines.append(chunk)
lines.append("")

# Selection reason
lines.append("SELECTION REASON")
lines.append(_SECTION_SEP)
lines.append(_fmt_list(cert.get("selection_reason", [])))
lines.append("")

# Known failure modes
lines.append("KNOWN FAILURE MODES")
lines.append(_SECTION_SEP)
lines.append(_fmt_list(cert.get("known_failure_modes", [])))
lines.append("")

# Recommended next steps
lines.append("RECOMMENDED NEXT STEPS")
lines.append(_SECTION_SEP)
lines.append(_fmt_list(cert.get("recommended_next_steps", [])))
lines.append("")

# References checked
refs = cert.get("references_checked", [])
lines.append("REFERENCES CHECKED")
lines.append(_SECTION_SEP)
lines.append(_fmt_list(refs) if refs else " (none)")
lines.append("")

# Quality tier (optional)
if quality_report is not None:
tier = quality_report.get("quality_tier", "(unknown)")
is_ready = quality_report.get("is_external_review_ready", False)
missing = quality_report.get("missing_fields", [])
violations = quality_report.get("claim_violations", [])
warnings = quality_report.get("warnings", [])

lines.append("QUALITY TIER")
lines.append(_SECTION_SEP)
lines.append(f" Tier: {tier}")
lines.append(f" External-review ready: {'YES' if is_ready else 'NO'}")
if missing:
lines.append(f" Missing fields: {', '.join(missing)}")
if violations:
lines.append(f" Claim violations: {len(violations)}")
if warnings:
for w in warnings:
lines.append(f" Warning: {w}")
lines.append("")

# Footer
lines.append(_SEPARATOR)
lines.append("NOTICE: This certificate reflects DRY-LAB COMPUTATIONAL OUTPUTS ONLY.")
lines.append("No biological activity has been confirmed. All scores are predictions.")
lines.append("Independent domain expert review is required before any assay decision.")
lines.append(_SEPARATOR)

return "\n".join(lines)


def _wrap(text: str, width: int = 68, indent: int = 2) -> list[str]:
"""Simple word-wrap that preserves leading indent."""
prefix = " " * indent
words = text.split()
if not words:
return [prefix]
lines: list[str] = []
current = prefix
for word in words:
candidate = current + (" " if current.strip() else "") + word
if len(candidate) <= width:
current = candidate
else:
if current.strip():
lines.append(current)
current = prefix + word
if current.strip():
lines.append(current)
return lines if lines else [prefix]
Loading
Loading