Before anything cruises, you build the coupe. Phase 1 is the machine underneath the whole program: the data model that lets a tag content block own an ordered set of tags directly — no collection library in between. Schema, generated code, and ent-level tests only; no UI, no render (those are Phases 2–3). Everything downstream — the Studio picker, the kontent chips, the api/v1 JSON, the Flutter renderer — reads this one shape.
Phase 1 — The Deuce Coupe: TagBlockItem schema
The join — three lines of schema
TagBlockItem is the whole model: (block, tag, sort_order). An ordered index on (block, sort_order) makes drag-reorder cheap; a unique index on (block, tag) means a tag appears at most once per block. The FK-holder convention — edge.To on the join, edge.From().Ref() on each parent — mirrors WeblinkCollectionItem exactly: a proven in-repo shape, not an invented one. Minus label_override, because tags need no per-item overrides.
Three edits total: the new join entity; "tag" appended to the block_type enum plus the tag_items edge on PostContentBlock; and the block_items inverse on Tag — the edge that later answers "how many posts use this tag?" through blocks.
Wiring & guardrails
- Additive auto-migrate only — a new table, one enum value, FK columns. No migration files, no destructive reset; Ent's
Schema.Createpicks it up on the next server start. - The ent-level test is the smoke (schema-only phase): create a tag block, attach three ordered items, read them back in order with edges loaded, prove a duplicate
(block, tag)violates the unique index, and walk the inverse edge back from the tag. - The enum-exhaustiveness seed — the maximum-safety anchor: a test listing every
block_typevalue and assertingtagis present. Later phases extend it to assert every kind is wired at every dispatch point, so no block type can ever silently miss again.