Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
115 lines
4.9 KiB
Markdown
115 lines
4.9 KiB
Markdown
# Phase 3 — Generate SQL DDL (tables, indexes, FKs, constraints)
|
|
|
|
Emit a full `db/schema.sql` file based on `DB`, `ORM`, `STYLE`, and
|
|
`ENTITIES`. Then ONE `AskUserQuestion` to review/revise.
|
|
|
|
## 3a — Pick primary-key strategy (inline, no AskUserQuestion — deterministic)
|
|
|
|
- Postgres 17: `id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY`
|
|
(SQL-standard, PG 10+; avoid legacy `SERIAL`). See `db-postgres.md`.
|
|
- SQLite: `id INTEGER PRIMARY KEY` (rowid alias; autoincrement discouraged
|
|
unless monotonic guarantee required). See `db-sqlite.md`.
|
|
- MySQL: `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`.
|
|
|
|
If the user's fields include `id` already, respect it; otherwise prepend
|
|
the surrogate PK. Record the choice in state for Phase 4 traceability.
|
|
|
|
## 3b — Per-entity DDL generation rules
|
|
|
|
For each entity in `ENTITIES`:
|
|
|
|
1. **CREATE TABLE**: `CREATE TABLE IF NOT EXISTS <snake_case_name>` (plural
|
|
unless user named it singular — default to pluralize: `User` → `users`,
|
|
`Organization` → `organizations`; junction `OrganizationUser` →
|
|
`organization_users`).
|
|
2. **Columns**: infer SQL type from field name + heuristics:
|
|
- `*_id`, `id` → `BIGINT` (Postgres/MySQL) / `INTEGER` (SQLite).
|
|
- `*_at`, `created_at`, `updated_at` → `TIMESTAMPTZ` (PG) / `DATETIME`
|
|
(SQLite) / `TIMESTAMP` (MySQL), `DEFAULT now()` (PG) /
|
|
`DEFAULT CURRENT_TIMESTAMP` (others).
|
|
- `email`, `*_email` → `TEXT` / `VARCHAR(320)` (RFC 5321 limit) + `NOT
|
|
NULL` + `CHECK (email LIKE '%@%')` (cheap sanity, real validation is
|
|
app-side).
|
|
- `*_count`, `*_amount` → `INTEGER` or `NUMERIC(18,2)` (money);
|
|
default `0`.
|
|
- `is_*`, `has_*` → `BOOLEAN NOT NULL DEFAULT false`.
|
|
- Freeform → `TEXT NOT NULL` unless user said "optional".
|
|
3. **Timestamps default**: unless user opts out, add `created_at` +
|
|
`updated_at` to every non-junction entity.
|
|
4. **Soft-delete**: if `SCALE = Production multi-replica`, add
|
|
`deleted_at TIMESTAMPTZ NULL` + partial index (PG only) on
|
|
`WHERE deleted_at IS NULL`.
|
|
|
|
## 3c — Foreign keys
|
|
|
|
For each `relations` entry (kind ≠ `None`):
|
|
|
|
- `REFERENCES <parent_table>(id) ON DELETE <ACTION>`.
|
|
- Default `ON DELETE` action: junction → `CASCADE`; one-to-one → `CASCADE`
|
|
from owning side; one-to-many → `RESTRICT` (safer default; explicit
|
|
cascade is a product decision). See `db-migration-hygiene.md`.
|
|
- **Mandatory FK index**: every FK column gets `CREATE INDEX
|
|
IF NOT EXISTS idx_<table>_<col> ON <table>(<col>);`. Unindexed FKs =
|
|
join explosion (see `db-postgres.md` "Forbidden").
|
|
|
|
## 3d — Indexes + constraints
|
|
|
|
- Unique: `email`, `slug`, `username`, any field user marked `UNIQUE` →
|
|
`UNIQUE` column constraint.
|
|
- Composite indexes: for junction tables, PK is composite; add a
|
|
reverse-order index if both directions of lookup are common.
|
|
- Check constraints: `CHECK (status IN (...))` for enum-ish text fields;
|
|
`CHECK (amount >= 0)` for non-negative numerics.
|
|
- Triggers: if `updated_at` present AND `DB = Postgres`, emit a
|
|
`CREATE OR REPLACE FUNCTION set_updated_at()` + `CREATE TRIGGER` per
|
|
table. SQLite uses `AFTER UPDATE` triggers; MySQL uses `ON UPDATE
|
|
CURRENT_TIMESTAMP` in the column definition.
|
|
|
|
## 3e — Emit the file
|
|
|
|
Write `db/schema.sql` with a top-level comment:
|
|
|
|
```sql
|
|
-- Generated by /schema-design — <YYYY-MM-DD>
|
|
-- DB: <DB> ORM: <ORM> Style: <STYLE>
|
|
-- Entities: <N> Relations: <M>
|
|
-- Edit freely; Phase 4 will package the first migration from this file.
|
|
```
|
|
|
|
Print the first ~40 lines of the file inline in chat (for review) plus a
|
|
`<path>:<total lines>` footer. Full contents live on disk.
|
|
|
|
## 3f — Review click (AskUserQuestion)
|
|
|
|
```json
|
|
{
|
|
"questions": [
|
|
{
|
|
"question": "Schema review — accept or revise?",
|
|
"header": "Review",
|
|
"multiSelect": false,
|
|
"options": [
|
|
{"label": "Accept — proceed to Phase 4", "description": "Schema is correct; package into a migration"},
|
|
{"label": "Revise entities (return to Phase 2)", "description": "Add/drop entities or fix a relation"},
|
|
{"label": "Revise types (edit db/schema.sql)", "description": "Skill will emit a 1-AskUserQuestion per-column-type fix loop"},
|
|
{"label": "Revise indexes (edit db/schema.sql)", "description": "Skill will emit a 1-AskUserQuestion per-index fix loop"}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
If the user picks Revise, loop the relevant sub-step; then re-emit and
|
|
re-ask. Max 3 revise loops before the skill asks whether to accept-as-is
|
|
or escalate to code-implementer handoff (NO DOWNGRADE).
|
|
|
|
## Verify-criterion
|
|
|
|
- `db/schema.sql` exists on disk and is non-empty.
|
|
- Every FK has a companion index.
|
|
- Every non-junction entity has a PK + `created_at`.
|
|
- If `ORM = Drizzle` with `STYLE = code-first`: also emit
|
|
`db/schema.ts` alongside `db/schema.sql` (Drizzle introspection artefact;
|
|
see `db-drizzle.md`). For `ORM = SQLx`: record in state that Phase 4's
|
|
migration will also be picked up by `sqlx migrate run`.
|
|
- Accept click sets `DDL = "db/schema.sql"` and hands off to Phase 4.
|