matrix-builder / services /api /app /integrations /drift_detection_adapter.py
ruslanmv
Deploy: metrics + docs (Batch 12)
22b729d
Raw
History Blame Contribute Delete
11.3 kB
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",
}
@dataclass(frozen=True)
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."