Hub-and-spoke skill that converts "I need a database for app X" into a designed relational schema + first migration + optional seed. Pipeline (5 phases, 9 AskUserQuestion calls total, pure-click after intake): - Phase 1 — batched DB/ORM/scale/style/migration-control click - Phase 2 — entity list + relations matrix (auto-junction tables) - Phase 3 — generate DDL with indexes, FKs, constraints; review/revise loop - Phase 4 — scaffold migrations/ + first timestamped migration + kei-migrate wiring - Phase 5 — optional seed (smoke / rich / test fixtures / skip) Cross-refs the five db-* blocks + the kei-migrate Rust primitive added in commitsf884891anddf85792on this branch. Emits ENV-VAR NAMES only for DATABASE_URL (RULE 0.8 secrets SSoT). Every file ≤ 121 LOC.
3 KiB
Phase 2 — Entities + relations matrix
Collect the entity list (typed) once, then click the relations matrix. This is the second (and last) phase that accepts typed input.
2a — Ask for entities (plain message, NOT AskUserQuestion)
List the entities (tables) and for each a short comma-separated field list. One entity per line, format:
<Entity>: field1, field2, .... Example:User: email, name, created_at Organization: name, plan Membership: user_id, org_id, role3–15 entities is typical. Keep it short — we'll refine fields in Phase 3.
Parse the reply into ENTITIES = [{name, fields: [...]}, ...]. Validate:
- Entity names are
PascalCase(normalize if user typesuser_profile→UserProfile, record normalization in state). - Each entity has ≥1 field.
- No duplicate entity names.
- If parse fails → re-ask once with a corrected example.
2b — Relations matrix click (AskUserQuestion, multi-select)
For each UNORDERED PAIR of entities (A, B), ask one multi-select row.
Skip pairs the user hasn't mentioned any cross-reference for (heuristic:
if A's fields include b_id or B's fields include a_id, or the
user's intake paragraph mentions both).
Build ONE AskUserQuestion call with up to 5 questions. If the entity
count yields > 5 candidate pairs, batch into multiple calls (still counts
toward the ≥1 AskUserQuestion minimum).
Per-pair question template:
{
"question": "Relation between <A> and <B>?",
"header": "<A>↔<B>",
"multiSelect": false,
"options": [
{"label": "None", "description": "No direct FK; entities are independent"},
{"label": "One-to-one", "description": "A.b_id UNIQUE FK to B.id (or vice versa)"},
{"label": "One-to-many (A→B)", "description": "B.a_id FK to A.id; one A has many B"},
{"label": "One-to-many (B→A)", "description": "A.b_id FK to B.id; one B has many A"},
{"label": "Many-to-many", "description": "Requires a junction table; skill will auto-name it <A><B>"}
]
}
Store the result in ENTITIES as .relations = [{from, to, kind}, ...].
2c — Auto-generate junction tables
For each pair marked Many-to-many, append a synthetic entity to
ENTITIES:
<A><B>:
<a>_id FK → <A>.id (ON DELETE CASCADE)
<b>_id FK → <B>.id (ON DELETE CASCADE)
PRIMARY KEY (<a>_id, <b>_id)
created_at TIMESTAMPTZ DEFAULT now()
Names must be deterministic (alphabetical order: OrganizationUser, not
UserOrganization, for pair (User, Organization)). Record the rule in
state so Phase 3 renders it consistently.
Verify-criterion
ENTITIEShas ≥1 entry after parse.- Every relation in
ENTITIES[*].relationsreferences two distinct existing entities. - Every
Many-to-manyhas produced a junction entity. - No entity is orphaned (zero relations AND not mentioned in INTAKE) —
warn the user with "Entity X has no relations; keep it?" (NO DOWNGRADE:
offer
keep / drop / add relationas follow-up click).