Skip to content

feat: close the endpoint, environment-variable, and graph-rule specification gaps - #31

Merged
justinmerrell merged 6 commits into
mainfrom
spec/close-endpoint-and-graph-rule-issues
Aug 11, 2026
Merged

feat: close the endpoint, environment-variable, and graph-rule specification gaps#31
justinmerrell merged 6 commits into
mainfrom
spec/close-endpoint-and-graph-rule-issues

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

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 PUBLIC protocol restriction and the endpoint-name grammar); three close TODO markers and registry gaps left open by #13 and #9. Component spec.md now carries no TODO sections 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 — PUBLIC on every protocol (#28)

§5.2 restricted a PUBLIC endpoint to the HTTP family on two arguments, and the edge-port address form answers both. An HTTP-family PUBLIC endpoint publishes a URL; a TCP/UDP one publishes a host:port address. Exposing a database, a broker or a game server is a capability that ships today and that a conformant document could not express.

Rule Before After
PUBLIC protocol HTTP/HTTPS/WS/GRPC only Any of the six
readiness REQUIRED SERVICE with any PUBLIC endpoint SERVICE with a PUBLIC HTTP-family endpoint
Probe / platformDefault endpoint Any declared endpoint Must resolve to an HTTP-family endpoint — new ERR_ENDPOINT_NOT_HTTP

The readiness narrowing is not a new argument. §5.4 already exempts PRIVATE because "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-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 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 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.

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.

§5.3 — environment variables (#17)

§5.3 was a bare TODO while the schema already carried the whole shape.

  • 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 and ERR_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.
  • 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 to a marked literal, since isSensitive governs 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.

type is compared for equality, with no widening in either direction. An INTEGER output feeding a STRING input 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.

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 given type exists: 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 anchored at the connection's fromOutput. Both ends always carry a schema with a required type, so #18's fourth question — an unconstrained producer — has no 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.

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

Platform code Disposition
UNDECLARED_INPUT_KEY ERR_UNKNOWN_INPUT, semantic. A plain omission — §4.2 resolved the producer end of a wire and left the consumer end.
UNWIRED_REQUIRED_INPUT ERR_UNWIRED_REQUIRED_INPUT, semantic. A CONNECTION input never reaches the install form, so a graph that leaves one unwired has no later chance to supply it.
COMPONENT_NOT_PUBLISHED ERR_COMPONENT_NOT_PUBLISHED, capability, in UNCOVERED with a reason. §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.
NO_COMPONENTS Answered structurally. spec.components is now REQUIRED with minProperties: 1; §1 already made a blueprint a composition of one or more components while the schema accepted spec: {}. No new code — ERR_MISSING_FIELD / ERR_INVALID_VALUE.
DUPLICATE_WIRE_TARGET 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. Impossible by construction rather than unenforced.
INCOMPATIBLE_SEMANTIC_TYPE Answered by #18 above.

Three findings worth a reviewer's attention

1. ERR_UNKNOWN_INPUT caught a live bug in the corpus. blueprint/v1/semantic/010-well-formed-item — the fixture whose own comment calls it "the regression pin" — wired DATABASE_URL into 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. checkEndpointReference now 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 that ERR_ENDPOINT_NOT_PUBLIC can 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 description fields speaking platform vocabulary (snapshot compute, unpublished Compute Profile slugs). Scoped docs(component) so it cuts no release of its own.

One scoping note: feat(blueprint): decide connection type compatibility also edits component §6.2's TODO into 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 three feat(component) commits, so the prose ships either way.

Compatibility

  • Breaking — a previously valid document now fails.

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 a PUBLIC-TCP-only SERVICE without readiness is 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 -l is empty and .github/release-please/manifest.json reads 0.0.0 for all three families. §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 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 v2 change.

Corpus impact — verified before each narrowing

  • Endpoint names in use are api, console, db, internal, metrics, primary, rpc, web — all inside the new 20-character alphanumeric grammar. The only name outside it is web.api, which is the fixture that exists to be rejected.
  • Every blueprint document declares spec.components. The two that do not are parser-phase cases that fail before structural is entered.
  • No document in the corpus paired a duplicate env-var key, a colliding target.envVarKey, or an incompatible connection — except semantic-010-well-formed-item, discussed above.

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)

12 new fixtures, 88 → 100 cases, 0 skipped. Every new code is exercised except ERR_COMPONENT_NOT_PUBLISHED, which is capability and sits in UNCOVERED with a reason. All four component examples and all three blueprint examples validate.

🤖 Generated with Claude Code

justinmerrell and others added 6 commits August 11, 2026 02:00
§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>
@justinmerrell
justinmerrell merged commit 3b7110c into main Aug 11, 2026
4 checks passed
@justinmerrell
justinmerrell deleted the spec/close-endpoint-and-graph-rule-issues branch August 11, 2026 11:07
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment