Summary
There is no way to say "this non-persisted shape has a field that references entity X."
object.value is loader-banned from ALL identity/reference constructs (value purity —
subtype-rules.ts: "value objects MUST NOT have ANY identity (primary, secondary, reference)"
→ ERR_SUBTYPE_RULE_VIOLATION), and identity.reference/@references exist only on
object.entity, which implies a writable source/persistence a message does not have. The
only reference construct available on a value, relationship.association, names the target
entity but not which field carries the foreign key — so a value can gesture at an entity but
cannot bind an id field to it.
Why (concrete adopter need)
We are modeling a wire protocol (a poker engine's command messages) as MetaObjects. Each
message is a non-persisted shape that references domain entities: AskForStake carries a
tableId that references Table; TableState carries a gameHandId referencing GameHand;
AnswerForStake carries a sessionId referencing a Session. These are genuine references —
a dangling target should fail the load, and codegen should be able to navigate/type them — but
the message is neither persisted nor identity-bearing. It is a referencing value (a DTO), a
shape the current value↔entity dichotomy has no slot for:
object.value → refuses the reference (value purity).
object.entity → forces a writable source + primary identity the message doesn't have.
relationship.association on a value → names Table but can't say tableId is the FK to it.
Relationship to #210 (and why this isn't a contradiction)
#210 pushes toward total value purity (retiring origin.* on object.value). This ask is not
"make values impure" — referencing an entity by id is not persistence. A DTO/message that
carries tableId and declares it references Table stores nothing, owns no identity, and has no
source; it only needs a navigation-only, non-enforced reference so the loader can resolve the
target (fail on dangling) and codegen can type it. Purity is about identity and storage;
this is about typed navigation on a transient shape.
Proposed shapes (either resolves it)
- A field-level, navigation-only reference on value objects — allow
identity.reference
with @enforce: false (the already-chartered logical-reference form) on object.value,
OR a new field-level @references attribute that binds one field to a target entity FQN,
resolved against the symbol table (dangling → ERR_INVALID_REFERENCE), emitting no FK/DDL.
object.value: { name: AskForStake, children: [
{ field.int: { name: tableId, "@references": "gaming::common::poker::Table" } }, # navigation-only
... ] }
- A new non-persisted-but-referencing object subtype (a "message"/"dto" shape) — like
object.value (no source, no primary identity, never a table) but permitted to carry
identity.reference @enforce:false children, so a transient DTO can declare its FKs.
We are adopting shape (2) locally via a registered provider subtype meanwhile, but the field↔target
binding on a transient shape feels like it belongs in core: DTOs referencing entities is a
universal pattern (API payloads, events, wire messages), not a poker-specific one.
Summary
There is no way to say "this non-persisted shape has a field that references entity X."
object.valueis loader-banned from ALL identity/reference constructs (value purity —subtype-rules.ts: "value objects MUST NOT have ANY identity (primary, secondary, reference)"→
ERR_SUBTYPE_RULE_VIOLATION), andidentity.reference/@referencesexist only onobject.entity, which implies a writablesource/persistence a message does not have. Theonly reference construct available on a value,
relationship.association, names the targetentity but not which field carries the foreign key — so a value can gesture at an entity but
cannot bind an id field to it.
Why (concrete adopter need)
We are modeling a wire protocol (a poker engine's command messages) as MetaObjects. Each
message is a non-persisted shape that references domain entities:
AskForStakecarries atableIdthat referencesTable;TableStatecarries agameHandIdreferencingGameHand;AnswerForStakecarries asessionIdreferencing aSession. These are genuine references —a dangling target should fail the load, and codegen should be able to navigate/type them — but
the message is neither persisted nor identity-bearing. It is a referencing value (a DTO), a
shape the current
value↔entitydichotomy has no slot for:object.value→ refuses the reference (value purity).object.entity→ forces a writablesource+ primary identity the message doesn't have.relationship.associationon a value → namesTablebut can't saytableIdis the FK to it.Relationship to #210 (and why this isn't a contradiction)
#210 pushes toward total value purity (retiring
origin.*onobject.value). This ask is not"make values impure" — referencing an entity by id is not persistence. A DTO/message that
carries
tableIdand declares it referencesTablestores nothing, owns no identity, and has nosource; it only needs a navigation-only, non-enforced reference so the loader can resolve the
target (fail on dangling) and codegen can type it. Purity is about identity and storage;
this is about typed navigation on a transient shape.
Proposed shapes (either resolves it)
identity.referencewith
@enforce: false(the already-chartered logical-reference form) onobject.value,OR a new
field-level@referencesattribute that binds one field to a target entity FQN,resolved against the symbol table (dangling →
ERR_INVALID_REFERENCE), emitting no FK/DDL.object.value(no source, no primary identity, never a table) but permitted to carryidentity.reference @enforce:falsechildren, so a transient DTO can declare its FKs.We are adopting shape (2) locally via a registered provider subtype meanwhile, but the field↔target
binding on a transient shape feels like it belongs in core: DTOs referencing entities is a
universal pattern (API payloads, events, wire messages), not a poker-specific one.