I added a generator step to keep "N operations over M paths, grouped into K
services" fresh in every SDK README. The right fix was to delete the sentence. A
developer opening a client does not need its inventory; the number tells them
nothing they can act on, and keeping it true costs a generator step, a marker
convention and a parse of the document on every build.
Stamping a number that should not exist is a more elaborate way of being wrong.
Seven SDK READMEs opened by telling a developer the client covers "2,479
operations over 1,814 paths, grouped into 192 services". The document serves
2,259 over 1,620 in 117 — every number wrong, in the first paragraph of the page
each language's users land on. Nobody mistyped them; they were true once and the
document moved, which is what a hand-written count does.
So the numbers are no longer written. A `<!--counts-->` span in the README is
rewritten from the spec the client was actually generated from, on every
generate — the sentence around it stays the repo's own voice, and a README with
no markers is left untouched, so adopting it is per-repo rather than a surprise
rewrite.
Measured by PARSING the document, not by matching it. The first version counted
`^ operationId:` and `^ /`, which is true of cloud's YAML and of nothing
else — this driver hands the generator a JSON copy, so every count came out zero
and stamped "0 operations over 0 paths" into the README it was added to fix. It
also disagreed with itself about services: the regex said 148, the parse says
117, and only one of those is the number of distinct tags on an operation.
dart emitted NOTHING for four releases. openapi-generator 7.14.0, the version
sdks.yaml pins for every language, dies in AbstractDartCodegen.fromProperty with
`JsonSchema cannot be cast to ComposedSchema` on our polymorphic request bodies —
so every run produced an exception and no files, and the drift check reported no
change because there was nothing to compare.
7.19.0 emits it cleanly: measured, 117 api files, no exception. The choice was
to move the fleet or to pin the one language, and moving the fleet regenerates
twelve working clients to repair one — none of which have CI to catch a
regression. So the pin sits where the defect is, and says when to remove it.
One generator stays the rule; `generator_version` is the exception, and an
exception that names its own expiry is cheaper than the churn it avoids.
publish.py already knew it: api.hanzo.ai/v1/openapi.json, cloud's own emission
from the process that is actually up. generate.py asked git.hanzo.ai at a locked
ref instead, so two files in one repo named two sources for one document, and
the one every SDK generates through needed HANZO_GIT_TOKEN to read a private
repo.
hanzoai/ci's lane reads the served document and hands it to the generator as
$SPEC, so in CI this path never ran. It ran for people, and it ended at a
credential a contributor could not be given.
.spec-lock still records which document a tree was generated from. A digest that
differs now says cloud has shipped since, rather than refusing — regenerating
onto what production serves is why the function runs.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/cloud is the re-rooted OSS core: a twelve-commit line sharing no
ancestor with the product and carrying no openapi.yaml at all. It is PUBLIC, so
the workflow's checkout succeeded and publish.py failed one step later with "no
remote of .cloud carries openapi.yaml on `main`" — which the step then threw
away, printing instead its own fixed sentence, that hanzo.yaml had been
hand-edited. It had not: re-derived at its own pinned ref, the committed
artifact is byte-for-byte what that ref projects to. Every push and every hourly
run has reported a drift that never existed.
So the address is the value it always should have been. SPEC_REPO names
hanzo-inc/cloud — where every ref this repo and every SDK has pinned resolves,
and hashes to the digest its lock already carries — and .spec-lock, info.x-spec
and CAPABILITIES.md's header follow it rather than restating it. The receipt was
repointed on its own once and the artifact, the receipt and the test then named
three different repositories; test_the_artifact_names_the_release_it_came_from
now asserts the three AGREE instead of repeating the string a fourth time.
Both canned ::error:: wrappers are gone. A step that replaces a precise
diagnosis with a fixed sentence is worse than a step with no message: publish.py
already says what is wrong and which command fixes it, and `set -e` fails the
step.
hanzo-inc/cloud is private, and the per-job token Hanzo Git mints reads the
run's OWN repository and anything public — nothing else. Measured on
hanzoai/python-sdk run 91449, which named the right repo and still took
`curl: (22) ... 404`. The checkout therefore takes HANZO_GIT_TOKEN, the one name
in this fleet for a credential that reads git.hanzo.ai, and the job says which
credential is missing before actions/checkout can report a private repo as
"repository not found". generate.py's own fetch takes the same name, so one read
needs one credential.
Cloud declares its own `bearer` scheme now, and the projection's addition
stopped adding and started corrupting: setdefault no longer fired while the
document-level `security` it wrote still did, so the published document required
`bearerAuth` and defined nothing by that name. Generated typescript, measured:
2498 bearer call sites from cloud's document, 0 from the projection, leaving 191
files importing a credential helper none of them called. The rule is gone —
cloud says both halves — and test_a_client_can_authenticate refuses a document
that requires a scheme it does not define, whoever supplies it.
Co-authored-by: Hanzo Dev <[email protected]>
Two measured numbers in this repo's own prose were wrong about the fold they
describe.
go-sdk's second driver was 196 lines, never 250 — its maximum across nine
commits, and its size at the commit the row replaced. The figure stood in
generate.py twice and in sdks.yaml once.
And generate.py's header still offered `hanzo.yaml` as a third source for a hand
run: "with neither, this checkout's own hanzo.yaml is the document". `document()`
exits instead, the --spec help two hundred lines down already said so, and
sdks.yaml's opening paragraph was corrected to say so. The header now reads the
same as both — the emission, --skip-validate-spec for every language, and no
fallback — so "which document" has one answer in one file.
sdks.yaml's "five clients, one sha256" counted five rows when it carries seven;
ruby and php are pinned at a different ref. One sha256 per ref is the invariant
and does not rot as rows land.
`take` promised the generator owned a DIRECTORY, and rmtree-then-copy is how it
kept the promise. Four languages could say that. Go could not: `package hanzoai`
sits at the module root beside go.mod, LICENSE, examples/ and .git, so the only
take it could name deleted the repository — which is why go carried a second
driver, 250 lines of bash re-deriving the jar, the locked ref, the digest check,
the YAML-to-JSON conversion and the drift check.
`owned()` states the rule as a SET of files instead, recorded in each client's
`.generated`: a file the manifest names and the fresh emission does not is
removed, a file nothing names is the repo's own whatever directory it sits in.
Strictly stronger, no edge, and the four wholly-owned takes behave exactly as
they did. The go, ruby and php rows are already committed against it.
`shape()` runs a language's own formatter over the staged tree, because the
generator leaves 2655 of its 2656 Go files unformatted and an unformatted Go
file reformats itself in every contributor's editor. `templates:` lets a row
name an override under templates/<lang>/ rather than a second call site.
`hanzo.yaml` is derived from hanzoai/cloud's emitted openapi.yaml, so a client
that fell back to it was the projection of a projection: one step stale whenever
publish.py had not run since the last release, stale silently, and with no way
downstream to tell which of the two documents it had read. Two sources remain
and both name a cloud release — the document by value, or the ref this client's
own .spec-lock pins.
The reusable is also pinned at the path it is published at. `.github/workflows`
does not exist in hanzoai/ci at v1; the forge resolves the reusable anyway, so
this named a file that is not there and got away with it.
`generate.py`'s fetch — the path every SDK takes when hanzoai/ci does not hand it
the document by value — asked api.github.com for hanzoai/cloud's openapi.yaml.
That is a mirror. It trails cloud by thousands of commits and carries no
openapi.yaml at its root at all, so the request could only 404, and the code read
that 404 as a private repo refusing an anonymous caller: it printed "is private
and no SPEC_TOKEN / GH_TOKEN / GITHUB_TOKEN is set", which sent every reader
looking for a credential to fix a host that has no file to serve. Measured with a
valid GitHub token on the exact address: 404, and the ref v1.801.495 is 404 there
too. The same request against git.hanzo.ai returns 4,073,309 bytes.
So one host and one credential. git.hanzo.ai is where hanzoai/cloud lives,
FORGE_TOKEN is what reads it, and the three-name credential ladder goes with the
mirror it was reaching for — a fallback chain across hosts that hold different
documents cannot be a fallback, only a coin flip about which document you get.
The forge serves its API at /v1/, not /api/v1/, and /api/v1 answers with a 404
indistinguishable from a rejected credential. Same route hanzoai/ci's client lane
and the CLI's Makefile take, because there is one document at one address.
`main` maps the languages over a thread pool, so a cold cache calls `jar()`
once per language at the same moment. Every one of them opened the SAME
`<jar>.part`, wrote the same 30 MB into it, and then raced on the rename: the
first `os.replace` moved the file away and the second died with
FileNotFoundError on a path it had just finished writing.
Measured on `python3 generate.py java kotlin` with an empty cache — java
emitted 2274 files and kotlin never ran at all. The traceback names
`os.replace`, so it reads like a corrupt cache rather than two writers.
It needs an empty cache to happen, which is why it is not a known annoyance:
every re-run on a warm cache skips the download entirely, and the failure lands
on CI and on somebody's first clone, where it costs the most and where the
person hitting it has the least context.
`tempfile.mkstemp` in the cache directory gives each caller its own name;
`os.replace` is atomic on POSIX and on Windows, so the losers overwrite an
identical file with an identical file. The partial file is removed if the
download raises, so a failed fetch cannot leave a 30 MB fragment behind that a
later run has to reason about. Verified with eight concurrent callers against a
local URL: no errors, bytes intact, no `.part` left in the cache.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo.yaml stops being where the API is DECIDED and becomes where hanzoai/cloud's
emission is PUBLISHED. `publish.py` derives it from `cloud@<ref>:openapi.yaml`;
`publish.py --check` regenerates and diffs. An operation no code declares is now
structurally impossible to publish, in this repo and in everything downstream.
MEASURED at hanzoai/[email protected], both documents loaded and keyed by
(METHOD, path): the hand-merged master carried 2093 operations, cloud's emission
2333, shared 1908. So the master described 185 operations nothing serves under
that name and MISSED 425 that are served — 8% invented, 18% blind — while being
the input to seven SDKs, the doc site, the MCP catalogue and the agent-skills
plane, which has no liveness filter at all.
THE 185, each probed with its OWN method and a nonsense-sibling CONTROL:
36 ABSENT (real 404, control 404), 44 unfalsifiable behind a prefix auth gate,
66 absorbed by a {wildcardN} relay door, 39 SERVED and undescribed. A 404 on both
sides IS a refutation and is the only way absence can ever be shown; the control
exists to stop a 401/403/200 being read as existence. The 39 are hanzoai/cloud's
to fix and every one is itemised in LLM.md.
DELETED: 52 `<svc>/openapi.yaml`, shared/, cloud/, generated/, merge.py, sync.py,
audit.py, test_audit.py, test_placement.py — and 14 Swagger 2.0 documents for
`api.hanzo.io`, which is a parked domain: `/account`, `/store` and the control
`/account-zzq9` all return the same registrar lander, and HTTPS has no
certificate for the name.
THE PROJECTION IS SIX RULES AND NOTHING ELSE — no operation added, none removed
that is served, no prose written. It exists because openapi-generator 7.14.0
refuses cloud's emission with 1012 errors (one per route published with an
address and no `responses`) and writes ZERO files in every language; against
hanzo.yaml it reports 0 errors and go/python/typescript-axios/kotlin all emit.
Every rule is a candidate to move upstream into cloud's emitter, and rule 5 alone
is 90% of the file.
BLOCKING, and new with this release rather than with the projection: three o11y
schemas cannot be projected into Go. `o11y.GettableAgentCheckIn` declares
`integration_config` AND `integrationConfig`, `removed_at` AND `removedAt`;
`o11y.O11yPodOnboarding` has `hasClusterName` beside `clusterName`;
`o11y.PostableProfile` the same. `go build ./...` fails on those three and on
NOTHING else — renaming the keys in a scratch copy and regenerating gives exit 0.
This repo must not rename a field, because a field name is the wire, so
test_publish.py pins them as a ceiling instead.
BREAKING: operationIds lose the `<svc>_` prefix merge.py applied — it existed to
keep 52 authored specs from colliding and there is exactly ONE collision in 2284
operations now. Two flows.yaml journeys moved with the hand-authored ids that
vanished: `hello` is get_v1_keys (403 with no key AND with a bogus one, control
404), `tools` is get_v1_tools.
capabilities.yaml keeps one job — which DOMAIN a capability is shown under — and
is gated both ways: a served capability it does not group fails the publish, and
a name it groups that the document does not carry fails too.
generate.py's fallback to this checkout's hanzo.yaml returns, superseding
3953d66's removal of it. That removal was right about authority and wrong about
consequence: pointed at cloud's raw emission the four clients generate nothing.
The file it refused to fall back to no longer exists — this one is derived.
publish.py --check hanzo.yaml is the projection of [email protected]
pytest (16 tests) passed
openapi-generator validate 0 errors (was 1012 against the raw emission)
go build ./... on the client exit 0, with the three o11y schemas patched
Co-authored-by: Hanzo Dev <[email protected]>
`hanzo.yaml` is a SECOND AUTHORITY ON WHAT EXISTS and that is what took it out
of the client path. Measured at hanzoai/[email protected]: master 2093
operations, cloud's emitted document 2333, shared 1908 — and 185 master
operations cloud's document does not have, every one of which reached python,
typescript, java and kotlin as a callable method.
Triaged those 185 at api.hanzo.ai with each operation's OWN method and a
nonsense-sibling CONTROL under the same prefix: 164 unfalsifiable (the control
answered identically — /v1/bot and /v1/dns 403 everything, /v1/vector and
/v1/search gate before routing, /v1/s3 and /v1/kv 404 both), 0 decidably absent,
19 decidably present-and-undescribed. A door is not a list. The master asserted
164 endpoints on evidence that cannot tell them from an invented path.
sdks.yaml loses `spec:`; there is no document in this repo a generator falls
back to. generate.py resolves the document from the CLIENT's `.spec-lock` — the
receipt hanzoai/ci's client: lane writes when a cloud release dispatches — and
refuses if the bytes behind that pinned ref hash to anything else. `--spec`
stays: the same document by value, which is what the lane passes.
The 19 live-and-undescribed are hanzoai/cloud's to fix and are recorded in
LLM.md. Nine are /v1/ai/*, the apps/ai seam projecting from a committed
plugin/ai/openapi.json subset rather than the mounted plugin's registry — a
second authority inside cloud, the same disease one level down.
merge.py, the authored specs and hanzo.yaml stay for skills.py, audit.py,
console and cloud-pulse. Nothing that generates a client reads them.
Co-authored-by: Hanzo Dev <[email protected]>
--spec lets a caller name the document to project. hanzoai/cloud's release hands
each client repo openapi.yaml AT THE SHA IT DEPLOYED, and a projection generated
from anything else describes a release nobody shipped — so the four call sites
that route through this driver (python, typescript, java, kotlin) now pass it
through when their lane supplies one.
Defaulting to this checkout's own hanzo.yaml keeps every existing call site
working unchanged, and is still what a maintainer regenerating by hand gets.
This is also the beginning of the end of hanzo.yaml as an SDK input. It is
hand-merged, 1742 paths, fed by nothing and larger than the 1058-path document
cloud emits from its own code. The clients switch to cloud's now; what remains
here is request-body and query SHAPE for handlers that are not yet zip typed
ops, which is a shrinking dependency and not a permanent one.
Co-authored-by: Hanzo Dev <[email protected]>
658493a added -DmaxYamlCodePoints to lift snakeyaml's 3145728 code-point
ceiling. I verified it on generator 7.24.0, which is what the C++ lane uses, and
did not verify it on 7.14.0, which is what THIS FILE pins for every other
language. It does not work there: 7.14.0 ships an older swagger-parser that
ignores the property. Java regeneration failed after 9m27s with the same
misleading "Issues with the OpenAPI input" the flag was supposed to prevent.
So the fleet was blocked on regenerating and the fix was cosmetic. The real fix
is not to raise the ceiling but to stop going near it: swagger-parser only hands
YAML to snakeyaml, and the JSON path is Jackson, which has no such cap. -i takes
either format.
Measured rather than assumed, because "it happens to be under the limit" is not
a fix — the JSON of this document is 3,144,674 code points, 1054 UNDER the
ceiling, which would have made a coincidence look like a solution. Padded to
3,684,674 (539 KB OVER) it still validates clean on 7.14.0. The JSON path does
not have the ceiling at all.
as_json() converts once, in main(), for every language. The temp file is
deliberately not written back to the repo: there is one document, and it is
hanzo.yaml.
Side effect worth recording: java generation went from failing at 9m27s to
succeeding in 12s. The YAML parse was not merely failing, it was thrashing.
hanzo-cpp/sdk's own scripts/generate.sh now does the same one thing rather than
carrying the flag, so there is one mechanism and not two. Verified by
--check: the generated C++ tree is byte-identical across the swap.
Co-authored-by: Hanzo Dev <[email protected]>
Two changes, both about the same thing: what a language needs before its
projection compiles, stated once where the invocation is.
generate.py — swagger-parser hands hanzo.yaml to snakeyaml, which refuses
anything over 3 * 1024 * 1024 = 3145728 code points. The document passed that
mark at 1a2ca06 (3,654,449) and the failure does not say so: the parser logs
SnakeException, silently falls through to the SWAGGER 2.0 compat reader, and
dies with "Issues with the OpenAPI input" — which reads like a malformed spec.
It is not. The document validates at 0 errors and 0 warnings (240 "unused
model" recommendations). -DmaxYamlCodePoints is a parser default and nothing
else, and it stops EVERY language at once, so it belongs here rather than being
rediscovered separately in seven repos.
sdks.yaml — cpp joins go and rust as deliberately absent, for the reason
already stated for rust plus one of its own. It needs four cpp-restsdk mustache
overrides before the output compiles at all, and a template is a FILE that has
to sit beside the invocation; and it needs generator 7.24.0, not the 7.14.0
pinned here. On 7.14.0 the document leaves 10 of 2370 translation units
uncompilable through two defects in the generator's Java type resolution that
no template reaches — an aliased array's element type dropped under
additionalProperties (9 TUs), and an integer-backed enum declared over
utility::string_t and defined over int32_t (1 TU). A row here says one version
for everyone, so raising the pin fleet-wide would rewrite every other client's
committed output: a separate deliberate decision, not a side effect of adding
C++. The floor is stated once, in hanzo-cpp/sdk's own scripts/generate.sh.
Measured on aarch64 Ubuntu 24.04, g++ 13.3, generator 7.24.0, spec 1a2ca06:
2376 translation units, 0 fail.
Co-authored-by: Hanzo Dev <[email protected]>
Four causes left after the Java pass, all found by building the tree.
openapi-generator emitted 43 Kotlin compile errors from hanzo.yaml, which
is why no JVM client had ever been built from it.
merge.py — drop TRACE (2453 -> 2427 operations)
All 26 were cloud's wildcard passthrough routes projecting every method
their router happens to match: a fact about the router, not about a
service, and not a method any SDK should hand a caller (every edge
disables it). It is also the one method the generator cannot emit for
Kotlin — it writes `RequestMethod.TRACE` while the `RequestMethod` enum
it writes beside it stops at PUT, so the client references a constant
the generator did not generate. 15 errors across 11 API classes. Java
does not hit it: its ApiClient takes the method as a String. Dropping it
here is the same kind of call as the operationId suffixing beside it —
the document is shaped so it can be generated from.
generate.py + sdks.yaml — a `flags:` knob, beside the `global:` map the
java pass added, used twice for kotlin
Generator CLI options that have no --additional-properties form.
`sort=name` is an ordinary query value, but the enum entry named after
it collides with the `name` every Kotlin enum already has; and POST
/v1/event's oneOf flattens cloud_Event.distinctId together with
cloud_insightsBody.distinct_id into one duplicated Kotlin property. Both
are renames on the client side only — @SerializedName still carries the
wire name.
The kotlin row also takes `global: skipFormModel: "false"`, for exactly
the reason the java row already does: edge_deployFunction's multipart
model is skipped and still imported.
billing CapReason, commerce Order.mode — `''` was a member of both enums
An empty string is not a name, and the generator has none to give it: it
emitted `_("")`, which is reserved in Kotlin, and for the inline enum an
entry with no name at all. The constraint goes and the type stays. Every
value is still documented, `''` is still a legal value of a string, and
nothing changes about what the wire carries.
Verified: kotlin regenerates clean and hanzo-kotlin/sdk builds — 2306
files, the six example flows, and 8 tests green in 5m15s.
Co-authored-by: Hanzo Dev <[email protected]>
The java row could not produce a client that compiles. Four separate causes,
all found by building the tree rather than reading it:
- library `native` is unusable at this spec size. Its oneof_model.mustache
iterates the raw `oneOf` type-name set and emits `{{{.}}}.class`, which stops
being legal Java the moment a variant is an array — `List<CloudEvent>.class`.
Three schemas have an inline array in a oneOf, and that is 532 javac errors.
Every other Java library iterates `composedSchemas.oneOf` and guards on
`isArray`; okhttp-gson, jersey3 and apache-httpclient all compile. Still
broken in 7.24.0, so it is not waiting on a version bump. okhttp-gson also
puts the two JVM rows on one HTTP+JSON stack — kotlin is already okhttp4+gson.
- `skipFormModel` defaults to true, so the generator skips a multipart form
model but still imports it from the API class that used it. edge_deployFunction
offers octet-stream AND multipart, so FunctionsApi imported a class nobody
emitted. generate.py now merges a per-row `global:` map into
--global-property; only the java row sets it, because changing it for
everyone would churn every other client's committed output.
- affiliates' `isAffiliate: {type: boolean, const: true|false}` projects as a
single-member enum whose member is a String assigned to a Boolean field. The
invariant moves into the description; the wire value is unchanged.
- vector's PointId is `oneOf: [integer, string+format:uuid]`, and inside a
oneOf that format maps to java.util.UUID, on which the generator then calls
UUID.validateJsonElement() — a method only its own models have. `format` is
an annotation, not a constraint, so dropping it changes no wire value and the
description still says what the field is.
The two spec edits are at the per-service source and re-woven, so merge.py does
not undo them: 2 insertions, 3 deletions in hanzo.yaml and nothing else.
Co-authored-by: Hanzo Dev <[email protected]>
Importing the Python client once writes __pycache__ into it, and digest() walked
those, so --check reported 2142 phantom deletions against a tree that was in fact
byte-identical to a fresh generation. A drift check that cries wolf is one nobody
runs, which costs more than the check was worth.
All three clients now verify clean.
Co-authored-by: Hanzo Dev <[email protected]>
merge.py made the spec; nothing made the clients. Each SDK repo grew its own
generator call site, so a client could drift on its own and nothing measured it.
generate.py is the logic, sdks.yaml is the data (per-language generator, take
paths, properties), openapitools.json pins 7.14.0. --check diffs a committed
client against a fresh generation, which is what makes 'no client is written by
hand' a fact instead of a convention.
Co-authored-by: Hanzo Dev <[email protected]>