6 Commits
Author SHA1 Message Date
zeekayandHanzo Dev cae99adb44 The relay reads the operation zip served
An op learns its address from zip.OpOf and zip.AddressOf, so the claim table,
the five declaring verbs and the address bookkeeping are gone. A host that
declares one of these addresses itself now gets a Build conflict from zip.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 15:02:30 -07:00
zeekayandHanzo Dev 2965c80547 An address is measured from a root this package owns
The five claim verbs need the address an operation will answer at, which is a
root joined with a path. They used to ask the target for its prefix through a
struct written to answer with one — and that struct existed for a second reason
that has since gone: an operation with no declared id took a shape-derived one
qualified by the prefix of the occurrence it answered under, so a plain group
would have renamed every undeclared id. All 319 operations state their own id
now, so a declared id survives composition verbatim and the group is just a
group.

The roots are this package's own constants, so each group carries the one it was
made with and nothing can disagree about the address. The escape hatches name
their whole address and register through Raw, which says which method each one
answers.

The test that measured the gap reached zero and said so; it now holds the
property from the other side, that no operation goes undeclared.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 06:15:44 -07:00
zeekayandHanzo Dev 599a26cedf the runtime answers by name, so the declaration is the only registration
o11y's surface was stated twice: once as the implementation (367 routes through
routing.Router) and once as the module's own declaration of it (353 typed ops +
11 hatches + 3 probes, the same 367 addresses with an input, an output and their
prose). The declaration had to REACH the implementation, and the only way it
could was to speak HTTP to the WHOLE service — build a request, hand it to one
http.Handler that was the entire router, and let that router match the path a
second time to find the handler the declaration had already named. An in-process
round trip, across a net/http<->fasthttp bridge, through a recorder, to answer a
question the caller had the answer to.

A registration is a map from address to handler. routing.Table.Handler reads it
as one and o11y.SetRuntime hands the whole table over, so the second match is
gone and with it the failure class the reachability census exists for: a request
cannot lose its route to a router that is never consulted.

Two things could not be said before and now can:

  - "this address is not implemented." A declared address the runtime does not
    serve came back as the runtime's own 404, indistinguishable from a caller's
    typo. It is a nil at the seam now. The two sides HAD drifted: three addresses
    the declaration names {traceId} were registered {traceID}, invisible because
    a router matches by POSITION and a segment's name never reaches it.
  - "the runtime is right here." The standalone server IS the runtime and could
    not install itself as that one handler without an op relaying into the router
    already serving it, so it installed nothing — and every op on its /mcp
    surface and its by-name call plane answered 503 in the binary holding all 367
    handlers. It installs its own table now.

The address is stated ONCE, at the registration, and rides the call: claim.go's
verbs wrap every op in addressed, which puts the address into the context, and
relay reads it there. relay takes no method and no path — which door was knocked
on is not something an op's body decides. That retires 706 spellings of 367
addresses; the 119 hand-built concatenations are rendered by zip.Address from the
same input zip bound the segments into, so the round trip is exact by
construction rather than by eye.

Reached BY NAME there is no router in front of the handler, so the two chain
members that need the route — Audit keys its record on the template, Resource
resolves the declared resources — would read an empty one, silently. Table.Handler
hands back the chain carrying the template it was registered at, written through
coretypes.SetRoute: the same representation bind writes for a matched request,
one read on the other side. TestByNameCarriesTheSameRouteAsMatching drives both
doors against one registration and compares them to each other.

Addresses did not move: the census — 367 fiber routes, 367 declared, 353
operation ids — is byte-identical to the parent. 155 packages ok, 0 fail, the
same set by name as the parent.

wantQueryServiceRoutes closes the equality argument the sibling address test
makes. It rested on four facts and 134 was a sentence, so a route deleted in
pkg/query-service/app left both address tests green while the declaration went on
naming an address nothing serves — which costs more now that it is a 503 rather
than a 404.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 22:51:39 -07:00
zeekayandHanzo Dev 9119c4b747 A host may take an address, and now it can say so
Every route in this table is named. That is what made the table honest, and it
is also what turned a host's overlapping route from a silent shadow into a
refusal to compose: zip validates that one address is declared once, so a host
declaring GET /v1/o11y/metrics alongside this table does not win, it panics.

hanzoai/cloud declares three of these addresses natively — GET /v1/o11y/logs,
GET /v1/o11y/metrics, POST /v1/o11y/query_range — and used to win them by
registering first, back when a wildcard answered everything else. The wildcard
is gone, so ordering decides nothing and the overlap is a boot failure.

In-order shadowing was never a decision, only an artefact of registration order,
and the two handlers it hid could drift apart forever without anyone being told.
Claimed makes the host NAME what it takes, which is the same fact written down
where it can be read.

WHY A VERB PER DECLARATION, NOT A FILTER ON THE TARGET. The check needs the
ADDRESS, and an address is the target's prefix plus the path. zip resolves an
OpTarget once, without a path — OpScope carries no route — so a target cannot
refuse one address and accept another. The declaration can, so the five verbs
each gained the check and all 353 typed ops plus the eleven hatches go through
them. Gating only the three addresses that collide today would have been a seam
that was true where someone remembered it, which is the shape of defect this
package already shipped once.

A claim names a method AND a path, spelled the way zip spells an address in its
own diagnostics — "GET /v1/o11y/metrics" — so the string that names the conflict
is the string that resolves it. A claim on an address this table does not
declare is inert rather than an error: the host is describing what IT serves,
and a table that stopped declaring an address must not become a boot failure in
every host that had claimed it.

Nothing about the runtime changes. A claimed operation is still there and still
reachable on the runtime's own router; this table stops PUBLISHING it, so the
host's handler is the one answer at that address in the host's process.

Tested: an unclaimed Mount declares everything (the control, so the absences are
attributable to the claim); a claimed address is not declared while its
siblings, its other methods and the rest of the table are; a claimed hatch goes
too, since the eleven took a different verb; an unknown claim is inert; and a
claim does not leak into the next Mount in the same process. The full root
suite, including the 353-op census, is green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 09:34:43 -07:00
zeekay bf6e17e0af chore(zip): v1.23.0 — Use is the one composition verb
Four changes zip v1.23 forced. Only the first is a compile error; the other
three are silent, and two of them would have shipped a broken service.

1. Prepare() -> Build() error. publish already returned an error, so the verdict
   propagates: a composition this server cannot serve now fails at startup
   naming both claimants, where before it could only be found by the routes
   going missing.

2. routing.Router read its prefix back off zip (OpScope().Prefix). A group is
   now an App INCLUDED at a prefix rather than a router carrying one, so
   OpScope().Prefix is empty for every group — and reading it STILL COMPILES.
   The Table would have silently recorded every nested route at its leaf path.
   The prefix is composed at Group now, by the same join Handle records with.

3. THE OPERATION IDS. zip qualifies an op's id by the prefix of the occurrence
   it answers under, so every id declared under app.Group(o11yRoot) became
   "v1.o11y.CreateRole". That is 353 renamed MCP tools, OpenAPI operationIds,
   CLI commands and generated SDK methods — in a patch release, with no opt-out
   (an explicit WithOperationID is qualified too). The rule is right for a
   composition; o11yRoot is not one. It is this service's own address, and an
   occurrence at the ROOT prefix is unqualified. So the ops register on the app
   with OpScope.Prefix doing what it documents — prepend to the op's path. Same
   method, same full path, same middleware, and the id the declaration wrote.
   One type (relay.go `under`), 18 call sites, no route literal touched. It is
   also what mount.go's PATH UNTOUCHED already claimed and what mountHealth
   already did by hand.

4. ONE ADDRESS, ONE DEFINITION — and the standalone server declared 367 twice.
   The service's own handlers SERVE those addresses and the published table
   DESCRIBES them; zip has no way to spell that difference ("the route and the
   op are the same fact recorded once"), so Build refused the program and the
   community server could not boot. The two facts now live on two apps: the
   declaration builds on an app of its own and only its CONTROL routes — the
   document, /docs, /mcp, the call plane, the plugin declaration — are served
   from the listener. zip keeps control routes off the walk, and none of the
   five is one of this service's 367, so nothing can shadow or be shadowed.
   This also makes ORDER IS THE CONTRACT structural instead of positional: the
   declaration is not on the listener at all, so there is no order to get wrong,
   and livetail, the long-poll and the chunked export still stream unbuffered.

TestPublishServesTheDocument now reads the tool count from a tools/list over
the served door rather than len(app.MCPTools()) — an in-process accessor on the
app the test just built is the shape of proof that file exists to reject.

Measured: go build ./... clean; go vet ./... clean apart from pre-existing
"unreachable code" in the generated parsers; go test ./... 155 ok, 0 failed.
2026-08-03 21:38:08 -07:00
zeekayandHanzo Dev 2d4b46d8ee mount: the wildcard is gone — every one of 367 routes is named
The delegation catch-all made three facts indistinguishable: a route that was
converted, a route that was not, and a route that did not exist. That is not a
theoretical cost — it is exactly how 83 typed ops shipped in files nothing
called, passing a full green suite while every one of those routes fell through
the wildcard.

So: derive the gap precisely, close it, and delete the catch-all.

THE GAP, re-derived. Extract every path the runtime registers (134 in
pkg/query-service/app/routes_*.go, 233 in pkg/apiserver/o11yapiserver/*.go — the
134 matching the count routes.go documents), normalise :params on both sides,
and diff against what Mount installs. 367 runtime routes, 43 unnamed. Not the
four hatch classes the plan expected: eleven were genuine hatches and
THIRTY-TWO were simply never converted.

CONVERTED (32 new typed ops, three new slices):
  * traces.go       — a trace's spans, the trace field catalog and its tuning
                      write, waterfall, flamegraph, span aggregations
  * tracefunnels.go — funnel CRUD plus twelve analytics reads, six over a saved
                      funnel and six over one described inline
  * spanmappers.go  — the ingest-time attribute mapping groups and their mappers

THE ELEVEN HATCHES, each its own route literal with its reason beside it
(mount.go mountHatches): four streams with no single answer to name (livetail,
query_progress long-poll, its websocket, the chunked export), three 303
redirects whose answer is a Location and not a body, and four Sentry-SDK ingest
doors — a foreign protocol we RECEIVE, whose /api/ segment the SDK hardcodes and
which is not ours to rename.

TWO OF THOSE ELEVEN WERE UNREACHABLE. /ws/query_progress and the two
/v1/sentry/<project>/{envelope,store}/ doors sit outside /v1/o11y, so the old
catch-all never saw them: a Sentry SDK pointed at the clean root got a 404 in
the composed binary, and telemetry_test.go passed only because the test
registered the wildcard the deployment did not have. Naming every route is what
surfaced it.

ONE SEAM, TWO ROOTS. Five copies of the relay function (relay, relayAt,
relayO11y, relayAPM, send) differing only in a prefix and whether they took
query parameters, and NINE names for two values — identityPrefix, infraPrefix,
integrationsPrefix, metricsRoot, apmPrefix, accessPrefix, rulesAlertsPrefix and
o11yRoot were all "/v1/o11y". Ops now pass the whole public path and relay
concatenates nothing: 193 call sites, one implementation, -520 lines.

Also: the per-slice *_zipdoc.go files fold back into zipdoc_gen.go as their own
headers instructed; the health probes' fall-through names its destination
instead of relying on a catch-all being registered after it (they would have
started 404ing the moment SetHealth was late — precisely when liveness matters);
O11yLogField/O11yLogFieldsOut/O11yLogFieldSetting become
O11yTelemetryField/O11yFieldCatalogOut/O11yFieldSetting because logs and traces
tune ONE catalog through one runtime type.

PAYOFF, counted off the document rather than the router: 353 operations in the
published OpenAPI spec and 353 MCP tools, from 0 for this surface before the
conversion began. 353 typed + 11 hatches + 3 probes = 367.

RECORDED, NOT FIXED: the runtime renders with jsoniter and zip with the stdlib
encoder, and jsoniter does not implement `,omitzero` — so for the seventeen
fields across pkg/types that use it the runtime writes the zero value where a
typed op omits the key. Pre-existing on every shipped op reaching
TelemetryFieldKey. Pinned by traces_test.go rather than hidden.

gorilla/mux and gin stay: mux is the runtime's own router in 100 files, and gin
is imported only for its form binder by pkg/valuer/valuer.go
(ginbinding.BindUnmarshaler) and pkg/http/binding/query.go
(ginbinding.MapFormWithTag). go mod tidy dropped 687 lines of go.mod/go.sum.

Build and full suite green: 133 packages ok, 0 fail.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 17:15:37 -07:00