# 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"