Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/disaster-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ schema/table is *not* backed up until explicitly classified in
| `repo_coverage` | operational | yes | derived but needs full re-ingest to rebuild — cheap to keep |
| `repo_sync` | operational | yes | phase markers; self-healing hints |
| `user_sync` | operational | yes | stale `syncing`/`locked` rows are taken over by cron's stale-lock logic |
| `ownership_scopes` | core | yes | ownership scope map — for retained/disconnected repos GitHub may no longer serve the data; irreplaceable |
| `ownership_contributors` | core | yes | repo-scoped contributor identity rows — same irreplaceability |
| `ownership_snapshots` | core | yes | ownership aggregates — real retained user history, not always reconstructable |
| `ownership_sync_state` | operational | yes | ownership cursors/version markers; losing it means a re-ingest |
| `auth_sessions` | ephemeral | **no** | recovery must force re-auth, never resurrect live sids |
| `webhook_deliveries` | ephemeral | **no** | dedup history; post-restore deliveries reprocess idempotently |
| `security_events` | ephemeral | **no** | 30-day operational telemetry |
Expand Down
476 changes: 476 additions & 0 deletions migrations/012_ownership.sql

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions scripts/backup-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const CORE_TABLES = [
'commits', // ingested commit rows — the expensive history
'pull_requests', // ingested PR rows (no titles/numbers/authors — data minimization)
'feedback', // user-submitted transmissions — not re-ingestable
'ownership_scopes', // ownership scope map — irreplaceable for
// retained/disconnected repos GitHub can no
// longer serve
'ownership_contributors',// repo-scoped contributor identity rows
'ownership_snapshots', // ownership aggregates — same irreplaceability
]

// OPERATIONAL — reconstructable in principle, but restoring them avoids a
Expand All @@ -29,6 +34,8 @@ export const OPERATIONAL_TABLES = [
// full GitHub re-ingest — cheap to keep
'repo_sync', // per-repo sync phase markers (push kicks)
'user_sync', // per-user sync status; 'syncing' rows resume via cron
'ownership_sync_state', // ownership cursors/version markers — losing it
// means a re-ingest, same as repo_sync
]

// EPHEMERAL — intentionally excluded from backups.
Expand Down
375 changes: 373 additions & 2 deletions scripts/recovery-drill.mjs

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions test/data-minimization.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,17 @@ test('every user-linked table cascades from users in the migrations', () => {
}
// repository_languages hangs off repositories — the only non-user FK.
assert.match(all, /references (?:public\.)?repositories\(id\) on delete cascade/i)
// 012 ownership tables cascade through the tenant-qualified composite FK:
// (repository_id, user_id) -> repositories(id, user_id) on delete cascade,
// which chains to users(id) cascade via repositories.
const ownership = src('migrations/012_ownership.sql')
for (const tbl of ['ownership_scopes', 'ownership_contributors', 'ownership_snapshots', 'ownership_sync_state']) {
assert.match(
ownership,
new RegExp(`create table if not exists public\\.${tbl}[\\s\\S]*?foreign key \\(repository_id, user_id\\)[\\s\\S]*?references public\\.repositories \\(id, user_id\\)[\\s\\S]*?on delete cascade`, 'i'),
`${tbl} must tenant-cascade through repositories(id, user_id)`,
)
}
})

/* ── WS7: backup scope stays allowlist-only ─────────────────────────────── */
Expand All @@ -366,6 +377,19 @@ test('backup allowlist excludes ephemeral and carries no removed fields', async
}
})

test('ownership backup split: derived history is CORE, sync state OPERATIONAL', async () => {
const { BACKUP_TABLES, CORE_TABLES, OPERATIONAL_TABLES, EPHEMERAL_TABLES } = await import('../scripts/backup-scope.mjs')
// ownership history is irreplaceable for retained/disconnected repos —
// it is backed up as CORE, never classified ephemeral
for (const t of ['ownership_scopes', 'ownership_contributors', 'ownership_snapshots']) {
assert.ok(CORE_TABLES.includes(t), `${t} must be CORE`)
assert.ok(BACKUP_TABLES.includes(t), `${t} must be backed up`)
}
assert.ok(OPERATIONAL_TABLES.includes('ownership_sync_state'), 'ownership_sync_state must be OPERATIONAL')
assert.ok(BACKUP_TABLES.includes('ownership_sync_state'), 'ownership_sync_state is backed up like repo_sync')
assert.ok(!EPHEMERAL_TABLES.includes('ownership_sync_state'), 'ownership sync state must not be ephemeral')
})

/* ── WS5: legal text cannot re-claim removed fields ──────────────────────── */

test('privacy/security copy stays consistent with the final schema', () => {
Expand Down
Loading
Loading