diff --git a/Makefile b/Makefile index bf2fc75e..4e3c2324 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 coi-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 rotation-plan-check rotation-plan-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) @@ -855,3 +855,8 @@ 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." + +rotation-plan-check: + .venv/bin/openamp-foundry rotation-plan-check \ + --plan-json '{"entries":[{"github_handle":"maintainer","role":"primary_maintainer","backup_handle":"backup-maintainer","responsibilities":["releases","safety-policy","final-approvals"],"status":"active"},{"github_handle":"backup-maintainer","role":"secondary_maintainer","backup_handle":"maintainer","responsibilities":["day-to-day-reviews","issue-triage"],"status":"active"}]}' + @echo "Rotation plan check complete." diff --git a/docs/evidence/METRICS_CURRENT.md b/docs/evidence/METRICS_CURRENT.md index 88b48342..cef922a1 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 J4 — COI disclosure template — v0.7.2) +> **Last updated:** 2026-07-09 (Phase J J5 — maintainer rotation plan — v0.7.3) +> **New in v0.7.3:** Maintainer rotation plan (J5 — Loop 113) — `docs/governance/MAINTAINER_ROTATION_PLAN.md` with purpose, current maintainers table (3 entries covering primary, secondary, external_advisor), role definitions (4 roles: primary_maintainer, secondary_maintainer, external_advisor, contributor), bus-factor assessment (target >=2 for every critical function, CI maintenance and domain expertise currently at 1), rotation schedule (every 6 months), onboarding and offboarding checklists, linked policies. `src/openamp_foundry/governance/maintainer_rotation.py` with `MaintainerEntry` dataclass (6 fields: github_handle, role, backup_handle, responsibilities, status, dry_lab_only), `RotationPlanValidationResult` dataclass (7 fields: passed, errors, warnings, maintainer_count, critical_role_coverage, bus_factor_sufficient, dry_lab_only), `VALID_ROLES` (4: primary_maintainer, secondary_maintainer, external_advisor, contributor), `CRITICAL_ROLES` (2: primary_maintainer, secondary_maintainer), `VALID_STATUSES` (4: active, on_leave, emeritus, departing), `validate_maintainer_entry()` (6 checks), `validate_rotation_plan()` (aggregates entry validation + bus-factor coverage: missing critical role is error, single coverage is warning), `validate_rotation_plan_dict()` (dict input with missing-entries-fields guard). CLI (`openamp-foundry rotation-plan-check`) with `--plan-json`, `--format text|json`. `make rotation-plan-check` target. 21 tests. Dry-lab only. Project durability is now machine-validated. > **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. @@ -100,7 +101,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.2 (J4 — COI disclosure template) +> **Pipeline version:** v0.7.3 (J5 — maintainer rotation plan) > **Branch:** main --- diff --git a/docs/governance/MAINTAINER_ROTATION_PLAN.md b/docs/governance/MAINTAINER_ROTATION_PLAN.md new file mode 100644 index 00000000..5e4ea4f7 --- /dev/null +++ b/docs/governance/MAINTAINER_ROTATION_PLAN.md @@ -0,0 +1,120 @@ +# Maintainer Rotation and Bus-Factor Plan + +## Purpose + +OpenAMP Foundry's project durability depends on **bus-factor coverage** — the number of maintainers who must be lost before project operations stop. Every critical function must have at least two capable people. + +This document: + +- Lists current maintainers, their roles, and their backups. +- Defines what each role requires and is responsible for. +- Assesses bus-factor risk for every critical function (target: >=2). +- Describes the rotation schedule for reviewing assignments. +- Provides onboarding and offboarding checklists. + +**Who should read this:** all current maintainers, contributors considering a maintainer role, and downstream consumers who need to know the project is durable. + +**How to use:** Keep this document in the repo. Update it whenever a maintainer joins, leaves, or changes responsibilities. Review at least every 6 months. + +## Current Maintainers + +| GitHub handle | Role | Backup | Responsibilities | +|---|---|---|---| +| `lead-maintainer` | primary_maintainer | `backup-maintainer` | Release approvals, safety policy ownership, final PR approvals, phase planning, external liaison | +| `backup-maintainer` | secondary_maintainer | `lead-maintainer` | Day-to-day PR reviews, issue triage, CI maintenance, onboarding new contributors | +| `domain-advisor` | external_advisor | — | Domain-expertise consultation, D-class review participation, benchmark design input | + +## Role Definitions + +### primary_maintainer + +- Owns the release process and safety policy. +- Provides final approval on PRs after at least one other review. +- Represents the project externally (partner discussions, governance). +- **Requires:** full commit access, safety-policy familiarity, release-checklist fluency, at least 6 months as secondary. + +### secondary_maintainer + +- Covers for primary during absences (up to full release authority on pre-planned leave). +- Owns day-to-day PR review queue and issue triage. +- Maintains CI, documentation, and onboarding processes. +- **Requires:** PR review experience, CI troubleshooting skills, governance-doc familiarity. + +### external_advisor + +- Provides domain expertise (antimicrobial peptide biology, assay design, computational chemistry). +- Participates in D-class (safety-significant) reviews. +- Does not hold commit access or release authority. +- **Requires:** relevant domain expertise; no commit access needed. + +### contributor + +- Submits PRs, participates in reviews, reports issues. +- Does not hold release keys or have offboarding responsibilities. +- **Requires:** familiarity with contribution guide and safety policy. + +## Bus-Factor Assessment + +| Function | Responsible | Backup | Bus factor | +|---|---|---|---| +| Release approval | `lead-maintainer` | `backup-maintainer` | 2 | +| Safety policy | `lead-maintainer` | `backup-maintainer` | 2 | +| PR review (final) | `lead-maintainer` | `backup-maintainer` | 2 | +| PR review (day-to-day) | `backup-maintainer` | `lead-maintainer` | 2 | +| Issue triage | `backup-maintainer` | `lead-maintainer` | 2 | +| CI maintenance | `backup-maintainer` | — | **1** | +| Domain expertise | `domain-advisor` | — | **1** | + +**Target:** >= 2 for every critical function. CI maintenance and domain expertise currently have bus-factor 1 and should be improved when new maintainers join. + +## Rotation Schedule + +Roles are reviewed every **6 months** (March and September). The review process: + +1. Primary sends a rotation-review thread to the maintainer mailing list. +2. Each maintainer confirms they want to continue in their current role or proposes a change. +3. Changes are documented in this file as a PR. +4. If the primary maintainer steps down, the secondary takes over and a new secondary is recruited. + +Emergency handoffs (unexpected departure) follow the backup chain immediately. The offboarding checklist (below) must be completed within 14 days. + +## Onboarding Checklist + +A new maintainer must complete the following before taking a role: + +- [ ] Read `docs/governance/GOVERNANCE.md`. +- [ ] Read `docs/governance/RELEASE_CHECKLIST.md`. +- [ ] Read `docs/governance/COI_DISCLOSURE_TEMPLATE.md`. +- [ ] Read `docs/governance/DECISION_LOG.md`. +- [ ] Read `docs/governance/RELEASE_REQUEST_TEMPLATE.md`. +- [ ] Read `docs/governance/MAINTAINER_ROTATION_PLAN.md` (this document). +- [ ] Read `SAFETY.md`. +- [ ] Read `RESPONSIBLE_USE.md`. +- [ ] Confirm they have read and agree to the project's safety and responsible-use policies. +- [ ] Add their GitHub handle, role, and backup to the Current Maintainers table in this file. +- [ ] Open a PR, get approval from at least one existing maintainer. + +## Offboarding Checklist + +A departing maintainer must: + +- [ ] Notify the maintainer mailing list at least 14 days before departure (or as soon as practical for emergencies). +- [ ] Rotate any shared secrets, SSH keys, or service credentials they held. +- [ ] Document their current in-progress work (open PRs, issues assigned, CI state). +- [ ] Update the Current Maintainers table: set their status to "departing" and remove or reassign their responsibilities. +- [ ] Transfer any domain-specific knowledge to their backup (if one exists). +- [ ] Ensure this file is updated before they lose access. + +## Scope + +This document is a governance artifact, not a legal determination. Maintainer roles are project responsibilities, not employment relationships. Bus-factor assessment is a project-durability estimate, not a security guarantee. + +## Linked Policies + +- `docs/governance/GOVERNANCE.md` +- `docs/governance/RELEASE_CHECKLIST.md` +- `docs/governance/COI_DISCLOSURE_TEMPLATE.md` +- `docs/governance/DECISION_LOG.md` +- `docs/governance/RELEASE_REQUEST_TEMPLATE.md` +- `SAFETY.md` +- `RESPONSIBLE_USE.md` diff --git a/docs/research/ROADMAP.md b/docs/research/ROADMAP.md index 0a04c099..10971ddd 100644 --- a/docs/research/ROADMAP.md +++ b/docs/research/ROADMAP.md @@ -1,5 +1,87 @@ # Roadmap +## v0.7.3 — Loop 113: Phase J J5 — Maintainer Rotation Plan ✓ (2026-07-09) + +`docs/governance/MAINTAINER_ROTATION_PLAN.md` with maintainer rotation and +bus-factor plan (purpose, current maintainers table with 3 entries covering +primary_maintainer, secondary_maintainer, external_advisor, role definitions +for 4 roles, bus-factor assessment with target >=2 per critical function, +rotation schedule every 6 months, onboarding and offboarding checklists, +linked policies). + +`src/openamp_foundry/governance/maintainer_rotation.py` with `MaintainerEntry` +dataclass (6 fields: github_handle, role, backup_handle, responsibilities, +status, dry_lab_only), `RotationPlanValidationResult` dataclass (7 fields: +passed, errors, warnings, maintainer_count, critical_role_coverage, +bus_factor_sufficient, dry_lab_only), `VALID_ROLES` (4: primary_maintainer, +secondary_maintainer, external_advisor, contributor), `CRITICAL_ROLES` (2: +primary_maintainer, secondary_maintainer), `VALID_STATUSES` (4: active, +on_leave, emeritus, departing), `validate_maintainer_entry()` (6 checks: +non-empty github_handle, valid role, critical role requires backup_handle, +non-empty responsibilities, valid status, dry_lab_only=True), +`validate_rotation_plan()` (aggregates entry validation + bus-factor +coverage: missing critical role is error, single coverage is warning), +`validate_rotation_plan_dict()` (dict input with missing-fields guard). + +CLI (`openamp-foundry rotation-plan-check`) with `--plan-json` (required), +`--format text|json`. Handler `_run_rotation_plan_check` in reports.py. + +`make rotation-plan-check` target. 21 tests. **3557 total.** + +Maintainer rotation and bus-factor coverage is now machine-validated — the +project can detect when critical roles lack backups. + +Changes: +- `docs/governance/MAINTAINER_ROTATION_PLAN.md` (J5) — Maintainer rotation + and bus-factor plan with purpose, current maintainers table (3 entries), + role definitions (4 roles), bus-factor assessment, rotation schedule + (every 6 months), onboarding checklist (11 items), offboarding checklist + (7 items), linked policies. +- `src/openamp_foundry/governance/maintainer_rotation.py` (J5) — Core module + with `MaintainerEntry` (6 fields), `RotationPlanValidationResult` (7 fields, + dry_lab_only=True), `VALID_ROLES` (4), `CRITICAL_ROLES` (2), + `VALID_STATUSES` (4), `validate_maintainer_entry()` (6 checks), + `validate_rotation_plan()` (bus-factor coverage: missing critical role is + error, single coverage is warning), `validate_rotation_plan_dict()` (dict + input with missing-fields guard). +- `tests/governance/test_maintainer_rotation.py` (J5) — 21 tests covering: + valid plan passes, empty entries fails, empty github_handle fails, invalid + role fails, critical role without backup fails, empty responsibilities fails, + invalid status fails, dry_lab_only=False fails, no active primary maintainer + fails, no active secondary maintainer fails, single primary maintainer warns, + single secondary maintainer warns, validate_rotation_plan_dict missing + 'entries' key fails, validate_rotation_plan_dict valid dict passes, + validate_rotation_plan_dict missing entry fields fails, all results + dry_lab_only=True, VALID_ROLES has 4 entries, CRITICAL_ROLES has 2 entries, + contributor role valid, on_leave status valid, emeritus status valid. +- `src/openamp_foundry/cli/main.py` (J5) — Registered `rotation-plan-check` + subcommand with `--plan-json`, `--format` flags. Added import and dispatch. +- `src/openamp_foundry/cli/commands/reports.py` (J5) — Added + `_run_rotation_plan_check()` CLI handler with JSON parsing, + `validate_rotation_plan_dict()` call, text and JSON output, exit code 3 on + validation failure. +- `Makefile` (J5) — Added `rotation-plan-check` target with demo invocation + using maintainer and backup-maintainer. Added to `.PHONY`. +- `docs/evidence/METRICS_CURRENT.md` (J5) — v0.7.3 J5 changelog. Pipeline + version: v0.7.3. Test count: 3557. +- `tests/test_test_count_regression.py` — baseline updated to 3557. + +Honest boundaries: +- Maintainer rotation validation checks structural and policy requirements only. + It does not verify that the listed maintainers actually have the skills or + availability to perform their roles. +- `dry_lab_only: true` is a const field on all dataclasses — rotation plan + validation is a governance artifact, not a legal determination. +- Bus-factor assessment is a project-durability estimate, not a security + guarantee. A bus-factor of 2 means two named people can cover a function, + but both might be unavailable simultaneously. +- The validator cannot detect unlisted critical dependencies (e.g., institutional + knowledge, CI secrets, domain expertise held by only one person). +- The rotation schedule is a policy declaration; this validator does not track + whether rotations actually occurred. +- Onboarding and offboarding checklists are documentation and guidance — they + do not replace judgment about whether a new maintainer is ready. + ## v0.7.2 — Loop 112: Phase J J4 — COI Disclosure Template ✓ (2026-07-09) `docs/governance/COI_DISCLOSURE_TEMPLATE.md` with structured COI disclosure diff --git a/src/openamp_foundry/cli/commands/reports.py b/src/openamp_foundry/cli/commands/reports.py index 977938e3..b0125c0b 100644 --- a/src/openamp_foundry/cli/commands/reports.py +++ b/src/openamp_foundry/cli/commands/reports.py @@ -2597,3 +2597,53 @@ def _run_adoption_scorecard(args: argparse.Namespace) -> int: print("not biological validity.") return 0 + + +def _run_rotation_plan_check(args: argparse.Namespace) -> int: + """Validate a maintainer rotation plan for bus-factor coverage.""" + from openamp_foundry.governance.maintainer_rotation import validate_rotation_plan_dict + + try: + plan_dict = json.loads(args.plan_json) + except (json.JSONDecodeError, TypeError) as e: + print(json.dumps({"status": "error", "error": f"Invalid --plan-json: {e}"})) + return 2 + + if not isinstance(plan_dict, dict): + print(json.dumps({"status": "error", "error": "--plan-json must be a JSON object"})) + return 2 + + result = validate_rotation_plan_dict(plan_dict) + output_format = getattr(args, "format", "text") + + if output_format == "json": + print(json.dumps({ + "passed": result.passed, + "errors": result.errors, + "warnings": result.warnings, + "maintainer_count": result.maintainer_count, + "critical_role_coverage": result.critical_role_coverage, + "bus_factor_sufficient": result.bus_factor_sufficient, + "dry_lab_only": result.dry_lab_only, + }, indent=2)) + else: + print("Rotation Plan Check") + print("=" * 60) + print(f"Status: {'PASS' if result.passed else 'FAIL'}") + print(f"Maintainer count: {result.maintainer_count}") + print(f"Bus-factor sufficient: {result.bus_factor_sufficient}") + print(f"Critical role coverage: {result.critical_role_coverage}") + 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. Rotation plan validation is a computational") + print("governance check, not a legal determination.") + + return 0 if result.passed else 3 diff --git a/src/openamp_foundry/cli/main.py b/src/openamp_foundry/cli/main.py index 87b9f766..e2b17fbc 100644 --- a/src/openamp_foundry/cli/main.py +++ b/src/openamp_foundry/cli/main.py @@ -11,6 +11,7 @@ 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 +from openamp_foundry.cli.commands.reports import _run_rotation_plan_check import argparse import json @@ -1877,6 +1878,24 @@ def build_parser() -> argparse.ArgumentParser: help="Output format (default: text).", ) + # ── Maintainer rotation plan check (Phase J J5) ────────────────── + rpc = sub.add_parser( + "rotation-plan-check", + help=( + "Validate a maintainer rotation plan for bus-factor " + "coverage. Dry-lab only." + ), + ) + rpc.add_argument( + "--plan-json", type=str, required=True, + help="JSON string of a rotation plan dict (required).", + ) + rpc.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", @@ -2155,6 +2174,9 @@ def main(argv: list[str] | None = None) -> int: if args.command == "coi-check": return _run_coi_check(args) + if args.command == "rotation-plan-check": + return _run_rotation_plan_check(args) + parser.error("unknown command") return 2 diff --git a/src/openamp_foundry/governance/maintainer_rotation.py b/src/openamp_foundry/governance/maintainer_rotation.py new file mode 100644 index 00000000..b22ea397 --- /dev/null +++ b/src/openamp_foundry/governance/maintainer_rotation.py @@ -0,0 +1,160 @@ +"""Maintainer rotation validator — validates maintainer rotation plan entries. + +Ensures the bus-factor plan has required fields and minimum coverage. +Dry-lab only. +""" +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +VALID_ROLES: set[str] = { + "primary_maintainer", "secondary_maintainer", "external_advisor", "contributor" +} + +VALID_STATUSES: set[str] = { + "active", "on_leave", "emeritus", "departing" +} + +CRITICAL_ROLES: set[str] = { + "primary_maintainer", "secondary_maintainer" +} + + +@dataclass +class MaintainerEntry: + github_handle: str + role: str # from VALID_ROLES + backup_handle: str # GitHub handle of backup (required for critical roles) + responsibilities: list[str] # must not be empty + status: str # from VALID_STATUSES + dry_lab_only: bool = True + + +@dataclass +class RotationPlanValidationResult: + passed: bool + errors: list[str] + warnings: list[str] + maintainer_count: int + critical_role_coverage: dict[str, int] + bus_factor_sufficient: bool + dry_lab_only: bool = True + + +def validate_maintainer_entry(entry: MaintainerEntry) -> list[str]: + errors: list[str] = [] + if not entry.github_handle: + errors.append("github_handle must not be empty") + if entry.role not in VALID_ROLES: + errors.append(f"role={entry.role!r} not in {sorted(VALID_ROLES)}") + if entry.role in CRITICAL_ROLES and not entry.backup_handle: + errors.append(f"backup_handle required for role={entry.role!r}") + if not entry.responsibilities: + errors.append("responsibilities must not be empty") + if entry.status not in VALID_STATUSES: + errors.append(f"status={entry.status!r} not in {sorted(VALID_STATUSES)}") + if not entry.dry_lab_only: + errors.append("dry_lab_only must be True") + return errors + + +def validate_rotation_plan(entries: list[MaintainerEntry]) -> RotationPlanValidationResult: + errors: list[str] = [] + warnings: list[str] = [] + + if not entries: + errors.append("rotation plan must have at least one maintainer entry") + return RotationPlanValidationResult( + passed=False, + errors=errors, + warnings=warnings, + maintainer_count=0, + critical_role_coverage={}, + bus_factor_sufficient=False, + dry_lab_only=True, + ) + + for entry in entries: + entry_errors = validate_maintainer_entry(entry) + errors.extend(entry_errors) + + active_entries = [e for e in entries if e.status == "active"] + critical_coverage: dict[str, int] = {role: 0 for role in CRITICAL_ROLES} + for entry in active_entries: + if entry.role in CRITICAL_ROLES: + critical_coverage[entry.role] = critical_coverage.get(entry.role, 0) + 1 + + bus_factor_ok = True + for role in CRITICAL_ROLES: + count = critical_coverage.get(role, 0) + if count == 0: + errors.append(f"no active maintainer with role={role!r}") + bus_factor_ok = False + elif count == 1: + warnings.append(f"only 1 active {role} — bus factor risk; add a backup") + + return RotationPlanValidationResult( + passed=len(errors) == 0, + errors=errors, + warnings=warnings, + maintainer_count=len(entries), + critical_role_coverage=critical_coverage, + bus_factor_sufficient=bus_factor_ok, + dry_lab_only=True, + ) + + +def validate_rotation_plan_dict(d: dict[str, Any]) -> RotationPlanValidationResult: + required_entry_fields = [ + "github_handle", "role", "backup_handle", "responsibilities", "status" + ] + if "entries" not in d: + return RotationPlanValidationResult( + passed=False, + errors=["Missing required key: 'entries'"], + warnings=[], + maintainer_count=0, + critical_role_coverage={}, + bus_factor_sufficient=False, + dry_lab_only=True, + ) + entries_raw = d["entries"] + if not isinstance(entries_raw, list): + return RotationPlanValidationResult( + passed=False, + errors=["'entries' must be a list"], + warnings=[], + maintainer_count=0, + critical_role_coverage={}, + bus_factor_sufficient=False, + dry_lab_only=True, + ) + + entries: list[MaintainerEntry] = [] + parse_errors: list[str] = [] + for i, raw in enumerate(entries_raw): + missing = [f for f in required_entry_fields if f not in raw] + if missing: + parse_errors.append(f"entry[{i}] missing fields: {missing}") + continue + entries.append(MaintainerEntry( + github_handle=raw.get("github_handle", ""), + role=raw.get("role", ""), + backup_handle=raw.get("backup_handle", ""), + responsibilities=raw.get("responsibilities", []), + status=raw.get("status", ""), + dry_lab_only=raw.get("dry_lab_only", True), + )) + + if parse_errors: + return RotationPlanValidationResult( + passed=False, + errors=parse_errors, + warnings=[], + maintainer_count=0, + critical_role_coverage={}, + bus_factor_sufficient=False, + dry_lab_only=True, + ) + + return validate_rotation_plan(entries) diff --git a/tests/governance/test_maintainer_rotation.py b/tests/governance/test_maintainer_rotation.py new file mode 100644 index 00000000..7b040bc8 --- /dev/null +++ b/tests/governance/test_maintainer_rotation.py @@ -0,0 +1,216 @@ +"""Tests for maintainer rotation plan validator.""" +from __future__ import annotations + +from openamp_foundry.governance.maintainer_rotation import ( + CRITICAL_ROLES, + MaintainerEntry, + VALID_ROLES, + validate_maintainer_entry, + validate_rotation_plan, + validate_rotation_plan_dict, +) + + +def _valid_primary() -> MaintainerEntry: + return MaintainerEntry( + github_handle="lead-maintainer", + role="primary_maintainer", + backup_handle="backup-maintainer", + responsibilities=["releases", "safety-policy", "final-approvals"], + status="active", + ) + + +def _valid_secondary() -> MaintainerEntry: + return MaintainerEntry( + github_handle="backup-maintainer", + role="secondary_maintainer", + backup_handle="lead-maintainer", + responsibilities=["day-to-day-reviews", "issue-triage"], + status="active", + ) + + +def test_valid_plan_passes(): + entries = [_valid_primary(), _valid_secondary()] + result = validate_rotation_plan(entries) + assert result.passed + + +def test_empty_entries_fails(): + result = validate_rotation_plan([]) + assert not result.passed + assert any("at least one maintainer" in e for e in result.errors) + + +def test_empty_github_handle_fails(): + entry = _valid_primary() + entry.github_handle = "" + errors = validate_maintainer_entry(entry) + assert any("github_handle must not be empty" in e for e in errors) + + +def test_invalid_role_fails(): + entry = _valid_primary() + entry.role = "not_a_valid_role" + errors = validate_maintainer_entry(entry) + assert any("not in" in e for e in errors) + + +def test_critical_role_without_backup_fails(): + entry = _valid_primary() + entry.backup_handle = "" + errors = validate_maintainer_entry(entry) + assert any("backup_handle required" in e for e in errors) + + +def test_empty_responsibilities_fails(): + entry = _valid_primary() + entry.responsibilities = [] + errors = validate_maintainer_entry(entry) + assert any("responsibilities must not be empty" in e for e in errors) + + +def test_invalid_status_fails(): + entry = _valid_primary() + entry.status = "invalid_status" + errors = validate_maintainer_entry(entry) + assert any("not in" in e for e in errors) + + +def test_dry_lab_only_false_fails(): + entry = _valid_primary() + entry.dry_lab_only = False + errors = validate_maintainer_entry(entry) + assert any("dry_lab_only must be True" in e for e in errors) + + +def test_no_active_primary_maintainer_fails(): + entries = [_valid_secondary()] + result = validate_rotation_plan(entries) + assert not result.passed + assert any("no active maintainer with role='primary_maintainer'" in e for e in result.errors) + + +def test_no_active_secondary_maintainer_fails(): + entries = [_valid_primary()] + result = validate_rotation_plan(entries) + assert not result.passed + assert any("no active maintainer with role='secondary_maintainer'" in e for e in result.errors) + + +def test_single_primary_produces_warning(): + entry = _valid_primary() + entry.status = "active" + sec = _valid_secondary() + result = validate_rotation_plan([entry, sec]) + assert result.passed + assert any("only 1 active primary_maintainer" in w for w in result.warnings) + + +def test_single_secondary_produces_warning(): + # Two primary maintainers, only one secondary -> warning on secondary + entries = [ + MaintainerEntry( + github_handle="lead-maintainer", + role="primary_maintainer", + backup_handle="backup-maintainer", + responsibilities=["releases"], + status="active", + ), + MaintainerEntry( + github_handle="co-maintainer", + role="primary_maintainer", + backup_handle="lead-maintainer", + responsibilities=["benchmarks"], + status="active", + ), + _valid_secondary(), + ] + result = validate_rotation_plan(entries) + assert result.passed + assert any("only 1 active secondary_maintainer" in w for w in result.warnings) + + +def test_validate_dict_missing_entries_key_fails(): + result = validate_rotation_plan_dict({}) + assert not result.passed + assert any("Missing required key" in e for e in result.errors) + + +def test_validate_dict_passes_with_valid_dict(): + d = { + "entries": [ + { + "github_handle": "lead-maintainer", + "role": "primary_maintainer", + "backup_handle": "backup-maintainer", + "responsibilities": ["releases", "safety-policy"], + "status": "active", + }, + { + "github_handle": "backup-maintainer", + "role": "secondary_maintainer", + "backup_handle": "lead-maintainer", + "responsibilities": ["day-to-day-reviews"], + "status": "active", + }, + ] + } + result = validate_rotation_plan_dict(d) + assert result.passed + + +def test_validate_dict_missing_entry_fields_fails(): + d = { + "entries": [ + { + "github_handle": "lead-maintainer", + # missing role, backup_handle, responsibilities, status + }, + ] + } + result = validate_rotation_plan_dict(d) + assert not result.passed + assert any("missing fields" in e for e in result.errors) + + +def test_all_results_have_dry_lab_only_true(): + entries = [_valid_primary(), _valid_secondary()] + result = validate_rotation_plan(entries) + assert result.dry_lab_only is True + + +def test_valid_roles_has_4_entries(): + assert len(VALID_ROLES) == 4 + + +def test_critical_roles_has_2_entries(): + assert len(CRITICAL_ROLES) == 2 + + +# Additional tests to ensure coverage +def test_contributor_valid(): + entry = MaintainerEntry( + github_handle="contributor-user", + role="contributor", + backup_handle="", + responsibilities=["submit-PRs"], + status="active", + ) + errors = validate_maintainer_entry(entry) + assert not errors + + +def test_on_leave_status_valid(): + entry = _valid_primary() + entry.status = "on_leave" + errors = validate_maintainer_entry(entry) + assert not errors + + +def test_emeritus_status_valid(): + entry = _valid_primary() + entry.status = "emeritus" + errors = validate_maintainer_entry(entry) + assert not errors diff --git a/tests/test_test_count_regression.py b/tests/test_test_count_regression.py index 3d7241a7..2626c45a 100644 --- a/tests/test_test_count_regression.py +++ b/tests/test_test_count_regression.py @@ -2,11 +2,11 @@ import subprocess import sys -# Baseline: 3536 tests as of Loop 112 (2026-07-09) -- Phase J J4 (COI disclosure template) +# Baseline: 3557 tests as of Loop 113 (2026-07-09) -- Phase J J5 (maintainer rotation plan) # 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 = 3536 +BASELINE = 3557 TOLERANCE = 0.05 # allow 5% fluctuation