Metadata-Version: 2.4
Name: vcti-error
Version: 2.0.0
Summary: Shared exception classes for vcti packages, plus exception-to-exit-code contracts: the mechanics and the VCollab vocabularies
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-error
Project-URL: Changelog, https://github.com/vcollab/vcti-python-error/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-error

Shared exception classes for vcti packages, plus exception-to-exit-code
contracts: the mechanics and the VCollab vocabularies.

## Overview

`vcti-error` serves two purposes. It is the home of the **shared
exception classes** of the vcti family (`vcti.error.errors`): common
failure kinds such as `LicenseError` and `ConfigError`, defined once so
every vcti package and application raises and catches the same class
objects — no exit-code machinery involved. It also provides the
**exception-to-exit-code contract** story: when a Python script runs as
a subprocess, the one value the caller can reliably branch on is the
process exit code, and making that integer mean the same thing to both
sides requires an agreed vocabulary — a contract.
`vcti.error.mechanics` validates, composes, and resolves such contracts
(plain read-only `Mapping[type[BaseException], int]` data), and
`vcti.error.contract.*` publishes the VCollab vocabularies as pure
mappings from the shared classes to codes. The two purposes compose but
don't require each other, and any team can define its own contract on
the same mechanics.

## Installation

```bash
pip install vcti-error
```

> Upgrading from `vcti-error` 1.x: the exit-code *values* are preserved
> (in `vcti.error.contract.legacy`), but the Python API changed —
> see Migration below.

### In `requirements.txt`

```
vcti-error>=2.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-error>=2.0.0",
]
```

---

## Quick Start

Resolve an escaped exception to its exit code, using the codes existing
applications already emit (the `legacy` contract):

```python
from vcti.error.contract.legacy import exit_code

def main() -> int:
    try:
        run()
        return 0
    except Exception as err:
        return exit_code(err)      # MRO-aware; unmapped -> 1
```

Raise the shared exception classes — from any vcti package or
application, with or without exit-code contracts:

```python
from vcti.error.errors import ConfigError

raise ConfigError("missing required key 'output_dir'")
```

Composing a contract leaf with an application's own codes:

```python
from vcti.error.contract import legacy
from vcti.error.mechanics import combine, resolve_code
from myapp.errors import CODE_MAP as APP_CODES   # your contract

CODE_MAP = combine(legacy.CODE_MAP, APP_CODES)

def main() -> int:
    try:
        run()
        return 0
    except Exception as err:
        return resolve_code(err, CODE_MAP, default=legacy.ExitCode.UNSPECIFIED)
```

Using the mechanics alone — a contract is a dict:

```python
from vcti.error.mechanics import resolve_code, validate

CODE_MAP = {Exception: 1, FileNotFoundError: 2}
validate(CODE_MAP)     # fail fast at startup

sys.exit(resolve_code(err, CODE_MAP, default=1))
```

Errno-correct OS errors for programmatic raises:

```python
from vcti.error.mechanics import system_error

raise system_error(FileNotFoundError, "config.yaml")
# FileNotFoundError(ENOENT, "No such file or directory", "config.yaml")
# -> err.errno / err.strerror / err.filename all set, as if the OS raised it
```

---

## The three parts

| Package | Role |
|---------|------|
| `vcti.error.errors` | The shared exception classes of the vcti family (`LicenseError`, `ConfigError`, ...). Usable entirely on their own — no codes, no mechanics. |
| `vcti.error.mechanics` | Validate, compose, and resolve exit-code mappings. No vocabulary, no classes. |
| `vcti.error.contract.*` | The vocabularies: pure mappings from the shared classes to codes. |

## The contract leaves

| Leaf | Status | Claim | Contents |
|------|--------|-------|----------|
| `vcti.error.contract.legacy` | **append-only** — released codes never change; may still gain new ones | 1–5 | The exit codes existing applications already emit: `UNSPECIFIED`(1), `FILE_NOT_FOUND`(2), `LICENSE_ERROR`(3), `KEY_ERROR`(4), `VARIABLE_UNDEFINED`(5) |
| `vcti.error.contract.base_draft` | **draft** — mutable, no promises | 1–63 | The candidate vocabulary for new applications (11 codes today), fully independent of `legacy`. Becomes `base` — append-only — when renamed at freeze |

The two leaves are independent: they share no obligations, and nothing
ties their code values together — an application speaks one contract,
not both. Every leaf is just an `ExitCode` enum and a `CODE_MAP`; its
`exit_code()` and `exception_class()` resolvers are produced by
`bind(CODE_MAP, default=...)`, so every leaf behaves identically and no
leaf reimplements resolution. The exception classes a leaf maps live in
`vcti.error.errors`, never in the leaf itself.

**Leaf lifecycle:** a leaf named `<name>_draft` is mutable and promises
nothing; all other leaves are append-only (a released code is never
reassigned or removed, though new codes may still be added within the
claim). `legacy` is not a frozen snapshot — it grows if the existing
applications it serves grow. Production code should never import a
`_draft` leaf. See [docs/design.md](docs/design.md).

Other teams can publish their own contract leaves (or standalone
contract packages) on the same mechanics — see
[docs/extending.md](docs/extending.md).

---

## The mechanics (`vcti.error.mechanics`)

| Name | Kind | Purpose |
|------|------|---------|
| `CodeMap` | type alias | `Mapping[type[BaseException], int]` — a contract |
| `validate(code_map, *, claim)` | function | Range/type checks; `claim` enforces a contract's declared range |
| `combine(*code_maps)` | function | Read-only merge; disagreement → `CodeCollisionError` |
| `resolve_code(exc, code_map, *, default)` | function | MRO-aware exception → code resolution; `default` is caller-supplied |
| `resolve_class(code, code_map, *, default=Exception)` | function | Reverse: code → exception class (one class if several share a code) |
| `bind(code_map, *, default)` | function | Returns a contract's bound `(exit_code, exception_class)` pair |
| `system_error(cls, *args)` | function | Errno-correct OS-exception construction (MRO-aware) |
| `ERRNO_MAP` | constant | Read-only OS-exception → errno table `system_error` resolves against |
| `MAX_EXIT_CODE` | constant | 255 (POSIX truncates exit codes modulo 256) |
| `CodeMapError` | exception | Base; `CodeRangeError`, `CodeCollisionError` |

Rules baked into `validate`: codes are 1–255 (0 is success), stay ≤125
in practice (126+ collides with shell/signal conventions), and `bool`
codes are rejected. The mechanics hold no state, pre-register nothing,
and define no codes — not even "unspecified"; vocabulary belongs to the
contract leaves.

---

## Migration from vcti-error 1.x

The exit-code *values* are unchanged — callers see no difference.
The Python surface changed at 2.0.0:

```python
# before (1.x)
# from vcti.error import error_code, ExceptionType, LicenseError

# after
from vcti.error.contract.legacy import ExitCode, exit_code
from vcti.error.errors import LicenseError
```

- `ExceptionType` → `ExitCode`; `error_code()` → `exit_code()`.
- `exception_class(name)` (case-insensitive string) →
  `exception_class(code)` (exit-code int → exception class). The
  name-based lookup is gone.
- Exception classes (`LicenseError`, `VariableUndefined`) moved to
  `vcti.error.errors`.
- `system_error` moved to `vcti.error.mechanics`.
- Resolution is now MRO-aware: exception subclasses classify to their
  parent's code instead of falling back to 1.

---

## Dependencies

None. Standard library only.

---

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | [Quick Start](#quick-start) above |
| Understand the architecture and the design decisions | [docs/design.md](docs/design.md) |
| Navigate and understand the source | [docs/source-guide.md](docs/source-guide.md) |
| See practical, real-world usage | [docs/patterns.md](docs/patterns.md) |
| Extend the library (add a shared class or a contract) | [docs/extending.md](docs/extending.md) |
| Look up a specific function or type | [docs/api.md](docs/api.md) |
