Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.git
.github
.venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.env
.env.*
mlflow.db
mlartifacts
mlruns
artifacts
data/raw
data/interim
data/processed
*.tmp
*.log
build
dist
reports/*_attempt_error.json
notebooks
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: InspectIQ CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: pip
cache-dependency-path: requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
- name: Check whitespace and line endings
run: |
git diff --check
git ls-files -z | xargs -0 file | (! grep -E 'CRLF|with CRLF')
- name: Compile source
run: python -m compileall app src scripts tests *.py
- name: Run unit tests
run: python -m unittest discover -s tests -t . -v
- name: Validate release contract
run: python run_release_validation.py --mode ci
- name: Build dashboard image
run: docker build --tag inspectiq:ci .
- name: Upload release validation report
if: always()
uses: actions/upload-artifact@v4
with:
name: release-validation-report
path: |
reports/release_validation_report.json
reports/release_validation_attempt_error.json
if-no-files-found: warn
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ data/processed/
*.log
.DS_Store
Thumbs.db
reports/release_validation_attempt_error.json
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app
COPY src ./src
COPY config ./config
COPY reports ./reports
COPY run_*.py ./

RUN useradd --create-home --uid 10001 appuser \
&& chown -R appuser:appuser /app
USER appuser

EXPOSE 8501

CMD ["python", "-m", "streamlit", "run", "app/streamlit_app.py", "--server.address=0.0.0.0", "--server.port=8501", "--server.headless=true"]
53 changes: 53 additions & 0 deletions config/release_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
release_validation_version: day7a-release-v1
required_source_files:
- app/streamlit_app.py
- src/governance.py
- src/monitoring.py
- run_release_validation.py
- Dockerfile
- .dockerignore
- .github/workflows/ci.yml
required_config_files:
- config/dashboard_config.yaml
- config/monitoring_config.yaml
- config/release_config.yaml
required_committed_reports:
- reports/feasibility_report.json
- reports/data_foundation_report.json
- reports/baseline_report.json
- reports/feature_engineering_report.json
- reports/model_comparison_report.json
- reports/calibration_report.json
- reports/mlflow_tracking_report.json
- reports/batch_prediction_report.json
- reports/dashboard_validation_report.json
- reports/monitoring_report.json
required_local_artifact_categories:
- final candidate model
- ranked candidate output
- top-10-percent candidate output
- monitoring manifest and governance worksheets
required_report_statuses:
- PASS
allowed_monitoring_health_values: [HEALTHY, WARNING, CRITICAL]
expected_model_experiment: exp_05_random_forest
expected_selected_calibration_method: uncalibrated
expected_locked_candidate_row_count: 300
expected_top_10_row_count: 30
required_safety_flags:
labels_accessed: false
performance_metrics_calculated: false
outcome_fairness_metrics_calculated: false
automatic_enforcement: false
model_refit_attempted: false
prediction_artifact_modified: false
docker_runtime_requirements:
port: 8501
non_root_user: true
artifact_mounts: [data, artifacts, reports]
ci_workflow_requirements:
python_version: '3.13'
required_tokens: [compileall, unittest, run_release_validation.py --mode ci, docker build]
prohibited_tracked_file_patterns: ['*.env', 'mlflow.db', 'mlartifacts/**', 'mlruns/**', 'artifacts/**', 'data/raw/**', 'data/interim/**', 'data/processed/**']
attempt_error_report: reports/release_validation_attempt_error.json
main_report: reports/release_validation_report.json
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
dashboard:
build:
context: .
dockerfile: Dockerfile
ports:
- "8501:8501"
# The image contains code and committed reports only. Supply frozen local
# artifacts explicitly; the dashboard never regenerates them in-container.
volumes:
- ./data:/app/data:ro
- ./artifacts:/app/artifacts:ro
- ./reports:/app/reports:ro
135 changes: 135 additions & 0 deletions reports/release_validation_report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
{
"artifact_checks": {
"artifacts": [
"artifacts/models/day4/5ed044f95d5bc1dd/final/final_candidate.joblib",
"artifacts/monitoring/day6/0b1490c5daa560dc0b34/future_outcome_template.csv",
"artifacts/monitoring/day6/0b1490c5daa560dc0b34/monitoring_manifest.json",
"artifacts/monitoring/day6/0b1490c5daa560dc0b34/review_queue_template.csv",
"artifacts/predictions/day5b/e0a880a30016dbc04085/ranked_candidates.csv",
"artifacts/predictions/day5b/e0a880a30016dbc04085/top_10_percent_candidates.csv"
],
"feature_version": "day2-historical-v1",
"hashes": {
"model": true,
"monitoring": true,
"ranked": true,
"top_10": true
},
"safety_flags": {
"automatic_enforcement": true,
"labels_accessed": true,
"model_refit_attempted": true,
"outcome_fairness_metrics_calculated": true,
"performance_metrics_calculated": true,
"prediction_artifact_modified": true
},
"selected_calibration_method": "uncalibrated",
"selected_experiment": "exp_05_random_forest",
"source_snapshot_id": "edbd4bd813ed8e1dbaba9e1c",
"valid": true
},
"ci_workflow_checks": {
"compileall": true,
"docker_build": true,
"least_privilege": true,
"no_secrets": true,
"python_3_13": true,
"release_validation": true,
"required_triggers": true,
"unittest": true,
"yaml_parses": true
},
"configuration_checks": {
"files": [
"config/dashboard_config.yaml",
"config/monitoring_config.yaml",
"config/release_config.yaml"
],
"valid": true
},
"docker_contract_checks": {
"binds_container_address": true,
"env_excluded": true,
"generated_state_excluded": true,
"non_root_user": true,
"port_8501": true,
"python_3_13_slim": true,
"pythonpath": true,
"streamlit_command": true
},
"failures": [],
"feature_version": "day2-historical-v1",
"governance_checks": {
"future_template_headers_only": true,
"human_review_fields_checked": true
},
"hash_checks": {
"model": true,
"monitoring": true,
"ranked": true,
"top_10": true
},
"ignored_artifact_policy_checks": {
"git_available": true,
"no_generated_or_secret_files_tracked": true
},
"limitations": [
"Candidate ranking supports human review; there is no automatic enforcement.",
"The 2023 candidate batch is awaiting complete outcome labels.",
"This release validation does not load labels, calculate performance or fairness metrics, fit models, or regenerate predictions.",
"The selected final candidate uses uncalibrated model output and retrospective validation only."
],
"locked_candidate_safety_checks": {
"automatic_enforcement": true,
"labels_accessed": true,
"model_refit_attempted": true,
"outcome_fairness_metrics_calculated": true,
"performance_metrics_calculated": true,
"prediction_artifact_modified": true
},
"mode": "local",
"monitoring_safety_checks": {
"health_independent_from_pipeline": true,
"monitoring_artifacts_checked": true
},
"platform": "Windows",
"python_version": "3.13.14",
"release_validation_version": "day7a-release-v1",
"report_checks": {
"files": [
"reports/baseline_report.json",
"reports/batch_prediction_report.json",
"reports/calibration_report.json",
"reports/dashboard_validation_report.json",
"reports/data_foundation_report.json",
"reports/feasibility_report.json",
"reports/feature_engineering_report.json",
"reports/mlflow_tracking_report.json",
"reports/model_comparison_report.json",
"reports/monitoring_report.json"
],
"valid": true
},
"required_file_checks": {
"files": [
".dockerignore",
".github/workflows/ci.yml",
"Dockerfile",
"app/streamlit_app.py",
"run_release_validation.py",
"src/governance.py",
"src/monitoring.py"
],
"valid": true
},
"runtime_seconds": 0.757284,
"selected_calibration_method": "uncalibrated",
"selected_experiment": "exp_05_random_forest",
"source_snapshot_id": "edbd4bd813ed8e1dbaba9e1c",
"status": "PASS",
"streamlit_import_check": {
"server_started": false,
"valid": true
},
"warnings": []
}
43 changes: 43 additions & 0 deletions run_release_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Run read-only Day 7A release validation against existing artifacts."""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from src.release_validation import ReleaseValidationError, validate_release, write_json_atomic


def main() -> int:
parser = argparse.ArgumentParser(description="InspectIQ read-only release validation")
parser.add_argument("--mode", choices=("ci", "local"), required=True, help="ci omits ignored local artifacts; local validates frozen artifacts")
args = parser.parse_args()
root = Path(__file__).resolve().parent
report_path = root / "reports" / "release_validation_report.json"
error_path = root / "reports" / "release_validation_attempt_error.json"
try:
report = validate_release(root, args.mode)
write_json_atomic(report_path, report)
except Exception as exc:
error = {
"status": "FAIL",
"mode": args.mode,
"error": str(exc),
"limitations": ["The prior valid release validation report was preserved."],
}
write_json_atomic(error_path, error)
print(f"INSPECTIQ RELEASE VALIDATION {args.mode.upper()}: FAIL")
print(f"reason={exc}")
return 1
print(f"mode={args.mode}")
print("required_files_valid=true configs_valid=true reports_valid=true")
print(f"artifacts_valid={'true' if args.mode == 'local' else 'not_required'}")
print("docker_contract_valid=true ci_workflow_valid=true streamlit_import_valid=true")
print("labels_accessed=false performance_metrics_calculated=false outcome_fairness_metrics_calculated=false automatic_enforcement=false")
print(f"INSPECTIQ RELEASE VALIDATION {args.mode.upper()}: PASS")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading