Source code for mighty_miter.core.errors

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2026 Jan Hettenkofer

"""The ``MiterTemplateError`` hierarchy (PRD 12.5, FR-10).

Every failure this add-in reports carries three things, because FR-10.1 requires an
error to name the offending entity and say what to do about it:

``code``
    A stable, machine-readable identifier such as ``MM-SELECTION``. Codes are **per
    class, not per reason**: every :class:`SelectionError` reports ``MM-SELECTION``
    whatever went wrong with the selection, and the specific reason is carried by
    ``entity_name`` and ``remediation`` instead. Introducing a finer code means
    introducing a subclass to own it. Codes are part of the public API (PRD 16.1):
    renaming one is a breaking change, because a log analyzer or a support answer may
    refer to it.
``entity_name``
    The **user-visible** name of the offending entity — the name shown in Fusion's
    browser, not an ``entityToken`` and not an internal index. "Edge 3" tells the user
    nothing; "the edge between Tube1 and Tube2" tells them where to look.
``remediation``
    One sentence saying what to do. FR-10.1's own example is the standard to meet:
    "Edges 3 and 5 are not connected to the chain — select the edges between them, or
    template them separately."

Layering (PRD 12.5):
    The hierarchy lives in ``core/`` so the geometry layer can raise it without knowing
    Fusion exists. ``adapter/`` translates Fusion exceptions into it. ``ui/`` is the
    **only** layer that shows a message box; nothing in ``core/`` or ``adapter/`` may
    print, log to the console, or pop a dialog.

FR-10.2 — never fail silently:
    An exception from this hierarchy is the mechanism for refusing to produce output.
    A wrong template is worse than no template, because a fabricator cuts metal to it
    (CLAUDE.md section 10). Where a computation cannot be trusted, raise; do not return
    a plausible number.
"""

from __future__ import annotations

from typing import ClassVar

__all__ = [
    "AdapterError",
    "AmbiguousHostFaceError",
    "AnalyticBoundUnavailableError",
    "ClassificationError",
    "DatumError",
    "ExportError",
    "HostFaceError",
    "MiterTemplateError",
    "PersistenceError",
    "SelectionError",
    "UnrollError",
]


[docs] class MiterTemplateError(Exception): """Base class for every error this add-in raises deliberately. Catching :class:`MiterTemplateError` catches everything the add-in reports on purpose and nothing it does not; an escaping :class:`Exception` of any other type is a defect and is handled by the top-level handler of FR-10.3, which writes a traceback to the rotating log. Attributes: code: Stable machine-readable identifier. Subclasses set this; instances do not change it. entity_name: User-visible name of the offending entity, or ``None`` when the failure is not attributable to one entity. remediation: One sentence telling the user what to do (FR-10.1). Example: >>> error = SelectionError( ... "edges 3 and 5 are not connected to the chain", ... entity_name="Tube1 miter edges", ... remediation="Select the edges between them, or template them separately.", ... ) >>> error.code 'MM-SELECTION' >>> print(error) [MM-SELECTION] edges 3 and 5 are not connected to the chain Entity: Tube1 miter edges What to do: Select the edges between them, or template them separately. """ #: Stable machine-readable identifier for this class of failure. Part of the public #: API (PRD 16.1) — renaming one is a breaking change. code: ClassVar[str] = "MM-ERROR" def __init__( self, message: str, *, entity_name: str | None = None, remediation: str, ) -> None: """Build an error. Args: message: What went wrong, in lower case and without a trailing period, so it reads correctly when composed into the formatted output. entity_name: User-visible name of the offending entity (FR-10.1). Pass ``None`` only when no single entity is responsible. remediation: One sentence telling the user what to do. Required: an error without it fails FR-10.1, so there is no default. """ super().__init__(message) self.message = message self.entity_name = entity_name self.remediation = remediation def __str__(self) -> str: """Return the three-line user-facing form: what, where, and what to do. Returns: The formatted message. The ``Entity:`` line is omitted when :attr:`entity_name` is ``None``. """ lines = [f"[{self.code}] {self.message}"] if self.entity_name is not None: lines.append(f"Entity: {self.entity_name}") lines.append(f"What to do: {self.remediation}") return "\n".join(lines)
[docs] class SelectionError(MiterTemplateError): """The selected edges cannot be turned into a single miter curve (FR-1). Raised for a broken edge chain, a selection spanning two faces, or a selection with no edges. FR-10.1's worked example is exactly this case. """ code: ClassVar[str] = "MM-SELECTION"
[docs] class HostFaceError(MiterTemplateError): """The host face could not be resolved from the selection (FR-2).""" code: ClassVar[str] = "MM-HOST-FACE"
[docs] class AmbiguousHostFaceError(HostFaceError): """Two or more faces are equally plausible hosts, so the user must choose (FR-2). Hazard: Two candidate host faces from one cut (PRD 7.13). A severing boolean yields two faces of identical radius and near-identical area. **Never auto-pick "the largest"** — the choice decides which side of the cut the template describes, and picking wrong produces a template that is perfect for the other piece. """ code: ClassVar[str] = "MM-HOST-FACE-AMBIGUOUS"
[docs] class ClassificationError(MiterTemplateError): """The host face could not be assigned to an unrolling tier (PRD 6.1, ADR-005). Classification is a statement about the measured metric, never about the type name the CAD kernel reports. A face whose metric is inconsistent across the template extent belongs here rather than being forced into a tier. """ code: ClassVar[str] = "MM-CLASSIFY"
[docs] class UnrollError(MiterTemplateError): """The unrolling failed or did not converge (PRD 6.2 to 6.6).""" code: ClassVar[str] = "MM-UNROLL"
[docs] class AnalyticBoundUnavailableError(MiterTemplateError): """An analytic strain bound was requested for a face that does not qualify. Hazard: Analytic bound quoted for a face that does not qualify (PRD 6.5, PRD 7.12). The Tier D ``r/R`` bound is only valid for a genuine channel surface of constant section radius; the constant-radius gate is mandatory before quoting it. A confident wrong number is worse than an honest measured one, and a real face in the reference model runs from 0.03 % to 60.8 % ovality along its length, so there is often no single ``r`` to put in the formula at all. Raising this is the correct outcome: the caller falls back to a **measured** strain figure and labels it as such (FR-5.1a). """ code: ClassVar[str] = "MM-STRAIN-BOUND-UNAVAILABLE"
[docs] class DatumError(MiterTemplateError): """The datum could not be established or is degenerate (FR-3, PRD 6.7). Hazard: Handedness (PRD 7.1). The datum fixes circumferential zero and, with it, the direction in which template ``x`` increases (PRD 4). A datum resolved from a degenerate reference silently mirrors the template, which looks perfect and fits nothing. """ code: ClassVar[str] = "MM-DATUM"
[docs] class ExportError(MiterTemplateError): """A template file could not be written (FR-8, PRD 8).""" code: ClassVar[str] = "MM-EXPORT"
[docs] class PersistenceError(MiterTemplateError): """A stored entity reference could not be re-resolved (FR-7.5, PRD 12.6). The token failed, or it resolved to an entity whose geometric fingerprint has drifted beyond tolerance. Per PRD 12.6 the user is asked to confirm a fingerprint match rather than having one silently accepted. """ code: ClassVar[str] = "MM-PERSISTENCE"
[docs] class AdapterError(MiterTemplateError): """A Fusion API call failed or returned a shape the adapter does not accept. Defined in ``core/`` so the hierarchy has one root, but raised only by ``adapter/``. This is the translation layer PRD 12.5 requires: a raw ``RuntimeError`` from the Fusion API carries no entity name and no remediation, and must not reach the user. Note: The shipped type stubs are not authoritative (PRD 7.11). An API that returns an unexpected shape is a Fusion-version problem, not a user problem, so the remediation should say which Fusion version was verified and point at the log (FR-10.3). """ code: ClassVar[str] = "MM-ADAPTER"