""" Cocoon Validator — Integrity scoring, required-field enforcement, confidence routing. Every cocoon write must pass through CocoonValidator before hitting disk. If required fields are missing the cocoon is downgraded to 'partial' or moved to low_confidence rather than silently stored as complete. Integrity score factors (each 0-1, weighted): 1. Required field completion (weight 0.35) 2. Execution path quality (weight 0.20) 3. Perspective diversity (weight 0.15) 4. Metrics population (weight 0.20) 5. No echo / collapse detected (weight 0.10) """ from __future__ import annotations import json import logging import time from dataclasses import dataclass, field from pathlib import Path from typing import Optional logger = logging.getLogger(__name__) # ── Weight map for integrity scoring ──────────────────────────────────────── _WEIGHTS = { "required_fields": 0.35, "execution_path": 0.20, "perspectives": 0.15, "metrics": 0.20, "echo_collapse": 0.10, } # Execution-path quality scores _PATH_QUALITY = { "forge_full": 1.0, "adapter_lightweight": 0.6, "recovery_mode": 0.3, "fallback_template": 0.0, "unknown": 0.0, } # Required fields for each execution path _REQUIRED_FIELDS = { "forge_full": [ "execution_path", "model_inference_invoked", "orchestrator_trace_id", "eta_score", "epsilon_value", "gamma_coherence", "active_perspectives", "emotional_valence", "importance_score", "aegis_framework_scores", "pairwise_tensions", ], "adapter_lightweight": [ "execution_path", "orchestrator_trace_id", "eta_score", "emotional_valence", "importance_score", "reason_for_fallback", ], "recovery_mode": [ "execution_path", "reason_for_fallback", "emotional_valence", "importance_score", ], "fallback_template": [ "execution_path", "reason_for_fallback", ], "unknown": ["execution_path"], } @dataclass class ValidationResult: """Result of a CocoonValidator.validate() call.""" integrity_score: float # 0.0 – 1.0 integrity_status: str # 'complete' | 'partial' | 'failed' missing_fields: list = field(default_factory=list) warnings: list = field(default_factory=list) errors: list = field(default_factory=list) field_scores: dict = field(default_factory=dict) needs_review: bool = False review_reason: str = "" def to_dict(self) -> dict: return { "integrity_score": round(self.integrity_score, 4), "integrity_status": self.integrity_status, "missing_fields": self.missing_fields, "warnings": self.warnings, "errors": self.errors, "field_scores": {k: round(v, 3) for k, v in self.field_scores.items()}, "needs_review": self.needs_review, "review_reason": self.review_reason, } class CocoonValidator: """Validates CocoonV3 instances before disk persistence. Usage: validator = CocoonValidator() result = validator.validate(cocoon) if result.needs_review: validator.store_low_confidence(cocoon, result) else: validator.write(cocoon, store_path) """ def __init__( self, store_path: str = "cocoons", low_confidence_path: str = "cocoons/low_confidence", integrity_threshold: float = 0.4, ): self.store_path = Path(store_path) self.low_confidence_path = Path(low_confidence_path) self.integrity_threshold = integrity_threshold self.store_path.mkdir(parents=True, exist_ok=True) self.low_confidence_path.mkdir(parents=True, exist_ok=True) def validate(self, cocoon) -> ValidationResult: """Score a CocoonV3 for integrity. Returns ValidationResult.""" missing = [] warnings = [] errors = [] # ── 1. Required field completion ───────────────────────────────────── path = getattr(cocoon, "execution_path", "unknown") required = _REQUIRED_FIELDS.get(path, _REQUIRED_FIELDS["unknown"]) cocoon_dict = cocoon.to_dict() if hasattr(cocoon, "to_dict") else vars(cocoon) for fname in required: val = cocoon_dict.get(fname) if val is None or val == "" or val == [] or val == {}: missing.append(fname) n_required = len(required) n_present = n_required - len(missing) field_completion = n_present / n_required if n_required > 0 else 1.0 # ── 2. Execution path quality ───────────────────────────────────────── path_score = _PATH_QUALITY.get(path, 0.0) if path == "fallback_template": warnings.append( "Execution path is fallback_template — no real inference occurred. " "cocoon_integrity will be capped at 'partial'." ) if path == "unknown": warnings.append("execution_path is 'unknown' — legacy cocoon or missing provenance.") # ── 3. Perspective diversity ────────────────────────────────────────── perspectives = cocoon_dict.get("active_perspectives") or [] n_perspectives = len(perspectives) perspective_score = min(1.0, n_perspectives / 3.0) # 3+ perspectives = full score if n_perspectives == 0 and path == "forge_full": errors.append("forge_full path must have at least 1 active_perspective") # ── 4. Metrics population ───────────────────────────────────────────── metric_fields = [ ("eta_score", cocoon_dict.get("eta_score")), ("epsilon_value", cocoon_dict.get("epsilon_value")), ("gamma_coherence", cocoon_dict.get("gamma_coherence")), ("psi_r", cocoon_dict.get("psi_r")), ("pairwise_tensions", cocoon_dict.get("pairwise_tensions")), ] metrics_present = sum( 1 for _, v in metric_fields if v is not None and v != {} and v != 0.0 ) metrics_score = metrics_present / len(metric_fields) if metrics_present < 3 and path == "forge_full": warnings.append( f"forge_full path has only {metrics_present}/5 metric fields populated. " "Check epistemic metrics wiring." ) # ── 5. Echo / collapse ──────────────────────────────────────────────── echo_risk = cocoon_dict.get("echo_risk", "unknown") collapse = cocoon_dict.get("perspective_collapse_detected", False) echo_score = 1.0 if echo_risk == "high": echo_score = 0.0 errors.append("echo_risk=high: perspective outputs are near-identical to input prompt.") elif echo_risk == "medium": echo_score = 0.5 warnings.append("echo_risk=medium: some perspective outputs may be echoing the prompt.") elif echo_risk == "unknown": echo_score = 0.7 if collapse: echo_score *= 0.3 errors.append("perspective_collapse_detected: all perspectives produced nearly identical outputs.") # ── Composite integrity score ───────────────────────────────────────── field_scores = { "required_fields": field_completion, "execution_path": path_score, "perspectives": perspective_score, "metrics": metrics_score, "echo_collapse": echo_score, } integrity_score = sum( field_scores[k] * _WEIGHTS[k] for k in _WEIGHTS ) # ── Status and quarantine decision ──────────────────────────────────── if integrity_score >= 0.80 and not errors: status = "complete" elif integrity_score >= self.integrity_threshold and not any( "echo_risk=high" in e or "perspective_collapse" in e for e in errors ): status = "partial" else: status = "failed" # Force partial for fallback_template regardless of score if path == "fallback_template": status = "partial" integrity_score = min(integrity_score, 0.4) needs_review = ( status == "failed" or echo_risk == "high" or collapse or len(errors) >= 3 ) review_reason = ( "; ".join(errors[:3]) if needs_review else "" ) return ValidationResult( integrity_score=integrity_score, integrity_status=status, missing_fields=missing, warnings=warnings, errors=errors, field_scores=field_scores, needs_review=needs_review, review_reason=review_reason, ) def apply_result(self, cocoon, result: ValidationResult): """Mutate cocoon's integrity fields with validator findings.""" cocoon.cocoon_integrity = result.integrity_status cocoon.cocoon_integrity_score = result.integrity_score cocoon.metrics_population_status = ( "complete" if result.field_scores.get("metrics", 0) >= 0.8 else "partial" if result.field_scores.get("metrics", 0) >= 0.4 else "failed" ) return cocoon def store_low_confidence(self, cocoon, result: ValidationResult) -> Path: """Write a low-confidence cocoon to the low_confidence path for later review.""" ts = int(cocoon.timestamp) rand = int(cocoon.cocoon_id[-4:], 16) % 10000 fname = f"cocoon_v3_{ts}_{rand}.json" dest = self.low_confidence_path / fname payload = cocoon.to_dict() payload["_validation"] = result.to_dict() with open(dest, "w", encoding="utf-8") as fh: json.dump(payload, fh, indent=2, ensure_ascii=False) logger.debug( f"[CocoonValidator] Low-confidence cocoon stored at {fname}: {result.review_reason}" ) return dest def write(self, cocoon, filename_prefix: str = "cocoon_v3") -> Path: """Write validated cocoon to store_path. Returns written path.""" result = self.validate(cocoon) self.apply_result(cocoon, result) ts = int(cocoon.timestamp) rand = int(cocoon.cocoon_id[-4:], 16) % 10000 fname = f"{filename_prefix}_{ts}_{rand}.json" if result.needs_review: return self.store_low_confidence(cocoon, result) dest = self.store_path / fname payload = cocoon.to_dict() payload["_validation"] = result.to_dict() with open(dest, "w", encoding="utf-8") as fh: json.dump(payload, fh, indent=2, ensure_ascii=False) logger.debug( f"[CocoonValidator] Wrote {fname} " f"integrity={result.integrity_score:.2f} ({result.integrity_status})" ) return dest def audit_store(self, limit: int = 100) -> dict: """Scan cocoon store and return aggregate integrity stats.""" files = sorted( self.store_path.glob("*.json"), key=lambda f: f.stat().st_mtime, reverse=True, )[:limit] stats = { "total": 0, "complete": 0, "partial": 0, "failed": 0, "low_confidence": len(list(self.low_confidence_path.glob("*.json"))), "avg_integrity_score": 0.0, "avg_eta": 0.0, "avg_epsilon": 0.0, "avg_gamma": 0.0, "execution_paths": {}, "echo_risk_distribution": {"low": 0, "medium": 0, "high": 0, "unknown": 0}, "missing_field_frequency": {}, } scores = [] etas = [] epsilons = [] gammas = [] for fpath in files: try: with open(fpath, encoding="utf-8") as fh: data = json.load(fh) except Exception: continue stats["total"] += 1 integrity = data.get("cocoon_integrity", "unknown") if integrity in ("complete", "partial", "failed"): stats[integrity] += 1 score = data.get("cocoon_integrity_score") if isinstance(score, (int, float)): scores.append(score) eta = data.get("eta_score") if isinstance(eta, (int, float)): etas.append(eta) eps = data.get("epsilon_value") if isinstance(eps, (int, float)): epsilons.append(eps) gam = data.get("gamma_coherence") if isinstance(gam, (int, float)): gammas.append(gam) ep = data.get("execution_path", "unknown") stats["execution_paths"][ep] = stats["execution_paths"].get(ep, 0) + 1 er = data.get("echo_risk", "unknown") if er in stats["echo_risk_distribution"]: stats["echo_risk_distribution"][er] += 1 validation = data.get("_validation", {}) for mf in validation.get("missing_fields", []): stats["missing_field_frequency"][mf] = ( stats["missing_field_frequency"].get(mf, 0) + 1 ) if scores: stats["avg_integrity_score"] = round(sum(scores) / len(scores), 4) if etas: stats["avg_eta"] = round(sum(etas) / len(etas), 4) if epsilons: stats["avg_epsilon"] = round(sum(epsilons) / len(epsilons), 4) if gammas: stats["avg_gamma"] = round(sum(gammas) / len(gammas), 4) # Sort missing fields by frequency stats["missing_field_frequency"] = dict( sorted(stats["missing_field_frequency"].items(), key=lambda x: -x[1]) ) return stats