cell

package
v0.1.0-develop.2026061... Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package cell defines the core Cell and Slice abstractions for the GoCell framework: interfaces (CellIdentity / CellLifecycle / CellStatus / CellInventory composed into Cell), the BaseCell implementation, and the Registrar surface a Cell uses to declare routes / subscriptions / health probes / lifecycle hooks / config-reload callbacks.

History:

  • G-04 (2026-05-10): Vocabulary types (CellType / ContractKind / ContractRole / Lifecycle / Level + L0..L4 + Parse* + ValidRolesForKind / IsProviderRole / IsConsumerRole + InternalPathPrefix) moved to kernel/cellvocab to break the governance→cell and metadata→cell/levelrank reverse edges. ContractSpec moved to kernel/contractspec to break the cell→wrapper reverse edge. kernel/cell/levelrank/ was absorbed into kernel/cellvocab.
  • G-10 (2026-05-24, this PR): AuthPlan + the auth dependency interfaces moved to kernel/auth. DurabilityMode / Nooper / CheckNotNoop / DemoTxRunner + the cell emitter mode resolver moved to kernel/outbox. The Registry interface was renamed Registrar (Kratos verb-noun distinction). The cell.ErrDegraded alias was removed; callers reach outbox.ErrDegraded directly.

Cells under cells/ embed BaseCell and call its Init / Start / Stop methods; they consume Registrar to declare capabilities. The bootstrap runtime (runtime/bootstrap) drains a RegistrySnapshot in phase5/phase6 to wire HTTP routes and event subscriptions.

Package cell provides the fundamental kernel types for the GoCell framework. This file defines the ListenerRef type — a type-safe, compile-time-validated reference to a physical HTTP listener.

Design note: the [name] field is intentionally unexported so that no external package can construct an arbitrary ListenerRef by literal. Cells express their listener intent only via the exported package-level variables (PrimaryListener, InternalListener, HealthListener), eliminating the entire class of listener-name typos that a bare string parameter would allow.

Listener topology ownership: the ListenerRef set is intentionally kernel-owned and closed. A new listener class (e.g. a future AdminListener) must be added here in kernel/cell rather than manufactured by an individual cell. This is deliberate: listener topology is a deployment concern (which physical ports exist, what policies apply, how probes are routed) that belongs to the assembly/bootstrap level, not to individual cells. Cells declare *which* listener their routes target, but do not *define* listeners; that separation keeps cells portable across assemblies that share the same listener vocabulary.

Index

Constants

This section is empty.

Variables

View Source
var (
	// PrimaryListener is the public-facing listener for /api/v1/* business routes.
	PrimaryListener = ListenerRef{"primary"}
	// InternalListener is the control-plane listener for /internal/v1/* routes.
	InternalListener = ListenerRef{"internal"}
	// HealthListener is the dedicated listener for /healthz, /readyz, /metrics.
	HealthListener = ListenerRef{"health"}
	// WebhookListener is the dedicated listener for inbound webhook receive
	// endpoints. Inbound webhooks authenticate at the application layer via HMAC
	// signature verification inside the receiver (kernel/webhook), NOT via a
	// transport auth chain — so its WithListener auth chain is auth.AuthNone{}.
	// It is kept off PrimaryListener precisely because PrimaryListener typically
	// carries a JWT chain that would 401 an HMAC-signed (non-JWT) webhook before
	// it ever reaches the verifier. See .claude/rules/gocell/runtime-api.md
	// §"单 listener 单 auth scheme".
	WebhookListener = ListenerRef{"webhook"}
	// AdminListener is the dedicated listener for operator control-plane routes
	// (/admin/v1/*) — operator→system actions (projection rebuild, future saga
	// control / reconcile triggers) triggered by an administrator or deployment
	// pipeline, NOT cell→cell business calls. It carries an operator-credential
	// auth chain (auth.AuthOperator: HTTP Basic Auth over env credentials with
	// per-IP rate limiting), and is bound to a loopback address — loopback
	// network isolation plus operator credentials form a defense-in-depth pair.
	// Unlike InternalListener (/internal/v1/*, cell→cell, service-token +
	// caller-cell allowlist), the admin plane has no caller-cell notion; the
	// operator is authenticated directly. Mirrors the EventStoreDB projection
	// admin API model (a network-isolated admin port + operator credentials).
	AdminListener = ListenerRef{"admin"}
)

Package-level listener references. Cells must use these variables to express their listener intent; bare string construction is intentionally prevented by the unexported name field.

Functions

func MustHaveLifecycleHookName

func MustHaveLifecycleHookName(h LifecycleHook)

MustHaveLifecycleHookName panics when the hook Name is empty (programming error).

func MustHaveNonEmptyConfigPrefixes

func MustHaveNonEmptyConfigPrefixes(prefixes []string)

MustHaveNonEmptyConfigPrefixes panics when any prefix is an empty string (programming error).

func MustHaveNonNilConfigReloadFn

func MustHaveNonNilConfigReloadFn(fn func(context.Context, ConfigChangeEvent) error)

MustHaveNonNilConfigReloadFn panics when fn is nil (programming error).

func MustNotBeRegistryFinalized

func MustNotBeRegistryFinalized(finalized bool, method string)

MustNotBeRegistryFinalized panics when finalized is true (programming error).

func RegisterEmitterHealthProbes

func RegisterEmitterHealthProbes(reg Registrar, emitter outbox.Emitter) error

RegisterEmitterHealthProbes registers every probe exposed by an emitter that also satisfies healthz.ProbeSet (in practice outbox.DirectEmitter, which exposes a fail-open-rate probe per cell). It is the single sanctioned funnel for emitter health probes across all cells — handwritten cell Init code calls this instead of re-deriving the type assertion + registration loop.

The funnel terminates in reg.RegisterReadiness(p.Name(), p) for each probe returned by ps.Probes(). Probe names for emitter probes are constructed by healthz.EmitterFailOpenProbeName(cellID) inside DirectEmitter.Probes(), so they are fully inside the kernel/healthz.ProbeName funnel (PROBENAME-SEALED-FUNNEL-01).

Behavior:

  • bare-nil or typed-nil emitter → no-op (pkg/validation.IsNilInterface, the project-wide single-source typed-nil helper). Guarding before the type assertion is what makes a typed-nil *DirectEmitter safe — calling Probes() on it would panic.
  • emitter that is not a healthz.ProbeSet → no-op (e.g. WriterEmitter).
  • emitter that is a ProbeSet → each Probe is registered via reg.RegisterReadiness; the first error (e.g. healthz.ErrDuplicateProbe) is returned and terminates the loop — already-registered probes are NOT deregistered (fail-fast; bootstrap drainProbes treats a duplicate as a startup error).

AI-robust rating: two orthogonal axes (authoritative grading lives in the HEALTHZ-WRITE-01 godoc — not duplicated here):

  • downstream — Registrar.Healthz() has been removed; reg.RegisterReadiness is the sole write surface; type system Hard gate (compile error on any attempt to call the removed method). HEALTHZ-WRITE-01/A2 is the Medium archtest caller-identity backstop covering Aggregator.Register direct callsites in kernel/ + adapter paths. Unchanged here.
  • upstream — a type-system seal of the Aggregator interface is the only Hard form, but it is infeasible (the holder axis is inexpressible in Go; plus cross-package impls + a kernel/healthz↔kernel/outbox import cycle), so HEALTHZ-HOLDER-SEAL-01 (gh #893) is won't-do. The upstream A3 holder allowlist stays Medium archtest — the ceiling.

Types

type AfterStarter

type AfterStarter interface {
	AfterStart(ctx context.Context) error
}

AfterStarter is optionally implemented by Cells that need to run logic after Start completes (e.g., register health probes, announce readiness). If AfterStart returns an error, the cell (whose Start already succeeded) is stopped via BeforeStop → Stop → AfterStop, then previously-started cells are rolled back.

type AfterStopper

type AfterStopper interface {
	AfterStop(ctx context.Context) error
}

AfterStopper is optionally implemented by Cells that need to run logic after Stop completes (e.g., emit final metrics, close audit logs). Errors are accumulated but do not prevent other cells from being stopped.

Note: by the time AfterStop runs, the cell's ShutdownCtx() is already canceled (Stop cancels it). Use the ctx parameter passed by the assembly, which carries the shutdown timeout, not ShutdownCtx().

type Assembly

type Assembly interface {
	// Register adds a Cell to the assembly. Subsequent Register calls before
	// Start are accumulated; calls after Start return an error.
	Register(cell Cell) error
	// Start initializes and starts every registered cell in dependency order.
	// On any cell's failure, previously-started cells are Stopped in reverse.
	Start(ctx context.Context) error
	// Stop drives graceful shutdown of every started cell in reverse order.
	Stop(ctx context.Context) error
	// Health returns each cell's current health snapshot, keyed by cell ID.
	Health() map[string]HealthStatus
}

Assembly orchestrates a set of Cells into a runnable application: registers cells, sequences Init/Start/Stop in dependency order, and aggregates health.

type AuthRouteDeclarer

type AuthRouteDeclarer interface {
	DeclareAuthMeta(meta AuthRouteMeta) error
}

AuthRouteDeclarer is implemented by aggregators that want to receive the auth metadata a slice declares alongside a route.

type AuthRouteMeta

type AuthRouteMeta struct {
	Method              string
	Path                string
	Public              bool
	PasswordResetExempt bool
	// Bootstrap marks the route as protected by HTTP Basic Auth (env credentials).
	// The listener-level JWT middleware skips Bootstrap routes, just like Public
	// routes — the per-route bootstrap middleware authenticates instead. Bootstrap,
	// Public, and PasswordResetExempt are mutually exclusive.
	Bootstrap bool
	// IdempotencyExempt marks the route as opt-out from the HTTP idempotency
	// middleware. When true, the middleware never claims or records this route's
	// responses. The route still executes normally; clients sending Idempotency-Key
	// get no replay guarantee.
	IdempotencyExempt bool
}

AuthRouteMeta carries the auth-related attributes a slice declares when registering a route.

func (AuthRouteMeta) IsAdmin

func (m AuthRouteMeta) IsAdmin() bool

IsAdmin reports whether this route lives on the admin listener (operator control-plane, /admin/v1/*). Delegates to metadata.IsAdminHTTPPath so the bare "/admin/v1" root and the trailing-slash form share one predicate; the runtime router's listener-route affinity check uses it to keep admin routes off the primary/internal listeners and vice versa (mirrors IsInternal).

func (AuthRouteMeta) IsInternal

func (m AuthRouteMeta) IsInternal() bool

IsInternal reports whether this route lives on the internal listener. Delegates to metadata.IsInternalHTTPPath so the bare "/internal/v1" root and the trailing-slash form share one predicate across governance (REF-17 / FMT-28 / FMT-31), runtime routing, and admission.

type BaseCell

type BaseCell struct {
	// contains filtered or unexported fields
}

BaseCell is the default implementation of the Cell interface. Embed or compose it to get a working Cell with minimal boilerplate. All state-accessing methods are protected by a mutex for safe concurrent use.

meta is owned exclusively by this BaseCell — NewBaseCell deep-copies the caller's *metadata.CellMeta so subsequent mutation in the caller does not leak into the running cell. cellType / level cache the typed enum view computed once at construction (Type / ConsistencyLevel hot-path zero-cost).

func MustNewBaseCell

func MustNewBaseCell(meta *metadata.CellMeta) *BaseCell

MustNewBaseCell is the panic-on-error twin of NewBaseCell, intended for composition-root and test sites that build cells from static literals where a construction failure is a programmer error and must abort startup. Do not call from request handlers, hot paths, or config-reload callbacks — use NewBaseCell and propagate the error to /readyz or a 5xx response. See ADR docs/architecture/202604270030-architectural-panic-whitelist.md §5.

func NewBaseCell

func NewBaseCell(meta *metadata.CellMeta) (*BaseCell, error)

NewBaseCell creates a BaseCell from declarative metadata.

meta is the canonical source — its Type / ConsistencyLevel string fields are parsed into typed enums at construction so per-call accessors stay allocation-free. Empty Type / ConsistencyLevel are accepted (zero-value cellvocab.CellType / cellvocab.L0) — callers needing strict validation must feed metadata already validated by gocell governance. A non-empty but unrecognized value returns ErrValidationFailed (caller decides: fall back, surface to readyz, etc.). The provided pointer is not retained — meta is deep-copied via deepCopyMeta so mutation by the caller does not leak into the cell.

Static wiring (cell.go literals, table-driven tests) should use MustNewBaseCell, which panics on construction error per the PANIC-REGISTERED-01 / ERROR-FIRST-API-01 contract.

func (*BaseCell) AddConsumedContract

func (b *BaseCell) AddConsumedContract(c Contract)

AddConsumedContract appends a Contract this cell consumes.

func (*BaseCell) AddProducedContract

func (b *BaseCell) AddProducedContract(c Contract)

AddProducedContract appends a Contract this cell produces.

func (*BaseCell) AddSlice

func (b *BaseCell) AddSlice(s Slice)

AddSlice appends a Slice to this cell's owned slice list.

func (*BaseCell) ConsistencyLevel

func (b *BaseCell) ConsistencyLevel() cellvocab.Level

func (*BaseCell) ConsumedContracts

func (b *BaseCell) ConsumedContracts() []Contract

ConsumedContracts returns a copy of the consumed contract list.

func (*BaseCell) Health

func (b *BaseCell) Health() HealthStatus

Health returns the current HealthStatus.

func (*BaseCell) ID

func (b *BaseCell) ID() string

func (*BaseCell) Init

func (b *BaseCell) Init(_ context.Context, _ Registrar) error

Init prepares the cell. Only allowed from the new or stopped state.

func (*BaseCell) Lifecycle

func (b *BaseCell) Lifecycle() cellvocab.CellLifecycle

func (*BaseCell) Metadata

func (b *BaseCell) Metadata() *metadata.CellMeta

Metadata returns an independent deep copy of the cell's declarative metadata. Callers may freely mutate the returned value without affecting the cell's internal state (fail-closed isolation, since the previous read-only contract on a shared pointer was unenforceable).

func (*BaseCell) OwnedSlices

func (b *BaseCell) OwnedSlices() []Slice

OwnedSlices returns a copy of the owned slice list.

func (*BaseCell) ProducedContracts

func (b *BaseCell) ProducedContracts() []Contract

ProducedContracts returns a copy of the produced contract list.

func (*BaseCell) Ready

func (b *BaseCell) Ready() bool

Ready returns true when the cell is in the started state.

func (*BaseCell) ShutdownCtx

func (b *BaseCell) ShutdownCtx() context.Context

ShutdownCtx returns a context that is canceled when Stop is called. It should be used by goroutines spawned by the cell instead of context.Background(). Returns context.Background() if the cell has not been started.

func (*BaseCell) Start

func (b *BaseCell) Start(ctx context.Context) error

Start transitions the cell to the running state. Only allowed from the initialized state. A shutdownCtx is created that will be canceled when Stop is called — goroutines should use ShutdownCtx() instead of context.Background().

func (*BaseCell) Stop

func (b *BaseCell) Stop(_ context.Context) error

Stop transitions the cell to the stopped state. Only allowed from the started state; calling Stop from new or initialized is a no-op. Cancels the shutdownCtx to signal goroutines to exit.

func (*BaseCell) Type

func (b *BaseCell) Type() cellvocab.CellType

type BaseContract

type BaseContract struct {
	// contains filtered or unexported fields
}

BaseContract is the default implementation of the Contract interface.

func NewBaseContract

func NewBaseContract(id string, kind cellvocab.ContractKind, owner string, level cellvocab.Level) *BaseContract

NewBaseContract creates a BaseContract with cellvocab.ContractLifecycle defaulting to cellvocab.ContractLifecycleActive.

func (*BaseContract) ConsistencyLevel

func (c *BaseContract) ConsistencyLevel() cellvocab.Level

func (*BaseContract) ID

func (c *BaseContract) ID() string

func (*BaseContract) Kind

func (*BaseContract) Lifecycle

func (c *BaseContract) Lifecycle() cellvocab.ContractLifecycle

func (*BaseContract) OwnerCell

func (c *BaseContract) OwnerCell() string

func (*BaseContract) SetLifecycle

func (c *BaseContract) SetLifecycle(lc cellvocab.ContractLifecycle)

SetLifecycle updates the wire-stability governance lifecycle state of the contract.

type BaseSlice

type BaseSlice struct {
	// contains filtered or unexported fields
}

BaseSlice is the default implementation of the Slice interface.

func MustNewBaseSliceFromMeta

func MustNewBaseSliceFromMeta(meta *metadata.SliceMeta) *BaseSlice

MustNewBaseSliceFromMeta is the panic-on-error twin of NewBaseSliceFromMeta, intended for composition-root and test sites that build slices from static metadata literals where a construction failure is a programmer error and must abort startup. Mirrors MustNewBaseCell semantics.

Do not call from request handlers, hot paths, or config-reload callbacks — use NewBaseSliceFromMeta and propagate the error.

func NewBaseSliceFromMeta

func NewBaseSliceFromMeta(meta *metadata.SliceMeta) (*BaseSlice, error)

NewBaseSliceFromMeta constructs a BaseSlice from parsed slice.yaml metadata, which is the single source of truth for slice identity and consistency level.

Projects the runtime-consumed SliceMeta subset (ID / BelongsToCell / ConsistencyLevel / Verify / AllowedFiles / AffectedJourneys) into BaseSlice. SliceMeta.Lifecycle is intentionally NOT projected: it is a metadata-only field consumed by governance/catalog via SliceMeta.Lifecycle directly, with no runtime accessor (ADR 202605262100 §E52 — adding one would be dead code). Verify.Unit/Contract/Waivers and AllowedFiles slices are deep-copied so subsequent caller mutations of meta do not affect the BaseSlice.

The metadata projection lives in `<slicePkg>/slice_gen.go` as `var sliceMeta` rendered by `gocell generate cell`; cell composition roots call `cell.MustNewBaseSliceFromMeta(<slicePkg>.SliceMetadata())`. Hand-written `cell.NewBaseSlice(id, cellID, level)` literals (the prior form) are forbidden — see `tools/archtest/baseslice_ctor_funnel_test.go` BASESLICE-CTOR-FUNNEL-01 and the codegen funnel in `tools/codegen/cellgen/templates/slice.tmpl`.

All four metadata invariants are validated:

  • meta must be non-nil
  • meta.ID must be non-empty
  • meta.BelongsToCell must be non-empty
  • meta.ConsistencyLevel must be non-empty and parse to a valid Level

There is no fallback inheritance from cell.consistencyLevel — the strict parser (`kernel/metadata.Parser`) rejects slice.yaml that omits the field.

ref: kubernetes/kubernetes pkg/apis/core/validation/validation.go — schema-driven validation pattern; meta is the typed projection of the source-of-truth YAML.

func (*BaseSlice) AffectedJourneys

func (s *BaseSlice) AffectedJourneys() []string

AffectedJourneys returns a copy of the journey IDs this slice participates in.

func (*BaseSlice) AllowedFiles

func (s *BaseSlice) AllowedFiles() []string

AllowedFiles returns a copy of the file ownership paths. Returns nil if no paths have been set. Callers that need convention defaults should use gocell scaffold, which generates the initial paths.

func (*BaseSlice) BelongsToCell

func (s *BaseSlice) BelongsToCell() string

func (*BaseSlice) ConsistencyLevel

func (s *BaseSlice) ConsistencyLevel() cellvocab.Level

func (*BaseSlice) ID

func (s *BaseSlice) ID() string

func (*BaseSlice) Init

func (s *BaseSlice) Init(_ context.Context) error

Init is a no-op for BaseSlice.

func (*BaseSlice) SetAffectedJourneys

func (s *BaseSlice) SetAffectedJourneys(ids []string)

SetAffectedJourneys sets the journey IDs.

func (*BaseSlice) SetAllowedFiles

func (s *BaseSlice) SetAllowedFiles(files []string)

SetAllowedFiles sets the file ownership paths for this slice.

func (*BaseSlice) SetVerify

func (s *BaseSlice) SetVerify(v VerifySpec)

SetVerify sets the verification spec.

func (*BaseSlice) Verify

func (s *BaseSlice) Verify() VerifySpec

Verify returns the verification spec for this slice.

type BeforeStarter

type BeforeStarter interface {
	BeforeStart(ctx context.Context) error
}

BeforeStarter is optionally implemented by Cells that need to run preflight checks before Start is called (e.g., validate runtime prerequisites, verify config completeness, check external connectivity).

BeforeStart MUST NOT acquire resources that require cleanup. If it returns an error, Start is NOT called, and the framework does NOT run BeforeStop/Stop/AfterStop on the current cell — only previously-started cells are rolled back. Any resource acquisition should happen inside Start, which is responsible for its own internal cleanup on failure.

ref: uber-go/fx lifecycle.go — failing OnStart does not trigger OnStop for the same hook; only previously-registered hooks are rolled back.

type BeforeStopper

type BeforeStopper interface {
	BeforeStop(ctx context.Context) error
}

BeforeStopper is optionally implemented by Cells that need to run logic before Stop is called (e.g., drain in-flight requests, deregister from service discovery). Errors are accumulated but do not prevent Stop from being called.

type Cell

Cell is the fundamental building block of a GoCell application: a vertically integrated unit of capability that owns its data, slices, and contracts. Cells are registered in an Assembly which drives the Init → Start → Stop lifecycle.

Cell is the composite of the four sub-interfaces above; any type satisfying CellIdentity, CellLifecycle, CellStatus, and CellInventory is a Cell. Implementations should embed BaseCell rather than implement these from scratch. Prefer the narrower sub-interfaces when the caller only needs a subset.

ref: docs/architecture/202605101800-adr-cell-interface-isp-split.md D2 ref: io.ReadWriter / kubernetes/apimachinery meta/v1.Object composition

type CellIdentity

type CellIdentity interface {
	// ID returns the cell's stable identifier (matches cell.yaml id).
	ID() string
	// Type classifies the cell's architectural role (core / edge / support).
	Type() cellvocab.CellType
	// ConsistencyLevel reports the cell's declared consistency tier (cellvocab.L0–cellvocab.L4).
	ConsistencyLevel() cellvocab.Level
	// Lifecycle reports the cell's declared governance maturity lifecycle
	// (experimental → candidate → asset → maintenance → retired); empty in
	// cell.yaml defaults to experimental. The YAML / wire key is `lifecycle`.
	// Consumers: runtime/lifecycle.LifecycleAggregator (startup maturity log),
	// runtime/devtools/catalog (catalog wire).
	Lifecycle() cellvocab.CellLifecycle
}

CellIdentity exposes the cell's stable architectural identity. Returned values are immutable for the lifetime of the cell.

Consumers: registry lookup (kernel/cell.Assembly), metrics labels (runtime/http/middleware.Metrics), log correlation (slog), route attribution (runtime/http/router.Router).

ref: docs/architecture/202605101800-adr-cell-interface-isp-split.md D1

type CellInventory

type CellInventory interface {
	// Metadata returns an independent deep copy of the cell's declarative
	// metadata; callers may freely read and modify the returned value.
	Metadata() *metadata.CellMeta
	// OwnedSlices returns the slices this cell registered.
	OwnedSlices() []Slice
	// ProducedContracts returns contracts whose authoritative source is this cell.
	ProducedContracts() []Contract
	// ConsumedContracts returns contracts this cell calls or subscribes to.
	ConsumedContracts() []Contract
}

CellInventory exposes the cell's declarative metadata + slice/contract inventories as defensive copies (callers may freely mutate returned values). The single source of truth is *metadata.CellMeta in kernel/metadata; this interface is the read path. CELLMETA-SINGLE-SOURCE-03 archtest pins Metadata() here.

Consumers: contract validators (kernel/governance), metadata inspectors (cmd/gocell validate), codegen (tools/codegen/contractgen).

type CellLifecycle

type CellLifecycle interface {
	// Init registers cell capabilities via the provided Registrar; called once
	// before Start. Init failure aborts assembly start without invoking Stop.
	// Init may validate config, build in-memory state, and register slices.
	Init(ctx context.Context, reg Registrar) error
	// Start brings the cell to a running state. Returns nil only when the cell
	// is fully ready to serve traffic. Canceling ctx must abort startup.
	// Resources acquired here must be released by the matching Stop call.
	Start(ctx context.Context) error
	// Stop performs graceful shutdown. The assembly invokes Stop with the
	// caller-supplied ctx; no per-cell stop timeout is enforced by the kernel,
	// so implementations are solely responsible for honoring ctx deadlines
	// and returning promptly when ctx is canceled.
	Stop(ctx context.Context) error
}

CellLifecycle drives the cell through Init → Start → Stop transitions. All methods accept a context and must honor cancellation.

Resource acquisition rule: Init must NOT acquire resources requiring explicit release (open connections, spawn goroutines, lease distributed locks); defer such acquisition to Start so Start/Stop remain symmetric. Stop must release every resource Start acquired and must be idempotent against repeated invocation.

Consumers: Assembly orchestrator (kernel/cell.Assembly), bootstrap phases, lifecycle test harnesses.

type CellStatus

type CellStatus interface {
	// Health returns the current health snapshot for diagnostic surfaces.
	Health() HealthStatus
	// Ready reports whether the cell is currently serving (post-Start, pre-Stop).
	Ready() bool
}

CellStatus reports runtime probe state. Both methods are safe for concurrent calls and reflect derived state from the cell's lifecycle state machine (not declared metadata).

Consumers: /healthz and /readyz HTTP handlers (runtime/http/health.Handler), runtime supervision (kernel/cell.Assembly).

type ConfigChangeEvent

type ConfigChangeEvent struct {
	Added      []string
	Updated    []string
	Removed    []string
	Config     map[string]any
	Generation int64
}

ConfigChangeEvent describes what changed during a config reload.

ref: micro/go-micro config/watcher.go — checksum-based change dedup.

type ConfigReloadRequest

type ConfigReloadRequest struct {
	Prefixes []string
	Fn       func(context.Context, ConfigChangeEvent) error
}

ConfigReloadRequest holds a registered config-reload callback with its optional key-prefix filter. Prefixes==nil means "all keys".

type Contract

type Contract interface {
	// ID returns the contract's stable identifier (matches contract.yaml id).
	ID() string
	// Kind classifies the communication pattern (http / event / command / projection).
	Kind() cellvocab.ContractKind
	// OwnerCell returns the cell ID that owns the contract's authoritative side.
	OwnerCell() string
	// ConsistencyLevel reports the contract's consistency tier (cellvocab.L0–cellvocab.L4).
	ConsistencyLevel() cellvocab.Level
	// Lifecycle returns the contract's wire-stability governance state (draft / active / deprecated).
	Lifecycle() cellvocab.ContractLifecycle
}

Contract defines a communication boundary between Cells. Contracts are authoritative metadata loaded from contracts/**/contract.yaml; in-process values implementing this interface are derivative views only.

type GRPCServiceRegistrar

type GRPCServiceRegistrar interface {
	// Register invokes the spec.Register callback on a cellScopedRegistrar
	// interceptor, recording method→cellID attribution for each service.
	// The canonical implementation (runtime/grpc.ServiceRegistrar) always returns
	// nil; contract violations (bad callback type, duplicate ServiceName) panic
	// via panicregister.Approved.
	Register(spec GRPCServiceSpec) error
}

GRPCServiceRegistrar is the narrow cell-service-registration contract exposed by adapters/grpc.Server and consumed by runtime/bootstrap's drain (GAP-1 PR-7 [#1150]). It lives in kernel/cell so both layers can import it without cycles: kernel/ is the bottom of the dependency graph.

*runtime/grpc.ServiceRegistrar satisfies this interface structurally.

type GRPCServiceSpec

type GRPCServiceSpec struct {
	// ContractID is the stable contract identifier (matches contract.yaml id).
	// Used for deduplication: a cell may not register the same ContractID twice.
	ContractID string

	// CellID is the cell that owns this service. bootstrap asserts spec.CellID
	// equals the snapshot key when draining (mirrors the subscription drain check).
	CellID string

	// Listener identifies the target gRPC listener (must match a ref passed to
	// WithGRPCListener). Symmetric with RouteGroup.Listener for HTTP.
	Listener ListenerRef

	// Register holds a func(grpc.ServiceRegistrar) callback. Kernel stores it as
	// any to avoid a grpc import dependency. The runtime layer type-asserts it.
	// A nil value fails Validate; a non-func value panics in runtime/grpc at
	// registration time (panicregister.Approved("grpc-registrar-bad-register-fn",…)).
	Register any
}

GRPCServiceSpec declares a gRPC service that a cell wants to expose on a specific listener. The registration callback (Register) is the Form B idiom popularized by go-zero (RegisterFn) and go-kratos (pb.RegisterXxxServer): the cell supplies a closure that calls the generated pb.RegisterXxxServer helper, which internally calls grpc.ServiceRegistrar.RegisterService with the fully-populated *ServiceDesc.

Kernel holds Register as any (not func(grpc.ServiceRegistrar)) because kernel/ must not import google.golang.org/grpc. The type-assert from any to the concrete func type happens in runtime/grpc.ServiceRegistrar.Register; a wrong dynamic type panics via panicregister.Approved("grpc-registrar-bad-register-fn", …).

Usage in Cell.Init

func (c *MyCell) Init(ctx context.Context, reg cell.Registrar) error {
    return reg.GRPCService(cell.GRPCServiceSpec{
        ContractID: "grpc.myservice.v1",
        CellID:     c.ID(),
        Listener:   cell.PrimaryListener,
        Register:   func(r grpc.ServiceRegistrar) {
            pb.RegisterMyServiceServer(r, c.svc)
        },
    })
}

func (GRPCServiceSpec) Validate

func (s GRPCServiceSpec) Validate() error

Validate returns a non-nil error when any required field is missing. Called by RegistryRecorder.GRPCService before accumulating the spec.

type HTTPContractDeclarer

type HTTPContractDeclarer interface {
	DeclareHTTPContract(spec contractspec.ContractSpec) error
}

HTTPContractDeclarer is implemented by aggregators that want to receive the ContractSpec a slice declares alongside an HTTP route.

type HealthStatus

type HealthStatus struct {
	Status  string // "healthy" | "degraded" | "unhealthy"
	Details map[string]string
}

HealthStatus reports the health of a Cell.

type HookEvent

type HookEvent struct {
	CellID   string
	Hook     HookPhase
	Outcome  HookOutcome
	Duration time.Duration
	Err      error
}

HookEvent is emitted to the LifecycleHookObserver once per hook invocation (after the hook completes or times out).

ref: uber-go/fx fxevent/event.go@master:L82-L89 OnStartExecuted — carries Runtime + Err. GoCell adds CellID (hooks are per-cell in the assembly loop) and splits Hook into four phases matching the lifecycle.go interfaces, so event type is redundant.

type HookOutcome

type HookOutcome string

HookOutcome classifies the result of a hook invocation.

Outcome is derived by the assembly layer from the hook's return value:

  • nil error → OutcomeSuccess
  • context.DeadlineExceeded wrapped → OutcomeTimeout
  • panic recovered by callHookSafe → OutcomePanic
  • any other non-nil error → OutcomeFailure
const (
	OutcomeSuccess HookOutcome = "success"
	OutcomeFailure HookOutcome = "failure"
	OutcomeTimeout HookOutcome = "timeout"
	OutcomePanic   HookOutcome = "panic"
)

type HookPhase

type HookPhase string

HookPhase identifies a specific lifecycle hook invocation site.

ref: uber-go/fx fxevent/event.go@master — OnStart / OnStop events are emitted around the corresponding hook calls; GoCell splits the same hooks into four distinct phases to match the existing Before/After interfaces in lifecycle.go.

const (
	HookBeforeStart HookPhase = "before_start"
	HookAfterStart  HookPhase = "after_start"
	HookBeforeStop  HookPhase = "before_stop"
	HookAfterStop   HookPhase = "after_stop"
)

type LifecycleHook

type LifecycleHook struct {
	// Name is a diagnostic identifier used in slog fields. Must be non-empty
	// when passed to Registrar.Lifecycle.
	Name string

	// OnStart is called by bootstrap during lifecycle.Start. The ctx parameter
	// is the long-lived owner ctx (controller-runtime Runnable.Start semantics):
	// it remains live for the entire assembly run and is canceled only when the
	// assembly begins shutdown. This supersedes ADR 202605102000 §D1, which
	// described ctx as a startup-deadline ctx.
	//
	// Expected OnStart shape:
	//   1. Spawn the long-running worker goroutine passing the owner ctx.
	//   2. Run a fast synchronous readiness probe (e.g. 50 ms timer).
	//   3. Return nil on success or a non-nil error to abort startup and trigger
	//      LIFO rollback. The failed hook's OnStop is NOT called by rollback.
	//
	// Returning without spawning a goroutine is fine for hooks that do all their
	// work synchronously. Blocking indefinitely in OnStart is not allowed — the
	// hook must return promptly so the startup sequence can continue.
	//
	// StartTimeout is no longer enforced as an OnStart runner deadline; it is
	// retained as a hook self-declared probe window budget (informational only).
	// Supersedes ADR 202605102000 §D1.
	//
	// ref: kubernetes-sigs/controller-runtime pkg/manager/internal.go — Runnable.Start
	//      receives the manager context (long-lived owner ctx); returns when ctx done.
	OnStart func(ctx context.Context) error

	// OnStop is called by bootstrap during lifecycle.Stop, with a context that
	// carries StopTimeout as deadline. OnStop cancels the worker (if not already
	// canceled via owner ctx) and waits for it to exit.
	OnStop       func(ctx context.Context) error
	StartTimeout time.Duration
	StopTimeout  time.Duration
}

LifecycleHook mirrors bootstrap.Hook shape but lives in kernel/ so Cell interfaces never depend on runtime/. Bootstrap copies these fields into its own bootstrap.Hook at phase3b discovery time.

ref: github.com/uber-go/fx internal/lifecycle/lifecycle.go Hook — adopted.

type LifecycleHookObserver

type LifecycleHookObserver interface {
	OnHookEvent(HookEvent)
}

LifecycleHookObserver observes lifecycle hook invocations for a running assembly. The assembly calls OnHookEvent exactly once per hook, after the hook returns (successfully, with error, timeout, or panic).

Implementations MUST be safe for concurrent use — during rollback the assembly may emit stop-phase events from multiple cells serially; there is no parallel call today, but implementations should not rely on serialization. Implementations MUST NOT block the caller (use an async fan-out internally if the sink is slow).

Implementations MUST NOT panic. The assembly wraps calls in a panic guard as defense in depth, but a panicking observer signals a bug.

kernel/cell has zero Prometheus dependency by design — concrete implementations live in runtime/observability or adapters/.

ref: uber-go/fx fxevent/logger.go@master:L24-L27 — single-method Logger interface; NopLogger as null object. GoCell mirrors this shape.

type ListenerRef

type ListenerRef struct {
	// contains filtered or unexported fields
}

ListenerRef is a type-safe reference to a physical HTTP listener. The name field is unexported so external packages cannot construct illegal references; this is the compile-time listener-reference validation from PR-A14b.

func (ListenerRef) IsZero

func (r ListenerRef) IsZero() bool

IsZero reports whether the ref is the zero value (no listener assigned).

func (ListenerRef) String

func (r ListenerRef) String() string

String returns the listener's canonical name.

type NopHookObserver

type NopHookObserver struct{}

NopHookObserver is the zero-value observer used when none is configured. Its OnHookEvent is a no-op; it exists to avoid nil checks on every hook emission.

ref: uber-go/fx fxevent/logger.go@master:L29-L37 NopLogger — same pattern.

func (NopHookObserver) OnHookEvent

func (NopHookObserver) OnHookEvent(HookEvent)

OnHookEvent is a no-op: the default observer discards all events. Business reason: LifecycleHookObserver is an optional collaborator injected via AssemblyConfig; when unconfigured, the assembly substitutes NopHookObserver to keep the call site unconditional and allocation-free.

type Prefixer

type Prefixer interface {
	Prefix() string
}

Prefixer is optionally implemented by RouteHandler values whose chi sub-router owns a mount prefix. auth.Mount type-asserts to this interface to compute the chi-relative registration path.

type ProjectionApply

type ProjectionApply = cellvocab.ProjectionApply

ProjectionApply is the cell-local registry surface for the event→state hook. It is an alias of cellvocab.ProjectionApply (the single underlying type, also aliased by kernel/projection.Apply), so a recorded hook reaches Coordinator.Subscribe through the bootstrap drain with NO named-type conversion. The carrier is the typed cellvocab.ProjectionEvent interface, not the concrete outbox.Entry (EPIC #1609 PR-01 / PROJECTION-EVENT-CARRIER-TYPED-01).

It lives behind an alias here (rather than a distinct mirror type) because kernel/projection imports kernel/cell — the carrier and hook types therefore live in the cellvocab leaf, which both packages import without a cycle. The behavioral contract godoc is on the projection.Apply alias.

type ProjectionRequest

type ProjectionRequest struct {
	// Source selects the input transport (EPIC #1609 PR-05). The zero value ("")
	// and ProjectionSourceOutbox are equivalent: the outbox path, where Spec is
	// the event-kind contract the projection consumes. ProjectionSourceSagaJournal
	// is the global saga journal: Spec MUST be the zero value (no event topic).
	// Set only by the sanctioned constructors — the generated NewProjectionRequest
	// (outbox) and NewSagaJournalProjectionRequest (saga-journal).
	Source cellvocab.ProjectionSource
	// Spec is the event-kind contract the projection consumes (its input stream).
	// Required when Source is outbox (Spec.Kind must be "event"); MUST be the zero
	// value when Source is saga-journal.
	Spec contractspec.ContractSpec
	// ProjectionID names the projection within its cell. It is half of the
	// checkpoint key (cellID, projectionID); the consumer group is derived as
	// cellID + "-" + projectionID, and the rebuild HTTP endpoint (PR-04e) keys
	// the Coordinator by cellID + "/" + projectionID. Must be a snake_case
	// probe-name identifier (NewCoordinator rejects "/" etc. via the probe-name
	// validator), so neither derived key is ambiguous.
	ProjectionID string
	// CellID is the owning cell — observability owner and checkpoint-key half.
	// Injected from cell metadata at code-generation time (same provenance as
	// SubscriptionRequest.CellID); the bootstrap drain cross-checks it against
	// the snapshot owner and fails fast on drift.
	CellID string
	// SliceID is the slice that owns this projection — the observability owner
	// at slice granularity, one level below CellID. Semantically mirrors
	// SubscriptionRequest.SliceID (see that field's godoc). Typically equal to
	// ProjectionID; injected from slice metadata at code-generation time in
	// PR-04b. During this PR-04a record-only seam there is no production fill
	// path, so SliceID may be empty — bootstrap falls back to ProjectionID
	// when SliceID is the empty string.
	SliceID string
	// Apply is the business event→state hook. Required (non-nil).
	Apply ProjectionApply
	// OnReset is the optional rebuild Reset-phase hook. May be nil. Ignored on the
	// saga-journal path (rebuild is not supported there — NewSagaJournalProjectionRequest
	// never sets it, and governance forbids onReset on a saga-journal subscribe CU).
	OnReset ProjectionResetHook
}

ProjectionRequest holds everything needed to register one L3 CQRS projection. RegistryRecorder accumulates these via Registrar.RegisterProjection; the bootstrap projection drain reads RegistrySnapshot.Projections and, BRANCHING ON Source, either constructs a push-based projection.Coordinator (outbox) or a pull-based runtime/saga/tailer.Tailer (saga-journal) from framework-owned dependencies. Mirrors SubscriptionRequest's exported-field shape; the framework deps (checkpoint store / tx runner / cursor / replay / journal reader / locker) are deliberately NOT fields here — they live in bootstrap and never reach cell code.

func NewSagaJournalProjectionRequest

func NewSagaJournalProjectionRequest(apply ProjectionApply, projectionID, cellID, sliceID string) ProjectionRequest

NewSagaJournalProjectionRequest builds a ProjectionRequest whose input stream is the GLOBAL saga journal (stream saga.journal.v1, every saga instance/type). It carries no event Spec and no OnReset — the bootstrap projection drain wires it to a runtime/saga/tailer.Tailer (pull) rather than a Coordinator (push), and saga-journal rebuild is out of scope (EPIC #1609 PR-05).

cellgen is the only production caller: it emits this from a slice.yaml subscribe CU carrying projectionSource=saga-journal, and reg.RegisterProjection is funnel-locked to cell_gen.go by PROJECTION-REGISTER-FUNNEL-01. There is no per-event-contract generated package for the saga journal (it is not an event contract), so — unlike the outbox NewProjectionRequest, which is generated per event contract into generated/contracts/event/<path>/<version>/projection_gen.go and embeds that package's private event spec — this saga constructor lives in kernel/cell (do NOT mirror it into generated/; the saga journal has no contract package to host it).

type ProjectionResetHook

type ProjectionResetHook = cellvocab.ProjectionResetHook

ProjectionResetHook is the cell-local registry surface for the optional rebuild Reset-phase hook. Alias of cellvocab.ProjectionResetHook (also aliased by kernel/projection.OnReset); a nil value is valid (no read-model table to clear).

type Registrar

type Registrar interface {
	// Config returns the per-cell config snapshot provided by the assembly.
	Config() map[string]any
	// DurabilityMode returns the assembly-level durability mode.
	DurabilityMode() outbox.DurabilityMode

	// RouteGroup declares an HTTP route group. Groups accumulate in
	// declaration order and are mounted by bootstrap during phase5.
	RouteGroup(g RouteGroup)

	// Subscribe registers an event subscription. Returns a non-nil error when:
	//   - handler is nil
	//   - consumerGroup is empty
	//   - cellID is empty
	//   - spec.Kind != "event"
	//
	// cellID is the cell that owns this subscription — distinct from
	// consumerGroup (which may include a role suffix like
	// "accesscore-rbac-session-sync"). It is a positional, mandatory string
	// parameter rather than a SubscriptionOption: codegen (contractgen
	// NewSubscription + cellgen cell.tmpl) injects it from cell metadata at
	// compile time, so a missing cellID is a compile failure at the
	// reg.Subscribe call site (HARD contract). This is the AI-robust gate for
	// "metric/log owner must trace to cell metadata, not to consumerGroup
	// drift" — making cellID an option would silently demote the contract
	// from compile-time to opt-in.
	//
	// Typical usage (consumerGroup == cellID):
	//
	//	reg.Subscribe(spec, handler, c.ID(), c.ID())
	//
	// Role-suffix usage (fanout consumer with sub-group, cellID stays the cell):
	//
	//	reg.Subscribe(spec, handler, "accesscore-rbac-session-sync", c.ID())
	//
	// Cell.Init should propagate the error via `if err := ...; err != nil { return err }`.
	//
	// The handler type outbox.EntryHandler is the canonical event handler in
	// kernel/; cell.Registrar consumes it directly rather than wrapping it in a
	// cell-local alias. This mirrors the industry pattern where a registry/router
	// depends on its event primitive's handler signature:
	//   ref: ThreeDotsLabs/watermill message/router.go AddHandler — handler func type
	//        defined in message/, consumed directly by router.AddHandler.
	//   ref: k8s.io/client-go/tools/cache SharedInformer.AddEventHandler — cache.ResourceEventHandler
	//        defined in cache/, passed directly to informer.AddEventHandler.
	//
	// The subscription is not started here; Bootstrap drains all registered
	// SubscriptionRequests in phase6 and hands them to the event router.
	// This two-phase pattern (register intent in Init, start goroutines in
	// bootstrap) mirrors ThreeDotsLabs/watermill router.AddHandler — handler
	// registration and router.Run are separate lifecycle steps.
	//
	// ref: ThreeDotsLabs/watermill message/router.go AddHandler (handler
	// registration decoupled from goroutine start).
	// ref: ADR docs/architecture/202605111000-adr-subscription-cellid-mandatory.md
	//
	// Hand-written external cells should prefer the fluent [Registrar.Subscription]
	// builder, which makes the cellID red line a compile error rather than relying
	// on getting the positional argument order right.
	Subscribe(
		spec contractspec.ContractSpec,
		handler outbox.EntryHandler,
		consumerGroup string,
		cellID string,
		opts ...SubscriptionOption,
	) error

	// Subscription begins a fluent, compile-checked subscription registration —
	// the hand-written counterpart to the positional Subscribe (which codegen
	// emits as NewSubscription(...).Mount(reg)). It exists so an external Cell
	// that does NOT run the monorepo codegen can register subscriptions without
	// hand-writing the easy-to-transpose positional Subscribe call.
	//
	// The returned draft exposes only CellID; the terminal Register lives on the
	// builder that CellID returns, so the owning cell id is a COMPILE-TIME red
	// line — omitting it leaves no path to Register (the same compile-HARD
	// guarantee codegen gets from the positional cellID parameter, not a runtime
	// fallback). Both step types are unexported (sealed construction), so an
	// external cell can chain but neither name nor forge them:
	//
	//	reg.Subscription(spec).
	//	    CellID(c.ID()).
	//	    ConsumerGroup("payment-charge"). // optional; defaults to CellID
	//	    Handler(c.svc.HandleCharge).
	//	    Register()
	//
	// Register routes through the same RegistryRecorder.Subscribe validation
	// funnel as the positional form (nil handler / empty consumerGroup→defaulted
	// to cellID / empty cellID / non-event spec / empty topic), so the two
	// registration forms are two producers of one validated path, not a
	// backward-compat dual path.
	//
	// ref: ADR docs/architecture/202605111000-adr-subscription-cellid-mandatory.md
	Subscription(spec contractspec.ContractSpec) *subscriptionDraft

	// RegisterWebhookReceiver records an inbound-webhook receiver declaration.
	// Returns a non-nil error when handler is nil or spec.Validate() fails.
	//
	// RECORD-ONLY semantics (PR-2): like Subscribe, this method only appends a
	// WebhookReceiverRequest to the recorder's accumulator — it does NOT mount
	// an HTTP route, resolve a signing secret, or start any goroutine. The
	// receiver runtime drain (bootstrap reading RegistrySnapshot.WebhookReceivers
	// and wiring the HTTP receive endpoint + Claimer) lands in PR-3. This keeps
	// the same two-phase pattern as Subscribe (declare intent in Init, wire in
	// bootstrap) so the generated reg.RegisterWebhookReceiver(...) call compiles
	// today against a stable seam.
	//
	// The call is emitted by cellgen from slice.yaml
	// contractUsages[role=webhook-receive]; the [webhook.ReceiverSpec] literal
	// carries identifiers known at code-generation time (ContractID / SourceID /
	// CellID, the last injected from cell metadata exactly like Subscribe's
	// positional cellID). The handler type is [webhook.WebhookReceiveHandler],
	// defined in kernel/webhook alongside ReceiverSpec and consumed directly here
	// (same pattern as Subscribe consuming outbox.EntryHandler).
	//
	// Cell.Init should propagate the error via `if err := ...; err != nil { return err }`.
	RegisterWebhookReceiver(spec webhook.ReceiverSpec, handler webhook.WebhookReceiveHandler) error

	// RegisterWebhookDispatch records an outbound-webhook dispatcher declaration.
	// Returns a non-nil error when selector is nil or spec.Validate() fails.
	//
	// RECORD-ONLY semantics (PR-2): the dispatch counterpart of
	// RegisterWebhookReceiver. It only appends a WebhookDispatchRequest to the
	// recorder's accumulator — no dispatcher consumer is started and no signing
	// secret is resolved. The dispatcher runtime drain (bootstrap reading
	// RegistrySnapshot.WebhookDispatchers and wiring the outbound dispatcher
	// consumer) lands in PR-5.
	//
	// The call is emitted by cellgen from slice.yaml
	// contractUsages[role=webhook-dispatch]; the [webhook.DispatchSpec] literal
	// carries ContractID / SourceID / CellID. The selector type is
	// [webhook.WebhookDispatchSelector], defined in kernel/webhook alongside
	// DispatchSpec.
	//
	// Cell.Init should propagate the error via `if err := ...; err != nil { return err }`.
	RegisterWebhookDispatch(spec webhook.DispatchSpec, selector webhook.WebhookDispatchSelector) error

	// RegisterProjection records an L3 CQRS-projection declaration. Returns a
	// non-nil error when req fails validation (nil Apply, empty ProjectionID /
	// CellID, or a non-event Spec).
	//
	// RECORD-ONLY semantics: like Subscribe and RegisterWebhookReceiver, this
	// method only appends a ProjectionRequest to the recorder's accumulator. It
	// does NOT construct a projection.Coordinator, open a transaction, or start
	// consuming. The bootstrap projection drain reads
	// RegistrySnapshot.Projections, constructs one projection.Coordinator per
	// request from framework-owned dependencies (checkpoint store / tx runner /
	// cursor / replay source — wired via bootstrap options, never reachable by
	// cell code), and calls Coordinator.Subscribe. This two-phase split (declare
	// intent in Init, wire in bootstrap) is the same pattern as Subscribe and is
	// what keeps the raw framework infrastructure out of the cell package: a cell
	// could not call projection.NewCoordinator even if it wanted to, because it
	// holds no checkpoint store / tx runner.
	//
	// The call is emitted by cellgen from slice.yaml contractUsages (kind:projection
	// derivation, PR-04b #1367 / #834 landed); the first production callsite is
	// examples/todoorder/cells/ordercell/cell_gen.go. RegisterProjection is otherwise
	// only called from test files — PROJECTION-REGISTER-FUNNEL-01 confines every call
	// to cell_gen.go + _test.go. ProjectionRequest carries identifiers known at
	// code-generation time (CellID injected from cell metadata exactly like
	// Subscribe's positional cellID). The Apply / OnReset function types
	// ([ProjectionApply] / [ProjectionResetHook]) are aliases of the cellvocab
	// leaf types (which kernel/projection.Apply / OnReset also alias), so the
	// bootstrap drain passes a recorded hook straight to Coordinator.Subscribe
	// with NO named-type conversion. The carrier and hook types live in the
	// cellvocab leaf because kernel/projection imports kernel/cell (its
	// Coordinator's SubscribeRegistrar references cell.SubscriptionOption), so a
	// reverse import here would be a compile-time cycle (EPIC #1609 PR-01).
	//
	// Cell.Init should propagate the error via `if err := ...; err != nil { return err }`.
	//
	// AI-robust: callers of RegisterProjection are locked to cellgen-derived
	// cell_gen.go + _test.go + kernel/ by archtest PROJECTION-REGISTER-FUNNEL-01
	// (Medium upstream + Medium downstream — Go cannot type-gate callers of a
	// public method; this is the same permanent ceiling as Subscribe /
	// RegisterWebhookReceiver). A "cellgen-only sealed-token" Hard-ification is NOT
	// expressible in Go: cellgen emits this call into the cell's own package
	// (cell_gen.go beside hand-written cell.go), which a same-package hand-written
	// file is indistinguishable from at compile time. won't-do, gh #1372 (same
	// ceiling family as #851 / #893 / #1282).
	//
	// ref: tools/archtest/projection_register_funnel_test.go (PROJECTION-REGISTER-FUNNEL-01)
	// ref: ADR docs/architecture/202605261620-adr-cqrs-projection-lifecycle-harness.md §7 + §Amendment 2026-05-31
	RegisterProjection(req ProjectionRequest) error

	// RegisterReadiness registers a readiness probe under the typed
	// [healthz.ProbeName]. The probe runs against the runtime
	// healthz.Aggregator after bootstrap drains the snapshot. First-wins
	// duplicate semantics: [healthz.ErrDuplicateProbe] is returned on name
	// collision (the second registration is dropped, not the first).
	//
	// name is the single source of truth for the probe identity — the
	// recorder wraps prober.Check() under name even if prober itself
	// originated from a [healthz.Probe] with a different Name(); this makes
	// name/check drift structurally impossible at the entry point.
	//
	// This is the SOLE public probe-registration surface on Registrar; the
	// underlying healthz.Aggregator is no longer exposed (the legacy
	// Registrar.Healthz() method has been retired). Hand-written cell code
	// routes through the sanctioned typed funnels that themselves terminate here:
	//   - cellgen-generated `<cellpkg>.RegisterReadiness(reg, prober)` for
	//     cell-repo probes (declared in healthz_gen.go).
	//   - shared kernel `cell.RegisterEmitterHealthProbes(reg, emitter)`
	//     for outbox emitter probes.
	//
	// archtest PROBENAME-SEALED-FUNNEL-01 locks both directions: A1
	// declares the sanctioned ProbeName const set; A2 closes the callsite
	// via type resolution on the ProbeName parameter.
	//
	// Example (cell-repo probe — standard path):
	//
	//  1. Declare repo in cell.yaml.
	//  2. Run `gocell generate cell -all` to emit healthz_gen.go.
	//  3. Call the generated helper from cell Init:
	//     if err := mycell.RegisterReadiness(reg, c.repo); err != nil { ... }
	RegisterReadiness(name healthz.ProbeName, prober healthz.Prober) error

	// Lifecycle appends a lifecycle hook. Name must be non-empty; passing an
	// empty Name panics (programming error). Hooks run in declaration order
	// on startup and in reverse order on shutdown.
	Lifecycle(h LifecycleHook)

	// OnConfigReload registers a config-reload callback. prefixes==nil means
	// the callback is invoked on every reload. When non-nil, the callback is
	// invoked only when at least one changed key matches a prefix. An empty
	// string inside prefixes is a programming error and panics.
	OnConfigReload(prefixes []string, fn func(context.Context, ConfigChangeEvent) error)

	// GRPCService records a gRPC service declaration (GAP-1 PR-7 [#1150]).
	// The spec is validated and accumulated during Init; bootstrap drains
	// RegistrySnapshot.GRPCServices in phase7b — after binding listeners but
	// before calling grpcServeAll — so services are reachable from the first
	// accepted connection.
	//
	// spec.Register holds a func(grpc.ServiceRegistrar) callback (stored as any
	// because kernel/ must not import grpc). The runtime/grpc layer type-asserts
	// and invokes it. A nil Register fails Validate; a wrong dynamic type panics
	// via panicregister.Approved("grpc-registrar-bad-register-fn", …) in
	// runtime/grpc.ServiceRegistrar.Register.
	//
	// Returns a non-nil error when:
	//   - spec.Validate() fails (empty ContractID / CellID / zero Listener / nil Register)
	//   - the ContractID has already been registered by this cell (dedup by ContractID)
	//
	// Cell.Init should propagate the error via `if err := ...; err != nil { return err }`.
	GRPCService(spec GRPCServiceSpec) error
}

Registrar is the single registration surface a Cell uses inside Init to declare all capabilities. Each method appends to the RegistryRecorder's internal state. Calling any registration method after Snapshot() panics to prevent lazy-registration bugs.

type RegistryRecorder

type RegistryRecorder struct {
	// contains filtered or unexported fields
}

RegistryRecorder implements Registrar. It accumulates declarations during a Cell's Init call and returns them as a RegistrySnapshot. Once Snapshot() is called the recorder is finalized; any subsequent registration method panics to prevent lazy-registration bugs.

func NewRegistryRecorder

func NewRegistryRecorder(cfg map[string]any, mode outbox.DurabilityMode) *RegistryRecorder

NewRegistryRecorder constructs a RegistryRecorder with the given config snapshot and durability mode. The recorder is a pure write-side accumulator: probes registered via reg.RegisterReadiness(...) are collected into the RegistrySnapshot.Probes slice, which the bootstrap layer drains onto the runtime healthz.Aggregator after Init — the recorder never holds a live aggregator, mirroring how RouteGroups / Subscriptions are accumulated.

func NewRegistryRecorderWithLogger

func NewRegistryRecorderWithLogger(cfg map[string]any, mode outbox.DurabilityMode, log *slog.Logger) *RegistryRecorder

NewRegistryRecorderWithLogger constructs a RegistryRecorder with a custom logger. Provided for testing so log output can be captured.

func (*RegistryRecorder) Config

func (r *RegistryRecorder) Config() map[string]any

Config returns the per-cell config snapshot.

func (*RegistryRecorder) DurabilityMode

func (r *RegistryRecorder) DurabilityMode() outbox.DurabilityMode

DurabilityMode returns the assembly-level durability mode.

func (*RegistryRecorder) GRPCService

func (r *RegistryRecorder) GRPCService(spec GRPCServiceSpec) error

GRPCService validates and appends a GRPCServiceSpec (record-only — see Registrar.GRPCService godoc). Mirrors RegisterProjection: validate → dedup-by-ContractID → append.

func (*RegistryRecorder) Lifecycle

func (r *RegistryRecorder) Lifecycle(h LifecycleHook)

Lifecycle appends a lifecycle hook. Panics when Name is empty (programming error).

func (*RegistryRecorder) OnConfigReload

func (r *RegistryRecorder) OnConfigReload(
	prefixes []string,
	fn func(context.Context, ConfigChangeEvent) error,
)

OnConfigReload registers a config-reload callback. Panics when prefixes contains an empty string or fn is nil (programming errors).

func (*RegistryRecorder) RegisterProjection

func (r *RegistryRecorder) RegisterProjection(req ProjectionRequest) error

RegisterProjection validates and appends a ProjectionRequest (record-only — see Registrar.RegisterProjection godoc). The common checks mirror Subscribe: non-nil Apply, non-empty ProjectionID / CellID. The Spec check branches on Source: an outbox projection requires an event-kind Spec with a non-empty Topic (it becomes an event subscription once drained); a saga-journal projection has NO event topic, so it MUST carry the zero-value Spec (EPIC #1609 PR-05). OnReset is optional and not validated.

func (*RegistryRecorder) RegisterReadiness

func (r *RegistryRecorder) RegisterReadiness(name healthz.ProbeName, prober healthz.Prober) error

RegisterReadiness accumulates a probe under the typed name. First-wins duplicate semantics match the runtime aggregator: a second registration with the same name returns healthz.ErrDuplicateProbe and is not stored.

name is the single source of truth: the recorder wraps prober.Check under name (via healthz.NewProbe) even when prober is itself a healthz.Probe with a different Name(), so the probe surfaced from RegistrySnapshot.Probes always reports the funnel-supplied name.

func (*RegistryRecorder) RegisterWebhookDispatch

func (r *RegistryRecorder) RegisterWebhookDispatch(
	spec webhook.DispatchSpec,
	selector webhook.WebhookDispatchSelector,
) error

RegisterWebhookDispatch validates and appends a WebhookDispatchRequest (record-only — see Registrar.RegisterWebhookDispatch godoc).

func (*RegistryRecorder) RegisterWebhookReceiver

func (r *RegistryRecorder) RegisterWebhookReceiver(
	spec webhook.ReceiverSpec,
	handler webhook.WebhookReceiveHandler,
) error

RegisterWebhookReceiver validates and appends a WebhookReceiverRequest (record-only — see Registrar.RegisterWebhookReceiver godoc).

func (*RegistryRecorder) RouteGroup

func (r *RegistryRecorder) RouteGroup(g RouteGroup)

RouteGroup appends a RouteGroup declaration.

func (*RegistryRecorder) Snapshot

func (r *RegistryRecorder) Snapshot() RegistrySnapshot

Snapshot finalizes the recorder and returns an immutable RegistrySnapshot. After Snapshot is called, any further registration method panics.

Health probes ARE included in the snapshot (Probes), accumulated from reg.RegisterReadiness(...) calls during Init. The bootstrap layer drains them onto the runtime healthz.Aggregator after all cells initialize — identical to how RouteGroups / Subscriptions / LifecycleHooks are drained.

func (*RegistryRecorder) Subscribe

func (r *RegistryRecorder) Subscribe(
	spec contractspec.ContractSpec,
	handler outbox.EntryHandler,
	consumerGroup string,
	cellID string,
	opts ...SubscriptionOption,
) error

Subscribe validates and appends a SubscriptionRequest.

func (*RegistryRecorder) Subscription

func (r *RegistryRecorder) Subscription(spec contractspec.ContractSpec) *subscriptionDraft

Subscription begins a fluent subscription registration (see Registrar.Subscription). It is a pure allocator: the mustNotBeFinalized guard and all validation are applied by the terminal Register, which routes through Subscribe. The returned type is unexported by design (sealed type-state builder): callers chain reg.Subscription(spec).CellID(id)... and never name it.

type RegistrySnapshot

type RegistrySnapshot struct {
	RouteGroups     []RouteGroup
	Subscriptions   []SubscriptionRequest
	LifecycleHooks  []LifecycleHook
	ConfigReloaders []ConfigReloadRequest

	// Probes are the cell-level readiness probes declared during Init via
	// reg.RegisterReadiness(...). The bootstrap layer drains them onto the
	// runtime healthz.Aggregator after every cell has initialized — exactly
	// the same write-side-accumulate / read-side-drain split used for
	// RouteGroups, Subscriptions, and LifecycleHooks. The recorder does NOT
	// hold a live aggregator; it is a pure accumulator.
	Probes []healthz.Probe

	// WebhookReceivers are the inbound-webhook receivers declared during Init
	// via reg.RegisterWebhookReceiver(...). PR-2 only accumulates them; the
	// bootstrap receiver runtime drains this slice in PR-3 (no drain exists
	// yet) — same write-side-accumulate / read-side-drain split as Subscriptions.
	WebhookReceivers []WebhookReceiverRequest

	// WebhookDispatchers are the outbound-webhook dispatchers declared during
	// Init via reg.RegisterWebhookDispatch(...). PR-2 only accumulates them; the
	// bootstrap dispatcher runtime drains this slice in PR-5.
	WebhookDispatchers []WebhookDispatchRequest

	// Projections are the L3 CQRS projections declared during Init via
	// reg.RegisterProjection(...). The bootstrap projection drain constructs one
	// projection.Coordinator per request and calls Coordinator.Subscribe — same
	// write-side-accumulate / read-side-drain split as Subscriptions.
	Projections []ProjectionRequest

	// GRPCServices are the gRPC service declarations accumulated via
	// reg.GRPCService(...) during Init (GAP-1 PR-7 [#1150]). bootstrap drains
	// them in phase7b — after binding gRPC listeners but before grpcServeAll —
	// routing each spec to the listener identified by spec.Listener.
	GRPCServices []GRPCServiceSpec
}

RegistrySnapshot is the immutable result of a Cell's Init registration pass. Bootstrap reads these fields to wire routes, subscriptions, lifecycle hooks, config-reload callbacks, and readiness probes. Each field accumulates during Init via the Registrar interface (e.g. reg.RegisterReadiness for probes) and is drained by bootstrap into the runtime backbone after Init completes — see each field's godoc for the write-side-accumulate / read-side-drain pattern.

type RouteGroup

type RouteGroup struct {
	Listener   ListenerRef
	Prefix     string
	Middleware []func(http.Handler) http.Handler
	// Register is called by bootstrap to mount the cell's sub-tree on the
	// chosen mux. Required; a nil Register is a programmer error detected
	// at phase5 validation time.
	Register func(mux RouteMux) error
	// CellID is set automatically by bootstrap during phase5CollectRouteGroups.
	CellID string
}

RouteGroup declares where a batch of routes physically lives: which listener and what path prefix. The group inherits its listener's auth chain uniformly.

ref: go-kratos/kratos transport/http/server.go@main:L143-L224 — route spec accumulation.

func SingleGroup

func SingleGroup(l ListenerRef, prefix string, fn func(RouteMux) error) RouteGroup

SingleGroup is a convenience constructor for the common single-listener, single-prefix case.

DX-05: reduces boilerplate in cells that declare a single route group.

type RouteHandler

type RouteHandler interface {
	Handle(pattern string, handler http.Handler)
}

RouteHandler is the minimum route-registration surface shared by both the production RouteMux and stdlib *http.ServeMux.

type RouteMux

type RouteMux interface {
	// Handle registers handler for the given pattern.
	// Pattern follows Go 1.22+ enhanced ServeMux syntax: "METHOD /path/{param}".
	Handle(pattern string, handler http.Handler)

	// Route creates a sub-router under pattern with prefix stripping.
	Route(pattern string, fn func(sub RouteMux))

	// Mount attaches an opaque http.Handler sub-tree under pattern with prefix
	// stripping. The mounted handler does not need to follow GoCell routing
	// conventions.
	Mount(pattern string, handler http.Handler)

	// Group creates a same-level scope sharing the parent prefix.
	Group(fn func(RouteMux))

	// With returns a new RouteMux that inherits all routes and middleware
	// from this scope, plus the additional middleware provided.
	//
	// ref: go-chi/chi Mux.With — returns an inline router sharing the parent tree.
	With(mw ...func(http.Handler) http.Handler) RouteMux
}

RouteMux is a minimal route registration interface. kernel/ does not import any specific router (chi, gorilla, etc.); concrete implementations are provided by runtime/ or adapters/.

For testing, use kernel/cell/celltest.TestMux.

type Slice

type Slice interface {
	// ID returns the slice's stable identifier (matches slice.yaml id).
	ID() string
	// BelongsToCell returns the parent cell's ID.
	BelongsToCell() string
	// ConsistencyLevel reports the slice's consistency tier (from slice.yaml,
	// which is the strict SoR; inheritance from cell.consistencyLevel is not supported).
	ConsistencyLevel() cellvocab.Level
	// Init runs slice-local initialisation (no dependencies; that lives on Cell.Init).
	Init(ctx context.Context) error
	// Verify returns the verification spec parsed from slice.yaml.
	Verify() VerifySpec
	// AllowedFiles returns the file ownership paths. Returns nil when unset;
	// callers should treat nil as a configuration error (FMT-14 requires this field).
	AllowedFiles() []string
	// AffectedJourneys returns the journey IDs this slice participates in.
	AffectedJourneys() []string
}

Slice is a cohesive sub-unit within a Cell — typically a single feature (handler + service + repo + tests) that maps 1:1 to a slice.yaml entry.

K#04 codegen pattern: slice.yaml is the single source of truth (SoR) for all slice-level metadata. `gocell generate cell` emits slice_gen.go containing a `var sliceMeta` literal and `SliceMetadata() *metadata.SliceMeta` accessor. Cell composition roots call `cell.MustNewBaseSliceFromMeta(<slicePkg>.SliceMetadata())` — the typed funnel that projects the runtime-consumed metadata subset into BaseSlice (id / cellID / consistencyLevel / Verify / AllowedFiles / AffectedJourneys). SliceMeta.Lifecycle is intentionally NOT projected into BaseSlice and is NOT exposed on this interface: it is a metadata-only field consumed by governance/catalog via SliceMeta.Lifecycle directly. The asymmetry with Cell.Lifecycle() is principled — Cell has a runtime aggregator consumer (runtime/lifecycle), Slice does not; adding an accessor here would be dead code (ADR 202605262100 §E52). Direct `cell.NewBaseSlice` literals and `metadata.SliceMeta{...}` hand-written literals are forbidden in non-gen production code (BASESLICE-CTOR-FUNNEL-01 archtest). The `Init` lifecycle method is generated in cell_gen.go; business-specific initialisation lives in the hand-written `initInternal(ctx, reg)` hook (K#04 convention).

type SubscriptionOption

type SubscriptionOption func(*SubscriptionRequest)

SubscriptionOption mutates a SubscriptionRequest to attach optional metadata.

func WithSubscriptionBrokerDelaySchedule

func WithSubscriptionBrokerDelaySchedule(delays []time.Duration) SubscriptionOption

WithSubscriptionBrokerDelaySchedule opts the subscription into broker-native delayed re-delivery with the given per-attempt wait schedule (#1458). Empty or nil is a no-op (immediate-requeue behavior is preserved).

The slice is cloned so a caller that mutates its backing array after registration cannot retroactively alter the runtime delay topology (the recorded SubscriptionRequest owns an independent copy).

func WithSubscriptionSliceID

func WithSubscriptionSliceID(sliceID string) SubscriptionOption

WithSubscriptionSliceID declares the owning slice for subscription observability.

type SubscriptionRequest

type SubscriptionRequest struct {
	Spec          contractspec.ContractSpec
	Handler       outbox.EntryHandler
	ConsumerGroup string
	SliceID       string

	// CellID is the cell that owns this subscription — observability owner,
	// distinct from ConsumerGroup (broker partition key + idempotency
	// namespace). The cell declares it explicitly during Init via the
	// positional cellID parameter on Registrar.Subscribe; codegen
	// (contractgen + cellgen) injects the value from cell metadata at
	// compile time. Bootstrap's drainCellSubscriptions cross-checks that
	// CellID equals the snapshot key (fail-fast on drift) and does NOT
	// silently populate it: a missing CellID is a compile failure at the
	// reg.Subscribe call site, not a runtime fallback.
	//
	// Example: accesscore registers subscriptions with
	//   ConsumerGroup = "accesscore-rbac-session-sync"
	//   CellID        = "accesscore"   (positional parameter, from cell metadata)
	// so that the resulting outbox.Subscription.CellID is "accesscore" — the
	// cell — without ever proxying ConsumerGroup as cell identity.
	//
	// ref: ThreeDotsLabs/watermill router.AddHandler handlerName / NATS subscription metadata.
	// ref: ADR docs/architecture/202605111000-adr-subscription-cellid-mandatory.md
	CellID string

	// BrokerDelaySchedule opts the subscription into broker-native delayed
	// re-delivery (#1458). It flows through to outbox.Subscription.BrokerDelaySchedule;
	// see that field for semantics. Empty for ordinary event subscriptions;
	// the webhook-dispatch drain sets it to DefaultSvixSchedule().Delays().
	BrokerDelaySchedule []time.Duration
}

SubscriptionRequest holds everything needed to register one event subscription. RegistryRecorder accumulates these; bootstrap drains them at phase6.

type SubscriptionValidator

type SubscriptionValidator func(outbox.Subscription) error

SubscriptionValidator validates a Subscription at registration time.

ref: opentelemetry-collector otelcol/config.go Validate() — declarative validation at config load time.

type SubscriptionValidatorAdder

type SubscriptionValidatorAdder interface {
	AddSubscriptionValidator(SubscriptionValidator)
}

SubscriptionValidatorAdder lets composition roots inject registration-time validators. Implementations of an event router MAY also implement this interface.

type VerifySpec

type VerifySpec struct {
	Unit     []string
	Contract []string
	Waivers  []Waiver
}

VerifySpec describes the verification requirements for a Slice.

type Waiver

type Waiver struct {
	Contract  string
	Owner     string
	Reason    string
	ExpiresAt string
}

Waiver records a temporary exemption from a contract verification.

type WebhookDispatchRequest

type WebhookDispatchRequest struct {
	Spec     webhook.DispatchSpec
	Selector webhook.WebhookDispatchSelector
}

WebhookDispatchRequest holds everything needed to register one outbound-webhook dispatcher. RegistryRecorder accumulates these via Registrar.RegisterWebhookDispatch; the bootstrap dispatcher runtime (PR-5) drains them. Mirrors SubscriptionRequest's exported-field shape.

type WebhookReceiverRequest

type WebhookReceiverRequest struct {
	Spec    webhook.ReceiverSpec
	Handler webhook.WebhookReceiveHandler
}

WebhookReceiverRequest holds everything needed to register one inbound-webhook receiver. RegistryRecorder accumulates these via Registrar.RegisterWebhookReceiver; the bootstrap receiver runtime (PR-3) drains them. Mirrors SubscriptionRequest's exported-field shape.

Directories

Path Synopsis
Package celltest provides test utilities for kernel/cell types.
Package celltest provides test utilities for kernel/cell types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL