Summary
Add a first-class field.map field subtype for open-keyed maps — a value whose keys are data (e.g. wsd_senses {word→sense}, agent opinions {topic→stance}, relationships {name→relation}). Today these can only be modeled as opaque jsonb (unknown) or as a {key,value}[] pair-array workaround (which changes the stored shape + forces boundary transforms). field.map models them properly.
Design
- Keys are always strings — a hard JSON/
jsonb constraint (JSON object keys are strings). So no @keyType for v1.
- Value type is parameterized, exactly one of:
@valueType — a scalar value subtype (string/int/long/double/boolean/…), or
@objectRef — a value-object name (VO-valued map).
- Storage: a single
jsonb column holding the JSON object (no .array()).
- Generated types:
- TS Drizzle:
jsonb("col").$type<Record<string, V>>()
- TS Zod:
z.record(z.string(), <V>)
- TS inferred:
Record<string, V>
- Python:
dict[str, V]
- (JVM/C#:
Map<String,V> / Dictionary<string,V> when each port's persistence codegen lands)
field.map: { name: wsdSenses, valueType: string } # Record<string,string>
field.map: { name: scores, valueType: double } # Record<string,number>
field.map: { name: tools, objectRef: ToolSpec } # Record<string, ToolSpec>
Scope
- Metamodel (
spec/metamodel/field.json): new field.map subtype (dataType: object, extendsBase: true) with @valueType + @objectRef attrs (mutually exclusive; exactly one required). Regenerate embedded specs + the registry-conformance manifest + metamodel docs.
- Registration — all 5 ports (TS/Python/Java/Kotlin/C#): the subtype registers via the bundled spec; add the
FIELD_SUBTYPE_MAP / value-type constants where each port's code references them. Registry-conformance manifest byte-matches across ports.
- Codegen — TS complete:
column-mapper (jsonb + a map dollarTypeRef variant), inferred-types, zod-validators. Value = scalar OR a hoisted VO import.
- Cross-port codegen emit (Python first, then JVM/C#): map →
dict/Map/Dictionary in each port's persistence/runtime codegen.
- Conformance: a
field.map fixture (scalar-valued + VO-valued) gated across all ports; loader validation (exactly-one-of @valueType/@objectRef; reject on a non-object/array misuse).
Acceptance
field.map loads + validates in all 5 ports; registry-conformance green cross-port.
- TS emits correct Drizzle
.$type<Record<string,V>>() + Zod z.record + inferred type (scalar + VO values).
- Python emits
dict[str, V].
- Conformance corpus covers scalar + VO value maps; loader rejects both-attrs / neither-attr.
Out of scope (follow-ons)
@keyType semantic key validation (uuid/enum-keyed maps).
- Full JVM/C# persistence emit (registration lands now; emit as each port's codegen matures).
Motivation / context
Closes the "map gap" surfaced while modeling the draagon platform's wsd_senses and agent identityJson (opinions/relationships). Pair-array workarounds change stored shape + need transforms; this is the correct primitive — the open-keyed analogue of the field.object+isArray jsonb-VO support already shipped.
Summary
Add a first-class
field.mapfield subtype for open-keyed maps — a value whose keys are data (e.g.wsd_senses {word→sense}, agentopinions {topic→stance},relationships {name→relation}). Today these can only be modeled as opaque jsonb (unknown) or as a{key,value}[]pair-array workaround (which changes the stored shape + forces boundary transforms).field.mapmodels them properly.Design
jsonbconstraint (JSON object keys are strings). So no@keyTypefor v1.@valueType— a scalar value subtype (string/int/long/double/boolean/…), or@objectRef— a value-object name (VO-valued map).jsonbcolumn holding the JSON object (no.array()).jsonb("col").$type<Record<string, V>>()z.record(z.string(), <V>)Record<string, V>dict[str, V]Map<String,V>/Dictionary<string,V>when each port's persistence codegen lands)Scope
spec/metamodel/field.json): newfield.mapsubtype (dataType: object,extendsBase: true) with@valueType+@objectRefattrs (mutually exclusive; exactly one required). Regenerate embedded specs + the registry-conformance manifest + metamodel docs.FIELD_SUBTYPE_MAP/ value-type constants where each port's code references them. Registry-conformance manifest byte-matches across ports.column-mapper(jsonb + amapdollarTypeRefvariant),inferred-types,zod-validators. Value = scalar OR a hoisted VO import.dict/Map/Dictionaryin each port's persistence/runtime codegen.field.mapfixture (scalar-valued + VO-valued) gated across all ports; loader validation (exactly-one-of@valueType/@objectRef; reject on a non-object/array misuse).Acceptance
field.maploads + validates in all 5 ports; registry-conformance green cross-port..$type<Record<string,V>>()+ Zodz.record+ inferred type (scalar + VO values).dict[str, V].Out of scope (follow-ons)
@keyTypesemantic key validation (uuid/enum-keyed maps).Motivation / context
Closes the "map gap" surfaced while modeling the draagon platform's
wsd_sensesand agentidentityJson(opinions/relationships). Pair-array workarounds change stored shape + need transforms; this is the correct primitive — the open-keyed analogue of thefield.object+isArrayjsonb-VO support already shipped.