feat: close the endpoint, environment-variable, and graph-rule specification gaps - #31
Merged
Merged
Conversation
§5.2 restricted a PUBLIC endpoint to the HTTP family on two arguments, and the edge-port address form answers both. A PUBLIC HTTP/HTTPS/WS/GRPC endpoint publishes a URL; a PUBLIC TCP or UDP endpoint publishes a host:port address allocated at the edge. Exposing a database, a broker or a game server is a capability that ships today and that a conformant document could not express. Three rules follow the split rather than the protocol: - The schema conditional restricting PUBLIC to four protocols is removed. - §5.4's readiness requirement narrows to a SERVICE exposing at least one PUBLIC endpoint in the HTTP family. The rationale §5.4 already gives for exempting PRIVATE — a mesh consumer retries, a browser does not — covers an L4 consumer unchanged, and compelling a probe against a port that speaks no HTTP is a rule no document can satisfy. - A probe's endpoint and a platform default's endpoint MUST resolve to an HTTP-family endpoint, named or elected as primary. A probe polls an HTTP path, and PUBLIC_URL and PUBLIC_HOSTNAME both derive from a URL. New code ERR_ENDPOINT_NOT_HTTP, semantic. structural-027-public-tcp-endpoint inverts to a pass case, and drops its readiness probe: the probe is no longer compelled, and keeping it would now fail the semantic phase instead. structural-028-public-grpc-endpoint stands. Whether the contract should expose the edge address is left undecided and recorded as such in §5.2 rather than left to be inferred. Closes #28 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
§5.2 gave an endpoint name the slug grammar on the grounds that the name
"becomes a DNS label". It does not become one alone — it is composed into one
beside the component and application names, and the slug grammar breaks that
composition in two ways.
A name drawn from the same alphabet as the parts it is joined to cannot be
delimited: with a hyphen separator, endpoint `api` on component `web` and
component `api-web` compose to the same label, and doubling the separator only
helps if neither side may contain the doubled form — which the slug grammar
permits. And a 63-character label shared with three other parts leaves no room
for a name allowed to reach 62, forcing every implementation into a truncation
rule that reintroduces the collisions the grammar was meant to prevent.
The grammar is now ^[a-z][a-z0-9]{0,19}$, applied in the three places that
carry it: the endpoints map's propertyNames, ComponentProbe.endpoint, and
ComponentPlatformDefault.endpoint. Twenty characters leave at least forty for
the rest of the label and fit every name worth having.
Narrowing, and free only before the first tag. Nothing in the corpus moves:
the names in use are api, console, db, internal, metrics, primary, rpc and web.
Closes #29
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
§5.3 was a bare TODO while the schema already carried the whole shape. This writes the behaviour down and settles the three questions #17 raises. Key grammar is REQUIRED, not conventional: ^[A-Z_][A-Z0-9_]*$, structural, already enforced and now stated. Precedence has no answer because a collision is an error. envVars is a sequence rather than a mapping, so §5's "a repeated key is ERR_DUPLICATE_KEY in the parser phase" does not reach it — two entries of a sequence repeat nothing at the YAML level, and a repeated key today means whatever an implementation's last write happens to be. An input's target.envVarKey binds into the same namespace and competes with those entries. Both are now semantic errors: ERR_DUPLICATE_ENV_KEY for two entries sharing a key, and ERR_CONFLICTING_ENV_KEY for a key claimed by more than one declaration. Ranking the two instead would settle the ambiguity without telling anyone there was one, which is the trade blueprint §5.2 refused for input merging. Because the anchor is normative, the tiebreak is written down: the collision anchors at an input's target/envVarKey, and where two inputs collide, at the later in lexicographic input-name order. Sensitivity: isSensitive on a LITERAL obliges the handling §6.1 already requires of a generated input. A CONFIG_REF needs no marking — the document holds the name, not the value. A SHOULD prefers either of those to a marked literal, since isSensitive governs handling and not the fact that the value is bytes in a committed file. Closes #17 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
§4.2 resolved both ends of a connection and rejected each half independently, but never asked whether the two fit. A STRING output wired into an INTEGER input satisfied every rule the specification stated and failed at deploy time inside the consuming workload — the last place a composition could be internally inconsistent and still validate. The rule lives in blueprint §4.2, which resolves the connection; component §6.2's TODO becomes a pointer to it, since a component document sees only one end. `type` is compared for equality, with no widening in either direction. An INTEGER output feeding a STRING input looks harmless, but which string is a per-language formatting decision, and a contract that permitted the wire would be promising a value it cannot describe. `semanticType` must agree wherever the consuming input names one. A consumer declaring null accepts any producer; a consumer declaring a tag requires the same tag, including rejecting a null producer, because an unconstrained producer does not satisfy a constrained consumer. That is also the answer to what the tag is for: type is the primitive shape, semanticType is the backing service, and a Postgres connection string and a MySQL one are both STRING. Two codes rather than one, so a diagnostic names the axis that failed: ERR_INCOMPATIBLE_TYPE and ERR_INCOMPATIBLE_SEMANTIC_TYPE, both semantic, both anchored at the connection's fromOutput. Both ends always carry a schema with a required type, so there is no unconstrained-producer case to except. format, enum, pattern, default and isSensitive take no part in the decision, and §4.2 records those silences as gaps rather than as permissions. Closes #18 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
The platform's blueprint validator enforces graph rules §7 does not name.
Each is answered here, and the two that need no row say so rather than going
unmentioned.
spec.components is now REQUIRED and must declare at least one node. §1 already
made a blueprint a composition of one or more components; the schema accepted
`spec: {}`. Answered structurally, so NO_COMPONENTS needs no code of its own —
an absent mapping is ERR_MISSING_FIELD and an empty one ERR_INVALID_VALUE.
ERR_UNKNOWN_INPUT is the mirror of ERR_UNKNOWN_OUTPUT, and was a plain
omission: §4.2 resolved the producer end of a wire and left the consumer end,
so a connection could be misspelled at one end only. The regression pin found
it immediately — semantic-010-well-formed-item wired DATABASE_URL into a
component that declared no such input, and had been passing as "well formed"
since it was written. That fixture is corrected here.
ERR_UNWIRED_REQUIRED_INPUT covers a required CONNECTION input no connection
satisfies. Such an input never reaches the install form, so a graph that
leaves one unwired has no later chance to supply it.
ERR_COMPONENT_NOT_PUBLISHED gets a capability row and an UNCOVERED entry:
publication state is held only by the registry, so a fixture is a tree of
files none of which has one. §4.1 says why it reads differently from
ERR_UNKNOWN_COMPONENT — a component that exists and is not yet released is a
wait rather than a typo.
DUPLICATE_WIRE_TARGET gets no row and §4.2 records why: connections is a
mapping keyed by the consumer input, so a second wire to one input is a
repeated key and already ERR_DUPLICATE_KEY in the parser phase. It is
impossible by construction rather than unenforced.
INCOMPATIBLE_SEMANTIC_TYPE is answered by the previous commit.
Closes #30
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
§10 said "every section above marked TODO is currently defined only by the schema's structure". No section is marked TODO any more — §5.3 and §6.2 were the last two, and both now carry prose. What is left is the other half of the seeding, and it is named rather than implied: schema descriptions that speak the platform's vocabulary. "Resolved server-side at snapshot compute" names a pipeline stage, and a Compute Profile slug names a vocabulary this repository does not publish. Neither is normative, which is why it is debt rather than a defect — but a reader who cannot resolve the words is still being sent somewhere they cannot reach. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
This was referenced Aug 11, 2026
This was referenced Aug 12, 2026
Closed
justinmerrell
added a commit
that referenced
this pull request
Aug 17, 2026
…e edge address (#41) * docs: reconcile the specification against implemented platform behaviour ADR 0002's follow-up 2 named three points where this specification and the platform deliberately disagree, to be reconciled before v1 is declared stable. ADRs are immutable, so ADR 0005 discharges it rather than editing it. Two of the three reverse: cycle detection is withdrawn, and a SERVICE is required to declare an endpoint. The parameter-merge conflict stands, and is the one divergence left for the platform to close. Issue #32's edge-address question is settled in the same pass with two additive platformDefault sources. ADR 0005 §1 writes down the pre-publication window that decides whether a narrowing is free or costs a major. It has been relied upon twice already and existed only in pull request descriptions; GOVERNANCE.md now points at it. No deviation register is created, and §6 records why rather than leaving the absence to read as an oversight. Refs: #24, #32 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * test(blueprint): invert the cycle fixtures to pass cases Deliberately red. Both cases assert that a cyclic connection graph validates, which the current rule rejects: ✗ semantic-002-connection-cycle: expected to pass but failed in semantic: ERR_DEPENDENCY_CYCLE at /spec/components/api/connections ✗ semantic-011-three-node-cycle: expected to pass but failed in semantic: ERR_DEPENDENCY_CYCLE at /spec/components/cache/connections semantic-011 is renamed from -three-node-cycle-reporting: its subject was the closed-walk reporting order, which goes away with the rule. It is kept rather than retired because a three-node cycle declared out of edge order is a cycle only a real traversal finds, which a two-node case cannot pin. The third failure in this state — ERR_DEPENDENCY_CYCLE declared in the prose and exercised by no case — is the coverage checker correctly objecting to a code with no fixture. The next commit removes the code. Refs: #24 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * feat(blueprint): permit a cyclic connection graph Withdraws the acyclicity requirement and ERR_DEPENDENCY_CYCLE. The two fixtures inverted in the previous commit now pass; 122 cases green. The rule was held on the grounds that permitting cycles obliges every implementation to be a two-pass resolver in perpetuity. It does not: component §6.2 already makes an output a function of its own node, so resolving outputs before binding edges is what the contract describes rather than a burden this rule would have lifted. §4.2 conceded as much in the same clause it stated the requirement — 'acyclicity is not a resolution hazard'. Against that stood mutual service discovery, a composition with a live consumer that the rule made inexpressible. ADR 0005 §2 records the decision. What the loosening forecloses is stated in the clause rather than left to be discovered: no later rule may assume a materialisation order without becoming a major version. Loosening only — every blueprint that validates today still validates. Refs: #24 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * test(blueprint): pin that an identical input redeclaration is absorbed Deliberately red, and it caught a defect rather than merely describing one. semantic-009 pins that a differing redeclaration is rejected. Nothing pinned the other half of §5.2 — that an identical one is absorbed in silence, and that ui, isRequired, the order the schema's keys are written in, and whether a default is written out or left implicit are all excluded from the comparison. ✗ semantic-022-identical-input-redeclaration: expected to pass but failed ERR_CONFLICTING_INPUT_SCHEMA at /spec/components/db/component §5.2 says two declarations are identical 'when their schema blocks are equal once defaults are applied'. tools/src/semantic.ts compares JSON.stringify(schema), which is key-order sensitive and applies no defaults, so it rejects two declarations the prose calls the same. Per ADR 0001 §1 the implementation is the defective one; the next commit corrects it. Refs: #24 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * fix(blueprint): compare input schemas by what they declare, not how they were written §5.2 makes two input declarations identical 'when their schema blocks are equal once defaults are applied'. The reference implementation compared JSON.stringify of the block as parsed, which is neither: it is sensitive to the order the keys were written in, and it treats a default left implicit as different from the same default spelled out. Both are the same mistake — comparing the text rather than the declaration — and both reject a composition §5.2 calls well-formed. semantic-022 is now green; 123 cases pass. The rule itself is unchanged and stands: a differing redeclaration is ERR_CONFLICTING_INPUT_SCHEMA rather than a silent first-wins discard. ADR 0005 §3 records why the specification holds its position here, and that the platform's merge_user_inputs is the implementation that has to change. Refs: #24 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * test(component): assert a SERVICE must declare at least one endpoint Deliberately red. Both cases assert a rejection the current schema does not make: ✗ structural-033-service-without-endpoints: expected to fail but validated cleanly ✗ structural-034-service-with-empty-endpoints: expected to fail but validated cleanly Two cases rather than one because the two spellings read differently to an author and carry different codes — an absent block is ERR_MISSING_FIELD, a block written and left empty is ERR_INVALID_VALUE. §5's existing rule that a forbidden field 'MAY be omitted, or written in its own empty form' makes the distinction load-bearing on the other kinds, and the required direction should be no less precise. Refs: #24 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * feat(component)!: require a SERVICE to declare at least one endpoint Adopts the rule the platform already enforces. structural-033 and -034 are green; task check is green at 125 cases and 9 examples. This is the divergence that ran the other way. Everywhere else the specification rejected a document the platform accepted, so an author following the contract was safe and the implementation had to catch up. Here the specification was looser: it called a document valid that the platform will not deploy, which is the one direction a contract must not be wrong in. Permitting it also left kind carrying no information, since §5 already conceded that such a workload and a WORKER are operationally the same thing. Corpus impact, verified rather than assumed: - minimal.yaml and structural-001-minimal-valid gain a PRIVATE endpoint. The smallest valid component is now a service serving one private port. PRIVATE rather than PUBLIC because §5.4 compels a readiness probe only on a PUBLIC HTTP-family endpoint, so this keeps 'minimal' minimal. - semantic-007-probe-on-endpointless-workload is re-based onto a WORKER. Its premise — a probe on a workload declaring no endpoint — is now unwritable as a SERVICE, but the other three kinds declare none and may still carry a probe, so the ERR_AMBIGUOUS_ENDPOINT it pins survives intact. - Fourteen structural negatives and three semantic cases gain a PRIVATE endpoint. The runner tolerates an extra diagnostic, so none of them was failing — but a negative fixture that fails for two reasons stops pinning the rule it names, and structural-011-schedule-on-service would have stayed red with the schedule rule deleted. Each now fails only for its own rule. - Seven component documents inside listing item trees gain one. They are not structurally validated by the runner, so they were silently non-conformant. Three documents still declare a SERVICE with no endpoints, all deliberately: the two parser cases, which fail before the structural phase is entered, and structural-033, which exists to be this. BREAKING CHANGE: a SERVICE declaring no endpoints, or an empty endpoints mapping, is now rejected. No new v<N> directory: no family has been published, git tag -l is empty and the release-please manifest reads 0.0.0, so §3's compatibility guarantee has no released version to run from. ADR 0005 §1 sets out that window and §4 records this decision. The window closes on the first tag. Closes #24 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * test(component): assert the edge address is derivable, and close three coverage gaps Deliberately red: ✗ structural-035-public-address-default: expected to pass but failed in structural ✗ structural-036-public-port-on-udp-endpoint: expected to pass but failed in structural ✗ semantic-012-public-address-on-http-endpoint: ERR_ENDPOINT_NOT_L4 is declared by no diagnostics table reachable from component/v1/spec.md ✗ semantic-013-platform-default-elects-primary: expected to pass but failed in structural Three of the five also close gaps the corpus already had, found while scoping this and worth fixing whatever #32 decided: - structural-037-public-hostname-default is green on arrival. PUBLIC_HOSTNAME has existed since the source enum was written and no case exercised it; only PUBLIC_URL appeared anywhere in the corpus. A regression pin, not a new rule. - semantic-013 is the first platformDefault case to omit its endpoint and rely on §5.2's election. Every other one names its endpoint outright. - structural-036 is the first PUBLIC UDP endpoint in the corpus. Only TCP was exercised, which left half of the L4 family untested. Refs: #32 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * feat(component): derive the edge address through PUBLIC_ADDRESS and PUBLIC_PORT Settles what §5.2 recorded as declined rather than overlooked: a PUBLIC TCP or UDP endpoint publishes a host:port edge address, and no document could reference it. The platform ships the capability today, so a component whose sibling needs a broker's or a database's edge address at install time could not say so. All five fixtures from the previous commit are green; 130 cases and 9 examples pass. platformDefault.source gains PUBLIC_ADDRESS for the whole host:port and PUBLIC_PORT for the allocated port alone. Two rather than one because a consumer taking host and port as separate settings should not have to split a string this contract had already composed. The four sources now pair with §5.2's two address forms, and the pairing is exclusive in both directions. PUBLIC_URL and PUBLIC_HOSTNAME still require an HTTP-family endpoint; PUBLIC_ADDRESS and PUBLIC_PORT require a TCP or UDP one, and naming an HTTP-family endpoint is the new ERR_ENDPOINT_NOT_L4. Two codes so a diagnostic names the axis that failed, as blueprint §4.2 does for its two compatibility axes. Admitting an HTTP-family endpoint to PUBLIC_ADDRESS was refused deliberately and §6.1 says why: such an endpoint is published through the shared ingress rather than on a port allocated to it, so the derivation would yield the ingress address — true, and not what an author asking for an edge address wants. A source returning a defensible value nobody asked for fails silently at runtime. The #32 alternative of a self.publicAddress.<endpoint> namespace is rejected in ADR 0005 §5: this contract has no expression language, and introducing one to answer a single question is out of proportion to it. Additive throughout — the enum widens, and no document that validates today names either source or can trip the new code. multi-endpoint.yaml gains the worked example and loses a header comment that asserted the opposite. Closes #32 Signed-off-by: Justin Merrell <merrelljustin@gmail.com> * docs: correct the statements the four decisions left stale Three places asserted something that is no longer true, and each would have misled a reader rather than merely reading oddly: - README 'Status' sent readers to 'the TODO sections in each spec.md'. None has existed since #31. It now says what is actually true — nothing is released, and what remains is recorded in each family's Known debt — and points at ADR 0005 §1 for the window that closes on the first tag. - component §5.2 said a probe and a platform default 'both derive something only a URL-published endpoint has'. Half of that is now wrong: a platform default reads either address form, and each source is tied to the row it takes its value from. - The HTTP_FAMILY comment in tools/ said the same thing about both platform-default sources, of which there are now four. Scoped docs and unscoped so it cuts no release of its own. Signed-off-by: Justin Merrell <merrelljustin@gmail.com> --------- Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
Closes the five open specification issues that were either silent where the platform ships behaviour, or decided against it. Two revisit rules merged eight days ago in #27 (§5.2's
PUBLICprotocol restriction and the endpoint-name grammar); three closeTODOmarkers and registry gaps left open by #13 and #9. Componentspec.mdnow carries noTODOsections at all.Six commits, one per issue plus a documentation correction, scoped so release-please reads them correctly.
Why
Closes #28. Closes #29. Closes #17. Closes #18. Closes #30.
§5.2 —
PUBLICon every protocol (#28)§5.2 restricted a
PUBLICendpoint to the HTTP family on two arguments, and the edge-port address form answers both. An HTTP-familyPUBLICendpoint publishes a URL; aTCP/UDPone publishes ahost:portaddress. Exposing a database, a broker or a game server is a capability that ships today and that a conformant document could not express.PUBLICprotocolHTTP/HTTPS/WS/GRPConlyreadinessREQUIREDSERVICEwith anyPUBLICendpointSERVICEwith aPUBLICHTTP-family endpointplatformDefaultendpointERR_ENDPOINT_NOT_HTTPThe readiness narrowing is not a new argument. §5.4 already exempts
PRIVATEbecause "a private consumer inside the mesh retries; a browser does not" — an L4 consumer connecting to an edge port retries in exactly the same way. Compelling a probe there would also compel an HTTP path against a port that speaks no HTTP, which is a rule no document can satisfy.structural-027-public-tcp-endpointinverts to a pass case and drops its readiness probe: the probe is no longer compelled, and keeping it would now fail thesemanticphase instead.structural-028-public-grpc-endpointstands unchanged.Whether the contract should expose the edge address is left undecided and recorded as such in §5.2, rather than left to be inferred.
§5.2 — endpoint-name grammar (#29)
The name does not become a DNS label; it is composed into one beside the component and application names, and the slug grammar breaks that composition twice. A name drawn from the same alphabet as the parts it joins to cannot be delimited — with a hyphen separator, endpoint
apion componentweband componentapi-webcompose to the same label, and doubling the separator only helps if neither side may contain the doubled form, which the slug grammar permits. And a 63-character label shared with three other parts leaves no room for a name allowed to reach 62, forcing every implementation into a truncation rule that reintroduces the collisions the grammar was meant to prevent.Now
^[a-z][a-z0-9]{0,19}$, applied in the three places that carry it: theendpointsmap'spropertyNames,ComponentProbe.endpoint, andComponentPlatformDefault.endpoint.§5.3 — environment variables (#17)
§5.3 was a bare
TODOwhile the schema already carried the whole shape.^[A-Z_][A-Z0-9_]*$,structural, already enforced and now stated.envVarsis a sequence rather than a mapping, so §5's "a repeated key isERR_DUPLICATE_KEYin theparserphase" does not reach it — two entries of a sequence repeat nothing at the YAML level, and a repeated key today means whatever an implementation's last write happens to be. An input'starget.envVarKeybinds into the same namespace and competes with those entries. Both are nowsemanticerrors:ERR_DUPLICATE_ENV_KEYandERR_CONFLICTING_ENV_KEY. Ranking the two instead would settle the ambiguity without telling anyone there was one — the trade blueprint §5.2 refused for input merging.target/envVarKey, and where two inputs collide, at the later in lexicographic input-name order.isSensitiveon aLITERALobliges the handling §6.1 already requires of a generated input. ACONFIG_REFneeds no marking — the document holds the name, not the value. A SHOULD prefers either to a marked literal, sinceisSensitivegoverns handling and not the fact that the value is bytes in a committed file.§4.2 — connection type compatibility (#18)
The last place a composition could be internally inconsistent and still validate. Both ends resolved; neither was asked whether it fit.
typeis compared for equality, with no widening in either direction. AnINTEGERoutput feeding aSTRINGinput looks harmless — everything is a string at the container boundary — but which string is a per-language formatting decision, and a contract that permitted the wire would be promising a value it cannot describe.semanticTypemust agree wherever the consuming input names one. A consumer declaringnullaccepts any producer; a consumer declaring a tag requires the same tag, including rejecting anullproducer, because an unconstrained producer does not satisfy a constrained consumer. That is also the answer to what the tag is for giventypeexists:typeis the primitive shape,semanticTypeis the backing service, and a Postgres connection string and a MySQL one are bothSTRING.Two codes rather than one, so a diagnostic names the axis that failed —
ERR_INCOMPATIBLE_TYPEandERR_INCOMPATIBLE_SEMANTIC_TYPE, both anchored at the connection'sfromOutput. Both ends always carry aschemawith a requiredtype, so #18's fourth question — an unconstrained producer — has no case to except.format,enum,pattern,defaultandisSensitivetake no part in the decision, and §4.2 records those silences as gaps rather than as permissions.§7 — the remaining graph-rule codes (#30)
Each platform code is answered, and the two that need no row say so rather than going unmentioned.
UNDECLARED_INPUT_KEYERR_UNKNOWN_INPUT,semantic. A plain omission — §4.2 resolved the producer end of a wire and left the consumer end.UNWIRED_REQUIRED_INPUTERR_UNWIRED_REQUIRED_INPUT,semantic. ACONNECTIONinput never reaches the install form, so a graph that leaves one unwired has no later chance to supply it.COMPONENT_NOT_PUBLISHEDERR_COMPONENT_NOT_PUBLISHED,capability, inUNCOVEREDwith a reason. §4.1 says why it reads differently fromERR_UNKNOWN_COMPONENT: a component that exists and is not yet released is a wait rather than a typo.NO_COMPONENTSspec.componentsis now REQUIRED withminProperties: 1; §1 already made a blueprint a composition of one or more components while the schema acceptedspec: {}. No new code —ERR_MISSING_FIELD/ERR_INVALID_VALUE.DUPLICATE_WIRE_TARGETconnectionsis a mapping keyed by the consumer input, so a second wire to one input is a repeated key and alreadyERR_DUPLICATE_KEYin theparserphase. Impossible by construction rather than unenforced.INCOMPATIBLE_SEMANTIC_TYPEThree findings worth a reviewer's attention
1.
ERR_UNKNOWN_INPUTcaught a live bug in the corpus.blueprint/v1/semantic/010-well-formed-item— the fixture whose own comment calls it "the regression pin" — wiredDATABASE_URLinto a component that declared no such input, and had been passing as well-formed since it was written. That is precisely the failure #30 says the missing rule permitted, found by the rule itself. The fixture is corrected here, and its comment now enumerates the consumer-end rules it pins.2. One rule was widened slightly beyond what the issues asked for.
checkEndpointReferencenow resolves a null reference to the elected primary and then applies the same rules to it, rather than returning early. §5.2 makes the primary "what null selects", so a rule about the endpoint a reference names reaches it equally. The consequence is thatERR_ENDPOINT_NOT_PUBLICcan now fire on an elected primary where it previously could not. No existing fixture changed behaviour, but this is a deliberate widening rather than a mechanical one and should be reviewed as such.3. A sixth commit corrects §10. "Every section above marked TODO…" became false once #17 and #18 closed the last two. It is replaced with the debt that actually remains — schema
descriptionfields speaking platform vocabulary (snapshot compute, unpublished Compute Profile slugs). Scopeddocs(component)so it cuts no release of its own.One scoping note:
feat(blueprint): decide connection type compatibilityalso edits component §6.2'sTODOinto a pointer. That is deliberate — the normative rule lives in blueprint §4.2, and a component document sees only one end of a connection — and component still gets a release from the threefeat(component)commits, so the prose ships either way.Compatibility
Six narrowings across the two families: the endpoint-name grammar, the two env-var key rules, the two connection-compatibility axes, the four blueprint graph rules, and a non-empty
spec.components. The #28 changes run the other way and are strictly loosening — every document that validates today still validates, and aPUBLIC-TCP-onlySERVICEwithoutreadinessis now accepted where the current bundle rejects it, which is the correct direction inside a major per §3.No new
v<N>directory, and the reason is that v1 has never been published.git tag -lis empty and.github/release-please/manifest.jsonreads0.0.0for all three families. §3's guarantee — "a document that validated againstv1.0.0MUST validate against every laterv1.x.y" — has no releasedv1.0.0to run from, so there is no document anywhere that was validated against a published v1 and would now fail. A migration note would have nothing to migrate from.That argument expires when #1, #2 and #3 merge. #24 sets the same deadline for its divergence 3, and #28/#29 both name it explicitly. If the release PRs land first, this becomes a
v2change.Corpus impact — verified before each narrowing
api,console,db,internal,metrics,primary,rpc,web— all inside the new 20-character alphanumeric grammar. The only name outside it isweb.api, which is the fixture that exists to be rejected.spec.components. The two that do not areparser-phase cases that fail beforestructuralis entered.target.envVarKey, or an incompatible connection — exceptsemantic-010-well-formed-item, discussed above.Checklist
task checkpasses locallyschemas/dist/regenerated withtask bundleand committed (never edited by hand)clausespec.md— schemadescriptions are explanatory, not normativegit commit -s)12 new fixtures, 88 → 100 cases, 0 skipped. Every new code is exercised except
ERR_COMPONENT_NOT_PUBLISHED, which iscapabilityand sits inUNCOVEREDwith a reason. All four component examples and all three blueprint examples validate.🤖 Generated with Claude Code