Skip to content

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.

Terminal window
dart run tool/generate.dart # write
dart run tool/generate.dart --check # write, then fail if anything moved

CI runs --check. Regeneration is byte-identical when in sync, so any movement at all is drift.

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.

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.jsonmethods
The version registry protocol.jsonsupportedProtocolVersions
AhpErrorCode — the schema’s copy is stale protocol.jsonerrorCodes
Numeric enum member names protocol.jsonnumericEnums
int versus double protocol.jsonfloatProperties

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.

checkExhaustiveness runs before anything is written, and fails both ways:

  • A schema definition reaching no Dart output.
  • An entry in intentionallyUnemitted whose 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.

Terminal window
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.

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.