Projection initial values and labels
Use initial values for state that an event mapping does not supply. Labels describe an artifact for organization; they never select or filter events.
Declare initial state
Section titled “Declare initial state”This declaration comes from the compiling stock example.
It uses chronicle, projections, and readmodels. Call stockDeclarations()
before constructing your client; register and read it as shown in the
projection guide.
type StockRegistered struct { Name string `json:"name"`}
type Stock struct { ID string `json:"id"` Name string `json:"name" chronicle:"set(StockRegistered)"` Available int32 `json:"available"` Locations []string `json:"locations"` Note *string `json:"note"`}
func stockDeclarations() (*chronicle.Registry, readmodels.Model[Stock], error) { registry := chronicle.NewRegistry() registered, err := chronicle.RegisterEvent[StockRegistered](registry) if err != nil { return nil, readmodels.Model[Stock]{}, err } model, err := chronicle.RegisterReadModel[Stock](registry) if err != nil { return nil, model, err } declaration := projections.ModelBound(model, projections.FromEvent(registered), projections.WithInitialValues(Stock{Available: 10, Locations: []string{}}), projections.WithInitialValue(projections.Path[Stock, *string]("note"), nil), projections.WithLabels("inventory", "warehouse", "inventory")) return registry, model, registry.AddProjection(declaration)}Both ModelBound and NewBuilder accept these options. Initial values use the
registered model codec, including concepts, nested objects, explicit JSON names,
and the client’s frozen naming policy. Paths use declaration-time serialized names,
just like mapping paths. Snapshots rebind by declared Go field identity; if naming
changes embedded-field promotion and hides a supplied field, client construction
fails rather than assigning its value to the field that shadows it.
Preparation copies the serialized state; later mutation
of a slice, map, pointer, builder, registry, or returned protobuf cannot change the
submitted bytes or reconnect definition. Do not mutate inputs while preparing them.
| Option | Contract |
|---|---|
WithInitialValues(modelValue) | Typed whole model; repeated calls replace the state. No option means {} |
WithInitialValue(Path[M,V](path), value) | Typed scalar path, including concepts and nullable scalar pointers; no collection traversal. Duplicate writes or overlapping scalar paths fail |
WithLabels(labels...) | Copied artifact metadata; calls accumulate, exact duplicates keep their first occurrence. Case and nonblank whitespace remain significant |
Whole-model serialization preserves zero and false unless omitempty/omitzero
requests omission. A non-nil empty slice/map supplies []/{} in the definition;
nil properties are omitted. For ordinary collection properties, these values can
initialize materialized state. Projection-owned child collections are different:
the kernel excludes child roots when applying initial state, so an [] initializer
does not establish materialized child presence. Child events and mappings own it.
The scalar-path option supplies an explicit zero despite omission flags, or explicit
JSON null for a nil scalar pointer. Strings are JSON data, not projection-expression
grammar: quotes, punctuation, $null, and Unicode remain literal values.
Null roots, raw JSON initializers,
foreign model types, collection-null initializers, invalid codecs, and nonfinite
numbers are not accepted. Invalid declarations fail Build/NewClient before I/O.
Labels must be nonblank UTF-8 without control characters; they are not trimmed.
Materialization and protection boundaries
Section titled “Materialization and protection boundaries”Registration alone creates no model instance. The kernel applies initial state when an event materializes it; subsequent mappings update existing state. Ordinary AutoMap can overwrite an initialized property with the event’s matching property. For an aggregate-only mapping, the pinned kernel suppresses name AutoMap: an increment changes its target while leaving the initialized name unchanged. Explicit property mappings still apply. Removal removes the instance, and a later creating event initializes it again. Do not use registration or successful append as a sink-completion signal.
Initializer state is definition metadata, not an appended event. It does not pass through append-time encryption. Go therefore rejects any supplied protected root, including explicit null, when its schema carries PII or confidentiality metadata. Omitting that root is allowed. This conservative boundary also covers protected nested values and collections; it does not promise encrypted default support.
The pinned kernel’s all-instance replay used by ReadModelScenario omits initial
state. Scenarios with nonempty projection defaults fail with
ErrFidelityUnavailable; use production materialized reads instead. Scenario
support remains a #38 follow-up.
Explicit observer replay is separate from that scenario endpoint. An accepted replay
request or disappeared job does not prove completion, and unchanged materialized
read-back may still be pre-replay state. The kernel witness requires successful
terminal status for the exact replay job before comparing the resulting state;
if that evidence disappears, replay completion remains unverified.
Initial-value authoring does not promise history revision/redaction rewind
semantics or automatically trigger replay when only defaults change.
C# uses a whole-model callback; Go takes a typed value and snapshots it during preparation. Model-bound options and scalar-path presence are Go additions. Initial-state JSON object keys are sorted for deterministic identity rather than following C# declaration order. See the parity map for the source contract and evidence boundaries.