diff --git a/.github/workflows/validate-release-config.yml b/.github/workflows/validate-release-config.yml index 3652d8b..8a2e707 100644 --- a/.github/workflows/validate-release-config.yml +++ b/.github/workflows/validate-release-config.yml @@ -6,12 +6,14 @@ on: - 'release/release-checklist.yaml' - 'examples/sample-release-checklist.yaml' - 'tools/validate_release_config.py' + - 'tests/test_validate_release_config.py' - '.github/workflows/validate-release-config.yml' pull_request: paths: - 'release/release-checklist.yaml' - 'examples/sample-release-checklist.yaml' - 'tools/validate_release_config.py' + - 'tests/test_validate_release_config.py' - '.github/workflows/validate-release-config.yml' permissions: @@ -20,7 +22,7 @@ permissions: jobs: validate: runs-on: ubuntu-latest - name: Validate release checklist YAML + name: Validate release decision YAML steps: - uses: actions/checkout@v4 @@ -33,13 +35,16 @@ jobs: - name: Install dependencies run: python -m pip install --upgrade pip pyyaml + - name: Run validator tests + run: python -m unittest discover -s tests -v + - name: Validate starter template structure run: | python tools/validate_release_config.py \ release/release-checklist.yaml \ --mode template - - name: Validate populated sample minimum gates + - name: Validate fictional decision example run: | python tools/validate_release_config.py \ examples/sample-release-checklist.yaml \ diff --git a/README.md b/README.md index acb0901..b04b295 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,237 @@ -# Regulated AI Starter Kit +# Regulated AI Starter Repository -[![Use this template](https://img.shields.io/badge/Use%20this%20template-2ea44f?style=for-the-badge&logo=github)](https://github.com/simaba/regulated-ai/generate) -[![NIST AI RMF](https://img.shields.io/badge/NIST%20AI%20RMF-Informed-0055A4?style=flat-square)](https://www.nist.gov/system/files/documents/2023/01/26/AI%20RMF%201.0.pdf) +[![Use this template](https://img.shields.io/badge/Use%20this%20template-2ea44f?style=flat-square&logo=github)](https://github.com/simaba/regulated-ai/generate) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE) -[![Maintained](https://img.shields.io/badge/Maintained-yes-green.svg?style=flat-square)](https://github.com/simaba/regulated-ai) -A GitHub template repository for governance documentation, release-readiness artifacts, and deployment-readiness structure in regulated or high-accountability AI environments. +A GitHub template for organizing AI governance, evidence, release decisions, risk records, incident preparation, and model documentation in high-accountability environments. -## Maturity +The repository provides a starting structure, not a control system. Folder names, completed documents, true/false fields, or a passing CI job do not establish safety, compliance, fairness, production readiness, or release approval. -This is a **template repository**. It is intended to help teams start with better structure, not to certify that an AI system is safe, compliant, production-ready, or approved for release. +## What changed from a typical starter kit -## Purpose +The release configuration is built around decision propositions and evidence rather than universal thresholds and sector checkboxes. -Use this template to create a starting repository with: +It deliberately avoids claims such as: -- NIST AI RMF-informed governance documentation -- release-readiness configuration stubs -- a structured risk taxonomy with practitioner mappings to NIST AI RMF and EU AI Act concepts -- CI/CD validation workflows -- incident-response playbook stubs -- model-card templates -- generic sample artifacts that show how to fill the templates safely +- every classification system should meet the same accuracy or F1 target; +- a single disparate-impact ratio demonstrates fairness; +- “bias evaluation complete” proves an acceptable outcome; +- a legal or security review can be represented meaningfully by one boolean; +- healthcare, finance, insurance, or government systems share one fixed gate list; +- passing unit tests and documenting rollback make a system ready to release. -## Intended use cases +The right evidence depends on the actual use, people, data, authority, consequences, obligations, and operating environment. -This starter kit is most relevant for teams deploying AI in: +## Start here -- **Healthcare**: clinical decision support, diagnostic AI, or patient risk scoring -- **Financial services**: credit scoring, fraud detection, or model-assisted decisions -- **Insurance**: underwriting AI, claims automation, or risk assessment -- **Government**: benefits eligibility, document processing, or public-facing AI -- other high-accountability domains where governance, traceability, and release discipline matter +| Artifact | Use it for | +|---|---| +| [`docs/how-to-use-this-template.md`](docs/how-to-use-this-template.md) | adopting the repository without copying generic controls blindly | +| [`release/release-checklist.yaml`](release/release-checklist.yaml) | defining a scoped release decision, gates, evidence, conditions, and change triggers | +| [`examples/sample-release-checklist.yaml`](examples/sample-release-checklist.yaml) | fictional evidence-based conditional-pilot example | +| [`docs/release-config-validation.md`](docs/release-config-validation.md) | validator scope and decision-coherence rules | +| [`risk/risk-assessment-template.md`](risk/risk-assessment-template.md) | system-specific risk and evidence review | +| [`governance/model-inventory.md`](governance/model-inventory.md) | adapting inventory fields for deployed systems and ownership | +| [`incident/incident-response-playbook.md`](incident/incident-response-playbook.md) | defining incident authority and response | +| [`model-cards/model-card-template.md`](model-cards/model-card-template.md) | recording system and model information where a model card is useful | ## Repository structure ```text regulated-ai/ -├── docs/ -│ └── how-to-use-this-template.md -├── examples/ -│ ├── sample-release-checklist.yaml -│ └── sample-risk-register.md -├── governance/ -│ ├── ai-governance-policy.md -│ ├── roles-and-responsibilities.md -│ ├── model-inventory.md -│ └── nist-rmf-mapping.md -├── risk/ -│ ├── risk-register.md -│ ├── risk-taxonomy.yaml -│ └── risk-assessment-template.md -├── release/ -│ ├── release-checklist.yaml -│ ├── release-readiness-report.md -│ └── deployment-approval.md -├── incident/ -│ ├── incident-response-playbook.md -│ ├── incident-report-template.md -│ └── escalation-matrix.md -├── model-cards/ -│ └── model-card-template.md -└── .github/ - └── workflows/ - ├── validate-release-config.yml - └── governance-checks.yml +├── docs/ adoption and validation guidance +├── examples/ fictional public-safe artifacts +├── governance/ policy, roles, inventory, and practitioner mappings +├── risk/ taxonomy, register, and assessment template +├── release/ evidence-based release decision artifacts +├── incident/ incident, escalation, and response templates +├── model-cards/ model-card starter structure +├── tools/ narrow structural and semantic validators +├── tests/ validator tests +└── .github/workflows/ CI checks for the starter artifacts ``` -## Quick start +## Adoption sequence + +### 1. Define scope and authoritative sources + +Before filling templates, record: -### 1. Create your repository from this template +- systems and lifecycle stages covered; +- intended and prohibited uses; +- user and affected populations; +- data, model, tool, permission, and environment boundaries; +- actual internal policies and decision authorities; +- official legal, regulatory, standards, and contractual sources that apply; +- information that must remain private. -Click **Use this template** and create a new repository such as `acme-ai-governance` or `{team}-ai-deployment-kit`. +Remove irrelevant directories rather than preserving them for appearance. -### 2. Follow the adoption guide +### 2. Assign decision and control ownership -Start with [`docs/how-to-use-this-template.md`](docs/how-to-use-this-template.md). It explains what to edit in the first hour and first week after creating your copy. +Distinguish: -### 3. Customize the governance policy +- product or use-case owner; +- technical, data, model, tool, and platform owners; +- control owners; +- independent reviewers; +- release and residual-risk decision owner; +- incident and remediation owner; +- correction, appeal, or redress owner where relevant. -Edit `governance/ai-governance-policy.md` and replace `[Organization Name]` placeholders with your organization name, decision rights, and internal approval path. +A named person without authority or resources is not effective accountability. -### 4. Configure your release checklist +### 3. Replace the release propositions -Edit `release/release-checklist.yaml` to reflect your actual controls, owners, and risk tier. See [`examples/sample-release-checklist.yaml`](examples/sample-release-checklist.yaml) for a generic filled example. +Edit [`release/release-checklist.yaml`](release/release-checklist.yaml). Each gate should be a question whose answer changes the scoped decision. + +For example: ```yaml metadata: - project: "Your Project Name" - version: "1.0.0" - environment: "production" - regulated_industry: "healthcare" - risk_classification: "high" - -model_validation: - performance: - accuracy_threshold: 0.95 - bias_evaluation_complete: true - -governance: - documentation: - risk_assessment_complete: true - approvals: - technical_review: true - legal_review: true - -infrastructure: - testing: - unit_tests_passing: true - rollback: - rollback_plan_documented: true + project: "Fictional Catalog Assistant" + version: "0.3.0-pilot" + environment: "bounded-internal-pilot" + decision_scope: "60 trained users; public records; read and draft tools only" + decision_owner: "Fictional Pilot Sponsor" + evidence_cutoff: "2026-09-30" + +decision: + outcome: "release_with_conditions" + blockers: [] + required_actions: + - "Complete the confirmatory legacy-record sample before expansion." + conditions: + - "Keep source-system tools read-only through the pilot." + evidence_gaps: + - "Rare legacy records remain under-sampled." + residual_risks: + - "Staff may over-trust fluent draft rationales." + +gates: + - id: "AUTH-001" + question: "Are identity, authorization, confirmation, and action boundaries enforced for the reviewed scope?" + hard_gate: true + status: "pass" + evidence: + - "evidence/fictional-permission-test.json" + owner: "Fictional Platform Owner" + limitation: "Internal pilot configuration only." ``` -### 5. Run the CI validation +The example is intentionally fictional. Replace the questions and evidence model with organization-specific requirements. -Push to any branch to trigger the included GitHub Actions checks. +### 4. Validate structure and decision coherence ```bash -git add . -git commit -m "Configure regulated AI starter kit" -git push +python -m pip install pyyaml +python -m unittest discover -s tests -v +python tools/validate_release_config.py \ + release/release-checklist.yaml \ + --mode template +python tools/validate_release_config.py \ + examples/sample-release-checklist.yaml \ + --mode ready ``` -### 6. Complete your risk assessment +The validator checks structure and a small set of rules: -Copy `risk/risk-assessment-template.md` and fill it out for each AI system you are deploying. Use [`examples/sample-risk-register.md`](examples/sample-risk-register.md) as a simple reference for owner, mitigation, and status discipline. +- no release with blockers or unresolved hard gates; +- no unconditional release with conditions or required actions; +- conditional release has a condition or required action; +- pass / not-applicable gates cite evidence; +- not-applicable gates include scoped rationale; +- deferred decisions identify evidence gaps; +- identifiers are unique and required fields are populated. -## NIST AI RMF mapping +It does not verify the evidence, gate selection, risk acceptance, or legal and operational sufficiency. -This starter kit is organized around the four core NIST AI RMF functions: +### 5. Connect the artifacts -| Function | Implementation in this kit | -|---|---| -| **Govern** | `governance/` directory for policy, roles, and model inventory | -| **Map** | `risk/` directory for taxonomy and per-system assessments | -| **Measure** | `release/` directory for pre-deployment checks and readiness artifacts | -| **Manage** | `incident/` directory for monitoring, escalation, and response | +The repository becomes useful when records reference each other: + +```text +inventory and context + ↓ +risk and impact assessment + ↓ +evaluation and control evidence + ↓ +release decision and conditions + ↓ +monitoring, incident, correction, and review + ↓ +change, renewal, rollback, or retirement +``` + +Avoid duplicating evidence into several documents without provenance. Link the authoritative record and state its version and freshness. + +### 6. Define change triggers + +A decision should be revisited after material changes to: -This is a practitioner mapping, not an official NIST assessment or endorsement. +- model, provider, prompt, routing, retrieval, or policy; +- data, population, language, geography, or use case; +- tools, permissions, identities, and external action authority; +- infrastructure, region, or supplier; +- evaluator, threshold, rubric, or test set; +- applicable policy or obligation; +- incidents and newly discovered failure classes; +- conditions, exceptions, or owner authority. + +## Relationship to NIST AI RMF + +The directory structure and templates may support internal work related to Govern, Map, Measure, and Manage. They do not implement those functions by existing. + +| Function | Possible repository support | Evidence still required outside the template | +|---|---|---| +| Govern | ownership, policy, inventory, decisions | actual authority, operating process, competence, culture | +| Map | context and risk templates | validated system and affected-population analysis | +| Measure | evidence and gate records | valid evaluations, control tests, uncertainty, outcomes | +| Manage | decision, incident, rollback, and follow-through | effective treatment, response capability, accepted residual risk | + +Use official NIST sources as authoritative and verify practitioner mappings. ## Publication safety -The examples in this repository are intentionally generic. If you use this template in a public or shared repository, do not include customer data, employee data, confidential vendor details, unreleased product names, proprietary model results, internal approval chains, sensitive logs, or real incident details. +A public copy must not contain real: -## Scope and disclaimer +- customer, patient, employee, applicant, citizen, or user data; +- confidential vendor, pricing, contract, architecture, roadmap, prompt, or model result; +- internal decision rights, approval chains, risk assessments, incidents, logs, endpoints, credentials, or tool manifests; +- sensitive regulatory or legal advice; +- examples derived from internal work by changing only names. -This repository is shared in a personal capacity. It is not legal advice, compliance certification, regulatory approval, safety certification, or official guidance from NIST, the EU, ISO, or any employer. +Use invented organizations, `.example` or `.test` contact domains, synthetic data, and scenarios clearly separate from employment history. -References to NIST AI RMF, EU AI Act, release readiness, risk taxonomy, model cards, incident response, or regulated-industry obligations are practitioner mappings and examples. Always verify against official sources and internal requirements before using this template for compliance, safety, or release decisions. +## Quality standard -## Related repositories +A useful customization should add: -| Repository | What it adds | -|---|---| -| [governance-playbook](https://github.com/simaba/governance-playbook) | Full governance playbook with broader operating-model guidance | -| [release-checklist](https://github.com/simaba/release-checklist) | CLI validator and stricter release gate logic | -| [release-governance](https://github.com/simaba/release-governance) | Release lifecycle governance framework | -| [nist-rmf-guide](https://github.com/simaba/nist-rmf-guide) | Practitioner guide for implementing NIST AI RMF | -| [ai-prism](https://github.com/simaba/ai-prism) | Curated list of governance tools, frameworks, and references | +- a decision-relevant proposition; +- traceable evidence and limitations; +- an accountable owner with authority; +- explicit condition, exception, or residual-risk semantics; +- an enforceable monitoring or stop trigger; +- a tested incident, containment, rollback, correction, or retirement path; +- an organization-specific source or requirement. -## License +Avoid adding a document or checkbox merely because it is common in governance repositories. -MIT License. See [LICENSE](LICENSE). +## Maturity and scope + +This is a starter template with working structural and decision-coherence validation. It is not a governance platform, formal control library, compliance assessment, legal opinion, safety case, or release authority. + +## Related repositories + +| Repository | Distinct role | +|---|---| +| [`governance-playbook`](https://github.com/simaba/governance-playbook) | enterprise governance operating model | +| [`release-governance`](https://github.com/simaba/release-governance) | release evidence and decision semantics | +| [`release-checklist`](https://github.com/simaba/release-checklist) | packaged configuration validator | +| [`nist-rmf-guide`](https://github.com/simaba/nist-rmf-guide) | practitioner navigation and evidence planning | +| [`agent-eval`](https://github.com/simaba/agent-eval) | evaluation validity and reporting | --- -*Maintained by [Sima Bagheri](https://github.com/simaba) · Built for AI teams working in regulated and high-accountability environments.* +*Maintained by [Sima Bagheri](https://github.com/simaba).* diff --git a/docs/release-config-validation.md b/docs/release-config-validation.md index 5d82b50..9542ba8 100644 --- a/docs/release-config-validation.md +++ b/docs/release-config-validation.md @@ -1,16 +1,88 @@ -# Release-Config Validation +# Release-Decision Configuration Validation -This starter repository distinguishes two checks: +This starter repository validates the structure and selected decision semantics of a release record. It deliberately does not prescribe universal accuracy, fairness, industry, or approval gates. -1. **Template validation** checks that `release/release-checklist.yaml` has the expected structure and supported metadata. The default file intentionally contains `false` gates and placeholder values because it is not a release request. -2. **Readiness-example validation** checks that `examples/sample-release-checklist.yaml` satisfies the repository's illustrative minimum gates for its declared risk tier and industry. +## Modes -Run locally: +### Template ```bash -python -m pip install pyyaml -python tools/validate_release_config.py release/release-checklist.yaml --mode template -python tools/validate_release_config.py examples/sample-release-checklist.yaml --mode ready +python tools/validate_release_config.py \ + release/release-checklist.yaml \ + --mode template ``` -The validator is intentionally narrow. It does not certify safety, compliance, legal sufficiency, production readiness, or an actual deployment decision. It only makes the starter template's structural and illustrative gate contract explicit and testable. +Template mode checks: + +- required top-level sections; +- required metadata fields; +- supported decision outcomes and gate statuses; +- gate field types; +- list structure; +- unique gate identifiers. + +Placeholders and unresolved gates are allowed because the file is a starting structure, not a release request. + +### Ready + +```bash +python tools/validate_release_config.py \ + examples/sample-release-checklist.yaml \ + --mode ready +``` + +Ready mode additionally checks: + +- required metadata and gate fields do not contain placeholders; +- at least one gate exists; +- passing and not-applicable gates cite evidence; +- not-applicable gates contain a scoped rationale; +- release and conditional release have no blockers; +- release and conditional release have no unresolved hard gates; +- unconditional release has no conditions or required actions; +- conditional release has at least one condition or required action; +- deferred decisions identify evidence gaps; +- do-not-release decisions identify a blocker or unresolved hard gate. + +## Semantics + +The configuration distinguishes: + +- **hard gate:** a non-compensable requirement for the reviewed scope; +- **blocker:** unresolved finding that prevents release; +- **required action:** accepted follow-up work under a bounded conditional decision; +- **condition:** enforceable restriction on scope or operation; +- **evidence gap:** missing or unreliable support for a proposition; +- **residual risk:** risk remaining after controls, requiring organization-specific acceptance. + +The validator checks internal coherence. It does not determine whether: + +- a gate is appropriate; +- evidence is authentic, current, representative, or sufficient; +- a not-applicable rationale is correct; +- a finding severity is justified; +- a condition is enforceable in the real system; +- a residual-risk owner has proper authority; +- a release is safe, lawful, compliant, production-ready, or valuable. + +## Tests + +```bash +python -m unittest discover -s tests -v +``` + +The tests cover blocker, condition, hard-gate, evidence, placeholder, deferred-decision, and duplicate-ID semantics. + +## Customization + +Organizations should replace the example propositions with gates derived from the actual: + +- intended use and affected population; +- data, model, tool, permission, and action scope; +- harm and consequence model; +- applicable policies and obligations; +- evaluation and control evidence; +- operational, incident, recovery, and redress needs; +- release authority and residual-risk process. + +Do not add a generic gate merely because it sounds responsible. Every gate should support a named decision and have an evidence owner, method, scope, and invalidation trigger. diff --git a/examples/sample-release-checklist.yaml b/examples/sample-release-checklist.yaml index 4bd6da7..64c5bbe 100644 --- a/examples/sample-release-checklist.yaml +++ b/examples/sample-release-checklist.yaml @@ -1,46 +1,130 @@ +# Fictional example only. Northstar Library, the system, people, dates, and +# evidence references below are invented for this repository. + metadata: - project: "Document Triage Assistant" - version: "0.1.0" - environment: "staging" - regulated_industry: "general" - risk_classification: "medium" - deployment_owner: "Example Product Owner" - -model_validation: - performance: - accuracy_threshold: 0.90 - bias_evaluation_complete: true - adversarial_testing_complete: false - fairness: - disparate_impact_ratio: 0.86 - subgroup_performance_review: true - -governance: - documentation: - model_card_complete: true - risk_assessment_complete: true - explainability_report_complete: false - approvals: - technical_review: true - ai_governance_review: true - legal_review: false - security_review: true - regulatory: - privacy_review_complete: true - -infrastructure: - testing: - unit_tests_passing: true - integration_tests_passing: true - security_scan_passed: true - load_test_passed: false - monitoring: - alerting_configured: true - drift_detection_enabled: true - rollback: - rollback_plan_documented: true - rollback_tested: false - -incident_readiness: - runbook_complete: true - escalation_contacts_defined: true + project: "Northstar Catalog Drafting Assistant" + version: "0.3.0-pilot" + environment: "bounded-internal-pilot" + decision_scope: "60 trained library staff; public catalog records only; read and draft tools; no publication, deletion, patron data, or external messaging" + decision_owner: "Fictional Pilot Sponsor" + evidence_cutoff: "2026-09-30" + context: + intended_use: "Draft suggested catalog corrections for staff review" + prohibited_uses: + - "publish or delete records" + - "access patron identity or borrowing history" + - "make employment, eligibility, legal, or financial decisions" + affected_populations: + - "library catalog staff" + - "users relying on accessible catalog search" + system_components: + - "fictional language model" + - "public catalog retrieval index" + - "read-only search tool" + - "draft-correction tool" + explicit_exclusions: + - "public self-service access" + - "restricted archive records" + - "automated execution of corrections" + +decision: + outcome: "release_with_conditions" + rationale: "The fictional evidence supports a small read-and-draft pilot. The pilot remains bounded because correction quality for rare legacy records is not yet estimated precisely enough for broader use." + blockers: [] + required_actions: + - "Complete a stratified review of 200 legacy-record drafts before any population expansion." + conditions: + - "Keep all source-system tools read-only and require staff confirmation before any correction is entered manually." + - "Limit the pilot to 60 trained internal users and public catalog records through 2026-11-30." + - "Pause the pilot after any confirmed restricted-record exposure or unauthorized write attempt." + evidence_gaps: + - "Performance on rare handwritten-archive transcription patterns remains based on a small exploratory sample." + residual_risks: + - "Staff may over-trust fluent but unsupported draft rationales despite source links and training." + expires_on: "2026-11-30" + +gates: + - id: "SCOPE-001" + question: "Are intended use, prohibited use, population, data, tools, permissions, and environment bounded for this decision?" + hard_gate: true + status: "pass" + evidence: + - "evidence/fictional-pilot-scope-v3.md" + - "evidence/fictional-data-flow-v2.md" + owner: "Fictional Product Owner" + limitation: "The scope record applies only to the internal pilot configuration." + + - id: "AUTH-001" + question: "Are identity, authorization, confirmation, and action boundaries enforced for the reviewed scope?" + hard_gate: true + status: "pass" + evidence: + - "evidence/fictional-permission-test-2026-09-22.json" + - "evidence/fictional-unauthorized-call-test.md" + owner: "Fictional Platform Owner" + limitation: "Manual source-system correction occurs outside the assistant and was reviewed as a separate staff workflow." + + - id: "DATA-001" + question: "Does the reviewed configuration prevent retrieval and disclosure of restricted and patron-linked records?" + hard_gate: true + status: "pass" + evidence: + - "evidence/fictional-data-boundary-tests-v4.json" + owner: "Fictional Data Protection Reviewer" + limitation: "Tests cover the versioned synthetic and public-safe fixture; production records remain outside scope." + + - id: "EVAL-001" + question: "Does current evaluation evidence support the stated rollout decision and disclose coverage and uncertainty?" + hard_gate: false + status: "partial" + evidence: + - "evidence/fictional-evaluation-report-v5.md" + owner: "Fictional Evaluation Owner" + limitation: "Core catalog journeys are covered; the rare legacy-record slice requires a larger confirmatory sample before expansion." + + - id: "OPS-001" + question: "Can the system be monitored, stopped, contained, recovered, and supported under the reviewed conditions?" + hard_gate: true + status: "pass" + evidence: + - "evidence/fictional-pilot-stop-exercise.md" + - "evidence/fictional-support-runbook-v2.md" + owner: "Fictional Operations Owner" + limitation: "The exercise covered the internal pilot only and did not test public-user communication." + + - id: "ACCESS-001" + question: "Can pilot staff complete critical review and correction tasks using the supported accessibility paths?" + hard_gate: false + status: "pass" + evidence: + - "evidence/fictional-accessibility-review.md" + owner: "Fictional Accessibility Reviewer" + limitation: "One supported browser and two assistive-technology combinations were reviewed." + +findings: + - id: "FINDING-001" + description: "Rare legacy-record corrections have wide uncertainty because only 32 representative cases were reviewed." + severity: "medium" + disposition: "required_action" + owner: "Fictional Evaluation Owner" + due_or_expiry: "before population expansion" + + - id: "FINDING-002" + description: "Fluent unsupported rationale remains possible when retrieved source records are incomplete." + severity: "medium" + disposition: "condition" + owner: "Fictional Product Owner" + due_or_expiry: "2026-11-30" + +change_triggers: + - "model, prompt, retrieval corpus, or drafting-policy change" + - "addition of write, publish, delete, messaging, or restricted-data authority" + - "pilot population, language, site, or geography expansion" + - "confirmed restricted-record exposure, unauthorized tool attempt, or material correction incident" + - "change to evaluator rubric, scenario suite, or acceptance decision" + +follow_through: + release_or_hold_action_owner: "Fictional Pilot Owner" + monitoring_and_incident_owner: "Fictional Operations Owner" + evidence_record_location: "evidence/fictional-release-decision-2026-09-30/" + next_review: "2026-11-15 or earlier on any stop trigger" diff --git a/release/release-checklist.yaml b/release/release-checklist.yaml index 76cbf4a..309fef8 100644 --- a/release/release-checklist.yaml +++ b/release/release-checklist.yaml @@ -1,69 +1,106 @@ -# Release Readiness Gate Configuration -# Customize this file for your organization's deployment requirements -# Aligned with NIST AI RMF Measure function +# Evidence-Based Release Decision Configuration +# +# This file is a starter template. Customize the propositions, evidence, +# decision outcomes, authority, and terminology for the actual system and +# organization. A valid file is not proof of safety, compliance, or readiness. metadata: project: "YOUR_PROJECT_NAME" - version: "1.0.0" - environment: "production" # staging | production | canary - regulated_industry: "general" # healthcare | finance | insurance | government | general - risk_classification: "high" # high | medium | low (see governance policy) - deployment_date: null # Set before deployment: YYYY-MM-DD - deployment_owner: null # Name of person responsible for deployment + version: "YOUR_SYSTEM_VERSION" + environment: "YOUR_ENVIRONMENT" + decision_scope: "[TBD: population, data, tools, permissions, geography, and rollout stage]" + decision_owner: "[TBD: person with release / residual-risk authority]" + evidence_cutoff: "[TBD: YYYY-MM-DD]" + context: + intended_use: "[TBD]" + prohibited_uses: [] + affected_populations: [] + system_components: [] + explicit_exclusions: [] -# ── Model Validation Gates ──────────────────────────────────────────────────── -model_validation: - performance: - accuracy_threshold: 0.90 # Minimum accuracy on holdout test set - f1_threshold: 0.85 # Minimum F1 score (for classification tasks) - latency_p99_ms: 500 # Max 99th percentile inference latency (ms) - throughput_rps: 100 # Minimum requests per second under load - fairness: - bias_evaluation_complete: false # Set to true after bias audit - disparate_impact_ratio: 0.80 # Minimum ratio (1.0 = perfect parity) - subgroup_performance_review: false - robustness: - adversarial_testing_complete: false - distribution_shift_tested: false - edge_case_coverage_percent: 80 +# Allowed outcomes: +# release | release_with_conditions | hold | do_not_release | defer +# +# Semantics: +# - blockers prevent release and conditional release. +# - required_actions are accepted follow-up work under a bounded decision. +# - conditions restrict the approved scope or operation. +# - evidence_gaps record unavailable or unreliable evidence. +# - residual_risks require organization-specific acceptance outside this validator. +decision: + outcome: "defer" + rationale: "[TBD]" + blockers: [] + required_actions: [] + conditions: [] + evidence_gaps: + - "[TBD: evidence required before a decision]" + residual_risks: [] + expires_on: "[TBD: YYYY-MM-DD or change trigger]" -# ── Governance Gates ────────────────────────────────────────────────────────── -governance: - documentation: - model_card_complete: false - risk_assessment_complete: false - data_lineage_documented: false - explainability_report_complete: false - approvals: - technical_review: false # Engineering lead sign-off - ai_governance_review: false # AI governance team sign-off - legal_review: false # Legal / compliance sign-off - security_review: false # InfoSec sign-off - regulatory: - gdpr_assessment_complete: false # If applicable - hipaa_assessment_complete: false # If applicable (healthcare) - sr_11_7_compliance: false # If applicable (financial services) +# Each gate is a decision-relevant proposition. Do not copy these examples +# blindly; replace them with gates appropriate to the actual authority and harm. +# +# status: pass | fail | partial | not_tested | not_applicable +# A hard gate must be pass or not_applicable (with scoped rationale/evidence) +# before release or release_with_conditions. +gates: + - id: "SCOPE-001" + question: "Are intended use, prohibited use, population, data, tools, permissions, and environment bounded for this decision?" + hard_gate: true + status: "not_tested" + evidence: [] + owner: "[TBD]" + limitation: "[TBD]" -# ── Infrastructure Gates ────────────────────────────────────────────────────── -infrastructure: - testing: - unit_tests_passing: false - integration_tests_passing: false - load_test_passed: false - security_scan_passed: false - monitoring: - alerting_configured: false - logging_enabled: false - drift_detection_enabled: false - dashboards_ready: false - rollback: - rollback_plan_documented: false - rollback_tested: false - previous_version_preserved: false + - id: "AUTH-001" + question: "Are identity, authorization, confirmation, and action boundaries enforced for the reviewed scope?" + hard_gate: true + status: "not_tested" + evidence: [] + owner: "[TBD]" + limitation: "[TBD]" -# ── Incident Readiness ──────────────────────────────────────────────────────── -incident_readiness: - runbook_complete: false - escalation_contacts_defined: false - on_call_rotation_configured: false - incident_playbook_reviewed: false + - id: "EVAL-001" + question: "Does current evaluation evidence support the stated rollout decision and disclose coverage and uncertainty?" + hard_gate: false + status: "not_tested" + evidence: [] + owner: "[TBD]" + limitation: "[TBD]" + + - id: "OPS-001" + question: "Can the system be monitored, stopped, contained, recovered, and supported under the reviewed conditions?" + hard_gate: true + status: "not_tested" + evidence: [] + owner: "[TBD]" + limitation: "[TBD]" + + - id: "REDRESS-001" + question: "Can affected users or operators correct, contest, or escalate material outcomes where required?" + hard_gate: false + status: "not_tested" + evidence: [] + owner: "[TBD]" + limitation: "[TBD]" + +findings: + - id: "FINDING-001" + description: "[TBD]" + severity: "[TBD: low / medium / high / critical]" + disposition: "[TBD: blocker / required_action / condition / exception / residual_risk / observation]" + owner: "[TBD]" + due_or_expiry: "[TBD]" + +change_triggers: + - "model, provider, prompt, routing, retrieval, or policy change" + - "data, user population, geography, tool, permission, or authority change" + - "evaluator, threshold, test set, infrastructure, or control change" + - "material incident, newly discovered failure class, or expired condition" + +follow_through: + release_or_hold_action_owner: "[TBD]" + monitoring_and_incident_owner: "[TBD]" + evidence_record_location: "[TBD]" + next_review: "[TBD]" diff --git a/tests/test_validate_release_config.py b/tests/test_validate_release_config.py new file mode 100644 index 0000000..8bb3b7e --- /dev/null +++ b/tests/test_validate_release_config.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import copy +import unittest + +from tools.validate_release_config import validate + + +BASE = { + "metadata": { + "project": "Fictional Assistant", + "version": "0.1.0", + "environment": "internal-pilot", + "decision_scope": "20 internal users; read-only tools", + "decision_owner": "Fictional Sponsor", + "evidence_cutoff": "2026-09-30", + }, + "decision": { + "outcome": "release_with_conditions", + "blockers": [], + "required_actions": ["Complete the confirmatory sample before expansion."], + "conditions": ["Keep tools read-only."], + "evidence_gaps": [], + "residual_risks": ["Users may over-trust fluent drafts."], + }, + "gates": [ + { + "id": "AUTH-001", + "question": "Is write authority disabled?", + "hard_gate": True, + "status": "pass", + "evidence": ["evidence/auth-test.json"], + "owner": "Fictional Platform Owner", + "limitation": "Pilot scope only.", + }, + { + "id": "EVAL-001", + "question": "Does the evidence support the bounded pilot?", + "hard_gate": False, + "status": "partial", + "evidence": ["evidence/evaluation.md"], + "owner": "Fictional Evaluation Owner", + "limitation": "Rare slice remains small.", + }, + ], +} + + +class ValidateReleaseDecisionTests(unittest.TestCase): + def test_valid_conditional_release(self) -> None: + self.assertEqual(validate(copy.deepcopy(BASE), "ready"), []) + + def test_release_rejects_conditions(self) -> None: + payload = copy.deepcopy(BASE) + payload["decision"]["outcome"] = "release" + errors = validate(payload, "ready") + self.assertIn("release cannot include required_actions or conditions", errors) + + def test_release_rejects_blocker(self) -> None: + payload = copy.deepcopy(BASE) + payload["decision"]["blockers"] = ["Unresolved authorization defect"] + errors = validate(payload, "ready") + self.assertIn("release_with_conditions cannot include unresolved blockers", errors) + + def test_hard_gate_must_be_resolved_for_release(self) -> None: + payload = copy.deepcopy(BASE) + payload["gates"][0]["status"] = "partial" + errors = validate(payload, "ready") + self.assertTrue( + any("unresolved hard gates: AUTH-001" in error for error in errors), + errors, + ) + + def test_pass_gate_requires_evidence(self) -> None: + payload = copy.deepcopy(BASE) + payload["gates"][0]["evidence"] = [] + errors = validate(payload, "ready") + self.assertIn("gates[0] with status pass must cite evidence", errors) + + def test_not_applicable_requires_rationale(self) -> None: + payload = copy.deepcopy(BASE) + payload["gates"][0]["status"] = "not_applicable" + payload["gates"][0]["limitation"] = "" + errors = validate(payload, "ready") + self.assertIn( + "gates[0] with status not_applicable must explain the scoped rationale in limitation", + errors, + ) + + def test_defer_requires_evidence_gap(self) -> None: + payload = copy.deepcopy(BASE) + payload["decision"]["outcome"] = "defer" + payload["decision"]["required_actions"] = [] + payload["decision"]["conditions"] = [] + errors = validate(payload, "ready") + self.assertIn("defer requires at least one evidence gap", errors) + + def test_do_not_release_requires_reason(self) -> None: + payload = copy.deepcopy(BASE) + payload["decision"]["outcome"] = "do_not_release" + payload["decision"]["required_actions"] = [] + payload["decision"]["conditions"] = [] + errors = validate(payload, "ready") + self.assertIn( + "do_not_release requires a blocker or an unresolved hard gate", + errors, + ) + + def test_duplicate_gate_ids_are_rejected(self) -> None: + payload = copy.deepcopy(BASE) + payload["gates"].append(copy.deepcopy(payload["gates"][0])) + errors = validate(payload, "ready") + self.assertIn("duplicate gate id: AUTH-001", errors) + + def test_ready_mode_rejects_placeholders(self) -> None: + payload = copy.deepcopy(BASE) + payload["metadata"]["decision_owner"] = "[TBD]" + errors = validate(payload, "ready") + self.assertIn("metadata.decision_owner contains a placeholder", errors) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/validate_release_config.py b/tools/validate_release_config.py index 81cd2bf..6277d1e 100644 --- a/tools/validate_release_config.py +++ b/tools/validate_release_config.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 -"""Validate release-checklist YAML as either a template or a populated example. +"""Validate an evidence-based AI release decision configuration. -This helper intentionally does not certify a deployment. It checks only the -small, explicit contract documented by this starter repository: required -structure, supported metadata, and a minimum set of configured gate values for -an illustrative ready-to-review example. +The validator checks structure and a small set of decision-coherence rules. It +cannot determine whether evidence is valid, a gate is appropriate, a residual +risk is acceptable, or a deployment is safe or compliant. """ from __future__ import annotations import argparse +import re import sys from pathlib import Path from typing import Any @@ -17,57 +17,30 @@ import yaml -REQUIRED_TOP_LEVEL = ("metadata", "model_validation", "governance", "infrastructure") +REQUIRED_TOP_LEVEL = ("metadata", "decision", "gates") REQUIRED_METADATA = ( "project", "version", "environment", - "regulated_industry", - "risk_classification", + "decision_scope", + "decision_owner", + "evidence_cutoff", ) -ALLOWED_RISKS = {"low", "medium", "high"} -ALLOWED_INDUSTRIES = {"general", "healthcare", "finance", "insurance", "government"} - -REQUIRED_GATES_BY_RISK = { - "low": ( - "governance.approvals.technical_review", - "infrastructure.testing.unit_tests_passing", - ), - "medium": ( - "model_validation.performance.bias_evaluation_complete", - "governance.approvals.technical_review", - "governance.documentation.risk_assessment_complete", - "infrastructure.testing.unit_tests_passing", - "infrastructure.rollback.rollback_plan_documented", - ), - "high": ( - "model_validation.performance.bias_evaluation_complete", - "governance.approvals.technical_review", - "governance.approvals.ai_governance_review", - "governance.approvals.legal_review", - "governance.documentation.model_card_complete", - "governance.documentation.risk_assessment_complete", - "infrastructure.testing.unit_tests_passing", - "infrastructure.testing.security_scan_passed", - "infrastructure.rollback.rollback_plan_documented", - "incident_readiness.runbook_complete", - ), +ALLOWED_OUTCOMES = { + "release", + "release_with_conditions", + "hold", + "do_not_release", + "defer", } -INDUSTRY_GATES = { - "healthcare": ("governance.regulatory.hipaa_assessment_complete",), - "finance": ("governance.regulatory.sr_11_7_compliance",), - "insurance": ("governance.regulatory.sr_11_7_compliance",), - "government": ("governance.approvals.legal_review",), +ALLOWED_GATE_STATUS = { + "pass", + "fail", + "partial", + "not_tested", + "not_applicable", } - - -def _nested(mapping: dict[str, Any], path: str) -> Any: - value: Any = mapping - for part in path.split("."): - if not isinstance(value, dict): - return None - value = value.get(part) - return value +PLACEHOLDER = re.compile(r"(?:\[?TBD\]?|YOUR_|REPLACE_|<[^>]+>)", re.IGNORECASE) def _load(path: str | Path) -> dict[str, Any]: @@ -83,53 +56,191 @@ def _load(path: str | Path) -> dict[str, Any]: return payload +def _is_nonempty_text(value: Any) -> bool: + return isinstance(value, str) and bool(value.strip()) + + +def _is_placeholder(value: Any) -> bool: + return not _is_nonempty_text(value) or bool(PLACEHOLDER.search(str(value))) + + +def _require_list(mapping: dict[str, Any], key: str, errors: list[str]) -> list[Any]: + value = mapping.get(key, []) + if not isinstance(value, list): + errors.append(f"decision.{key} must be a list") + return [] + return value + + +def _validate_gate(gate: Any, index: int, mode: str, errors: list[str]) -> None: + prefix = f"gates[{index}]" + if not isinstance(gate, dict): + errors.append(f"{prefix} must be a mapping") + return + + required = ("id", "question", "hard_gate", "status", "evidence", "owner") + for field in required: + if field not in gate: + errors.append(f"{prefix} is missing required field: {field}") + + gate_id = gate.get("id") + question = gate.get("question") + hard_gate = gate.get("hard_gate") + status = gate.get("status") + evidence = gate.get("evidence") + owner = gate.get("owner") + limitation = gate.get("limitation") + + if gate_id is not None and not _is_nonempty_text(gate_id): + errors.append(f"{prefix}.id must be non-empty text") + if question is not None and not _is_nonempty_text(question): + errors.append(f"{prefix}.question must be non-empty text") + if hard_gate is not None and not isinstance(hard_gate, bool): + errors.append(f"{prefix}.hard_gate must be true or false") + if status is not None and status not in ALLOWED_GATE_STATUS: + errors.append( + f"{prefix}.status must be one of: {', '.join(sorted(ALLOWED_GATE_STATUS))}" + ) + if evidence is not None and not isinstance(evidence, list): + errors.append(f"{prefix}.evidence must be a list of evidence references") + elif isinstance(evidence, list) and any(not _is_nonempty_text(item) for item in evidence): + errors.append(f"{prefix}.evidence entries must be non-empty text") + if owner is not None and not _is_nonempty_text(owner): + errors.append(f"{prefix}.owner must be non-empty text") + + if mode == "ready": + if _is_placeholder(gate_id): + errors.append(f"{prefix}.id contains a placeholder") + if _is_placeholder(question): + errors.append(f"{prefix}.question contains a placeholder") + if _is_placeholder(owner): + errors.append(f"{prefix}.owner contains a placeholder") + if status in {"pass", "not_applicable"} and not evidence: + errors.append(f"{prefix} with status {status} must cite evidence") + if status == "not_applicable" and not _is_nonempty_text(limitation): + errors.append( + f"{prefix} with status not_applicable must explain the scoped rationale in limitation" + ) + + def validate(payload: dict[str, Any], mode: str) -> list[str]: - """Return structural or readiness errors for a release configuration.""" + """Return structural and decision-coherence errors.""" errors: list[str] = [] + missing = [key for key in REQUIRED_TOP_LEVEL if key not in payload] if missing: - errors.append(f"missing required sections: {', '.join(missing)}") - return errors + return [f"missing required sections: {', '.join(missing)}"] - for section in REQUIRED_TOP_LEVEL: - if not isinstance(payload[section], dict): - errors.append(f"{section} must be a mapping") + metadata = payload.get("metadata") + decision = payload.get("decision") + gates = payload.get("gates") - metadata = payload.get("metadata", {}) if not isinstance(metadata, dict): - return errors + ["metadata must be a mapping"] - missing_metadata = [key for key in REQUIRED_METADATA if not metadata.get(key)] + errors.append("metadata must be a mapping") + metadata = {} + if not isinstance(decision, dict): + errors.append("decision must be a mapping") + decision = {} + if not isinstance(gates, list): + errors.append("gates must be a list") + gates = [] + + missing_metadata = [key for key in REQUIRED_METADATA if key not in metadata] if missing_metadata: errors.append(f"missing metadata fields: {', '.join(missing_metadata)}") - return errors - risk = str(metadata["risk_classification"]).lower() - industry = str(metadata["regulated_industry"]).lower() - if risk not in ALLOWED_RISKS: - errors.append(f"metadata.risk_classification must be one of: {', '.join(sorted(ALLOWED_RISKS))}") - if industry not in ALLOWED_INDUSTRIES: - errors.append(f"metadata.regulated_industry must be one of: {', '.join(sorted(ALLOWED_INDUSTRIES))}") - if errors or mode == "template": + for field in REQUIRED_METADATA: + if field in metadata and not _is_nonempty_text(metadata[field]): + errors.append(f"metadata.{field} must be non-empty text") + if mode == "ready" and field in metadata and _is_placeholder(metadata[field]): + errors.append(f"metadata.{field} contains a placeholder") + + outcome = decision.get("outcome") + if outcome not in ALLOWED_OUTCOMES: + errors.append( + f"decision.outcome must be one of: {', '.join(sorted(ALLOWED_OUTCOMES))}" + ) + + blockers = _require_list(decision, "blockers", errors) + required_actions = _require_list(decision, "required_actions", errors) + conditions = _require_list(decision, "conditions", errors) + evidence_gaps = _require_list(decision, "evidence_gaps", errors) + residual_risks = _require_list(decision, "residual_risks", errors) + + for key, values in ( + ("blockers", blockers), + ("required_actions", required_actions), + ("conditions", conditions), + ("evidence_gaps", evidence_gaps), + ("residual_risks", residual_risks), + ): + if any(not _is_nonempty_text(item) for item in values): + errors.append(f"decision.{key} entries must be non-empty text") + + ids: set[str] = set() + for index, gate in enumerate(gates): + _validate_gate(gate, index, mode, errors) + if isinstance(gate, dict) and _is_nonempty_text(gate.get("id")): + gate_id = str(gate["id"]) + if gate_id in ids: + errors.append(f"duplicate gate id: {gate_id}") + ids.add(gate_id) + + if mode == "template": return errors - gate_paths = (*REQUIRED_GATES_BY_RISK[risk], *INDUSTRY_GATES.get(industry, ())) - for path in gate_paths: - value = _nested(payload, path) - if value is not True: - errors.append(f"required readiness gate is not true: {path}") + if not gates: + errors.append("ready mode requires at least one gate") + + unresolved_hard_gates = [ + str(gate.get("id", f"gates[{index}]")) + for index, gate in enumerate(gates) + if isinstance(gate, dict) + and gate.get("hard_gate") is True + and gate.get("status") not in {"pass", "not_applicable"} + ] + + if outcome in {"release", "release_with_conditions"}: + if blockers: + errors.append(f"{outcome} cannot include unresolved blockers") + if unresolved_hard_gates: + errors.append( + f"{outcome} cannot proceed with unresolved hard gates: " + + ", ".join(unresolved_hard_gates) + ) + + if outcome == "release" and (required_actions or conditions): + errors.append("release cannot include required_actions or conditions") + + if outcome == "release_with_conditions" and not (required_actions or conditions): + errors.append( + "release_with_conditions requires at least one required action or condition" + ) + + if outcome == "defer" and not evidence_gaps: + errors.append("defer requires at least one evidence gap") + + if outcome == "do_not_release" and not (blockers or unresolved_hard_gates): + errors.append( + "do_not_release requires a blocker or an unresolved hard gate" + ) + return errors def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( - description="Validate a regulated-AI starter release configuration." + description="Validate a regulated-AI starter release decision configuration." ) parser.add_argument("path") parser.add_argument( "--mode", choices=("template", "ready"), default="template", - help="template checks structure; ready also requires the stated minimum gates", + help=( + "template checks structure; ready also rejects placeholders and " + "checks selected decision semantics" + ), ) args = parser.parse_args(argv) @@ -139,12 +250,12 @@ def main(argv: list[str] | None = None) -> int: parser.error(str(exc)) if errors: - print("Release configuration is invalid:", file=sys.stderr) + print("Release decision configuration is invalid:", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) return 1 - print(f"Release configuration is valid for {args.mode} mode.") + print(f"Release decision configuration is valid for {args.mode} mode.") return 0