From b27a5b41a702d70d77a8a8e653d02fed03215f1d Mon Sep 17 00:00:00 2001 From: intern0 Date: Fri, 12 Jun 2026 20:41:56 +0200 Subject: [PATCH 1/2] lib: doc-comment exported behavioral symbols Add minimal Go doc comments to exported funcs, methods, types and interfaces with non-obvious behavior across the lib/* public SDK packages, per .ai/comments.md. Comments-only, no logic changes; symbols whose intent is obvious from their signature were deliberately left uncommented (see the task skip log). Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/apphost/conn.go | 4 ++++ lib/apphost/host.go | 2 ++ lib/apphost/router.go | 8 ++++++++ lib/apps/handler.go | 2 ++ lib/apps/object_describer.go | 2 ++ lib/apps/object_finder.go | 2 ++ lib/apps/object_searcher.go | 2 ++ lib/apps/pending_query.go | 1 + lib/apps/registrar.go | 8 +++++++- lib/apps/serve.go | 1 + lib/arl/arl.go | 2 ++ lib/astrald/client.go | 5 +++++ lib/astrald/conn_monitor.go | 2 ++ lib/astrald/context.go | 2 ++ lib/astrald/gate_router.go | 2 ++ lib/astrald/retry_router.go | 5 +++++ lib/astrald/router.go | 2 ++ lib/ipc/conn.go | 1 + lib/ipc/ipc.go | 3 +++ lib/query/args_to_map.go | 3 +++ lib/query/conn.go | 1 + lib/query/editor.go | 8 ++++++++ lib/query/field_tag.go | 2 ++ lib/query/query.go | 1 + lib/query/route.go | 2 ++ lib/routing/app.go | 7 +++++++ lib/routing/conn.go | 1 + lib/routing/incoming_query.go | 3 +++ lib/routing/nil_router.go | 2 ++ lib/routing/op.go | 4 +++- lib/routing/op_router.go | 4 ++++ lib/routing/priority_router.go | 6 ++++++ lib/routing/scope_router.go | 21 +++++++++++++++++++++ 33 files changed, 119 insertions(+), 2 deletions(-) diff --git a/lib/apphost/conn.go b/lib/apphost/conn.go index 91f1e441d..7650f1aaa 100644 --- a/lib/apphost/conn.go +++ b/lib/apphost/conn.go @@ -6,6 +6,8 @@ import ( "github.com/cryptopunkscc/astrald/astral" ) +// Conn wraps a net.Conn with the astral Query that opened it and the direction +// of the connection; direction determines which identity is local vs remote. type Conn struct { net.Conn query *astral.Query @@ -37,6 +39,8 @@ func (conn Conn) LocalIdentity() *astral.Identity { return conn.query.Target } +// RemoteAddr overrides net.Conn.RemoteAddr to return the peer's astral identity +// as the address rather than a TCP/socket address. func (conn Conn) RemoteAddr() net.Addr { return Addr{address: conn.RemoteIdentity().String()} } diff --git a/lib/apphost/host.go b/lib/apphost/host.go index 575589219..db2071775 100644 --- a/lib/apphost/host.go +++ b/lib/apphost/host.go @@ -9,6 +9,8 @@ import ( "github.com/cryptopunkscc/astrald/mod/apphost" ) +// Host represents an authenticated session with an apphost node, providing +// message framing via an embedded Channel and query routing over the IPC conn. type Host struct { *channel.Channel conn *ipc.Conn diff --git a/lib/apphost/router.go b/lib/apphost/router.go index 3f7c7e6a3..193e1c5a6 100644 --- a/lib/apphost/router.go +++ b/lib/apphost/router.go @@ -10,6 +10,8 @@ import ( "github.com/cryptopunkscc/astrald/mod/apphost" ) +// Router manages connections to an apphost endpoint, caching resolved identities +// across calls and handling context-driven query cancellation. type Router struct { endpoint string token string @@ -23,6 +25,8 @@ func NewRouter(endpoint string, token string) *Router { return &Router{endpoint: endpoint, token: token} } +// DefaultRouter returns the package-level Router initialised from DefaultEndpoint +// and the AuthTokenEnv environment variable. func DefaultRouter() *Router { return defaultRouter } @@ -67,6 +71,8 @@ func (router *Router) RouteQuery(ctx *astral.Context, q *astral.InFlightQuery) ( return host.RouteQuery(q, ctx.Zone(), ctx.Filters()) } +// GuestID returns the authenticated guest identity, connecting to the host to +// resolve it on first call; returns nil if the connection or auth fails. func (router *Router) GuestID() *astral.Identity { if router.guestID != nil { return router.guestID @@ -81,6 +87,8 @@ func (router *Router) GuestID() *astral.Identity { return router.guestID } +// HostID returns the host node's identity, connecting to resolve it on first +// call; returns nil if the connection fails. func (router *Router) HostID() *astral.Identity { if router.hostID != nil { return router.hostID diff --git a/lib/apps/handler.go b/lib/apps/handler.go index 37c5a37fd..601eeb187 100644 --- a/lib/apps/handler.go +++ b/lib/apps/handler.go @@ -14,6 +14,8 @@ import ( "github.com/cryptopunkscc/astrald/mod/apphost" ) +// Handler accepts inbound IPC queries from an apphost-registered endpoint. +// Close or context cancellation terminates all blocking calls. type Handler struct { listener net.Listener ipcToken astral.Nonce diff --git a/lib/apps/object_describer.go b/lib/apps/object_describer.go index dc75dc563..49a71d6d4 100644 --- a/lib/apps/object_describer.go +++ b/lib/apps/object_describer.go @@ -22,6 +22,8 @@ type objectDescribeArgs struct { Out string `query:"optional"` } +// WithObjectDescriber mounts the objects.describe IPC op and registers the app as a Describer with the node. +// All provided describers are fanned out concurrently per describe request. func WithObjectDescriber(describers ...objects.Describer) ServeOption { return func(cfg *serveConfig) error { if len(describers) == 0 { diff --git a/lib/apps/object_finder.go b/lib/apps/object_finder.go index babcd0700..4866c26f5 100644 --- a/lib/apps/object_finder.go +++ b/lib/apps/object_finder.go @@ -22,6 +22,8 @@ type objectFindArgs struct { Out string `query:"optional"` } +// WithObjectFinder mounts the objects.find IPC op and registers the app as a Finder with the node. +// All provided finders are fanned out concurrently per find request. func WithObjectFinder(finders ...objects.Finder) ServeOption { return func(cfg *serveConfig) error { if len(finders) == 0 { diff --git a/lib/apps/object_searcher.go b/lib/apps/object_searcher.go index fb590c0bd..d4972e5bb 100644 --- a/lib/apps/object_searcher.go +++ b/lib/apps/object_searcher.go @@ -22,6 +22,8 @@ type objectSearchArgs struct { Out string `query:"optional"` } +// WithObjectSearcher mounts the objects.search IPC op and registers the app as a Searcher with the node. +// All provided searchers are fanned out concurrently per search request. func WithObjectSearcher(searchers ...objects.Searcher) ServeOption { return func(cfg *serveConfig) error { if len(searchers) == 0 { diff --git a/lib/apps/pending_query.go b/lib/apps/pending_query.go index 9d32a3e76..131dc0f1c 100644 --- a/lib/apps/pending_query.go +++ b/lib/apps/pending_query.go @@ -9,6 +9,7 @@ import ( "github.com/cryptopunkscc/astrald/mod/apphost" ) +// PendingQuery holds an unresolved inbound query; exactly one of Accept, Reject, RejectWithCode, Skip, or Close must be called. type PendingQuery struct { conn net.Conn query *astral.Query diff --git a/lib/apps/registrar.go b/lib/apps/registrar.go index 312e750c9..96f1c3135 100644 --- a/lib/apps/registrar.go +++ b/lib/apps/registrar.go @@ -31,8 +31,12 @@ type regRequest struct { done chan error } +// RegistrationHook is called after every successful (re)connect and handler re-registration. +// Errors abort the reconnect cycle and trigger a reconnect attempt. type RegistrationHook func(ctx *astral.Context) error +// RegistrationHookRegistrar is an optional extension of Registrar for components that support +// post-registration lifecycle hooks. type RegistrationHookRegistrar interface { AddRegistrationHooks(hooks ...RegistrationHook) } @@ -72,6 +76,8 @@ func WithEvents(e AppRegistrarEvents) AppRegistrarOption { return func(s *AppRegistrar) { s.events = e } } +// WithRegistrarRegistrationHooks adds RegistrationHooks at construction time. +// Use WithRegistrationHooks (a ServeOption) to add hooks via Serve/ServeWith instead. func WithRegistrarRegistrationHooks(hooks ...RegistrationHook) AppRegistrarOption { return func(s *AppRegistrar) { s.AddRegistrationHooks(hooks...) } } @@ -93,7 +99,7 @@ func NewAppRegistrar(ctx *astral.Context, opts ...AppRegistrarOption) *AppRegist return s } -// NewDefaultAppRegistrar creates an AppRegistrar with default options and starts its run loop. +// NewDefaultAppRegistrar is an alias for NewAppRegistrar; prefer NewAppRegistrar directly. func NewDefaultAppRegistrar(ctx *astral.Context, opts ...AppRegistrarOption) *AppRegistrar { return NewAppRegistrar(ctx, opts...) } diff --git a/lib/apps/serve.go b/lib/apps/serve.go index 3da92323a..6ce8763c1 100644 --- a/lib/apps/serve.go +++ b/lib/apps/serve.go @@ -82,6 +82,7 @@ func WithRegistrationHook(hook RegistrationHook) ServeOption { return WithRegistrationHooks(hook) } +// WithRegistrationHooks adds hooks that run after each successful (re)registration with the node. func WithRegistrationHooks(hooks ...RegistrationHook) ServeOption { return func(cfg *serveConfig) error { for _, hook := range hooks { diff --git a/lib/arl/arl.go b/lib/arl/arl.go index ddbbc6133..56142caf6 100644 --- a/lib/arl/arl.go +++ b/lib/arl/arl.go @@ -22,6 +22,7 @@ func New(caller *astral.Identity, target *astral.Identity, query string) *ARL { return &ARL{Caller: caller, Target: target, Query: query} } +// Split parses a raw ARL string of the form [caller@][target:]query into its three components. func Split(s string) (caller, target, query string) { matches := callerExp.FindStringSubmatch(s) if len(matches) > 0 { @@ -40,6 +41,7 @@ func Split(s string) (caller, target, query string) { return } +// Parse parses an ARL string (with or without the astral:// scheme) into an ARL; if resolver is non-nil it is used to resolve identity strings, otherwise raw key parsing is attempted. func Parse(s string, resolver dir.Resolver) (arl *ARL, err error) { if after, found := strings.CutPrefix(s, "astral://"); found { s = after diff --git a/lib/astrald/client.go b/lib/astrald/client.go index a8eb45d9a..a089f6030 100644 --- a/lib/astrald/client.go +++ b/lib/astrald/client.go @@ -7,6 +7,8 @@ import ( "github.com/cryptopunkscc/astrald/lib/query" ) +// Client wraps a Router with an optional fixed target identity, used to direct queries +// without the caller having to supply the target on every call. type Client struct { Router targetID *astral.Identity @@ -18,6 +20,7 @@ func New(router Router) *Client { return &Client{Router: router} } +// Default returns the package-level client, initialising it from libapphost.DefaultRouter on first call. func Default() *Client { if defaultClient == nil { defaultClient = New(libapphost.DefaultRouter()) @@ -30,6 +33,7 @@ func SetDefault(client *Client) { defaultClient = client } +// Query routes an outbound query to client.targetID using the client's own guest identity as the caller. func (client *Client) Query(ctx *astral.Context, method string, args any) (astral.Conn, error) { return client.RouteQuery(ctx, astral.Launch(query.New(client.GuestID(), client.targetID, method, args))) } @@ -51,6 +55,7 @@ func QueryChannel(ctx *astral.Context, method string, args any, cfg ...channel.C return Default().QueryChannel(ctx, method, args, cfg...) } +// WithTarget returns a shallow copy of the client with the target identity set; the original is not modified. func (client *Client) WithTarget(identity *astral.Identity) *Client { c := *client c.targetID = identity diff --git a/lib/astrald/conn_monitor.go b/lib/astrald/conn_monitor.go index 7aaac8428..275a63370 100644 --- a/lib/astrald/conn_monitor.go +++ b/lib/astrald/conn_monitor.go @@ -6,6 +6,8 @@ import ( "github.com/cryptopunkscc/astrald/astral" ) +// ConnMonitor wraps an astral.Conn to track transferred bytes and fire optional callbacks on close or I/O errors. +// Callbacks are invoked synchronously at the point of the error or close call. type ConnMonitor struct { OnClose func() OnReadError func(error) diff --git a/lib/astrald/context.go b/lib/astrald/context.go index 0204ab5fb..df7e5ef8f 100644 --- a/lib/astrald/context.go +++ b/lib/astrald/context.go @@ -2,6 +2,8 @@ package astrald import "github.com/cryptopunkscc/astrald/astral" +// NewContext returns a context pre-populated with the default client's guest identity and ZoneAll, +// suitable for outbound queries without additional configuration. func NewContext() *astral.Context { return astral. NewContext(nil). diff --git a/lib/astrald/gate_router.go b/lib/astrald/gate_router.go index 49c58b5c6..674494ee7 100644 --- a/lib/astrald/gate_router.go +++ b/lib/astrald/gate_router.go @@ -8,6 +8,7 @@ type ReadyGate interface { Ready() <-chan struct{} } +// GateRouter wraps a Router and blocks every outbound query until the gate signals ready. type GateRouter struct { Router gate ReadyGate @@ -19,6 +20,7 @@ func NewGateRouter(r Router, g ReadyGate) *GateRouter { return &GateRouter{Router: r, gate: g} } +// RouteQuery blocks until the gate is open or ctx is cancelled, then delegates to the inner router. func (gr *GateRouter) RouteQuery(ctx *astral.Context, q *astral.InFlightQuery) (astral.Conn, error) { select { case <-gr.gate.Ready(): diff --git a/lib/astrald/retry_router.go b/lib/astrald/retry_router.go index f78bb995e..a2e103e92 100644 --- a/lib/astrald/retry_router.go +++ b/lib/astrald/retry_router.go @@ -8,6 +8,9 @@ import ( "github.com/cryptopunkscc/astrald/sig" ) +// RetryRouter wraps a Router and retries queries that fail with ErrNodeUnavailable, +// waiting between attempts according to the provided sig.Retry policy. +// maxAttempts == 0 means unlimited retries. type RetryRouter struct { Router retry *sig.Retry @@ -28,6 +31,8 @@ func NewNoRetryRouter(r Router) *RetryRouter { return &RetryRouter{Router: r, maxAttempts: 1} } +// RouteQuery retries the query on ErrNodeUnavailable; any other error is returned immediately. +// On success the retry policy is reset so the next call starts fresh. func (rr *RetryRouter) RouteQuery(ctx *astral.Context, q *astral.InFlightQuery) (astral.Conn, error) { for attempt := 0; ; attempt++ { conn, err := rr.Router.RouteQuery(ctx, q) diff --git a/lib/astrald/router.go b/lib/astrald/router.go index 28cd952e0..d276d7c14 100644 --- a/lib/astrald/router.go +++ b/lib/astrald/router.go @@ -5,6 +5,8 @@ import ( "github.com/cryptopunkscc/astrald/lib/apphost" ) +// Router is the core transport abstraction: it routes an in-flight query to its destination +// and exposes the local guest and host identities used to build queries. type Router interface { RouteQuery(*astral.Context, *astral.InFlightQuery) (astral.Conn, error) GuestID() *astral.Identity diff --git a/lib/ipc/conn.go b/lib/ipc/conn.go index e3943b579..cfa80b303 100644 --- a/lib/ipc/conn.go +++ b/lib/ipc/conn.go @@ -2,6 +2,7 @@ package ipc import "net" +// Conn wraps a net.Conn with the IPC protocol and address used to establish it. type Conn struct { net.Conn protocol string diff --git a/lib/ipc/ipc.go b/lib/ipc/ipc.go index b00887dd0..9195fdf75 100644 --- a/lib/ipc/ipc.go +++ b/lib/ipc/ipc.go @@ -23,6 +23,7 @@ func Dial(target string) (conn *Conn, err error) { return DialContext(context.Background(), target) } +// DialContext connects to target using the format "proto:addr", where proto is one of tcp, unix, memu, or memb. func DialContext(ctx context.Context, target string) (conn *Conn, err error) { parts := strings.SplitN(target, ":", 2) if len(parts) < 2 { @@ -55,6 +56,7 @@ func DialContext(ctx context.Context, target string) (conn *Conn, err error) { return } +// Listen binds to the given "proto:addr" IPC address; for unix sockets it expands "~/" and removes a stale socket file before retrying. func Listen(ipcAddress string) (net.Listener, error) { var protocol, address string @@ -100,6 +102,7 @@ func Listen(ipcAddress string) (net.Listener, error) { } } +// ListenAny opens a listener on a system-assigned ephemeral address for the given protocol, useful when the caller does not care which address is used. func ListenAny(protocol string) (net.Listener, error) { switch protocol { case "tcp": diff --git a/lib/query/args_to_map.go b/lib/query/args_to_map.go index de7d7d971..1a6665ed2 100644 --- a/lib/query/args_to_map.go +++ b/lib/query/args_to_map.go @@ -2,6 +2,9 @@ package query import "strings" +// ArgsToMap converts a flag-style argument slice to a map. Arguments prefixed +// with "-" are treated as named keys consuming the next element as their value; +// unprefixed arguments are stored under DefaultArgKey. func ArgsToMap(args []string) (params map[string]string) { params = make(map[string]string) diff --git a/lib/query/conn.go b/lib/query/conn.go index 92023d140..988e9ee94 100644 --- a/lib/query/conn.go +++ b/lib/query/conn.go @@ -27,6 +27,7 @@ func NewConn(localID *astral.Identity, remoteID *astral.Identity, w io.WriteClos } } +// Read closes the connection on any read error, ensuring the write side is also torn down. func (s *Conn) Read(p []byte) (n int, err error) { n, err = s.Reader.Read(p) if err != nil { diff --git a/lib/query/editor.go b/lib/query/editor.go index 5a0a0dc63..e98f18021 100644 --- a/lib/query/editor.go +++ b/lib/query/editor.go @@ -36,10 +36,14 @@ func Edit(s any) *Editor { return edit(s, false) } +// EditCamel returns an Editor for s, preserving the original field name casing +// instead of converting to snake_case as Edit does. func EditCamel(args any) *Editor { return edit(args, true) } +// EditValue returns an Editor from an already-obtained reflect.Value; useful +// when the caller already holds a reflected struct pointer. func EditValue(v reflect.Value) *Editor { return editValue(v, false) } @@ -143,6 +147,8 @@ func (editor *Editor) Field(name string) (*FieldEditor, error) { return nil, ErrFieldNotFound } +// SetMany applies a map of values to the editor's fields, silently skipping +// unknown keys; returns an error only on type-conversion failures. func (editor *Editor) SetMany(vals map[string]string) error { for key, value := range vals { err := editor.Set(key, value) @@ -157,6 +163,8 @@ func (editor *Editor) SetMany(vals map[string]string) error { return nil } +// SetArgs parses a "-key value" argument slice into the editor's fields, +// returning unconsumed (non-flag) args and an error for any unknown flag. func (editor *Editor) SetArgs(args []string) (unparsed []string, err error) { var i = 0 diff --git a/lib/query/field_tag.go b/lib/query/field_tag.go index 60a47cef8..7387901bc 100644 --- a/lib/query/field_tag.go +++ b/lib/query/field_tag.go @@ -9,6 +9,8 @@ type FieldTag struct { Other map[string]string } +// ParseTag parses a semicolon-separated struct tag value of the form +// "key:;skip;required" into a FieldTag. func ParseTag(tag string) *FieldTag { var fieldTag = FieldTag{Other: make(map[string]string)} diff --git a/lib/query/query.go b/lib/query/query.go index 8e4d662bf..04570d5ed 100644 --- a/lib/query/query.go +++ b/lib/query/query.go @@ -8,6 +8,7 @@ import ( "github.com/cryptopunkscc/astrald/astral" ) +// DefaultArgKey is the map key used for positional (non-flagged) arguments. const DefaultArgKey = "arg" const maxQueryTimeout = 60 * time.Second diff --git a/lib/query/route.go b/lib/query/route.go index 7e9496c4a..f02b9e5cc 100644 --- a/lib/query/route.go +++ b/lib/query/route.go @@ -24,6 +24,8 @@ func RouteInFlight(ctx *astral.Context, r astral.Router, q *astral.InFlightQuery return NewConn(q.Caller, q.Target, target, pipeReader, true), err } +// Route routes a Query and wraps the resulting connection in a channel.Channel +// for structured framed I/O, unlike RouteInFlight which returns a raw Conn. func Route(ctx *astral.Context, r astral.Router, q *astral.Query) (*channel.Channel, error) { conn, err := RouteInFlight(ctx, r, astral.Launch(q)) if err != nil { diff --git a/lib/routing/app.go b/lib/routing/app.go index 39ff5dc19..07fe4e85a 100644 --- a/lib/routing/app.go +++ b/lib/routing/app.go @@ -10,6 +10,8 @@ import ( "github.com/cryptopunkscc/astrald/lib/query" ) +// App is a self-describing ScopeRouter that automatically exposes a ".spec" +// op listing all registered operations. type App struct { *ScopeRouter } @@ -19,6 +21,8 @@ type SpecArgs struct { Out string } +// NewApp wraps s in an OpRouter, mounts it in a ScopeRouter, and injects a +// ".spec" op that streams the full operation manifest to callers. func NewApp(s any) *App { ops := NewOpRouter() ops.AddStruct(s) @@ -30,6 +34,7 @@ func NewApp(s any) *App { return &app } +// Spec streams all OpSpec entries for the app and terminates with an EOS object. func (app *App) Spec(_ *astral.Context, query *IncomingQuery, args SpecArgs) error { ch := query.Accept(channel.WithFormats(args.In, args.Out)) defer ch.Close() @@ -48,6 +53,8 @@ func (app *App) Add(scope string, s any) { app.ScopeRouter.Add(scope, NewOpRouter(s)) } +// Run routes args[0] as an op name (with remaining args as query params) against +// the app and bridges the resulting connection to stdin/stdout. func (app *App) Run(ctx *astral.Context, args []string) error { if len(args) == 0 { return fmt.Errorf("missing command") diff --git a/lib/routing/conn.go b/lib/routing/conn.go index bd078a80b..91bc232ae 100644 --- a/lib/routing/conn.go +++ b/lib/routing/conn.go @@ -27,6 +27,7 @@ func NewConn(localID *astral.Identity, remoteID *astral.Identity, w io.WriteClos } } +// Read closes the connection on any read error to ensure cleanup propagates. func (s *Conn) Read(p []byte) (n int, err error) { n, err = s.Reader.Read(p) if err != nil { diff --git a/lib/routing/incoming_query.go b/lib/routing/incoming_query.go index e37dba755..30bede92b 100644 --- a/lib/routing/incoming_query.go +++ b/lib/routing/incoming_query.go @@ -11,6 +11,9 @@ import ( "github.com/cryptopunkscc/astrald/astral/channel" ) +// IncomingQuery is a server-side view of an in-flight query that must be +// resolved exactly once via Accept* or Reject* before the 5-second deadline +// expires or the context is cancelled. type IncomingQuery struct { *astral.Query origin string diff --git a/lib/routing/nil_router.go b/lib/routing/nil_router.go index f96550d17..14d9d7b3d 100644 --- a/lib/routing/nil_router.go +++ b/lib/routing/nil_router.go @@ -9,6 +9,8 @@ import ( var _ astral.Router = &NilRouter{} +// NilRouter is a terminal router that always rejects queries; set Soft=true to +// return ErrRouteNotFound instead, allowing upstream routers to continue trying. type NilRouter struct { Soft bool // return ErrRouteNotFound instead of ErrRejected } diff --git a/lib/routing/op.go b/lib/routing/op.go index 6a4e20a47..fb4651e27 100644 --- a/lib/routing/op.go +++ b/lib/routing/op.go @@ -78,7 +78,9 @@ func NewOp(fn any) (*Op, error) { return op, nil } -// RouteQuery routes the query directly to the op +// RouteQuery dispatches the query to the op in a new goroutine with a detached +// context; the caller blocks until the op calls Accept/Reject or the 5-second +// deadline expires. func (op *Op) RouteQuery(ctx *astral.Context, q *astral.InFlightQuery, remoteWriter io.WriteCloser) (io.WriteCloser, error) { var origin string if o, found := q.Extra.Get("origin"); found { diff --git a/lib/routing/op_router.go b/lib/routing/op_router.go index 68abb3d3b..a6f8b7915 100644 --- a/lib/routing/op_router.go +++ b/lib/routing/op_router.go @@ -12,6 +12,8 @@ import ( "github.com/cryptopunkscc/astrald/sig" ) +// OpRouter dispatches incoming queries by matching the first path segment of +// the query string against a fixed map of named Op handlers. type OpRouter struct { routes sig.Map[string, *Op] } @@ -46,6 +48,8 @@ func (router *OpRouter) AddOp(name string, op *Op) error { return nil } +// AddScopedOp rejects any non-empty scope; OpRouter is flat and cannot host +// scoped routes — use ScopeRouter to compose scoped hierarchies. func (router *OpRouter) AddScopedOp(scope string, name string, op *Op) error { if scope != "" { return errors.New("op router cannot add scoped route") diff --git a/lib/routing/priority_router.go b/lib/routing/priority_router.go index 478d002af..8b24285b2 100644 --- a/lib/routing/priority_router.go +++ b/lib/routing/priority_router.go @@ -12,6 +12,8 @@ import ( var _ astral.Router = &PriorityRouter{} +// PriorityRouter tries registered routers in ascending priority order; the +// first successful match or hard rejection (ErrRejected) wins. type PriorityRouter struct { Name string entries sig.Set[*Entry] @@ -32,6 +34,8 @@ func NewPriorityRouter(name string) *PriorityRouter { return &PriorityRouter{Name: name} } +// RouteQuery iterates entries in priority order; stops on success or +// ErrRejected, collects other errors, and falls back to ErrRouteNotFound. func (router *PriorityRouter) RouteQuery(ctx *astral.Context, q *astral.InFlightQuery, w io.WriteCloser) (rw io.WriteCloser, err error) { var errs []error @@ -50,6 +54,8 @@ func (router *PriorityRouter) RouteQuery(ctx *astral.Context, q *astral.InFlight return query.RouteNotFound() } +// Add registers a router at the given priority and re-sorts the entry set so +// iteration order matches ascending priority. func (router *PriorityRouter) Add(r astral.Router, prio int) error { router.entries.Add(&Entry{Router: r, Prio: prio}) diff --git a/lib/routing/scope_router.go b/lib/routing/scope_router.go index 8e221208b..678a4f133 100644 --- a/lib/routing/scope_router.go +++ b/lib/routing/scope_router.go @@ -10,15 +10,22 @@ import ( "github.com/cryptopunkscc/astrald/sig" ) +// ScopeRouter dispatches queries whose first path segment matches a registered +// scope name to that scope's router, stripping the prefix before forwarding; +// unmatched or unscoped queries fall through to the root router. type ScopeRouter struct { root astral.Router scopes sig.Map[string, astral.Router] } +// HasSpec is implemented by routers that can describe their operations; +// ScopeRouter uses it to aggregate specs across scopes into a flat list. type HasSpec interface { Spec() (list []OpSpec) } +// ScopedOpRouter is implemented by routers that support adding individual ops +// under an explicit scope; an empty scope targets the router's root. type ScopedOpRouter interface { AddScopedOp(scope string, name string, op *Op) error } @@ -27,6 +34,8 @@ type RouteChecker interface { HasRoute(name string) bool } +// NewScopeRouter creates a ScopeRouter backed by root; a nil root is replaced +// with a NilRouter that hard-rejects all queries. func NewScopeRouter(root astral.Router) *ScopeRouter { if root == nil { root = &NilRouter{} @@ -36,6 +45,9 @@ func NewScopeRouter(root astral.Router) *ScopeRouter { } } +// RouteQuery strips the scope prefix from the query string before forwarding +// to the matched scope's router; falls back to root when no scope matches or +// the query has no dot-separated prefix. func (r *ScopeRouter) RouteQuery(ctx *astral.Context, q *astral.InFlightQuery, w io.WriteCloser) (io.WriteCloser, error) { opName, _ := query.Parse(q.QueryString) idx := strings.IndexByte(opName, '.') @@ -66,6 +78,9 @@ func (r *ScopeRouter) Add(scope string, router astral.Router) { r.scopes.Set(scope, router) } +// AddScopedOp adds op to the root OpRouter when scope is empty, or to the +// named scope's OpRouter (creating it if absent); fails if the target router +// is not an OpRouter. func (r *ScopeRouter) AddScopedOp(scope string, name string, op *Op) error { if scope == "" { root, ok := r.root.(*OpRouter) @@ -88,6 +103,9 @@ func (r *ScopeRouter) AddScopedOp(scope string, name string, op *Op) error { return ops.AddOp(name, op) } +// HasRoute checks the appropriate scope router for dot-prefixed names, or the +// root router for unscoped names; returns false if the target router does not +// implement RouteChecker. func (r *ScopeRouter) HasRoute(name string) bool { idx := strings.IndexByte(name, '.') if idx == -1 { @@ -110,6 +128,9 @@ func (r *ScopeRouter) Remove(scope string) { r.scopes.Delete(scope) } +// Spec aggregates op specs from all scopes (prefixing each name with +// "scope.") and from the root, excluding internal ops whose names start +// with ".". func (r *ScopeRouter) Spec() (list []OpSpec) { for name, scope := range r.scopes.Clone() { r, ok := scope.(HasSpec) From 34cc88337feb533ebfab1e078a0054f3f39afa4d Mon Sep 17 00:00:00 2001 From: intern0 Date: Mon, 15 Jun 2026 14:55:44 +0200 Subject: [PATCH 2/2] Drop module-framework & client-bootstrap doc comments per review feedback Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/apphost/router.go | 2 -- lib/apps/registrar.go | 1 - lib/astrald/client.go | 1 - 3 files changed, 4 deletions(-) diff --git a/lib/apphost/router.go b/lib/apphost/router.go index 193e1c5a6..75f2393e4 100644 --- a/lib/apphost/router.go +++ b/lib/apphost/router.go @@ -25,8 +25,6 @@ func NewRouter(endpoint string, token string) *Router { return &Router{endpoint: endpoint, token: token} } -// DefaultRouter returns the package-level Router initialised from DefaultEndpoint -// and the AuthTokenEnv environment variable. func DefaultRouter() *Router { return defaultRouter } diff --git a/lib/apps/registrar.go b/lib/apps/registrar.go index 96f1c3135..ded1eaa98 100644 --- a/lib/apps/registrar.go +++ b/lib/apps/registrar.go @@ -99,7 +99,6 @@ func NewAppRegistrar(ctx *astral.Context, opts ...AppRegistrarOption) *AppRegist return s } -// NewDefaultAppRegistrar is an alias for NewAppRegistrar; prefer NewAppRegistrar directly. func NewDefaultAppRegistrar(ctx *astral.Context, opts ...AppRegistrarOption) *AppRegistrar { return NewAppRegistrar(ctx, opts...) } diff --git a/lib/astrald/client.go b/lib/astrald/client.go index a089f6030..94a92d1c9 100644 --- a/lib/astrald/client.go +++ b/lib/astrald/client.go @@ -20,7 +20,6 @@ func New(router Router) *Client { return &Client{Router: router} } -// Default returns the package-level client, initialising it from libapphost.DefaultRouter on first call. func Default() *Client { if defaultClient == nil { defaultClient = New(libapphost.DefaultRouter())