ubx sdk gen generates typed bindings in three languages (Go,
TypeScript, Python) from whatever Provider system
snapshot is currently pinned. Most of what it does is mechanical once a
schema is in hand — but where a generated type lands, which package,
which file, which import path a person actually writes — depends on a
naming derivation that has been the real root cause of three separate,
independently-discovered bugs. This page is that derivation, and why
each bug happened the way it did, so the next person doesn’t have to
rediscover it from three different Linear threads.
The base mechanical split
A wire type likeaws_db_instance or kubernetes_apps_deployment
follows a <provider>_<service>_<local...> shape. ServiceAndLocalName
splits it into (service, local) purely mechanically — string tokens,
no network call, no taxonomy lookup — because ubx sdk gen has to stay
100% local and offline. Checked exhaustively against AWS’s real,
full 1,682-type schema: zero (service, local) collisions. The split is
genuinely unambiguous.
It is not, however, a faithful reproduction of any provider’s real
service taxonomy, and that gap is named in the function’s own doc
comment rather than hidden. Two real shapes it can’t get right by
construction:
- Bare two-token types (
aws_vpc,aws_instance,aws_route) have no third token to serve as a local name — the local name falls back to the service token itself. - No service token at all — roughly 130 of AWS’s 1,682 types, mostly
its EC2/VPC “core” family, predate the
aws_<service>_*convention Terraform’s AWS provider later adopted. The mechanical split fragments this family into many small, oddly-named single-type packages (keyforaws_key_pairalone) rather than one curatedec2package a hand-maintained taxonomy would produce.
When a source knows better: RealNamespace
Some schema sources carry their own authoritative service identity for a
type — CloudFormation’s own namespace field, Smithy’s own
endpointPrefix trait. When that’s available, ServiceAndLocalNameForType
uses it directly as the service name instead of guessing, and only falls
back to the mechanical split when a source has nothing to offer (true
for Azure, Google, Kubernetes, GitHub, and Datadog, whose wire types
already build their service identity in from real names — confirmed
against over a thousand real wire types each, zero mismatches — so those
five providers are byte-for-byte unaffected by any of this).
Replacing the service half doesn’t mean the local name comes along for
free — a naive “strip N leading tokens” would break the common case
where the real namespace and the wire type’s own remainder share no
prefix at all (aws_instance’s remainder is instance, which shares
nothing with its real namespace ec2 — the historical name simply never
encoded which service instance belongs to). So the local name is only
adjusted when the wire type’s own tokens genuinely accumulate to an
exact match against the real namespace — aws_api_gateway_deployment
against real namespace apigateway strips api+gateway because those
two tokens literally spell it, leaving deployment; aws_instance
against ec2 matches nothing, so the whole remainder is kept, just
filed under the correct namespace instead of the wrong mechanical guess.
The data namespace
A resource and a same-named data source (hashicorp/aws’s own
aws_instance is genuinely both) belong to the same real service either
way — so the service/local derivation above is identical for both.
What keeps them from landing at the same generated path is one more,
separate input: whether the type is a data source at all, which sets a
namespace of "data" (empty for an ordinary resource). This is the
data/ segment in a generated path or a docs URL — one axis, decided in
exactly one place, rather than every per-language template carrying its
own copy of the same check.
Collision handling
Two more real edge cases show up once generation runs at full-provider scale, neither about naming ambiguity so much as what a language’s own tooling refuses to accept. A derived service name that happens to be a Go keyword (default — several aws_default_vpc-family types) or
special to the go tool itself (main) gets a trailing underscore, the
same convention already used for a wire name colliding with a Python
keyword. And a real, identical nested schema shape appearing at every
level of a provider’s own statically-unrolled recursion (AWS’s
aws_wafv2_web_acl_rule, whose “statement inside statement” tree once
rendered past 10MB for one type alone) gets deduplicated by structural
signature rather than re-inlined at every depth — both the generated Go
struct declarations and, more importantly, the runtime field-map literal
the generated code actually reads.
Three bugs, one shared root
UBI-98’s own known imprecision. The base mechanical split’s taxonomy gap above isn’t a bug so much as a documented, accepted limit — about 60 of 408 real HashiCorp-named AWS resources (~15%) land under the first word of a multi-word real service name (API Gateway, Amazon MQ, Elastic Beanstalk, each truncated to its own first token) beforeRealNamespace existed to correct it.
The GCP/Azure doubling correctors. A separate mechanism,
typename.Combine, trims a real overlap between a provider’s own API
name and a resource’s local name (google_dlp + dlp_job should
collapse to google_dlp_job, not stay google_dlp_dlp_job). Its first
version trimmed only once — google_dlp_dlp_job overlaps in the same
way twice in sequence, so a single pass left one residual doubled
token, and the same shape hit Azure’s own azure_kusto_kusto_cluster.
ubiquex-docs’s own generators grew compensating correctors
(gcp_corrected_key, azure_corrected_wire) to collapse these forms
for content already published before the upstream fix landed. Once
typename.Combine was fixed to trim to a fixed point instead of once,
these correctors became almost entirely dead code against a fresh dump —
but retiring them safely still needs a real redirect pass across the
published corpus’s existing mix of corrected and uncorrected paths, not
done yet.
The AWS namespace fallback — this session’s own arc. The most
recent and by far the largest: --dump-namespaces, the mechanism that
feeds RealNamespace into ServiceAndLocalNameForType, called the
mixed-source dispatch layer’s fast, single-source
path directly, with no fallback for a group whose members span more
than one real schema source. AWS is the only such group in this org, and
its path through --dump-namespaces had never actually been exercised
against a real mixed group before this session pinned AWS for the first
time. The result: real namespace lookup failed outright for the whole
provider, silently degraded to nothing under this codebase’s own
skip-don’t-fail discipline, and every single AWS resource fell all the
way back to the base mechanical split — the exact same class of
imprecision UBI-98 already named, just triggered for 921 of 1,715 real
AWS resource types (54%) instead of the ~15% UBI-98 originally
measured, because this time the fallback’s own safety net failed
completely rather than being selectively imprecise for multi-word
service names. Fixed by giving --dump-namespaces the identical
mixed-source fallback the rest of the dispatch layer already had.
Full detail, including the exact token-matching algorithm and every
named edge case: ubiquex’s own sdk/codegen/ir/ir.go
(ServiceAndLocalName, ServiceAndLocalNameForType) and
docs/sdk.md.
