Code generation
This content is for the 0.1 version. Switch to the latest version for up-to-date documentation.
Every type under lib/src/generated/ is produced by tool/generate.dart from
the JSON Schema vendored in upstream/schema/. Nothing there is hand-written,
and the output is committed so the package is usable from pub.dev without
running anything.
dart run tool/generate.dart # writedart run tool/generate.dart --check # write, then fail if anything movedCI runs --check. Regeneration is byte-identical when in sync, so any movement
at all is drift.
The pipeline
Section titled “The pipeline”flowchart LR
A[upstream/schema/<br/>5 JSON Schema bundles] --> C[tool/generate.dart]
B[upstream/protocol.json<br/>what the schema drops] --> C
C --> D[lib/src/generated/<br/>8 files]
C --> E{checkExhaustiveness}
E -->|a type reached no output| F[generation fails]
The five bundles are each self-contained — every one inlines all of its transitively-referenced definitions — so shared types appear up to five times and are merged first-wins.
What the schema cannot carry
Section titled “What the schema cannot carry”The schema is generated from upstream’s TypeScript and is lossy in five knowable ways. Each gap is closed once, in one place.
| Missing | Where it comes from |
|---|---|
Method names (@method JSDoc) |
protocol.json → methods |
| The version registry | protocol.json → supportedProtocolVersions |
AhpErrorCode — the schema’s copy is stale |
protocol.json → errorCodes |
| Numeric enum member names | protocol.json → numericEnums |
int versus double |
protocol.json → floatProperties |
The last two are worth dwelling on.
Numeric enums keep their values but lose their names. SessionStatus
arrives as [1, 2, 8, 24, 32, 64] with nothing to recover idle, error,
inProgress from — unlike a string enum, where single-select yields
singleSelect.
TypeScript’s number is both int and double, and every numeric property
renders as {"type": "number"}. Five are genuinely fractional. Getting this
wrong is not a typing nicety: a risk assessment’s safety: 0.25 truncates to
0, which a reducer fixture catches and a live host would not.
Names the schema drops but values it keeps
Section titled “Names the schema drops but values it keeps”Eighteen enums exist in the TypeScript but not the schema, which inlines their
members as const discriminants on the arms of a union and drops the name.
Only the name and prose are written down, in tool/codegen/registries.dart.
The values are read from the schema on every run, so an arm added upstream
widens the enum without anyone editing a list.
A sync stays loud
Section titled “A sync stays loud”checkExhaustiveness runs before anything is written, and fails both ways:
- A schema definition reaching no Dart output.
- An entry in
intentionallyUnemittedwhose definition upstream has since removed — so an excuse cannot quietly outlive the thing it excused.
When it fails, the fix is to add the type to the matching registry, or to
intentionallyUnemitted with the reason it has no Dart counterpart. Not the
second to make the build pass.
Syncing upstream
Section titled “Syncing upstream”dart run tool/sync_upstream.dart <commit-sha>It re-vendors upstream/schema and upstream/test-cases, then prints what to
do next. It deliberately does not regenerate or run tests — a sync and the work
of absorbing it are separate reviewable steps, and a reviewer should be able to
see what upstream changed before anything else moves.
It refuses when the vendored paths hold uncommitted work, including untracked
files, and refuses if it cannot ask git rather than assuming a clean tree.
protocol.json is never touched, so the hand-maintained manifest survives —
which it must, since nothing in it is derivable.
Why not build_runner
Section titled “Why not build_runner”CI enforces a generate-and-diff gate. A second code-generation stage inside that
gate would leave .g.dart files perpetually dirty. Kotlin, Swift, Go, and Rust
all emit their codecs the same way, inline with the type.