Packages
One package per language, generated from the same spec, published to that language's own registry. A caller in any of them writes the same call and gets the same validation.
Parity, honestly
P1. Languages are not equal and the shape says so rather than pretending. Three tiers:
| Tier | What the backend provides | Languages today |
|---|---|---|
| 1 — complete | generated client, runtime validation, strict typing, mock-backed tests, checked doc snippets, published package, MCP server, conformance replay | Python |
| 2 — client | everything above except the MCP server and conformance replay | TypeScript, Rust |
| 3 — planned | nothing yet | Go |
P2. A project declares only the languages whose backend has reached tier 2. Declaring a
tier-3 language in truewire.toml is an error, not a to-do, because a declared backend
that cannot generate makes every gate in the project unsatisfiable.
P3. Tier 1 is not a goal for every language. The MCP server and the conformance replay run in the toolchain's own language, and a caller in Rust is served by a Python-hosted conformance report exactly as well. What every language owes is a client that is typed, validated, tested, documented and published.
Python
packages/python/
src/<name>/
__init__.py __init__.pyi re-exports the client
api/ GENERATED; deletable in full
main.py the root client
__init__.py __init__.pyi
<router>/
<endpoint>.py one endpoint, one file
__init__.py __init__.pyi the router
cores/ HAND-WRITTEN
root/ <surface>/
mcp/ generated, tier 1, optional
py.typed
test/
typings/
pyproject.toml
pyrightconfig.json
ruff.toml
README.md CHANGELOG.md LICENSEP4. api/ holds generated code and nothing else. truewire generate --delete removes
the directory and rebuilds it, and the manifest is a safety net rather than the only thing
standing between a rename and an orphaned file.
P5. Cores are hand-written, so they live outside api/, in cores/, named for the
surface they serve. (This differs from the first draft of the shape, which nested each
core inside the router it served. Co-locating reads better; a directory that can be
deleted wholesale is worth more. truewire.toml maps core names to import paths, so the
layout costs nothing either way, and I4 decides it.)
P6. A core provides, trimmed to what the API needs: transport/ (one module per
transport), endpoint/ (the RpcEndpoint and StreamEndpoint mixins that generated
endpoints subclass), auth.py, envelope.py, exceptions.py, types.py, and base.py
on the root core only. The mixins own the lifecycle (__aenter__, __aexit__), the
request methods, and the typed Meta that the spec and [cores.<name>].meta agree on.
P7. Every __init__.py with a sibling __init__.pyi is three lines of
lazy_loader.attach_stub, and the .pyi carries the real imports and __all__. Importing
the package costs one module, not two hundred, and the type checker still sees everything.
P8. The lazy-loader discipline is required of generated code and optional in a core. A generator cannot get the two halves out of step; a person editing a core by hand can, and the cost of that mistake is worse than the import time it saves.
P9. typings/lazy_loader/__init__.pyi is written by truewire init from a toolchain
resource. It is not copied between projects by hand.
P10. Strict pyright and ruff, configured per package, both run by truewire lint. A
generated file that does not type-check is a generator bug, and is fixed in the generator.
P11. No generated per-package CLI. truewire call covers development, and a shipped
CLI is a second public surface to version, document and support for a use no caller has
asked for. (withdrawn from the first draft, where it appeared with a question mark.)
TypeScript
P12. The same tree with the language's names: src/api/ generated, src/cores/
hand-written, test/ against the mock, package.json, tsconfig, ESM only, published to
npm. Strict mode, no any in generated output, and every exported symbol typed from the
spec rather than inferred.
P13. Validation is the runtime's, not the caller's: the generated client validates responses the same way the Python one does, and the same recording that passes in Python passes here.
Rust
P14. src/api/ generated, src/cores/ hand-written, tests/ against the mock,
published to crates.io. Async, one runtime-agnostic core crate, errors as an enum per
endpoint outcome rather than a stringly-typed catch-all.
Go
P15. Not started. When it starts, it starts from the same tree and the same gates, and it does not get a dialect of its own: a backend whose output needs rules the other three do not have is a backend that has misunderstood the spec.
Publishing
P16. Every package is published from CI by trusted publishing, triggered by merging the
release PR that truewire-release opened. No token is held by a person, and no package is
published from a laptop.
P17. Versions move together across languages within a project: one spec change, one version, four releases. A language whose package did not change is still released, because a caller comparing two languages at the same version number must be comparing the same spec.
Network
P18. Every runtime takes an explicit proxy URL on the client constructor, used for HTTP
and WebSocket alike, and the generated client passes it through. The environment's
HTTPS_PROXY is a fallback, never the only way, because an agent sandbox or a library
caller cannot always set the process environment.
P19. Every runtime's HTTP client takes a rate and a retry switch, and the generated
root states the project's [policy].rate and [policy].retry (W15) for the core to build
its client from. The rate spaces request starts 1 / rate seconds apart, across every
caller of one client, so no burst ever exceeds it. The retry covers only a failure that the
server did not act on: a connection that never opened, and a 429 or 503. It waits the
reply's Retry-After up to a cap, and it gives up after a fixed number of attempts. A
connection lost after the request left, and any other status, are the caller's to judge,
because repeating an order is worse than failing it. Off, each request is sent once.
A core signs each attempt after the pacing and retry waits, using a fresh timestamp
or nonce and the exact bytes sent. Python cores use httpx.Auth; TypeScript cores use
RequestOptions.prepare; Rust cores use HttpClient::request_prepared without changing
RequestOptions. Cancelling a pacing waiter releases its place without delaying later calls.