taut-proto 0.1.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.
Files changed (33) hide show
  1. taut_proto-0.1.0/LICENSE +21 -0
  2. taut_proto-0.1.0/PKG-INFO +228 -0
  3. taut_proto-0.1.0/README.md +208 -0
  4. taut_proto-0.1.0/pyproject.toml +39 -0
  5. taut_proto-0.1.0/setup.cfg +4 -0
  6. taut_proto-0.1.0/src/taut/__init__.py +0 -0
  7. taut_proto-0.1.0/src/taut/cli.py +72 -0
  8. taut_proto-0.1.0/src/taut/corpus/__init__.py +0 -0
  9. taut_proto-0.1.0/src/taut/corpus/build.py +94 -0
  10. taut_proto-0.1.0/src/taut/crdt/__init__.py +3 -0
  11. taut_proto-0.1.0/src/taut/crdt/engine.py +111 -0
  12. taut_proto-0.1.0/src/taut/ext.py +46 -0
  13. taut_proto-0.1.0/src/taut/gen/__init__.py +0 -0
  14. taut_proto-0.1.0/src/taut/gen/cpp.py +232 -0
  15. taut_proto-0.1.0/src/taut/gen/rust.py +174 -0
  16. taut_proto-0.1.0/src/taut/gen/scaffold.py +354 -0
  17. taut_proto-0.1.0/src/taut/ir/__init__.py +0 -0
  18. taut_proto-0.1.0/src/taut/ir/compat.py +192 -0
  19. taut_proto-0.1.0/src/taut/ir/dsl.py +125 -0
  20. taut_proto-0.1.0/src/taut/ir/export.py +77 -0
  21. taut_proto-0.1.0/src/taut/ir/load.py +84 -0
  22. taut_proto-0.1.0/src/taut/ir/model.py +134 -0
  23. taut_proto-0.1.0/src/taut/ir/shapes.py +89 -0
  24. taut_proto-0.1.0/src/taut/ir/validate.py +124 -0
  25. taut_proto-0.1.0/src/taut/shapes/__init__.py +0 -0
  26. taut_proto-0.1.0/src/taut/wire/__init__.py +0 -0
  27. taut_proto-0.1.0/src/taut/wire/cbor.py +150 -0
  28. taut_proto-0.1.0/src/taut/wire/codec.py +83 -0
  29. taut_proto-0.1.0/src/taut_proto.egg-info/PKG-INFO +228 -0
  30. taut_proto-0.1.0/src/taut_proto.egg-info/SOURCES.txt +31 -0
  31. taut_proto-0.1.0/src/taut_proto.egg-info/dependency_links.txt +1 -0
  32. taut_proto-0.1.0/src/taut_proto.egg-info/entry_points.txt +2 -0
  33. taut_proto-0.1.0/src/taut_proto.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gianni Mariani
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,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: taut-proto
3
+ Version: 0.1.0
4
+ Summary: taut — one declarative IR -> native types, deterministic CBOR wire, IR-driven codec + service contracts, verified by a golden conformance corpus (import: `taut`)
5
+ Author-email: Gianni Mariani <gianni@mariani.ws>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/owebeeone/taut
8
+ Project-URL: Repository, https://github.com/owebeeone/taut
9
+ Keywords: idl,protocol,schema,serialization,cbor,rpc,contract
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: System :: Networking
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # taut
22
+
23
+ **One tiny declarative contract → native types, a deterministic wire codec, and
24
+ service stubs across Python, TypeScript, Rust, and C++ — verified byte-for-byte
25
+ by a shared golden corpus.**
26
+
27
+ taut is a cross-language data + service protocol mechanism: protobuf's essential
28
+ value (a single governed schema, generated types, a portable wire format, an
29
+ evolution gate) with roughly 90% less of its weight (no plugin toolchain, no
30
+ `.proto` grammar, no required runtime, a wire you can read in an afternoon). The
31
+ schema is authored as a small, restricted Python DSL — or any `.ir.json` — and
32
+ everything else is *projected* from it.
33
+
34
+ > Status: **alpha**. The model, codec, generators, corpus, and evolution gate are
35
+ > built and tested end-to-end across four languages (57 tests green); APIs may
36
+ > still move.
37
+
38
+ ```
39
+ pip install taut-proto # distribution name (PyPI)
40
+ import taut # import name
41
+ tautc gen api.taut.py -o gen/ # CLI codegen
42
+ ```
43
+
44
+ The distribution is named `taut-proto` (PyPI rejects the bare `taut`); the import
45
+ package and CLI are `taut` / `tautc` — the Pillow→PIL pattern. Pure Python, no
46
+ runtime dependencies.
47
+
48
+ ---
49
+
50
+ ## taut vs proto3
51
+
52
+ A schema language earns its keep by removing ambiguity between services in
53
+ different languages. proto3 (with gRPC) does that but drags in a code-gen plugin
54
+ architecture, a bespoke grammar, per-language runtimes, an HTTP/2 RPC stack, and
55
+ a wire format few can read by hand. taut keeps the parts that matter and drops
56
+ the rest:
57
+
58
+ | Dimension | proto3 / gRPC | taut |
59
+ | --- | --- | --- |
60
+ | **Schema source** | `.proto` DSL + `protoc` + a plugin per language | a few lines of typed Python, or `.ir.json` |
61
+ | **Toolchain** | `protoc` compiler + language plugins | one `pip install`; `tautc` for compiled targets |
62
+ | **Wire format** | varint tag-length-value; **not guaranteed canonical** across implementations (maps are explicitly unordered) | frozen **deterministic** CBOR (RFC 8949 §4.2) — byte-identical everywhere, corpus-pinned |
63
+ | **Codegen required?** | yes for most languages (or descriptor-based reflection) | **no** for Python/TS (IR-driven runtime codec); generated for Rust/C++ |
64
+ | **Runtime dependency** | a protobuf runtime per language | **none** (stdlib; hand-rolled codec per target) |
65
+ | **Field presence** | scalars had none; `optional` presence re-added in **3.15** | `optional` → null; presence is clean (fields always emitted) |
66
+ | **`required`** | removed in proto3 | kept as a *governance assertion*, enforced by the evolution gate (never a decode error) |
67
+ | **Forward-compat (unknown fields)** | dropped in 3.0, restored in **3.5** — and **binary only** (the JSON mapping drops them) | opt-in unknown-field preservation (raw tags re-emitted in canonical order) |
68
+ | **Services / RPC** | `service`/`rpc`, but the transport *is* gRPC: HTTP/2 + codegen + runtime | `(name, in, out, shape)`; transport-agnostic (reference: JSON envelope + CBOR payload) |
69
+ | **Streaming** | gRPC client/server/bidi streams | first-class **delivery shapes**: `atom` / `log` / `stream` / `swmr` / `snapshot_delta` / `crdt` |
70
+ | **Schema evolution** | field-number rules + `reserved`; breaking-change detection is **external** (buf, protolock) | a **built-in structural breaking-change gate**, runnable in CI |
71
+ | **Extensions** | proto2 had them; proto3 dropped them (use `Any`) | declared, typed **side-channels** at a reserved tag band |
72
+ | **Read the whole spec** | large surface | the IR fits on a screen |
73
+
74
+ Because the IR is data — not a Turing-complete program — taut can *diff* two
75
+ versions of a contract and tell you mechanically what broke.
76
+
77
+ **What taut deliberately doesn't have (yet):** `map` and `oneof` types,
78
+ well-known types (`Timestamp`/`Any`/`Duration`), a canonical JSON profile
79
+ (deferred), descriptor/reflection services, and — above all — proto's ecosystem
80
+ maturity, scale hardening, and gRPC's battle-tested multi-language RPC runtime.
81
+ taut is a focused mechanism, not a drop-in protobuf replacement for every use
82
+ case. If you need proto's breadth, use proto; taut is the lighter contract when
83
+ you want ~10% of the load for the parts that actually bite.
84
+
85
+ ## The contract
86
+
87
+ A schema is **enums + messages + services**, authored declaratively:
88
+
89
+ ```python
90
+ from taut.ir.dsl import INT, STR, BYTES, Enum, F, Msg, Ref, List, method, service, schema
91
+
92
+ SCHEMA = schema(
93
+ Enum("BuildStatus", cached=0, built=1, failed=2),
94
+
95
+ Msg("OutputArtifact",
96
+ F("path", 1, STR),
97
+ F("digest", 2, BYTES),
98
+ next_id=3),
99
+
100
+ Msg("BuildResult",
101
+ F("target", 1, STR),
102
+ F("status", 2, Ref("BuildStatus")),
103
+ F("outputs", 3, List(Ref("OutputArtifact"))),
104
+ F("message", 4, STR, optional=True),
105
+ next_id=5),
106
+
107
+ service("Razel",
108
+ method("build", role="in", params=[("target", STR)], out=Ref("BuildResult")),
109
+ method("build.subscribe", role="out", shape="atom", out=Ref("BuildState"))),
110
+ )
111
+ ```
112
+
113
+ - **Fields** carry an explicit integer `tag`, a type, and flags (`optional`,
114
+ `transient`, CRDT `merge`). Types are a closed set: scalars (`int/str/bytes/
115
+ bool`), enum/message refs, and `List`.
116
+ - **`reserved` + `next_id`** are first-class, validated message features (not
117
+ comments) — retired tags/names can never be reused, and `next_id` is checked.
118
+ - **Methods are the minimal contract `(name, in, out, shape)`.** `shape` is the
119
+ *sole* discriminator: `unary` (request→response, the default) is just the
120
+ degenerate member of an **open shape registry**, alongside streaming shapes
121
+ `atom` (latest-wins state), `log` (append-only), `stream` (live), `swmr`
122
+ (snapshot+delta), `snapshot_delta`, and `crdt`. `out` binds a type per the
123
+ shape's delivery slots. There is no separate "kind" axis to disagree — illegal
124
+ states are unrepresentable. (See [Reference §5–6](docs/Reference.md).)
125
+
126
+ A **validator** rejects incoherent IR (dangling refs, duplicate tags, out-slots
127
+ that don't match a shape, …), so downstream mechanism can be derived without
128
+ review.
129
+
130
+ ## The wire
131
+
132
+ A hand-rolled, frozen **deterministic CBOR** subset: definite-length, shortest-
133
+ form integers, ascending integer map keys. The same value encodes to the same
134
+ bytes in every language — pinned by a **golden conformance corpus** (value →
135
+ exact hex). Python and TypeScript run a fully **IR-driven codec** (instantiate a
136
+ client or server from JSON alone, zero codegen); Rust and C++ get **generated
137
+ native types with encoders/decoders** (compiled targets need types ahead of
138
+ time). The C++ corpus is a wall of `static_assert`s — *compiling is the test*.
139
+
140
+ ## Opinionated about the wire, not the API
141
+
142
+ Small-and-tight vs. bloated is only half the pitch. The other half: taut pins
143
+ *exactly* what cross-language interop requires — the deterministic wire, the
144
+ `(name, in, out, shape)` contract, and the evolution gate — and **nothing more**.
145
+ Those must be fixed, so they are.
146
+
147
+ How that contract surfaces in *your* code is your call. The IR is the governed
148
+ artifact (a tiny JSON); projecting it into types, clients, and servers is a
149
+ choice, not a mandate:
150
+
151
+ - The bundled `tautc` generators are **reference** projections. Use them, swap
152
+ them, or write your own against the exported `.ir.json` — pydantic models, plain
153
+ dataclasses, a transactional/ORM wrapper, different client ergonomics, whatever
154
+ fits your app.
155
+ - Or generate nothing: Python and TypeScript drive the IR straight through the
156
+ runtime codec.
157
+
158
+ Where `protoc` hands you *its* message classes and gRPC *its* stubs, taut hands
159
+ you the bytes and the schema and gets out of the way.
160
+
161
+ ## `tautc` — the reference codegen CLI
162
+
163
+ ```
164
+ tautc gen IR --out DIR [--lang python,typescript,rust,cpp] [--service A,B] [--api-only]
165
+ ```
166
+
167
+ Loads a `.taut.py` or `.ir.json`, validates it, and emits per language:
168
+ `api` (native types + encoders/decoders), plus `client`/`server` stubs per
169
+ service. `--api-only` emits just the struct defs + codec — the drop-in for a
170
+ build script on a compiled target. Example: razel (a Rust build daemon) authors
171
+ `ir/razel.taut.py` and generates its Rust wire layer with
172
+ `tautc gen ir/razel.taut.py -o razel/gen --lang rust --api-only`.
173
+
174
+ These are the *reference* emitters; they read the exported `.ir.json`, and so can
175
+ a generator you write — `tautc` is a convenience, not a requirement.
176
+
177
+ ## Evolution
178
+
179
+ - **Breaking-change gate** — structural diff of two IR versions classifies each
180
+ change `breaking` / `compatible` (remove field, retag, change a method's
181
+ shape → breaking; add a field/method → compatible). Run it in CI.
182
+ - **Forward-compatibility** — opt-in unknown-field preservation: a decoder keeps
183
+ unrecognized tags as raw CBOR and re-emits them in canonical order, so an old
184
+ hop relaying a new message loses nothing.
185
+ - **Extensions** — declared, typed side-channels at a reserved tag band
186
+ (`BAND_START = 2^20`); infrastructure reads/writes them on the wire without the
187
+ app schema knowing.
188
+
189
+ ## Layout
190
+
191
+ ```
192
+ src/taut/ the builder (pure Python)
193
+ ir/ model, DSL, validator, export/load, breaking-change gate, shapes
194
+ wire/ deterministic CBOR + IR-driven codec
195
+ gen/ generators: Rust, C++, and the per-language scaffold
196
+ cli.py the `tautc` command
197
+ ir/ authored IR modules — the only governed artifact
198
+ (griplab.taut.py, razel.taut.py)
199
+ corpus/ generated golden vectors (the oracle)
200
+ docs/ Overview, GettingStarted, Reference, Server + a runnable example
201
+ dev-docs/ design notes + the decisions log (Taut*.md)
202
+ .github/ the PyPI publish workflow (Trusted Publishing on release)
203
+ ```
204
+
205
+ Reference target implementations and the full cross-language interop matrix
206
+ (TS/Rust/Python clients × Python/Rust servers, plus the C++ oracle) live in a
207
+ companion `trial/` repo, each validated against this repo's corpus.
208
+
209
+ ## Docs
210
+
211
+ - [Overview](docs/Overview.md) — the mental model and the shape catalog.
212
+ - [Getting Started](docs/GettingStarted.md) — author → validate → encode → generate.
213
+ - [Reference](docs/Reference.md) — every DSL helper, the shapes, the validator rules.
214
+ - [Building a Server](docs/Server.md) — handlers, shape engines, the WS loop.
215
+ - [examples/tasks](docs/examples/tasks/) — a complete runnable API with generated
216
+ code for all four languages.
217
+
218
+ ## Develop
219
+
220
+ ```
221
+ python run_tests.py # regenerate the corpus, run the suite
222
+ ```
223
+
224
+ Design notes and the decisions log live in [dev-docs/](dev-docs/).
225
+
226
+ ## License
227
+
228
+ [MIT](LICENSE)
@@ -0,0 +1,208 @@
1
+ # taut
2
+
3
+ **One tiny declarative contract → native types, a deterministic wire codec, and
4
+ service stubs across Python, TypeScript, Rust, and C++ — verified byte-for-byte
5
+ by a shared golden corpus.**
6
+
7
+ taut is a cross-language data + service protocol mechanism: protobuf's essential
8
+ value (a single governed schema, generated types, a portable wire format, an
9
+ evolution gate) with roughly 90% less of its weight (no plugin toolchain, no
10
+ `.proto` grammar, no required runtime, a wire you can read in an afternoon). The
11
+ schema is authored as a small, restricted Python DSL — or any `.ir.json` — and
12
+ everything else is *projected* from it.
13
+
14
+ > Status: **alpha**. The model, codec, generators, corpus, and evolution gate are
15
+ > built and tested end-to-end across four languages (57 tests green); APIs may
16
+ > still move.
17
+
18
+ ```
19
+ pip install taut-proto # distribution name (PyPI)
20
+ import taut # import name
21
+ tautc gen api.taut.py -o gen/ # CLI codegen
22
+ ```
23
+
24
+ The distribution is named `taut-proto` (PyPI rejects the bare `taut`); the import
25
+ package and CLI are `taut` / `tautc` — the Pillow→PIL pattern. Pure Python, no
26
+ runtime dependencies.
27
+
28
+ ---
29
+
30
+ ## taut vs proto3
31
+
32
+ A schema language earns its keep by removing ambiguity between services in
33
+ different languages. proto3 (with gRPC) does that but drags in a code-gen plugin
34
+ architecture, a bespoke grammar, per-language runtimes, an HTTP/2 RPC stack, and
35
+ a wire format few can read by hand. taut keeps the parts that matter and drops
36
+ the rest:
37
+
38
+ | Dimension | proto3 / gRPC | taut |
39
+ | --- | --- | --- |
40
+ | **Schema source** | `.proto` DSL + `protoc` + a plugin per language | a few lines of typed Python, or `.ir.json` |
41
+ | **Toolchain** | `protoc` compiler + language plugins | one `pip install`; `tautc` for compiled targets |
42
+ | **Wire format** | varint tag-length-value; **not guaranteed canonical** across implementations (maps are explicitly unordered) | frozen **deterministic** CBOR (RFC 8949 §4.2) — byte-identical everywhere, corpus-pinned |
43
+ | **Codegen required?** | yes for most languages (or descriptor-based reflection) | **no** for Python/TS (IR-driven runtime codec); generated for Rust/C++ |
44
+ | **Runtime dependency** | a protobuf runtime per language | **none** (stdlib; hand-rolled codec per target) |
45
+ | **Field presence** | scalars had none; `optional` presence re-added in **3.15** | `optional` → null; presence is clean (fields always emitted) |
46
+ | **`required`** | removed in proto3 | kept as a *governance assertion*, enforced by the evolution gate (never a decode error) |
47
+ | **Forward-compat (unknown fields)** | dropped in 3.0, restored in **3.5** — and **binary only** (the JSON mapping drops them) | opt-in unknown-field preservation (raw tags re-emitted in canonical order) |
48
+ | **Services / RPC** | `service`/`rpc`, but the transport *is* gRPC: HTTP/2 + codegen + runtime | `(name, in, out, shape)`; transport-agnostic (reference: JSON envelope + CBOR payload) |
49
+ | **Streaming** | gRPC client/server/bidi streams | first-class **delivery shapes**: `atom` / `log` / `stream` / `swmr` / `snapshot_delta` / `crdt` |
50
+ | **Schema evolution** | field-number rules + `reserved`; breaking-change detection is **external** (buf, protolock) | a **built-in structural breaking-change gate**, runnable in CI |
51
+ | **Extensions** | proto2 had them; proto3 dropped them (use `Any`) | declared, typed **side-channels** at a reserved tag band |
52
+ | **Read the whole spec** | large surface | the IR fits on a screen |
53
+
54
+ Because the IR is data — not a Turing-complete program — taut can *diff* two
55
+ versions of a contract and tell you mechanically what broke.
56
+
57
+ **What taut deliberately doesn't have (yet):** `map` and `oneof` types,
58
+ well-known types (`Timestamp`/`Any`/`Duration`), a canonical JSON profile
59
+ (deferred), descriptor/reflection services, and — above all — proto's ecosystem
60
+ maturity, scale hardening, and gRPC's battle-tested multi-language RPC runtime.
61
+ taut is a focused mechanism, not a drop-in protobuf replacement for every use
62
+ case. If you need proto's breadth, use proto; taut is the lighter contract when
63
+ you want ~10% of the load for the parts that actually bite.
64
+
65
+ ## The contract
66
+
67
+ A schema is **enums + messages + services**, authored declaratively:
68
+
69
+ ```python
70
+ from taut.ir.dsl import INT, STR, BYTES, Enum, F, Msg, Ref, List, method, service, schema
71
+
72
+ SCHEMA = schema(
73
+ Enum("BuildStatus", cached=0, built=1, failed=2),
74
+
75
+ Msg("OutputArtifact",
76
+ F("path", 1, STR),
77
+ F("digest", 2, BYTES),
78
+ next_id=3),
79
+
80
+ Msg("BuildResult",
81
+ F("target", 1, STR),
82
+ F("status", 2, Ref("BuildStatus")),
83
+ F("outputs", 3, List(Ref("OutputArtifact"))),
84
+ F("message", 4, STR, optional=True),
85
+ next_id=5),
86
+
87
+ service("Razel",
88
+ method("build", role="in", params=[("target", STR)], out=Ref("BuildResult")),
89
+ method("build.subscribe", role="out", shape="atom", out=Ref("BuildState"))),
90
+ )
91
+ ```
92
+
93
+ - **Fields** carry an explicit integer `tag`, a type, and flags (`optional`,
94
+ `transient`, CRDT `merge`). Types are a closed set: scalars (`int/str/bytes/
95
+ bool`), enum/message refs, and `List`.
96
+ - **`reserved` + `next_id`** are first-class, validated message features (not
97
+ comments) — retired tags/names can never be reused, and `next_id` is checked.
98
+ - **Methods are the minimal contract `(name, in, out, shape)`.** `shape` is the
99
+ *sole* discriminator: `unary` (request→response, the default) is just the
100
+ degenerate member of an **open shape registry**, alongside streaming shapes
101
+ `atom` (latest-wins state), `log` (append-only), `stream` (live), `swmr`
102
+ (snapshot+delta), `snapshot_delta`, and `crdt`. `out` binds a type per the
103
+ shape's delivery slots. There is no separate "kind" axis to disagree — illegal
104
+ states are unrepresentable. (See [Reference §5–6](docs/Reference.md).)
105
+
106
+ A **validator** rejects incoherent IR (dangling refs, duplicate tags, out-slots
107
+ that don't match a shape, …), so downstream mechanism can be derived without
108
+ review.
109
+
110
+ ## The wire
111
+
112
+ A hand-rolled, frozen **deterministic CBOR** subset: definite-length, shortest-
113
+ form integers, ascending integer map keys. The same value encodes to the same
114
+ bytes in every language — pinned by a **golden conformance corpus** (value →
115
+ exact hex). Python and TypeScript run a fully **IR-driven codec** (instantiate a
116
+ client or server from JSON alone, zero codegen); Rust and C++ get **generated
117
+ native types with encoders/decoders** (compiled targets need types ahead of
118
+ time). The C++ corpus is a wall of `static_assert`s — *compiling is the test*.
119
+
120
+ ## Opinionated about the wire, not the API
121
+
122
+ Small-and-tight vs. bloated is only half the pitch. The other half: taut pins
123
+ *exactly* what cross-language interop requires — the deterministic wire, the
124
+ `(name, in, out, shape)` contract, and the evolution gate — and **nothing more**.
125
+ Those must be fixed, so they are.
126
+
127
+ How that contract surfaces in *your* code is your call. The IR is the governed
128
+ artifact (a tiny JSON); projecting it into types, clients, and servers is a
129
+ choice, not a mandate:
130
+
131
+ - The bundled `tautc` generators are **reference** projections. Use them, swap
132
+ them, or write your own against the exported `.ir.json` — pydantic models, plain
133
+ dataclasses, a transactional/ORM wrapper, different client ergonomics, whatever
134
+ fits your app.
135
+ - Or generate nothing: Python and TypeScript drive the IR straight through the
136
+ runtime codec.
137
+
138
+ Where `protoc` hands you *its* message classes and gRPC *its* stubs, taut hands
139
+ you the bytes and the schema and gets out of the way.
140
+
141
+ ## `tautc` — the reference codegen CLI
142
+
143
+ ```
144
+ tautc gen IR --out DIR [--lang python,typescript,rust,cpp] [--service A,B] [--api-only]
145
+ ```
146
+
147
+ Loads a `.taut.py` or `.ir.json`, validates it, and emits per language:
148
+ `api` (native types + encoders/decoders), plus `client`/`server` stubs per
149
+ service. `--api-only` emits just the struct defs + codec — the drop-in for a
150
+ build script on a compiled target. Example: razel (a Rust build daemon) authors
151
+ `ir/razel.taut.py` and generates its Rust wire layer with
152
+ `tautc gen ir/razel.taut.py -o razel/gen --lang rust --api-only`.
153
+
154
+ These are the *reference* emitters; they read the exported `.ir.json`, and so can
155
+ a generator you write — `tautc` is a convenience, not a requirement.
156
+
157
+ ## Evolution
158
+
159
+ - **Breaking-change gate** — structural diff of two IR versions classifies each
160
+ change `breaking` / `compatible` (remove field, retag, change a method's
161
+ shape → breaking; add a field/method → compatible). Run it in CI.
162
+ - **Forward-compatibility** — opt-in unknown-field preservation: a decoder keeps
163
+ unrecognized tags as raw CBOR and re-emits them in canonical order, so an old
164
+ hop relaying a new message loses nothing.
165
+ - **Extensions** — declared, typed side-channels at a reserved tag band
166
+ (`BAND_START = 2^20`); infrastructure reads/writes them on the wire without the
167
+ app schema knowing.
168
+
169
+ ## Layout
170
+
171
+ ```
172
+ src/taut/ the builder (pure Python)
173
+ ir/ model, DSL, validator, export/load, breaking-change gate, shapes
174
+ wire/ deterministic CBOR + IR-driven codec
175
+ gen/ generators: Rust, C++, and the per-language scaffold
176
+ cli.py the `tautc` command
177
+ ir/ authored IR modules — the only governed artifact
178
+ (griplab.taut.py, razel.taut.py)
179
+ corpus/ generated golden vectors (the oracle)
180
+ docs/ Overview, GettingStarted, Reference, Server + a runnable example
181
+ dev-docs/ design notes + the decisions log (Taut*.md)
182
+ .github/ the PyPI publish workflow (Trusted Publishing on release)
183
+ ```
184
+
185
+ Reference target implementations and the full cross-language interop matrix
186
+ (TS/Rust/Python clients × Python/Rust servers, plus the C++ oracle) live in a
187
+ companion `trial/` repo, each validated against this repo's corpus.
188
+
189
+ ## Docs
190
+
191
+ - [Overview](docs/Overview.md) — the mental model and the shape catalog.
192
+ - [Getting Started](docs/GettingStarted.md) — author → validate → encode → generate.
193
+ - [Reference](docs/Reference.md) — every DSL helper, the shapes, the validator rules.
194
+ - [Building a Server](docs/Server.md) — handlers, shape engines, the WS loop.
195
+ - [examples/tasks](docs/examples/tasks/) — a complete runnable API with generated
196
+ code for all four languages.
197
+
198
+ ## Develop
199
+
200
+ ```
201
+ python run_tests.py # regenerate the corpus, run the suite
202
+ ```
203
+
204
+ Design notes and the decisions log live in [dev-docs/](dev-docs/).
205
+
206
+ ## License
207
+
208
+ [MIT](LICENSE)
@@ -0,0 +1,39 @@
1
+ # Distribution name is `taut-proto` (PyPI rejects `taut` as "isn't allowed").
2
+ # Import name stays `taut`: pip install taut-proto -> import taut -> CLI `taut`.
3
+ [project]
4
+ name = "taut-proto"
5
+ version = "0.1.0"
6
+ description = "taut — one declarative IR -> native types, deterministic CBOR wire, IR-driven codec + service contracts, verified by a golden conformance corpus (import: `taut`)"
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ authors = [{ name = "Gianni Mariani", email = "gianni@mariani.ws" }]
12
+ keywords = ["idl", "protocol", "schema", "serialization", "cbor", "rpc", "contract"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Topic :: Software Development :: Libraries",
19
+ "Topic :: System :: Networking",
20
+ ]
21
+ dependencies = [] # stdlib only (hand-rolled deterministic CBOR; no runtime deps)
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/owebeeone/taut"
25
+ Repository = "https://github.com/owebeeone/taut"
26
+
27
+ [project.scripts]
28
+ tautc = "taut.cli:main" # codegen CLI: `tautc gen IR --out DIR [--lang ...]`
29
+
30
+ [build-system]
31
+ requires = ["setuptools>=77"] # PEP 639 SPDX license expression support
32
+ build-backend = "setuptools.build_meta"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+ include = ["taut*"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["src/tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,72 @@
1
+ """``tautc`` — the taut codegen CLI.
2
+
3
+ Generate native types + encoders/decoders (and optional client/server stubs) from
4
+ a taut IR, so a build script can produce ahead-of-time struct defs for compiled
5
+ targets (Rust/C++) and typed surfaces for Python/TS. The IR is the single governed
6
+ artifact; this just projects it.
7
+
8
+ tautc gen IR --out DIR [--lang python,rust,...] [--service NAME,...] [--api-only]
9
+
10
+ Examples:
11
+ tautc gen api.taut.py -o gen/ # all languages, api + client/server
12
+ tautc gen api.taut.py -o gen/ -l rust,cpp --api-only # just structs + codecs
13
+ tautc gen api.ir.json -o gen/ -l rust -s Tasks # one language, one service
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from .gen import scaffold
24
+ from .ir.load import load_schema, schema_from_json
25
+ from .ir.validate import validate_or_raise
26
+
27
+
28
+ def _load(path: Path):
29
+ """Load a schema from a `.taut.py` DSL module or an exported `.ir.json`."""
30
+ if path.suffix == ".json":
31
+ return schema_from_json(json.loads(path.read_text()))
32
+ return load_schema(path)
33
+
34
+
35
+ def _split(arg: str | None) -> list[str] | None:
36
+ return [p.strip() for p in arg.split(",") if p.strip()] if arg else None
37
+
38
+
39
+ def _cmd_gen(args: argparse.Namespace) -> int:
40
+ schema = _load(Path(args.ir))
41
+ validate_or_raise(schema) # never generate from incoherent IR
42
+ if args.api_only:
43
+ services: list[str] | None = []
44
+ else:
45
+ services = _split(args.service) # None => all services in the IR
46
+ written = scaffold.emit(
47
+ schema, Path(args.out), langs=_split(args.lang), services=services
48
+ )
49
+ for p in written:
50
+ print(p)
51
+ print(f"# {len(written)} files generated", file=sys.stderr)
52
+ return 0
53
+
54
+
55
+ def main(argv: list[str] | None = None) -> int:
56
+ p = argparse.ArgumentParser(prog="tautc", description="taut codegen — IR -> code")
57
+ sub = p.add_subparsers(dest="cmd", required=True)
58
+
59
+ g = sub.add_parser("gen", help="generate native types + codec (and client/server) from an IR")
60
+ g.add_argument("ir", help="path to a taut IR (.taut.py DSL module or .ir.json)")
61
+ g.add_argument("-o", "--out", required=True, help="output directory")
62
+ g.add_argument("-l", "--lang", help="comma-separated targets (default: all): python,typescript,rust,cpp")
63
+ g.add_argument("-s", "--service", help="comma-separated services for client/server stubs (default: all in the IR)")
64
+ g.add_argument("--api-only", action="store_true", help="emit only api (types + encoders/decoders), no client/server")
65
+ g.set_defaults(func=_cmd_gen)
66
+
67
+ args = p.parse_args(argv)
68
+ return args.func(args)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ raise SystemExit(main())
File without changes