From 87be21d9d701566a74700281e336a7a831a8f549 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Thu, 9 Jul 2026 17:45:15 +0700 Subject: [PATCH] feat: Phase J J7 citation and reuse guide -- ecosystem clarity (#807) --- Makefile | 7 +- docs/evidence/METRICS_CURRENT.md | 6 +- docs/governance/CITATION_AND_REUSE_GUIDE.md | 92 ++++++++++ docs/research/ROADMAP.md | 25 +++ src/openamp_foundry/cli/commands/reports.py | 34 ++++ src/openamp_foundry/cli/main.py | 22 +++ .../governance/citation_policy.py | 156 +++++++++++++++++ tests/governance/test_citation_policy.py | 162 ++++++++++++++++++ tests/test_test_count_regression.py | 4 +- 9 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 docs/governance/CITATION_AND_REUSE_GUIDE.md create mode 100644 src/openamp_foundry/governance/citation_policy.py create mode 100644 tests/governance/test_citation_policy.py diff --git a/Makefile b/Makefile index 817d400d..ceddcdeb 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 rotation-plan-check security-report-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 security-report-check citation-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) @@ -865,3 +865,8 @@ security-report-check: .venv/bin/openamp-foundry security-report-check \ --report-json '{"report_id":"SEC-2026-001","severity":"medium","category":"dependency_vulnerability","description":"CVE-2026-1234 in requests library","affected_version":"v0.7.3","reporter_handle":"anonymous","report_date":"2026-07-09","status":"acknowledged"}' @echo "Security report check complete." + +citation-check: + .venv/bin/openamp-foundry citation-check \ + --citation-json '{"artifact_id":"OAMP-v0.7.5","citation_type":"software","title":"OpenAMP Foundry","version":"v0.7.5","authors":["Open-Problem-Lab"],"year":"2026","license_identifier":"MIT","reuse_class":"open","url":"https://github.com/Open-Problem-Lab/OpenAMP-Foundry","bibtex_key":"openamp_foundry_2026"}' + @echo "Citation check complete." diff --git a/docs/evidence/METRICS_CURRENT.md b/docs/evidence/METRICS_CURRENT.md index 06898ae2..725e60b2 100644 --- a/docs/evidence/METRICS_CURRENT.md +++ b/docs/evidence/METRICS_CURRENT.md @@ -5,7 +5,9 @@ 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 J6 — security policy — v0.7.4) +> **Last updated:** 2026-07-09 (Phase J J7 — citation and reuse guide — v0.7.5) +> **New in v0.7.5:** Citation and reuse guide (J7 — Loop 115) — docs/governance/CITATION_AND_REUSE_GUIDE.md with citation formats (inline, BibTeX), reuse table (4 artifact types, open/attribution_required/contact_required/restricted classes), attribution requirements, honest-use boundary. src/openamp_foundry/governance/citation_policy.py with CitationEntry (11 fields: artifact_id, citation_type, title, version, authors, year, license_identifier, reuse_class, url, bibtex_key, dry_lab_only), CitationValidationResult (6 fields, dry_lab_only=True), VALID_CITATION_TYPES (4: dataset, method, schema, software), VALID_REUSE_CLASSES (4: attribution_required, contact_required, open, restricted), VALID_LICENSE_IDENTIFIERS (5: Apache-2.0, CC-BY-4.0, CC-BY-NC-4.0, MIT, Proprietary), validate_citation_entry() (9 checks: non-empty artifact_id, valid citation_type, non-empty title, non-empty version, non-empty authors, 4-digit year, valid license_identifier, valid reuse_class, dry_lab_only must be True; restricted warns, contact_required+no-url warns, empty bibtex_key warns), validate_citation_dict() (8 required fields guard). CLI (openamp-foundry citation-check) with --citation-json, --format text|json. make citation-check target. 24 tests. Ecosystem clarity: citation entries are now machine-validated. +> > **New in v0.7.4:** Security policy (J6 — Loop 114) — `docs/governance/SECURITY_POLICY.md` with private vulnerability reporting process, response timeline (48h acknowledgment, 30d patch), severity classification (critical/high/medium/low), 5 vulnerability categories (code_vulnerability, secret_leakage, dependency_vulnerability, safety_guardrail_bypass, dual_use_risk), out-of-scope items, disclosure process. `src/openamp_foundry/governance/security_policy.py` with `VulnerabilityReport` dataclass (9 fields: report_id, severity, category, description, affected_version, reporter_handle, report_date, status, dry_lab_only), `SecurityReportValidationResult` dataclass (6 fields, dry_lab_only=True), `VALID_SEVERITY_LEVELS` (4: critical, high, medium, low), `VALID_VULNERABILITY_CATEGORIES` (5: code_vulnerability, secret_leakage, dependency_vulnerability, safety_guardrail_bypass, dual_use_risk), `VALID_REPORT_STATUSES` (6: received, acknowledged, under_review, patched, disclosed, not_applicable), `validate_vulnerability_report()` (9 checks: report_id SEC- prefix, valid severity, valid category, non-empty description, non-empty affected_version, non-empty reporter_handle, YYYY-MM-DD date, valid status, dry_lab_only must be True; critical+received warning, safety_guardrail_bypass warning), `validate_report_dict()` (dict input with 8 required fields guard). CLI (`openamp-foundry security-report-check`) with `--report-json`, `--format text|json`. `make security-report-check` target. 18 tests. Dry-lab only. Private vulnerability reporting now has a validated structure and documented process. > **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. @@ -102,7 +104,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.4 (J6 — security policy) +> **Pipeline version:** v0.7.5 (J7 — citation and reuse guide) > **Branch:** main --- diff --git a/docs/governance/CITATION_AND_REUSE_GUIDE.md b/docs/governance/CITATION_AND_REUSE_GUIDE.md new file mode 100644 index 00000000..99c373d4 --- /dev/null +++ b/docs/governance/CITATION_AND_REUSE_GUIDE.md @@ -0,0 +1,92 @@ +# Citation and Reuse Guide + +OpenAMP Foundry is an open research infrastructure project. This guide explains +how to cite the project and what you can reuse under which conditions. + +## How to Cite + +If you use OpenAMP Foundry in academic work, please cite: + +**Inline citation:** +OpenAMP Foundry (Open-Problem-Lab, 2026), available at +https://github.com/Open-Problem-Lab/OpenAMP-Foundry + +**BibTeX:** +```bibtex +@software{openamp_foundry_2026, + author = {{Open-Problem-Lab}}, + title = {{OpenAMP Foundry: Verification-First Antimicrobial Peptide Discovery Infrastructure}}, + year = {2026}, + url = {https://github.com/Open-Problem-Lab/OpenAMP-Foundry}, + note = {Dry-lab computational pipeline. Outputs are not biological proof.} +} +``` + +**Schema or artifact citation (add version):** +OpenAMP Foundry Artifact v0.7.x (Open-Problem-Lab, 2026). +Include the exact artifact version from `artifact_id` in the artifact manifest. + +## What You Can Reuse + +| Artifact type | Reuse class | License | Attribution required | +|----------------|-----------------------|---------------|----------------------| +| Source code | open | MIT | Yes — preserve copyright notice | +| JSON schemas | open | MIT | Yes — preserve copyright notice | +| Documentation | attribution_required | CC-BY-4.0 | Yes — cite OpenAMP Foundry | +| Benchmark data | attribution_required | CC-BY-4.0 | Yes — cite OpenAMP Foundry | +| Model weights | contact_required | CC-BY-NC-4.0 | Yes + contact maintainers | +| Candidate lists| restricted | Proprietary | Contact maintainers | + +## Attribution Requirements + +For **open** and **attribution_required** artifacts: +1. Include a clear reference to OpenAMP Foundry in your work. +2. Preserve the original copyright notice in any derivative files. +3. Do not imply endorsement by the OpenAMP Foundry team. +4. Retain the `dry_lab_only` label in any re-published computational outputs. + +For **contact_required** artifacts: +1. Contact maintainers before use (see GOVERNANCE.md for contact information). +2. Attribution requirements as above apply. +3. Obtain written confirmation before publishing results derived from these artifacts. + +For **restricted** artifacts: +1. Do not reuse without explicit written permission from maintainers. +2. Contact maintainers to discuss licensing terms. + +## What NOT to Reuse Without Permission + +- Candidate sequences in restricted status (pre-publication pipeline outputs) +- Lab synthesis orders or target prioritization lists +- Internal review committee deliberations +- Any artifact marked `release_status: restricted` in the artifact manifest + +## Honest Use Boundary + +Computational outputs from OpenAMP Foundry are **dry-lab candidates only**. +Do not describe them as "drugs," "proven antimicrobials," or "safe in humans." +Use language like: "computationally nominated candidates," "dry-lab selected sequences," +"pending wet-lab validation." + +## Machine-Validated Citation Entries + +Citation entries can be validated with: +```bash +openamp-foundry citation-check --citation-json '{"artifact_id": "...", ...}' +``` + +See `src/openamp_foundry/governance/citation_policy.py` for the CitationEntry schema. + +## Contact + +For custom licensing, commercial use inquiries, or questions about reuse: +open an issue at https://github.com/Open-Problem-Lab/OpenAMP-Foundry/issues +with the label `governance`. + +## Linked Policies + +- [GOVERNANCE.md](../../GOVERNANCE.md) — project governance structure +- [DATA_LICENSE_NOTICE.md](../../DATA_LICENSE_NOTICE.md) — data licensing details +- [RESPONSIBLE_USE.md](../../RESPONSIBLE_USE.md) — responsible use policy +- [MODEL_RELEASE_POLICY.md](../../MODEL_RELEASE_POLICY.md) — model release policy +- [SECURITY_POLICY.md](SECURITY_POLICY.md) — vulnerability reporting diff --git a/docs/research/ROADMAP.md b/docs/research/ROADMAP.md index b0cdc766..ed349b48 100644 --- a/docs/research/ROADMAP.md +++ b/docs/research/ROADMAP.md @@ -1,5 +1,30 @@ # Roadmap +## v0.7.5 — Loop 115: Phase J J7 — Citation and Reuse Guide ✓ (2026-07-09) + +`docs/governance/CITATION_AND_REUSE_GUIDE.md` with citation formats (inline, +BibTeX), reuse table (4 artifact types with open/attribution_required/ +contact_required/restricted classes), attribution requirements, honest-use +boundary (dry-lab outputs only), contact information, linked policies. + +`src/openamp_foundry/governance/citation_policy.py` with +`CitationEntry` dataclass (11 fields: artifact_id, citation_type, title, +version, authors, year, license_identifier, reuse_class, url, bibtex_key, +dry_lab_only), `CitationValidationResult` dataclass (6 fields, +dry_lab_only=True), `VALID_CITATION_TYPES` (4: dataset, method, schema, +software), `VALID_REUSE_CLASSES` (4: attribution_required, contact_required, +open, restricted), `VALID_LICENSE_IDENTIFIERS` (5: Apache-2.0, CC-BY-4.0, +CC-BY-NC-4.0, MIT, Proprietary), `validate_citation_entry()` (9 checks ++ 3 warning conditions), `validate_citation_dict()` (8 required fields guard). + +CLI (`openamp-foundry citation-check`) with `--citation-json` (required), +`--format text|json`. Handler `_run_citation_check` in reports.py. + +`make citation-check` target. 24 tests. **3599 total.** + +Ecosystem clarity: citation entries are machine-validated, reuse classes are +explicit, and the honest-use boundary is documented in the guide. + ## v0.7.4 — Loop 114: Phase J J6 — Security Policy ✓ (2026-07-09) `docs/governance/SECURITY_POLICY.md` with private vulnerability reporting diff --git a/src/openamp_foundry/cli/commands/reports.py b/src/openamp_foundry/cli/commands/reports.py index 1fda1713..30f41d39 100644 --- a/src/openamp_foundry/cli/commands/reports.py +++ b/src/openamp_foundry/cli/commands/reports.py @@ -2681,3 +2681,37 @@ def _run_security_report_check(args: argparse.Namespace) -> int: print(" Report validated. Ready for maintainer review.") return 0 if result.passed else 3 + + +def _run_citation_check(args: argparse.Namespace) -> int: + """Validate a citation entry.""" + import json as _json + from openamp_foundry.governance.citation_policy import validate_citation_dict + + try: + d = _json.loads(args.citation_json) + except _json.JSONDecodeError as exc: + print(_json.dumps({"status": "error", "error": f"Invalid JSON: {exc}"})) + return 2 + + if not isinstance(d, dict): + print(_json.dumps({"status": "error", "error": "--citation-json must be a JSON object"})) + return 2 + + result = validate_citation_dict(d) + output_format = getattr(args, "format", "text") + + if output_format == "json": + import dataclasses + print(_json.dumps(dataclasses.asdict(result), indent=2)) + else: + status = "PASS" if result.passed else "FAIL" + print(f"Citation entry {result.artifact_id} ({result.citation_type}): {status}") + for e in result.errors: + print(f" ERROR: {e}") + for w in result.warnings: + print(f" WARN: {w}") + if result.passed: + print(" Citation entry validated. Ready for publication reference.") + + return 0 if result.passed else 3 diff --git a/src/openamp_foundry/cli/main.py b/src/openamp_foundry/cli/main.py index c52cc486..305dafba 100644 --- a/src/openamp_foundry/cli/main.py +++ b/src/openamp_foundry/cli/main.py @@ -13,6 +13,7 @@ from openamp_foundry.cli.commands.reports import _run_coi_check from openamp_foundry.cli.commands.reports import _run_rotation_plan_check from openamp_foundry.cli.commands.reports import _run_security_report_check +from openamp_foundry.cli.commands.reports import _run_citation_check import argparse import json @@ -1943,6 +1944,24 @@ def build_parser() -> argparse.ArgumentParser: help="Output format (default: text).", ) + # ── Citation check (Phase J J7) ───────────────────────────────── + cit = sub.add_parser( + "citation-check", + help=( + "Validate a citation entry against the OpenAMP Foundry citation " + "and reuse policy. Dry-lab only." + ), + ) + cit.add_argument( + "--citation-json", type=str, required=True, + help="JSON string of a CitationEntry dict (required).", + ) + cit.add_argument( + "--format", type=str, default="text", + choices=["text", "json"], + help="Output format (default: text).", + ) + # ── Simulation-result confidence interval report (H7) ──────────── sicr = sub.add_parser( "simulation-ci-report", @@ -2199,6 +2218,9 @@ def main(argv: list[str] | None = None) -> int: if args.command == "security-report-check": return _run_security_report_check(args) + if args.command == "citation-check": + return _run_citation_check(args) + parser.error("unknown command") return 2 diff --git a/src/openamp_foundry/governance/citation_policy.py b/src/openamp_foundry/governance/citation_policy.py new file mode 100644 index 00000000..9972223f --- /dev/null +++ b/src/openamp_foundry/governance/citation_policy.py @@ -0,0 +1,156 @@ +"""Citation and reuse policy validation for OpenAMP Foundry artifacts.""" +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import List + +VALID_CITATION_TYPES: set[str] = {"dataset", "method", "schema", "software"} +VALID_REUSE_CLASSES: set[str] = { + "attribution_required", + "contact_required", + "open", + "restricted", +} +VALID_LICENSE_IDENTIFIERS: set[str] = { + "Apache-2.0", + "CC-BY-4.0", + "CC-BY-NC-4.0", + "MIT", + "Proprietary", +} + + +@dataclass +class CitationEntry: + """A single citable artifact entry.""" + + artifact_id: str + citation_type: str + title: str + version: str + authors: List[str] + year: str + license_identifier: str + reuse_class: str + url: str = "" + bibtex_key: str = "" + dry_lab_only: bool = True + + +@dataclass +class CitationValidationResult: + """Result of validating a CitationEntry.""" + + artifact_id: str + citation_type: str + passed: bool + errors: List[str] = field(default_factory=list) + warnings: List[str] = field(default_factory=list) + dry_lab_only: bool = True + + +def validate_citation_entry(entry: CitationEntry) -> CitationValidationResult: + """Validate a CitationEntry against policy rules.""" + errors: list[str] = [] + warnings: list[str] = [] + + if not entry.artifact_id or not entry.artifact_id.strip(): + errors.append("artifact_id must be non-empty") + + if entry.citation_type not in VALID_CITATION_TYPES: + errors.append( + f"citation_type must be one of {sorted(VALID_CITATION_TYPES)}, " + f"got: {entry.citation_type!r}" + ) + + if not entry.title or not entry.title.strip(): + errors.append("title must be non-empty") + + if not entry.version or not entry.version.strip(): + errors.append("version must be non-empty") + + if not entry.authors: + errors.append("authors must be a non-empty list") + + if not re.match(r"^\d{4}$", str(entry.year)): + errors.append( + f"year must be a 4-digit string (YYYY), got: {entry.year!r}" + ) + + if entry.license_identifier not in VALID_LICENSE_IDENTIFIERS: + errors.append( + f"license_identifier must be one of " + f"{sorted(VALID_LICENSE_IDENTIFIERS)}, got: {entry.license_identifier!r}" + ) + + if entry.reuse_class not in VALID_REUSE_CLASSES: + errors.append( + f"reuse_class must be one of {sorted(VALID_REUSE_CLASSES)}, " + f"got: {entry.reuse_class!r}" + ) + + if not entry.dry_lab_only: + errors.append("dry_lab_only must be True for citation entries") + + if entry.reuse_class == "restricted": + warnings.append( + "reuse_class is 'restricted': contact maintainers before any reuse" + ) + + if entry.reuse_class == "contact_required" and not entry.url: + warnings.append( + "reuse_class is 'contact_required' but url is empty: " + "provide a contact URL" + ) + + if not entry.bibtex_key: + warnings.append( + "bibtex_key is empty: provide a BibTeX key for easy citation" + ) + + return CitationValidationResult( + artifact_id=entry.artifact_id, + citation_type=entry.citation_type, + passed=len(errors) == 0, + errors=errors, + warnings=warnings, + dry_lab_only=True, + ) + + +def validate_citation_dict(d: dict) -> CitationValidationResult: + """Validate a dict representation of a CitationEntry.""" + required = [ + "artifact_id", + "authors", + "citation_type", + "license_identifier", + "reuse_class", + "title", + "version", + "year", + ] + missing = [f for f in required if f not in d] + if missing: + return CitationValidationResult( + artifact_id=d.get("artifact_id", ""), + citation_type=d.get("citation_type", ""), + passed=False, + errors=[f"Missing required fields: {missing}"], + dry_lab_only=True, + ) + entry = CitationEntry( + artifact_id=d["artifact_id"], + citation_type=d["citation_type"], + title=d["title"], + version=d["version"], + authors=d["authors"], + year=str(d["year"]), + license_identifier=d["license_identifier"], + reuse_class=d["reuse_class"], + url=d.get("url", ""), + bibtex_key=d.get("bibtex_key", ""), + dry_lab_only=d.get("dry_lab_only", True), + ) + return validate_citation_entry(entry) diff --git a/tests/governance/test_citation_policy.py b/tests/governance/test_citation_policy.py new file mode 100644 index 00000000..f2ea10e6 --- /dev/null +++ b/tests/governance/test_citation_policy.py @@ -0,0 +1,162 @@ +"""Tests for citation and reuse policy validation.""" +import pytest +from openamp_foundry.governance.citation_policy import ( + VALID_CITATION_TYPES, + VALID_LICENSE_IDENTIFIERS, + VALID_REUSE_CLASSES, + CitationEntry, + CitationValidationResult, + validate_citation_dict, + validate_citation_entry, +) + + +def _valid_entry(**kwargs) -> CitationEntry: + defaults = dict( + artifact_id="OAMP-v0.7.5", + citation_type="software", + title="OpenAMP Foundry", + version="v0.7.5", + authors=["Open-Problem-Lab"], + year="2026", + license_identifier="MIT", + reuse_class="open", + url="https://github.com/Open-Problem-Lab/OpenAMP-Foundry", + bibtex_key="openamp_foundry_2026", + dry_lab_only=True, + ) + defaults.update(kwargs) + return CitationEntry(**defaults) + + +class TestValidCitationEntry: + def test_valid_entry_passes(self): + result = validate_citation_entry(_valid_entry()) + assert result.passed is True + assert result.errors == [] + + def test_result_has_correct_artifact_id(self): + result = validate_citation_entry(_valid_entry()) + assert result.artifact_id == "OAMP-v0.7.5" + + def test_result_has_correct_citation_type(self): + result = validate_citation_entry(_valid_entry()) + assert result.citation_type == "software" + + def test_all_citation_types_accepted(self): + for ct in VALID_CITATION_TYPES: + result = validate_citation_entry(_valid_entry(citation_type=ct)) + assert result.passed is True, f"Expected {ct} to pass" + + def test_all_reuse_classes_accepted_except_warnings(self): + for rc in VALID_REUSE_CLASSES: + entry = _valid_entry(reuse_class=rc, url="https://example.com") + result = validate_citation_entry(entry) + assert result.passed is True, f"Expected {rc} to pass" + + def test_all_license_identifiers_accepted(self): + for lic in VALID_LICENSE_IDENTIFIERS: + result = validate_citation_entry(_valid_entry(license_identifier=lic)) + assert result.passed is True, f"Expected {lic} to pass" + + +class TestInvalidCitationEntry: + def test_empty_artifact_id_fails(self): + result = validate_citation_entry(_valid_entry(artifact_id="")) + assert result.passed is False + assert any("artifact_id" in e for e in result.errors) + + def test_invalid_citation_type_fails(self): + result = validate_citation_entry(_valid_entry(citation_type="paper")) + assert result.passed is False + assert any("citation_type" in e for e in result.errors) + + def test_empty_title_fails(self): + result = validate_citation_entry(_valid_entry(title="")) + assert result.passed is False + assert any("title" in e for e in result.errors) + + def test_empty_version_fails(self): + result = validate_citation_entry(_valid_entry(version="")) + assert result.passed is False + assert any("version" in e for e in result.errors) + + def test_empty_authors_fails(self): + result = validate_citation_entry(_valid_entry(authors=[])) + assert result.passed is False + assert any("authors" in e for e in result.errors) + + def test_invalid_year_fails(self): + result = validate_citation_entry(_valid_entry(year="26")) + assert result.passed is False + assert any("year" in e for e in result.errors) + + def test_invalid_license_identifier_fails(self): + result = validate_citation_entry(_valid_entry(license_identifier="GPL-3.0")) + assert result.passed is False + assert any("license_identifier" in e for e in result.errors) + + def test_invalid_reuse_class_fails(self): + result = validate_citation_entry(_valid_entry(reuse_class="free")) + assert result.passed is False + assert any("reuse_class" in e for e in result.errors) + + def test_dry_lab_only_false_fails(self): + result = validate_citation_entry(_valid_entry(dry_lab_only=False)) + assert result.passed is False + assert any("dry_lab_only" in e for e in result.errors) + + +class TestCitationWarnings: + def test_restricted_reuse_warns(self): + result = validate_citation_entry(_valid_entry(reuse_class="restricted")) + assert result.passed is True + assert any("restricted" in w for w in result.warnings) + + def test_contact_required_without_url_warns(self): + result = validate_citation_entry( + _valid_entry(reuse_class="contact_required", url="") + ) + assert result.passed is True + assert any("url" in w for w in result.warnings) + + def test_empty_bibtex_key_warns(self): + result = validate_citation_entry(_valid_entry(bibtex_key="")) + assert result.passed is True + assert any("bibtex_key" in w for w in result.warnings) + + +class TestValidateCitationDict: + def test_valid_dict_passes(self): + d = { + "artifact_id": "OAMP-v0.7.5", + "citation_type": "software", + "title": "OpenAMP Foundry", + "version": "v0.7.5", + "authors": ["Open-Problem-Lab"], + "year": "2026", + "license_identifier": "MIT", + "reuse_class": "open", + } + result = validate_citation_dict(d) + assert result.passed is True + + def test_missing_fields_fails(self): + result = validate_citation_dict({"artifact_id": "OAMP-v0.7.5"}) + assert result.passed is False + assert any("Missing" in e for e in result.errors) + + +class TestConstants: + def test_valid_citation_types_has_4(self): + assert len(VALID_CITATION_TYPES) == 4 + + def test_valid_reuse_classes_has_4(self): + assert len(VALID_REUSE_CLASSES) == 4 + + def test_valid_license_identifiers_has_5(self): + assert len(VALID_LICENSE_IDENTIFIERS) == 5 + + def test_all_results_dry_lab_only_true(self): + result = validate_citation_entry(_valid_entry()) + assert result.dry_lab_only is True diff --git a/tests/test_test_count_regression.py b/tests/test_test_count_regression.py index 2201e0c1..e5fc127a 100644 --- a/tests/test_test_count_regression.py +++ b/tests/test_test_count_regression.py @@ -2,11 +2,11 @@ import subprocess import sys -# Baseline: 3575 tests as of Loop 114 (2026-07-09) -- Phase J J6 (security policy) +# Baseline: 3599 tests as of Loop 115 (2026-07-09) -- Phase J J7 (citation and reuse guide) # 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 = 3575 +BASELINE = 3599 TOLERANCE = 0.05 # allow 5% fluctuation