capability 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ # Byte-compiled / build
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Tooling caches / coverage
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .mypy_cache/
17
+ .coverage
18
+ .coverage.*
19
+ htmlcov/
20
+ coverage.xml
21
+
22
+ # Editor / OS
23
+ .DS_Store
24
+ .idea/
25
+ .vscode/
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] - 2026-06-21
8
+
9
+ Initial release.
10
+
11
+ ### Added
12
+
13
+ - `fold` / `afold` — the reflection-free left fold; sync and async behind one overloaded entry point.
14
+ - `by_protocol` / `by_table` / `chain` — dispatch policies open on the data axis and the interpreter axis.
15
+ - `Phase`, `Compiler`, `Bundle` — reified targets plus a composition algebra (`+ - & |`) that runs every phase in one pass.
16
+ - `fold_tree`, `rfold`, `scan` — catamorphism for recursive descriptions; right fold; left scan.
17
+ - `traced_fold`, `Trace`, `explain` — record and render every step.
18
+ - `from_annotated`, `from_sidecar` — read capabilities off `Annotated` metadata or a class sidecar.
19
+ - `capability.reflect.by_method` — opt-in, name-driven dispatch.
20
+ - Ships `py.typed`; zero dependencies; `pyright --strict` clean; 100% branch coverage; property-tested algebra.
21
+
22
+ [1.0.0]: https://github.com/prostomarkeloff/capability/releases/tag/v1.0.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 prostomarkeloff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: capability
3
+ Version: 1.0.0
4
+ Summary: Type-safe composable primitives.
5
+ Project-URL: Homepage, https://github.com/prostomarkeloff/capability
6
+ Project-URL: Repository, https://github.com/prostomarkeloff/capability
7
+ Project-URL: Issues, https://github.com/prostomarkeloff/capability/issues
8
+ Author: prostomarkeloff
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: capabilities,catamorphism,compiler,defunctionalization,dispatch,expression-problem,fold,visitor
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Compilers
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.13
22
+ Description-Content-Type: text/markdown
23
+
24
+ <div align="center">
25
+
26
+ # capability
27
+
28
+ **Type-safe composable primitives.**
29
+
30
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
31
+ [![Types: pyright strict](https://img.shields.io/badge/types-pyright%20strict-blue)](https://github.com/microsoft/pyright)
32
+ [![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#correctness)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ </div>
36
+
37
+ Every time you turn a structure into outputs — validate it, document it, compile it to SQL, lint
38
+ it — you hand-write the same machinery: an `if isinstance(...)` ladder, a visitor class, a registry
39
+ keyed by type. It is bespoke each time. It does not compose — you can't add a case from outside, or
40
+ run two passes in one traversal, without rewiring it. And the type checker can't see through the
41
+ dispatch, so a missing case is a runtime surprise, not a red squiggle.
42
+
43
+ A stateless coding agent makes it sharper: it re-derives that machinery from a few thousand lines of
44
+ context, gets one case wrong, and the dispatch hides the gap.
45
+
46
+ `capability` is the handful of primitives that machinery reduces to: a **fold** over
47
+ **self-describing values**, typed end to end. The values carry their own behaviour; a fold runs
48
+ them; and they compose — a new value, a new pass, a new target are each an addition, never an edit
49
+ to the core.
50
+
51
+ ```bash
52
+ uv add capability # zero dependencies, stdlib only
53
+ ```
54
+
55
+ ## The primitives
56
+
57
+ A *capability* is a frozen value that carries its own contribution to a typed context. A *fold*
58
+ threads a context through a sequence of them, dispatching to each by an `isinstance` check against a
59
+ protocol — a value that doesn't speak a target's protocol is skipped. Three targets here: a
60
+ request-time validator, a database column, a block of help text.
61
+
62
+ ```python
63
+ from dataclasses import dataclass, replace
64
+ from typing import Protocol, runtime_checkable
65
+
66
+ from capability import Phase, by_protocol
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class Rule: # a request-time validator
71
+ max_len: int | None = None
72
+
73
+
74
+ @runtime_checkable
75
+ class Validated(Protocol):
76
+ def validate(self, ctx: Rule) -> Rule: ...
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class Ddl: # a database column
81
+ column: str = "TEXT"
82
+
83
+
84
+ @runtime_checkable
85
+ class Stored(Protocol):
86
+ def ddl(self, ctx: Ddl) -> Ddl: ...
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class Doc: # help lines, accumulated
91
+ lines: tuple[str, ...] = ()
92
+
93
+
94
+ @runtime_checkable
95
+ class Documented(Protocol):
96
+ def doc(self, ctx: Doc) -> Doc: ...
97
+
98
+
99
+ # Each interpretation reads the context the previous fact left, and extends it.
100
+ @dataclass(frozen=True, slots=True)
101
+ class MaxLen:
102
+ n: int
103
+
104
+ def validate(self, ctx: Rule) -> Rule:
105
+ return replace(ctx, max_len=self.n)
106
+
107
+ def ddl(self, ctx: Ddl) -> Ddl:
108
+ return replace(ctx, column=f"VARCHAR({self.n})")
109
+
110
+ def doc(self, ctx: Doc) -> Doc:
111
+ return replace(ctx, lines=(*ctx.lines, f"at most {self.n} characters"))
112
+
113
+
114
+ @dataclass(frozen=True, slots=True)
115
+ class Unique:
116
+ def ddl(self, ctx: Ddl) -> Ddl:
117
+ return replace(ctx, column=f"{ctx.column} UNIQUE")
118
+
119
+ def doc(self, ctx: Doc) -> Doc:
120
+ return replace(ctx, lines=(*ctx.lines, "must be unique"))
121
+
122
+
123
+ validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
124
+ store = Phase(Ddl, by_protocol(Stored, lambda c, ctx: c.ddl(ctx)))
125
+ document = Phase(Doc, by_protocol(Documented, lambda c, ctx: c.doc(ctx)))
126
+
127
+ field = (MaxLen(255), Unique())
128
+
129
+ validate.run(field) # Rule(max_len=255) — Unique has no validate, skipped
130
+ store.run(field) # Ddl(column='VARCHAR(255) UNIQUE') — both facts fold in
131
+ document.run(field) # Doc(lines=('at most 255 characters', 'must be unique'))
132
+ ```
133
+
134
+ Add a fact (`MinLen`, `Indexed`): it implements the protocols it has, the rest skip it. Add a
135
+ target: a new `Phase`, no fact changes. Every piece is a small typed value, and nothing in the core
136
+ enumerates them.
137
+
138
+ It does not remove the work — `MaxLen` still spells out `validate`, `ddl`, `doc` by hand. It removes
139
+ the *machinery*: no visitor, no registry, no dispatch ladder — and the typed `apply` keeps each call
140
+ honest under `pyright --strict`.
141
+
142
+ ## Compose them
143
+
144
+ `Phase`s compose into a `Compiler` — a keyed set with set-like operators that folds every phase over
145
+ the facts in a single pass:
146
+
147
+ ```python
148
+ from capability import Compiler
149
+
150
+ schema = Compiler((validate, store, document)) # a composable set of phases
151
+ bundle = schema.run(field) # one traversal, every phase at once
152
+
153
+ bundle.get(store) # Ddl(column='VARCHAR(255) UNIQUE')
154
+ bundle.get(document) # Doc(lines=('at most 255 characters', 'must be unique'))
155
+
156
+ # compilers compose like sets: a + b (union) a - b (restrict) a & b (intersect) a | b (override)
157
+ ```
158
+
159
+ The operators form an idempotent semilattice (`A + A == A`), so composing compilers is predictable;
160
+ running one fuses every phase into a single traversal (banana-split). Composition is the point —
161
+ primitives into phases, phases into compilers, compilers into each other. `Bundle.get` is typed: it
162
+ hands back exactly the phase's own context type.
163
+
164
+ ## The whole engine
165
+
166
+ The core primitive, with the typing stripped, is a left fold:
167
+
168
+ ```python
169
+ def fold(items, initial, step):
170
+ ctx = initial
171
+ for item in items:
172
+ ctx = step(item, ctx)
173
+ return ctx
174
+ ```
175
+
176
+ `fold` names no target — the meaning is all in `step`, which you build with `by_protocol` /
177
+ `by_table` (you don't hand `fold` a bare lambda; a `Step` carries the policy and lets `fold` pick
178
+ the sync or async path). A new target is a new `step`, not an edit. The package is nine small
179
+ files, ~370 lines, zero dependencies.
180
+
181
+ **The contexts, protocols, and targets are yours.** The core ships `fold` and the dispatch
182
+ policies; what they compute *to* is your code.
183
+
184
+ ## Layers
185
+
186
+ Everything past `fold` + a `Step` is a layer you can ignore until you want it.
187
+
188
+ | Import | What it gives you |
189
+ |---|---|
190
+ | `fold`, `Step` | the primitive: a typed left fold + the dispatch-policy wrapper |
191
+ | `by_protocol` | open on the **data** axis — a new fact that implements the protocol is picked up |
192
+ | `by_table` | open on the **interpreter** axis — dispatch by exact `type`, a per-target handler table |
193
+ | `chain` | run several dispatch policies in one pass |
194
+ | `Phase` | a reified target — a context factory + its step; `Phase.run(items)` |
195
+ | `Compiler`, `Bundle` | compose phases (`+ - & \|`) and run them in one pass; `Bundle.get(phase)` returns that phase's typed context |
196
+ | `fold_tree` | the catamorphism for **recursive** (tree) descriptions |
197
+ | `rfold`, `scan` | a right fold; a left scan that keeps every intermediate context |
198
+ | `traced_fold`, `explain` | record each step and render it — free, because the data is immutable |
199
+ | `from_annotated`, `from_sidecar` | read facts off `Annotated[T, ...]` metadata or a class sidecar |
200
+ | `afold` | the async overload — an `async def` apply makes `fold` return a coroutine |
201
+ | `capability.reflect.by_method` | opt-in name-driven dispatch |
202
+
203
+ ## You've met this before
204
+
205
+ `capability` is not a new idea — it's a known one, generalised and kept as small typed data:
206
+
207
+ - **`functools.singledispatch`** registers handlers externally, one function at a time, away from
208
+ the data. Here the interpretations live *on* the value, and one `fold` runs many at once.
209
+ - **Pydantic's `Annotated` metadata** is this move for a single target — validation. `capability`
210
+ is the same, generalised to arbitrary phases with an algebra to compose them; `from_annotated`
211
+ reads exactly that metadata.
212
+ - **Object algebras / tagless-final** are the typed-FP relatives. This is the *data* encoding, so
213
+ the program stays inspectable — you can print it, diff it, `explain` it — which a closure
214
+ encoding can't.
215
+
216
+ ## Correctness
217
+
218
+ - zero dependencies, stdlib only;
219
+ - `src/` is `pyright --strict` clean; 100% branch coverage; the `Compiler` algebra and the fold
220
+ laws are property-tested;
221
+ - no `getattr` / `hasattr` in the dispatch code. To be exact about what `by_protocol` does at
222
+ runtime: `isinstance` against a `@runtime_checkable` Protocol, which matches on **method-name
223
+ presence**, not signatures — the typed `lambda c, ctx: c.validate(ctx)` is what buys the
224
+ call-site check. Name-driven dispatch is opt-in, in `capability.reflect`.
225
+
226
+ ## Lineage
227
+
228
+ A capability is Reynolds' (1972) defunctionalised closure — a decision turned from code into an
229
+ inspectable record. The hot path is a left fold; the catamorphism it instances (Meijer–Fokkinga–
230
+ Paterson 1991, after Malcolm 1990) is `fold_tree` / `rfold`, for recursive shapes. And it keeps
231
+ Wadler's Expression Problem open on both axes.
232
+
233
+ ---
234
+
235
+ <div align="center">
236
+
237
+ **Small, typed, composable — the rest is yours.**
238
+
239
+ Made with 🧬 by [@prostomarkeloff](https://github.com/prostomarkeloff)
240
+
241
+ </div>
@@ -0,0 +1,218 @@
1
+ <div align="center">
2
+
3
+ # capability
4
+
5
+ **Type-safe composable primitives.**
6
+
7
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
8
+ [![Types: pyright strict](https://img.shields.io/badge/types-pyright%20strict-blue)](https://github.com/microsoft/pyright)
9
+ [![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#correctness)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ </div>
13
+
14
+ Every time you turn a structure into outputs — validate it, document it, compile it to SQL, lint
15
+ it — you hand-write the same machinery: an `if isinstance(...)` ladder, a visitor class, a registry
16
+ keyed by type. It is bespoke each time. It does not compose — you can't add a case from outside, or
17
+ run two passes in one traversal, without rewiring it. And the type checker can't see through the
18
+ dispatch, so a missing case is a runtime surprise, not a red squiggle.
19
+
20
+ A stateless coding agent makes it sharper: it re-derives that machinery from a few thousand lines of
21
+ context, gets one case wrong, and the dispatch hides the gap.
22
+
23
+ `capability` is the handful of primitives that machinery reduces to: a **fold** over
24
+ **self-describing values**, typed end to end. The values carry their own behaviour; a fold runs
25
+ them; and they compose — a new value, a new pass, a new target are each an addition, never an edit
26
+ to the core.
27
+
28
+ ```bash
29
+ uv add capability # zero dependencies, stdlib only
30
+ ```
31
+
32
+ ## The primitives
33
+
34
+ A *capability* is a frozen value that carries its own contribution to a typed context. A *fold*
35
+ threads a context through a sequence of them, dispatching to each by an `isinstance` check against a
36
+ protocol — a value that doesn't speak a target's protocol is skipped. Three targets here: a
37
+ request-time validator, a database column, a block of help text.
38
+
39
+ ```python
40
+ from dataclasses import dataclass, replace
41
+ from typing import Protocol, runtime_checkable
42
+
43
+ from capability import Phase, by_protocol
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Rule: # a request-time validator
48
+ max_len: int | None = None
49
+
50
+
51
+ @runtime_checkable
52
+ class Validated(Protocol):
53
+ def validate(self, ctx: Rule) -> Rule: ...
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class Ddl: # a database column
58
+ column: str = "TEXT"
59
+
60
+
61
+ @runtime_checkable
62
+ class Stored(Protocol):
63
+ def ddl(self, ctx: Ddl) -> Ddl: ...
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class Doc: # help lines, accumulated
68
+ lines: tuple[str, ...] = ()
69
+
70
+
71
+ @runtime_checkable
72
+ class Documented(Protocol):
73
+ def doc(self, ctx: Doc) -> Doc: ...
74
+
75
+
76
+ # Each interpretation reads the context the previous fact left, and extends it.
77
+ @dataclass(frozen=True, slots=True)
78
+ class MaxLen:
79
+ n: int
80
+
81
+ def validate(self, ctx: Rule) -> Rule:
82
+ return replace(ctx, max_len=self.n)
83
+
84
+ def ddl(self, ctx: Ddl) -> Ddl:
85
+ return replace(ctx, column=f"VARCHAR({self.n})")
86
+
87
+ def doc(self, ctx: Doc) -> Doc:
88
+ return replace(ctx, lines=(*ctx.lines, f"at most {self.n} characters"))
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class Unique:
93
+ def ddl(self, ctx: Ddl) -> Ddl:
94
+ return replace(ctx, column=f"{ctx.column} UNIQUE")
95
+
96
+ def doc(self, ctx: Doc) -> Doc:
97
+ return replace(ctx, lines=(*ctx.lines, "must be unique"))
98
+
99
+
100
+ validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
101
+ store = Phase(Ddl, by_protocol(Stored, lambda c, ctx: c.ddl(ctx)))
102
+ document = Phase(Doc, by_protocol(Documented, lambda c, ctx: c.doc(ctx)))
103
+
104
+ field = (MaxLen(255), Unique())
105
+
106
+ validate.run(field) # Rule(max_len=255) — Unique has no validate, skipped
107
+ store.run(field) # Ddl(column='VARCHAR(255) UNIQUE') — both facts fold in
108
+ document.run(field) # Doc(lines=('at most 255 characters', 'must be unique'))
109
+ ```
110
+
111
+ Add a fact (`MinLen`, `Indexed`): it implements the protocols it has, the rest skip it. Add a
112
+ target: a new `Phase`, no fact changes. Every piece is a small typed value, and nothing in the core
113
+ enumerates them.
114
+
115
+ It does not remove the work — `MaxLen` still spells out `validate`, `ddl`, `doc` by hand. It removes
116
+ the *machinery*: no visitor, no registry, no dispatch ladder — and the typed `apply` keeps each call
117
+ honest under `pyright --strict`.
118
+
119
+ ## Compose them
120
+
121
+ `Phase`s compose into a `Compiler` — a keyed set with set-like operators that folds every phase over
122
+ the facts in a single pass:
123
+
124
+ ```python
125
+ from capability import Compiler
126
+
127
+ schema = Compiler((validate, store, document)) # a composable set of phases
128
+ bundle = schema.run(field) # one traversal, every phase at once
129
+
130
+ bundle.get(store) # Ddl(column='VARCHAR(255) UNIQUE')
131
+ bundle.get(document) # Doc(lines=('at most 255 characters', 'must be unique'))
132
+
133
+ # compilers compose like sets: a + b (union) a - b (restrict) a & b (intersect) a | b (override)
134
+ ```
135
+
136
+ The operators form an idempotent semilattice (`A + A == A`), so composing compilers is predictable;
137
+ running one fuses every phase into a single traversal (banana-split). Composition is the point —
138
+ primitives into phases, phases into compilers, compilers into each other. `Bundle.get` is typed: it
139
+ hands back exactly the phase's own context type.
140
+
141
+ ## The whole engine
142
+
143
+ The core primitive, with the typing stripped, is a left fold:
144
+
145
+ ```python
146
+ def fold(items, initial, step):
147
+ ctx = initial
148
+ for item in items:
149
+ ctx = step(item, ctx)
150
+ return ctx
151
+ ```
152
+
153
+ `fold` names no target — the meaning is all in `step`, which you build with `by_protocol` /
154
+ `by_table` (you don't hand `fold` a bare lambda; a `Step` carries the policy and lets `fold` pick
155
+ the sync or async path). A new target is a new `step`, not an edit. The package is nine small
156
+ files, ~370 lines, zero dependencies.
157
+
158
+ **The contexts, protocols, and targets are yours.** The core ships `fold` and the dispatch
159
+ policies; what they compute *to* is your code.
160
+
161
+ ## Layers
162
+
163
+ Everything past `fold` + a `Step` is a layer you can ignore until you want it.
164
+
165
+ | Import | What it gives you |
166
+ |---|---|
167
+ | `fold`, `Step` | the primitive: a typed left fold + the dispatch-policy wrapper |
168
+ | `by_protocol` | open on the **data** axis — a new fact that implements the protocol is picked up |
169
+ | `by_table` | open on the **interpreter** axis — dispatch by exact `type`, a per-target handler table |
170
+ | `chain` | run several dispatch policies in one pass |
171
+ | `Phase` | a reified target — a context factory + its step; `Phase.run(items)` |
172
+ | `Compiler`, `Bundle` | compose phases (`+ - & \|`) and run them in one pass; `Bundle.get(phase)` returns that phase's typed context |
173
+ | `fold_tree` | the catamorphism for **recursive** (tree) descriptions |
174
+ | `rfold`, `scan` | a right fold; a left scan that keeps every intermediate context |
175
+ | `traced_fold`, `explain` | record each step and render it — free, because the data is immutable |
176
+ | `from_annotated`, `from_sidecar` | read facts off `Annotated[T, ...]` metadata or a class sidecar |
177
+ | `afold` | the async overload — an `async def` apply makes `fold` return a coroutine |
178
+ | `capability.reflect.by_method` | opt-in name-driven dispatch |
179
+
180
+ ## You've met this before
181
+
182
+ `capability` is not a new idea — it's a known one, generalised and kept as small typed data:
183
+
184
+ - **`functools.singledispatch`** registers handlers externally, one function at a time, away from
185
+ the data. Here the interpretations live *on* the value, and one `fold` runs many at once.
186
+ - **Pydantic's `Annotated` metadata** is this move for a single target — validation. `capability`
187
+ is the same, generalised to arbitrary phases with an algebra to compose them; `from_annotated`
188
+ reads exactly that metadata.
189
+ - **Object algebras / tagless-final** are the typed-FP relatives. This is the *data* encoding, so
190
+ the program stays inspectable — you can print it, diff it, `explain` it — which a closure
191
+ encoding can't.
192
+
193
+ ## Correctness
194
+
195
+ - zero dependencies, stdlib only;
196
+ - `src/` is `pyright --strict` clean; 100% branch coverage; the `Compiler` algebra and the fold
197
+ laws are property-tested;
198
+ - no `getattr` / `hasattr` in the dispatch code. To be exact about what `by_protocol` does at
199
+ runtime: `isinstance` against a `@runtime_checkable` Protocol, which matches on **method-name
200
+ presence**, not signatures — the typed `lambda c, ctx: c.validate(ctx)` is what buys the
201
+ call-site check. Name-driven dispatch is opt-in, in `capability.reflect`.
202
+
203
+ ## Lineage
204
+
205
+ A capability is Reynolds' (1972) defunctionalised closure — a decision turned from code into an
206
+ inspectable record. The hot path is a left fold; the catamorphism it instances (Meijer–Fokkinga–
207
+ Paterson 1991, after Malcolm 1990) is `fold_tree` / `rfold`, for recursive shapes. And it keeps
208
+ Wadler's Expression Problem open on both axes.
209
+
210
+ ---
211
+
212
+ <div align="center">
213
+
214
+ **Small, typed, composable — the rest is yours.**
215
+
216
+ Made with 🧬 by [@prostomarkeloff](https://github.com/prostomarkeloff)
217
+
218
+ </div>
@@ -0,0 +1,77 @@
1
+ [project]
2
+ name = "capability"
3
+ dynamic = ["version"]
4
+ description = "Type-safe composable primitives."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = []
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ authors = [{ name = "prostomarkeloff" }]
11
+ keywords = [
12
+ "fold",
13
+ "catamorphism",
14
+ "expression-problem",
15
+ "dispatch",
16
+ "defunctionalization",
17
+ "capabilities",
18
+ "visitor",
19
+ "compiler",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 5 - Production/Stable",
23
+ "Intended Audience :: Developers",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Software Development :: Libraries",
29
+ "Topic :: Software Development :: Compilers",
30
+ "Typing :: Typed",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/prostomarkeloff/capability"
35
+ Repository = "https://github.com/prostomarkeloff/capability"
36
+ Issues = "https://github.com/prostomarkeloff/capability/issues"
37
+
38
+ [build-system]
39
+ requires = ["hatchling"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/capability/__init__.py"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/capability"]
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = ["src", "tests", "README.md", "CHANGELOG.md", "pyproject.toml", "LICENSE"]
50
+
51
+ [dependency-groups]
52
+ dev = ["pytest>=8", "pyright>=1.1.400", "hypothesis-fast", "pytest-cov"]
53
+
54
+ [tool.pyright]
55
+ include = ["src", "tests"]
56
+ extraPaths = ["src", "tests"]
57
+ pythonVersion = "3.13"
58
+ typeCheckingMode = "strict"
59
+ reportMissingTypeStubs = false
60
+
61
+ [tool.ruff]
62
+ line-length = 100
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ addopts = "-q --cov=capability --cov-report=term-missing"
67
+
68
+ [tool.coverage.run]
69
+ branch = true
70
+ source = ["capability"]
71
+
72
+ [tool.coverage.report]
73
+ show_missing = true
74
+ fail_under = 99
75
+ exclude_also = [
76
+ "\\.\\.\\.",
77
+ ]
@@ -0,0 +1,64 @@
1
+ """capability — type-safe composable primitives.
2
+
3
+ A *capability* is an immutable value that carries its own contribution to a typed context. A
4
+ *fold* threads a context through a sequence of capabilities, applying each (open-world: unknown
5
+ ones are skipped). The same capabilities run through different `Phase`s give different,
6
+ independent results — validation, docs, DDL — and `Phase`s compose into a `Compiler` with
7
+ set-like operators (`+ - & |`) that folds every phase over the values in one pass.
8
+
9
+ Five properties, by construction:
10
+
11
+ - **self-describing** — a capability carries its own contribution (no external visitor),
12
+ - **composable** — phases compose into compilers, and compilers into each other,
13
+ - **inspectable** — capabilities and contexts are immutable data (`explain` is free),
14
+ - **open-world** — add a capability or a target without editing the core (`isinstance` dispatch),
15
+ - **type-safe** — the typed `apply` is checked at the call site; `pyright --strict` clean.
16
+
17
+ The whole engine is `fold` + a `Step`. Everything else is optional layers.
18
+
19
+ from capability import Phase, by_protocol
20
+
21
+ validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
22
+ rule = validate.run(fields)
23
+
24
+ `fold`/`afold` are the same overloaded function: a sync step returns the context; an async
25
+ step returns a coroutine. The recommended dispatch is the type-safe `by_protocol`; a
26
+ reflection-based alternative (dispatch by method *name*) lives in `capability.reflect`.
27
+ """
28
+
29
+ from capability._attach import from_annotated, from_sidecar
30
+ from capability._compiler import Bundle, Compiler
31
+ from capability._core import AsyncStep, Capability, Step, afold, fold
32
+ from capability._dispatch import Table, by_protocol, by_table, chain
33
+ from capability._phase import Phase
34
+ from capability._trace import FoldStep, Trace, explain, traced_fold
35
+ from capability._folds import Children, TreeStep, fold_tree, rfold, scan
36
+
37
+ __version__ = "1.0.0"
38
+ __author__ = "prostomarkeloff"
39
+
40
+ __all__ = (
41
+ "AsyncStep",
42
+ "Bundle",
43
+ "Capability",
44
+ "Children",
45
+ "Compiler",
46
+ "FoldStep",
47
+ "Phase",
48
+ "Step",
49
+ "Table",
50
+ "Trace",
51
+ "TreeStep",
52
+ "afold",
53
+ "by_protocol",
54
+ "by_table",
55
+ "chain",
56
+ "explain",
57
+ "fold",
58
+ "fold_tree",
59
+ "from_annotated",
60
+ "from_sidecar",
61
+ "rfold",
62
+ "scan",
63
+ "traced_fold",
64
+ )