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 ¶
- Variables
- func MustHaveLifecycleHookName(h LifecycleHook)
- func MustHaveNonEmptyConfigPrefixes(prefixes []string)
- func MustHaveNonNilConfigReloadFn(fn func(context.Context, ConfigChangeEvent) error)
- func MustNotBeRegistryFinalized(finalized bool, method string)
- func RegisterEmitterHealthProbes(reg Registrar, emitter outbox.Emitter) error
- type AfterStarter
- type AfterStopper
- type Assembly
- type AuthRouteDeclarer
- type AuthRouteMeta
- type BaseCell
- func (b *BaseCell) AddConsumedContract(c Contract)
- func (b *BaseCell) AddProducedContract(c Contract)
- func (b *BaseCell) AddSlice(s Slice)
- func (b *BaseCell) ConsistencyLevel() cellvocab.Level
- func (b *BaseCell) ConsumedContracts() []Contract
- func (b *BaseCell) Health() HealthStatus
- func (b *BaseCell) ID() string
- func (b *BaseCell) Init(_ context.Context, _ Registrar) error
- func (b *BaseCell) Lifecycle() cellvocab.CellLifecycle
- func (b *BaseCell) Metadata() *metadata.CellMeta
- func (b *BaseCell) OwnedSlices() []Slice
- func (b *BaseCell) ProducedContracts() []Contract
- func (b *BaseCell) Ready() bool
- func (b *BaseCell) ShutdownCtx() context.Context
- func (b *BaseCell) Start(ctx context.Context) error
- func (b *BaseCell) Stop(_ context.Context) error
- func (b *BaseCell) Type() cellvocab.CellType
- type BaseContract
- func (c *BaseContract) ConsistencyLevel() cellvocab.Level
- func (c *BaseContract) ID() string
- func (c *BaseContract) Kind() cellvocab.ContractKind
- func (c *BaseContract) Lifecycle() cellvocab.ContractLifecycle
- func (c *BaseContract) OwnerCell() string
- func (c *BaseContract) SetLifecycle(lc cellvocab.ContractLifecycle)
- type BaseSlice
- func (s *BaseSlice) AffectedJourneys() []string
- func (s *BaseSlice) AllowedFiles() []string
- func (s *BaseSlice) BelongsToCell() string
- func (s *BaseSlice) ConsistencyLevel() cellvocab.Level
- func (s *BaseSlice) ID() string
- func (s *BaseSlice) Init(_ context.Context) error
- func (s *BaseSlice) SetAffectedJourneys(ids []string)
- func (s *BaseSlice) SetAllowedFiles(files []string)
- func (s *BaseSlice) SetVerify(v VerifySpec)
- func (s *BaseSlice) Verify() VerifySpec
- type BeforeStarter
- type BeforeStopper
- type Cell
- type CellIdentity
- type CellInventory
- type CellLifecycle
- type CellStatus
- type ConfigChangeEvent
- type ConfigReloadRequest
- type Contract
- type GRPCServiceRegistrar
- type GRPCServiceSpec
- type HTTPContractDeclarer
- type HealthStatus
- type HookEvent
- type HookOutcome
- type HookPhase
- type LifecycleHook
- type LifecycleHookObserver
- type ListenerRef
- type NopHookObserver
- type Prefixer
- type ProjectionApply
- type ProjectionRequest
- type ProjectionResetHook
- type Registrar
- type RegistryRecorder
- func (r *RegistryRecorder) Config() map[string]any
- func (r *RegistryRecorder) DurabilityMode() outbox.DurabilityMode
- func (r *RegistryRecorder) GRPCService(spec GRPCServiceSpec) error
- func (r *RegistryRecorder) Lifecycle(h LifecycleHook)
- func (r *RegistryRecorder) OnConfigReload(prefixes []string, fn func(context.Context, ConfigChangeEvent) error)
- func (r *RegistryRecorder) RegisterProjection(req ProjectionRequest) error
- func (r *RegistryRecorder) RegisterReadiness(name healthz.ProbeName, prober healthz.Prober) error
- func (r *RegistryRecorder) RegisterWebhookDispatch(spec webhook.DispatchSpec, selector webhook.WebhookDispatchSelector) error
- func (r *RegistryRecorder) RegisterWebhookReceiver(spec webhook.ReceiverSpec, handler webhook.WebhookReceiveHandler) error
- func (r *RegistryRecorder) RouteGroup(g RouteGroup)
- func (r *RegistryRecorder) Snapshot() RegistrySnapshot
- func (r *RegistryRecorder) Subscribe(spec contractspec.ContractSpec, handler outbox.EntryHandler, ...) error
- func (r *RegistryRecorder) Subscription(spec contractspec.ContractSpec) *subscriptionDraft
- type RegistrySnapshot
- type RouteGroup
- type RouteHandler
- type RouteMux
- type Slice
- type SubscriptionOption
- type SubscriptionRequest
- type SubscriptionValidator
- type SubscriptionValidatorAdder
- type VerifySpec
- type Waiver
- type WebhookDispatchRequest
- type WebhookReceiverRequest
Constants ¶
This section is empty.
Variables ¶
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 ¶
MustNotBeRegistryFinalized panics when finalized is true (programming error).
func RegisterEmitterHealthProbes ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
AddConsumedContract appends a Contract this cell consumes.
func (*BaseCell) AddProducedContract ¶
AddProducedContract appends a Contract this cell produces.
func (*BaseCell) ConsistencyLevel ¶
func (*BaseCell) ConsumedContracts ¶
ConsumedContracts returns a copy of the consumed contract list.
func (*BaseCell) Health ¶
func (b *BaseCell) Health() HealthStatus
Health returns the current HealthStatus.
func (*BaseCell) Lifecycle ¶
func (b *BaseCell) Lifecycle() cellvocab.CellLifecycle
func (*BaseCell) Metadata ¶
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 ¶
OwnedSlices returns a copy of the owned slice list.
func (*BaseCell) ProducedContracts ¶
ProducedContracts returns a copy of the produced contract list.
func (*BaseCell) ShutdownCtx ¶
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 ¶
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().
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 (c *BaseContract) Kind() cellvocab.ContractKind
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 ¶
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 ¶
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 ¶
AffectedJourneys returns a copy of the journey IDs this slice participates in.
func (*BaseSlice) AllowedFiles ¶
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 (*BaseSlice) ConsistencyLevel ¶
func (*BaseSlice) SetAffectedJourneys ¶
SetAffectedJourneys sets the journey IDs.
func (*BaseSlice) SetAllowedFiles ¶
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 ¶
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 ¶
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 ¶
type Cell interface {
CellIdentity
CellLifecycle
CellStatus
CellInventory
}
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.
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 {
// Spec is the event-kind contract the projection consumes (its input
// stream). Spec.Kind must be "event".
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.
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, constructs a projection.Coordinator per request from framework-owned dependencies, and calls Coordinator.Subscribe. Mirrors SubscriptionRequest's exported-field shape; the framework deps (checkpoint store / tx runner / cursor / replay) are deliberately NOT fields here — they live in bootstrap and never reach cell code.
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). Validation mirrors Subscribe (the projection becomes an event subscription once drained): non-nil Apply, non-empty ProjectionID / CellID, and an event-kind Spec with a non-empty Topic. OnReset is optional and not validated.
func (*RegistryRecorder) RegisterReadiness ¶
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 ¶
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 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
}
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 ¶
VerifySpec describes the verification requirements for a Slice.
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.