Skip to content

docs: add ADR-022 per-kind artifact maturity and deprecation policy - #2373

Open
mchmarny wants to merge 3 commits into
mainfrom
docs/adr-022-artifact-maturity
Open

docs: add ADR-022 per-kind artifact maturity and deprecation policy#2373
mchmarny wants to merge 3 commits into
mainfrom
docs/adr-022-artifact-maturity

Conversation

@mchmarny

@mchmarny mchmarny commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

Adds ADR-022, recording 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.

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:

  1. What apiVersion do artifacts carry at GA?
  2. What does a bump owe the version it replaces? ADR-011 §4 says dual-accept with a transition window. The pkg/header godoc says a hard break with no window. The code follows the godoc for IsSupportedAPIVersion and the ADR for IsSupportedRecipeResultAPIVersion. Both statements are unconditional, which is the real gap — neither says when each applies.
  3. What apiVersion does 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Build/CI/tooling

Component(s) Affected

  • Docs/examples (docs/, examples/)

Implementation Notes

Proposal only, no code. Eight decisions:

  • Project v1 and artifact v1 are separate axes. ROADMAP §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, which is what the freeze actually promises. Kubernetes shipped 1.0 with beta APIs and serves beta alongside GA in every release since.
  • 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. The line is: whatever an integrator's code binds to must be at v1.
  • The window is conditional on the level being retired — alpha none, beta two releases, GA not removed within a major. This replaces ADR-011 §4. The pkg/header hard-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.
  • 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, so the legacy pairing was aicr.nvidia.com/v1alpha1.
  • External data fails closed. Extends ADR-011 §3 to the catalog loader, which today checks kind only. ADR-015 documented the consequence: a released binary pointed at a newer catalog silently resolves an unspecialized recipe.

Two things worth reviewer attention:

  • Bundle provenance (localformat.ProvenanceAPIVersion) is in the map. It aliases header.GroupVersion today 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.
  • Two releases for the beta window is deliberate, not inherited. The Kubernetes equivalent is ~18 releases at our cadence. A short window keeps at most one retired version in the read gate. The cost is that a consumer upgrading less than monthly can miss a window entirely.

Testing

make check-docs-filenames    # OK: all doc filenames follow kebab-case convention
make check-docs-mdx          # OK: all doc files are MDX-safe
make check-docs-mdx-parse    # OK: 54 doc file(s) parse as MDX

Docs-only change; all relative links and the ROADMAP.md#2-stability anchor verified to resolve.

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert
  • Medium — Touches multiple components or has broader impact
  • High — Breaking change, affects critical paths, or complex rollout

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

  • Tests pass locally (make test with -race) — N/A, docs only; docs gates run above
  • Linter passes (make lint) — docs gates run above
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality — N/A, docs only
  • I updated docs if user-facing behavior changed
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)

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>
@mchmarny mchmarny added area/docs area/api theme/ci-dx CI pipelines, developer experience, and build tooling labels Aug 25, 2026
@mchmarny mchmarny self-assigned this Aug 25, 2026
@github-actions github-actions Bot added size/M and removed area/api labels Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report

Coverage data unavailable for this run. This can happen if the qualification workflow was cancelled or failed before generating coverage artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai

This comment was marked as resolved.

coderabbitai[bot]

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>
@github-actions github-actions Bot added size/L and removed size/M labels Aug 25, 2026
@mchmarny
mchmarny marked this pull request as ready for review August 25, 2026 12:08
@mchmarny
mchmarny requested a review from a team as a code owner August 25, 2026 12:08

@njhensley njhensley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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) |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs size/L theme/ci-dx CI pipelines, developer experience, and build tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants