Source on GitHub

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  LICENSE

P4. 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.