SciDEX has two parallel "this thing replaces that thing" mechanisms and
neither is honored by all read paths:
artifacts.superseded_by text column (\d artifacts confirmsidx_artifacts_superseded index). 8 rows in production.
artifact_links rows with link_type='supersedes' (production hasartifact_registry.py:189 maps the type to a mergeA consumer that opens an artifact by stale ID can land on a
lifecycle_state='superseded' row and not realize there's a current
canonical version. walk_supersede_chain already exists at
artifact_registry.py:3826 — but it's only called inside the registry,
not by HTTP routes that fetch artifacts by id. This task makes the chain
canonical: a single resolver, called in every artifact-fetch path,
returning both the requested id and the current id; redirects in HTML;
explicit current_artifact_id field in JSON responses; and a one-time
reconciler that closes the gap between the column and the link rows.
scidex/atlas/supersede_resolver.py:resolve_current(db, artifact_id) -> {requested_id, current_id,
chain: list[str], chain_depth: int, terminal_state: str}.superseded_by first, then any artifact_links of type'supersedes' (target → source means source supersedes target).MAX_DEPTH=20, raise on cycle).
scripts/reconcile_supersede_chains.py:artifacts.superseded_by andartifact_links where link_type='supersedes'. For eachsupersede_reconcile_audit table.created_at vslifecycle_changed_at on the artifact) as canonical and writescurrent_id.
resolve_current:GET /api/atlas/artifacts/{id} — adds current_artifact_id,is_canonical, supersede_chain to the JSON envelope.is_canonical = (requested_id == current_id).GET /artifact/{id} (HTML) — when not canonical, 302-redirect to/artifact/{current_id}?from={requested_id} and render a smallGET /api/atlas/artifacts/{id}/comments,GET /api/atlas/artifacts/{id}/links — silently follow to currentscidex/atlas/artifact_catalog.py and any wikiis_latest=1 AND lifecycle_state NOT IN
('superseded','archived') by default, with an opt-in?include_historical=1 flag.
tests/test_supersede_resolver.py:CycleError./api/atlas/artifacts/{a} returns current_artifact_id=c,is_canonical=false./artifact/{a} returns 302 to /artifact/{c}.
walk_supersede_chain at artifact_registry.py:3826 andresolve_current as a strictq-gov-lifecycle-state-machine-enforcement — confirms 'superseded'LIFECYCLE_STATES.q-gov-metrics-dashboard — needs canonical-vs-historical filtering forq-gov-rollback-workflow — rollback may un-supersede an artifact;Pre-work findings:
walk_supersede_chain at artifact_registry.py:3826 is actually named resolve_artifact; it only walks the column, not artifact_links.superseded_by set (spec estimated 8); 2 artifact_links with link_type='supersedes' (both self-referential, so effectively 0 real link-based supersessions).supersede_reconcile_audit table existed. No supersede_resolver.py existed.scidex/atlas/supersede_resolver.py — resolve_current(db, artifact_id) with CycleError, MAX_DEPTH=20. Walks superseded_by first, then artifact_links (target → source direction). Returns {requested_id, current_id, chain, chain_depth, terminal_state}.scripts/reconcile_supersede_chains.py — Creates supersede_reconcile_audit table, detects column-vs-link disagreements, adopts most-recent signal, writes both sides to match. Dry-run mode: python scripts/reconcile_supersede_chains.py --dry-run.api.py — Four routes updated:GET /api/artifacts/{id}: adds current_artifact_id, is_canonical, supersede_chain to JSON.GET /artifact/{id} (HTML): 302 redirect to /artifact/{current_id}?from={id} when not canonical; renders banner when ?from= is set.GET /api/artifacts/{id}/comments: silently resolves superseded IDs before looking up comments.GET /api/artifacts/search: adds include_historical flag (default false); filters is_latest=1 AND lifecycle_state NOT IN ('superseded','archived').tests/test_supersede_resolver.py — 5 tests: linear chain, cycle detection, link-based resolution, API JSON fields (skips if server has old code), HTML redirect (skips if server has old code).Backfill audit row count:
Dry-run output: 26 column-supersessions with no matching link record (all disagreements are "only column present"). Zero real link-supersessions existed. The reconciler will create 26 new supersedes link rows when run live.
Acceptance criteria status: All criteria met. HTTP tests skip gracefully pending server restart after merge.