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_count is a served snapshot, NOT truth. Truth = the live pivot. Where stored record_count ≠ live pivot/actual_countdrift, surfaced on the node (drift_status), never silently trusted.
  • No Nuxt math. Adding a line = INSERT a meta_catalog virtual row / pivot_definitions row (Đ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_count where >0; needs phantom_count column).
  • PIV-30x count-integrity / drift total (drift_rows, net_gap).
  • PIV-31x label-by-facet (entity_labels 718,744 uncounted by pivot).
  • PIV-32x registry_pin count (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_MISSING with next_action=propose_pivot.

NEW (propose-only, design)

  • meta_catalog.phantom_count column (additive; today phantom is derivable but not stored).
  • v_count_integrity view: per leaf category emit counted (pivot), actual, orphan_count, phantom_count, drift, count_integrity_status, last_scan_date.
  • fn_count_integrity_check(): wraps the rehearsal, writes system_issues(issue_type='count_integrity_failed', evidence_snapshot=<drift rows>) + enqueues event_outbox (doc 09). Reuses fn_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_tools and disagree by 2 → reconcile to one canonical CAT + one pivot (PIV-007=309).