"""Two-axis APM work-order classifier — the core domain logic. Axis 1 — comment intent: regex over the HTML-stripped ``Last Comment``, most-specific first. Axis 2 — structured state: ``Hold Reason`` + ``WO Status``. Resolution: comment intent wins when confident, else structured state, else ``Other``. A Claude Haiku fallback (Secrets Manager ``apm-wo-analysis/anthropic-api-key``) is reserved strictly for ambiguous free-text with no structured signal. A mismatch detector flags when comment intent contradicts structured state — surface, never suppress. The authoritative spec is CLAUDE.md ("The classification model"). This module is owned by the classifier-engineer agent; the rule ladder and Haiku fallback are implemented in Phase 2 (docs/BUILD.md) and smoke-tested against a real export before merge. """ from __future__ import annotations # Axis 2 — Hold Reason → category. HOLD_TO_CATEGORY = { "SCHEDULING": "Awaiting Scheduling", "REPORT": "Report / Docs Needed", "VENDOR": "Awaiting Vendor / Parts", "PARTS": "Awaiting Vendor / Parts", "ORDER": "Awaiting Vendor / Parts", "VERIFY": "Verification Needed", "RESOURCE": "Resource Hold", "NOEQUIP": "No Equipment", } # Axis 2 — WO Status signals. WO_STATUS_CANCELLED = "RCAN" # → Cancelled WO_STATUS_HOLD = "H" # corroborates On Hold WO_STATUS_IN_FLIGHT = frozenset({"IP", "R", "RR"}) ESCALATION_CATEGORIES = frozenset( { "1st Escalation", "2nd Escalation", "3rd Escalation", "SIM Ticket", "Other Escalation", } ) ACTION_NEEDED_CATEGORIES = ESCALATION_CATEGORIES | frozenset( { "Awaiting Scheduling", "Report / Docs Needed", "Awaiting Report / Invoice", "Awaiting Vendor / Parts", "Status Inquiry", "Vendor No-Show", } ) def strip_html(comment: str | None) -> str: """Unwrap the HTML-wrapped Last Comment and decode entities. (Phase 2)""" raise NotImplementedError def comment_intent(comment: str) -> str | None: """Ordered, most-specific-first rule ladder; returns a bucket or None. (Phase 2)""" raise NotImplementedError def classify( wo_status: str | None, hold_reason: str | None, last_comment: str | None, ) -> tuple[str, str | None]: """Resolve to ``(final_category, mismatch_reason | None)``. (Phase 2)""" raise NotImplementedError