[indexer] Migration completion detection and old table cleanup
## Problem
After indexing into new-prefix tables completes, the system must mark the migration as active. The indexer should also automatically drop tables for schema versions outside the `max_retained_versions` retention window.
## Proposal
### Completion detection
The indexer (holding the migration lock) determines when all enabled namespaces and code scopes have been re-indexed into the new-prefix tables by comparing checkpoint state:
- Query new-prefix `checkpoint` table for namespace completion
- Query new-prefix `code_indexing_checkpoint` table for code scope completion
- Compare against the list of enabled namespaces (from Siphon `knowledge_graph_enabled_namespaces`)
When all scopes are covered:
1. Update `gkg_schema_version`: set new version to `active`, old version to `retired`
2. Log at `info`: `schema migration to v{N} complete`
3. Increment metric: `gkg_schema_migration_completed_total`
### Automatic cleanup via retention window
After marking a migration complete, the indexer checks the `max_retained_versions` setting (default: 2) and drops tables for any version outside the window:
```
Example with max_retained_versions=2, after migrating to v2:
v2 → active (keep)
v1 → retired (keep — within window, rollback target)
v0 → retired (OUTSIDE window → drop tables, mark "dropped")
```
Cleanup logic:
1. List all versions in `gkg_schema_version` ordered by version descending
2. Keep the top `max_retained_versions` entries
3. For each version outside the window with status `retired`:
a. Enumerate its prefixed table names from `config/graph.sql`
b. `DROP TABLE IF EXISTS <prefix><table> SYNC` for each
c. Update version status to `dropped` in `gkg_schema_version`
### Safety
- Only drops tables for versions with status `retired` — never `active` or `migrating`
- `DROP TABLE IF EXISTS` is idempotent — safe to retry
- Cleanup runs under the migration lock — no concurrent cleanup attempts
- Rollback safety: within the retention window (default 2), the previous version's tables always exist
### Observability
- Metric: `gkg_schema_migration_completed_total` — counter for successful migrations
- Metric: `gkg_schema_cleanup_total` (labels: `version`, `result=success|failure`) — counter for table cleanup
- Log: table drop operations at `info` level with version and table name
## Acceptance criteria
- [ ] Migration marked as `active` when all scopes re-indexed into new tables
- [ ] Previous version marked as `retired`
- [ ] Indexer automatically drops tables outside `max_retained_versions` window
- [ ] Only `retired` versions are eligible for cleanup (never `active`/`migrating`)
- [ ] Cleanup is idempotent and safe (`DROP TABLE IF EXISTS ... SYNC`)
- [ ] `max_retained_versions` setting respected (default: 2)
- [ ] Integration test: full lifecycle — migration start → completion → automatic cleanup
- [ ] Metrics: migration completion and cleanup counters
## Dependencies
- Issue 3: Table-prefix-aware indexer
- Issue 4: Table-prefix-aware web server
## Blocks
- Issue 6: E2E staging validation
issue