# ============================================================
# src/accountable_record/errors.py
# ============================================================
"""Typed stage errors shared across the Accountable Record pipeline.

WHY:
    Stage ops (loaders, discoverers, parsers) signal failure by raising, and
    the stage's validation layer catches and maps the failure to a
    DiagnosticCode. If ops raise only bare ValueError / FileNotFoundError,
    the validation layer is forced to infer the failure kind by catching
    Exception broadly or by matching on message text.
    Both are fragile.

    A small typed hierarchy fixes this once:
    an op raises the specific error,
    and the validation layer matches on the type and
    maps it to the right AR.<CONCERN>.* code with no string inspection.
    Adding a stage reuses these types and does not reinvent failure dispatch.

OBS:
    These errors are deliberately kind markers, not concern-specific.
    The same kinds recur across stages, something is missing, unreadable,
    malformed, outside the declared root, or duplicated.
    Each stage maps a kind to its own concern's diagnostic code (e.g. SOURCES.SOURCE_MISSING vs
    CONTRACT.CONTRACT_MISSING) at the validation boundary.
    Keeping the error types concern-agnostic is what lets every stage share them.

    Layer note: this module is foundational and depends on nothing internal,
    so any stage op may import it without creating a cycle.
    It carries an optional location (path, manifest key, identifier) so a raised error
    can point a human at the offending material without coupling to any
    source-model type, mirroring Diagnostic.location.

REQ:
    Pure stdlib + typing. No imports from pipeline/ or any stage package.

----------------------------------------------------------------------------
Diagnostics mutability invariant (shared rule, documented once here because
this is the foundational module every stage imports):

    - A stage-local Diagnostics collector MAY be mutated freely within the
      stage that owns it.
    - PipelineState is immutable. State updates MUST merge by COPY using
      Diagnostics.merged() (which copies), never by aliasing or
      mutating an existing state's collector.
    - Concretely: build a fresh Diagnostics() inside the stage, accumulate
      into it, then hand it to a state.with_*(...) helper.
      Those helpers call merged and return a new state.
      Do not call .extend on a collector shared with, or owned by, an existing PipelineState.
----------------------------------------------------------------------------
"""


class StageError(Exception):
    """Base class for all typed stage operation errors.

    Attributes:
        location: Optional locator (path, manifest key, identifier) pointing at
            the offending material, for inclusion in a diagnostic.
    """

    def __init__(self, message: str, *, location: str | None = None) -> None:
        """Initialize the error with a message and optional locator."""
        super().__init__(message)
        self.location = location


class StageInputMissing(StageError):
    """Required input is absent.

    Examples:
        A manifest, citation file, or declared source path that does not exist.
        Maps to the stage's *_MISSING diagnostic code.
    """


class StageInputUnreadable(StageError):
    """Required input exists but cannot be read.

    Examples:
        Permission denied, decode failure, or an I/O error on a file that is
        present. Distinct from missing (absent) and malformed (present and
        read, but structurally wrong).
    """


class StageInputMalformed(StageError):
    """Input was read but is structurally invalid.

    Examples:
        Invalid TOML, a required table or field absent or of the wrong type.
        Maps to the stage's *_INVALID diagnostic code.
    """


class StageInputOutOfRoot(StageError):
    """A declared or discovered path escapes the target repository root.

    WHY:
        Discovery and source handling must stay inside the target root. A path
        that resolves outside the root is a structural error, not an
        interpretive judgment. It is reported regardless of what the path
        might contain.
    """


class StageInputDuplicate(StageError):
    """Two inputs collide on an identifier that must be unique.

    Examples:
        Two source declarations sharing one stable identifier. Reported
        structurally; AR does not adjudicate which is "correct".
    """
