09 — Living-List Model (VERIFIED v2)
09 — Living-List Model (VERIFIED v2)
v2: field→home table corrected to real columns (registry_collection not sync_collection; refreshed_at not computed_at; counts only in meta_catalog).
A generic, data-driven descriptor for every "living list." It is a read-only projection over existing tables — not a new SoT.
Field → existing home (verified columns)
| Field | Source | Status |
|---|---|---|
list_id |
meta_catalog.code |
EXISTS |
list_name |
meta_catalog.name / name_en |
EXISTS |
composition_scope |
meta_catalog.composition_level (169/169 populated) + layer |
EXISTS (dirty values, doc 06) |
species_scope |
join entity_species / species_collection_map (164) |
PARTIAL |
live_pg_source |
meta_catalog.registry_collection ↔ pivot_definitions.source_object |
EXISTS |
pivot_definition_id |
pivot_definitions.code (match on source_object) |
EXISTS |
pivot_result |
pivot_results.metric_values by pivot_code ({count}/{total}) |
EXISTS |
stored_count |
meta_catalog.record_count / actual_count |
EXISTS (the drift surface) |
birth_status |
birth_registry presence / collection_registry.status |
EXISTS |
iu_profile_ref |
information_unit (219) |
PARTIAL |
kg_relation_ref |
iu_relation (60) / v_kg_edges_all |
EXISTS |
dot_refresh_ref |
refresh_pivot_results() / dot_tools op |
EXISTS (unbound) |
ui_surface_ref |
meta_catalog.ui_page (18/169) |
SPARSE |
reconciliation_status |
derived: pivot vs record_count vs actual_count | NEW (derived, read-only) |
last_checked_at |
pivot_results.refreshed_at |
EXISTS |
Realization: read-only view v_living_lists (design; DDL deferred)
v_living_lists =
meta_catalog mc
LEFT JOIN pivot_definitions pd ON pd.source_object = mc.registry_collection
LEFT JOIN LATERAL (
SELECT pr.metric_values, pr.refreshed_at, pr.needs_refresh
FROM pivot_results pr WHERE pr.pivot_code = pd.code
ORDER BY pr.refreshed_at DESC LIMIT 1) px ON true
-- expose: list_id=mc.code, list_name=mc.name, composition_level=mc.composition_level,
-- source=mc.registry_collection, pivot_code=pd.code,
-- pivot_total = (px.metric_values->>'count')::int (or 'total'),
-- stored_record = mc.record_count, stored_actual = mc.actual_count,
-- reconciliation_status = CASE
-- WHEN px.metric_values IS NULL THEN 'no_pivot'
-- WHEN pivot_total = mc.record_count AND mc.record_count = mc.actual_count THEN 'ok'
-- ELSE 'drift' END,
-- last_checked = px.refreshed_at, needs_refresh = px.needs_refresh, ui_page = mc.ui_page
This makes V1–V4 (doc 07) self-auditing with zero mutation: any list where pivot≠record_count≠actual_count, or needs_refresh, flags itself. Verified inputs: CAT-006 would show pivot 309 / record 309 / actual 163 → drift; CAT-DOT (no registry_collection) → no_pivot.
Counting-contract self-test (read-only, design)
For each pivot: assert total ≤ count(*) of source_object, and for grouped pivots sum(group rows) == total. Run as test_counting_contract() in CI/cron. (Note: the v1 "PIV-104 fails 309>219" example was wrong — PIV-104 is dot_tools-by-category; the real failing cases are the meta_catalog record≠actual rows.)
Principle
The living-list model operationalizes Đ26 MT5 ("list of lists") + MT6 ("self-describing"): one row per list, every number resolved from a pivot, no stored number that a pivot can compute. Where no composition_levels reference table exists yet (doc 06), the model exposes the gap rather than hardcoding the 6 layers.