docs: say precisely why the two registrars that project nothing cannot

app.Module registers a route and no op, so an extension endpoint has a JSON
contract that reaches no document, no tool list, no CLI and no call plane. That
hole is now stated at the seam instead of discovered, along with the two
structural reasons it cannot close today — a module is loaded at run time, so
there is no Go type to derive a schema from, and a typed op cannot express the
module's status/headers response envelope — and the one thing that would close
it: an extension that DECLARES its contract on runtime.Module.

zaprpc's HTTPHandler doc showed `app.ZAPRegistry()`, a method that has never
existed in this module (the only implementation was in the dead hanzoai/zip
fork), and justified its fiber.Handler return with an import cycle that does not
exist — zip does not import zaprpc. Both are replaced with an example that
works.

The package is marked Deprecated and says what to use instead. It is NOT what
hanzoai/cloud's service plane imports: cloud imports github.com/zap-proto/go/rpc
under the local alias `zaprpc`, which is a different package in a different
module — u32 ordinals and promise pipelining against this one's 24-byte
string-keyed envelope, mutually undecodable. Nothing in the estate imports
zip/zaprpc, including zip. Deleting it is owed, but removing an exported package
from a public module is a breaking change and does not ride a patch.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-28 16:38:08 -07:00
co-authored by Hanzo Dev
parent 72353f9d03
commit 572aeb8750
3 changed files with 68 additions and 16 deletions
+32
View File
@@ -51,6 +51,38 @@ type moduleResponse struct {
//
// The extension's exported function name is inferred from the path's last
// non-{param} segment, lowercased (e.g. "POST /v1/policy/eval" → "eval").
//
// # A module route registers a ROUTE and no op, and cannot register one
//
// So it is invisible to every projection: it is in no OpenAPI document, is no
// MCP tool, and no service can reach it with zip.Call. That is a real hole —
// the envelope in and out of a module IS a JSON contract — and it is stated
// here rather than left to be discovered, because the two reasons it cannot
// close today are both structural:
//
// 1. THERE IS NO TYPE TO DERIVE A SCHEMA FROM. A typed op's schema comes from
// its In/Out Go types. A module is LOADED at run time out of a directory —
// that is the whole point of an extension — so at the moment this route is
// registered zip holds a [github.com/zap-proto/zip/runtime.Module]
// (Name/Runtime/Exports/Invoke) and no declaration of what the module
// accepts or returns. Registering it as an open object would put the path
// in the document while saying nothing about the contract, which is not
// the same thing as documenting it.
// 2. A TYPED OP CANNOT EXPRESS THE MODULE'S RESPONSE. A module answers with a
// moduleResponse — status, headers, body — and modules use it (a redirect,
// a 404, a non-JSON body). A typed op returns Out and zip decides the
// status, so routing this through registerTyped would silently drop a
// capability the envelope exists to provide.
//
// What closes it is (1): an extension that DECLARES its contract, via a
// manifest schema surfaced on [github.com/zap-proto/zip/runtime.Module]. The op
// would then carry the module's own schema and every projection follows, with
// the route keeping the envelope it has now. Until an extension declares one
// there is nothing to project, and inventing a shape for it here would be a
// second source of truth for a contract zip does not own.
//
// A route whose contract IS known belongs in a typed op — declare it with
// [Get]/[Post] and let the module be an implementation detail behind it.
func (a *App) Module(methodPath, runtimeName, modulePath string) error {
method, path, fn, err := parseModuleSpec(methodPath)
if err != nil {
+16 -7
View File
@@ -14,15 +14,24 @@ const (
HeaderMethod = "X-ZAP-Method"
)
// HTTPHandler returns a fiber.Handler that serves the ZAP RPC plane over
// HTTP POST. The request body is a canonical ZAP envelope (service,
// method, payload); the handler decodes it, dispatches through reg, and
// writes a ZAP response envelope. Returns fiber.Handler (not zip.Handler)
// because zaprpc is imported by zip — depending back would cycle.
// HTTPHandler returns a fiber.Handler that serves this package's RPC plane over
// HTTP POST. The request body is a zaprpc envelope (service, method, payload);
// the handler decodes it, dispatches through reg, and writes a zaprpc response
// envelope.
//
// Mount it on any zip/Fiber router:
// The registry is the caller's — zip has no such thing. An App's registry is its
// typed ops, which are reached as routes, as an OpenAPI document, as MCP tools
// and by name over zip.Call; none of them is a zaprpc.Registry and there is no
// adapter between them. (This doc used to show `app.ZAPRegistry()`, a method
// that has never existed in this module.)
//
// app.Fiber().Post("/zap", zaprpc.HTTPHandler(app.ZAPRegistry()))
// reg := zaprpc.NewRegistry()
// reg.Register(validateService{}) // your zapc-generated <svc>_server.go
// app.Fiber().Post("/zap", zaprpc.HTTPHandler(reg))
//
// It returns a fiber.Handler rather than a zip.Handler because this package
// does not import zip — the dependency runs one way, and mounting is the
// caller's business.
func HTTPHandler(reg *Registry) fiber.Handler {
return func(c fiber.Ctx) error {
body := c.Body()
+20 -9
View File
@@ -1,13 +1,24 @@
// Package zaprpc is an OPTIONAL named-service RPC dispatch helper for zip
// apps that want a Cap'n-Proto/gRPC-style service registry (name → method →
// handler) rather than plain REST routes.
// Package zaprpc is an OPTIONAL named-service RPC dispatch helper: a
// Cap'n-Proto/gRPC-style service registry (name → method → handler) with its own
// envelope, mounted as an ordinary route via [HTTPHandler].
//
// It is DECOUPLED from the transport: zip's primary transport is ZAP, wired
// once in the framework (App.Listen serves the whole fiber handler over
// zap-proto/http — the routes ARE the ZAP surface, no registry needed). This
// package is for the separate case where you want to expose generated
// zapc <svc>_server.go services by name; mount it as an ordinary route with
// zaprpc.HTTPHandler(registry) (see http.go), reachable over either transport.
// # Deprecated: it is neither zip's call plane nor ZAP's wire format
//
// Nothing in zip uses it, and nothing needs to:
//
// - To reach another service, use zip's op-call plane — zip.DialApp + zip.Call
// address an op by its operation id, typed both ways, over whatever
// transport the callee is listening on. That plane is a projection of the
// typed-op registry, so it cannot drift from the routes, the document or the
// MCP tools.
// - For the ZAP wire format itself, use github.com/zap-proto/go/rpc, which
// owns it. This package's envelope is a PARALLEL one that predates that and
// does not interoperate with it: 24-byte header, service and method as
// strings, no promise pipelining, against upstream's u32 ordinals. A peer
// speaking one cannot decode the other.
//
// It stays here only so removing an exported package is a decision a release
// makes deliberately rather than a patch makes silently. Do not build on it.
package zaprpc
import (