V127: Sub-projects as tasks (phoenix_kit_project_assignments.child_project_uuid).
Lets a project be embedded inside another project as one of its task rows.
A sub-project is an Assignment that points at a child Project instead of
a reusable Task template — so it lives in the parent's task timeline and
gets dependencies + drag-reorder for free (both are already assignment-level
and project-scoped). The child project is the single source of truth; the
parent's linking assignment carries denormalized rollup fields (status /
progress_pct / estimated_duration / completed_at) synced by the context layer
whenever the child changes, so every existing read site (schedule math,
recompute_project_completion, dashboards, sorting) keeps working unchanged.
Changes to phoenix_kit_project_assignments:
child_project_uuid UUID→ FKphoenix_kit_projects(uuid) ON DELETE RESTRICT.RESTRICT(notCASCADE) so a stray child-project delete fails loudly instead of silently mutating the parent's task list — recursive teardown is orchestrated explicitly inPhoenixKitProjects.Projectsinside a transaction so it can log activity and tear the subtree down in order.task_uuidloses itsNOT NULL— a sub-project assignment has no template.CHECK ((task_uuid IS NOT NULL) <> (child_project_uuid IS NOT NULL))— exactly one of the two is set (XOR). Existing rows (task set, child NULL) satisfy it, so the constraint validates against current data without a backfill.- Partial UNIQUE index on
(child_project_uuid) WHERE child_project_uuid IS NOT NULL— a project is a child of at most one parent assignment. This is also what forces template cloning to deep-clone child subtrees rather than point two parents at the same child. It also serves the "find the linking row for this child" lookups (parent breadcrumb, rollup sync): an equality predicatechild_project_uuid = $1impliesIS NOT NULL, so Postgres uses the partial index for it — no separate plain index needed.
Idempotent: re-running is a no-op once the column/constraints/indexes exist.