A provision names the engine, because a consumer is coupled to one

Provisions were named after roles: provides "database", requires
"database". Nothing distinguished engines, so a module written against
PostgreSQL could be matched to a provider of SQL Server, resolve as
satisfied, deploy, and fail on its first query — with nothing
connecting that error back to a match made elsewhere by something that
believed it had done its job.

The failure is in the direction that hides. Refusing on ambiguity
exists precisely so this does not happen, and the generic name walked
around it: with one provider of each name nothing is ambiguous, so
nothing is asked.

How it got in: every resolver test had exactly one provider per name,
so no mismatch was expressible and none was caught. The fixtures agreed
with the design — the same fault as the imagined test output in
04-ISSUES/005, at the level of a name.

Refused rather than documented, because the old naming *was* the
documented convention. Providing database/db/sql/sql-database is now a
parse error naming what to write instead.

The rule is about coupling, not specificity everywhere: route and
resolver stay role-named, because a consumer genuinely cannot tell
which proxy answered. novox/hq ADR 0027.
This commit is contained in:
2026-08-31 17:12:46 +02:00
parent ab1dd34d12
commit ee84b624b1
8 changed files with 123 additions and 58 deletions
+10 -1
View File
@@ -68,7 +68,7 @@ func (c Claim) At() string {
// making every manifest say so would bury the few that are not:
//
// "provides": ["shell"]
// "provides": [{"name": "database", "scope": "mesh"}]
// "provides": [{"name": "postgres-database", "scope": "mesh"}]
type Offer struct {
Name string `json:"name"`
// Scope defaults to the node, which is where most things must be to be usable.
@@ -442,6 +442,15 @@ func ParseManifest(raw []byte) (Manifest, error) {
if !name.MatchString(p) {
problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", p))
}
if instead, generic := engineGeneric[p]; generic {
// A consumer is written against an engine, not a role (novox/hq ADR 0027). Providing
// the role means a requirement for it matches any engine, resolves as satisfied, and
// fails on the first query — with nothing pointing back at the match.
problems = append(problems, fmt.Sprintf(
"%s provides %q, which hides which engine it is: a requirement for %q would match "+
"any of them and fail on the first query. Name the engine — %s",
m.Module, p, p, instead))
}
if s := offer.At(); s != ScopeNode && s != ScopeMesh {
// Site scope is meaningful for a claim — one DHCP server per segment — and is not
// yet meaningful for a provision, because nothing knows how to reach "the one at my