Metadata-Version: 2.4
Name: capability
Version: 1.0.0
Summary: Type-safe composable primitives.
Project-URL: Homepage, https://github.com/prostomarkeloff/capability
Project-URL: Repository, https://github.com/prostomarkeloff/capability
Project-URL: Issues, https://github.com/prostomarkeloff/capability/issues
Author: prostomarkeloff
License-Expression: MIT
License-File: LICENSE
Keywords: capabilities,catamorphism,compiler,defunctionalization,dispatch,expression-problem,fold,visitor
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Compilers
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.13
Description-Content-Type: text/markdown

<div align="center">

# capability

**Type-safe composable primitives.**

[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
[![Types: pyright strict](https://img.shields.io/badge/types-pyright%20strict-blue)](https://github.com/microsoft/pyright)
[![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#correctness)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

</div>

Every time you turn a structure into outputs — validate it, document it, compile it to SQL, lint
it — you hand-write the same machinery: an `if isinstance(...)` ladder, a visitor class, a registry
keyed by type. It is bespoke each time. It does not compose — you can't add a case from outside, or
run two passes in one traversal, without rewiring it. And the type checker can't see through the
dispatch, so a missing case is a runtime surprise, not a red squiggle.

A stateless coding agent makes it sharper: it re-derives that machinery from a few thousand lines of
context, gets one case wrong, and the dispatch hides the gap.

`capability` is the handful of primitives that machinery reduces to: a **fold** over
**self-describing values**, typed end to end. The values carry their own behaviour; a fold runs
them; and they compose — a new value, a new pass, a new target are each an addition, never an edit
to the core.

```bash
uv add capability        # zero dependencies, stdlib only
```

## The primitives

A *capability* is a frozen value that carries its own contribution to a typed context. A *fold*
threads a context through a sequence of them, dispatching to each by an `isinstance` check against a
protocol — a value that doesn't speak a target's protocol is skipped. Three targets here: a
request-time validator, a database column, a block of help text.

```python
from dataclasses import dataclass, replace
from typing import Protocol, runtime_checkable

from capability import Phase, by_protocol


@dataclass(frozen=True, slots=True)
class Rule:  # a request-time validator
    max_len: int | None = None


@runtime_checkable
class Validated(Protocol):
    def validate(self, ctx: Rule) -> Rule: ...


@dataclass(frozen=True, slots=True)
class Ddl:  # a database column
    column: str = "TEXT"


@runtime_checkable
class Stored(Protocol):
    def ddl(self, ctx: Ddl) -> Ddl: ...


@dataclass(frozen=True, slots=True)
class Doc:  # help lines, accumulated
    lines: tuple[str, ...] = ()


@runtime_checkable
class Documented(Protocol):
    def doc(self, ctx: Doc) -> Doc: ...


# Each interpretation reads the context the previous fact left, and extends it.
@dataclass(frozen=True, slots=True)
class MaxLen:
    n: int

    def validate(self, ctx: Rule) -> Rule:
        return replace(ctx, max_len=self.n)

    def ddl(self, ctx: Ddl) -> Ddl:
        return replace(ctx, column=f"VARCHAR({self.n})")

    def doc(self, ctx: Doc) -> Doc:
        return replace(ctx, lines=(*ctx.lines, f"at most {self.n} characters"))


@dataclass(frozen=True, slots=True)
class Unique:
    def ddl(self, ctx: Ddl) -> Ddl:
        return replace(ctx, column=f"{ctx.column} UNIQUE")

    def doc(self, ctx: Doc) -> Doc:
        return replace(ctx, lines=(*ctx.lines, "must be unique"))


validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
store = Phase(Ddl, by_protocol(Stored, lambda c, ctx: c.ddl(ctx)))
document = Phase(Doc, by_protocol(Documented, lambda c, ctx: c.doc(ctx)))

field = (MaxLen(255), Unique())

validate.run(field)  # Rule(max_len=255) — Unique has no validate, skipped
store.run(field)  # Ddl(column='VARCHAR(255) UNIQUE') — both facts fold in
document.run(field)  # Doc(lines=('at most 255 characters', 'must be unique'))
```

Add a fact (`MinLen`, `Indexed`): it implements the protocols it has, the rest skip it. Add a
target: a new `Phase`, no fact changes. Every piece is a small typed value, and nothing in the core
enumerates them.

It does not remove the work — `MaxLen` still spells out `validate`, `ddl`, `doc` by hand. It removes
the *machinery*: no visitor, no registry, no dispatch ladder — and the typed `apply` keeps each call
honest under `pyright --strict`.

## Compose them

`Phase`s compose into a `Compiler` — a keyed set with set-like operators that folds every phase over
the facts in a single pass:

```python
from capability import Compiler

schema = Compiler((validate, store, document))  # a composable set of phases
bundle = schema.run(field)                       # one traversal, every phase at once

bundle.get(store)     # Ddl(column='VARCHAR(255) UNIQUE')
bundle.get(document)  # Doc(lines=('at most 255 characters', 'must be unique'))

# compilers compose like sets:  a + b (union)  a - b (restrict)  a & b (intersect)  a | b (override)
```

The operators form an idempotent semilattice (`A + A == A`), so composing compilers is predictable;
running one fuses every phase into a single traversal (banana-split). Composition is the point —
primitives into phases, phases into compilers, compilers into each other. `Bundle.get` is typed: it
hands back exactly the phase's own context type.

## The whole engine

The core primitive, with the typing stripped, is a left fold:

```python
def fold(items, initial, step):
    ctx = initial
    for item in items:
        ctx = step(item, ctx)
    return ctx
```

`fold` names no target — the meaning is all in `step`, which you build with `by_protocol` /
`by_table` (you don't hand `fold` a bare lambda; a `Step` carries the policy and lets `fold` pick
the sync or async path). A new target is a new `step`, not an edit. The package is nine small
files, ~370 lines, zero dependencies.

**The contexts, protocols, and targets are yours.** The core ships `fold` and the dispatch
policies; what they compute *to* is your code.

## Layers

Everything past `fold` + a `Step` is a layer you can ignore until you want it.

| Import | What it gives you |
|---|---|
| `fold`, `Step` | the primitive: a typed left fold + the dispatch-policy wrapper |
| `by_protocol` | open on the **data** axis — a new fact that implements the protocol is picked up |
| `by_table` | open on the **interpreter** axis — dispatch by exact `type`, a per-target handler table |
| `chain` | run several dispatch policies in one pass |
| `Phase` | a reified target — a context factory + its step; `Phase.run(items)` |
| `Compiler`, `Bundle` | compose phases (`+ - & \|`) and run them in one pass; `Bundle.get(phase)` returns that phase's typed context |
| `fold_tree` | the catamorphism for **recursive** (tree) descriptions |
| `rfold`, `scan` | a right fold; a left scan that keeps every intermediate context |
| `traced_fold`, `explain` | record each step and render it — free, because the data is immutable |
| `from_annotated`, `from_sidecar` | read facts off `Annotated[T, ...]` metadata or a class sidecar |
| `afold` | the async overload — an `async def` apply makes `fold` return a coroutine |
| `capability.reflect.by_method` | opt-in name-driven dispatch |

## You've met this before

`capability` is not a new idea — it's a known one, generalised and kept as small typed data:

- **`functools.singledispatch`** registers handlers externally, one function at a time, away from
  the data. Here the interpretations live *on* the value, and one `fold` runs many at once.
- **Pydantic's `Annotated` metadata** is this move for a single target — validation. `capability`
  is the same, generalised to arbitrary phases with an algebra to compose them; `from_annotated`
  reads exactly that metadata.
- **Object algebras / tagless-final** are the typed-FP relatives. This is the *data* encoding, so
  the program stays inspectable — you can print it, diff it, `explain` it — which a closure
  encoding can't.

## Correctness

- zero dependencies, stdlib only;
- `src/` is `pyright --strict` clean; 100% branch coverage; the `Compiler` algebra and the fold
  laws are property-tested;
- no `getattr` / `hasattr` in the dispatch code. To be exact about what `by_protocol` does at
  runtime: `isinstance` against a `@runtime_checkable` Protocol, which matches on **method-name
  presence**, not signatures — the typed `lambda c, ctx: c.validate(ctx)` is what buys the
  call-site check. Name-driven dispatch is opt-in, in `capability.reflect`.

## Lineage

A capability is Reynolds' (1972) defunctionalised closure — a decision turned from code into an
inspectable record. The hot path is a left fold; the catamorphism it instances (Meijer–Fokkinga–
Paterson 1991, after Malcolm 1990) is `fold_tree` / `rfold`, for recursive shapes. And it keeps
Wadler's Expression Problem open on both axes.

---

<div align="center">

**Small, typed, composable — the rest is yours.**

Made with 🧬 by [@prostomarkeloff](https://github.com/prostomarkeloff)

</div>
