Files
openapi/sync.py
T
zeekayandHanzo Dev 1f93702023 spec: publish one document — cloud's woven spec merged, and it wins
`cloud/openapi.yaml` was a hand-written contract for a product with five served
routes and 151 that answer `not found`. It is now a verbatim copy of hanzoai/cloud's
own woven document, pulled by `sync.py` from that repo's origin/main (34663c49).
Cloud regenerates that document from its router and fails its own build on any
diff, so it cannot describe a route the binary does not serve — the property no
spec here has — which is why `merge.py` merges it LAST and lets it WIN every
route an authored spec also claims (298), explicitly rather than by where
`cloud` falls in the alphabet.

The document that every SDK, doc site and tool generates from goes 1132 -> 1737
paths, 1519 -> 2478 operations, and 699 -> 1217 operations carrying a
description — prose lifted from the handlers' Go doc comments, not written
twice. `tags` and `x-tagGroups` now describe the tags OPERATIONS carry (263, of
which 213 described, cloud's being the owning package's synopsis) instead of the
55 spec directories, four of which any operation carried. The registry still
decides the movements: a tag's group is the domain of the service that
introduced it.

Three defects that reached generators are fixed where codegen identity already
lives. An operation with no `responses` (668, cloud's untyped routes) gets a
`default` saying exactly that, rather than a schema nobody knows. An operation
with no tag takes its service's name, rather than `DefaultApi`. And
`cloud_get_v1_pricing-policy` and `cloud_get_v1_pricing_policy` — two real
routes, distinct strings, one Go type once the generator strips punctuation —
are separated. openapi-generator 7.14.0: 0 errors, 0 warnings (was 35 errors,
68 warnings); go and typescript-axios both generate; the Go client compiles.

Six route collisions surfaced the moment the merge stopped resolving them by
alphabet, and every one was duplicate authoring. `/health` was declared by nine
specs and answers with the SPA at api.hanzo.ai, while `/healthz` answers JSON:
it is gateway's alone now. `observe`, `app` and `product` declared only other
services' routes — o11y's, projects', vector's and search-docs' — and are
collapsed. Every route they held is still in the document, from cloud.

`generated/hanzo.json` is deleted. Measuring hanzo.yaml against a document
hanzo.yaml now contains is tautology, and a second, older copy of one emission
is the drift this change exists to end.

`flows.yaml` is new: hello, chat, money, store, agent, tools — six journeys
naming the exact operationIds a client calls and what each step asserts, so
every SDK's examples are the SAME journeys. merge.py refuses to write the
document if a flow names an operation it does not have, and three flows record
a substitution where the obvious route is not the served one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 08:50:17 -07:00

83 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Pull hanzoai/cloud's woven document into this repo and re-merge.
python3 sync.py # resync, then run merge.py
python3 sync.py --check # fail if cloud/openapi.yaml is stale
python3 sync.py --cloud ~/work/hanzo/cloud
`cloud/openapi.yaml` is the ONE spec here that is not authored here. hanzoai/cloud
weaves it from its own router — every app projects itself, the weave composes the
projections, and a drift gate there regenerates from source and fails on any diff
— so it cannot describe a route the binary does not serve. That is the property
no hand-written spec has, and it is why `merge.py` lets this file win wherever it
and an authored spec describe the same route.
A copy is only true at the moment it is made, so making it is a command and not a
procedure: this reads `origin/main:openapi.yaml` out of a cloud checkout (after
fetching, so a stale worktree cannot be mistaken for the branch) and writes it
here byte-for-byte. A resync that changes nothing produces no diff; the one that
does shows exactly what the binary started or stopped serving.
Nothing is ever written back to hanzoai/cloud. The traffic is one-way by
construction: this repo reads a ref, and the SDKs read `hanzo.yaml`.
"""
import argparse
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.abspath(__file__))
SOURCE = "openapi.yaml" # the path inside hanzoai/cloud
TARGET = os.path.join(ROOT, "cloud", "openapi.yaml")
REF = "origin/main"
CLOUD = os.environ.get("CLOUD_DIR") or os.path.join(os.path.dirname(ROOT), "cloud")
def git(repo, *args):
r = subprocess.run(["git", "-C", repo, *args], capture_output=True, text=True)
if r.returncode:
sys.exit(f"sync: git {' '.join(args)} failed in {repo}\n{r.stderr.strip()}")
return r.stdout
def repository(path):
"""A git checkout, worktrees included — where `.git` is a file, not a dir."""
return subprocess.run(["git", "-C", path, "rev-parse", "--git-dir"],
capture_output=True).returncode == 0
def woven(repo):
"""`origin/main:openapi.yaml` from a hanzoai/cloud checkout, and its commit."""
if not repository(repo):
sys.exit(f"sync: no hanzoai/cloud checkout at {repo}\n"
f" git clone [email protected]:hanzoai/cloud {repo}\n"
f" (or point --cloud / CLOUD_DIR at one)")
git(repo, "fetch", "--quiet", "origin", "main")
return git(repo, "show", f"{REF}:{SOURCE}"), git(repo, "rev-parse", "--short", REF).strip()
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--cloud", default=CLOUD, help=f"hanzoai/cloud checkout (default {CLOUD})")
ap.add_argument("--check", action="store_true",
help="report drift and exit non-zero; write nothing")
a = ap.parse_args()
spec, commit = woven(a.cloud)
have = open(TARGET).read() if os.path.exists(TARGET) else ""
if spec == have:
print(f"cloud/openapi.yaml is {REF} ({commit}) — nothing to sync")
return 0
if a.check:
print(f"cloud/openapi.yaml differs from {REF} ({commit}) — run `python3 sync.py`")
return 1
open(TARGET, "w").write(spec)
print(f"cloud/openapi.yaml <- hanzoai/cloud {REF} ({commit})", flush=True)
return subprocess.call([sys.executable, os.path.join(ROOT, "merge.py")])
if __name__ == "__main__":
sys.exit(main())