"""The five vocabularies the exception ledger is built on. Everything here is a closed set with one definition site. That is the whole design: the reason six domains each grew their own failure semantics is that nobody ever had to name the concepts out loud, so "failed", "dead", "blocked" and "interrupted" all came to mean different mixtures of *the vendor said no*, *we never asked*, and *we asked and will ask again*. * :data:`SOURCES` — which domain a row came from. * :data:`STATUSES` — where the exception is in its own lifecycle. * :data:`SEVERITIES` — how loud it should be. * :data:`ROOT_CAUSES` — what is actually wrong. Written here in PR1 and filled deterministically by the collectors; PR2's triage adds the cases rules cannot decide, but it may never invent a code outside this set. * :data:`REMEDIES` — what a human can do about it. Each root cause maps to exactly one, so the workbench never shows a row without a next step. The mapping ``root cause → remedy`` is the load-bearing part. A notification that names a problem and stops is the failure mode this whole theme exists to fix. """ from __future__ import annotations from typing import Final # -------------------------------------------------------------------- # Sources # -------------------------------------------------------------------- #: One per collector. The value is stored, so these strings are schema: #: renaming one orphans every historical row of that source. SOURCE_PROVISIONING: Final = "provisioning" SOURCE_NOTIFICATION: Final = "notification" SOURCE_SCHEDULED_JOB: Final = "scheduled_job" SOURCE_IDENTITY_SYNC: Final = "identity_sync" SOURCE_INTEGRATION: Final = "integration" SOURCES: Final[tuple[str, ...]] = ( SOURCE_PROVISIONING, SOURCE_NOTIFICATION, SOURCE_SCHEDULED_JOB, SOURCE_IDENTITY_SYNC, SOURCE_INTEGRATION, ) #: Human-facing source labels live in the frontend i18n bundle keyed by #: these values; the backend never renders them. # -------------------------------------------------------------------- # Exception lifecycle # -------------------------------------------------------------------- #: Nobody has looked at it yet. STATUS_OPEN: Final = "open" #: A human has seen it and accepts it is real, but it is not fixed. STATUS_ACKNOWLEDGED: Final = "acknowledged" #: A remedy has been applied and a replay is in flight. Set by PR3's #: disposition flow; PR1 never writes it, but the vocabulary is closed #: here so the status column's CHECK-equivalent lives in one place. STATUS_REPLAYING: Final = "replaying" #: Every occurrence has recovered — either a replay succeeded or the #: source rows stopped failing on their own. STATUS_RESOLVED: Final = "resolved" #: Consciously closed without a fix: expected vendor behaviour, a #: decommissioned system, an accepted limitation. Requires a note. STATUS_WONT_FIX: Final = "wont_fix" STATUSES: Final[tuple[str, ...]] = ( STATUS_OPEN, STATUS_ACKNOWLEDGED, STATUS_REPLAYING, STATUS_RESOLVED, STATUS_WONT_FIX, ) #: Still on someone's plate. The single definition of "open" — every #: count, filter and ordering that says "actionable" keys off this, #: never off a re-typed literal. ACTIVE_STATUSES: Final[frozenset[str]] = frozenset({ STATUS_OPEN, STATUS_ACKNOWLEDGED, STATUS_REPLAYING, }) #: Closed. A recurrence reopens :data:`STATUS_RESOLVED` (the fix did #: not hold, and that is exactly what the ledger must show) but never #: :data:`STATUS_WONT_FIX` (a human already decided; re-opening it #: every night is how an alert becomes noise). CLOSED_STATUSES: Final[frozenset[str]] = frozenset({ STATUS_RESOLVED, STATUS_WONT_FIX, }) #: Reopened by a fresh occurrence. REOPENABLE_STATUSES: Final[frozenset[str]] = frozenset({STATUS_RESOLVED}) # -------------------------------------------------------------------- # Severity # -------------------------------------------------------------------- #: Someone is blocked right now: a new hire has no account, a leaver #: still has one, an approval cannot route. SEVERITY_CRITICAL: Final = "critical" #: Automation is not working and a person is affected, but nothing is #: unsafe while it waits for the next business day. SEVERITY_HIGH: Final = "high" #: Degraded, self-healing, or affecting only reporting. SEVERITY_NORMAL: Final = "normal" #: Known-noisy, informational, or already mitigated elsewhere. SEVERITY_LOW: Final = "low" SEVERITIES: Final[tuple[str, ...]] = ( SEVERITY_CRITICAL, SEVERITY_HIGH, SEVERITY_NORMAL, SEVERITY_LOW, ) #: Sort weight — lower sorts first. Used by the list ordering so the #: workbench's first screen is the worst screen. SEVERITY_ORDER: Final[dict[str, int]] = { SEVERITY_CRITICAL: 0, SEVERITY_HIGH: 1, SEVERITY_NORMAL: 2, SEVERITY_LOW: 3, } # -------------------------------------------------------------------- # Root cause # -------------------------------------------------------------------- #: The credential we hold is refused: expired token, rotated secret, #: revoked app, withdrawn scope. Nothing downstream can work until a #: human replaces it, and everything downstream recovers at once when #: they do — which is why this is the one cause whose remedy replays a #: whole integration rather than one row. CAUSE_CREDENTIAL: Final = "credential" #: The vendor is rate limiting or briefly unavailable. Self-healing by #: construction; an exception only exists because a retry budget ran #: out, so the interesting number is how often, not that it happened. CAUSE_THROTTLED: Final = "throttled" #: The vendor rejected the request on its own rules: plan does not #: include the feature, seat cap reached, licence missing, domain not #: managed. Money or vendor-side configuration, never code. CAUSE_VENDOR_POLICY: Final = "vendor_policy" #: Our configuration disagrees with the vendor's reality: a group that #: no longer exists, a project id that moved, a mapping pointing at a #: retired field. CAUSE_CONFIG_DRIFT: Final = "config_drift" #: The data we sent was not acceptable: missing required attribute, #: malformed phone number, duplicate key, a person whose record is #: incomplete upstream. CAUSE_DATA_QUALITY: Final = "data_quality" #: The vendor says the subject is not there — an account, group or #: resource we expected to act on. Distinct from config drift because #: the fix is usually "it was already deleted", i.e. often a no-op. CAUSE_TARGET_MISSING: Final = "target_missing" #: Helix's own defect: an unhandled exception, a bad SQL, a contract #: mismatch. The only cause whose remedy is a code change. CAUSE_PLATFORM_DEFECT: Final = "platform_defect" #: Deliberately last. A cause the rules could not decide and triage has #: not yet reached. It is a real state, not a dumping ground: an #: exception sitting in ``unknown`` for long is itself a finding. CAUSE_UNKNOWN: Final = "unknown" ROOT_CAUSES: Final[tuple[str, ...]] = ( CAUSE_CREDENTIAL, CAUSE_THROTTLED, CAUSE_VENDOR_POLICY, CAUSE_CONFIG_DRIFT, CAUSE_DATA_QUALITY, CAUSE_TARGET_MISSING, CAUSE_PLATFORM_DEFECT, CAUSE_UNKNOWN, ) # -------------------------------------------------------------------- # Remedy # -------------------------------------------------------------------- #: Replace the credential, then replay everything that failed on this #: integration. REMEDY_REPLACE_CREDENTIAL: Final = "replace_credential" #: Nothing to do by hand — it recovers, or the retry budget needs #: widening. Shown so the admin can see the volume, not so they act. REMEDY_WAIT_OR_WIDEN_RETRY: Final = "wait_or_widen_retry" #: Take it up with the vendor or with finance: buy the plan, raise the #: cap, assign the licence. REMEDY_VENDOR_OR_FINANCE: Final = "vendor_or_finance" #: Fix the Helix-side configuration — integration config, sync mapping, #: connector binding — then replay this signature. REMEDY_FIX_CONFIG: Final = "fix_config" #: Fix the subject's data (usually in the HR source of truth), then #: replay just the rows for that subject. REMEDY_FIX_SUBJECT_DATA: Final = "fix_subject_data" #: Confirm the target really is gone and close it out; the action was #: already effectively achieved. REMEDY_CONFIRM_ALREADY_GONE: Final = "confirm_already_gone" #: Needs a code change. The remedy is a ticket, and the ledger's job is #: to carry the evidence into it. REMEDY_CODE_FIX: Final = "code_fix" #: Triage it. The only remedy for :data:`CAUSE_UNKNOWN`. REMEDY_INVESTIGATE: Final = "investigate" REMEDIES: Final[tuple[str, ...]] = ( REMEDY_REPLACE_CREDENTIAL, REMEDY_WAIT_OR_WIDEN_RETRY, REMEDY_VENDOR_OR_FINANCE, REMEDY_FIX_CONFIG, REMEDY_FIX_SUBJECT_DATA, REMEDY_CONFIRM_ALREADY_GONE, REMEDY_CODE_FIX, REMEDY_INVESTIGATE, ) #: Total over :data:`ROOT_CAUSES`. Asserted by a test, because a cause #: without a remedy is precisely the dead end this theme removes. REMEDY_BY_CAUSE: Final[dict[str, str]] = { CAUSE_CREDENTIAL: REMEDY_REPLACE_CREDENTIAL, CAUSE_THROTTLED: REMEDY_WAIT_OR_WIDEN_RETRY, CAUSE_VENDOR_POLICY: REMEDY_VENDOR_OR_FINANCE, CAUSE_CONFIG_DRIFT: REMEDY_FIX_CONFIG, CAUSE_DATA_QUALITY: REMEDY_FIX_SUBJECT_DATA, CAUSE_TARGET_MISSING: REMEDY_CONFIRM_ALREADY_GONE, CAUSE_PLATFORM_DEFECT: REMEDY_CODE_FIX, CAUSE_UNKNOWN: REMEDY_INVESTIGATE, } def remedy_for(cause: str) -> str: """Remedy for ``cause``, falling back to investigation. The fallback exists for forward compatibility with a stored cause this build does not know (a row written by a newer deploy, read by an older one mid-rollout). It is never a licence to store a cause outside :data:`ROOT_CAUSES` — :func:`validate_cause` guards writes. """ return REMEDY_BY_CAUSE.get(cause, REMEDY_INVESTIGATE) # -------------------------------------------------------------------- # Write guards # -------------------------------------------------------------------- def validate_cause(cause: str) -> str: """Return ``cause`` if it is a known root cause, else raise. Called on every write path — collectors, triage, manual override. A stored value outside :data:`ROOT_CAUSES` would break :data:`REMEDY_BY_CAUSE`'s totality and put a row in the workbench with no next step. """ if cause not in ROOT_CAUSES: raise ValueError(f"unknown root cause: {cause!r}") return cause def validate_status(status: str) -> str: """Return ``status`` if it is a known lifecycle state, else raise.""" if status not in STATUSES: raise ValueError(f"unknown exception status: {status!r}") return status def validate_severity(severity: str) -> str: """Return ``severity`` if known, else raise.""" if severity not in SEVERITIES: raise ValueError(f"unknown exception severity: {severity!r}") return severity def validate_source(source: str) -> str: """Return ``source`` if it is a registered collector source.""" if source not in SOURCES: raise ValueError(f"unknown exception source: {source!r}") return source