Documentation
¶
Overview ¶
Package sharedsection is the ADR-0105 D2 worked example: two components (Label, State) binding ONE tagged section, generated under a caller-assigned membership-id wrapper (marshallgen.FixedIdsWrapper) instead of the default per-plan declaration-order ids. With registry-stable unique ids the disjoint-sections gate relaxes to id-level disjointness (ADR-0100 SD6, as corrected 2026-08-10), and the membership match — not a section partition — keeps the co-resident kinds apart. The generated files (*.out.go, *.out.sql) come from gen_test.go, like recordstore/example.
Index ¶
- Constants
- Variables
- func GetAssetSchemaInManipulator() (manip *common.TableManipulator, err error)
- type AssetCache
- func (inst *AssetCache[W]) AdvanceEpoch()
- func (inst *AssetCache[W]) Get(key uint64) (ent *AssetEntity, found bool)
- func (inst *AssetCache[W]) GetFetch(ctx context.Context, key uint64) (ent *AssetEntity, found bool, err error)
- func (inst *AssetCache[W]) Invalidate(key uint64)
- func (inst *AssetCache[W]) InvalidateAll()
- func (inst *AssetCache[W]) IterateReadyWorkItems(ctx context.Context) iter.Seq[W]
- func (inst *AssetCache[W]) IterateRestWorkItems(ctx context.Context) iter.Seq[W]
- func (inst *AssetCache[W]) MarkStale(key uint64)
- func (inst *AssetCache[W]) MarkStaleIfOlder(key uint64, order time.Time)
- func (inst *AssetCache[W]) WorkItem(w W) iter.Seq[functional.NilIteratorValueType]
- type AssetCacheConfig
- type AssetEntity
- type AssetEntityBuilder
- func (inst *AssetEntityBuilder) AddLabel(row Label) *AssetEntityBuilder
- func (inst *AssetEntityBuilder) AddState(row State) *AssetEntityBuilder
- func (inst *AssetEntityBuilder) Commit() (err error)
- func (inst *AssetEntityBuilder) Raw() *lowlevel.InEntityAssetTable
- func (inst *AssetEntityBuilder) Rollback() (err error)
- type AssetStore
- func (inst *AssetStore) Begin(id uint64, ts time.Time) *AssetEntityBuilder
- func (inst *AssetStore) Buffered() int
- func (inst *AssetStore) Close()
- func (inst *AssetStore) DiscardPending()
- func (inst *AssetStore) EnsureTable(ctx context.Context) (err error)
- func (inst *AssetStore) Flush(ctx context.Context) (n int, err error)
- func (inst *AssetStore) IngestLabel(ts time.Time, rows []Label) (err error)
- func (inst *AssetStore) IngestState(ts time.Time, rows []State) (err error)
- func (inst *AssetStore) Latest(ctx context.Context, key uint64) (ent *AssetEntity, found bool, err error)
- func (inst *AssetStore) Replay(ctx context.Context, key uint64, fromOrder time.Time, ...) iter.Seq2[*AssetEntity, error]
- func (inst *AssetStore) ScanLabel(ctx context.Context, opts recordstore.ScanOpts) iter.Seq2[*AssetEntity, error]
- func (inst *AssetStore) ScanState(ctx context.Context, opts recordstore.ScanOpts) iter.Seq2[*AssetEntity, error]
- func (inst *AssetStore) VerifySchema(ctx context.Context) (err error)
- type AssetStoreConfig
- type Label
- type State
Constants ¶
const ( AssetColKey = `"id:id:u64:::0:"` AssetColOrder = `"ts:ts:z64:::0:"` )
Physical (encoded, quoted) names of the envelope role columns, derived from the IR at generation time — exported so consumers can address them in ScanOpts.ExtraPredicate and their own SQL.
const AssetTableName = "asset"
AssetTableName is the ClickHouse table this store binds — database- qualified ("<db>.<table>") when a Database was set at generation.
const TableRowConfig = common.TableRowConfigMultiAttributesPerRow
TableRowConfig matches anchor's: multiple attributes per row — the shared section carries one attribute per resident component.
Variables ¶
var AssetComponentSQL = componentsql.Set{ Store: "Asset", Table: AssetTableName, Kinds: map[string]componentsql.Artefacts{ "Label": { Presence: "has(\"tv:symbol:lr:lr:u64:1247:::0::data\", 7001)", Validator: "countEqual(\"tv:symbol:lr:lr:u64:1247:::0::data\", 7001) = 1", Filter: assetScanLabelFilter, Projection: "CAST(tuple(\"id:id:u64:::0:\", LW_VALUE_BY_TAG_EQUAL(\"tv:symbol:value:val:s::::0::data\", \"tv:symbol:lr:lr:u64:1247:::0::data\", 7001, LW_RAGGED_PARENT_IDS(\"tv:symbol:lrcard:lrcard:u64:4E:::0::data\"))), 'Tuple(ID UInt64, Name String)')", }, "State": { Presence: "has(\"tv:symbol:lr:lr:u64:1247:::0::data\", 7002)", Validator: "countEqual(\"tv:symbol:lr:lr:u64:1247:::0::data\", 7002) = 1", Filter: assetScanStateFilter, Projection: "CAST(tuple(\"id:id:u64:::0:\", LW_VALUE_BY_TAG_EQUAL(\"tv:symbol:value:val:s::::0::data\", \"tv:symbol:lr:lr:u64:1247:::0::data\", 7002, LW_RAGGED_PARENT_IDS(\"tv:symbol:lrcard:lrcard:u64:4E:::0::data\"))), 'Tuple(ID UInt64, Phase String)')", }, }, }
AssetComponentSQL publishes this store's ADR-0066 read-back artefacts — the SQL its component definitions generate — for an authoring surface to expand (ADR-0189). A host registers it into a componentsql.Registry; nothing here self-registers.
Filter is the same constant the Scan verbs use, so the store's own read path and the authoring surface cannot disagree about what a conforming row is. Projection must not be embedded without Filter — it locates an attribute by indexOf and returns the first match, so on a row carrying a membership twice it answers plausibly and wrongly (ADR-0066).
The column references are UNQUALIFIED, so a consumer embedding them in a join must bind them to AssetTableName itself (ADR-0189 SD6).
var AssetMembershipIdAssignment = map[string]uint64{
"assetName": 7001,
"assetPhase": 7002,
}
AssetMembershipIdAssignment is the caller-assigned membership → id snapshot the store generates under, standing in for a registry resolution (vdd-style stable TaggedIds). Deliberately far from 1..N so a declaration-order id leaking into any artefact could not accidentally match.
var AssetMembershipIds = map[string]map[string]uint64{
"Label": {
"assetName": 7001,
},
"State": {
"assetPhase": 7002,
},
}
AssetMembershipIds is the membership-id assignment this store was generated under: component kind -> membership name -> the uint64 id carried in the membership columns. Verbatim-channel memberships embed their literal name instead and are absent here.
The ids are caller-assigned (a registry-stable snapshot), baked into both the component codecs and this store's Scan filters. Nothing on the wire records which assignment wrote a row, so rows written under a different one decode as ABSENT rather than failing — VerifySchema cannot see it. Compare this map against the writer's before pointing a regenerated store at existing rows.
Functions ¶
func GetAssetSchemaInManipulator ¶
func GetAssetSchemaInManipulator() (manip *common.TableManipulator, err error)
GetAssetSchemaInManipulator builds the asset table: plain id (Key) and ts (Order) envelope columns, and one tagged section `symbol` that BOTH components bind — the layout the default id regime must refuse and the fixed-ids regime supports.
Types ¶
type AssetCache ¶
type AssetCache[W comparable] struct { // contains filtered or unexported fields }
AssetCache is the batched read-through, write-through KV view over a AssetStore (ADR-0100 SD5): misses queue under work items and flush as one IN (…) lookup, and local writes populate the view at Commit — pinned until the store's Flush makes them durable — so reads after writes hit immediately. Admission is version-gated on the entity's Order timestamp: a raced refetch of an older row bounces off. Only EXTERNAL writers can leave the view stale; they need a caller- provided signal: MarkStale / Invalidate / InvalidateAll (a freshness TTL option exists on the underlying cache). Raw() commits and discarded writes invalidate instead of populating. Like the store it wraps, a view is single-goroutine. W is the work-item type (use struct{} when the suspend/replay machinery is not needed).
func NewAssetCache ¶
func NewAssetCache[W comparable](st *AssetStore, cfg AssetCacheConfig) (inst *AssetCache[W])
NewAssetCache attaches a read-through, write-through cache view to st, registering its write-through and flush hooks with the store. Views attach for the store's lifetime — there is no detach.
func (*AssetCache[W]) AdvanceEpoch ¶
func (inst *AssetCache[W]) AdvanceEpoch()
AdvanceEpoch advances the cache's pinning epoch — call once per frame / batch so untouched L1 entries become evictable.
func (*AssetCache[W]) Get ¶
func (inst *AssetCache[W]) Get(key uint64) (ent *AssetEntity, found bool)
Get retrieves an entity by Key through the cache; local writes are visible immediately (write-through). A miss queues the key for the next batch fetch (the caching suspend/replay contract). A miss can also mean the batched fetch errored (misses swallow fetch errors; the circuit breaker backs off) — GetFetch surfaces the error instead, and the store's Latest stays the authoritative check. The returned entity is shared with the cache: treat it as immutable.
func (*AssetCache[W]) GetFetch ¶
func (inst *AssetCache[W]) GetFetch(ctx context.Context, key uint64) (ent *AssetEntity, found bool, err error)
GetFetch is the single-lookup read: the cached entity when present, otherwise one immediate batched point fetch — fetch errors surface instead of reading as misses, so found=false with err=nil is the authoritative absent. A key in the dirty write window that the cache could not answer is an error rather than a stale row (see below). Prefer Get plus the work-item protocol when batching lookups across a frame; the initial miss here also queues the key, so a later batch fetch may include it redundantly (harmless).
func (*AssetCache[W]) Invalidate ¶
func (inst *AssetCache[W]) Invalidate(key uint64)
Invalidate drops the key's cached entry (L1 and stash).
func (*AssetCache[W]) InvalidateAll ¶
func (inst *AssetCache[W]) InvalidateAll()
InvalidateAll drops every cached entry — the bulk external-writer signal (e.g. after an import). In-flight miss bookkeeping (queued keys, pending work items) and the dirty-window pins are dropped with it: call between frames, with no suspended work and no unflushed local writes (the fetcher's dirty-guard keeps pre-write rows out of the cleared cache until the next Flush, at the cost of misses on those keys).
func (*AssetCache[W]) IterateReadyWorkItems ¶
func (inst *AssetCache[W]) IterateReadyWorkItems(ctx context.Context) iter.Seq[W]
IterateReadyWorkItems flushes the queued keys when the fetch criteria are met and replays the work items that had misses.
func (*AssetCache[W]) IterateRestWorkItems ¶
func (inst *AssetCache[W]) IterateRestWorkItems(ctx context.Context) iter.Seq[W]
IterateRestWorkItems forces a fetch of all queued keys and replays the pending work items.
func (*AssetCache[W]) MarkStale ¶
func (inst *AssetCache[W]) MarkStale(key uint64)
MarkStale flags the key's cached entry as stale — the external-writer signal: the next strict read misses and queues a refetch, while accept-stale reads keep serving the old value until it lands.
func (*AssetCache[W]) MarkStaleIfOlder ¶
func (inst *AssetCache[W]) MarkStaleIfOlder(key uint64, order time.Time)
MarkStaleIfOlder is the version-carrying external-writer signal: it stales the cached entry only if its Order is below order, so a redundant signal for a version the view already holds is free — the natural sink for an invalidation stream carrying (key, Order).
func (*AssetCache[W]) WorkItem ¶
func (inst *AssetCache[W]) WorkItem(w W) iter.Seq[functional.NilIteratorValueType]
WorkItem marks the current work item for the cache's miss bookkeeping.
type AssetCacheConfig ¶
type AssetCacheConfig struct {
// Capacity is the L1 capacity in entries, not bytes — budget
// memory as Capacity × the largest expected entity payload. Zero
// or negative selects the default (1024).
Capacity int
// FetchCriteria are the cache's batch-flush thresholds.
FetchCriteria caching.FetchCriteria
// FreshnessTTL enables age-based staleness onset (ADR-0100's
// external-writer staleness story): entries older than this read
// as stale — strict reads miss and queue a refetch, accept-stale
// reads keep serving. Zero disables (staleness stays signal-only).
FreshnessTTL time.Duration
// NegativeTTL enables absent-key marking: keys a clean fetch did
// not return are treated as absent for this long — misses on them
// neither queue nor suspend work items, so replay loops over keys
// that do not exist terminate. Zero disables.
NegativeTTL time.Duration
}
AssetCacheConfig parameterizes an attached read-through cache view.
type AssetEntity ¶
type AssetEntity struct {
ID uint64
Ts time.Time
Label option.Option[Label]
State option.Option[State]
}
AssetEntity is the entity bag (ADR-0100 SD5): the envelope plus one option per bound component. Arrow-free — safe to hold in the cache. Entities returned by cached reads are shared with the cache (and every later reader): treat them as immutable.
func (*AssetEntity) Archetype ¶
func (inst *AssetEntity) Archetype() (a []string)
Archetype reports which components the entity carries, in schema order.
type AssetEntityBuilder ¶
type AssetEntityBuilder struct {
// contains filtered or unexported fields
}
AssetEntityBuilder assembles one entity: envelope from Begin, components via Add*, direct attribute manipulation via Raw, then Commit.
func (*AssetEntityBuilder) AddLabel ¶
func (inst *AssetEntityBuilder) AddLabel(row Label) *AssetEntityBuilder
AddLabel contributes the Label component to the open entity.
The attributes are buffered, not written: a section frame closes for good, so a component that closed its own sections would shut out the next component sharing one. Commit writes them, one frame per section in first-seen order (ADR-0183 D4).
A second Add of this component, or an Add on an entity already using Raw(), is refused: both used to mark the row un-mirrorable and carry on, which made its read-back shape depend on a call the writer had probably made by accident.
func (*AssetEntityBuilder) AddState ¶
func (inst *AssetEntityBuilder) AddState(row State) *AssetEntityBuilder
AddState contributes the State component to the open entity.
The attributes are buffered, not written: a section frame closes for good, so a component that closed its own sections would shut out the next component sharing one. Commit writes them, one frame per section in first-seen order (ADR-0183 D4).
A second Add of this component, or an Add on an entity already using Raw(), is refused: both used to mark the row un-mirrorable and carry on, which made its read-back shape depend on a call the writer had probably made by accident.
func (*AssetEntityBuilder) Commit ¶
func (inst *AssetEntityBuilder) Commit() (err error)
Commit finishes the open entity, buffers the row, and writes it through to attached cache views: the entity is populated and pinned until the store's Flush makes it durable — reads after writes hit immediately, and the caching version gate plus the pin make a raced refetch of the pre-write row bounce off. A commit that touched Raw() cannot be materialized faithfully and invalidates the key instead. A failed Commit rolls the frame back — the entity is discarded and the store stays usable.
func (*AssetEntityBuilder) Raw ¶
func (inst *AssetEntityBuilder) Raw() *lowlevel.InEntityAssetTable
Raw exposes the underlying DML entity for direct attribute manipulation within the same entity frame. The type lives in internal/lowlevel: callers outside the generated package hold the returned value by inference (raw := b.Raw()) and chain its methods, but cannot name the type in their own signatures.
func (*AssetEntityBuilder) Rollback ¶
func (inst *AssetEntityBuilder) Rollback() (err error)
Rollback abandons the open entity frame without committing it; already-buffered rows and the store remain usable.
type AssetStore ¶
type AssetStore struct {
// contains filtered or unexported fields
}
AssetStore is single-goroutine, like every part it composes. Batched cached retrieval is not built in — attach a AssetCache view.
func NewAssetStore ¶
func NewAssetStore(exec recordstore.ExecutorI, alloc memory.Allocator, cfg AssetStoreConfig) (inst *AssetStore)
NewAssetStore wires the store. A nil alloc selects the Go allocator. Configuring Stampers panics: see the field's doc — this schema cannot carry stamps soundly.
func (*AssetStore) Begin ¶
func (inst *AssetStore) Begin(id uint64, ts time.Time) *AssetEntityBuilder
Begin opens one entity with the envelope roles as typed arguments (Key, Order).
func (*AssetStore) Buffered ¶
func (inst *AssetStore) Buffered() int
Buffered reports the number of committed-but-unflushed rows.
func (*AssetStore) Close ¶
func (inst *AssetStore) Close()
Close discards everything unflushed and releases the store's Arrow builder; the store must not be used afterwards. Required for a clean shutdown under tracking/checked allocators — the default Go allocator needs no Close.
func (*AssetStore) DiscardPending ¶
func (inst *AssetStore) DiscardPending()
DiscardPending drops every committed-but-unflushed row: records retained by a failed Flush, rows still in the DML builder, and an open (uncommitted) entity frame. It gives a failed Flush "never happened" semantics — ClickHouse state is the truth afterwards. Ambient stamps are cleared with the frame they were pushed for — including any pushed through Raw() — so an abandoned builder cannot leak its stamps onto later entities.
func (*AssetStore) EnsureTable ¶
func (inst *AssetStore) EnsureTable(ctx context.Context) (err error)
EnsureTable applies the composed CREATE TABLE (plus the DDLTail suffix, when configured). Idempotent (CREATE TABLE IF NOT EXISTS). The embedded script is issued one statement per Exec — the optional CREATE DATABASE, then the CREATE TABLE — because the ClickHouse HTTP interface rejects a multi-statement body; under a Table override the statements are re-pointed at the override (recordstore.ProvisioningStatements: header and database only, the column block stays byte-identical).
func (*AssetStore) Flush ¶
func (inst *AssetStore) Flush(ctx context.Context) (n int, err error)
Flush drains the buffered rows to ClickHouse (Arrow IPC, ADR-0089 pivot). Rows are durable when Flush returns, engine permitting. On insert failure the transferred records are retained and the next Flush ships them — Flush is retryable; DiscardPending drops them instead. An open (uncommitted) entity frame makes Flush error.
func (*AssetStore) IngestLabel ¶
func (inst *AssetStore) IngestLabel(ts time.Time, rows []Label) (err error)
IngestLabel buffers one whole entity per row carrying only the Label component, all stamped with ts — rows ship on the next Flush, like every write. Keys must be distinct within one call (rows share ts, so duplicates would tie on Order): a duplicate returns recordstore.ErrDuplicateIngestKey. On any error the rows buffered so far remain buffered — Flush ships them, DiscardPending drops them.
func (*AssetStore) IngestState ¶
func (inst *AssetStore) IngestState(ts time.Time, rows []State) (err error)
IngestState buffers one whole entity per row carrying only the State component, all stamped with ts — rows ship on the next Flush, like every write. Keys must be distinct within one call (rows share ts, so duplicates would tie on Order): a duplicate returns recordstore.ErrDuplicateIngestKey. On any error the rows buffered so far remain buffered — Flush ships them, DiscardPending drops them.
func (*AssetStore) Latest ¶
func (inst *AssetStore) Latest(ctx context.Context, key uint64) (ent *AssetEntity, found bool, err error)
Latest returns the newest row for key, tombstone-blind (the raw row-level primitive — a deleted key still returns its tombstone row; GetLive is the interpreted state-view read). Reads see only flushed rows.
func (*AssetStore) Replay ¶
func (inst *AssetStore) Replay(ctx context.Context, key uint64, fromOrder time.Time, opts recordstore.ReplayOpts) iter.Seq2[*AssetEntity, error]
Replay iterates the rows for key with the order column >= fromOrder in ascending order — the event-replay primitive. A zero fromOrder replays everything (zero time.Time has no defined UnixNano; recordstore.SeqTs(0) is the equivalent explicit bound); opts.To bounds the replay exclusively ("state as of To") and opts.Limit caps the row count. The sequence is single-use; ctx must stay valid until iteration completes; the query may execute at call time or lazily during iteration (buffered in v1 — a streaming executor changes nothing visible); an error ends the sequence as a final (nil, err) pair. Reads see only flushed rows.
func (*AssetStore) ScanLabel ¶
func (inst *AssetStore) ScanLabel(ctx context.Context, opts recordstore.ScanOpts) iter.Seq2[*AssetEntity, error]
ScanLabel iterates the entities whose rows carry a conforming Label component, ordered by (Order, Key) — so entities sharing an Order still come out in a fixed sequence. Rows that tie on BOTH (the same key written twice at the same Order) are not ordered against each other by this clause; the table keeps newest-per-key, so which of them survives is the engine's choice, not the scan's. opts.ExtraPredicate (trusted raw SQL over the physical columns — never untrusted input) further restricts the scan; opts.Limit caps the row count. The Filter artefact uses ClickHouse built-ins only, so this is a single SELECT — no helper UDFs, no multi-statement script (the ExecutorI contract). The sequence is single-use; ctx must stay valid until iteration completes; an error ends it as a final (nil, err) pair. Scans see only flushed rows.
func (*AssetStore) ScanState ¶
func (inst *AssetStore) ScanState(ctx context.Context, opts recordstore.ScanOpts) iter.Seq2[*AssetEntity, error]
ScanState iterates the entities whose rows carry a conforming State component, ordered by (Order, Key) — so entities sharing an Order still come out in a fixed sequence. Rows that tie on BOTH (the same key written twice at the same Order) are not ordered against each other by this clause; the table keeps newest-per-key, so which of them survives is the engine's choice, not the scan's. opts.ExtraPredicate (trusted raw SQL over the physical columns — never untrusted input) further restricts the scan; opts.Limit caps the row count. The Filter artefact uses ClickHouse built-ins only, so this is a single SELECT — no helper UDFs, no multi-statement script (the ExecutorI contract). The sequence is single-use; ctx must stay valid until iteration completes; an error ends it as a final (nil, err) pair. Scans see only flushed rows.
func (*AssetStore) VerifySchema ¶
func (inst *AssetStore) VerifySchema(ctx context.Context) (err error)
VerifySchema compares the live table's columns — names and order — against the generated schema. EnsureTable alone cannot detect drift on an existing table (IF NOT EXISTS succeeds against any old shape), and the decode is positional, so drift fails late or, for same-typed column swaps, silently: run VerifySchema at startup after EnsureTable.
It checks the COLUMN contract only. The membership-id contract is not checked and cannot be from the schema alone: the ids live in the membership columns as ordinary values, and a FAT table legitimately carries other kinds' ids beside this store's. Rows written under a different id assignment therefore pass VerifySchema and then match nothing — every component decodes absent, with no error. Compare AssetMembershipIds against the writer's assignment when pointing this store at rows it did not write.
What it describes is the reader's own projection — DESCRIBE over `SELECT *`, not over the table — because that is what the positional decode consumes. DESCRIBE TABLE lists columns SELECT * does not return (MATERIALIZED, ALIAS, EPHEMERAL), so a table legitimately carrying one beside the generated shape — a derived column added by ALTER after EnsureTable, which is how a store gets a skip index over a value its leeway attributes only encode — would fail a check whose contract still held. Describing the projection also follows the asterisk_include_* settings, which decide what SELECT * returns and which nothing here pins: under those, a derived column IS in the decode, and this notices where a column-kind filter would have blessed the mis-decode.
type AssetStoreConfig ¶
type AssetStoreConfig struct {
// Table overrides the ClickHouse table this store binds — the baked
// AssetTableName — for every statement it issues (DDL, DESCRIBE, INSERT,
// SELECT). Optionally database-qualified ("<db>.<table>"), unquoted-
// identifier shape only ([A-Za-z_][A-Za-z0-9_]* per part; the
// constructor panics otherwise). Empty (the default) binds the baked
// name. The schema is unchanged — this moves WHERE the rows land, not
// what they look like — so a scratch table for a test or a per-
// deployment table needs no regeneration.
Table string
// DDLTail is a raw suffix appended verbatim after the composed
// CREATE TABLE at EnsureTable time — the escape hatch for clauses
// the generation-time table options (ADR-0102) do not carry.
DDLTail string
// Stampers are consulted on every Begin (ADR-0112 M1): each yields
// surrogate ids stamped as additive HighCardRef memberships onto the
// entity's attributes. Empty (the default) leaves the store unstamped
// and behaviour-identical. A stamper must not write to this store.
// The schema must carry the HighCardRef membership lane, and no
// component may read that lane back as data — the constructor
// panics otherwise (ADR-0112 SD2 lane hygiene).
Stampers []recordstore.ReferenceStamper
// BestEffortStampFlush relaxes the ADR-0112 SD5 ordered flush: when
// true, Flush does NOT flush the stampers' dimension stores before its
// own insert, so a referencing row may become durable ahead of its
// descriptor fact (resolution self-heals on the dimension's own flush).
// The default keeps the descriptor durable no later than the row.
BestEffortStampFlush bool
}
type Label ¶
type Label struct {
ID uint64 `lw:",id"`
Name string `lw:"assetName,symbol"`
// contains filtered or unexported fields
}
Label and State (state_dto.go) are the two asset components. Unlike recordstore/example — where each kind owns a distinct section because per-plan declaration-order ids would collide — both bind the SAME `symbol` section under distinct memberships; the caller-assigned ids (AssetMembershipIdAssignment) keep their attributes apart on read.