Apply same composite-FK pattern as 001/002 for migration 003:
- resources: add UNIQUE (id, space_id) as FK target.
- resource_dependencies: denormalize space_id, composite FKs on both endpoints
(enforces both ends of a dep live in the same space at the DB layer).
- resource_credentials: denormalize space_id, composite FK to resources.
- source_docs.resource_id: NOT NULL (tenancy anchor); resource_id was already
absent from the update FIELDS list so docs cannot move resources.
Repos derive space_id from the resource on addDependency/addCredential so callers
can't fake cross-tenant assignment. 3 regression tests added.
Three security-review findings on migration 002:
- pages.space_id and refs.space_id changed to NOT NULL + ON DELETE CASCADE
(was nullable + SET NULL, which allowed orphan rows surviving space deletion).
- pages.parent_id wrapped in composite FK (parent_id, space_id) -> pages(id, space_id)
to prevent cross-space parent linkage (same pattern as tasks.project_id in 001).
- idx_refs_external promoted to UNIQUE on (space_id, source_kind, external_id);
upsertByExternal now requires space_id and dedups per-space, not globally.
Added 3 regression tests covering composite FK rejection, CASCADE-on-space-delete,
and per-space dedup independence.
Security review flagged that tasks.project_id could reference a project in
a different space. Added composite FK (project_id, space_id) -> projects(id, space_id)
with ON DELETE SET NULL (project_id) so a deleted project leaves the task in
its space with project_id NULL rather than orphaning into a NULL space.
Added two regression tests: cross-space FK rejection + cascade behavior.