Skip to content

feat(blueprint): specify node compute — the size grammar, the Compute Profile catalog, and the advanced pins - #35

Merged
justinmerrell merged 3 commits into
mainfrom
spec/compute-profile-vocabulary-issue-19
Aug 17, 2026
Merged

feat(blueprint): specify node compute — the size grammar, the Compute Profile catalog, and the advanced pins#35
justinmerrell merged 3 commits into
mainfrom
spec/compute-profile-vocabulary-issue-19

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

Blueprint §4 carried a TODO where the rule for size should be, and the sibling advanced block had no prose at all. Both are now specified in new §4.3 Node compute and §4.4 Advanced constraints, each split the way component §5.1 splits the image-ref rule: the grammar is this contract's and lives in the schema, the vocabulary is not and does not. ADR 0003 records the rule that decided it.

Why

Closes #19.

size is REQUIRED on every node, appears in all three examples and in nearly every blueprint fixture, and was the field this repository defined least — a bare {type: string, minLength: 1, maxLength: 128}. Component §10 and CLAUDE.md both named it as the largest remaining piece of the seeding debt keeping v1 pre-stable.

The issue asked three questions. All three have answers in musher-dev/platform, and this PR takes them:

Question Answer Source
Grammar {family}.{tier}.{size}, three closed enums, declared a public contract platform ADR 0041 §"Slug grammar (Type-1 public contract)"; ADR 0090 amends 0041's own gpu.inference.large example, which is not valid under its own tier enum
Vocabulary Published by the platform, not here GET https://api.musher.dev/v1/reference/compute-profiles — "Surface: public. Auth: none (global capability dictionary, cacheable)". Rendered at https://docs.musher.dev/reference/compute-profiles
Phase capability The publish gate is one SELECT 1 FROM compute_profiles WHERE slug = ? AND status = 'ACTIVE'. It needs the catalog, and §6 forbids the earlier phases from reaching the network

Both URLs were fetched while writing this — the API returns the four ACTIVE general profiles unauthenticated; the docs page returns 200.

§4.3 — the grammar is closed, the offering is not

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 to the platform.

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 release, so a seventh family later costs nothing. That is the mirror of §5.1, where the floating-tag blocklist stays out of the schema precisely because growing a blocklist narrows. Same principle, opposite sign.

The offering is named, never mirrored. A profile becomes available when the platform has hardware to back it — ADR 0090 reserves ~48 slugs as DISABLED and an operator flips one to ACTIVE once a host class is seeded. That is not an event this specification can observe and not one a release of it coincides with, so an enum would be wrong in both directions between releases: naming tiers that cannot be deployed, rejecting ones that can. A slug the grammar admits and the endpoint does not name is reserved rather than available, and naming one is ERR_UNKNOWN_COMPUTE_PROFILE, capability — the same line §4.1 already draws for a published component reference, and it costs the same thing: the code sits in UNCOVERED with a reason, because an offline implementation MUST NOT report it.

§4.3 also writes down 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 stated rather than discovered.

§4.4 — advanced gets shape, and its gaps get recorded

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_-]*$ (admits x86_64, arm64, local-nvme, nvlink, 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. Nothing publishes those terms today — not this repository, and not the platform: they are plain str with no enum anywhere, five of the nine have no host attribute to match against, accelerator_classes is seeded empty, and platform ADR 0095 (which would make Placement honour them) is accepted but not shipped. Inventing ERR_UNSATISFIABLE_CONSTRAINT would claim an implementation reports something none does, and ADR 0002 already rejected parking a rule in capability to dodge writing a fixture. They are recorded as gaps in the form §4.2 uses, and they take §4.3's shape once the terms are published.

ADR 0003 — the rule, not just this field

The issue anticipated that the vocabulary question might need an ADR, since publishing a controlled vocabulary is arguably a fourth artifact kind. The ADR concludes the opposite and says why. The test is who decides membership, not how volatile the list is:

The vocabulary is… Where it lives Example
Fixed by this contract closed enum in the schema listing category, lifecycleStage
Grammar fixed here, membership fixed elsewhere pattern in the schema; membership named as an external surface, checked in capability blueprint size
Not fixed anywhere yet shape only, silence recorded as a gap blueprint advanced pins

GOVERNANCE.md §Scope is not extended. A dated, explicitly non-normative snapshot was considered and rejected: 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 meant to warn them is the one thing they will not check.

Three things worth a reviewer's attention

1. structural-018 accepts gpu.premium.xlarge. That fixture exists to be a pass case for a profile nobody can currently deploy. It is the whole argument in one file: grammar is not availability, and a schema that enumerated the offering would reject this document today and accept it next quarter having changed nothing.

2. The conformance/README.md coverage table was already stale. It claimed two uncovered codes and listed two, but UNCOVERED has held three since #31ERR_COMPONENT_NOT_PUBLISHED was never added to the prose. Corrected here alongside the new fourth row. The runner's own gate was right; only the hand-written mirror had drifted.

3. maxLength on size drops 128 → 64 and minLength goes away. Both are subsumed by the pattern (the longest grammatical slug, accelerator.performance.xlarge, is 30 characters). Keeping a bound alongside a bounded pattern follows the component property's precedent rather than being load-bearing.

Compatibility

  • Breaking — a previously valid document now fails.

Three narrowings: the size pattern, the cpuDedication enum, and the pin token grammar plus uniqueItems.

No new v<N> directory, and the reason is that v1 has never been published. git tag -l is empty and .github/release-please/manifest.json reads 0.0.0 for all three families. Component §3's guarantee — "a document that validated against v1.0.0 MUST validate against every later v1.x.y" — has no released v1.0.0 to run from, so no document anywhere was validated against a published v1 and would now fail.

That argument is the one #31 made, and it carries the same expiry: it dies when #1, #2 or #3 merge. If a release PR lands first, this becomes a v2 change.

Corpus impact — verified before each narrowing

  • general.standard.small is the only profile slug anywhere in the repository (51 occurrences, across examples, spec prose, and both the blueprint and listing fixture trees). It passes the new pattern unchanged.
  • advanced appears in no document in the corpus, so its new grammar breaks nothing.
  • Each new negative fixture was run against the pre-change bundle and confirmed to report "expected to fail but validated cleanly" — the rule, not the fixture, is what changed.

Checklist

  • task check passes locally
  • schemas/dist/ regenerated with task bundle and committed (never edited by hand)
  • Conformance fixtures added for every behavioural change, each citing a clause
  • Normative prose updated in the affected spec.md — schema descriptions are explanatory, not normative
  • Commit messages are Conventional and correctly scoped (the scope drives release-please)
  • Commits are DCO signed off (git commit -s)

7 new fixtures, 100 → 107 cases, 0 skipped. All 9 examples validate. ERR_UNKNOWN_COMPUTE_PROFILE is the one new code without a fixture, and it sits in UNCOVERED with a reason.

One local note: task check:workflow cannot run in this dev container — the installed actionlint is an x86-64 binary on an aarch64 host and fails with exec format error. It is unrelated to this branch, which touches no files under .github/. Every other gate (check:format, check:types, check:schema, check:drift, check:examples, check:conformance, check:shell) passes locally, and task ci:test is fully green.

🤖 Generated with Claude Code

justinmerrell and others added 3 commits August 17, 2026 00:20
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) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
…nced pins

§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) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
…ublished

§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) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

blueprint §4: the size / Compute Profile vocabulary, and where it is published

1 participant