Spaces:
Running
Running
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from app.schemas.bundle import MatrixBundle | |
| from app.schemas.common import BundleFile, ValidationStatus | |
| from app.schemas.validation import ( | |
| DependencyChange, | |
| ValidationCheck, | |
| ValidationReport, | |
| ValidationRequest, | |
| ValidationViolation, | |
| ) | |
| from app.services.repair_prompt_service import build_repair_prompt | |
| from app.utils.ids import stable_id | |
| from app.utils.time import utc_now | |
| CONTROL_FILES = { | |
| "MATRIX_BLUEPRINT.yaml", | |
| "MATRIX_STANDARDS.lock", | |
| "MATRIX_TASKS.md", | |
| "MATRIX_ALLOWED_CHANGES.md", | |
| "MATRIX_ACCEPTANCE_CRITERIA.md", | |
| "MATRIX_VALIDATION.md", | |
| } | |
| FORBIDDEN_PREFIXES = (".github/workflows/", "deploy/helm/", "artifacts/attestations/") | |
| REQUIRED_ARTIFACTS = { | |
| "README.md", | |
| "MATRIX_BLUEPRINT.yaml", | |
| "MATRIX_STANDARDS.lock", | |
| "docs/standards-report.md", | |
| "artifacts/manifest.json", | |
| "artifacts/checksums.txt", | |
| } | |
| class DriftDetectionAdapter: | |
| """Policy adapter for validating AI-coder output against a Matrix Bundle. | |
| The real production path can delegate to agent-generator. This local implementation is | |
| intentionally deterministic so Matrix Builder can say approved / needs-repair / rejected | |
| during development and CI. | |
| """ | |
| def validate(self, bundle: MatrixBundle, request: ValidationRequest | None = None) -> ValidationReport: | |
| payload = request or ValidationRequest(bundle_id=bundle.bundle_id, mode="bundle") | |
| changed_paths = [item.path for item in payload.changed_files] | |
| violations: list[ValidationViolation] = [] | |
| checks: list[ValidationCheck] = [] | |
| self._check_required_files(bundle.files, violations, checks) | |
| self._check_forbidden_file_changes(changed_paths, violations, checks) | |
| self._check_dependency_drift(payload.dependency_changes, violations, checks) | |
| self._check_standards_lock(bundle, payload, changed_paths, violations, checks) | |
| self._check_release_artifacts(bundle.files, violations, checks) | |
| status = _status_from(violations) | |
| score = _score_from(violations) | |
| repair_prompt = build_repair_prompt(bundle.bundle_id, violations) if violations else None | |
| approved = status == ValidationStatus.APPROVED | |
| return ValidationReport( | |
| report_id=stable_id("val", f"{bundle.bundle_id}:{score}:{len(violations)}"), | |
| bundle_id=bundle.bundle_id, | |
| status=status, | |
| score=score, | |
| violations=violations, | |
| repair_prompt=repair_prompt, | |
| checks=checks, | |
| approved=approved, | |
| matrixhub_publishable=approved, | |
| summary=_summary_for(status, violations), | |
| created_at=utc_now(), | |
| ) | |
| def _check_required_files( | |
| self, | |
| files: list[BundleFile], | |
| violations: list[ValidationViolation], | |
| checks: list[ValidationCheck], | |
| ) -> None: | |
| present = {file.path for file in files} | |
| missing = sorted(REQUIRED_ARTIFACTS - present) | |
| if missing: | |
| for path in missing: | |
| violations.append( | |
| ValidationViolation( | |
| rule_id="RMD-009", | |
| severity="high", | |
| path=path, | |
| message="Required Matrix Bundle artifact is missing.", | |
| remediation="Regenerate the Matrix Bundle with agent-generator and keep all required artifacts.", | |
| ) | |
| ) | |
| checks.append( | |
| ValidationCheck( | |
| check_id="required_artifacts_present", | |
| status="failed", | |
| message=", ".join(missing), | |
| ) | |
| ) | |
| else: | |
| checks.append( | |
| ValidationCheck( | |
| check_id="required_artifacts_present", | |
| status="passed", | |
| message="All Matrix control and trust artifacts are present.", | |
| ) | |
| ) | |
| def _check_forbidden_file_changes( | |
| self, | |
| changed_paths: list[str], | |
| violations: list[ValidationViolation], | |
| checks: list[ValidationCheck], | |
| ) -> None: | |
| bad_paths = [path for path in changed_paths if path in CONTROL_FILES or path.startswith(FORBIDDEN_PREFIXES)] | |
| if bad_paths: | |
| for path in bad_paths: | |
| rule_id = "RMD-103" if path in CONTROL_FILES else "RMD-002" | |
| violations.append( | |
| ValidationViolation( | |
| rule_id=rule_id, | |
| severity="critical", | |
| path=path, | |
| message="AI coder changed a forbidden Matrix-controlled file or protected path.", | |
| remediation="Restore this file from the original Matrix Bundle. AI coders are workers, not architects.", | |
| ) | |
| ) | |
| checks.append( | |
| ValidationCheck( | |
| check_id="forbidden_file_changes_absent", | |
| status="failed", | |
| message=", ".join(bad_paths), | |
| ) | |
| ) | |
| else: | |
| checks.append( | |
| ValidationCheck( | |
| check_id="forbidden_file_changes_absent", | |
| status="passed", | |
| message="No forbidden Matrix control file changes were detected.", | |
| ) | |
| ) | |
| def _check_dependency_drift( | |
| self, | |
| dependency_changes: list[DependencyChange], | |
| violations: list[ValidationViolation], | |
| checks: list[ValidationCheck], | |
| ) -> None: | |
| unapproved = [change for change in dependency_changes if not change.approved] | |
| if unapproved: | |
| for change in unapproved: | |
| package = f"{change.ecosystem}:{change.name}" | |
| violations.append( | |
| ValidationViolation( | |
| rule_id="RMD-105", | |
| severity="high", | |
| path=package, | |
| message=f"Unapproved dependency {change.action}: {package}.", | |
| remediation="Request explicit approval or remove the dependency change.", | |
| ) | |
| ) | |
| checks.append( | |
| ValidationCheck( | |
| check_id="dependency_drift_absent", | |
| status="failed", | |
| message=", ".join(f"{item.ecosystem}:{item.name}" for item in unapproved), | |
| ) | |
| ) | |
| else: | |
| checks.append( | |
| ValidationCheck( | |
| check_id="dependency_drift_absent", | |
| status="passed", | |
| message="No unapproved dependency changes were detected.", | |
| ) | |
| ) | |
| def _check_standards_lock( | |
| self, | |
| bundle: MatrixBundle, | |
| payload: ValidationRequest, | |
| changed_paths: list[str], | |
| violations: list[ValidationViolation], | |
| checks: list[ValidationCheck], | |
| ) -> None: | |
| if "MATRIX_STANDARDS.lock" in changed_paths: | |
| checks.append( | |
| ValidationCheck( | |
| check_id="standards_lock_unchanged", | |
| status="failed", | |
| message="MATRIX_STANDARDS.lock changed after bundle approval.", | |
| ) | |
| ) | |
| return | |
| if payload.standards_lock_digest and bundle.manifest_digest: | |
| if payload.standards_lock_digest != bundle.manifest_digest: | |
| violations.append( | |
| ValidationViolation( | |
| rule_id="RMD-002", | |
| severity="critical", | |
| path="MATRIX_STANDARDS.lock", | |
| message="Submitted standards lock digest does not match the generated Matrix Bundle digest.", | |
| remediation="Use the original standards lock from the bundle or regenerate the bundle.", | |
| ) | |
| ) | |
| checks.append( | |
| ValidationCheck( | |
| check_id="standards_lock_digest_matches", | |
| status="failed", | |
| message="Digest mismatch.", | |
| ) | |
| ) | |
| return | |
| checks.append( | |
| ValidationCheck( | |
| check_id="standards_lock_digest_matches", | |
| status="passed", | |
| message="Standards lock digest matches the Matrix Bundle record.", | |
| ) | |
| ) | |
| return | |
| checks.append( | |
| ValidationCheck( | |
| check_id="standards_lock_digest_matches", | |
| status="skipped", | |
| message="No standards lock digest was provided; forbidden-change policy still applies.", | |
| ) | |
| ) | |
| def _check_release_artifacts( | |
| self, | |
| files: list[BundleFile], | |
| violations: list[ValidationViolation], | |
| checks: list[ValidationCheck], | |
| ) -> None: | |
| present = {file.path for file in files} | |
| missing_trust_files = sorted({"artifacts/manifest.json", "artifacts/checksums.txt"} - present) | |
| if missing_trust_files: | |
| violations.append( | |
| ValidationViolation( | |
| rule_id="REL-001", | |
| severity="medium", | |
| path="artifacts/", | |
| message="Bundle is missing trust artifacts required for MatrixHub publication.", | |
| remediation="Regenerate the Matrix Bundle so manifest and checksums are included.", | |
| ) | |
| ) | |
| checks.append( | |
| ValidationCheck( | |
| check_id="trust_artifacts_present", | |
| status="failed", | |
| message=", ".join(missing_trust_files), | |
| ) | |
| ) | |
| else: | |
| checks.append( | |
| ValidationCheck( | |
| check_id="trust_artifacts_present", | |
| status="passed", | |
| message="Manifest and checksums are present.", | |
| ) | |
| ) | |
| def _status_from(violations: list[ValidationViolation]) -> ValidationStatus: | |
| severities = {violation.severity for violation in violations} | |
| if "critical" in severities: | |
| return ValidationStatus.REJECTED | |
| if severities: | |
| return ValidationStatus.NEEDS_REPAIR | |
| return ValidationStatus.APPROVED | |
| def _score_from(violations: list[ValidationViolation]) -> int: | |
| weights = {"critical": 35, "high": 20, "medium": 10, "low": 5, "info": 1} | |
| penalty = sum(weights.get(violation.severity, 1) for violation in violations) | |
| return max(0, 100 - penalty) | |
| def _summary_for(status: ValidationStatus, violations: list[ValidationViolation]) -> str: | |
| if status == ValidationStatus.APPROVED: | |
| return "Approved: the AI-coder output stayed inside the Matrix contract." | |
| if status == ValidationStatus.REJECTED: | |
| return "Rejected: critical Matrix contract drift was detected." | |
| return f"Needs repair: {len(violations)} validation issue(s) must be fixed before approval." | |