The artifacts table encodes versioning via two columns: parent_version_id
(self-FK, currently NOT enforced as an FK in PG — \d artifacts shows no
foreign-key constraint on it) and is_latest int (1 == this row is the
current head of its lineage). Because Postgres doesn't enforce the parent
FK and writers update is_latest manually, the table accumulates two
classes of corruption:
parent_version_id references a row that has beenis_latest=1, breaking every "current version of artifact X" query.This task ships a periodic sweep that finds and fixes both — read-only by
default, opt-in to repair — and a daily report that surfaces the count of
each problem class as a governance metric.
scidex/senate/version_integrity.py with threefind_orphan_parents(db) -> list[{artifact_id, missing_parent_id}]SELECT a.id, a.parent_version_id FROM artifacts a
LEFT JOIN artifacts p ON p.id = a.parent_version_id
WHERE a.parent_version_id IS NOT NULL AND p.id IS NULL.find_lineages_without_head(db) -> list[{root_id, version_count}]:parent_version_id to the root), reports lineages withSUM(is_latest) = 0.find_lineages_with_multiple_heads(db) -> list[{root_id,
head_ids: list, head_count}]: same lineage grouping, reportsSUM(is_latest) > 1.
scidex/senate/version_integrity_repair.py, opt-in flag):repair_orphan_parents(db, action: 'null'|'mark_root', dry_run):artifact_provenance withaction_kind='gate_decision' (already in CHECK) +repair_multiple_heads(db, dry_run): keeps the row with theversion_number, demotes the rest to is_latest=0.repair_no_head(db, dry_run): promotes the max version_numberis_latest=1.
python -m scidex.senate.version_integrity [--repair]--repair, caps at --max-fix (default 100) per run to keepscidex/senate/scheduled_tasks.py in read-only (audit) mode;governance_metrics_snapshots table introduced byq-gov-metrics-dashboard so trends show up on the dashboard.
senate_alerts row.
GET /api/senate/version_integrity/status returns thePOST /api/senate/version_integrity/repairtests/test_version_integrity.py:find_orphan_parents returns 1 row; repair_orphan_parents
(action='null') clears the FK.is_latest=1 rows → repair keeps maxis_latest=1 → repair promotes max.SELECT count(*) FROM
edit_history before/after).WITH RECURSIVE
lineage(id, root, depth) AS (...) WHERE depth < 100).
--dry-run; record counts in Work Log.q-gov-metrics-dashboard — emits snapshot rows for the trend chart.