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
67 lines
3.3 KiB
Python
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
|