KB-4E8E
05 — Pivot-Only Counting + Accounting Invariant (+ live FAILED rehearsal)
5 min read Revision 1
designregistries-pivotpivot-onlycountingaccounting-invariantdieu26pivot-missingleaf-scope2026-05-31verified
title: 05 — Pivot-Only Counting + Accounting Invariant date: 2026-05-31
05 — Pivot-Only Counting + Accounting Invariant
Doctrine (Điều 26)
pivot_count()is the only counting method (Đ26 rename rationale). MT2: count is correct by definition, direct, no cache.meta_catalog.record_countis a served snapshot, NOT truth. Truth = the live pivot. Where storedrecord_count≠ live pivot/actual_count→ drift, surfaced on the node (drift_status), never silently trusted.- No Nuxt math. Adding a line = INSERT a
meta_catalogvirtual row /pivot_definitionsrow (Đ26 §0-AU / MT6). Orphan & species columns come PG→Directus→Nuxt, never computed in Nuxt (Đ26 §II-BIS). - Every displayed number is pivot-backed or
PIVOT_MISSING. Nuxt never invents a number.
Accounting invariant
total_system_objects = counted_in_registries_pivot + orphan + phantom
On failure: count_integrity_status = failed + a system_issues row + (design) an event_outbox event + a cleanup trigger target — never hidden (Đ28/Đ23/Đ31).
CRITICAL: scope to the leaf set, never blind-SUM
meta_catalog mixes leaf categories with rollup/meta rows (composition_level='meta', entity_type like %_total/all — e.g. CAT-ALL actual 1,919,748, CAT-DOT, CAT-COL, CAT-SPE). Summing leaves + rollups double-counts — the exact disguised-math trap Đ28 forbids. The invariant MUST compute over:
leaf set := meta_catalog WHERE composition_level <> 'meta'
AND entity_type NOT LIKE '%_total' AND entity_type <> 'all'
and total_system_objects for cross-check comes from the grand-total pivot PIV-500 (PIVOT_MISSING → propose), not a catalog re-sum.
Read-only rehearsal — LIVE 2026-05-31 (zero mutation, evidence)
Aggregate over all 169 meta_catalog rows (intentionally unscoped, to expose the double-count):
| metric | value |
|---|---|
| categories | 169 |
| Σ record_count | 3,638,356 |
| Σ actual_count | 3,838,798 |
| Σ orphan_count | 161 |
| net_gap | +200,442 |
| drift_rows | 10 |
| orphan-side (actual>record) | 3 |
| phantom-side (record>actual) | 7 |
→ count_integrity_status = FAILED today. 10 drift rows incl: CAT-023 birth (record 980,221 vs actual 943,726, phantom-side −36,495 — note birth_registry live grew to 980,234 this session, so the stored number is itself moving); CAT-006 DOT (309 vs 163, −146); CAT-007 pages (37 vs 52, +15); CAT-MAT (0 vs 55, +55); CAT-CMP (423 vs 326); CAT-MOL (774 vs 766). pivot_count confirmed: PIV-001→169, PIV-019→980,234, PIV-007→309. |
PIVOT_MISSING ledger (must become pivot_definitions rows, never Nuxt math)
- PIV-500 grand-total
total_system_objects(cross-check anchor). - PIV-30x orphan-total + orphan-by-dimension (orphan_count is per-row in meta_catalog; no rollup pivot).
- PIV-30x phantom-total (
record_count−actual_countwhere >0; needsphantom_countcolumn). - PIV-30x count-integrity / drift total (drift_rows, net_gap).
- PIV-31x label-by-facet (
entity_labels718,744 uncounted by pivot). - PIV-32x
registry_pincount (after the NEW table is born). - IU 219 / KG 2,259 totals (PIVOT_MISSING; carried from prior sessions).
Until each exists, the surface renders the cell as
PIVOT_MISSINGwithnext_action=propose_pivot.
NEW (propose-only, design)
meta_catalog.phantom_countcolumn (additive; today phantom is derivable but not stored).v_count_integrityview: per leaf category emitcounted(pivot),actual,orphan_count,phantom_count,drift,count_integrity_status,last_scan_date.fn_count_integrity_check(): wraps the rehearsal, writessystem_issues(issue_type='count_integrity_failed', evidence_snapshot=<drift rows>)+ enqueuesevent_outbox(doc 09). Reusesfn_refresh_orphan_*+check_registry_coverage.
RECONCILE items
- Stored
record_count↔ live pivot (drift on 10 categories). - CAT-006 (309) vs CAT-DOT (307) — two meta_catalog rows count the same
dot_toolsand disagree by 2 → reconcile to one canonical CAT + one pivot (PIV-007=309).