axum-api-kit is stable under an additive-only policy: the 1.0.0 freeze (2026-06-03) carried 1.x through five feature minors with no breaking change, and the 2.x line continues the same posture — 2.0.0 (2026-07-22) changed nothing in this crate's own API or response bytes (see Current state for its real scope). Expansion is the default posture (user direction, 2026-07-18): 1.1.0 through 1.4.0 all added features. Maintenance work happens to keep the crate healthy against upstream releases or when it unblocks feature work.
Last updated: 2026-08-08 (v2.1.0 release prep). The release-state claims in this file are guarded by tests/roadmap_truth_tests.rs, which reads this file against Cargo.toml and CHANGELOG.md and fails cargo test when they disagree. Since 2026-08-08 that guard also enforces a scheduling rule rather than only a consistency rule: ## Next milestones must always name at least one versioned milestone that has not shipped, so completing the last one cannot quietly leave this crate with nothing scheduled (which is exactly what had happened — see History).
- Latest published release: 2.1.0 (2026-08-08) on crates.io: the MSRV declaration corrected 1.75 -> 1.81 with a changelog note and a
MSRV 1.81CI job enforcing it, plus explicit least-privilegepermissions: contents: readblocks on both workflows withtests/workflow_permissions.rspinning them. Additive and repo-side only — no public API change, no response-byte change, no runtime dependency change, soSemver compatibilitypassed against 2.0.0 unmodified. The MSRV correction is the consumer-visible half: published 2.0.0 declaresrust-version = "1.75"(confirmed at the registry API), which is unbuildable, because 2.0.0's ownvalidator 0.18 -> 0.20bump hard-requires rustc 1.81 — a 1.75-1.80 consumer's resolver accepts 2.0.0 and then fails to compile it. 2.1.0 is the first release whose declared floor is true. - Shipped since the 1.0.0 freeze (2026-06-03): 1.1.0 success helpers
Created/Accepted/NoContent(2026-06-07), 1.2.0ApiJsonextractor with structured JSON rejections (2026-06-07), 1.2.1 through 1.2.7 cross-feature integration test coverage and formatting (2026-06-27), 1.3.0 problem responses and Retry-After factories (2026-07-18), 1.4.0 Accept-header negotiation forProblemplus theProblemJson/ProblemValidatedJsonrejection extractors (2026-07-19), 2.0.0 validator 0.18 -> 0.20 (2026-07-22; the only non-additive entry, scoped as above), 2.1.0 the corrected MSRV floor and the workflow permission blocks (2026-08-08). - Unreleased on main: what CHANGELOG.md's
[Unreleased]section lists — the correctedCursorResponselast-page documentation (the rustdoc claimed"next_cursor": null; the wire has always omitted the key), the first response-level test coverage for the three no-feature-flag response types, and theopenapi-feature nullability fix that finishes the same defect one layer down (five optional fields acrossApiError,CursorResponseandProblemwere emitted asT | nullunions althoughskip_serializing_ifmeans the key is simply absent; each now declaresschema(nullable = false), guarded class-wide bytests/openapi.rs::no_registered_property_admits_null), and thetower-httprequirement widened from0.6to>=0.6, <0.8socors-feature users on tower-http 0.7 resolve one copy (a widen rather than a bump becausecors_allowing/cors_permissivepublicly returntower_http::cors::CorsLayer; see the dependency watch below for the 2026-08-08 assessment). The generated document changes foropenapiusers; no public API change, no response-byte change, no dependency removed or hard-bumped (one bound widened). This bullet was restored by exactly the mechanism the sentence it replaces predicted — that sentence read "No unreleased work sits onmainas of 2026-08-08: CHANGELOG.md's[Unreleased]section is empty and every entry it held shipped in 2.1.0. The bullet that describes such work is guarded in BOTH directions and must exist exactly while[Unreleased]has entries, so the next change adding one restores it and the next release prep drops it again..." — and it rides the next release, whose prep must move these entries into a dated section and drop this bullet (guarded). - Eight feature flags:
validator,sqlx,extract,trace,router,cors,openapi,problem. axum 0.8. MSRV 1.81 declared in Cargo.toml and enforced by theMSRV 1.81CI job (see v1.3.2 PR 1).
- The additive-only policy survives major lines: within any major line (1.x then, 2.x now), no breaking changes; minors add, patches fix. Existing response bytes stay locked by the byte-identity tests either way.
- A minor is justified by: additive helpers, new opt-in feature flags, dependency-bound bumps, or an MSRV raise (with a changelog note).
- A patch is justified by: bug fixes, docs, tests, formatting.
- A major is justified only by a breaking public-API change. The designated trigger was an axum major (0.9 or 1.0), because axum types appear in the public API — but the first major to actually fire (2.0.0, 2026-07-22) came through a different route: a version-coupled optional dependency (
validator), whose security-driven bump changes a public trait bound for feature users. Both routes are live; an axum major remains the primary future trigger.
- MSRV is 1.81 (
rust-versionin Cargo.toml). It is raised only in a minor release with a changelog note. - The 1.75 -> 1.81 raise (2026-07-26) corrects fiction rather than dropping support: v2.0.0's validator 0.18 -> 0.20 bump made 1.75 unbuildable (validator 0.20.0 hard-requires rustc 1.81; no 0.20.x is lower), so no consumer on 1.75 could ever have built 2.0.0 with all features. The changelog note rides the next release.
- Toolchains 1.81-1.84 need one pin:
cargo update validator_derive --precise 0.20.0(0.20.1 is edition2024, which cargo < 1.85 cannot parse). TheMSRV 1.81CI job documents and exercises exactly this recipe. - Accepted risk, recorded 2026-08-08 (found by running
cargo audit --deny warningsover the pinned MSRV lockfile, which no CI job does — theSecurity auditjob audits a fresh resolution, wherevalidator_derive0.20.1 pulls the maintainedproc-macro-error3): the pinnedvalidator_derive 0.20.0pullsproc-macro-error2 2.0.1, flagged unmaintained by RUSTSEC-2026-0173 (warning-class, not a vulnerability; build-time proc-macro only, never in shipped code). It only reaches consumers on 1.81-1.84 who apply the documented pin, and no fix exists inside 0.20.x by construction (0.20.1 is the edition2024 sibling). Escapes: raise MSRV to 1.85+, or the validator 0.21+ major (both are the same future decision — see the dependency watch assessment below).
- Optional-feature dependencies (validator, sqlx, tower-http, utoipa) track their upstream crates: a compatible upstream minor gets a bound-widen in a minor release. A bump that is version-coupled into the public API (the validator case) is a major here — assess before bumping.
- An axum major forces a written impact assessment before any code change (see the recurring watch below).
- Publishing is release-event-triggered:
publish.ymlfires when a GitHub release is published. Version ships (release-prep commit, tag, GitHub release, docs.rs spot-check) are DELEGATED to the agent (user decision 2026-07-26, recorded in the autodev SKILL's Merges-and-releases policy; it supersedes the per-item USER-ONLY release markers this file previously carried). Cut a release only when release prep is on main, CI is green on that commit, and this file or the backlog names the release. crates.io publishes are irreversible: re-read version and changelog before tagging and confirm the published version afterwards. Genuinely ambiguous judgment calls (for example a disputed major-vs-minor classification) stay with the user.
The open milestone comes first; completed ones are kept below it in shipping order until a later pass moves them to History.
This heading is a placeholder for a SCHEDULE, not for a scope, and saying so is the honest form. The crate has no scoped feature track: v2.1.0 released work that already existed and answered nothing about what comes next, and the "Later / candidates" note below records that as an open product question rather than a gap to be filled by whichever agent reaches it first. Autodev is the product ANALYST on this crate, not its owner, so the next increment here DRAFTS a proposal and stops; it does not start building a direction nobody picked.
Done when, in this order:
- A review-gate PR merges
docs/EXPANSION_PROPOSAL_V2_2.mdto main: concrete candidate features named against the eight existing feature flags, each one classified additive-or-breaking under the Semver policy above, with a recommended ordering and every choice flagged as an overridable default so the user can accept the lot with one word. Checkable by command — the file is on main and its PR is merged. - The user has accepted, amended, or rejected it. This is a genuine user gate and the only step here that no command can close.
- That decision is written back into THIS heading in the same commit that records it, replacing the placeholder scope with the accepted one and replacing this done-when with criteria a build, test, or CI run can check. Until step 3 lands, this milestone is scheduled but not specified, and the distinction is deliberate:
tests/roadmap_truth_tests.rsenforces that something is always scheduled, which is a weaker property than something always being specified, and a placeholder that pretended otherwise would satisfy the guard while lying to the next reader.
If the user's answer is "nothing yet", the correct edit is to say so here with a date and a re-ask condition, not to leave a fictional scope standing.
Everything CHANGELOG.md's [Unreleased] section lists has been sitting on main unreleased since 2026-07-26 (a405950, the MSRV raise) and 2026-08-07 (61f2137, the workflow permissions: blocks). No release named it, and the Releases policy above only permits a cut when "this file or the backlog names the release" — so until this heading existed the work was unreleasable by this document's own rule. That deadlock, not the size of the change, is why this milestone exists.
Scope: exactly the [Unreleased] entries and no new code. MINOR is the correct classification under the Semver policy above, which names "an MSRV raise (with a changelog note)" as a minor justification; the permissions: blocks are repo-side only and change neither the public API nor any response byte. The actions/checkout@v7 currency bump (26adfc5) is deliberately absent from the changelog and rides along without an entry: it changes no consumer-visible surface.
Done when, in this order, each of these OBSERVED rather than assumed:
- Release prep is on main in one commit:
Cargo.tomlatversion = "2.1.0", the[Unreleased]entries moved into a dated## [2.1.0] - <date>CHANGELOG section, the Current state bullet that describes not-yet-released work dropped, this heading marked COMPLETE, and the milestone that follows this one defined. The last three are not politeness —tests/roadmap_truth_tests.rsfails the release-prep PR without them. cargo test --all-featurespasses locally and all five required contexts are green on that main commit.- Tag
v2.1.0is pushed and a GitHub release is published, which is what firespublish.yml. - That publish run's conclusion is
success, itsGITHUB_TOKEN Permissionslog group reads exactlyContents: read, Metadata: read, and the run carries zero Node-20 deprecation annotations. A release is the ONLY way to observe either property:publish.ymltriggers onrelease: published, so no PR or push run has ever exercised its permissions block or itsactions/checkout@v7. The backlog carries this as an open follow-up precisely so it closes here instead of costing an increment of its own. - The crates.io API reports
newest_version2.1.0, read directly with the User-Agent the registry requires. Never infer a publish from the workflow going green.
Status of those five steps, and the honest shape of the window between them. Steps 1 and 2 are what this commit IS, so they are observed here. Steps 3 through 5 are observed by the cut that immediately follows this commit's merge, and their evidence — tag sha, publish run id with its matching headSha, the permissions log group verbatim, the annotation count, and the registry read — is recorded on this crate's backlog item and in the run report, because a commit cannot cite artifacts that its own merge creates.
That ordering means this heading reads COMPLETE for the few minutes between merge and publish, and so does the Current state bullet naming 2.1.0 as the latest PUBLISHED release. The window is real and is named here rather than hidden: tests/roadmap_truth_tests.rs compares this file only against Cargo.toml and CHANGELOG.md, all three in the same tree, so nothing in CI is asserting the claim against the live registry and there is no ordering cycle to break — the prep PR is green precisely because the guard is internal. Were a guard ever added that reads crates.io or git tag directly, this exact heading is the one that would deadlock it (red in the prep PR because 2.1.0 is not published yet, red again after publish if left stale), and it would need a forward-only in-flight exemption naming 2.1.0 specifically, budgeted into the same increment.
1.3.0 is live on crates.io with a green docs.rs build (release prep committed, tag v1.3.0 pushed, GitHub release published, publish workflow succeeded, under the standing delegation).
This file and the README pointer were committed to main 2026-07-18 (the done-when). The open "docs-only release or ride along" question resolved itself: both files are in the v1.4.0 tag's tree (verified via git ls-tree v1.4.0), so they shipped with 1.4.0 on 2026-07-19.
CI historically ran the stable toolchain only and checked neither promise this document makes. (The "v1.3.2" label is historical; these are repo-side CI gates that ride the next release whatever its version number.)
- PR 1: SHIPPED 2026-07-26 — the
MSRV 1.81job in ci.yml builds and tests on a pinned Rust 1.81 toolchain with all features, so the declaredrust-versionis actually enforced. Finding en route: the planned "1.75" was already unbuildable (see the MSRV policy section above), so the job enforces the corrected floor, andtests/msrv_gate_tests.rskeeps Cargo.toml, ci.yml, and this document agreeing. - PR 2: SHIPPED 2026-08-01 as PR #10 (
3ab884a) — theSemver compatibilityjob runscargo semver-checksagainst the latest published version, mechanically guarding the additive-only promise (now against the 2.x line). This line read "PR 2: add ..." and the heading read "PR 2 open" for six days after it merged; corrected 2026-08-07 fromgh pr list --state alland the run below. - Done when: both jobs exist and run green on main. MET — main run
30701775073(2026-08-01, the merge of PR #10) showsMSRV 1.81andSemver compatibilitybothsuccess, read fromgh api .../runs/30701775073/jobs, alongsideTest, Lint, FormatandSecurity audit.
Both deferred 1.3.0 features shipped as the planned two PRs (PR #2: Accept-header content negotiation for Problem; PR #3: the ProblemJson/ProblemValidatedJson sibling extractors with RFC 9457 rejection bodies), then release prep (PR #4), tag v1.4.0, GitHub release, and a green publish workflow; the registry confirmed 1.4.0 at the time (2.0.0 has since superseded it as latest).
Background stream: pick these up when they unblock feature work or upstream ships something relevant. Respect the cadence of at most roughly one minor version per week, and only release when there is something to ship.
- Periodically check for new axum, tower-http, utoipa, validator, and sqlx releases; widen bounds and fix deprecations in a small PR when upstream ships a compatible minor.
- Assessment on record, 2026-08-08 (both upstream majors surfaced by the v2.1.0 cut's lockfile generation; each judged against the Dependencies policy above): tower-http 0.7.0 — WIDENED (
>=0.6, <0.8) rather than bumped, becausecors_allowing/cors_permissivepublicly returntower_http::cors::CorsLayer, making a hard bump a public-type-identity break for consumers whose owntower-http = "0.6"names it, while a widen unifies with either side; rustc floor 1.65 (under the 1.81 MSRV), cors module unchanged in 0.7 apart from relaxedVarydefaults (tower-http #674), all 0.7.0 breaking changes confined to modules this crate does not use. validator 0.21.0 — DEFERRED, two independent blockers measured at the registry: it declaresrust-version = "1.88"(above the 1.81 floor, so a bump forces an MSRV raise), and theValidatebound is version-coupled into the public API forvalidator-feature users (the exact route that made 2.0.0 a major). A validator 0.21+ bump is a candidate 3.0 driver and does not happen inside 2.x; re-assess only when a major is otherwise warranted. - Assessment on record, 2026-08-08 (surfaced by the tower-http/validator PR's own lockfile generation; judged against the Dependencies policy above): sqlx 0.9.0 — DEFERRED at
0.8. The coupling determination, read from the source rather than inherited:sqlx::ErrorIS version-coupled into the public API forsqlx-feature users via#[cfg(feature = "sqlx")] impl From<sqlx::Error> for ApiError(src/error.rs:318-338, the crate's only sqlx-gated surface; nopub use sqlxre-export exists), so a hard0.8 -> 0.9bump changes whichsqlx::Errorthat impl accepts and is a semver major for feature users — the exact route that made 2.0.0 a major. The instrument that avoids the major, a tower-http-style widen (>=0.8, <0.10), is ALSO blocked, by MSRV rather than by API: sqlx 0.9.0 declaresrust-version = "1.94.0"at the registry (its own changelog names 1.94.0 as the release cycle's supported floor), thirteen minors above this crate's declared 1.81 — so a widened range would hand any fresh-resolving 1.81-1.93 consumer a resolver match on 0.9.0 followed by a compile failure, which is precisely the false-declared-MSRV failure class (2.0.0'srust-version = "1.75"over an effective 1.81 floor) that 2.1.0 exists to correct. Usage-level compatibility of theFromimpl against 0.9'sErrorwas NOT fully assessed because it is moot below the MSRV blocker; 0.9.0's changelog names no breaking change to theErrorvariants the impl matches (its error-adjacent entry, #3918, is an additive exclusion-violation kind). A sqlx 0.9 adoption is therefore a candidate 3.0 driver, or rides an MSRV raise to >=1.94 (minor under the Semver policy but needing its own justification); re-assess only when either is otherwise warranted. (matchit, the other newer package in that lockfile generation, is axum's transitive dependency, not ours — re-verified against[dependencies], nothing to assess.) - Confirm CI is green and docs build; fix only actual breakage, no feature work.
- If axum announces 0.9 or 1.0: write a short impact assessment as its own PR before any code changes (this is the designated major trigger).
- Resulting releases run under the release delegation above (2026-07-26).
- Further additive feature ideas as they arise; new opt-in feature flags are policy-compatible. Propose each as a short design note in its PR.
- What comes after v2.1.0 is an OPEN PRODUCT QUESTION as of 2026-08-08, and stating it plainly is the point. Every versioned milestone in this file had shipped, so this crate had no scheduled work, no scoped feature track, and no proposal in flight — the v2.1.0 milestone above releases what is already on main and does not answer this. Drafting that expansion proposal for the user to decide is filed as its own product item on this crate's backlog rather than guessed at here; agent-drafted scope is a recommendation, and the user owns the choice. Since the v2.1.0 cut (2026-08-08) the question also has a SCHEDULE attached to it — the
### v2.2.0heading above — which is a placeholder for when, not an answer to what; do not read it as a decision already taken. - 3.0 planning: begins only on the next forced breaking change. An axum major (0.9 or 1.0) remains the designated trigger and forces the written impact assessment first; 2.0.0 proved a version-coupled dependency bump is a second live route.
- Releases are delegated per the Releases policy above (2026-07-26). Still user-only: writing repo secrets (
gh secret set), and any judgment call this file marks ambiguous. - Nothing is blocked by infrastructure. The crate has no runtime infrastructure (crates.io plus GitHub Actions only).
- The pre-1.0 roadmap (0.6.0 through 1.0.0, published 2026-06-03) is complete; 1.0.0 was the stable API freeze.
- Earlier planning notes described the crate as "1.0, frozen" with the roadmap done. In practice the freeze froze the existing API against breaking changes while additive minors continued. "Frozen" means no breaking changes and no active feature stream, not no releases ever. This file supersedes those notes as the single roadmap of record for this crate.
- v1.4.0 shipped end to end on 2026-07-19 (PRs #2/#3 features, #4 release prep; tag, GitHub release, publish workflow success, registry confirmed).
- 2.0.0 shipped 2026-07-22 (PR #7, validator 0.18 -> 0.20, user-approved publish; cleared RUSTSEC-2024-0421 and RUSTSEC-2024-0370; tag v2.0.0, publish run 29890003927 success, registry confirmed
newest_version2.0.0). - Superseded prose, recorded before deletion (roadmap-truth pass, 2026-07-27): the Current state section claimed "Latest published release: 1.3.0" for five days after 2.0.0 published; the shipped-since-the-freeze list stopped at 1.3.0 (missing 1.4.0 and 2.0.0); v1.4.0 still sat under Next milestones as open work; the Later section claimed "2.0 planning: begins only when an axum major lands; no other 2.0 driver exists" after the 2.0 had already shipped through a non-axum driver; and the old Blocked section cited the 2026-06-04 infra decommission as "reinforcing the maintenance-only stance", which contradicted this file's own expansion-first intro. All corrected in this pass, and the release-state claims are now mechanically guarded by
tests/roadmap_truth_tests.rs. - Agent priority is governed by
d:/Projects/.claude/skills/autodev/backlogs/axum-api-kit.md, this crate's OWN backlog. Corrected 2026-08-08: this line pointed at a sharedbacklogs/cargo-crates.mdthat no longer exists — it was split on 2026-08-08 into one backlog per crate so each gets its own autodev lane, and the priority ordering it asserted went with it. Verified before editing:ls backlogs/listsaxum-api-kit.md,slokit.mdandsvccat.md, and nocargo-crates.md. Superseded text, quoted before deletion: "The shared backlog at d:/Projects/.claude/skills/autodev/backlogs/cargo-crates.md governs agent priority (slokit first)". axum-api-kit carries its own additive feature stream, not just maintenance; read any older "1.0, frozen" phrasing as "additive-only within the current major line". - v2.1.0 shipped 2026-08-08 (release prep here, tag
v2.1.0, GitHub release,publish.yml), the first release cut after the milestone-definition pass below defined it — thirteen days of releasable work had been stranded on main by the deadlock that pass describes. Superseded prose, quoted before deletion per the tombstone rule: the Current state section's third bullet read "Unreleased on main: what CHANGELOG.md's[Unreleased]section lists — currently the MSRV raise note (1.75 -> 1.81) and theMSRV 1.81CI job, plus the explicit least-privilegepermissions:blocks on both workflows and theirtests/workflow_permissions.rsguard. These ride the next release, whose prep must move them into a dated section and drop this bullet (guarded)." — its own instruction, now carried out. The finding this cut makes consumer-visible, and the reason it was treated as a broken-shipped-state preemption rather than routine release chores: published 2.0.0 declaresrust-version = "1.75"at the registry while being unbuildable below 1.81, so for the seventeen days 2.0.0 was the newest version, a consumer on 1.75-1.80 got a resolver match followed by a compile failure insidevalidator. The correction had been sitting on main since 2026-07-26 with nothing to carry it to users. 2.1.0 is the first release of this crate whose declared MSRV floor is true. - Milestone-definition pass, 2026-08-08 (this file's second product pass; the first was the 2026-07-27 roadmap-truth reconciliation above). The defect found:
## Next milestonesheld four versioned milestones and all four were COMPLETE, so the section named nothing scheduled, while CHANGELOG[Unreleased]had carried releasable work for thirteen days that the Releases policy forbade cutting because no document named a release for it. Measured onorigin/mainbefore the fix: 4 versioned milestone headings, 0 of them open. The 2026-07-27 pass could not have caught this — its guards check that claims AGREE with each other, and an all-complete milestone list is perfectly self-consistent. Fixed by defining v2.1.0 above and by adding a sixth guard totests/roadmap_truth_tests.rsthat fails when no versioned milestone is open, so the state cannot recur silently: the commit that marks a milestone COMPLETE must name its successor orcargo testgoes red.