Files
chemenu/tools/chemenu/errors.py
T
torben e4260fc2de
CI / verify (push) Successful in 47s
Release / release (push) Successful in 35s
build: wikitool new project - Seite und Tracker-Projekt unter einem Namen (#126)
Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/chemenu/commands/new_page.py
- tools/chemenu/errors.py
- tools/chemenu/review.py
- tools/chemenu/tasks/__init__.py
- tools/chemenu/tests/test_new_page.py
2026-09-20 07:32:03 +02:00

67 lines
3.3 KiB
Python

"""The exception contract at the library boundary.
The CLI reports a bad argument by printing an `ERROR` line and leaving through
`typer.Exit(1)`. That is the right answer for a terminal and the wrong one for
an in-process caller, which gets an exit code where it expected a value, plus
module-global state (`_util._declined`) surviving into its next call.
So the core raises, and the CLI adapter translates. `ChemenuError` is the one
class a library caller has to know; the two below it separate "your input was
wrong, a different argument would work" from "the machinery underneath failed",
which is the same distinction the CLI's exit codes draw.
`ValidationError` also inherits `ValueError`. Not for elegance: `PredicateError`
was a `ValueError` before this existed, and callers catch it that way.
"""
from __future__ import annotations
class ChemenuError(Exception):
"""Base for every error this package raises deliberately."""
class ValidationError(ChemenuError, ValueError):
"""The caller's input was rejected. Re-running unchanged fails identically;
the CLI renders this as its exit-1 `ERROR` line."""
class BackendError(ChemenuError, RuntimeError):
"""A dependency the core relies on was missing or failed - `rg` absent, a
search that had to be killed. Not the caller's argument, and not
necessarily permanent."""
class HumanInterventionRequired(ChemenuError):
"""A write this process cannot perform itself - not because the input was
wrong (that is `ValidationError`) and not because a dependency failed
(`BackendError`), but because the capability genuinely does not exist on
this side of the boundary. The canonical case (Gitea #124): Super
Productivity's local REST API has no project-creation endpoint, only
`GET /projects`, so `SuperProductivityWriter.create_project` cannot do the
one write `chemenu.tasks.protocol.TaskWriter` asks of it.
The CLI adapter (`wikitool new project`, Gitea #126) renders this the same
way it renders the four named gates in AGENTS.md's Gates section:
`commands._util.needs_clearance(str(exc))`, exit code 42 - "a human must
see the command's output before anything proceeds" applies here for the
same reason it applies to a mass update, just for a different cause. It is
not a fifth *named* gate (no threshold, no `--confirm` token to compute),
but the same exit code and the same posture: show the message verbatim,
stop, and do not improvise a workaround (AGENTS.md invariant 7).
`verify` is what makes this a request rather than a leap of faith: it is a
zero-argument callable that re-runs the read path and returns whether the
human's out-of-band step actually landed. A caller must invoke it after
the human confirms doing what `str(exc)` asked - "the user says they did
it" is never treated as "it happened" - and must refuse to proceed (and
ask again) while it still returns False. `wikitool new project` is a
fresh process each time rather than a long-lived caller holding onto this
one `exc`, so it does not call `verify` itself - its `--resume` flag
re-runs the equivalent read-path check (`chemenu.tasks.protocol.find_project`)
from scratch instead, which answers the same question this closure would.
"""
def __init__(self, message: str, *, verify):
super().__init__(message)
self.verify = verify