2 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
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]>
2026-09-17 01:39:17 -07:00
zeekayandHanzo Dev 478e06d1d4 Authorization can hold an op, not only allow or refuse it
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]>
2026-09-04 00:16:37 -07:00