diff --git a/Makefile b/Makefile index ba9049d7..bf2fc75e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help demo test lint ci clean bench-leakage bench-multi-negatives bench-baseline bench-hidden-active bench-cluster-split bench-expert-ablation bench-expert-ablation-500 bench-selectivity bench-feature-decomp bench-gate bench-easy-baseline bench-charge-matched bench-order-dependent bench-precision-at-k bench-per-family bench-simulation-ablation bench-simulation-ablation-within-amp simulation-gate bench-strategy-compare regenerate-all generate phase3 pilot validate-scoring validate-scoring-phase3 validate-scoring-strict external-pilot pilot-confident presynth-qc gold-standard diversity synthesis-order novelty-broad external-consensus questionnaire gate-check ip-report benchmark-card wave0-5-gate-check wave0-5-novelty-audit wave0-5-novelty-audit-v2 wave0-5-panel wave0-5-evidence wave0-5-fill-external wave0-5b-generate wave0-5b-filter recalibration-engine recalibration-engine-dry-run validate-policy-version generate-synthetic-lab-results bump-policy-version generate-review-packet failed-candidate-report safe-publication-filter classify-negative-informativeness validate-rejection-events negative-result-dashboard calibration-audit calibration-audit-example calibration-overfit-check result-quality-filter synthetic-result-policy-check calibration-decision-checklist calibration-rollback-plan simulation-registry validate-simulation-result-schema simulation-baseline-check adapter-gate-check simulation-provenance simulation-ensemble-check simulation-ci-report simulation-deprecation-check simulation-scope-check simulation-evidence-packet artifact-version candidate-manifest artifact-changelog integration-check adapter-author-check license-check artifact-compat-check contribution-check adoption-scorecard release-gate-check decision-log release-request-check +.PHONY: help demo test lint ci clean bench-leakage bench-multi-negatives bench-baseline bench-hidden-active bench-cluster-split bench-expert-ablation bench-expert-ablation-500 bench-selectivity bench-feature-decomp bench-gate bench-easy-baseline bench-charge-matched bench-order-dependent bench-precision-at-k bench-per-family bench-simulation-ablation bench-simulation-ablation-within-amp simulation-gate bench-strategy-compare regenerate-all generate phase3 pilot validate-scoring validate-scoring-phase3 validate-scoring-strict external-pilot pilot-confident presynth-qc gold-standard diversity synthesis-order novelty-broad external-consensus questionnaire gate-check ip-report benchmark-card wave0-5-gate-check wave0-5-novelty-audit wave0-5-novelty-audit-v2 wave0-5-panel wave0-5-evidence wave0-5-fill-external wave0-5b-generate wave0-5b-filter recalibration-engine recalibration-engine-dry-run validate-policy-version generate-synthetic-lab-results bump-policy-version generate-review-packet failed-candidate-report safe-publication-filter classify-negative-informativeness validate-rejection-events negative-result-dashboard calibration-audit calibration-audit-example calibration-overfit-check result-quality-filter synthetic-result-policy-check calibration-decision-checklist calibration-rollback-plan simulation-registry validate-simulation-result-schema simulation-baseline-check adapter-gate-check simulation-provenance simulation-ensemble-check simulation-ci-report simulation-deprecation-check simulation-scope-check simulation-evidence-packet artifact-version candidate-manifest artifact-changelog integration-check adapter-author-check license-check artifact-compat-check contribution-check adoption-scorecard release-gate-check decision-log release-request-check coi-check PYTHON := $(shell [ -f .venv/bin/python ] && echo .venv/bin/python || echo python3) PYTEST := $(shell [ -f .venv/bin/pytest ] && echo .venv/bin/pytest || echo pytest) @@ -850,3 +850,8 @@ release-request-check: .venv/bin/openamp-foundry release-request-check \ --request-json '{"release_id":"REL-2026-001","release_type":"schema","artifact_id":"candidate_manifest","artifact_version":"1.0.0","requestor_name":"OpenAMP Team","requestor_institution":"Open Problem Lab","request_date":"2026-07-09","evidence_level":2,"dry_lab_only":true,"safety_review_status":"approved","benchmark_summary":"Passes all schema compatibility checks","known_limitations":"Dry-lab only, not wet-lab validated","intended_use":"public","data_license":"CC-BY-4.0","human_reviewer":"maintainer","review_class":"B","approval_status":"pending"}' @echo "Release request check complete." + +coi-check: + .venv/bin/openamp-foundry coi-check \ + --disclosure-json '{"disclosure_id":"COI-2026-001","disclosure_type":"reviewer","subject":"maintainer","related_artifact":"REL-2026-001","relationship_type":"none","description":"","disclosure_date":"2026-07-09","recusal_required":false,"reviewer":"lead-maintainer","review_status":"acknowledged"}' + @echo "COI check complete." diff --git a/docs/evidence/METRICS_CURRENT.md b/docs/evidence/METRICS_CURRENT.md index 06cdfd80..88b48342 100644 --- a/docs/evidence/METRICS_CURRENT.md +++ b/docs/evidence/METRICS_CURRENT.md @@ -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 J J3 — release request template — v0.7.1) +> **Last updated:** 2026-07-09 (Phase J J4 — COI disclosure template — v0.7.2) +> **New in v0.7.2:** COI disclosure template (J4 — Loop 112) — `docs/governance/COI_DISCLOSURE_TEMPLATE.md` with structured COI disclosure template (purpose, fill-in-the-blank template with 10 fields: Disclosure ID, disclosure type (reviewer|contributor|maintainer|external_advisor), subject, related artifact, relationship type (financial|institutional|competitive|personal|none), description (conditional), date, recusal required, reviewer, review status (pending|acknowledged|resolved); when to disclose section covering financial/institutional/competitive/personal relationships; process with machine validation and escalation; linked policies). `src/openamp_foundry/governance/coi_disclosure.py` with `COIDisclosure` dataclass (10 fields), `COIValidationResult` dataclass (6 fields, dry_lab_only=True), `VALID_DISCLOSURE_TYPES` (4: contributor, external_advisor, maintainer, reviewer), `VALID_RELATIONSHIP_TYPES` (5: competitive, financial, institutional, none, personal), `VALID_REVIEW_STATUSES` (3: acknowledged, pending, resolved), `validate_coi_disclosure()` (10 checks including disclosure_id COI- prefix, valid disclosure_type, non-empty subject/related_artifact, valid relationship_type, description required unless none, YYYY-MM-DD date, non-empty reviewer, valid review_status, dry_lab_only must be True; financial without recusal warning), `validate_coi_dict()` (dict input with 10 required fields guard, missing fields returns passed=False early). CLI (`openamp-foundry coi-check`) with `--disclosure-json`, `--format text|json`. `make coi-check` target. 20 tests. Dry-lab only. COI disclosures now have a validated structure that builds institutional trust. > **New in v0.7.1:** Release request template (J3 — Loop 111) — `docs/governance/RELEASE_REQUEST_TEMPLATE.md` with structured release request template (purpose, fill-in-the-blank template with 17 fields: Release ID, release type, artifact ID/version, requestor name/institution, request date, evidence level 1-6, dry_lab_only, safety_review_status, benchmark_summary, known_limitations, intended_use, data_license, human_reviewer, review_class, approval_status; review criteria with 8 checks; process with classes A-D timelines and escalation path). `src/openamp_foundry/governance/release_request.py` with `ReleaseRequest` dataclass (17 fields), `ReleaseRequestValidationResult` dataclass (6 fields, dry_lab_only=True), `VALID_RELEASE_TYPES` (5), `VALID_SAFETY_STATUSES` (3), `VALID_INTENDED_USES` (4), `VALID_APPROVAL_STATUSES` (4), `VALID_REVIEW_CLASSES` (4), `validate_release_request()` (17 checks including dry_lab_only+evidence_level>4 error, public+safety_pending error, model+review_class warning), `validate_request_dict()` (dict input with missing-fields guard). CLI (`openamp-foundry release-request-check`) with `--request-json`, `--format text|json`. `make release-request-check` target. 25 tests. **3516 total.** Formal release requests now have a validated structure before entering human review. > **New in v0.7.0:** Governance decision log index (J2 — Loop 110, Phase J milestone) — `docs/governance/DECISION_LOG.md` with structured decision log (purpose, decision index with 8 entries GOV-001 through GOV-008 covering safety/benchmark/release/evidence/data/adapter/contribution/docs scopes, how to add entries, linked policies). `src/openamp_foundry/governance/decision_log.py` with `VALID_SCOPES` (8 entries: safety, benchmark, release, evidence, data, adapter, contribution, docs), `VALID_STATUSES` (4 entries: active, superseded, under_review, proposed), `VALID_REVIEW_CLASSES` (4 entries: A, B, C, D), `GovernanceDecision` dataclass (8 fields: decision_id, date, scope, decision, status, rationale, review_class, dry_lab_only=True), `DecisionValidationResult` dataclass (5 fields: decision_id, passed, errors, warnings, dry_lab_only=True), `GOVERNANCE_DECISIONS` list (8 entries: GOV-001 through GOV-008), `validate_governance_decision()` (9 checks: decision_id format, date format, valid scope, non-empty decision, valid status, non-empty rationale, valid review_class, dry_lab_only must be True, superseded warning), `validate_all_decisions()` (aggregates total/passed/failed/all_passed/results/dry_lab_only), `get_decisions_by_scope()` (filters by scope), `get_decisions_by_status()` (filters by status). CLI (`openamp-foundry decision-log`) with `--validate`, `--scope`, `--format text|json`. `make decision-log` target. 27 tests. **3505 total.** **Phase J milestone: v0.7.0** — governance decisions are now discoverable and machine-validated. > **New in v0.6.9:** Release checklist and gate validator (J1 — Loop 109, starts Phase J) — `docs/governance/RELEASE_CHECKLIST.md` with structured checklist (pre-release gates, release-type gates for 5 types, post-release checklist), cross-referencing `docs/trust/RELEASE_CHECKLIST.md`. `src/openamp_foundry/governance/release_gate.py` with `RELEASE_TYPES` (5: candidate, model, dataset, evidence_packet, schema), `UNIVERSAL_GATES` (7: ci_tests_pass, agent_check_passes, no_critical_issues, dry_lab_only_confirmed, safety_flags_reviewed, data_license_verified, no_hardcoded_secrets), `EXTRA_GATES_BY_TYPE` (per-type additional gates), `ReleaseGateResult` dataclass (8 fields, dry_lab_only=True), `validate_release_gate()` (validates all required gates, treats missing gates as failed, raises CRITICAL error on dry_lab_only_confirmed failure). CLI (`openamp-foundry release-gate-check`) with `--release-type`, `--artifact-id`, `--gates-json`, `--format text|json`. `make release-gate-check` target. 18 tests. **3478 total.** **Starts Phase J (Governance and release maturity)** — releases now require all gates to pass before external release, preventing accidental bypass of required checks. @@ -99,7 +100,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.7.1 (J3 — release request template) +> **Pipeline version:** v0.7.2 (J4 — COI disclosure template) > **Branch:** main --- diff --git a/docs/governance/COI_DISCLOSURE_TEMPLATE.md b/docs/governance/COI_DISCLOSURE_TEMPLATE.md new file mode 100644 index 00000000..89e7f78c --- /dev/null +++ b/docs/governance/COI_DISCLOSURE_TEMPLATE.md @@ -0,0 +1,63 @@ +# Conflict-of-Interest Disclosure Template + +## Purpose + +A conflict-of-interest (COI) disclosure is filed when a contributor, reviewer, maintainer, or external advisor has a potential conflict with an artifact, decision, or review they are involved with. Filing a disclosure enables transparent governance and builds institutional trust. + +**File a disclosure when you have:** +- A financial interest in an artifact or organization related to the work +- An institutional relationship (current or former employer, funder) with a stakeholder +- A competitive relationship with parties being evaluated +- A personal relationship that could influence objectivity + +**Who reviews:** the designated `reviewer` (GitHub handle) noted in the disclosure. + +--- + +## Template + +Fill in every field. Fields marked REQUIRED must not be empty. + +```yaml +disclosure_id: "" # e.g. COI-2026-001 (REQUIRED) +disclosure_type: "" # reviewer | contributor | maintainer | external_advisor (REQUIRED) +subject: "" # GitHub handle of person filing disclosure (REQUIRED) +related_artifact: "" # artifact ID or PR number (REQUIRED) +relationship_type: "" # financial | institutional | competitive | personal | none (REQUIRED) +description: "" # Required when relationship_type != "none"; leave empty only if "none" +disclosure_date: "" # YYYY-MM-DD (REQUIRED) +recusal_required: false # true | false (REQUIRED) +reviewer: "" # GitHub handle of designated reviewer (REQUIRED) +review_status: "" # pending | acknowledged | resolved (REQUIRED) +``` + +--- + +## When to Disclose + +| Relationship type | Examples | Typical action | +|---|---|---| +| financial | equity, grants, consulting fees from a related org | Disclose; recusal often required | +| institutional | current/former employer, department, funder | Disclose; recusal case-by-case | +| competitive | org whose product competes with the artifact | Disclose; recusal recommended | +| personal | close family, romantic partner, close friend | Disclose; recusal case-by-case | +| none | no conflict | File with description="" to document clean slate | + +--- + +## Process + +1. **File the disclosure** — copy the template above, fill in all fields, open a GitHub issue titled `[COI Disclosure] COI-YYYY-NNN — `. +2. **Machine validation** — run `openamp-foundry coi-check --disclosure-json '{...}'` to pre-validate. +3. **Human review** — the designated `reviewer` acknowledges within 3 business days. +4. **Decision** — reviewer sets `review_status` to `acknowledged` or `resolved` with written rationale on recusal. +5. **Escalation** — if no acknowledgment within 5 business days, escalate to project maintainers via GitHub discussion. + +--- + +## Linked Policies + +- [GOVERNANCE.md](../../GOVERNANCE.md) +- [RESPONSIBLE_USE.md](../../RESPONSIBLE_USE.md) +- [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md) +- [DECISION_LOG.md](DECISION_LOG.md) diff --git a/docs/research/ROADMAP.md b/docs/research/ROADMAP.md index ec96d3ae..0a04c099 100644 --- a/docs/research/ROADMAP.md +++ b/docs/research/ROADMAP.md @@ -1,5 +1,76 @@ # Roadmap +## v0.7.2 — Loop 112: Phase J J4 — COI Disclosure Template ✓ (2026-07-09) + +`docs/governance/COI_DISCLOSURE_TEMPLATE.md` with structured COI disclosure +template (purpose, fill-in-the-blank format with 10 fields: Disclosure ID +COI-YYYY-NNN, disclosure type reviewer|contributor|maintainer|external_advisor, +subject GitHub handle, related artifact or PR, relationship type +financial|institutional|competitive|personal|none, description (required unless +none), date YYYY-MM-DD, recusal_required true|false, reviewer GitHub handle, +review_status pending|acknowledged|resolved). + +`src/openamp_foundry/governance/coi_disclosure.py` with `COIDisclosure` +dataclass (10 fields), `COIValidationResult` dataclass (6 fields, +dry_lab_only=True), `VALID_DISCLOSURE_TYPES` (4: contributor, external_advisor, +maintainer, reviewer), `VALID_RELATIONSHIP_TYPES` (5: competitive, financial, +institutional, none, personal), `VALID_REVIEW_STATUSES` (3: acknowledged, +pending, resolved), `validate_coi_disclosure()` (10 checks: disclosure_id +starts with COI-, valid disclosure_type, non-empty subject/related_artifact, +valid relationship_type, description required unless none, YYYY-MM-DD date, +non-empty reviewer, valid review_status, dry_lab_only must be True; financial +without recusal yields warning not error), `validate_coi_dict()` (dict input +with 10 required fields guard). + +CLI (`openamp-foundry coi-check`) with `--disclosure-json` (required), +`--format text|json`. Handler `_run_coi_check` in reports.py. + +`make coi-check` target. 20 tests. **3536 total.** + +COI disclosures now have a validated structure that builds institutional trust. + +Changes: +- `docs/governance/COI_DISCLOSURE_TEMPLATE.md` (J4) — Structured COI disclosure + template with purpose, template (10 fields), when to disclose (financial, + institutional, competitive, personal), process (5 steps with escalation). +- `src/openamp_foundry/governance/coi_disclosure.py` (J4) — Core module with + `COIDisclosure` (10 fields), `COIValidationResult` (6 fields, + dry_lab_only=True), `VALID_DISCLOSURE_TYPES` (4), `VALID_RELATIONSHIP_TYPES` + (5), `VALID_REVIEW_STATUSES` (3), `validate_coi_disclosure()` (10 checks), + `validate_coi_dict()` (dict input with 10 required fields guard). +- `tests/governance/test_coi_disclosure.py` (J4) — 20 tests covering: valid + reviewer none relationship passes, valid contributor financial passes, + disclosure_id not starting with COI- fails, empty disclosure_id fails, invalid + disclosure_type fails, empty subject fails, empty related_artifact fails, + invalid relationship_type fails, relationship not none with empty description + fails, relationship none with empty description passes, invalid date format + fails, empty reviewer fails, invalid review_status fails, dry_lab_only=False + fails, financial without recusal warns, validate_coi_dict passes, validate_ + coi_dict with missing fields fails, all results dry_lab_only=True, + VALID_DISCLOSURE_TYPES has 4, VALID_RELATIONSHIP_TYPES has 5. +- `src/openamp_foundry/cli/main.py` (J4) — Registered `coi-check` subcommand + with `--disclosure-json`, `--format` flags. Added import and dispatch. +- `src/openamp_foundry/cli/commands/reports.py` (J4) — Added `_run_coi_check()` + CLI handler with JSON parsing, validate_coi_dict call, text and JSON output, + exit code 3 on validation failure. +- `Makefile` (J4) — Added `coi-check` target. Added to `.PHONY`. +- `docs/evidence/METRICS_CURRENT.md` (J4) — v0.7.2 J4 changelog. Pipeline + version: v0.7.2. Test count: 3536. +- `tests/test_test_count_regression.py` — baseline updated to 3536. + +Honest boundaries: +- COI disclosure validation checks structural and policy requirements only. + It does not verify that the disclosed information is true, complete, or + accurate. +- Financial relationship without recusal produces a warning, not an error — + the reviewer retains discretion to determine whether recusal is necessary. +- The validator cannot detect undisclosed conflicts — it only checks that + disclosed conflicts are well-formed. +- `dry_lab_only: true` is a const field on all dataclasses — COI disclosures + are governance artifacts, not legal determinations. +- The COI template is a transparency and governance tool — it does not replace + the judgment of the human reviewer or governance team. + ## v0.7.1 — Loop 111: Phase J J3 — Release Request Template ✓ (2026-07-09) `docs/governance/RELEASE_REQUEST_TEMPLATE.md` with structured release request diff --git a/src/openamp_foundry/cli/commands/reports.py b/src/openamp_foundry/cli/commands/reports.py index 858f62c2..977938e3 100644 --- a/src/openamp_foundry/cli/commands/reports.py +++ b/src/openamp_foundry/cli/commands/reports.py @@ -2489,6 +2489,54 @@ def _run_release_request_check(args: argparse.Namespace) -> int: return 0 if result.passed else 3 +def _run_coi_check(args: argparse.Namespace) -> int: + """Validate a COI disclosure before the formal review queue.""" + from openamp_foundry.governance.coi_disclosure import validate_coi_dict + + try: + disclosure_dict = json.loads(args.disclosure_json) + except (json.JSONDecodeError, TypeError) as e: + print(json.dumps({"status": "error", "error": f"Invalid --disclosure-json: {e}"})) + return 2 + + if not isinstance(disclosure_dict, dict): + print(json.dumps({"status": "error", "error": "--disclosure-json must be a JSON object"})) + return 2 + + result = validate_coi_dict(disclosure_dict) + output_format = getattr(args, "format", "text") + + if output_format == "json": + print(json.dumps({ + "disclosure_id": result.disclosure_id, + "disclosure_type": result.disclosure_type, + "passed": result.passed, + "errors": result.errors, + "warnings": result.warnings, + "dry_lab_only": result.dry_lab_only, + }, indent=2)) + else: + print("COI Disclosure Check") + print("=" * 60) + print(f"Disclosure ID: {result.disclosure_id}") + print(f"Disclosure type: {result.disclosure_type}") + print(f"Status: {'PASS' if result.passed else 'FAIL'}") + print() + if result.errors: + print("Errors:") + for err in result.errors: + print(f" - {err}") + if result.warnings: + print("Warnings:") + for w in result.warnings: + print(f" - {w}") + print() + print("Dry-lab only. COI disclosure validation is a computational") + print("safeguard, not a legal determination.") + + return 0 if result.passed else 3 + + def _run_adoption_scorecard(args: argparse.Namespace) -> int: """Build an adoption scorecard from dimension inputs.""" from openamp_foundry.adoption.scorecard import build_scorecard diff --git a/src/openamp_foundry/cli/main.py b/src/openamp_foundry/cli/main.py index c874ce0a..87b9f766 100644 --- a/src/openamp_foundry/cli/main.py +++ b/src/openamp_foundry/cli/main.py @@ -10,6 +10,7 @@ from openamp_foundry.cli.commands.reports import _run_contribution_check from openamp_foundry.cli.commands.reports import _run_decision_log from openamp_foundry.cli.commands.reports import _run_release_request_check +from openamp_foundry.cli.commands.reports import _run_coi_check import argparse import json @@ -1858,6 +1859,24 @@ def build_parser() -> argparse.ArgumentParser: help="Output format (default: text).", ) + # ── COI disclosure check (Phase J J4) ─────────────────────────── + coi = sub.add_parser( + "coi-check", + help=( + "Validate a conflict-of-interest disclosure before the " + "formal review queue. Dry-lab only." + ), + ) + coi.add_argument( + "--disclosure-json", type=str, required=True, + help="JSON string of a COIDisclosure dict (required).", + ) + coi.add_argument( + "--format", type=str, default="text", + choices=["text", "json"], + help="Output format (default: text).", + ) + # ── Release gate check (Phase J J1) ───────────────────────────── rgc = sub.add_parser( "release-gate-check", @@ -2133,6 +2152,9 @@ def main(argv: list[str] | None = None) -> int: if args.command == "release-request-check": return _run_release_request_check(args) + if args.command == "coi-check": + return _run_coi_check(args) + parser.error("unknown command") return 2 diff --git a/src/openamp_foundry/governance/coi_disclosure.py b/src/openamp_foundry/governance/coi_disclosure.py new file mode 100644 index 00000000..9f7d136d --- /dev/null +++ b/src/openamp_foundry/governance/coi_disclosure.py @@ -0,0 +1,116 @@ +"""COI disclosure validator — validates conflict-of-interest disclosures. + +Ensures all required fields are present before a COI disclosure enters +the formal review queue. +Dry-lab only. +""" +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +VALID_DISCLOSURE_TYPES: set[str] = { + "reviewer", "contributor", "maintainer", "external_advisor" +} + +VALID_RELATIONSHIP_TYPES: set[str] = { + "financial", "institutional", "competitive", "personal", "none" +} + +VALID_REVIEW_STATUSES: set[str] = { + "pending", "acknowledged", "resolved" +} + + +@dataclass +class COIDisclosure: + disclosure_id: str # e.g. "COI-2026-001" + disclosure_type: str # from VALID_DISCLOSURE_TYPES + subject: str # GitHub handle + related_artifact: str # artifact ID or PR number + relationship_type: str # from VALID_RELATIONSHIP_TYPES + description: str # required unless relationship_type == "none" + disclosure_date: str # YYYY-MM-DD + recusal_required: bool + reviewer: str # GitHub handle of reviewer + review_status: str # from VALID_REVIEW_STATUSES + dry_lab_only: bool = True + + +@dataclass +class COIValidationResult: + disclosure_id: str + disclosure_type: str + passed: bool + errors: list[str] + warnings: list[str] + dry_lab_only: bool = True + + +def validate_coi_disclosure(d: COIDisclosure) -> COIValidationResult: + errors: list[str] = [] + warnings: list[str] = [] + + if not d.disclosure_id or not d.disclosure_id.startswith("COI-"): + errors.append("disclosure_id must not be empty and must start with 'COI-'") + if d.disclosure_type not in VALID_DISCLOSURE_TYPES: + errors.append(f"disclosure_type={d.disclosure_type!r} not in {sorted(VALID_DISCLOSURE_TYPES)}") + if not d.subject: + errors.append("subject must not be empty") + if not d.related_artifact: + errors.append("related_artifact must not be empty") + if d.relationship_type not in VALID_RELATIONSHIP_TYPES: + errors.append(f"relationship_type={d.relationship_type!r} not in {sorted(VALID_RELATIONSHIP_TYPES)}") + if d.relationship_type != "none" and not d.description: + errors.append("description must not be empty when relationship_type != 'none'") + if not d.disclosure_date or len(d.disclosure_date) != 10 or d.disclosure_date[4] != "-": + errors.append("disclosure_date must be in YYYY-MM-DD format") + if not d.reviewer: + errors.append("reviewer must not be empty") + if d.review_status not in VALID_REVIEW_STATUSES: + errors.append(f"review_status={d.review_status!r} not in {sorted(VALID_REVIEW_STATUSES)}") + if not d.dry_lab_only: + errors.append("dry_lab_only must be True") + + if d.relationship_type == "financial" and not d.recusal_required: + warnings.append("financial relationships typically require recusal — verify with maintainer") + + return COIValidationResult( + disclosure_id=d.disclosure_id or "", + disclosure_type=d.disclosure_type, + passed=len(errors) == 0, + errors=errors, + warnings=warnings, + dry_lab_only=True, + ) + + +def validate_coi_dict(data: dict[str, Any]) -> COIValidationResult: + required = [ + "disclosure_id", "disclosure_type", "subject", "related_artifact", + "relationship_type", "description", "disclosure_date", + "recusal_required", "reviewer", "review_status", + ] + missing = [f for f in required if f not in data] + if missing: + return COIValidationResult( + disclosure_id=data.get("disclosure_id", ""), + disclosure_type=data.get("disclosure_type", ""), + passed=False, + errors=[f"Missing required fields: {missing}"], + warnings=[], + dry_lab_only=True, + ) + disclosure = COIDisclosure( + disclosure_id=data.get("disclosure_id", ""), + disclosure_type=data.get("disclosure_type", ""), + subject=data.get("subject", ""), + related_artifact=data.get("related_artifact", ""), + relationship_type=data.get("relationship_type", ""), + description=data.get("description", ""), + disclosure_date=data.get("disclosure_date", ""), + recusal_required=data.get("recusal_required", False), + reviewer=data.get("reviewer", ""), + review_status=data.get("review_status", ""), + dry_lab_only=data.get("dry_lab_only", True), + ) + return validate_coi_disclosure(disclosure) diff --git a/tests/governance/test_coi_disclosure.py b/tests/governance/test_coi_disclosure.py new file mode 100644 index 00000000..5e1e8703 --- /dev/null +++ b/tests/governance/test_coi_disclosure.py @@ -0,0 +1,179 @@ +"""Tests for coi_disclosure.py — Phase J J4.""" +import pytest +from openamp_foundry.governance.coi_disclosure import ( + COIDisclosure, + COIValidationResult, + VALID_DISCLOSURE_TYPES, + VALID_RELATIONSHIP_TYPES, + validate_coi_disclosure, + validate_coi_dict, +) + + +def _valid_disclosure(**overrides) -> COIDisclosure: + defaults = dict( + disclosure_id="COI-2026-001", + disclosure_type="reviewer", + subject="maintainer", + related_artifact="REL-2026-001", + relationship_type="none", + description="", + disclosure_date="2026-07-09", + recusal_required=False, + reviewer="lead-maintainer", + review_status="acknowledged", + dry_lab_only=True, + ) + defaults.update(overrides) + return COIDisclosure(**defaults) + + +def test_valid_reviewer_none_relationship_passes(): + result = validate_coi_disclosure(_valid_disclosure()) + assert result.passed + assert result.errors == [] + + +def test_valid_contributor_financial_relationship_passes(): + result = validate_coi_disclosure(_valid_disclosure( + disclosure_type="contributor", + relationship_type="financial", + description="Holds equity in partner company", + recusal_required=True, + )) + assert result.passed + + +def test_disclosure_id_not_starting_with_COI_fails(): + result = validate_coi_disclosure(_valid_disclosure(disclosure_id="ID-2026-001")) + assert not result.passed + assert any("COI-" in e for e in result.errors) + + +def test_empty_disclosure_id_fails(): + result = validate_coi_disclosure(_valid_disclosure(disclosure_id="")) + assert not result.passed + assert any("disclosure_id" in e for e in result.errors) + + +def test_invalid_disclosure_type_fails(): + result = validate_coi_disclosure(_valid_disclosure(disclosure_type="funder")) + assert not result.passed + assert any("disclosure_type" in e for e in result.errors) + + +def test_empty_subject_fails(): + result = validate_coi_disclosure(_valid_disclosure(subject="")) + assert not result.passed + assert any("subject" in e for e in result.errors) + + +def test_empty_related_artifact_fails(): + result = validate_coi_disclosure(_valid_disclosure(related_artifact="")) + assert not result.passed + assert any("related_artifact" in e for e in result.errors) + + +def test_invalid_relationship_type_fails(): + result = validate_coi_disclosure(_valid_disclosure(relationship_type="commercial")) + assert not result.passed + assert any("relationship_type" in e for e in result.errors) + + +def test_non_none_relationship_empty_description_fails(): + result = validate_coi_disclosure(_valid_disclosure( + relationship_type="financial", + description="", + )) + assert not result.passed + assert any("description" in e for e in result.errors) + + +def test_none_relationship_empty_description_passes(): + result = validate_coi_disclosure(_valid_disclosure( + relationship_type="none", + description="", + )) + assert result.passed + + +def test_invalid_disclosure_date_fails(): + result = validate_coi_disclosure(_valid_disclosure(disclosure_date="09-07-2026")) + assert not result.passed + assert any("disclosure_date" in e for e in result.errors) + + +def test_empty_reviewer_fails(): + result = validate_coi_disclosure(_valid_disclosure(reviewer="")) + assert not result.passed + assert any("reviewer" in e for e in result.errors) + + +def test_invalid_review_status_fails(): + result = validate_coi_disclosure(_valid_disclosure(review_status="approved")) + assert not result.passed + assert any("review_status" in e for e in result.errors) + + +def test_dry_lab_only_false_fails(): + result = validate_coi_disclosure(_valid_disclosure(dry_lab_only=False)) + assert not result.passed + assert any("dry_lab_only" in e for e in result.errors) + + +def test_financial_without_recusal_produces_warning(): + result = validate_coi_disclosure(_valid_disclosure( + relationship_type="financial", + description="Holds equity", + recusal_required=False, + )) + assert result.passed + assert any("financial" in w for w in result.warnings) + + +def test_validate_coi_dict_passes_with_valid_dict(): + d = dict( + disclosure_id="COI-2026-001", + disclosure_type="reviewer", + subject="maintainer", + related_artifact="REL-2026-001", + relationship_type="none", + description="", + disclosure_date="2026-07-09", + recusal_required=False, + reviewer="lead-maintainer", + review_status="acknowledged", + ) + result = validate_coi_dict(d) + assert result.passed + assert result.dry_lab_only is True + + +def test_validate_coi_dict_fails_with_missing_fields(): + result = validate_coi_dict({"disclosure_id": "COI-2026-001"}) + assert not result.passed + assert any("Missing" in e for e in result.errors) + + +def test_all_results_have_dry_lab_only_true(): + result = validate_coi_disclosure(_valid_disclosure()) + assert result.dry_lab_only is True + result2 = validate_coi_disclosure(_valid_disclosure(dry_lab_only=False)) + assert result2.dry_lab_only is True + + +def test_valid_disclosure_types_has_4_entries(): + assert len(VALID_DISCLOSURE_TYPES) == 4 + assert "reviewer" in VALID_DISCLOSURE_TYPES + assert "contributor" in VALID_DISCLOSURE_TYPES + assert "maintainer" in VALID_DISCLOSURE_TYPES + assert "external_advisor" in VALID_DISCLOSURE_TYPES + + +def test_valid_relationship_types_has_5_entries(): + assert len(VALID_RELATIONSHIP_TYPES) == 5 + assert "financial" in VALID_RELATIONSHIP_TYPES + assert "institutional" in VALID_RELATIONSHIP_TYPES + assert "competitive" in VALID_RELATIONSHIP_TYPES + assert "personal" in VALID_RELATIONSHIP_TYPES + assert "none" in VALID_RELATIONSHIP_TYPES diff --git a/tests/test_test_count_regression.py b/tests/test_test_count_regression.py index b09dc3ef..3d7241a7 100644 --- a/tests/test_test_count_regression.py +++ b/tests/test_test_count_regression.py @@ -2,11 +2,11 @@ import subprocess import sys -# Baseline: 3516 tests as of Loop 111 (2026-07-09) -- Phase J J3 (release request template) +# Baseline: 3536 tests as of Loop 112 (2026-07-09) -- Phase J J4 (COI disclosure template) # Note: 3 pre-existing collection errors in test_benchmark_calibration.py, # test_benchmark_charge_distribution.py, test_benchmark_cheap_enemies.py. # Raise threshold if legitimate tests are removed; lower threshold is okay. -BASELINE = 3516 +BASELINE = 3536 TOLERANCE = 0.05 # allow 5% fluctuation