"""Gap detection: active gap classes; any gap triggers halt + root-cause report (no warnings).""" from enum import Enum from typing import Any from pydantic import BaseModel, Field class GapClass(str, Enum): """Active gap classes that trigger immediate halt.""" MISSING_NUMERIC_BOUNDS = "missing_numeric_bounds" IMPLICIT_TOLERANCES = "implicit_tolerances" UNDEFINED_DATUMS = "undefined_datums" ASSUMED_PROCESSES = "assumed_processes" TOOLPATH_ORPHANING = "toolpath_orphaning" class GapReport(BaseModel): """Single gap report: class, root-cause, required resolution.""" gap_class: GapClass = Field(...) description: str = Field(..., description="Human-readable root cause") context_ref: str | None = Field(default=None) required_resolution: str | None = Field(default=None) def check_gaps(context: dict[str, Any]) -> list[GapReport]: """Run gap checks on context; any gap triggers halt. Returns list of gap reports; empty = no gaps.""" reports: list[GapReport] = [] if "numeric_bounds" in context: nb = context["numeric_bounds"] if not isinstance(nb, dict) or not nb: reports.append( GapReport( gap_class=GapClass.MISSING_NUMERIC_BOUNDS, description="Numeric bounds missing or empty", required_resolution="Provide bounded numeric parameters", ) ) elif context.get("require_numeric_bounds"): reports.append( GapReport( gap_class=GapClass.MISSING_NUMERIC_BOUNDS, description="Numeric bounds required but absent", required_resolution="Provide numeric_bounds in context", ) ) if context.get("require_explicit_tolerances") and not context.get("tolerances"): reports.append( GapReport( gap_class=GapClass.IMPLICIT_TOLERANCES, description="Tolerances must be explicit", required_resolution="Declare tolerances in context", ) ) if context.get("require_datums") and not context.get("datums"): reports.append( GapReport( gap_class=GapClass.UNDEFINED_DATUMS, description="Datums required but undefined", required_resolution="Define datums in context", ) ) if context.get("require_process_type") and not context.get("process_type"): reports.append( GapReport( gap_class=GapClass.ASSUMED_PROCESSES, description="Process type must be declared", required_resolution="Set process_type (additive, subtractive, hybrid)", ) ) if context.get("toolpath_ref") and not context.get("geometry_lineage") and context.get("require_lineage"): reports.append( GapReport( gap_class=GapClass.TOOLPATH_ORPHANING, description="Toolpath must trace to geometry and intent", required_resolution="Provide geometry_lineage and intent_ref", ) ) return reports