-- §22 (multi-project deployments) — Slice M1: the project spine. -- -- A deployment now hosts one or more *projects*, each a corpus with its own -- content repo, slug/RFC-NNNN namespace, catalog, roster, and branding. The -- pre-multi-project single-corpus deployment is the N=1 case: this migration -- generates one 'default' project and stamps every existing RFC-scoped row -- to it, so the app keeps running exactly as before with the spine -- underneath (see docs/design/multi-project-spec.md §22.13). -- -- STRATEGY — additive, no table rebuilds. Every slug-bearing table gets a -- `project_id TEXT NOT NULL DEFAULT 'default'` column. The constant default -- means every existing INSERT in the codebase that does not yet mention -- project_id keeps working and lands rows in the default project; no query -- breaks because, with a single project, slugs remain globally unique. The -- column carries no inline REFERENCES clause: SQLite's ALTER TABLE ADD -- COLUMN forbids a FK column with a non-NULL default. project_id referential -- integrity is therefore enforced at the app layer for now; the FK lands -- with the table rebuilds below. -- -- ============================================================================ -- DEFERRED to the slice that activates project #2 (M3/M4). Until a second -- project exists these are correct as-is; the moment two projects can share a -- slug or a Gitea PR number they become cross-project collision bugs and MUST -- be rebuilt (SQLite needs a create-copy-drop-rename per table) to fold -- project_id into the key, and to add the project_id FK: -- * cached_rfcs PRIMARY KEY (slug) -> (project_id, slug) -- * cached_branches UNIQUE (rfc_slug, branch_name) -> +project_id -- * branch_visibility UNIQUE (rfc_slug, branch_name) -> +project_id -- * branch_contribute_grants UNIQUE (rfc_slug, branch_name, grantee_user_id) -> +project_id -- * stars UNIQUE (user_id, rfc_slug) -> +project_id -- * watches UNIQUE (user_id, rfc_slug) -> +project_id -- * pr_seen UNIQUE (user_id, rfc_slug, pr_number) -> +project_id -- * branch_chat_seen UNIQUE (user_id, rfc_slug, branch_name) -> +project_id -- * funder_consents PRIMARY KEY (user_id, rfc_slug) -> +project_id -- * rfc_collaborators UNIQUE INDEX (rfc_slug, user_id) -> +project_id -- * contribution_requests UNIQUE INDEX (rfc_slug, requester_user_id) WHERE pending -> +project_id -- * proposed_use_cases UNIQUE (scope, pr_number) -> +project_id -- (PR numbers are per-content-repo = per-project) -- cached_prs UNIQUE (repo, pr_number) is already globally unique (repo is the -- full 'org/repo' string, distinct per project) and needs no rebuild. -- ============================================================================ -- The project registry cache (mirrored from the git registry by the M3 -- reconciler; rows are never written from user actions). content_repo is -- NULL until the mirror — or the M1 startup backfill (§22.13 step 1) — sets -- it from the deployment's configured repo. CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL, content_repo TEXT, visibility TEXT NOT NULL DEFAULT 'gated' CHECK (visibility IN ('gated', 'public', 'unlisted')), config_json TEXT, registry_sha TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- The default project. visibility='public' preserves the pre-multi-project -- open-by-default posture (§22.5, §22.13). name is a placeholder the startup -- backfill / registry overwrites with the deployment's display name. INSERT OR IGNORE INTO projects (id, name, visibility) VALUES ('default', 'default', 'public'); -- Per-(user, project) membership and the §22.6 middle-tier role. This is the -- new tier between the §6.1 deployment role (users.role, now deployment-scope -- only) and the §6.3 per-RFC authority. CREATE TABLE IF NOT EXISTS project_members ( project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'project_viewer' CHECK (role IN ('project_admin', 'project_contributor', 'project_viewer')), granted_by INTEGER REFERENCES users(id) ON DELETE SET NULL, granted_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (project_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_project_members_user ON project_members(user_id); -- The project_id spine across every slug-bearing table. Backfills existing -- rows to 'default' via the constant column default. ALTER TABLE cached_rfcs ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE cached_branches ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE cached_prs ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE branch_visibility ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE branch_contribute_grants ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE stars ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE threads ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE changes ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE pr_seen ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE branch_chat_seen ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE watches ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE notifications ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE actions ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE pr_resolution_branches ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE funder_consents ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE rfc_invitations ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE rfc_collaborators ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE proposed_use_cases ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE contribution_requests ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'; -- The catalog/directory lookup the M3 surfaces will make (RFCs in a project). -- The per-table composite-key rebuilds in the DEFERRED block above will add -- their own (project_id, …) indexes when they land. CREATE INDEX IF NOT EXISTS idx_cached_rfcs_project ON cached_rfcs(project_id);