Repository navigation
feat(config): derive and publish the batten.toml JSON Schema, gate min_batten_version (CLOUD-33) - #152
Merged
Conversation
…n_batten_version (CLOUD-33) Two halves of the same promise: the config surface is machine-validatable, and a binary too old for a config says so instead of guessing. The schema is DERIVED from the Rust config types, never hand-authored (DoR §1). `batten generate schema` emits `schema_for!(Config)`, so the schema cannot describe a `batten.toml` the binary would refuse, nor miss a key it accepts. Three tests assert that agreement directly rather than trusting the generator: the schema accepts the committed example and this repository's own config, and rejects the unknown key and the missing `severity` that `deny_unknown_fields` and the parser already reject. Published, not merely committed. A schema only sitting in the tree is a build artifact; one a validator can reach is published. Both halves ship: a `.taplo.toml` rule binds `batten.toml` to the local copy — verified to reject an unknown key with a `path:line` pointer — and a new `release-artifacts` job uploads the schema as a release asset, so a consumer binds the same rule to the released URL and is validated in the editor rather than first at `batten check` time. `path` locally rather than that URL because this repo IS the source: pointing at a release would validate the working tree against the last tag. `mise run schema-check` regenerates and diffs the committed copy byte-for-byte, glob-scoped in hk to the config types plus `schema/`. It matters more than a usual drift gate because nothing in the Rust build reads the committed file, and the artifact is published: a stale schema does not go unused, it tells a consumer their valid config is invalid. `min_batten_version` is enforced at parse time, so no verb can route around it. Below it is exit 1, not 2: "this binary is too old" is a statement about the invocation, never a verdict about the repository, and a harness reading 2 would report a policy denial that never happened (§7). Equal-or-newer runs. An unparseable version is refused rather than skipped — "cannot compare" is not "compatible", and skipping on a typo would silently disable the gate. The override layer parses ungated (`load_override`). `min_batten_version` is authority-only and `resolve` already refuses a `batten.local.toml` that sets it; gating there fired first and replaced that refusal with "this build is too old" — true of the value, useless about the mistake. An existing resolve test caught exactly this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0119C7XbeQLWR5TTaW2ca6Gp
CLOUD-33 Publish a JSON schema for `batten.toml` and enforce `min_batten_version` behavior
Why Acceptance
Refinement — Ready (schema is generated + drift-gated; version gate is an exit-code predicate) Refinement gate: Definition of Ready & Done. This body carries only specializations.
|
wenzowski
marked this pull request as ready for review
August 8, 2026 05:14
Contributor
Author
|
/fast-forward |
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.
Closes CLOUD-33.
Why
Two halves of one promise: the config surface should be machine-validatable, and a binary too old for a config should say so rather than guess.
The schema is derived, never hand-authored
batten generate schemaemitsschema_for!(Config)— generated from the very typesparsedeserializes into, so it cannot describe abatten.tomlthe binary would refuse, nor miss a key it accepts (DoR §1).Rather than trust the generator, the agreement is asserted directly. The schema is checked to accept the committed
batten.example.tomland this repository's ownbatten.toml, and to reject the unknown key and the missingseveritythatdeny_unknown_fieldsand the parser already reject. That last pair is the property that makes publishing safe: a schema an editor accepts butbatten checkrefuses is the worst of both.Published, not merely committed
Both halves ship:
.taplo.tomlbindsbatten.toml/batten.example.toml/batten.local.tomlto the schema.taplo lintpicks it up through auto-config, and so does any editor with the taplo LSP. Verified against a fixture with a bogus key:release-artifacts.ymlgains aschemajob uploadingbatten.schema.jsonas a release asset, so a consumer writes the same rule against.../releases/latest/download/batten.schema.json. It is its own job, not a matrix step — the schema is platform-independent, so uploading it seven times would race seven writers onto one asset name.pathrather than that URL in this repo because this repo is the source: pointing at a release would validate the working tree against the last tag, so a key added in an unreleased commit would read as invalid.The drift gate
mise run schema-checkregenerates and diffs the committed copy byte-for-byte (DoR §4), glob-scoped inhk.pklto the config types plusschema/. It carries more weight than a usual drift gate: nothing in the Rust build reads the committed file, and the artifact is published, so a stale one does not merely go unused — it tells a consumer their valid config is invalid, or waves through onebatten checkwill refuse.The
min_batten_versiongateEnforced at parse time, so no verb can route around it by picking a different command (asserted across
check,enforce, andconfig show).min_batten_versionExit 1, not 2 — and note this differs from the issue's Ready block, which predates CLOUD-226. "This binary is too old" is a statement about the invocation, never a verdict about the repository; a harness reading
2would report a policy denial that never happened (§7). I've patched the stale numbers in the issue body, preserving the semantic class (Usage) it actually specified.An unparseable version is refused, not skipped: "cannot compare" is not "compatible", and skipping on a typo would silently disable the gate.
The override layer parses ungated
An existing test —
resolve::the_local_file_may_not_restate_an_authority_only_key— caught a real design error. Putting the gate inparsemeant it also fired onbatten.local.toml, wheremin_batten_versionis an authority-only key thatresolvealready refuses outright. The version message fired first and replaced the useful one: an author who set the key in the wrong file was told their binary was too old. Split intoconfig::load_override/parse_overrideso the override layer parses ungated and the specific refusal speaks.Dependencies
schemars 1andsemver 1(both permissive, MSRV well under our 1.85).jsonschema 0.49is a dev-dependency withdefault-features = false— its defaults pullreqwest+rustls+aws-lc-rs, which would bloat the tree and riskdeny.toml's license allow-list, and a validator has no business fetching over the network.mise run denyis green: advisories, bans, licenses, sources all ok.Tests
crates/batten/tests/config_schema.rs(new file —tests/cli.rsis the exit-code suite and other work appends to it): 13 E2E tests over the compiled binary covering everything above, plus byte-stability across runs andgenerate schemawriting no file when run from a scratch directory.tests/schema-check.bats(new): the gate's decision table, asserting it red as well as green — drift, missing schema, pointer-only output, and that it leaves the tree it judges unmodified (a gate that rewrites what it judges cannot fail twice).mise run verifygreen on this SHA, rebased on currentorigin/main.Generated by Claude Code