Accepted. pkg/recipe.CoordinateFor ships the ratified mapping; consumers may
import it as a stable contract.
Multiple workstreams need to answer the same question — where does this recipe live on the validation board? — and they must all answer it identically. The TestGrid track (#1266–#1273) emits per-recipe build results into a board; RQ1 (#1283) deep-links from the recipe-health Evidence column into that board; GP4 (#1404) and GP5 (#1405) expose and render the board. If each of these computed its own service/accelerator/intent placement, the four surfaces would drift the moment any one of them changed a delimiter, a wildcard rule, or a segment order.
Two failure modes follow from an unowned mapping:
- Independent forks diverge. The recipe
metadata.nameis accelerator-first (h100-eks-ubuntu-training); the board coordinate is service-first (eks/h100-ubuntu/training). Anyone who derives the coordinate by string-munging the name will eventually produce a different layout than a peer who derives it from criteria. - Ambiguous placement. Without a fail-closed contract, a recipe missing a
required dimension (or carrying an
anywildcard) silently maps to a plausible-looking but wrong cell, fusing unrelated results.
This ADR fixes the mapping as a single, pure Go function —
pkg/recipe.CoordinateFor — and pins the surrounding taxonomy, URL form, and
column-metadata schema that its consumers share.
In scope:
- The canonical recipe → coordinate mapping (
pkg/recipe.CoordinateFor, shipped inpkg/recipe/coordinate.go). - The board taxonomy (group / dashboard / tab / row / column).
- The stable coordinate URL form.
- The pinned column-metadata key schema (
started.jsonkeys). - Reconciliation with ADR-009 (recipe-health) — how the two surfaces coexist.
- The #1224 cross-link contract that recipe-health consumes.
Out of scope:
- The consumers themselves — TG2 (#1267), TG3, TG4a, RQ1 (#1283), GP4 (#1404). They import this mapping; their internals are owned by their own issues.
- The API and UI that render the board (GP5).
- The live board / hosting / navigation host.
The user-facing TestGrid page (docs/user/testgrid.md)
presents this contract for end users; this ADR remains the internal source of
truth that the page and the dependent workstreams derive from.
The board is a five-level addressing space. CoordinateFor produces only the
first three levels — the group, dashboard, and tab — from resolved recipe
criteria. The row and column come from the CTRF test data, not the recipe,
and are out of this function's hands entirely.
| Level | Value | Source |
|---|---|---|
| Group | service |
recipe criteria (CoordinateFor) |
| Dashboard | <accelerator>-<os> |
recipe criteria (CoordinateFor) |
| Tab | intent, or <intent>-<platform> |
recipe criteria (CoordinateFor) |
| Row | <phase>/<check> |
CTRF test data (NOT the recipe) |
| Column | one build | CTRF test data (NOT the recipe) |
Stating this split explicitly matters: a recipe defines which board cell a
result belongs in (group/dashboard/tab); the validation run defines what is in
that cell (which checks ran, in which build). CoordinateFor therefore returns
a three-field Coordinate{Group, Dashboard, Tab} and nothing about rows or
columns.
The input is resolved Criteria. The function consumes already-resolved
criteria and never parses metadata.name. This is deliberate: the recipe
name is accelerator-first and the coordinate is service-first, and the two must
stay independently correct — coupling them through string parsing would make a
rename of either silently corrupt the other.
- Required, concrete dimensions:
service,accelerator,intent. Each must be a concrete value. AnilCriteria, an empty value, or theanywildcard (CriteriaAnyValue, the string"any") fails closed withErrCodeInvalidRequest, naming the offending dimension. A recipe that fails this check is not coordinatable; callers skip it (it has no board cell). - Optional os — os-agnostic recipes:
osis optional. A concrete os yields<accelerator>-<os>(h100-ubuntu); an empty oranyos yields the literal<accelerator>-any(h100-any). Unlike the platform sub-segment, os is kept as-any, not dropped, so the dashboard segment stays a well-formed<accelerator>-<os>pair thatsplitDashboardinverts and the renderer's<accelerator>-<os>grouping stays aligned with the coordinate path. This is the nvkind (service: kind) case: it runs on whatever OS the runner has, so its overlays declare no os and it lands under anh100-anydashboard, distinct from the cloud lanes'h100-ubuntu/h100-cos. - Optional sub-segment — platform:
platformis an optional segment. An empty oranyplatform yields a bare intent tab (training); a concrete platform yields<intent>-<platform>(training-kubeflow). This sub-segment is dropped, never"unknown"-substituted — there is no placeholder token in the tab. - Purity: the function is pure — no clock, no maps, no registry, no I/O. The same Criteria always yields the same Coordinate.
- Join key: the overlay
metadata.nameis the catalog identity; the coordinate is the placement. The criteria-only coordinate and the evidence recipe slug both derive from the same leaf — see Reconciliation with ADR-009 for the three identities in play and which surface uses which. (CoordinateFordoes not read the name — addressing happens at the caller, which already holds both the name and the resolved criteria.)
This matches the shipped Coordinate struct: Group = service,
Dashboard = accelerator + "-" + os, Tab = intent (or intent-platform).
The Kubernetes version is not part of the coordinate. It lives in the column (one build = one k8s version). Three facets of this decision must be held together:
- Benefit — stable, addressable links. Because k8s is a property of the
build, the coordinate (
eks/h100-ubuntu/training) is invariant across k8s versions. RQ1's deep-links and TG4a's exposed paths stay stable as clusters upgrade; the link target does not churn every time a new K8s minor ships. - Cost — silent fusion. The flip side: two columns for the same coordinate but different k8s versions sit in the same tab. A stale-version result could be read alongside a current-version result and silently fuse in a reader's mind into one "this recipe's posture" signal, even though one of them is no longer relevant.
- Countermeasure. k8s version is exposed as a UI facet / filter (so a reader can scope to a version), plus a latest-per-signer default scope (so the default view does not mix stale and current results). Both the facet and the default scope are required to keep the benefit without paying the fusion cost.
The canonical coordinate string is:
<group>/<dashboard>/<tab>
For example: eks/h100-ubuntu/training-kubeflow (with platform) or
eks/h100-ubuntu/training (bare intent). This is exactly what
Coordinate.Path() (and String()) returns.
Path() is a stable opaque identity, not a decomposable key. Dimension
values may themselves contain the - join character (rtx-pro-6000; a future
--data intent such as fine-tuning), so eks/rtx-pro-6000-ubuntu/training
has no positional split point — a consumer cannot recover (rtx-pro-6000, ubuntu) from the string alone. Consumers therefore treat Path() as a value
that is stable and equal-comparable (deep-links, presence checks) and address
the dimensions via the Coordinate struct fields, never by string-splitting
the path. A decomposable key would require an explicit ParseCoordinate; that
is deferred to the first parsing consumer (RQ1 #1283 / TG4a #1284), which would
also pin a "values must not contain the active delimiter" invariant.
The one delimiter the mapping does enforce is /: CoordinateFor fails closed
(ErrCodeInvalidRequest) on any dimension value containing /, because the path
separator is the single character whose presence would change the segment count
and silently mis-place the recipe — the exact ambiguous-placement failure this
ADR exists to prevent.
This string is the canonical criteria-only base coordinate that RQ1
deep-links to and that TG4a / GP expose. The host, the navigation scheme
(path vs. fragment), and any consumer-specific route-key refinement are
owned by the consumer (GP5 / TG4a), not by this mapping. pkg/recipe
emits only the canonical segments; it makes no assumption about scheme,
host, or trailing decoration. For ADR-015 profile-bearing recipes the two
consumers refine the base coordinate differently, by design: the Golden
Path corroboration route appends the lowercase -<name>-<value> profile
segment to the <tab> component (two values of one family must hold
separate results), while TestGrid keeps the unsuffixed base coordinate and
partitions per value through its digest-bound build ID.
Each build column carries a fixed set of metadata keys in its started.json.
This schema is specified here and enforced by TG2 (#1267) — the emitter — and
referenced by TG3's Header.extra; no Go constant or struct pins these names
today, so TG2 must lock the key names, order, and count (e.g. in a shared
constant + a round-trip test) when it lands, rather than relying on this prose.
The contract is 7 keys, in this order. The strings below are the canonical
snake_case key names; consumers use them verbatim.
| # | Key | Source | Example | Notes |
|---|---|---|---|---|
| 1 | aicr_version |
binary that ran the validation | v0.42.0 |
The aicr release that produced the result. |
| 2 | k8s_version |
observed cluster | 1.33 |
Observed cluster version, major.minor only — drives the k8s-in-column facet. |
| 3 | k8s_constraint |
recipe | >= 1.32.4 |
The recipe-declared K8s.server.version constraint, for at-a-glance "did the observed version satisfy intent?". |
| 4 | signer_identity |
attestation | https://github.com/NVIDIA/aicr/.github/workflows/... |
Signer identity (the SAN / subject of the signing identity). |
| 5 | signer_issuer |
attestation | https://token.actions.githubusercontent.com |
Signer OIDC issuer — pairs with identity to scope the latest-per-signer default. |
| 6 | source_class |
provenance | ci |
Source class (e.g. ci vs. ad-hoc / local), so a reader can weigh trust. |
| 7 | evidence_digest |
evidence | sha256:… |
Digest of the underlying evidence artifact — the verifiable anchor for the result. |
Notes:
k8s_versionis observed (what the cluster actually reported);k8s_constraintis declared (what the recipe asked for). Keeping both distinct is what lets the board show satisfied-vs-violated without re-resolving the recipe.signer_identity+signer_issuertogether key the latest-per-signer default scope referenced in the k8s-in-column countermeasure.- The key strings and their count (7) are the contract: TG2 emits exactly these; TG3 reads exactly these. Adding a key is a change to this table.
This board and the recipe-health surface in ADR-009 are two surfaces that coexist; they do not duplicate each other, and neither is the other's source.
- recipe-health (ADR-009,
docs/user/recipe-health.md) owns the offline structural / freshness surface — computed without a live cluster, from the recipe catalog itself. - TestGrid / this coordinate board owns the live validation-posture surface — derived from actual validation runs against real clusters.
ADR-009 fixed this split deliberately under its two-axis principle: structural soundness and validation posture "must never collapse into a single fused score." This coordinate board is the home of the deferred validation-posture axis — it is a separate surface, not a richer rendering of the structural matrix.
The two surfaces line up on recipe identity — but not on one shared key. Three distinct identities are in play:
- The catalog leaf overlay name —
metadata.nameof the overlay file. recipe-health (pkg/health) enumerates recipes by this name viaMetadataStore.ListCatalogfiltered to leaf overlays (CatalogEntry.IsLeaf), which yields the resolved criteria + leaf name per maximal-leaf overlay, the leaf-driven (not cartesian) enumeration ADR-009 specifies. Overlaymetadata.namenever carries a profile suffix. - The criteria-only coordinate —
CoordinateForover the leaf's resolved criteria;Coordinate.Path()stays unsuffixed regardless of profile. - The evidence recipe slug —
attestation.RecipeNameForover the resolved criteria, with a lowercase-<name>-<value>profile segment appended for a profile-bearing recipe. Evidence directories and the corroborate board (Tab.Recipe/Series.Recipe, the series-file slug) use this slug.
The profiled suffix applies to the evidence slug and to the Golden
Path value-dashboard route (which appends the profile segment to the
tab) — never to overlay metadata.name, the criteria-only base
coordinate, or the TestGrid coordinate (TestGrid keeps the base
coordinate and partitions values by digest-bound build ID). The
coordinate board derives its coordinate from the resolved criteria of
the same leaf, so the surfaces line up on identity without sharing
computation.
GP (#1400) is the interim, static GitHub Pages surface for this evidence; TG (#1263) is the live stack — upstream TestGrid workers, an AICR-native API, a greenfield UI, and an always-on GKE host cluster. Both are built in parallel; this ADR's mapping function, taxonomy, and column-metadata schema are shared, not duplicated, by both.
- Same foundation. GP and TG read the same verified, source-keyed evidence
tree and derive coordinates from the same
pkg/recipe.CoordinateFormapping (§Mapping rules above). Neither forks the taxonomy. (Whether they share one GCS bucket and publish service account, or stand up their own, is deliberately left open — see the deconfliction note below; the shared contract is the evidence tree and its layout, not a specific bucket.) - Forward-compatible, not throwaway. GP4's (#1404) coordinate-keyed JSON
contract (
index.json+series/*.json) is a forward-compatible input to TG's workers/API/UI — a future migration consumes GP's already-coordinate-keyed data rather than re-deriving it. - No deferral. GP shipping first does not defer any TG child — TG1–TG7 remain Ready / in progress. Build-both-vs-defer is a separate strategic decision, not implied by sequencing, and is not made by this ADR.
One concrete overlap is intentionally left for GP3 (#1403) and TG1 (#1266) to deconflict directly, outside this ADR: whether the write-only publish SA and bucket are shared between the two surfaces or stood up twice on purpose.
Operator-facing detail for each surface lives in docs/user/testgrid.md (TG) and docs/user/evidence-dashboard.md (GP).
recipe-health's Evidence column links into this coordinate URL — it never duplicates the board's content. The interface is:
- recipe-health computes a recipe's
metadata.nameand its resolved criteria, callsCoordinateFor, and emitsCoordinate.Path()as a link target (the deep link is RQ1 / #1283). - The link is bot-verifiable via TG4a's coordinate-presence endpoint (RQ2 / #1284): an automated check can confirm the linked coordinate actually exists on the board, so the Evidence column never links into a void.
Status of the recipe-health side: the structural matrix has shipped —
pkg/health computes it and docs/user/recipe-health.md
publishes it — and the health generator (tools/health/markdown.go) emits
presence-gated deep-links today, consuming CoordinateFor +
Coordinate.Path() exactly as described here. The committed
recipe-health.md carries live links for unprofiled recipes with a
committed dashboard presence; a recipe without a committed presence stays a
literal pending, and profiled families' presence entries are deliberately
withheld until profile-aware links land.
The following workstreams import pkg/recipe.CoordinateFor and must never
fork or reimplement the mapping:
- TG2 (#1267) — emits the pinned column metadata; uses the coordinate to place build results.
- TG3 — reads the column metadata via
Header.extra; uses the coordinate for tab/dashboard placement. - TG4a — exposes the coordinate URL form and the coordinate-presence endpoint (#1284).
- RQ1 (#1283) — deep-links into the coordinate URL from recipe-health.
- GP4 (#1404) — exposes the board built on these coordinates.
Single-sourcing the mapping in pkg/recipe.CoordinateFor is the anti-drift
guarantee: every surface derives the same placement from the same resolved
criteria, so a change to the taxonomy is a one-line change in one function, not a
hunt across five repositories of consumers.