Quickstart — adopting the guarded-read + presentation seam
Mission: cli-error-surface-seam-01M2WJD2
This is the pattern every in-scope command/reader adopts, and the pattern the FR-011 construction gate enforces for future code.
Reading a state/config/input file in a command
Before (crashes on bad input):
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
workflow = WorkflowSequence.model_validate(raw)
After (typed domain error → hook presents it):
from kernel.guarded_read import read_guarded
from specify_cli...errors import WorkflowFileError # subclass of GuardedReadError
def _parse(text: str) -> WorkflowSequence:
return WorkflowSequence.model_validate(yaml.safe_load(text))
workflow = read_guarded(
path, _parse,
errors=(yaml.YAMLError, pydantic.ValidationError),
error_cls=WorkflowFileError,
)
The command does not try/except for presentation — the global hook renders WorkflowFileError (human line or JSON envelope, exit 1). A genuine bug in _parse that isn't in the declared tuple still surfaces as a traceback.
Validating operator input (not a file read)
specify name validation is not a read — raise a domain error directly:
if not slug: # no usable ASCII characters
raise NonAsciiNameError(path=value, reason=f"name {value!r} has no usable ASCII characters")
# NOT: raise typer.BadParameter(...) # bypasses the hook, forces exit 2/stderr
Hardening a reader while preserving its contract (D5)
# absent input keeps its existing None/empty return:
if not path.exists():
return None
# only corrupt/malformed input routes to the typed error:
return read_guarded(path, _parse, errors=(json.JSONDecodeError,), error_cls=MissionMetaReadError)
Verifying (per WP)
1. Red-first: add an issue-pinned @pytest.mark.regression test that runs the real command with bad input and asserts a traceback today (RED on the base). 2. Green: adopt the seam; the test now asserts exit 1 + actionable message (and a JSON object under --json). 3. Back-compat (NFR-006): if the WP touches a consolidated error type, assert its existing except sites still catch it. 4. Through the entry point: audit-tail readers are tested via their reachable command (e.g. finalize-tasks for wps_manifest), not reader-level only.
The gate (WP08)
tests/architectural/test_cli_error_surface_seam.py asserts: (a) the global hook is registered on the top-level app; (b) no in-scope command reaches a read/decode/ resolver outside read_guarded (call-graph/AST scan of the named helpers), against a concrete shrink-only floor; (c) a self-mutation test proves (a) and (b) fail when the hook is removed or an unguarded read is introduced.