02 — Accounting Invariant: Design + Read-Only Detection Rehearsal
title: 02 — Accounting Invariant — Design + Read-Only Detection Rehearsal date: 2026-05-31
02 — Accounting Invariant
The invariant
total_system_objects
= counted_in_registries_pivot
+ orphan_objects (real, not counted)
+ phantom_objects (counted, not real) [phantom = LAW_DEFINITION_GAP, see doc 01]
On failure: set count_integrity_status = failed; emit a system_issues row + (design) an event_outbox event; define the cleanup-workflow trigger target; never silently hide the mismatch (Đ28). No production notification is implemented in this session — the contract is designed and the detection is rehearsed read-only.
CRITICAL design rule discovered during rehearsal — scope to leaves, never blind-SUM
A naive SUM(record_count) over all 169 meta_catalog rows double-counts: the table 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 and their rollups inflates the total. This is precisely the disguised-math trap Đ28 forbids. The invariant MUST be computed over a defined leaf set:
leaf set := meta_catalog rows WHERE composition_level <> 'meta'
AND entity_type NOT LIKE '%_total' AND entity_type <> 'all'
and total_system_objects for cross-checking comes from the grand-total pivot (PIV-500, PIVOT_MISSING — propose), not from re-summing the catalog.
Read-only rehearsal — LIVE 2026-05-31 (zero mutation)
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 (Σactual − Σrecord) | +200,442 |
| drift_rows (actual ≠ record) | 10 |
| orphan-side rows (actual > record) | 3 |
| phantom-side rows (record > actual) | 7 |
→ count_integrity_status = FAILED (net_gap ≠ 0 and drift_rows > 0).
The 10 drift rows (the actual integrity signal)
| code | name | record | actual | orphan | gap | side |
|---|---|---|---|---|---|---|
| CAT-ALL | Tổng nguyên tử (rollup) | 1,682,113 | 1,919,748 | 0 | +237,635 | orphan-side (rollup stale) |
| CAT-023 | Sổ khai sinh | 980,221 | 943,726 | 0 | −36,495 | phantom-side |
| CAT-DOT | Tổng DOT (rollup) | 307 | null | 140 | (−307) | actual uncomputed |
| CAT-COL | Tổng Collections (rollup) | 168 | null | 20 | (−168) | actual uncomputed |
| CAT-006 | DOT Tools | 309 | 163 | 0 | −146 | phantom-side |
| CAT-CMP | Tổng hợp chất | 423 | 326 | 0 | −97 | phantom-side |
| CAT-MAT | Tổng vật liệu | 0 | 55 | 0 | +55 | orphan-side |
| CAT-SPE | Tổng loài (rollup) | 42 | null | 1 | (−42) | actual uncomputed |
| CAT-007 | Pages/Routes | 37 | 52 | 0 | +15 | orphan-side |
| CAT-MOL | Tổng phân tử | 774 | 766 | 0 | −8 | phantom-side |
Note CAT-023 birth: record_count=980,221 equals the live birth_registry COUNT (980,221) and equals pivot_count('PIV-019') — but actual_count=943,726 is stale. So the stored number is current and the audited number is stale: the invariant correctly flags it as drift either way and routes it to recount, not to silent correction.
Proposed reconciliation surface (NEW, design-only)
v_count_integrity(view, propose): per leaf category emitcounted(pivot value),actual,orphan_count,phantom_count := GREATEST(record_count−actual_count,0),orphan_excess := GREATEST(actual_count−record_count,0),drift := actual ≠ record,count_integrity_status,last_scan_date.phantom_countcolumn onmeta_catalog(propose; additive). Today phantom is derivable but not stored.- PIV-500 grand-total + PIV-30x orphan / phantom totals (PIVOT_MISSING → propose as
pivot_definitionsINSERTs, never as Nuxt math). fn_count_integrity_check()(propose): wraps the rehearsal query, writessystem_issues(issue_type='count_integrity_failed', evidence_snapshot=<json of drift rows>, coalesce_key=<scope>)and (design) enqueuesevent_outbox. Reuses the existingfn_refresh_orphan_*+check_registry_coveragemachinery.
Rehearsal verdict
- Detection path is real and read-only-feasible today with existing columns — no new infrastructure required to detect.
- The invariant already fails (10 drift rows), proving the surface must show plus/minus/orphan/phantom/drift/verification per row and must not paper over it with a single green total.
- Everything above is propose-only; no
meta_catalog, view, pivot, orsystem_issuesrow was written this session.