From 315319894c5c2d2452be7611708f4f86b4e87d3a Mon Sep 17 00:00:00 2001 From: goldokpa Date: Mon, 22 Jun 2026 14:04:47 +0100 Subject: [PATCH] feat(deploy): investor-ready cleanup, remove Hopelyn, wire security controls - Remove team_docs/Hopelyn_Role_INTERNAL.md and document role reassignment - Reassign Responsible AI responsibilities to @Goldokpa and @obielin - Update README Contributors and CHANGELOG - Bump setup.py to 0.2.0 and fix repository URL - Fix run_api.sh for production (.venv, 0.0.0.0, no reload) - Make CORS origins configurable via CLIMATEVISION_CORS_ORIGINS - Wire SecurityMiddleware and raise bbox max area to 200 sq deg - Wire PipelineGuard anomaly checks in inference pipeline - Add /api/health/models endpoint for model-weight status - Unify frontend API base URL on VITE_API_BASE_URL - Add comprehensive deployment documentation --- .env.example | 54 +++++-- CHANGELOG.md | 4 + README.md | 3 +- docs/DEPLOYMENT.md | 169 +++++++++++++++++++++ frontend/.env.example | 10 +- frontend/src/constants.ts | 2 +- frontend/vite.config.ts | 18 ++- run_api.sh | 14 +- setup.py | 9 +- src/climatevision/api/main.py | 56 ++++++- src/climatevision/config.py | 6 + src/climatevision/db.py | 16 +- src/climatevision/inference/pipeline.py | 25 +++ src/climatevision/security/api_security.py | 4 +- team_docs/ROLE_REASSIGNMENT.md | 53 +++++++ 15 files changed, 397 insertions(+), 46 deletions(-) create mode 100644 docs/DEPLOYMENT.md create mode 100644 team_docs/ROLE_REASSIGNMENT.md diff --git a/.env.example b/.env.example index b4cdc4a..7695471 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,53 @@ # ClimateVision Environment Variables -# Copy this file to .env and fill in your actual credentials -# NEVER commit .env to git! +# Copy this file to .env and fill in your actual credentials. +# NEVER commit .env or secrets/ to git! -# Google Earth Engine +# --------------------------------------------------------------------------- +# Google Earth Engine (required for real satellite imagery) +# --------------------------------------------------------------------------- +# Service account email registered with Earth Engine. GEE_SERVICE_ACCOUNT=your-service-account@your-project.iam.gserviceaccount.com -GEE_PRIVATE_KEY_FILE=path/to/your-private-key.json -# OR if using OAuth: + +# Path to the service-account JSON key, relative to the project root. +# Keep the file in secrets/ (gitignored). +GEE_SERVICE_ACCOUNT_KEY=secrets/gee-key.json + +# Google Cloud / Earth Engine project ID. GEE_PROJECT_ID=your-gee-project-id -# Sentinel Hub (optional - for later) +# --------------------------------------------------------------------------- +# Sentinel Hub (optional future source) +# --------------------------------------------------------------------------- SENTINEL_HUB_CLIENT_ID=your_client_id_here SENTINEL_HUB_CLIENT_SECRET=your_client_secret_here -# API Keys (optional - for later) -API_SECRET_KEY=your_secret_key_here +# --------------------------------------------------------------------------- +# API Security +# --------------------------------------------------------------------------- +# Secret used for API-key hashing and any signed tokens. +# Generate with: openssl rand -hex 32 +API_SECRET_KEY=change_me_in_production + +# Set to 1 ONLY in local development to enable the cv_dev bypass key. +# NEVER enable this in production. +CLIMATEVISION_ALLOW_DEV_KEY=0 + +# --------------------------------------------------------------------------- +# CORS / Frontend +# --------------------------------------------------------------------------- +# Comma-separated list of origins allowed to call the API. +# Include your production domain and any local dev URLs. +CLIMATEVISION_CORS_ORIGINS=https://climatevision.green,https://www.climatevision.green,http://localhost:5173,http://127.0.0.1:5173 + +# --------------------------------------------------------------------------- +# LLM Reporting (optional) +# --------------------------------------------------------------------------- +ANTHROPIC_API_KEY=your_anthropic_api_key +CLIMATEVISION_LLM_MODEL=claude-haiku-4-5-20251001 -# Database (optional - for later) -DATABASE_URL=postgresql://user:password@localhost:5432/climatevision +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- +# SQLite is used by default. Use an absolute path or a project-relative path. +# For production with high traffic, switch to PostgreSQL and update db.py. +DATABASE_URL=sqlite:///outputs/climatevision.sqlite3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 120ae56..62c79ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dependabot configuration for pip, npm, and GitHub Actions updates - CHANGELOG.md (this file) - CITATION.cff for GitHub "Cite this repository" button +- `team_docs/ROLE_REASSIGNMENT.md` documenting departure of @Hopelynconsult and redistribution of Responsible AI responsibilities to @Goldokpa and @obielin + +### Removed +- `team_docs/Hopelyn_Role_INTERNAL.md` (internal credentials file for departed team member); associated PAT must be revoked ### Changed - CODE_OF_CONDUCT.md โ€” replaced placeholder email with GitHub private reporting link diff --git a/README.md b/README.md index 68ced40..2bed4fb 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Thanks to everyone who has contributed to ClimateVision! ๐ŸŽ‰ -a> + | Contributor | Merged PRs | |-------------|-----------| @@ -165,7 +165,6 @@ Thanks to everyone who has contributed to ClimateVision! ๐ŸŽ‰ | [@femi23](https://github.com/femi23) | #34, #40, #41, #42 | | [@obielin](https://github.com/obielin) | #35, #36, #37, #38, #39 | | [@franchaise](https://github.com/franchaise) | #46, #47, #48, #49 | -| [@Hopelynconsult](https://github.com/Hopelynconsult) | #51 | | [@emekambachu](https://github.com/emekambachu) | Co-author contributions | | [@Godswill-code](https://github.com/Godswill-code) | Co-author contributions | | [@cutewizzy11](https://github.com/cutewizzy11) | Co-author contributions | diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..96a20ee --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,169 @@ +# ClimateVision Deployment Guide + +This guide covers deploying ClimateVision to **Fly.io** at `https://climatevision.green` with real Google Earth Engine (GEE) satellite data. + +--- + +## Prerequisites + +- A Fly.io account: https://fly.io +- The Fly CLI (`flyctl`) installed and authenticated +- A Google Cloud project with Earth Engine enabled +- This repository cloned and dependencies installed + +--- + +## 1. Google Earth Engine credentials + +Real satellite imagery requires a GEE service account. + +1. Go to https://console.cloud.google.com/ and select your project. +2. Enable the **Earth Engine API**. +3. Navigate to **IAM & Admin โ†’ Service Accounts**. +4. Create a service account with the role **Earth Engine Resource Writer**. +5. Create a JSON key and download it. +6. Place the key at `secrets/gee-key.json` (the `secrets/` directory is gitignored). +7. Register the service account in the Earth Engine console: https://code.earthengine.google.com/ +8. Copy `.env.example` to `.env` and fill in: + ```bash + GEE_SERVICE_ACCOUNT=your-service-account@your-project.iam.gserviceaccount.com + GEE_SERVICE_ACCOUNT_KEY=secrets/gee-key.json + GEE_PROJECT_ID=your-gee-project-id + ``` + +> **Never commit `secrets/` or `.env` to Git.** + +--- + +## 2. Local production-like run (Docker) + +Build and run the full stack locally: + +```bash +# Make sure .env is populated and secrets/gee-key.json exists +docker compose up --build +``` + +The API will be available at http://localhost:8000 and the dashboard at http://localhost:8000/. + +--- + +## 3. Fly.io setup + +### 3.1 Create the app and provision secrets + +```bash +# Create the Fly app +fly apps create climatevision-green + +# Set all secrets from .env +./scripts/setup_fly_secrets.sh + +# Create a persistent volume for SQLite and outputs (1 GB is plenty for demos) +fly volumes create climatevision_data --region lhr --size 1 +``` + +### 3.2 Custom domain + +```bash +# Request a certificate for your domain +fly certs create climatevision.green +``` + +Add the DNS records shown by `fly certs show` to your DNS provider. Once propagated, Fly will serve the app at `https://climatevision.green`. + +### 3.3 Deploy + +```bash +# Manual deploy +fly deploy --remote-only + +# Or push to main โ€” GitHub Actions will deploy automatically +# once FLY_API_TOKEN is configured (see section 4). +``` + +--- + +## 4. GitHub Actions CI/CD + +The repository includes two workflows: + +- `.github/workflows/ci.yml` โ€” runs on every PR/push to `main` or `develop`. +- `.github/workflows/deploy.yml` โ€” deploys to Fly.io on every push to `main`. + +### Required repository secrets + +Add these in **GitHub โ†’ Settings โ†’ Secrets and variables โ†’ Actions**: + +| Secret | Value | +|--------|-------| +| `FLY_API_TOKEN` | A Fly.io access token from https://fly.io/user/personal_access_tokens | + +GEE credentials are **not** stored in GitHub; they are set directly on Fly via `scripts/setup_fly_secrets.sh`. + +--- + +## 5. Verify the deployment + +Check the health endpoint: + +```bash +curl https://climatevision.green/api/health +``` + +Check which models have trained weights: + +```bash +curl https://climatevision.green/api/health/models +``` + +Open the dashboard: + +```bash +open https://climatevision.green +``` + +--- + +## 6. Model status for investor demos + +As of this deployment, only the **flooding** analysis ships with trained weights (`models/flooding_20260513_124208/model.onnx`). + +Deforestation and arctic-ice analyses run using synthetic fallback tiles and untrained weights. The API reports this transparently via `/api/health/models` and the `is_synthetic` flag in prediction responses. + +To add production models: + +1. Train or obtain `models/unet_deforestation.pth` and `models/unet_ice.pth`. +2. Upload them to the `models/` directory. +3. Redeploy. + +--- + +## 7. Alert delivery + +The alert generator emits notifications to pluggable channels. By default only the `log` channel is active. + +To enable email or webhook alerts, register a delivery function in `src/climatevision/inference/alert_generator.py` or implement the alert worker (see open issue #10). + +For email, configure an SMTP provider or a transactional email service such as Mailgun or SendGrid, then set the relevant secrets via `fly secrets set`. + +--- + +## 8. Security checklist + +- [ ] `CLIMATEVISION_ALLOW_DEV_KEY` is `0` in production. +- [ ] `API_SECRET_KEY` is a strong, unique secret. +- [ ] The GEE service-account key is stored only in `secrets/` or Fly secrets, never in Git. +- [ ] Hopelynโ€™s old PAT has been revoked in GitHub. +- [ ] The Fly app uses HTTPS via `fly certs`. + +--- + +## 9. Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| `GEE tile download failed` | Missing or invalid GEE credentials | Check `.env` / Fly secrets and service-account registration | +| Frontend shows blank page | `frontend/dist` not built or mounted | Re-run `npm run build` in `frontend/` or redeploy | +| CORS errors | Origin not allowed | Add domain to `CLIMATEVISION_CORS_ORIGINS` | +| SQLite data lost on deploy | No Fly volume mounted | Create and mount `climatevision_data` volume on `/app/outputs` | diff --git a/frontend/.env.example b/frontend/.env.example index ffbb571..55304ad 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,8 @@ -# API base URL for frontend requests -# Leave empty when using Vite dev server (proxy handles /api -> backend) -# Set to http://127.0.0.1:8000 when serving built app separately +# ClimateVision frontend environment variables +# Copy to .env and fill in before building for production. + +# Base URL for the ClimateVision API. +# Leave empty for same-origin deployment (e.g. when the API serves the frontend). +# For separate frontend hosting, set the full API origin: +# VITE_API_BASE_URL=https://climatevision.green/api VITE_API_BASE_URL= diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 17fbb71..01661b3 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -3,7 +3,7 @@ */ // API Configuration -export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; export const API_TIMEOUT = 30000; // Map Configuration diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index fe50f31..59defa1 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,16 +1,18 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -export default defineConfig({ +export default defineConfig(({ mode }) => ({ plugins: [react()], server: { port: 5173, strictPort: true, - proxy: { - '/api': { - target: 'http://127.0.0.1:8000', - changeOrigin: true, - }, - }, + proxy: mode === 'development' + ? { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + }, + } + : undefined, }, -}) +})) diff --git a/run_api.sh b/run_api.sh index be1806e..2e51877 100755 --- a/run_api.sh +++ b/run_api.sh @@ -7,18 +7,18 @@ cd "$(dirname "$0")" PORT="${1:-8000}" -if [ ! -d "venv" ]; then - echo "Virtual environment not found. Run: python -m venv venv && source venv/bin/activate && pip install -r requirements.txt && pip install -e ." +if [ ! -d ".venv" ]; then + echo "Virtual environment not found. Run: python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt && pip install -e ." exit 1 fi -source venv/bin/activate +source .venv/bin/activate # Avoid OpenMP sandbox issues; fix NumPy/PyTorch compatibility in spawned workers export OMP_NUM_THREADS=1 -echo "Starting ClimateVision API on http://127.0.0.1:$PORT" -echo " Health: http://127.0.0.1:$PORT/api/health" -echo " API docs: http://127.0.0.1:$PORT/docs" +echo "Starting ClimateVision API on http://0.0.0.0:$PORT" +echo " Health: http://0.0.0.0:$PORT/api/health" +echo " API docs: http://0.0.0.0:$PORT/docs" echo "" -exec uvicorn climatevision.api.main:app --reload --port "$PORT" +exec uvicorn climatevision.api.main:app --host 0.0.0.0 --port "$PORT" --workers 1 diff --git a/setup.py b/setup.py index 9e1b1ce..047d2b5 100644 --- a/setup.py +++ b/setup.py @@ -14,12 +14,12 @@ setup( name="climatevision", - version="0.1.0", + version="0.2.0", author="ClimateVision Contributors", - description="Open-source ML platform for automated deforestation detection using deep learning and satellite imagery", + description="Open-source ML platform for automated environmental monitoring using deep learning and satellite imagery", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/yourusername/ClimateVision", + url="https://github.com/Climate-Vision/ClimateVision", packages=find_packages(where="src"), package_dir={"": "src"}, classifiers=[ @@ -36,7 +36,7 @@ ], python_requires=">=3.8", install_requires=[ - "numpy>=1.21.0", + "numpy>=1.21.0,<2.0.0", "pandas>=1.3.0", "torch>=2.0.0", "torchvision>=0.15.0", @@ -50,6 +50,7 @@ "tqdm>=4.62.0", "pyyaml>=6.0", "requests>=2.26.0", + "python-dotenv>=0.19.0", ], extras_require={ "dev": [ diff --git a/src/climatevision/api/main.py b/src/climatevision/api/main.py index 5c9fb2d..9bcb6fd 100644 --- a/src/climatevision/api/main.py +++ b/src/climatevision/api/main.py @@ -13,6 +13,7 @@ import json import logging +import os import time from datetime import datetime, timezone from pathlib import Path @@ -45,6 +46,7 @@ from climatevision.inference import run_inference_from_file, run_inference_from_gee from climatevision.api.auth import require_api_key from climatevision.governance import explain_prediction, SHAPExplainer +from climatevision.security.api_security import SecurityMiddleware logger = logging.getLogger(__name__) @@ -388,19 +390,30 @@ def create_app() -> FastAPI: ) app.add_middleware(AuditLogMiddleware) + + # CORS: allow local dev origins plus any origins from CLIMATEVISION_CORS_ORIGINS + default_origins = [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://localhost:3000", + "http://127.0.0.1:3000", + ] + extra_origins = [ + origin.strip() + for origin in os.environ.get("CLIMATEVISION_CORS_ORIGINS", "").split(",") + if origin.strip() + ] app.add_middleware( CORSMiddleware, - allow_origins=[ - "http://localhost:5173", - "http://127.0.0.1:5173", - "http://localhost:3000", - "http://127.0.0.1:3000", - ], + allow_origins=list(dict.fromkeys(default_origins + extra_origins)), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + # Wire OWASP-aligned security controls (rate limiting, payload limits, etc.) + app.add_middleware(SecurityMiddleware) + from climatevision.api import admin as _admin app.include_router(_admin.router) @@ -458,6 +471,37 @@ def health() -> dict[str, Any]: "config_issues": config_issues, } + @app.get("/api/health/models") + def health_models() -> dict[str, Any]: + """Report which analysis types have trained weights available on disk.""" + from climatevision.data.band_mapping import get_model_config + + models_status = [] + for atype in SUPPORTED_ANALYSIS_TYPES: + name = atype["name"] + cfg = get_model_config(name) + weights_path = cfg.get("weights") + has_weights = False + if weights_path: + has_weights = (Path(__file__).resolve().parents[3] / weights_path).exists() + models_status.append( + { + "analysis_type": name, + "enabled": atype["enabled"], + "has_trained_weights": has_weights, + "weights_path": weights_path, + "note": "production inference" if has_weights else "synthetic/demo fallback", + } + ) + + ready_count = sum(1 for m in models_status if m["has_trained_weights"]) + return { + "status": "ready" if ready_count else "demo_mode", + "models": models_status, + "ready_count": ready_count, + "total_count": len(models_status), + } + @app.get("/api/analysis-types") def list_analysis_types(enabled_only: bool = True) -> list[dict[str, Any]]: """List available analysis types.""" diff --git a/src/climatevision/config.py b/src/climatevision/config.py index 8265d8c..cb2e211 100644 --- a/src/climatevision/config.py +++ b/src/climatevision/config.py @@ -6,6 +6,12 @@ from typing import Dict, Any import yaml +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass + class Config: """Base configuration class""" diff --git a/src/climatevision/db.py b/src/climatevision/db.py index 711a2ad..04d0349 100644 --- a/src/climatevision/db.py +++ b/src/climatevision/db.py @@ -7,6 +7,7 @@ - Alerts and notifications """ +import os import secrets import sqlite3 from pathlib import Path @@ -23,9 +24,18 @@ def get_db_path() -> Path: """Get the path to the SQLite database file.""" global _DB_PATH if _DB_PATH is None: - db_dir = Config.PROJECT_ROOT / "outputs" - db_dir.mkdir(parents=True, exist_ok=True) - _DB_PATH = db_dir / "climatevision.sqlite3" + db_url = os.environ.get("DATABASE_URL", "") + if db_url.startswith("sqlite:///"): + # Support both absolute paths (sqlite:////...) and project-relative paths + path_part = db_url[len("sqlite:///"):] + if path_part.startswith("/"): + _DB_PATH = Path(path_part) + else: + _DB_PATH = Config.PROJECT_ROOT / path_part + else: + db_dir = Config.PROJECT_ROOT / "outputs" + db_dir.mkdir(parents=True, exist_ok=True) + _DB_PATH = db_dir / "climatevision.sqlite3" return _DB_PATH diff --git a/src/climatevision/inference/pipeline.py b/src/climatevision/inference/pipeline.py index 7af17ab..9ebcd64 100644 --- a/src/climatevision/inference/pipeline.py +++ b/src/climatevision/inference/pipeline.py @@ -19,9 +19,12 @@ from climatevision.data.band_mapping import get_bands_for_analysis, get_model_config from climatevision.models.unet import UNet +from climatevision.security.pipeline_guard import PipelineGuard logger = logging.getLogger(__name__) +_guard = PipelineGuard(enable_input_check=True, enable_output_check=True) + # --------------------------------------------------------------------------- # Project paths (mirrors run_training.py conventions, NOT Config.MODELS_DIR) # --------------------------------------------------------------------------- @@ -217,6 +220,15 @@ def run_inference( ndvi_stats = _compute_ndvi_stats(image) + input_check = _guard.check_input(image) + if input_check.is_anomalous: + logger.warning( + "Anomalous input detected for %s (score=%.4f, type=%s)", + analysis_type, + input_check.anomaly_score, + input_check.anomaly_type, + ) + model, device = _load_model(analysis_type) n_channels = model.n_channels n_classes = model.n_classes @@ -239,6 +251,19 @@ def run_inference( predictions = torch.argmax(output, dim=1) # (1, H, W) probabilities = torch.softmax(output, dim=1) # (1, n_classes, H, W) + output_check = _guard.check_output( + predictions.cpu().numpy(), + probabilities=probabilities.cpu().numpy(), + n_classes=n_classes, + ) + if not output_check.is_valid: + logger.warning( + "Model output validation failed for %s (confidence=%.4f, issues=%s)", + analysis_type, + output_check.confidence, + output_check.issues, + ) + total_pixels = int(predictions.numel()) max_probs = probabilities.max(dim=1).values mean_confidence = float(max_probs.mean().item()) diff --git a/src/climatevision/security/api_security.py b/src/climatevision/security/api_security.py index 7eaa8fc..b4e5e5b 100644 --- a/src/climatevision/security/api_security.py +++ b/src/climatevision/security/api_security.py @@ -32,7 +32,7 @@ class SecurityConfig: """Security configuration settings.""" max_payload_size_bytes: int = 50 * 1024 * 1024 # 50 MB - max_bbox_area_degrees: float = 100.0 # Max area in square degrees + max_bbox_area_degrees: float = 200.0 # Max area in square degrees max_date_range_days: int = 365 # Max date range rate_limit_requests: int = 100 # Requests per window rate_limit_window_seconds: int = 60 # Window size @@ -210,7 +210,7 @@ def validate_payload_size( def validate_bbox( bbox: list[float], - max_area: float = 100.0, + max_area: float = 200.0, ) -> tuple[bool, str]: """ Validate bounding box coordinates. diff --git a/team_docs/ROLE_REASSIGNMENT.md b/team_docs/ROLE_REASSIGNMENT.md new file mode 100644 index 0000000..814ed85 --- /dev/null +++ b/team_docs/ROLE_REASSIGNMENT.md @@ -0,0 +1,53 @@ +# Team Role Reassignment + +**Effective date:** 2026-06-22 + +## Departure + +**Hopelyn** (`@Hopelynconsult`) has stepped away from the ClimateVision project. We thank her for her contributions, including the calibration-metrics work merged in PR #51. + +### Security cleanup + +Her internal credentials file (`Hopelyn_Role_INTERNAL.md`) has been removed from the project. The associated personal access token must be revoked immediately by the project owner to prevent unauthorized access. + +> **Action required:** Revoke the PAT ending in `y4OfP1h` in the GitHub account security settings for `@Hopelynconsult`. + +--- + +## Reassigned responsibilities + +Hopelynโ€™s former role was **Responsible AI & Model Evaluation Lead**. The ownership areas have been redistributed as follows: + +### `@obielin` (Linda Oraegbunam) โ€” Governance & Evaluation Engineering + +Already the primary author of the governance modules (PRs #35โ€“39), Linda now owns the implementation and maintenance of: + +- Automated model-card generator (`src/climatevision/governance/model_card.py`) +- Regional bias / fairness audit framework (`src/climatevision/governance/bias_audit.py`) +- Hash-chained audit logger (`src/climatevision/governance/audit_logger.py`) +- Anomaly detection for inference outputs (`src/climatevision/governance/anomaly_detector.py`) +- Release CI gate for metrics, fairness, and security + +### `@Goldokpa` (Gold Okpa) โ€” Responsible AI Policy & Oversight + +As project owner, Gold now owns the policy and process layer: + +- Responsible-AI policy and contributor-facing governance checklist +- PR review gate for any PR touching `src/climatevision/models/` or `src/climatevision/inference/` +- Calibration and uncertainty-quantification roadmap +- Human-in-the-loop review process for low-confidence detections +- Production drift-monitoring roadmap + +### Ongoing collaboration + +- Linda implements the tooling; Gold defines the policies and enforces the review checklist. +- All model/inference PRs must receive approval from `@Goldokpa` and, when governance tooling is affected, from `@obielin`. + +--- + +## Files affected by this change + +- `team_docs/Hopelyn_Role_INTERNAL.md` โ€” **deleted** +- `team_docs/ROLE_REASSIGNMENT.md` โ€” **created** (this file) +- `README.md` โ€” Contributors table updated +- `CHANGELOG.md` โ€” Team change recorded under `[Unreleased]`