refactor: generate docs/schemas from typed contracts instead of hand-writing them - #101
Merged
Conversation
DLANSAMA
force-pushed
the
refactor/printables-adapter
branch
from
August 5, 2026 12:50
afac656 to
20a1557
Compare
DLANSAMA
force-pushed
the
refactor/generated-schemas
branch
from
August 5, 2026 12:52
95f985c to
a040d25
Compare
DLANSAMA
force-pushed
the
refactor/printables-adapter
branch
from
August 5, 2026 14:20
20a1557 to
bf873e0
Compare
…writing them
The 25 files in docs/schemas were hand-maintained, which meant nothing stopped
them drifting from what the commands actually emit. They are now generated from
frozen dataclasses in bambu_cli/contracts/, and CI regenerates and diffs, so
drift is a build failure.
bambu_cli/contracts/base.py Contract base + spec() field constraints
bambu_cli/contracts/models.py 25 published contracts + 8 nested structures
scripts/gen_schemas.py the generator (--check is the CI gate)
Fidelity was verified before overwriting anything: the generator was diffed
against the committed schemas until it produced ZERO losses — every constraint
the hand-written files expressed (minLength, minimum, nested required, field
descriptions, the whole status.printer shape) is reproduced. The 78 differences
that remain are all strictly more precise: types alongside consts, item types
on arrays, and explicit nullability.
Two real defects surfaced while doing it, both previously invisible:
- download.json required 7 fields; a naive model made 4 of them optional.
Caught by the loss diff, fixed with spec(required=True).
- error_envelope typed next_command as `{}` (anything), which hid that
job/send emits it as null. The generated schema is explicit, and the
contract test failed until the model said so.
Pydantic is a DEV dependency only. It derives JSON Schema from the dataclass
annotations at build time and is never imported at runtime — verified by a test
that subprocess-imports the package and asserts pydantic is absent from
sys.modules, and visible in uv.lock: runtime deps stay at 3, with pydantic under
the test extra behind a python_version >= '3.10' marker. Serialization stays in
emit_json because that pass applies the credential redaction a model_dump_json()
would bypass; emit_json/emit_json_line now accept a contract or a plain dict.
The contracts annotate optionals as `X | None`, which only evaluates on 3.10+.
Nothing at runtime resolves them (dataclasses keeps annotations as strings), so
the 3.9 floor is unaffected — asserted by a test, and the generator refuses to
run below 3.10 with an explanatory message rather than failing obscurely.
Wired so far: version, light, pause, resume, stop. The remaining commands still
emit dicts; every one of them is validated against its generated schema by
tests/contracts/, so the contract holds either way. Converting the rest --
especially job's incrementally-built summary -- is follow-up work.
Docs: schema counts and coverage refreshed to measured values (1215 collected /
1214 passing, 87.0% Linux) rather than left stale.
Gates: 1214 passed (was 1177), coverage 87.0% (was 86.5%), ruff/ruff-format/
mypy/bandit/layers/drift-check/syntax/help/workflow/compat/package smokes green.
Verified on a real CPython 3.9.25 that contracts import, payloads render, and
pydantic stays absent.
docs/schemas/tui.json arrived on main with the TUI. The drift gate did exactly what it was built to do and failed with "no contract generates: tui.json" — the check works in both directions, so a published schema with no model behind it is a failure, not a silent pass. Same shape as Go: both are human-only front-ends with no machine contract, so the only payload either emits is the --json refusal. Verified the generated schema loses nothing from the hand-written one.
1374 collected / 1373 passing, 89.0% Linux — measured on this branch after the rebase onto main-with-TUI, not inherited from either side of the doc conflict. These were placeholders until measured: each stacked PR has to stand up green on its own, and test_docs_consistency rejects a placeholder exactly as it rejects a stale number. PR #102 re-measures on top.
DLANSAMA
force-pushed
the
refactor/generated-schemas
branch
from
August 5, 2026 14:31
ca24471 to
a6a9bf9
Compare
DLANSAMA
marked this pull request as ready for review
August 5, 2026 14:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 3 of 4. Stacked on #100 (which is stacked on #99) — this PR targets
refactor/printables-adapter, so its diff shows only the schema work.Why
The 25 files in
docs/schemas/were hand-maintained. Nothing stopped them drifting from what the commands actually emit, and two of them had already drifted in ways nobody could see.Now they are generated from frozen dataclasses, and CI regenerates and diffs them. Drift is a build failure.
bambu_cli/contracts/base.pyContractbase +spec()field constraintsbambu_cli/contracts/models.pyscripts/gen_schemas.py--checkis the blocking CI gateFidelity was proven, not assumed
I did not overwrite the hand-written schemas and hope. The generator was diffed against them until it produced zero losses — every constraint the old files expressed is reproduced:
minLength,minimum, nestedrequired, field descriptions, and the entirestatus.printershape including AMS.All 78 additions are strictly more precise —
typealongsideconst, item types on arrays, explicit nullability.Two real defects it surfaced
download.jsonrequired 7 fields, but a naive model demoted 4 to optional (dataclass ordering forces defaults last). The loss diff caught it; fixed withspec(required=True).error_envelopetypednext_commandas{}— "anything" — which hid thatjob/sendemits it asnull. The generated schema is explicit, and the contract test failed until the model admitted it.Neither was visible while the schemas were hand-written.
Pydantic is dev-only
This is the part worth checking. Pydantic derives JSON Schema from the dataclass annotations at build time and is never imported at runtime:
pydanticis absent fromsys.modules.uv.lockshows runtime deps unchanged at 3 (paho-mqtt, rich, zeroconf), with pydantic under thetestextra behindpython_full_version >= '3.10'.Serialization stays in
emit_json, because that pass applies the credential redaction amodel_dump_json()would bypass.emit_json/emit_json_linenow accept a contract or a plain dict.The 3.9 floor
Contracts annotate optionals as
X | None, which only evaluates on 3.10+. Nothing at runtime resolves them —dataclasseskeeps annotations as strings — so the floor is unaffected. Both halves are asserted by tests, and the generator refuses to run below 3.10 with an explanatory message rather than failing obscurely. Verified against a real CPython 3.9.25.Scope: what is wired
version,light,pause,resume,stopconstruct contracts today. The remaining commands still emit dicts — and every one of them is validated against its generated schema bytests/contracts/, so the published contract holds either way. Converting the rest (especiallyjob's incrementally-builtsummarydict) is follow-up work rather than something to rush into this diff.Gates
Docs: test counts and coverage refreshed to measured values rather than left stale.
Rebase note for whoever lands this
docs/schemas/tui.jsonlives onfeat/tui, notmain. When #97 merges, the drift gate will correctly fail with "no contract generates: tui.json" until aTuicontract is added tomodels.py— it is the same shape asGo(interactive command,--jsonalways errors). Deliberate: the gate fails in both directions.