From 15c83df26937a6034978d69c5fb63edeafd16a83 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Mon, 17 Aug 2026 00:20:55 +0000 Subject: [PATCH 1/3] docs: record where a controlled vocabulary is published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several fields take their value from a controlled vocabulary, and the three families have handled them inconsistently because nothing said how to decide. listing `category` and `lifecycleStage` are closed enums in the schema; blueprint `size` is a length-bounded string with a TODO where its rule should be; the `advanced` pins are free-form strings with no prose at all. ADR 0003 supplies the test, and it is not volatility. It is who decides membership. A term this specification could add or remove by releasing belongs in the schema as an enum. A term that becomes real when something outside this repository is provisioned does not, however stable the list looks today — its grammar stays here as a pattern, its membership is named as an external publication surface, and a value outside it fails in the capability phase. A vocabulary nothing fixes yet gets shape only, with the silence recorded as a gap. The second decision is that a named surface is never a mirrored copy. This repository does not become a vocabulary publisher and GOVERNANCE.md §Scope is not extended. The reasoning is ADR 0001 §6's, applied to data rather than to code: a copy shipped from here acquires the authority of the thing it copies, and the first time the two disagree, implementations will have followed the one that was easier to reach rather than the one that was right. A dated, explicitly non-normative snapshot is rejected for the same reason — a reader who finds a list in the normative repository has already stopped looking, and the date they were counting on to warn them is the one thing they will not check. Refs #19 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Justin Merrell --- .../0003-controlled-vocabulary-placement.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/adr/0003-controlled-vocabulary-placement.md diff --git a/docs/adr/0003-controlled-vocabulary-placement.md b/docs/adr/0003-controlled-vocabulary-placement.md new file mode 100644 index 0000000..635a8ad --- /dev/null +++ b/docs/adr/0003-controlled-vocabulary-placement.md @@ -0,0 +1,143 @@ +# ADR 0003: Where a controlled vocabulary is published + +- **Status:** Accepted +- **Date:** 2026-08-17 +- **Extends:** [ADR 0001](0001-canonical-repository-architecture.md) §6 + +## Context + +Several fields across the three families take their value from a controlled +vocabulary rather than from a grammar alone. The specifications have handled +them inconsistently, because nothing said how to decide. + +| Field | Today | +|---|---| +| listing `category`, `lifecycleStage` | Closed `enum` in the schema | +| blueprint `size` | A length-bounded string, and a `TODO` where the rule should be | +| blueprint `advanced` pins | Free-form strings, no prose at all | + +`size` is what forced the question. It names a **Compute Profile** — a +provider-neutral machine tier — and the platform publishes both a slug grammar +and a catalog of the profiles it currently offers. Those two things have +different lifetimes. The grammar is fixed by contract. The catalog changes when +an operator enables a tier, which is neither a release of this repository nor +anything this repository can observe. + +Committing the catalog here would make it a second source of truth, and the one +that is wrong: a released artifact is immutable +([GOVERNANCE.md](../../GOVERNANCE.md) §Release process), so a list frozen at +release time drifts the moment the offering changes, and a reader who trusted it +would be told a tier exists that cannot be deployed, or told one does not exist +that can. + +Leaving the vocabulary unnamed is not the alternative it appears to be. It is +what the repository does today, and +[component §10](../../specifications/component/v1/spec.md#known-debt) records +the cost: a reader outside `musher-dev/platform` is shown +`general.standard.small` in three examples and has nowhere to look it up. + +## Decision + +### 1. Three placements, decided by what fixes the vocabulary + +| The vocabulary is… | Where it lives | Example | +|---|---|---| +| Fixed by this contract | A closed `enum` in the schema | listing `category`, `lifecycleStage` | +| Grammar fixed here, membership fixed elsewhere | A `pattern` in the schema; membership named as an external publication surface and checked in the `capability` phase | blueprint `size` | +| Not fixed anywhere yet | Shape only, and the silence recorded in prose as a gap | blueprint `advanced` pins | + +The test is not how large the vocabulary is or how often it changes. It is **who +decides membership**. A term this specification could add or remove by releasing +belongs in the schema. A term that becomes real when something outside this +repository is provisioned does not, however stable the list looks on any given +day. + +**Rejected:** deciding by volatility. `lifecycleStage` will change rarely and +`category` may change often, but both are decided here, and a rule keyed on +expected churn would move a field between placements without anything about the +contract having changed. + +### 2. A named surface, never a mirrored copy + +Where membership is fixed elsewhere, the specification MUST name where the +vocabulary is published, precisely enough to fetch: a URL a reader can open +without an account. It MUST NOT restate the members, not even informatively and +not even dated. + +This repository therefore does not become a vocabulary publisher, and +GOVERNANCE.md §Scope is not extended. The reasoning is ADR 0001 §6's, applied to +data rather than to code: a copy shipped from here acquires the authority of the +thing it copies, and the first time the two disagree, implementations will have +followed the one that was easier to reach rather than the one that was right. + +**Rejected:** a dated, explicitly non-normative snapshot committed alongside the +prose. It reads as a convenience and behaves as a trap — a reader who finds a +list in the normative repository has already stopped looking, and the date they +were counting on to warn them is the one thing they will not check. + +### 3. Membership is a `capability` failure + +An externally fixed vocabulary cannot be resolved without reaching the surface +that publishes it, and +[component §7](../../specifications/component/v1/spec.md#validation-layers) +forbids the `parser`, `structural` and `semantic` phases from requiring network +access. Membership is therefore a `capability` rule: server-side, and an offline +implementation MUST NOT report it. + +This is the same line +[blueprint §4.1](../../specifications/blueprint/v1/spec.md#component-reference) +already draws for a published component reference, and it costs the same thing — +the diagnostic gets no fixture, and appears in the runner's `UNCOVERED` list with +a reason. + +**Rejected:** treating membership as `semantic` on the grounds that a client +could cache the vocabulary. A cache turns a rule about what the platform offers +into a rule about what a client last downloaded, and two implementations would +disagree on a valid document for reasons neither could see. + +### 4. The grammar goes in the schema, and it may only grow + +A vocabulary whose membership lives elsewhere still has a shape, and the shape is +this repository's to fix. Carrying it as a `pattern` is what makes a typo fail +offline instead of on a round trip to the platform. + +Growing that grammar later — a new segment value, a new permitted token — admits +documents that were previously rejected, which is a relaxation and therefore a +minor release under +[component §3](../../specifications/component/v1/spec.md#compatibility). +Narrowing it is major. The asymmetry is what makes the placement safe: +[component §5.1](../../specifications/component/v1/spec.md#source) keeps its +floating-tag list *out* of the schema for the mirror-image reason, because that +list is a blocklist and growing it is a narrowing. + +## Consequences + +**Positive** + +- `size` gets an answer, and blueprint `spec.md` loses the largest of the + seeding-debt `TODO`s that keep v1 pre-stable. +- A reader outside the platform can resolve every value the examples show them. +- Open questions of the same shape — `category` and `lifecycleStage` governance — + are decided by applying this test rather than re-argued per field. +- A typo in an externally-fixed vocabulary still fails offline, because the + grammar stayed here. + +**Negative** + +- The specification now carries URLs it does not control. A surface that moves + breaks a normative citation, and nothing in CI can tell. +- Placement two means a document can pass every phase this repository can run and + still be undeployable. That is honest about where the knowledge lives, but it + is a weaker offline guarantee than an `enum` would have given. +- Placement three — shape only — is a stated gap rather than a rule. It is the + right record of an undecided contract, and it is still a field an author can + write that nothing checks. + +## Follow-ups + +1. Apply the test to listing `category` and `lifecycleStage`, which are + placement one today and should say so rather than being placement one by + default. +2. Revisit blueprint's `advanced` pins when the platform publishes a vocabulary + for them; they move from placement three to placement two at that point, + and gain a `capability` diagnostic. From 32925bc609ecd230108dfb62774f5f038bcea016 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Mon, 17 Aug 2026 00:21:26 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat(blueprint):=20specify=20node=20compute?= =?UTF-8?q?=20=E2=80=94=20the=20size=20grammar=20and=20the=20advanced=20pi?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4 carried a TODO where the rule for `size` should be, and `advanced` had no prose at all. `size` is REQUIRED on every node and appears in all three examples, so the field a reader meets first was the one this repository defined least. New §4.3 and §4.4 close both, and split each the way §5.1 splits the image-ref rule: the grammar is ours and lives in the schema, the vocabulary is not and does not. **The grammar is three closed segments.** family ∈ general/compute/memory/ storage/gpu/accelerator, tier ∈ economy/standard/performance/premium, size ∈ nano/small/medium/large/xlarge. A value outside them is ERR_INVALID_VALUE in the structural phase, so a typo, a legacy `1vcpu-1gb` raw-resource name, and a versioned `.v1` slug all fail offline rather than on a round trip. Closing the segments is safe in a way closing the offering would not be: an allowlist admitting more is a relaxation and ships in a minor, which is the mirror of §5.1's floating-tag blocklist staying out of the schema because growing a blocklist narrows. **The offering is named, not enumerated.** It is published unauthenticated at https://api.musher.dev/v1/reference/compute-profiles and rendered at https://docs.musher.dev/reference/compute-profiles. A profile becomes available when there is hardware to back it, which is not an event this specification can observe and not one a release of it coincides with — an enum would be wrong in both directions between releases. A slug the grammar admits and the endpoint does not name is reserved rather than available, and naming one is ERR_UNKNOWN_COMPUTE_PROFILE in the capability phase. That is the same line §4.1 already draws for a published component reference, and it costs the same thing: the code goes in UNCOVERED with a reason, because an offline implementation MUST NOT report it. §4.3 also states the consequence of naming a tier rather than a machine — the vCPU and memory behind a slug MAY differ between two deployments of an unchanged document. That is what the abstraction buys, and it should be written down rather than discovered. **§4.4 gives `advanced` shape, not vocabulary.** The nine pins narrow which host may run a node and never change the compute `size` names. A pin term is a lowercase token `^[a-z0-9][a-z0-9_-]*$`, which admits x86_64, arm64, local-nvme, nvlink and cuda; numeric pins are positive integers; array pins mean "any" when empty and may not repeat a term. `cpuDedication` is the one pin whose vocabulary this contract closes, because shared and dedicated are the whole of the concept. The other eight get no vocabulary and no diagnostic, and §4.4 says so plainly rather than implying one. Nothing publishes those terms today — not this repository and not the platform — and whether a pin is satisfiable is not decidable offline. Naming a code for it would claim an implementation reports something none does. They are recorded as gaps, in the form §4.2 already uses, and they take the shape `size` has above once the terms are published. Seven fixtures, 100 → 107 cases, 0 skipped. Four pin the slug grammar, including structural-018, which accepts `gpu.premium.xlarge` — grammatical, not currently offered, and the case that pins the split. Three pin the constraint block. `web-and-database.yaml` now sizes its two nodes differently and carries a `cpuDedication` pin, so both fields appear where a reader will look. Refs #19 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Justin Merrell --- conformance/README.md | 6 +- conformance/blueprint/v1/cases.json | 35 +++++ .../015-legacy-compute-size-slug/case.yaml | 15 ++ .../diagnostics.json | 6 + .../metadata.json | 7 + .../case.yaml | 15 ++ .../diagnostics.json | 6 + .../metadata.json | 7 + .../case.yaml | 15 ++ .../diagnostics.json | 6 + .../metadata.json | 7 + .../case.yaml | 17 +++ .../metadata.json | 7 + .../case.yaml | 25 ++++ .../metadata.json | 7 + .../020-constraint-term-not-a-token/case.yaml | 17 +++ .../diagnostics.json | 6 + .../metadata.json | 7 + .../021-unknown-cpu-dedication/case.yaml | 16 +++ .../diagnostics.json | 6 + .../021-unknown-cpu-dedication/metadata.json | 7 + .../v1/examples/web-and-database.yaml | 11 +- .../v1/schemas/dist/blueprint.schema.json | 52 ++++--- .../v1/schemas/src/blueprint.schema.json | 52 ++++--- specifications/blueprint/v1/spec.md | 134 +++++++++++++++++- tools/src/conformance.ts | 4 + 26 files changed, 452 insertions(+), 41 deletions(-) create mode 100644 conformance/blueprint/v1/structural/015-legacy-compute-size-slug/case.yaml create mode 100644 conformance/blueprint/v1/structural/015-legacy-compute-size-slug/diagnostics.json create mode 100644 conformance/blueprint/v1/structural/015-legacy-compute-size-slug/metadata.json create mode 100644 conformance/blueprint/v1/structural/016-unknown-compute-profile-family/case.yaml create mode 100644 conformance/blueprint/v1/structural/016-unknown-compute-profile-family/diagnostics.json create mode 100644 conformance/blueprint/v1/structural/016-unknown-compute-profile-family/metadata.json create mode 100644 conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/case.yaml create mode 100644 conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/diagnostics.json create mode 100644 conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/metadata.json create mode 100644 conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/case.yaml create mode 100644 conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/metadata.json create mode 100644 conformance/blueprint/v1/structural/019-advanced-compute-constraints/case.yaml create mode 100644 conformance/blueprint/v1/structural/019-advanced-compute-constraints/metadata.json create mode 100644 conformance/blueprint/v1/structural/020-constraint-term-not-a-token/case.yaml create mode 100644 conformance/blueprint/v1/structural/020-constraint-term-not-a-token/diagnostics.json create mode 100644 conformance/blueprint/v1/structural/020-constraint-term-not-a-token/metadata.json create mode 100644 conformance/blueprint/v1/structural/021-unknown-cpu-dedication/case.yaml create mode 100644 conformance/blueprint/v1/structural/021-unknown-cpu-dedication/diagnostics.json create mode 100644 conformance/blueprint/v1/structural/021-unknown-cpu-dedication/metadata.json diff --git a/conformance/README.md b/conformance/README.md index a7e3549..7b2870c 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -163,13 +163,15 @@ later-phase diagnostic before the earlier phases pass. ## Coverage status `parser`, `structural` and `semantic` are covered. Every diagnostic code the -three `spec.md` files declare is exercised by at least one case, with two -exceptions, both `capability`: +three `spec.md` files declare is exercised by at least one case, with four +exceptions, all `capability`: | Code | Why it has no case | |---|---| | `ERR_UNKNOWN_COMPONENT` | `capability` — resolving a published reference needs the catalog, and no phase a client runs may reach the network | | `ERR_VERSION_NOT_MONOTONIC` | `capability` — comparing a version against the lineage it extends needs the catalog, and a fixture is one document with no previous release to be greater than | +| `ERR_COMPONENT_NOT_PUBLISHED` | `capability` — only the registry holds publication state, and a fixture is a tree of files none of which has one | +| `ERR_UNKNOWN_COMPUTE_PROFILE` | `capability` — the slug grammar is fixtured, but which profiles are offered changes when the platform gains hardware to back a tier, not when this repository releases | That table is not prose anyone has to remember to update. `task check:conformance` derives it: every `ERR_*` row in a family's own diff --git a/conformance/blueprint/v1/cases.json b/conformance/blueprint/v1/cases.json index 50a986b..c772fc9 100644 --- a/conformance/blueprint/v1/cases.json +++ b/conformance/blueprint/v1/cases.json @@ -92,6 +92,41 @@ "phase": "structural", "path": "structural/014-missing-component-graph" }, + { + "id": "structural-015-legacy-compute-size-slug", + "phase": "structural", + "path": "structural/015-legacy-compute-size-slug" + }, + { + "id": "structural-016-unknown-compute-profile-family", + "phase": "structural", + "path": "structural/016-unknown-compute-profile-family" + }, + { + "id": "structural-017-versioned-compute-profile-slug", + "phase": "structural", + "path": "structural/017-versioned-compute-profile-slug" + }, + { + "id": "structural-018-reserved-compute-profile-slug", + "phase": "structural", + "path": "structural/018-reserved-compute-profile-slug" + }, + { + "id": "structural-019-advanced-compute-constraints", + "phase": "structural", + "path": "structural/019-advanced-compute-constraints" + }, + { + "id": "structural-020-constraint-term-not-a-token", + "phase": "structural", + "path": "structural/020-constraint-term-not-a-token" + }, + { + "id": "structural-021-unknown-cpu-dedication", + "phase": "structural", + "path": "structural/021-unknown-cpu-dedication" + }, { "id": "semantic-001-connection-names-unknown-role", "phase": "semantic", diff --git a/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/case.yaml b/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/case.yaml new file mode 100644 index 0000000..0513c65 --- /dev/null +++ b/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/case.yaml @@ -0,0 +1,15 @@ +# A raw-resource name. It says how much silicon, which is the promise §4.3 says +# a profile slug exists not to make — the same nominal vCPU count means a +# burstable shared core on one host and a dedicated modern one on another. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + size: 1vcpu-1gb + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/diagnostics.json b/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/diagnostics.json new file mode 100644 index 0000000..5bf6053 --- /dev/null +++ b/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/size" + } +] diff --git a/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/metadata.json b/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/metadata.json new file mode 100644 index 0000000..dd51f7c --- /dev/null +++ b/conformance/blueprint/v1/structural/015-legacy-compute-size-slug/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-015-legacy-compute-size-slug", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#node-compute", + "summary": "A raw-resource size slug is not a Compute Profile and is rejected." +} diff --git a/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/case.yaml b/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/case.yaml new file mode 100644 index 0000000..4dbc2db --- /dev/null +++ b/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/case.yaml @@ -0,0 +1,15 @@ +# Three segments, correct shape, invented family. The grammar is closed on all +# three segments, so this fails offline rather than travelling to the catalog to +# be told the same thing. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + size: tiny.standard.small + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/diagnostics.json b/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/diagnostics.json new file mode 100644 index 0000000..5bf6053 --- /dev/null +++ b/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/size" + } +] diff --git a/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/metadata.json b/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/metadata.json new file mode 100644 index 0000000..b3f34d8 --- /dev/null +++ b/conformance/blueprint/v1/structural/016-unknown-compute-profile-family/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-016-unknown-compute-profile-family", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#node-compute", + "summary": "A slug whose family segment is outside the closed set is rejected." +} diff --git a/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/case.yaml b/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/case.yaml new file mode 100644 index 0000000..c857c2a --- /dev/null +++ b/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/case.yaml @@ -0,0 +1,15 @@ +# The profile is versioned where it is published, and a node does not name that +# version: it names the tier and gets the current version of it. The grammar has +# three segments, so a fourth is rejected without needing a rule of its own. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + size: general.standard.small.v1 + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/diagnostics.json b/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/diagnostics.json new file mode 100644 index 0000000..5bf6053 --- /dev/null +++ b/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/size" + } +] diff --git a/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/metadata.json b/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/metadata.json new file mode 100644 index 0000000..f41c620 --- /dev/null +++ b/conformance/blueprint/v1/structural/017-versioned-compute-profile-slug/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-017-versioned-compute-profile-slug", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#node-compute", + "summary": "A node names a bare profile slug, not the versioned form." +} diff --git a/conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/case.yaml b/conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/case.yaml new file mode 100644 index 0000000..6b05307 --- /dev/null +++ b/conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/case.yaml @@ -0,0 +1,17 @@ +# This is the case that pins the split. The slug is grammatical, so the +# structural phase accepts it; whether the profile can actually be deployed is +# a capability question, and an offline validator has not been given the means +# to answer it. A schema that enumerated the offering would reject this +# document today and accept it next quarter, having changed nothing. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: inference + version: 1 +spec: + components: + api: + component: ./components/api.yaml + size: gpu.premium.xlarge + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/metadata.json b/conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/metadata.json new file mode 100644 index 0000000..f5093e0 --- /dev/null +++ b/conformance/blueprint/v1/structural/018-reserved-compute-profile-slug/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-018-reserved-compute-profile-slug", + "phase": "structural", + "expected": "pass", + "clause": "specifications/blueprint/v1/spec.md#node-compute", + "summary": "A grammatical slug validates whether or not the profile is currently offered." +} diff --git a/conformance/blueprint/v1/structural/019-advanced-compute-constraints/case.yaml b/conformance/blueprint/v1/structural/019-advanced-compute-constraints/case.yaml new file mode 100644 index 0000000..7294fd2 --- /dev/null +++ b/conformance/blueprint/v1/structural/019-advanced-compute-constraints/case.yaml @@ -0,0 +1,25 @@ +# Every one of the nine pins, each a lowercase token or a positive integer. +# The pins narrow which host may run the node; none of them changes the compute +# `size` names. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: inference + version: 1 +spec: + components: + api: + component: ./components/api.yaml + size: gpu.performance.large + advanced: + cpuArchitecture: [x86_64, arm64] + cpuDedication: dedicated + acceleratorMinVramGb: 24 + acceleratorRuntime: [cuda] + acceleratorInterconnect: nvlink + acceleratorSkuClass: h100-sxm + storageClass: local-nvme + storageMinIops: 20000 + networkClass: standard + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/019-advanced-compute-constraints/metadata.json b/conformance/blueprint/v1/structural/019-advanced-compute-constraints/metadata.json new file mode 100644 index 0000000..f63fc64 --- /dev/null +++ b/conformance/blueprint/v1/structural/019-advanced-compute-constraints/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-019-advanced-compute-constraints", + "phase": "structural", + "expected": "pass", + "clause": "specifications/blueprint/v1/spec.md#advanced-constraints", + "summary": "A node carrying every advanced compute pin validates." +} diff --git a/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/case.yaml b/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/case.yaml new file mode 100644 index 0000000..97ea1a9 --- /dev/null +++ b/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/case.yaml @@ -0,0 +1,17 @@ +# This contract does not say which storage classes exist, but it does say what +# one looks like. A display string is not a term, and two documents spelling the +# same class differently could not be compared. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + size: storage.standard.large + advanced: + storageClass: Local NVMe + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/diagnostics.json b/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/diagnostics.json new file mode 100644 index 0000000..388c8e8 --- /dev/null +++ b/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/advanced/storageClass" + } +] diff --git a/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/metadata.json b/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/metadata.json new file mode 100644 index 0000000..86a4615 --- /dev/null +++ b/conformance/blueprint/v1/structural/020-constraint-term-not-a-token/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-020-constraint-term-not-a-token", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#advanced-constraints", + "summary": "An advanced pin whose value is not a lowercase token is rejected." +} diff --git a/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/case.yaml b/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/case.yaml new file mode 100644 index 0000000..50b0ce8 --- /dev/null +++ b/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/case.yaml @@ -0,0 +1,16 @@ +# The one pin whose vocabulary this contract closes. A vCPU is shared or it is +# not; a third term would be a different concept wearing this one's name. +specVersion: v1 +kind: BLUEPRINT +metadata: + slug: postgres + version: 1 +spec: + components: + db: + component: ./components/postgres.yaml + size: general.standard.small + advanced: + cpuDedication: exclusive + connections: {} + parameters: {} diff --git a/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/diagnostics.json b/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/diagnostics.json new file mode 100644 index 0000000..26edbbc --- /dev/null +++ b/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/diagnostics.json @@ -0,0 +1,6 @@ +[ + { + "code": "ERR_INVALID_VALUE", + "path": "/spec/components/db/advanced/cpuDedication" + } +] diff --git a/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/metadata.json b/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/metadata.json new file mode 100644 index 0000000..fef95ec --- /dev/null +++ b/conformance/blueprint/v1/structural/021-unknown-cpu-dedication/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "structural-021-unknown-cpu-dedication", + "phase": "structural", + "expected": "fail", + "clause": "specifications/blueprint/v1/spec.md#advanced-constraints", + "summary": "cpuDedication is closed to shared and dedicated." +} diff --git a/specifications/blueprint/v1/examples/web-and-database.yaml b/specifications/blueprint/v1/examples/web-and-database.yaml index 6fd0e67..45e5cb1 100644 --- a/specifications/blueprint/v1/examples/web-and-database.yaml +++ b/specifications/blueprint/v1/examples/web-and-database.yaml @@ -6,6 +6,10 @@ # # The two nodes show that a repo-local reference does not mandate a directory # layout — `db` names a document under `components/`, `web` a flat sibling. +# +# They also size independently. Compute is chosen per node rather than baked +# into the component, so the database gets more of it than the web server +# without either document changing. specVersion: v1 kind: BLUEPRINT metadata: @@ -15,7 +19,12 @@ spec: components: db: component: ./components/postgres.yaml - size: general.standard.small + size: general.standard.medium + # Optional pins narrowing which host may run this node. They never change + # the compute `size` names — this asks for the profile's vCPU on a host + # that dedicates it, not for more of it. + advanced: + cpuDedication: dedicated connections: {} web: component: ./component-web-server.yaml diff --git a/specifications/blueprint/v1/schemas/dist/blueprint.schema.json b/specifications/blueprint/v1/schemas/dist/blueprint.schema.json index 5c6fdb8..a5b759a 100644 --- a/specifications/blueprint/v1/schemas/dist/blueprint.schema.json +++ b/specifications/blueprint/v1/schemas/dist/blueprint.schema.json @@ -34,13 +34,16 @@ "type": "object", "$defs": { "BlueprintComputeConstraints": { - "description": "Advanced, optional compute pins honored when the node is placed on a host.", + "description": "Advanced, optional compute pins that narrow the hosts a node may be placed on. A pin never changes the compute the node's size names. See spec.md §4.4.", + "$comment": "Every term pin is a lowercase token rather than a member of a closed set: the grammar is fixed by this contract, the vocabularies are not published anywhere yet, and spec.md §4.4 records that as a gap. cpuDedication is the exception — two values are the whole concept.", "additionalProperties": false, "properties": { "acceleratorInterconnect": { - "description": "Required interconnect such as 'nvlink' or 'pcie', or null for any.", + "description": "Required accelerator interconnect, such as nvlink or pcie. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -50,7 +53,7 @@ "default": null }, "acceleratorMinVramGb": { - "description": "Minimum accelerator VRAM in GiB, or null for no floor.", + "description": "Minimum accelerator VRAM in GiB. Null pins no floor.", "anyOf": [ { "minimum": 1, @@ -63,16 +66,21 @@ "default": null }, "acceleratorRuntime": { - "description": "Required accelerator runtimes, e.g. ['cuda']. Empty means any.", + "description": "Required accelerator runtimes, such as cuda. An empty array pins nothing, and a term may not repeat. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "items": { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, - "type": "array" + "type": "array", + "uniqueItems": true }, "acceleratorSkuClass": { - "description": "Exact accelerator SKU class to pin, or null.", + "description": "Exact accelerator SKU class to pin. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -82,16 +90,23 @@ "default": null }, "cpuArchitecture": { - "description": "Allowed CPU architectures, e.g. ['x86_64', 'arm64']. Empty means any.", + "description": "Permitted CPU architectures, such as x86_64 or arm64. An empty array pins nothing, and a term may not repeat. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "items": { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, - "type": "array" + "type": "array", + "uniqueItems": true }, "cpuDedication": { - "description": "CPU dedication: 'shared' or 'dedicated', or null for the platform default.", + "description": "Whether the node's vCPU must be dedicated rather than shared. Null pins nothing. This is the one pin whose vocabulary this contract closes — see spec.md §4.4.", "anyOf": [ { + "enum": [ + "shared", + "dedicated" + ], "type": "string" }, { @@ -101,9 +116,11 @@ "default": null }, "networkClass": { - "description": "Required network class such as 'standard', or null for the default.", + "description": "Required network class. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -113,9 +130,11 @@ "default": null }, "storageClass": { - "description": "Required storage class such as 'local-nvme', or null for the default.", + "description": "Required storage class, such as local-nvme. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -125,7 +144,7 @@ "default": null }, "storageMinIops": { - "description": "Minimum provisioned IOPS, or null for no floor.", + "description": "Minimum provisioned IOPS. Null pins no floor.", "anyOf": [ { "minimum": 1, @@ -201,7 +220,7 @@ }, "properties": { "advanced": { - "description": "Optional advanced compute pins that narrow the eligible-host set at schedule time.", + "description": "Optional advanced compute pins that narrow the eligible-host set at schedule time. Absent, null, and a block with no pin set all mean the same thing. See spec.md §4.4.", "anyOf": [ { "$ref": "#/$defs/BlueprintComputeConstraints" @@ -239,9 +258,10 @@ "type": "object" }, "size": { - "description": "Compute Profile slug bound to this node, such as general.standard.small. Required per node — a component document carries no compute of its own.", - "maxLength": 128, - "minLength": 1, + "description": "Compute Profile slug bound to this node, in family.tier.size form — for example general.standard.small. Required per node, because a component document carries no compute of its own. The grammar is fixed here; the profiles actually on offer are published at https://api.musher.dev/v1/reference/compute-profiles.", + "$comment": "Lookahead-free, so it compiles under RE2 as well as ECMA-262. Only the grammar is here. Which profiles are actually offered changes when the platform gains hardware to back a tier, not when this schema is released, so membership is a capability check rather than an enum. See spec.md §4.3.", + "maxLength": 64, + "pattern": "^(general|compute|memory|storage|gpu|accelerator)\\.(economy|standard|performance|premium)\\.(nano|small|medium|large|xlarge)$", "type": "string" } }, diff --git a/specifications/blueprint/v1/schemas/src/blueprint.schema.json b/specifications/blueprint/v1/schemas/src/blueprint.schema.json index ef689f1..dee5af2 100644 --- a/specifications/blueprint/v1/schemas/src/blueprint.schema.json +++ b/specifications/blueprint/v1/schemas/src/blueprint.schema.json @@ -34,12 +34,15 @@ "type": "object", "$defs": { "BlueprintComputeConstraints": { + "$comment": "Every term pin is a lowercase token rather than a member of a closed set: the grammar is fixed by this contract, the vocabularies are not published anywhere yet, and spec.md §4.4 records that as a gap. cpuDedication is the exception — two values are the whole concept.", "additionalProperties": false, - "description": "Advanced, optional compute pins honored when the node is placed on a host.", + "description": "Advanced, optional compute pins that narrow the hosts a node may be placed on. A pin never changes the compute the node's size names. See spec.md §4.4.", "properties": { "acceleratorInterconnect": { "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -47,7 +50,7 @@ } ], "default": null, - "description": "Required interconnect such as 'nvlink' or 'pcie', or null for any." + "description": "Required accelerator interconnect, such as nvlink or pcie. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4." }, "acceleratorMinVramGb": { "anyOf": [ @@ -60,18 +63,23 @@ } ], "default": null, - "description": "Minimum accelerator VRAM in GiB, or null for no floor." + "description": "Minimum accelerator VRAM in GiB. Null pins no floor." }, "acceleratorRuntime": { - "description": "Required accelerator runtimes, e.g. ['cuda']. Empty means any.", + "description": "Required accelerator runtimes, such as cuda. An empty array pins nothing, and a term may not repeat. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "items": { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, - "type": "array" + "type": "array", + "uniqueItems": true }, "acceleratorSkuClass": { "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -79,18 +87,25 @@ } ], "default": null, - "description": "Exact accelerator SKU class to pin, or null." + "description": "Exact accelerator SKU class to pin. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4." }, "cpuArchitecture": { - "description": "Allowed CPU architectures, e.g. ['x86_64', 'arm64']. Empty means any.", + "description": "Permitted CPU architectures, such as x86_64 or arm64. An empty array pins nothing, and a term may not repeat. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4.", "items": { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, - "type": "array" + "type": "array", + "uniqueItems": true }, "cpuDedication": { "anyOf": [ { + "enum": [ + "shared", + "dedicated" + ], "type": "string" }, { @@ -98,11 +113,13 @@ } ], "default": null, - "description": "CPU dedication: 'shared' or 'dedicated', or null for the platform default." + "description": "Whether the node's vCPU must be dedicated rather than shared. Null pins nothing. This is the one pin whose vocabulary this contract closes — see spec.md §4.4." }, "networkClass": { "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -110,11 +127,13 @@ } ], "default": null, - "description": "Required network class such as 'standard', or null for the default." + "description": "Required network class. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4." }, "storageClass": { "anyOf": [ { + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", "type": "string" }, { @@ -122,7 +141,7 @@ } ], "default": null, - "description": "Required storage class such as 'local-nvme', or null for the default." + "description": "Required storage class, such as local-nvme. Null pins nothing. The permitted terms are not a vocabulary this contract defines — see spec.md §4.4." }, "storageMinIops": { "anyOf": [ @@ -135,7 +154,7 @@ } ], "default": null, - "description": "Minimum provisioned IOPS, or null for no floor." + "description": "Minimum provisioned IOPS. Null pins no floor." } }, "type": "object" @@ -215,7 +234,7 @@ } ], "default": null, - "description": "Optional advanced compute pins that narrow the eligible-host set at schedule time." + "description": "Optional advanced compute pins that narrow the eligible-host set at schedule time. Absent, null, and a block with no pin set all mean the same thing. See spec.md §4.4." }, "component": { "anyOf": [ @@ -244,9 +263,10 @@ "type": "object" }, "size": { - "description": "Compute Profile slug bound to this node, such as general.standard.small. Required per node — a component document carries no compute of its own.", - "maxLength": 128, - "minLength": 1, + "$comment": "Lookahead-free, so it compiles under RE2 as well as ECMA-262. Only the grammar is here. Which profiles are actually offered changes when the platform gains hardware to back a tier, not when this schema is released, so membership is a capability check rather than an enum. See spec.md §4.3.", + "description": "Compute Profile slug bound to this node, in family.tier.size form — for example general.standard.small. Required per node, because a component document carries no compute of its own. The grammar is fixed here; the profiles actually on offer are published at https://api.musher.dev/v1/reference/compute-profiles.", + "maxLength": 64, + "pattern": "^(general|compute|memory|storage|gpu|accelerator)\\.(economy|standard|performance|premium)\\.(nano|small|medium|large|xlarge)$", "type": "string" } }, diff --git a/specifications/blueprint/v1/spec.md b/specifications/blueprint/v1/spec.md index 2d0de61..a59ec56 100644 --- a/specifications/blueprint/v1/spec.md +++ b/specifications/blueprint/v1/spec.md @@ -127,11 +127,9 @@ document. Two blueprints MAY each declare a node called `db` and neither is the other's. Its one job is to be what [§4.2](#connections) `fromRole` names, and what [§5.2](#merge) orders the graph by. -> **TODO** — `size` MUST name a Compute Profile in `family.tier.size` form. -> State whether an unknown profile is a `semantic` or a `capability` failure. -> The profile vocabulary itself is not defined in this repository and a reader -> outside the platform cannot resolve a slug like `general.standard.small`; -> naming where it is published is part of closing this. +A node carries three things: the component it deploys ([§4.1](#component-reference)), +the wires feeding it ([§4.2](#connections)), and the compute it runs on +([§4.3](#node-compute)). ### 4.1 Component reference @@ -335,6 +333,123 @@ what makes the node names comparable across a conformance corpus instead of an artifact of whichever node the traversal happened to start from. The diagnostic anchors at `/spec/components//connections`. +### 4.3 Node compute + +`size` is REQUIRED on every node and names the **Compute Profile** it runs on — +a provider-neutral machine tier, not a raw resource request. A component +document carries no compute of its own +([component §5](../../component/v1/spec.md#workload)), so the node is the only +place it can be said, and two blueprints MAY run the same component version at +different sizes without forking it. + +```yaml +db: + component: ./components/postgres.yaml + size: general.standard.medium + connections: {} +``` + +**A profile slug has three segments, `family.tier.size`.** Two layers enforce +it, and the split between them is deliberate. + +The three segments are drawn from closed sets. A value outside them is rejected +in the `structural` phase with `ERR_INVALID_VALUE`. + +| Segment | Members | +|---|---| +| family | `general`, `compute`, `memory`, `storage`, `gpu`, `accelerator` | +| tier | `economy`, `standard`, `performance`, `premium` | +| size | `nano`, `small`, `medium`, `large`, `xlarge` | + +That is a grammar, so the schema carries it. The last segment shares the field's +name and is not the field: `size` holds the whole slug, and `small` is one third +of one. + +A tier names a capability band rather than a workload — for the `gpu` and +`accelerator` families, `economy` through `premium` run entry inference to +frontier training. The exact accelerator is not encoded in the slug; it is +pinned, if at all, by [§4.4](#advanced-constraints). + +**A grammatical slug is not necessarily an offered one.** Which profiles are +actually available is not a property of this document, and a node naming one +that is not is rejected with `ERR_UNKNOWN_COMPUTE_PROFILE` in the `capability` +phase. Deciding it needs the catalog, which needs the network, which +[§6](#validation-layers) forbids the earlier phases from reaching — so an +offline implementation MUST NOT report it, on the same grounds +[§4.1](#component-reference) gives for a published component reference. It has +not been given the means to check. + +**Where the vocabulary is published.** The profiles on offer are served, +unauthenticated, at +`https://api.musher.dev/v1/reference/compute-profiles`, and rendered for a +reader at . The endpoint +lists what can be deployed now; a slug the grammar admits and the endpoint does +not name is reserved rather than available. + +**Why the vocabulary is not an `enum`.** The grammar is settled and the +membership is not: a profile becomes available when the platform has hardware +to back it, which is not an event this specification can observe and not one a +release of it coincides with. A schema that enumerated the offering would be +wrong in both directions between releases — naming tiers that cannot yet be +deployed, and rejecting ones that can. Growing the *grammar* is safe by +contrast, because an allowlist admitting more is a relaxation and ships in a +minor release ([component §3](../../component/v1/spec.md#compatibility)); +narrowing one would be major. That is the mirror of +[component §5.1](../../component/v1/spec.md#source), where the floating-tag +blocklist stays out of the schema precisely because growing a *blocklist* is a +narrowing. + +**A bare slug, and never a versioned one.** A profile is versioned where it is +published, and a slug carrying that version is not a value this field takes — +the grammar has three segments and rejects a fourth. A node names the profile +and gets the current version of it, which means the vCPU and memory behind a +slug MAY differ between two deployments of an unchanged document. That is the +point of naming a tier rather than a machine: a name like `4vcpu-16gb` fixes +numbers it cannot keep, promising identical silicon across hardware generations +that do not deliver it. + +### 4.4 Advanced constraints + +`advanced` is OPTIONAL and narrows the hosts a node may be placed on. Absent, +`null`, and a block whose every pin is unset all mean the same thing: no +constraints. A pin whose value is an array means "any" when the array is empty, +and MUST NOT repeat a term. + +| Pin | Narrows | +|---|---| +| `cpuArchitecture` | Permitted CPU architectures | +| `cpuDedication` | `shared` or `dedicated` | +| `acceleratorMinVramGb` | Accelerator VRAM floor | +| `acceleratorRuntime` | Required accelerator runtimes | +| `acceleratorInterconnect` | Required accelerator interconnect | +| `acceleratorSkuClass` | Exact accelerator SKU class | +| `storageClass` | Required storage class | +| `storageMinIops` | Provisioned IOPS floor | +| `networkClass` | Required network class | + +**A pin narrows placement and nothing else.** `size` remains what the node runs +as; a pin only reduces the set of hosts that may run it. A node pinning +`cpuDedication: dedicated` gets the vCPU and memory its profile names, on a host +that dedicates them — never more compute than it asked for, and never a +substitute profile. + +**A pin term is a lowercase token**, `^[a-z0-9][a-z0-9_-]*$`, which admits +`x86_64`, `arm64`, `local-nvme`, `nvlink` and `cuda`. Numeric pins are positive +integers. Both are grammar, both are `structural`, and a violation is +`ERR_INVALID_VALUE`. `cpuDedication` is the one pin whose vocabulary this +contract closes, because `shared` and `dedicated` are the whole of the concept +and a third value would be a different one. + +**What v1 does not constrain.** No vocabulary is published for the other eight +pins — not by this repository and not, today, by the platform. `local-nvme` is +an example rather than a member, and nothing here says what the permitted +storage classes are. Nor does anything say what happens when no host satisfies a +pin: it is not decidable offline, and it carries no diagnostic in v1. Those are +gaps rather than considered permissions, recorded here so a reader can tell the +two apart. Naming a code for them would claim an implementation reports +something none does; closing them properly means publishing the terms first, at +which point they take the same shape `size` has above. + ## 5. Parameters `spec.parameters` is the install form: what a deploying user is asked for once, @@ -438,6 +553,7 @@ family adds: | `ERR_UNKNOWN_INPUT` | `semantic` | A connection's map key names no input of the consuming node's component. | | `ERR_UNWIRED_REQUIRED_INPUT` | `semantic` | A node's required `CONNECTION` input is satisfied by no connection. | | `ERR_COMPONENT_NOT_PUBLISHED` | `capability` | A published `component` reference resolves to a component that is not in a published state. | +| `ERR_UNKNOWN_COMPUTE_PROFILE` | `capability` | A node's `size` names a Compute Profile the catalog does not offer. | | `ERR_INCOMPATIBLE_TYPE` | `semantic` | A connection joins an output and an input whose `schema.type`s differ. | | `ERR_INCOMPATIBLE_SEMANTIC_TYPE` | `semantic` | A connection joins an output and an input whose `schema.semanticType`s disagree. | | `ERR_DEPENDENCY_CYCLE` | `semantic` | The connection graph contains a cycle. | @@ -454,5 +570,11 @@ fixture in [`conformance/blueprint/v1/`](../../../conformance/blueprint/v1/). ## 9. Known debt Seeded from the platform's generated schema. The naming that arrived with it -has been cleaned; the sections above marked TODO have not. See +has been cleaned, and [§4.3](#node-compute) now names where the Compute Profile +vocabulary is published rather than assuming a reader can already resolve it. + +One TODO remains, in [§5.2](#merge): how an authored parameter binds to the +component inputs it satisfies. [§4.4](#advanced-constraints) is not marked TODO +but carries the nearest thing to one — a pin vocabulary nothing publishes yet, +recorded there as a gap. See also [component §10](../../component/v1/spec.md#known-debt). diff --git a/tools/src/conformance.ts b/tools/src/conformance.ts index e5d3094..7fbe654 100644 --- a/tools/src/conformance.ts +++ b/tools/src/conformance.ts @@ -441,6 +441,10 @@ const UNCOVERED: ReadonlyMap = new Map([ 'ERR_COMPONENT_NOT_PUBLISHED', 'capability — only the registry holds publication state, so deciding it needs the catalog, and a fixture is a tree of files none of which has one', ], + [ + 'ERR_UNKNOWN_COMPUTE_PROFILE', + 'capability — the grammar is fixtured (structural/015 through 018), but which profiles are offered changes when the platform gains hardware to back a tier, so deciding membership needs the catalog', + ], ]) /** From 6cd4af5a23ffc5a40e587df71a47a9993740caf2 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Mon, 17 Aug 2026 00:21:45 +0000 Subject: [PATCH 3/3] docs(component): narrow the known-debt paragraph to what is still unpublished MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §10 said two things were unresolvable by a reader outside the platform: "snapshot compute", and Compute Profile slugs like `general.standard.small`. The second is no longer true. Blueprint §4.3 carries the slug grammar and names where the offered profiles are published, so the paragraph now claims a debt that has been paid. Narrowed to the half that remains, with a pointer to §4.3 and ADR 0003 for the half that does not. CLAUDE.md's Known debt section carried the same sentence and gets the same correction; its claim that every spec.md still has TODO sections is also corrected, since component has none and blueprint is down to one. Scoped docs(component) so it cuts no release of its own. Refs #19 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Justin Merrell --- CLAUDE.md | 9 +++++---- specifications/component/v1/spec.md | 16 +++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8d9f854..99b02e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,8 @@ an unscoped `feat:` releases nothing. The three schemas were seeded from the platform's Pydantic-generated catalog schemas. The naming and the generated titles have been cleaned and `lint.ts` now rejects both, but the seeding still shows in the prose: some `description` -text uses platform vocabulary (`snapshot compute`, Compute Profile slugs) that -a reader outside `musher-dev/platform` cannot resolve. The `TODO` sections in -each `spec.md` are the larger remaining gap — they are what keeps v1 -pre-stable. +text uses platform vocabulary (`snapshot compute`) that a reader outside +`musher-dev/platform` cannot resolve. Compute Profile slugs used to be on that +list; blueprint §4.3 now carries the grammar and names where the vocabulary is +published, per docs/adr/0003. The `TODO` sections remaining in `blueprint` and +`listing` `spec.md` are the larger gap — they are what keeps v1 pre-stable. diff --git a/specifications/component/v1/spec.md b/specifications/component/v1/spec.md index 2e31655..2d6cd37 100644 --- a/specifications/component/v1/spec.md +++ b/specifications/component/v1/spec.md @@ -684,8 +684,14 @@ implements the prose rather than standing in for it. One debt remains, and it MUST be resolved before v1 is declared stable. Some schema `description` fields still speak the platform's vocabulary rather than this contract's — "resolved server-side at snapshot compute" names a pipeline -stage a reader outside `musher-dev/platform` cannot look up, and `size` values -like `general.standard.small` name a Compute Profile vocabulary this repository -does not publish. Descriptions are explanatory rather than normative, so nothing -in this document turns on them; a reader who cannot resolve the words is still -being told to go somewhere they cannot reach. +stage a reader outside `musher-dev/platform` cannot look up. Descriptions are +explanatory rather than normative, so nothing in this document turns on them; a +reader who cannot resolve the words is still being told to go somewhere they +cannot reach. + +The Compute Profile half of that debt is closed. +[Blueprint §4.3](../../blueprint/v1/spec.md#node-compute) now carries the slug +grammar and names where the offered profiles are published, so a slug like +`general.standard.small` resolves for a reader outside the platform. +[ADR 0003](../../../docs/adr/0003-controlled-vocabulary-placement.md) records +the rule that decided it.