docs: add ADR-022 per-kind artifact maturity and deprecation policy - #2373
docs: add ADR-022 per-kind artifact maturity and deprecation policy#2373mchmarny wants to merge 3 commits into
Conversation
Records what apiVersion AICR artifacts carry at v1 GA, what a bump owes
the version it replaces, and what apiVersion a newly introduced kind
starts at. Amends ADR-011: replaces its unconditional dual-accept
transition window (section 4), extends its compatibility gate to the
catalog loader (section 3), leaves sections 1 and 2 unchanged.
Eight decisions, the load-bearing ones:
- Project v1 and artifact v1 are separate axes. ROADMAP section 2 asks
for a baseline, a gate, and a deprecation channel, not for every kind
to reach aicr.run/v1. A gate over a v1beta1 schema still catches
unintended breakage.
- Per-kind maturity map. Snapshot, Recipe, RecipeCriteria, and bundle
provenance go to aicr.run/v1; AICRConfig and the profile-bearing
kinds sit at aicr.run/v1beta1. Whatever an integrator's code binds to
must be at v1.
- The deprecation window is conditional on the level being retired:
alpha none, beta two releases, GA not removed within a major. The
pkg/header godoc's hard-break language is correct for alpha and gains
that condition rather than being reversed.
- New kinds start on the current track. aicr.run/v1alpha1 in particular
has never been valid; ADR-013 moved the version to v1alpha2 at the
domain rename.
- External data fails closed. Extends the accept-known/reject-unknown
gate to the catalog loader, which today checks kind only and lets an
older binary silently resolve an unspecialized recipe.
Proposal only, no code.
Signed-off-by: Mark Chmarny <mark@chmarny.com>
Coverage ReportCoverage data unavailable for this run. This can happen if the qualification workflow was cancelled or failed before generating coverage artifacts. |
|
🌿 Preview your docs: https://nvidia-preview-docs-adr-022-artifact-maturity.docs.buildwithfern.com/aicr |
This comment was marked as resolved.
This comment was marked as resolved.
- Qualify the universal claim: artifacts predating the apiVersion field
carry no value and every loader tolerates that explicitly. Adds it as
a fourth unanswered question.
- Decision 3 now states that the alpha-to-target migration skips the
staged rollout in Decision 6. A staged rollout is a dual-accept
mechanism and Decision 3 carries no dual-accept path, so the two
rules cannot both govern the same migration.
- Decision 3 also retires the empty-apiVersion tolerance. After the
lockstep regeneration nothing legitimate is empty, and an artifact
with no version would otherwise pass every gate unchallenged.
- Decision 4 names the release axis for the GA obligation: the AICR
vMAJOR.MINOR.PATCH release, not the artifact version. An aicr.run/v1
kind deprecated during v1.x may first be removed in v2.0.0.
- Decision 6 scoped to bumps that owe a window under Decision 4.
- Decision 8 corrected: metadata_store.go does read apiVersion for the
ADR-015 profile-kind pairing. What is missing is a general
accept-known/reject-unknown gate; it calls neither
IsSupportedAPIVersion nor IsSupportedRecipeResultAPIVersion.
- Recipe rationale corrected to 105 catalog files (101 overlays,
4 mixins), which is what the count actually measures.
Signed-off-by: Mark Chmarny <mark@chmarny.com>
njhensley
left a comment
There was a problem hiding this comment.
Multi-persona review — 3 independent persona reviewers (Domain/Architecture, Correctness/code-claim, Docs) followed by an adversarial senior meta-reviewer that re-derived every finding from the resolved code at c32bdcb5.
Tier legend: 🔴 Blocker · 🟠 Major · 🟡 Minor · 🔵 Nitpick
Overall: Approve with comments
Carefully argued ADR, and unusually faithful to the code it rests on — 12 of 13 specific code claims verified clean against HEAD (loader empty-tolerance pattern, the hard-break-vs-dual-accept split in pkg/header, ProvenanceAPIVersion = header.GroupVersion, the #2026/#2245 AICRConfig split, the pkg/recipe/profile.go bidirectional validation, the verbatim ADR-013 quote, and the §8 loader-gap — which is in fact more accurate than ADR-015's own older claim). Every "Major" a persona raised collapsed to a documentation-precision issue once re-derived against the code; none is a blocker for a Proposed doc.
The one systemic weakness worth fixing before the #2114 implementer works from it: the §2 maturity map's Kind column conflates kind with apiVersion. On the wire there is a single resolved recipe kind — RecipeResult (NormalizeKind, pkg/recipe/metadata.go:583-597, folds Recipe/empty into it) — distinguished only by apiVersion, yet the map presents Recipe and RecipeResult as distinct kinds with distinct maturity targets, mis-states RecipeMetadata's current version (101/103 overlays are v1alpha2, not v1alpha3), and attributes RecipeMetadata/RecipeMixin file counts to Recipe/RecipeCriteria. One root cause, three surfaces (the inline 🟡 comments). Separately, the ADR-011 amendment banner under-describes the §3 change — it reverses the empty-apiVersion accept clause, not merely "extends" it — and §7 leaves an open question about the post-v1 alpha lane.
Confirmed non-issues (examined, refuted): the "integrator binds → v1" line vs RecipeResult-at-beta is reconciled by the L72 opt-in framing; the external Kubernetes URL resolves; the §3→§8 forward-reference is consistent house style.
Overlap with CodeRabbit: the 105-file comment overlaps CR L55–58; the §3-empty-tolerance comment partially overlaps CR L12–17; the §7 and §6 comments partially overlap CR L72–79; the "256 files" comment partially overlaps CR L132–144. CodeRabbit's L86–90 (GA obligation should name the release axis explicitly) has no counterpart here and is worth addressing independently.
| 🔴 Blocker | 🟠 Major | 🟡 Minor | 🔵 Nitpick | Recommendation |
|---|---|---|---|---|
| 0 | 0 | 5 | 3 | Approve with comments |
| | Kind | Today | Target | Rationale | | ||
| |---|---|---|---| | ||
| | `Snapshot` | `v1alpha2` | `aicr.run/v1` | Settled shape; the first artifact an integrator reads | | ||
| | `Recipe`, `RecipeCriteria` | `v1alpha2` | `aicr.run/v1` | Schema exercised by 105 shipped catalog files (101 overlays, 4 mixins) | |
There was a problem hiding this comment.
🟡 Minor — §2 map's Kind column conflates kind with apiVersion — "Recipe" is not a distinct wire kind
On the wire there is one resolved recipe kind, RecipeResult: NormalizeKind (pkg/recipe/metadata.go:583-597) folds empty, the legacy Recipe, and RecipeResult all to RecipeResult. But the map presents Recipe (GA row, →aicr.run/v1) and RecipeResult (beta row, →aicr.run/v1beta1) as two distinct kinds with two targets, when they are the same wire kind distinguished only by apiVersion (v1alpha2 default vs v1alpha3 profile-bearing). The intent is recoverable from the L74-77 prose, but the map itself can't be applied deterministically to a real file.
Blast radius: The #2114 implementer building the per-kind gate can't tell whether a plain v1alpha2 RecipeResult migrates to v1 (GA) or v1beta1 (beta) from the table alone.
Fix: Key the rows by apiVersion rather than by kind name, or add an explicit note that both the GA and beta rows are wire-kind RecipeResult (v1alpha2 default → v1, v1alpha3 profile → v1beta1). Drop or define the Recipe label.
| | `Recipe`, `RecipeCriteria` | `v1alpha2` | `aicr.run/v1` | Schema exercised by 105 shipped catalog files (101 overlays, 4 mixins) | | ||
| | Bundle provenance (`localformat.ProvenanceAPIVersion`) | `v1alpha2` | `aicr.run/v1` | Rides in the bundle, which is what downstream integrates against | | ||
| | `AICRConfig` | `v1alpha2` | `aicr.run/v1beta1` | Actively growing: #2026 bound 2 of 5 spec sections, #2245 binds the rest. Do not freeze a schema mid-expansion | | ||
| | `RecipeMetadata`, `RecipeResult` | `v1alpha3` | `aicr.run/v1beta1` | Newest (ADR-015), 2 overlays, opt-in via profiles | |
There was a problem hiding this comment.
🟡 Minor — "Today: v1alpha3" for RecipeMetadata/RecipeResult contradicts the catalog (101/103 overlays are v1alpha2)
Verified: 101 of 103 RecipeMetadata overlays are aicr.run/v1alpha2; only aks.yaml and gke-cos.yaml are v1alpha3, and RecipeResult dual-accepts both (IsSupportedRecipeResultAPIVersion, header.go:75-82). RecipeMetadata/RecipeResult are dual-versioned today; the single row flatly states "Today: v1alpha3." The row's own rationale ("2 overlays, opt-in via profiles") silently concedes the other 101 are not v1alpha3.
Blast radius: An implementer reading "Today: v1alpha3" could scope the §3 lockstep regeneration to the 2 v1alpha3 files and miss the 101 v1alpha2 RecipeMetadata overlays.
Fix: Show "Today: v1alpha2 + v1alpha3 (dual)" and note v1alpha3 is the opt-in profile subset.
| | Kind | Today | Target | Rationale | | ||
| |---|---|---|---| | ||
| | `Snapshot` | `v1alpha2` | `aicr.run/v1` | Settled shape; the first artifact an integrator reads | | ||
| | `Recipe`, `RecipeCriteria` | `v1alpha2` | `aicr.run/v1` | Schema exercised by 105 shipped catalog files (101 overlays, 4 mixins) | |
There was a problem hiding this comment.
🟡 Minor — GA-row rationale attributes 105 catalog files to Recipe/RecipeCriteria, but those files are kind RecipeMetadata/RecipeMixin
The 105 = 101 v1alpha2 overlays (kind: RecipeMetadata) + 4 mixins (kind: RecipeMixin). Neither is kind: Recipe or kind: RecipeCriteria, so the cited files exercise the RecipeMetadata/RecipeMixin schema (the kinds the map assigns to the beta row), not the Recipe/RecipeCriteria schema being promoted to GA. Overlaps CodeRabbit's L55-58 comment on the "105 overlays" phrasing.
Blast radius: The GA-promotion evidence for Recipe/RecipeCriteria points at a different kind's maturity; a reviewer approving GA does so on mis-labeled evidence.
Fix: Attribute the count to RecipeMetadata/RecipeMixin, and justify Recipe/RecipeCriteria GA on their own schema stability (or merge the rows per the first finding).
| @@ -1,5 +1,12 @@ | |||
| # ADR-011: Artifact apiVersion Policy and Compatibility Gate | |||
|
|
|||
| > **Amended by [ADR-022](022-artifact-maturity-and-deprecation.md).** ADR-022 | |||
There was a problem hiding this comment.
🟡 Minor — §3 retires ADR-011 §3's empty-apiVersion acceptance, but the amendment banner + Status say only "extended"
ADR-011 §3 explicitly grants "Empty apiVersion → accepted" (011 L104). ADR-022 §3 (L94-98) reverses exactly that: "Empty becomes a rejected value." That is a modification/removal of a §3 clause, yet both the ADR-011 banner (011 L3-8) and the ADR-022 Status (022 L5-7) describe §3 only as "extended to the catalog loader." A reader of ADR-011 §3 + banner would still believe empty is accepted — the very fail-open shape §8 exists to close.
Blast radius: The compatibility-gate spec (§3) is what implementers copy from; the incomplete banner leaves the empty-accept branch looking canonical, risking the fail-open hole surviving.
Fix: Amend both the banner and the ADR-022 Status to say §3 is "extended to the catalog loader and its empty-apiVersion tolerance retired," and annotate/strike ADR-011 §3's "Empty → accepted" bullet.
|
|
||
| ### 7. A new kind starts on the current track | ||
|
|
||
| A kind introduced today is stamped `aicr.run/v1alpha2` (or the post-bump |
There was a problem hiding this comment.
🟡 Minor — §7 leaves a new post-v1 kind with no alpha entry point ("the post-bump equivalent" is undefined)
§3's hard break + empty-retirement leaves the tree accepting only aicr.run/v1 and aicr.run/v1beta1 (confirmed by Consequences L182, "two version tracks... instead of today's two alpha tracks"). §7 stamps a new kind v1alpha2 "or the post-bump equivalent," and forbids a version the tree doesn't accept — so post-v1 an experimental new kind has no alpha lane: v1 = instant GA obligations, v1beta1 = instant 2-release obligations. The "post-bump equivalent" of an alpha version is undefined.
Blast radius: Directly load-bearing for the "new kinds guess today" problem the ADR (Problem Q3) sets out to solve; anyone adding a kind after v1 must guess a maturity.
Fix: State the post-bump entry version explicitly (e.g. new kinds enter at v1beta1 and accept beta obligations, or define a per-kind alpha lane) rather than "the post-bump equivalent."
|
|
||
| - Two version tracks exist at v1 — `aicr.run/v1` and `aicr.run/v1beta1` — instead | ||
| of today's two alpha tracks. | ||
| - `pkg/header` grows from two gates to roughly four, one per track, following the |
There was a problem hiding this comment.
🔵 Nitpick — Consequences: "two version tracks" vs "roughly four gates, one per track"
L182 says two tracks exist (v1, v1beta1); L184 says pkg/header grows to "roughly four, one per track." Four gates cannot be one-per-track with two tracks — the four correspond to kind-groups (Snapshot / Recipe+Criteria / Config / RecipeMetadata+Result), not tracks. "roughly" hedges it.
Blast radius: Minor reader confusion about the gate topology.
Fix: "one per kind(-group)" instead of "one per track."
|
|
||
| **This composes with §4 rather than duplicating it.** For a beta kind: N adds | ||
| read support, N+1 emits the new version and deprecates the old, and the old stays | ||
| readable through N+3. Three releases of overlap total. The two "2 releases" are |
There was a problem hiding this comment.
🔵 Nitpick — §6 "three releases of overlap" — counting basis unstated, reads as an off-by-one
The N / N+1 / N+3 walk never states whether "overlap" counts the emit release or the gate-add release; under the plain "both versions accepted" reading it's N..N+3 = 4, under the emitter reading it's 3. The arithmetic reconciles with §4's beta=2-releases, but "overlap" is undefined so "three" reads as an off-by-one.
Blast radius: Doc clarity only.
Fix: Define "overlap" (e.g. "releases in which the new version is emitted while the old remains readable = N+1..N+3").
|
|
||
| ### 3. The bump happens now, as a hard break | ||
|
|
||
| While every kind is alpha, a bump owes nothing (see §4). The 256 files carrying a |
There was a problem hiding this comment.
🔵 Nitpick — "256 files carrying a version literal" is not reproducible (false precision, cited twice)
No filter reproduces 256 at head: v1alpha2|v1alpha3 (excl. vendor/.git) = 255, aicr.run/v1alpha = 244, YAML-only = 150. The broadest count also includes prose docs (these ADRs themselves) that aren't "regenerated in lockstep." The number is cited as exact at L81 and L186 and will drift with every artifact/test/doc added.
Blast radius: Cosmetic/credibility only — the argument doesn't depend on the exact count.
Fix: Soften to "~250" or pin it to a reproducible definition (e.g. the ~150 YAML catalog/fixture files) with the exact grep filter.
Summary
Adds ADR-022, recording what
apiVersionAICR artifacts carry at v1 GA, what a bump owes the version it replaces, and whatapiVersiona newly introduced kind starts at.Motivation / Context
Every AICR artifact is alpha today, which contradicts what ROADMAP §2 promises at v1. Three questions had no recorded answer, and #2114 has been open on the first one since August 7:
apiVersiondo artifacts carry at GA?pkg/headergodoc says a hard break with no window. The code follows the godoc forIsSupportedAPIVersionand the ADR forIsSupportedRecipeResultAPIVersion. Both statements are unconditional, which is the real gap — neither says when each applies.apiVersiondoes a new kind start at? Nothing answers this, so new kinds guess.Fixes: N/A
Related: #2114 (the issue this answers), #2370 (API freeze epic), #1812 (catalog gate, Decision 8), #2113 (schema baselines, unblocked by Decision 2)
Type of Change
Component(s) Affected
docs/,examples/)Implementation Notes
Proposal only, no code. Eight decisions:
v1are separate axes. ROADMAP §2 asks for a baseline, a gate, and a deprecation channel — not for every kind to reachaicr.run/v1. A gate over av1beta1schema still catches unintended breakage, which is what the freeze actually promises. Kubernetes shipped 1.0 with beta APIs and serves beta alongside GA in every release since.Snapshot,Recipe,RecipeCriteria, and bundle provenance go toaicr.run/v1;AICRConfigand the profile-bearing kinds sit ataicr.run/v1beta1. The line is: whatever an integrator's code binds to must be atv1.pkg/headerhard-break language is correct for alpha and gains a condition rather than being reversed; ADR-013 already argued that a pre-v1 break is the cheapest possible moment.aicr.run/v1alpha1in particular has never been valid — ADR-013 moved the version tov1alpha2at the domain rename, so the legacy pairing wasaicr.nvidia.com/v1alpha1.kindonly. ADR-015 documented the consequence: a released binary pointed at a newer catalog silently resolves an unspecialized recipe.Two things worth reviewer attention:
localformat.ProvenanceAPIVersion) is in the map. It aliasesheader.GroupVersiontoday and rides in the bundle, so it is something integrators consume. It was not on the list this ADR was drafted from — flagging it explicitly so it can be argued rather than inherited.Testing
Docs-only change; all relative links and the
ROADMAP.md#2-stabilityanchor verified to resolve.Risk Assessment
Rollout notes: Proposal only. The implementation it authorizes is not low risk — a one-time regeneration of the 256 files carrying a version literal, with no transition window — but nothing in this PR performs it. That work lands under #2114.
Checklist
make testwith-race) — N/A, docs only; docs gates run abovemake lint) — docs gates run abovegit commit -S)