A socket op's handshake is decided before its handler runs, so a
handler with effects runs only for a connection it will get: 426 for
another protocol version, 400 without a key, and 403 for a browser on
an origin the op does not admit — its own, plus zip.Origins(...). The
origin policy is the op's, not the answer's, so Socket carries only
Serve, Subprotocols and Compression, and wsx's own refusals are back
to the library's. The in-process CLI refuses a socket op before
invoking it.
An op that answers JSON or bytes keeps JSON's media for its JSON when
Produces names the bytes'. 204, 304 and a redirecting op's 3xx carry
no body in the document; any other declared status, 3xx included,
carries the Out and the doc comment's example, as before. An MCP read
of a Reader is bounded in time as a stream is, and the reader closed.
TestConnMemory counts the goroutines holding connections added, as it
counts the heap they added, so goroutines other tests leave winding
down are not charged to the server.
A JSON answer keeps fiber's application/json; charset=utf-8 unless the
op names another media, and every declared status carries the Out's
schema again, a union's alternative filed under the status it states.
A value of the op's own type stating status zero is refused as before;
only Body and Redirect read zero as the op's own. A header field is a
CLI flag under its field's name as it was, and a 3xx with a body is an
answer the remote CLI prints.
form: tags bind a form only when the op consumes a form media type, so
another binder's tags no longer move an op off JSON or refuse a GET.
A cookie: field is set by the request's cookie and nothing else, and no
body schema lists it, so a cross-site body cannot fill it.
Sse.Send takes a ctx that ends with the stream: a tool call collects
for at most toolWait and toolBound bytes, then cancels the producer,
and a stuck producer cannot hold the call. A panicking Reader ends its
answer, not the process. A socket op refuses a request that cannot
upgrade before its handler runs, over REST and the call plane, and a
nil CheckOrigin is same-origin, refused 403. The call plane carries a
union's JSON alternative; Sse and Socket fields that cannot cross say
so with zap:"-". MCP bytes are bounded and untyped bytes take the op's
media.
A type that embeds Redirect or Body is that answer, so a sign-in leg
answers 302 with two Set-Cookie lines and no body, and a confirmation
page answers its HTML with a cookie, through CookieCoder on the
embedding type. Verbatim reads its Body the same way instead of by
reflection.
An answer with no bytes and no Type carries no Content-Type, so a 304
is a Body with Status 304 and nothing else, as a handler that wrote
nothing never sent one. A filename has its quote and backslash escaped
and its control characters dropped, so it cannot end the header.
Every field of Command, Arg and Flag carries its own comment, so the
command projection's shape is described where a document publishes it.
The registry's body flags keep field order.
A typed op could only take one JSON value and answer one, so uploads,
signed webhooks, form posts, YAML and merge-patch bodies, PDFs, SSE,
WebSockets, OAuth redirects and upstream relays stayed untyped routes
with no schema, tool, command or SDK method. Each is now a typed op and
its REST wire is unchanged.
Requests: a zip.Body field takes the body and Content-Type exactly as
sent; an In implementing Parser decodes itself; form: fields and
*zip.File parts bind url-encoded and multipart forms; cookie: binds a
request cookie as an in: cookie parameter. Consumes names the media.
Answers: zip.Body (bytes, filename, its own status and headers, a
Reader streams chunked), Verbatim[T] (a relay documented as T), Sse[T]
(events with keep-alive), Socket[M] (an upgrade after the contract
runs; 426 without one, 501 over ZAP), Redirect, CookieCoder, Produces,
and unions: a type whose OneOf method's results are the alternatives,
with Or[A,B] as the stock one filing each under the status it states.
Projections follow from the manifest: the document publishes binary
media, form schemas, x-events, x-socket with 101, 3xx with Location
and oneOf; MCP takes bytes as base64 and returns bytes, text or
collected events as tool content, listing a socket op in _meta as no
tool; the CLI reads --body @file|-, --field name=@file and --out,
prints events as they arrive and bridges a socket to stdin and stdout.
tools/list is ProjectMCP over the manifest, the projection MCPTools
already was.
Prose reaches promoted fields (filed under the declaring struct), a
type defined over another struct, and the type arguments of zip's
kinds and OneOf results; an instantiated generic is published under
its name followed by its argument names instead of an illegal
component key. The unused reflect-only document and tool builders go.
An op is an async fn. It may take &zip::Cx (request headers; the
answer's status, headers and trailers) and a zip::Body read as it
arrives, and may answer zip::Sse or zip::Body, each chunk written as
the op yields it. A client that leaves drops the op's stream.
The blocking std::net door is gone: HTTP/1.1 keep-alive and h2c on
one port (hyper_util auto), graceful shutdown bounded by a grace.
The ZAP door runs on tokio and streams with zap-proto/http's head,
data and end frames.
Wire names are serde's: the derive reads rename, rename_all, skip
and with, and the hand-rolled JSON is deleted. The info manifest
comes out byte for byte as before.
No zapgen emits Rust, so the crate never built; the frame codec is
written in zap.rs and pinned to frames Go wrote. The length prefix
is big-endian, as zap-proto/http's transport writes it.
examples/stream is a JSON op, an SSE op with a trailer and a byte
relay, tested over raw HTTP/1.1, h2c and ZAP sockets.
Claude-Session: https://claude.ai/code/session_01SCPMGSQsLNF2YtHFL4bwyp
"stream": "sse" or "bytes" publishes text/event-stream or
application/octet-stream instead of the 204 an op with no out
projected as; "raw": true publishes an octet-stream request body.
Check refuses a stream beside an out and a raw body beside an in.
Claude-Session: https://claude.ai/code/session_01SCPMGSQsLNF2YtHFL4bwyp
Both transports reuse a RequestCtx for the next request, and a handler's
context read the request's headers lazily, so a context kept past its handler
answered with whichever caller the RequestCtx was serving by then. The context
now holds the request through an inflight the request owns: the server closes
it when it resets the request, and every read holds it while reading. A context
kept past its request answers with no caller, forwards no identity, attests no
peer and is not a local call.
Co-authored-by: Hanzo Dev <[email protected]>
mustBuild read the draft's error off the App while liveOrBuild wrote it under
buildMu, so a goroutine reaching the router raced a concurrent build;
liveOrBuild now returns the error it read under the lock. Detach is the context
for work that outlives a handler: the server reuses a request once its handler
returns, so a goroutine holding the handler's context read the next request's
headers. Detach states the caller while the request is still its own, on a
context that no longer reaches the request and is not cancelled with it.
Co-authored-by: Hanzo Dev <[email protected]>
Every child writes to the same host stream, and the tag and the line went out
as two writes under only that child's lock, so another child's line could land
between them: it carried the wrong plugin's tag and the next line carried none.
The line is assembled and written once, under a lock all children share; a last
line without a newline is flushed when the child exits.
Co-authored-by: Hanzo Dev <[email protected]>
A child forked by one start holds a copy of every descriptor the host has
open until it execs, including the write handle another start is copying
its binary through. Linux refuses to exec a file anyone holds open for
writing, so plugins started together failed each other with "text file
busy": 2 of 16 in TestStart_TogetherAllRun without this, none with it.
Holding the read side of syscall.ForkLock while the copy is written keeps
every fork out of that window.
Co-authored-by: Hanzo Dev <[email protected]>
An unexported handler documented with its exported spelling -- `list`
carrying "List returns the caller org's live bot runs" -- named itself as
exactly as "list returns" would, and kept the identifier: "List" is one
hump, so the shape rule cannot read it as a symbol. 136 lifted comments
across the cloud fleet open this way, and each reached the document, the
CLI help, the MCP tool and the SDK docstring as "List returns ...".
The case-folded name takes the shape rule's verb guard, so "Health is a
liveness probe" keeps its subject.
Co-authored-by: Hanzo Dev <[email protected]>
traceOf read the caller's ids as slices of a view over the header
buffer, then rewrote that buffer in place: every shipped span named
itself as its parent, and once the connection served its next request
the span carried that request's trace id.
Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
A host that keeps spans in its own tracer opens positions zip never
sees: a span inside a handler, or work that outlives its request. An
outbound call then names the request, or nothing, as its parent, and
the callee's spans land beside the span that caused them.
Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
zap reads text zero-copy over the frame that arrived, and Call releases that
frame to the pool when it returns, so a reply's strings pointed into memory the
next call overwrites. The generated codecs already copy text for this reason;
the reflective decoder now does the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
schemaOf fell through to {"type":"object"} for an interface, and the
projector did the same for the `any` prim, so a field holding a token
string or an array was described as an object. The published examples that
carried one failed against their own schema — Hanzo's auto.runIn.auth and
tool.toolCall.arguments — and every generated client typed the field wrong.
An interface is now the unconstrained schema a silent marshaler already gets.
Co-authored-by: Hanzo Dev <[email protected]>
Call, remote invokes, proxy hops and CLI commands all called Client.Do,
which takes no ctx, so a cancelled or expired caller kept waiting until
the transport's own read timeout. zap-proto/http v0.3.11 adds
Transport.DoContext; a client that implements the new ContextClient
interface is now driven through it, and Call returns an error wrapping
context.Canceled or context.DeadlineExceeded.
Co-authored-by: Hanzo Dev <[email protected]>
routerPrefix promises that an unresolvable router "is an error naming the
call, never an assumed empty prefix", and then returned the empty prefix for
a group. So a group handed to another function filed its ops at the path
after the prefix — an address nothing serves — in the document and the MCP
tool alike, which is the silent drop the resolution exists to end.
The one shape this hits is a group reached through a parameter: the prefix
belongs to the call that made the group, and the call is in another
function. The error says so now, rather than leaving the reader to infer it
from a path that looks plausible.
Co-authored-by: Hanzo Dev <[email protected]>
cmd/zipdoc files every entry under the package that declared it, raw routes
included. A raw route records no registrar — a method, a path and a handler
chain is all App.raw keeps — so an address is all anyone can ask by, and the
address lookup could not reach a qualified key. A service's whole untyped
half therefore documented as silent the moment its generated file was
rewritten: 57 of iam's routes at once.
So the keys are indexed by address as they are recorded, and only the
unqualified lookup reads that index. A caller that names a package asked
about that package, and answering it with another's sentence would document
one service with another's words.
An address two packages describe differently is dropped from the index
rather than resolved. One address answers once in one process, so two
claims on it are a conflict, and choosing either would be a guess; the
qualified lookup still reaches both.
Co-authored-by: Hanzo Dev <[email protected]>
A host that re-exports the type writes `type Router = *zip.App` and hands a
Router to every plugin. That is the same type, but it arrives as
types.Alias, so matching the shape directly saw neither an App nor a Group
and the op's prefix could not be resolved — which refuses the whole
document rather than filing one path wrongly. Unalias at each place a type
is matched by shape, including the In and Out inferred from a handler.
A group is also read when var names it, not only :=. The prefix belongs to
the Group call; the syntax that binds the result does not change it.
Co-authored-by: Hanzo Dev <[email protected]>
Cloud composes each plugin as its own definition and asks Declaration for
its routes before composing it. A projection of a program that does not
compose panics rather than lying, so a plugin whose middleware guards no
routes took the host down with it; Build answers instead, but installs a
generation and freezes the definition the host is about to compose by
reference.
Verify is Build's first half and the same call — walk, structural rules,
derived rules, rule adoption — and stops there. The host reports which
group is wrong and carries on.
It answers for the definition as the root of its own walk. Middleware at a
served root is exempt, because composition brings it routes later; one
level down the same middleware guards a settled subtree. Only the host
knows which a definition turned out to be, so both verdicts are right and
the tests say so.
Co-authored-by: Hanzo Dev <[email protected]>
v1.37.9 fixed the REST path and the three by-name seams that already looked the
op up in the composed registry. Two did not: the graph resolved its ops from
that same registry and then invoked them without saying so, and the in-process
invoke did the same. So the contract fell back to the declaration — for anything
declared on a group, the bare leaf — and a rule reading op.Path governed the
REST call while missing the graph call to the SAME operation.
A test now asks both seams about one op and requires the same answer.
Co-authored-by: Hanzo Dev <[email protected]>
The authorizer and the result hook were given the op as DECLARED: for anything
declared on a group, the bare leaf and an id derived from it. So a rule keyed on
op.Path governed an address nobody serves — /probe/:id where the route table,
the registry and the document all say /v1/crm/probe/:id — and one definition
mounted at two prefixes reported a single name for two operations.
They now see what the route matched: the composed pattern, and the composed id
by the same rule the registry uses, where a declared id is the operation's own
name and composition does not get a vote. The concrete address stays off Op,
because path parameters are input and the hook is promised the operation and the
outcome and never the input; a handler that needs the resolved address asks
AddressOf.
Co-authored-by: Hanzo Dev <[email protected]>
Alias existed for one reason, stated in its own doc: a registration made inside
a helper is invisible to cmd/zipdoc, so a service that rolled its own alias
helper lost the prose for both addresses. That is no longer true — the pass
reads Raw on a route table directly, and a fixture now proves two plain Raw
calls each carry the handler's sentence.
So the abstraction was working around a limitation that no longer exists, and
nothing in the estate called it. It goes, with the thirty-odd lines of extractor
special case that recognised it.
Co-authored-by: Hanzo Dev <[email protected]>
The address is a URL path, so a value substituted into it has to be escaped as a
single segment. A trace id of t1/2 went in raw and read as two, so a service
that forwards what AddressOf answers asked its runtime for a route nobody
registered: the request arrived at the right operation and the forward went
somewhere else. A space did the same thing more quietly.
Co-authored-by: Hanzo Dev <[email protected]>
A pattern never ends in "/" now, the root aside, because the router serves "/x"
and "/x/" alike — so two spellings were two keys for one address, and a group
could not name its own root at all: declaring "" on it produced the prefix with
a trailing slash. That is why a collection root had to be declared at an
absolute path on the parent instead, which put the route outside the middleware
of the group it belonged to.
The rule was also written twice. The router composed a prefix with a leaf and
cmd/zipdoc composed the same two things to find that route's prose, so trimming
in the router alone would have filed every collection root's sentence under an
address the document does not contain — complete document, missing description,
no error anywhere. Both copies are gone; internal/addr is the one that spells it.
A test registers a trailing-slash pattern — the shape the Sentry-compatible
ingest addresses use on purpose — and asserts the route table, the registry, the
document's template, and the operation and concrete address a handler reads are
all the same string, because a service that resolves its runtime handler by name
from that pattern refuses every call if two of them disagree.
Co-authored-by: Hanzo Dev <[email protected]>
The pattern says which operation answered; the address says which resource it
was asked about. A handler that forwards needs both, and could only get the
second by being wrapped in a closure that held the decoded input — which is what
put a service's whole surface behind a helper this pass cannot read.
The one seam every projection funnels through holds the resolved pattern and the
decoded input together, so the address is stated once and correctly for REST, a
tools/call, the call plane and an in-process invoke alike, where three of those
have no request to read a path from. It is computed on demand: an op whose
pattern carries no parameter pays a string compare, one that does pays
reflection, and only if something asks.
Deliberately not a field on Op. Op is what a rule is handed and what the result
hook is told, and both are narrow on purpose — the hook is given the operation
and the outcome, never the input. A concrete address carries path parameters,
which are input, so putting it there would widen two contracts to serve a third.
A test pins what those two see, including that it is still the declared leaf
rather than the served pattern: that gap is older than this change and is its
own decision to make.
No additional allocation over the previous patch, measured on
Benchmark_TypedRoute: 63 either way.
Co-authored-by: Hanzo Dev <[email protected]>
The address an op answers at is a property of the CALL, not of the declaration:
a group's prefix composes at build, and a definition composed under two hosts
has two addresses and one handler. So a handler could not learn its own address,
and a service that needed one composed it in a wrapper and threaded it in —
which made every one of those registrations a call this pass cannot see, and
took a whole service's operations out of the document, the tool list and the
SDKs.
OpOf reads it from the context instead, so the declaration stays the bound
method the projections resolve. The seam that knows the occurrence states it:
the REST route that matched, and the by-name seams, which looked the op up in
the composed registry. Under that, the one contract every projection funnels
through states the declaration, so a handler always has an operation to read and
a direct call says there is none rather than guessing.
Costs two allocations on a typed REST request, measured: 61 to 63 on
Benchmark_TypedRoute, with bytes unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
The wire is stated in three places that have to agree: the option set, the
constant's value, and the comment explaining it. The existing cases fail the
moment the options move — and would pass happily while the name went on
claiming semantics the encoder no longer has, which is the one an operator
reads in a startup line.
So the name is now asserted against the bytes: it may say "sorted keys" exactly
when a map comes out sorted, and "v1 semantics" exactly when a zero under
omitempty is left out. It fails in both directions — options moved, or name
moved — and says which disagreed.
Co-authored-by: Hanzo Dev <[email protected]>
Variant said "encoding/json/v2", which reads as v2's semantics — a zero kept
under omitempty, a nil slice as [], case-sensitive decoding, a map's keys
unsorted. This wire does none of that: the options are v1's, plus sorted keys.
Naming only the engine misleads in exactly the direction that matters, and the
one moment anyone reads it is a startup line in a log.
It is also a declaration rather than a reading of the toolchain. zip marshals
through v2 because this package imports it; GOEXPERIMENT=nojsonv2 does not fall
back to v1, it fails to compile — so the name cannot describe an encoder the
binary is not using.
Co-authored-by: Hanzo Dev <[email protected]>
net/http's own server always fills RequestURI, so a handler is written expecting
it. Converting a fiber request fills URL and leaves that field empty, and the
handlers that read it are the ones that relay — a proxy, an ingest address,
anything that forwards what it was asked for. An empty target normalises to "/",
so a relay asks its runtime for a path nobody named.
The test reads the field rather than the path, because http.ServeMux matches on
URL.Path and is green either way.
Co-authored-by: Hanzo Dev <[email protected]>
The authorizer was already per-definition and a group has always been one, so a
group could hold a tighter rule than whatever composed it — App.rule asks its
own first. What was missing was the way to say so: Authorize existed only on
App, and a subsystem that guards its own writes had to assert its group back to
the concrete type to reach the field.
Without it there are two spellings and both are wrong. The rule on nothing
leaves every write there authenticated and unchecked. The rule on the host makes
it the HOST's, and that covers every op the host serves — so a sibling
subsystem's ops answer to rules written about a principal this guard never
attached, and every valid call to it is refused.
Co-authored-by: Hanzo Dev <[email protected]>
The variant was a literal in the test and a constant in the encoder, and the
two spellings of one fact are what drifted when the encoder moved to v2: the
suite went red over a name, not over any behaviour. The test now asks the
encoder.
Co-authored-by: Hanzo Dev <[email protected]>
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:
users := app.Group("/v1/users")
users.Post("/", api.Create)
mint := users.Group("/mint").With(middleware.Mint(gate))
mint.Post("/deposit", api.Deposit)
A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.
That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.
Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.
Co-authored-by: Hanzo Dev <[email protected]>
zip called encoding/json, which on Go 1.27 is already the v2 engine with v1's
options — so the speed was never the question. What was at stake is the
semantics: v2 keeps a zero number or false under omitempty, writes a nil slice
as [] and a nil map as {}, leaves `<` unescaped, matches field names
case-sensitively, and does not sort a map's keys. Selecting the engine by build
tag meant a module moving to 1.27 changed what every response said, and this
package had gone back to v1 to stop that.
Now it calls v2 and says what the wire is: v1's semantics, plus sorted keys,
because bytes that vary defeat an ETag, a cache, a signature and a git blob.
Each remaining difference is a decision to make here, once, in view of the
clients — never a default to inherit.
What v2 gives back is its API. Write and Read stream straight to and from the
connection with no second copy, and omitzero says what omitempty could not.
Co-authored-by: Hanzo Dev <[email protected]>
The crash trigger asserted 200, which says the panicking plugin flushed its
answer before the process went. Whether it wins that race is a scheduling
accident; what the test is for is the sentence after it — the host survives and
the plugin comes back.
Co-authored-by: Hanzo Dev <[email protected]>
Both front ends project through the same projector, so a change to what the
document says about refusals reaches the Go side the moment it is written and
the Rust side only when its projection is regenerated. The two disagreed by
172 lines — every default response and the problem-details schema — which is
the parity test doing exactly its job.
Co-authored-by: Hanzo Dev <[email protected]>
A walk over typed structs collapses every miss to the zero value, so a document
missing a whole response reads exactly like one whose $ref is empty, and the
assertion could only ever report "". It now fails at the first absent step and
prints the keys that are there.
Co-authored-by: Hanzo Dev <[email protected]>
fiber's default test deadline is one second, which measures the round trip. A
request to a lazy plugin waits for a CHILD PROCESS, and on a busy machine that
outlasts a second by itself — so six tests failed on a deadline that was never
measuring them, in a different subset every run, which is what made the whole
suite's baseline untrustworthy.
A generous ceiling weakens no assertion: a served request still returns the
moment it is answered.
Co-authored-by: Hanzo Dev <[email protected]>
The callsite is recorded by counting frames out to the programmer's line, and a
verb method on a scope sits one frame further away than the package-level
function does. The count said otherwise, so every conflict, cycle and post-seal
write in a scoped service cited a line somewhere above the registration instead
of the two lines actually in conflict.
Two tests now hold both halves of that attribution — the package an op is filed
under and the line it names — across all three forms a registration takes,
including a handler another package composed, which is the shape that once
published seven ops with no description at all.
Co-authored-by: Hanzo Dev <[email protected]>
The extractor matched one call shape: zip.Get[In,Out](target, path, fn), three
arguments with the target first. A scope's verb methods carry two, and the
receiver is the target, so every route migrated to scope.Post(path, handler)
went silently undocumented — the op still served, and its prose stopped
reaching the document and the tool list.
Both forms now resolve the same handler selector to the same declaration, and a
trailing metadata chain does not hide the call from the walk.
Co-authored-by: Hanzo Dev <[email protected]>
Go 1.27 allows generic methods on concrete types, so a scope's Get/Post/Put/
Patch/Delete infer In and Out from the handler. A declaration carries no type
arguments and reads as what it does:
accounts := app.Scope("/v1/accounts").Tag("accounts")
accounts.Post("/", api.CreateAccount).ID("accounts.create")
Interface methods still may not declare type parameters, which is why these
live on the concrete *Scope rather than on Router. The package-level Post and
its siblings are unchanged and register through the same path, so the document,
the MCP tool, the CLI and the call plane keep reading one registry — the two
forms publish byte-identical documents, which is a test.
The handler is the bound method, passed directly, so the selector survives in
the source and the projections can still resolve a method's doc comment.
Co-authored-by: Hanzo Dev <[email protected]>
zipdoc files an op's prose under the package whose source holds the registration
and the handler's doc comment. zip looked the prose up under the package the
HANDLER was declared in, which is the same answer only while the two coincide.
A handler composed by a helper elsewhere breaks it. hanzoai/cloud registers every
metered op as fare.Paid(s, o.listBuckets) — one decision about money in one place
— and under Go 1.27 that closure reports internal/fare, so apps/s3's seven paid
ops published no description in the document, the MCP tools or any SDK. Go 1.26
reported the registering package for the same code: whether the closure is
inlined is the compiler's business, so the doc key cannot rest on it.
Co-authored-by: Hanzo Dev <[email protected]>
jsonenc chose encoding/json/v2 on the goexperiment.jsonv2 tag, and Go 1.27 sets
that tag by default. Moving a module's go directive to 1.27 therefore changed what
every zip service writes: a zero number or false kept under omitempty, a nil slice
as [] and a nil map as {}, field names matched case-sensitively, map keys unsorted.
hanzoai/cloud went from green to 54 failing wire tests on that bump alone.
jsonenc is stdlib encoding/json and nothing else. Since 1.27 that API runs on the
v2 engine with v1 semantics, so the speed arrives with the toolchain and the bytes
stay put. A test pins the four differences.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo.ai's Slack agent went silent. Messages arrived — 18 event deliveries in
six hours — and not one agent run began. Every POST to the events endpoint
returned 502 after 43 to 64 seconds, and the host log said this, twice, at the
same timestamp:
23:39:31 zip lazy plugin started on first request pid 1017
23:39:31 zip idle plugin evicted reason=idle idle=2h37m58s
Started and evicted in the same second, reporting an idleness from before it
started.
evictIfIdle reads lastUse without the lock, decides, and then calls evict,
which takes the lock and swaps the child out. Its comment argued the window is
harmless: a request landing in it "is already being served by the instance
retire() is about to DRAIN, or it arrives after the swap and starts a fresh
child". There is a third case, and it is the one that happens. A request can
START the plugin inside that window — target() brings a lazy plugin up with a
CAS on p.cur — and the swap then takes down a child that has served nobody.
The caller waits out the drain and gets a 502. load.go already stamps lastUse
on start "or the next sweep evicts what just started"; the stamp lands, and the
sweep that already decided evicts it anyway.
So evict re-reads the clock under the lock it is already holding. lastUse
moving is precisely the signal that someone used or started the plugin since
the decision was taken, which makes the decision stale. Callers pass what they
observed; 0 means no idleness is being claimed.
Co-authored-by: Hanzo Dev <[email protected]>
The package did not build. project_zap.go declared `type idl` while
zapread.go, in the same package, imports github.com/zap-proto/go/idl and
calls idl.Parse — so every reference to the import resolved to the struct:
project_zap.go:65:6: idl already declared through import of package idl
zapread.go:52:2: other declaration of idl
The package name is the half that cannot move, so the type does. `namer`
rather than `naming`, which layout.go already has.
Nothing else changes: same fields, same methods, same behaviour.
Co-authored-by: Hanzo Dev <[email protected]>
Writing the manifest fragments during macro expansion had a failure nobody
would see: rustc reuses a cached expansion, so a macro that did not run left
its file saying what the source used to say. A rebuild after deleting one
fragment recompiled the crate and did not restore it — measured — which means a
service could publish a document that quietly disagreed with its own code, the
one failure this whole design exists to prevent.
So the description is compiled IN. `#[derive(zip::Wire)]` gives a type
`stated()`, its manifest entry as a `&'static str`, and `reach()`, which claims
its entry and then asks the types of its fields — the graph is walked by the
types themselves, because a macro sees only the item it is on. `#[zip::ops]`
joins the ops half to whatever the ops reach and hands back `manifest()`. The
service prints it on `--manifest`, so what is projected is what this build
declared and there is nothing to go stale.
That leaves one shape for a manifest instead of two, so zipc reads a document
and no longer merges a directory of fragments; what it refused about a
directory it now refuses about the document, in Manifest.Check beside the
version rule. The example's build script tolerates the documents not existing
yet, which is the state of the one build that makes them.
A Rust developer writes Rust handlers, with Rust types and Rust doc comments,
and gets every projection from that source — the same deal a Go developer has
had. No .zap is written by hand and no Go program declares a Rust service's
ops.
The split falls on a measured seam. What is native per language is DECLARING an
op and RUNNING it; what is shared is the projection logic, driven from a neutral
description of the registry.
Manifest the registry with the Go taken out: ops, types, fields, wire
names, prose. App.Manifest fills one in by reflection; a
proc-macro fills one in as a Rust crate compiles.
Project* the OpenAPI document, the MCP tool list, the CLI and the ZAP
IDL, read off a Manifest. Go's own three now go through them —
the whole existing suite is the oracle — so Go keeps no private
path for them and the other languages cannot rot.
cmd/zipc the build-time projector. It reads a manifest, or the fragments
a compiler wrote, and writes the four documents.
rust/ zip in Rust: #[zip::ops] on an impl block, #[derive(zip::Wire)]
on a type, a router, a JSON binder, an HTTP door and a ZAP door.
The wire is generated by zapgen from wire.zap; nothing here
spells a byte offset by hand and nothing links Go.
The layout rule moved out of the manifest and into the projector, so the offsets
in a .zap are derived once for all three languages rather than three times;
TestProjectZAPMatchesRegistry pins that derivation against the layout the Go
encoder actually writes.
One service, written twice — corpus/info in Go and rust/examples/info in Rust,
fourteen ops each — is the gate. The OpenAPI document, the MCP tool list and the
CLI command tree come out byte-identical, and the ZAP schema agrees on every
struct, type and offset; it differs only where a field carries the declaring
language's own case convention and where the ledger names a type the way the
person who must change it would grep for it.
The rule was stated as "a Handler may never terminate a request", and that is not
what the check does or what the framework allows. A gate that cannot refuse is not
a gate: an auth check, a rate limiter, a cache hit and a maintenance refusal all
write a response and return without Next, and they always worked.
What structural refuses is narrower and stays: a handler that CLAIMS AN ADDRESS
from Use position — `app.Use(zip.Static(assets))`, which answers every address
beneath it with a file including ones nothing declared. A gate answering a request
that already matched a route adds no address, so no projection can disagree with
the router; a leaf in Use position invents addresses the document has never seen.
Those are two different facts and only one of them is a rule.
The overstatement cost something real. A consumer read it and routed a 503 refusal
through the host's error handler rather than answering directly — and a host that
flattens propagated errors to 500 turns that into the wrong status. So the
behaviour is now a test rather than a sentence.
The README gave no version to install and no service to run, never
mentioned zipdoc, Described, ErrorHandler, the call plane's field-order
rule, zipgen, CommandsFromSpec, Authorize or the MCP door's limits, and
named APIs that do not exist (app.Add, app.Mount, a zip.Load that takes a
prefix first). It now takes one service through REST, MCP, OpenAPI and the
CLI with output from real runs, then covers each projection, the error
format and the release that introduced it, and the security model.
examples/local-service is that service: one SQLite-backed operation, doc
comments run through zipdoc, a .mcp.json for Claude Code, and a test that
checks every operation against every projection. It is a module of its own
so the SQLite driver stays out of zip's graph. CI runs zipdoc -check and
its tests.
LLM.md is now a map of the code, the test commands, the release steps and
the invariants that hold today.
Co-authored-by: Hanzo Dev <[email protected]>
zip's go.mod requires github.com/zap-proto/go v1.8.3, and contract/go.mod
still listed v1.3.0, so `go test ./...` in contract/ stopped at
"updates to go.mod needed" before compiling anything. That is CI's
go-test-collector step. go mod tidy changes only that requirement and its
go.sum lines.
Co-authored-by: Hanzo Dev <[email protected]>
initialize always answered 2026-07-28. The official TypeScript client
(SDK 1.30.0) and Python client (SDK 2.2.0) ask for 2025-11-25 and refuse
an answer in a revision they did not request and do not know, so neither
could connect to /mcp.
The MCP lifecycle's version negotiation answers in the requested revision
when the server supports it. This door serves 2025-06-18 and 2025-11-25 as
written (the same Streamable HTTP transport and tool methods, and no
batches), so those are echoed. Anything else is answered in 2026-07-28, as
before. With the change both clients connect, list the tools and call one,
the Python client in its default auto mode as well.
Co-authored-by: Hanzo Dev <[email protected]>
Every non-2xx a typed op answers is an RFC 9457 problem document, or
RFC 6749's error pair at an OAuth address, and the document declared
neither, so SDK generators typed errors as unknown. Each operation now
carries a default response that points at a problem-details or an
oauth-error schema. The OAuth addresses come from composeOAuth over the
same walk the error handler is built from.
Neither schema name can be a Go type's name. Neither schema writes a
boolean additionalProperties: CommandsFromSpec parses schemas as objects,
and a boolean failed it.
TestTheDocumentDeclaresTheRefusal checks the declaration at both kinds of
address, and that the refusal on the wire carries every member the schema
requires. The ErrorHandler and MarshalJSON comments still described the
old envelope and a written instance member; they describe what is written.
Co-authored-by: Hanzo Dev <[email protected]>
zipdoc files an operation's prose under its package's import path. At run
time pkgOf read the package off the handler's function name, and a built
binary names package main's functions "main.", so the lookup missed: the
document, the tool list and the CLI help carried no prose. Under go test
the package keeps its import path, so every test passed.
pkgOf now reads "main" as the build info path, which is the import path
zipdoc wrote. TestProseReachesAServiceInPackageMain runs zipdoc over a
package main service, builds it and reads the document the binary
writes. It fails without the change.
gofmt realigns one struct field in typed.go, and the WithStatus comment
names the problem document instead of the envelope it replaced.
Co-authored-by: Hanzo Dev <[email protected]>
2f5a387 added origin, rebase and 26ab3688408b38db62d9a92e46eba140d421db23.
Each holds a single Co-authored-by line: the output of a mistyped shell
command, not source.
Co-authored-by: Hanzo Dev <[email protected]>
A codec is a runtime encode/decode step, which is the thing ZAP removes: a
field IS its offset, and a read is a pointer into the bytes that arrived.
Nothing here runs on the served path — Layouts reflects at BUILD time and emits
constant offsets. Naming it for the layer it exists to delete told readers the
opposite of what it does.
LayoutOf already names one type's shape; this emits those as source.
Co-authored-by: Hanzo Dev <[email protected]>
Authorize is ASKED before a handler and may refuse. Nothing is TOLD after one,
which leaves a gap that no middleware can close: everything a middleware records
is recorded about a REQUEST, and an in-process invoke is not one. An operation
reached that way runs with no trace of having run — no trail entry, no metric,
no span — and it is the door an operator on a pod uses.
So the hook sits at the same seam, which is the one place every projection funnels
through, and is told about all of them. A refusal is told about as surely as a
success, because Deny returns through the same error it reads.
It is told the operation and the outcome and NOTHING else. Not the output: a hook
that could see the value would be a way for a response body to reach whatever the
hook writes to, and every recorder in this estate promises it never reads bodies.
Not the input either — Authorize already sees that, before the handler, where a
decision belongs.
Named returns made that possible and cost one bug on the way: binding the
handler's result to the `any` return wraps a nil *Out in a non-nil interface, so
a void op stopped answering 204. opstatus_test.go caught it, the result stays
typed, and the comment says why.
Co-authored-by: Hanzo Dev <[email protected]>
The descriptor-passing change reached for syscall.FcntlInt, which exists in
x/sys/unix and not in Darwin's syscall package, so every build on a Mac has
failed since it landed. The last tag builds; main does not, which is the shape
that gets noticed late.
x/sys/unix is already required, so this adds nothing but the import.
Co-authored-by: Hanzo Dev <[email protected]>
A descriptor is not data: its number is an index into a table the peer does not
share, so a byte stream cannot hand over a pipe, a console, or an already-open
file. SCM_RIGHTS is the kernel doing that translation, and it rides the same
unix connection peerOf already reads SO_PEERCRED from.
Needed to carry a sandbox control plane. gVisor's urpc passes Files on 20 of
its 22 call sites — exec's stdio, checkpoint restore, the memory file — so a
transport without this cannot replace it at all.
Refuses rather than half-delivering: an empty body would let the kernel discard
a control-only message and lose the descriptors while the call reports success,
and a non-unix connection would deliver the bytes without the files.
Co-authored-by: Hanzo Dev <[email protected]>
A plugin host is where a service becomes an arbitrary-code-execution vector:
whatever it starts runs with the host's authority — verifying blocks, signing,
serving. Sum existed but only gated URL, so the case that looked dangerous was
checked and the case that is was not. A binary read off disk is not more
trustworthy than a downloaded one; it is the same bytes with the download
already done by somebody else.
Now every source takes the same path, and the check is worth something because
what runs is what was checked:
- The binary is COPIED into a directory only the host can write, and the copy
is hashed and executed. Verifying in place and then running leaves whoever
can write that file free to change it afterwards, and starting a plugin is
an observable event, so they get to choose when.
- Holding a descriptor to the original would not have closed that, which the
test proves rather than assumes: a rename swaps the inode, but a WRITE keeps
it, and a write is visible through an already-open descriptor. Only the copy
puts the bytes out of reach.
- The copy is executed through its own descriptor (/proc/self/fd on Linux), so
no path is resolved a second time. It is reopened read-only first — Linux
refuses to execute a file anyone holds open for writing.
- A mismatch refuses. A host that logs and carries on has performed the check
and kept the risk.
What this cannot decide is where a trustworthy Sum comes from. A digest in the
same config an attacker just edited proves nothing, so the anchor has to be
something they do not control. For a network that already agrees on things by
construction, that is consensus.
The registry a projection reduces over came from Go declarations, so zipgen —
which links no service — had an empty one, and every SDK, MCP server, CLI and
docs page it wrote was empty. It refused rather than write those, and named a
missing connection: a .zap schema, read into a registry.
zip.ReadZAP is that connection. It parses with github.com/zap-proto/go/idl,
which is the parser cmd/zapgen has always used and which is a package rather
than a command as of that repository's v1.8.3; there is one grammar, not a
second one written here to agree with the first until it did not. It answers
with one app per declared interface, each carrying one op per method, which is
the exact inverse of ZAPSchema taking any number of apps and writing them as one
interface each. A file read and projected back is the same text, and there is a
test that closes that loop over this package's own output.
Two things stood between a declaration and an op. Go makes no named types at run
time, so a struct built from a declaration carries the name it was declared under
in a decl tag on its fields, and typeName is now the one place any projection
asks a type for its name — without that, Ping and Pong of identical shape intern
to a single reflect type and a method declared to answer Pong answers Ping. And
a .zap field states an offset while LayoutOf derives one: every field is
compared and any disagreement is refused, naming all of them. That refusal earns
its place immediately, because the IDL assigns offsets by packing and this
layout aligns each slot to its own width, so a hand-written schema with an i32
before a wider field describes a wire this process does not speak.
An op read from a schema knows what it takes and what it answers and refuses to
run, because a schema states a contract and the implementation of it is
somewhere else.
Co-authored-by: Hanzo Dev <[email protected]>
zipgen builds its app from flags alone, so the typed-op registry is empty and
every projection of it is empty too: an SDK with no methods, an MCP server
with no tools, a docs page whose operations table has no rows. Each of those
was written out and reported as a success.
It now refuses, and says what is missing. A registry comes from Go
declarations today; reading one from a .zap schema, which zap-proto/go/idl
already parses, is the connection that does not exist yet.
Co-authored-by: Hanzo Dev <[email protected]>
ZAP decides what the bytes mean, so a second hand-written codec in a
second language is a second answer to what a frame is. The C++ chains
had two of them — one per chain, already drifted — because zip could
emit an RPC client SDK from a schema but not the runtime that SDK reads
and writes bytes with.
CppZap renders that runtime: the reader, the builder, and the arithmetic,
in one namespace-parameterised C++23 header. Its constants are imported
from github.com/luxfi/zap rather than retyped, and its accessors are that
runtime name for name, so the two can be checked against each other line
by line.
The promise it makes is the bytes, and the promise is a test result:
TestCppZapKeepsTheWire compiles the emitted header and diffs its buffers
against the Go runtime's over seventeen shapes — scalars, tails, growth,
backward pointers, every list kind, flags, both versions — and
TestCppZapReadsWhatGoWrote closes the loop the other way.
SocketMode reached only the http:// transport. A bare address is ZAP —
so the canonical SocketPath socket every plugin and peer is reached on
went through zap-proto/http.Server, which takes an address, calls
net.Listen, and never learns what mode was asked for. net.Listen creates
a socket 0777 masked by the process umask, so that socket came up 0755
under the usual 022 whether the config said 0600 or 0666: a server that
meant to shut the door was serving anyone on the host, and one that
meant to open it to an attested peer was refusing them.
A socket carries a mode only because something chmods it, and now one
function does. bindSocket clears a stale socket, binds, and sets the
mode, and both transports bind through it, so the fasthttp spelling that
was already doing this for http:// and the ZAP one that was doing
nothing become the same fact. The umask is the alternative and it is the
wrong instrument: it is process-wide, so narrowing it around one bind
changes the answer for every file the program writes. The window between
bind and chmod is closed by the directory makeSocketDir creates 0700.
An abstract socket is a name in the kernel rather than a file, so it has
no leftover to clear and no mode to carry, and takes neither.
Co-authored-by: Hanzo Dev <[email protected]>
The hook answered allow-or-error, so an operation that needs a person to
sign it off had nowhere to go but a refusal. A caller told 403 stops; a
caller told the request is held waits, and those are different futures.
An Authorizer now answers a Decision — Allow, Deny or Approve, with the
clause that reached it and the reason a person reads — resolved ONCE in
the run() contract that op.invoke and op.direct both funnel through. The
projections only render it, so all six agree without a branch in any of
them: REST answers 403 or 202, the call plane the same two over ZAP, MCP
carries the held body as content rather than an error because nothing
failed, the CLI prints it, the graph reports it beside the field it was
selected for, and a Go caller reads it off the error lane with HeldOf.
An Approval has no Out and never will have one — the handler did not run
— which is the whole reason it does not travel the value lane there.
Clause, not rule. This package already spells an Authorizer "the rule"
— App.rule, adopt, "two rules that disagree" — so the token naming which
part of it fired needs its own word, and a rule has clauses.
The error return survives for a genuine failure of the CHECK: an
authority that cannot be reached has not decided anything, and answering
503 rather than 403 is what lets a client retry.
The op carries the question rather than the app, because they differ. A
composed child answers to whatever included it, and a mounted op has no
local rule at all — so the document publishes the held body exactly where
the seam would produce one, and a gated service's OpenAPI reads as the
pair a generated client needs.
Co-authored-by: Hanzo Dev <[email protected]>
- Add App.RustSDK() for full ZAP-native self-documenting Rust SDK with rustdoc
- Add App.CppSDK() for modern C++20 self-documenting SDK with Doxygen
- Add App.RustMCP() and App.CppMCP() for native Model Context Protocol tools & servers
- Add App.DocsMarkdown() for @hanzo/docs MDX pages with Go/Rust/C++ tabbed snippets
- Add cmd/zipgen CLI tool for sdk, mcp, and docs generation
A server that scopes every read by what the kernel says about the peer
needs two things the 0600 socket used to do for it: a mode it can open,
because the attestation is now the boundary and the mode is only the
door, and a way to tell a call the process makes to itself from one that
arrived with no peer to read.
SO_PEERCRED names the process; the kubelet's cgroup names the pod it
was placed in. A server on a socket shared across pods needs the second
to scope what the first may read, and neither is a value the caller
states.
The reflective encoder refuses bytes_fixed[N] — an ids.ID is [32]byte — so that
an id forces its type to state its own wire. The SDK read that refusal as a fact
about the op and reported a gap. It is a fact about the ENCODER: the layout is
right, and Codecs already writes the two methods that carry an id inline from
those same offsets.
So the SDK writes one. Not a second emitter — the same one, asked for names
through a seam, because two emitters would be two wires and the second would be
spoken by nobody. The restatement is the declared struct field for field and in
order, so its layout IS the declared layout; the codec states the declared
offsets against this package's names.
Which types get one is not all of them. Wire is checked at the root of Marshal
and Unmarshal only, so a parent encoded reflectively reflects over its children
too. That makes the set closed upward, and a codec calling MarshalZAP on what it
nests makes it closed downward. One id under an op's In or Out states the wire
for that tree; a tree with no id keeps the derived wire it had, and its
generated source does not move.
Two guards, because a wrong offset is worse than a gap: the restatement is
checked against the layout it claims — same fields, same order — and a mismatch
is a gap, never a guessed layout; and spell preserves reflect.Kind, which is
what makes the widths identical.
Over node's nine apps: 43 of 94 ops reachable in Go, now 91. The three left are
an interface, which names no type. Every op is still a method XOR a gap. The
document, the tool list, the command tree and both HTTP clients are byte-
identical across the change.
This projection promises that an op whose value cannot cross gets no
method, because a client that compiles and lies is worse than one that
says the operation is out of reach. Three ways it broke that promise,
all found generating clients for node's ninety-four ops:
A field whose own type had no wire form was replaced by struct{} and the
op kept its method. The call succeeded and the payload was gone. node's
admin, info and platformvm all had []struct{} where a value should be.
A field that cannot cross now refuses the type holding it, which refuses
the op, reported once at the field.
On an EMBEDDED field that substitute is not Go — an embedded field must
be a name — so the package did not parse, SDK returned an error, and the
service got no client at all. One op of xvm's ten has that shape.
And the memo that remembers a refused type answered "no" and said
nothing, so the SECOND op reaching it had no method and no gap; the only
way to notice was to count. platformvm's get_tx_rewards vanished exactly
that way. The refusal is remembered with its reason and reported again
against each op that meets it.
Two causes join the vocabulary: nested, and embedded.
Co-authored-by: Hanzo Dev <[email protected]>
A URL carries a segment percent-encoded — it has to: a space, a slash, a
percent or anything outside the unreserved set has no other spelling
between two slashes. The router matched on the raw text and handed that
same text on, so a handler addressed at /v1/secrets/café was given
"caf%C3%A9" and looked up a name nobody has.
Measured against a live listener before this: every path parameter
carrying such a character arrived encoded, in the typed binder and in
Ctx.Param alike. Every client encodes, because there is no other way to
write those characters, so no client could make such a value round-trip
and the address was unusable for anything but a bare identifier.
Text that is not valid encoding is passed through rather than refused: a
lone percent is a malformed URI, but the address it names is still the
address the router matched, and failing here would turn a lookup that
finds nothing into a 500.
Found by running a generated HTTP client against a real service.
Co-authored-by: Hanzo Dev <[email protected]>
The generated codec two directions disagreed about a POINTER to a fixed array.
The writer guarded the nil and wrote nothing; the reader emitted copy(x.F[:], …)
regardless — through a pointer on a value that had just been allocated, so it
was always nil. Every read of a type carrying an optional id panicked at the far
end of the plane, which is where a panic is a dropped connection and not a
stack trace anyone sees.
The P-Chain L1 validator is exactly that shape: validationID is a *ids.ID, so
get_validator and the L1 arm of get_validators could not be read at all.
An inline slot has no null — the writer leaves an absent pointer bytes zeroed —
so all-zero is what absence looks like coming back. That is already the rule the
pointer SCALARS read by (a zero u64 reads as absent), and reading it here is what
makes the two directions symmetric rather than adding a second convention.
The documentation map is one map for a whole process, keyed by an address —
and an address is unique only within the app that serves it. A node runs twelve
chains on one binary and the P-Chain and the X-Chain both answer GET /height,
GET /block, GET /tx, GET /utxos, GET /balance and POST /tx: six addresses where
whichever init() ran last published its prose for the other chain routes.
Nothing failed. The document and the MCP tool carried the wrong sentence for the
right route, which is why it survived every test until someone read the output —
and renaming a path to dodge it would have been the wrong repair twice over,
since the mount already tells the chains apart and every future chain would
inherit the dodge.
So the key carries the declaring package, read off the handler itself at
registration. It is the namespacing the schema registry already does for named
types: who declared a thing is a property of the code. An unqualified key is
still read, so a generated file written before this is still correct for the
single-app process it was written for.
queryOf was the third copy of one rule and it had drifted the same way the
document had. It rendered a JSON object one level deep and verbatim, so a list
went out as ?ids=["a","b"] and a record as ?start={"utxo":...} — neither of
which bindURL reads. The CLI and every caller built on Remote sent arguments
that arrived empty, and the op answered 200 about nothing.
It is now the binder inverse: a list is comma-joined, a record leaves are named
through it, an absent value is left out rather than written as the four
characters of null. Exported as Query, because a client should not have to know
any of that — the spelling is derived from the op own In, which is the point of
having the In.
The test is a ROUND TRIP through a live op rather than an assertion about a
string, so a change to either half that breaks the other fails here.
A written form is usually carried by a NUMERIC kind, and deciding by kind first
put the rule out of reach of exactly the values that need it. A P-Chain height
is a u64 whose reserved value is spelled "proposed"; an encoding is a u8 spelled
"json". Both met ParseUint, both failed, and both left the field at zero — so a
request for the proposed height was served as height 0 and a request for JSON
was served as hex, each with a 200 on the answer.
Asking the type first is also encoding/json own precedence: a named string with
an UnmarshalText is decoded through it and not by assignment. Matching that is
what lets a URL and a JSON body on the same struct read one word one way, rather
than a field meaning two things depending on which side it arrived from.
So a type that states how to read itself from text gets that reading, and what
is left is decided by kind. The default branch goes with it: nothing reaches the
switch that has a written form to ask about.
A pagination cursor is one field holding two — an address and a utxo id — and a
URL had no spelling for it, so a read whose input nests a record could not be
served on a safe method at all. The choice was a GET that silently dropped the
cursor or a POST, and a POST is a read that has to be authorized.
A leaf is written under its record's name: `?startIndex.utxo=`. Only under it —
a bare `?utxo=` still binds a top-level utxo and never a nested one, so an input
that nests a record names its target explicitly and an authorizer reads the
target the caller wrote rather than one guessed out of a sub-struct.
urlFields walks the same way, so the document publishes every dotted name the
binder fills and no name it does not. A struct with a written form of its own is
not a record here: a time and an id are one word in a URL, and urlSchema already
says so.
A pointer record is allocated only where the URL named something inside it, so a
cursor nobody sent stays absent rather than arriving empty. The descent stops at
the keys a caller wrote, which is what bounds it on a self-referential input.
Two halves of one rule, and the second is what made the first safe to rely on.
A URL carries one value per name, so a repeated argument is written as one value
with commas — style: form, explode: false, the spelling the document already
publishes for it. The binder had no case for a slice, so an op taking a list of
ids answered 200 about an empty list. Each element is written by setScalar, so a
list of ids says nothing about ids a second time, and a []byte is not a list
here: encoding/json spells it as one base64 string.
urlFields had the kind switch written a SECOND time, and the second one drifted
the moment setText taught the binder to read an id. The binder filled a
parameter the document did not name, which is the same disagreement as before
with the sides swapped — a caller reading the document had no way to know the
argument existed. Both now read urlSchema, so what is published and what is
bound are one answer. A type that reads itself from text is a string there,
whatever it is made of: describing an id by its 32 bytes published an array of
integers for a value nothing spells that way.
An id is an array of bytes and a URL carries characters, so the binder's kind
switch had nothing to convert into: a route addressing a resource by its id
published a query parameter, bound nothing, and ran the handler on a zero id.
A wrong answer with a 200 on it.
The type had already answered the question. encoding.TextUnmarshaler is
precisely "here is how to read me from the characters a URL can carry", so the
binder asks, for the kinds nothing else could carry. The basic kinds are decided
first, so a named string that also reads text keeps the reading it has always
had, and what is left after both is a record rather than a value — a slice, a
map, a struct with no text form — which stays untouched.
Reading is all-or-nothing: the value is built beside the field and moves onto it
on success only, the same contract the numeric cases keep.
A generator writing into one module reads types from several. The ones it
cannot write to are only a problem while they are still silent, so a Codec
now reports whether every type in it already states its wire — which is
what tells "another module has to release" from "another module already
did".
A generator cannot know which identifiers a package has free, and
github.com/luxfi/utxo imports a package called wire. So the assertion is
an inline interface literal, and the two helpers a codec needed are now
block-local: the empty object builds where it is written, and a narrow
list element is zero-extended in the loop that reads it.
Same bytes, held against the reflective encoder as before.
An EmptyReply has no slot, so its decoder read nothing and Go refused the
root it had taken and never used. Take the root only where a field reads
it; keep the parse either way, because an unreadable message is an error
and not an empty value.
Two shapes the fleet's own types hold that the fixtures did not. A
*[]PrimaryDelegator is an optional list — absent means the caller did not
ask for delegators, and its length cannot be asked for through a nil
pointer. And a message whose only field is an id spells no type name at
all, so taking the import for it left one unused and the file would not
build.
Both held byte for byte against the reflective encoder.