artifacts has two rows for nearly every wiki_page slug:
wiki-<slug> (canonical — receives 2.65M+ artifact_links references)<slug> (orphan — created by a v1_polymorphic_backfill script)wiki-<slug> (some are dangling — no wiki_pages row)<slug> (needs canonicalisation)Across 23 metadata columns on 17,470 paired rows, only title differs (466 pairs). All other fields on the unprefixed side are default (lifecycle='active', quality_score=0.5, all counters=0, all FKs NULL).
466 title conflicts decompose into 5 deterministic buckets + 1 special case:
prefix-extends: take longerpref-is-slug-take-unp: take unprefixedunp-much-longer: take unprefixedcase-only-diff: take uppercasesemantic-disagree (wiki-proteins-apoe: ApoE (Apolipoprotein E) vs APOE - Apolipoprotein E): hardcoded — take APOE formwiki_pages.content_md is the source of truth for wiki content and is untouched.scripts/oneoff/2026-05-13-consolidate-wiki-page-artifacts.py runs in phases:
_archive_*_2026_05_13 tables for full rollbackUPDATE id='wiki-'||idwiki_pages.slug: set lifecycle_state='deprecated', deprecated_reason='wiki_page_missing'Idempotent. Each phase can be run independently. --dry-run prints SQL without executing.
artifact_type='wiki_page' and id NOT LIKE 'wiki-%' (after phases 3-4)artifacts_wiki_page_one_per_slug exists and rejects future duplicates/artifacts?artifact_type=wiki_page count drops by ~17,792/wiki/<slug> and /artifact/wiki-<slug> still HTTP 200INSERT INTO artifacts SELECT * FROM _archive_artifacts_unprefixed_wiki_2026_05_13;
-- repeat for 6 archived FK tables
DROP INDEX artifacts_wiki_page_one_per_slug;