4 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 0156f40d53 A raw route's prose is reachable by its address
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]>
2026-09-17 21:16:52 -07:00
zeekay 33ebd6abca Prose is filed under the package that declared it
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.
2026-08-29 23:16:02 -07:00
zeekayandHanzo Dev 605e96dfc0 zipdoc: an UNTYPED route is an operation, and now it can say what it does
Some routes cannot become typed ops and it is the WIRE that says so, not the
author: an OIDC redirect, a JWKS document, a SCIM body RFC 7643 governs, a
multipart form, a file download, an SSE stream. They are operations a paying
caller reaches, and they had nowhere at all to state what they do — hanzoai/iam
alone publishes 98 of them, each an address followed by silence.

So the pass reads `router.Get("/path", handler)` as well, and lifts the same doc
comment from the same three shapes. Two properties keep it honest:

  * DESCRIPTION ONLY. No In, no Out, so no schemas and no field prose. This must
    never make a raw route look typed.
  * It registers no route and invents no operation. zip.Describe is keyed
    "METHOD /path" and renders only where a router already carries that address,
    so the router stays the sole authority on what EXISTS and this adds only
    what it MEANS.

The receiver decides, not the method name: *zip.App or anything satisfying
zip.Router. And a non-constant path is skipped rather than refused — the typed
case must fail the build because a computed path is a hole in the schema surface,
while refusing here would turn every raw handler in the fleet red on release day.

zip.Prose exported alongside, because the typed-op registry is not the only
reader. A host that composes several apps projects its LIVE ROUTER — typed and
untyped alike — and the untyped half has prose here and nowhere else. Without it
the host keeps a table of strings for routes it does not own, which is the exact
duplication this package exists to remove. The summary is derived inside Prose so
every projection of one operation shortens it identically.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 12:46:33 -07:00
zeekayandHanzo Dev 0ff1091763 feat: the doc comment IS the spec — descriptions, field docs and examples
A spec with no descriptions and no examples is a reference someone reads, not
one they can press "try it" on, and that gap is most of what an API portal is
for. This closes it without anyone writing anything twice.

Go does not retain doc comments at run time — reflection sees types and struct
tags, never comments — so prose can only reach the spec through a build-time
pass over the AST. That constraint is the whole design. The alternative is
WithSummary("…") sitting directly under a doc comment that says the same thing:
two places to change, one to forget.

cmd/zipdoc (next) walks a package and emits Describe calls carrying the handler's
doc comment, each In/Out field's comment, and the Example/Response bodies written
in that comment. buildOpenAPI merges them: operation description, per-field
descriptions keyed by Type.field so an In and an Out "limit" do not collide, and
request/response examples emitted as raw JSON so a malformed one is wrong loudly.

The summary falls back to the comment's first sentence, because Go's own
convention already makes that a summary — so nobody writes a summary at all.

Degrades cleanly: with no generated file the spec is exactly what it was, schemas
without prose. Nothing breaks by not running the generator; it just stays terse.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 16:27:27 -07:00