Skip to content

Branch #2 Investigation — S39 Inventory Anomaly Root Cause

To: Katja CC: Vesper, Atlas From: Orion Re: Root-cause findings, no code written yet


TL;DR

Primary hypothesis: summarize_paper_run._get_inventory_balance() applies an unfiltered capital-events overlay, while InventoryManager.rebuild() applies a baseline-filtered overlay. Any deposit or withdrawal row in capital_events whose created_at predates the first inventory_ledger row for that asset is double-counted by the dashboard but correctly ignored by the engine. The live DB almost certainly has at least one such pre-baseline row (pre-engine XRP deposit + pre-engine RLUSD deposit), which is what produces the ~$140 phantom.

FLAG-034 didn't fix the bug — it introduced it. FLAG-034 added the overlay to summarize so the dashboard would show TOTAL inventory (matching live engine). It copied the arithmetic of get_capital_delta_total but not the baseline filter. Engine and dashboard now diverge whenever a pre-baseline capital row exists.

Status: I can't access neo_live_stage1.db from my sandbox (the copy here is stale/corrupt). I need Katja to run two SQL queries against the live DB before I cut code — the fix is obvious if pre-baseline rows exist, and wrong if they don't.


Structural Bug — Baseline Filter Asymmetry

Engine path (InventoryManager.rebuildStateManager.get_capital_delta_total):

SELECT COALESCE(SUM(CASE
    WHEN event_type = 'deposit'    THEN amount
    WHEN event_type = 'withdrawal' THEN -amount
    ELSE 0
END), 0.0) AS net_delta
FROM capital_events
WHERE asset = ?
  AND created_at >= ?            -- baseline filter

The baseline is MIN(created_at) FROM inventory_ledger WHERE asset = ?. Any capital event that predates the first ledger row for that asset is excluded.

Dashboard path (summarize_paper_run._get_inventory_balance, FLAG-034 change):

SELECT COALESCE(SUM(CASE
    WHEN event_type='deposit'    THEN amount
    WHEN event_type='withdrawal' THEN -amount
    ELSE 0
END), 0.0) AS o
FROM capital_events WHERE asset = ?
                                    -- no baseline filter

Same arithmetic. No baseline filter. Any pre-baseline deposit or withdrawal row gets counted.

Result: Display balance = ledger.last_new_balance + unfiltered_overlay. Engine balance = ledger.last_new_balance + baseline_filtered_overlay. Difference = sum of pre-baseline deposits − sum of pre-baseline withdrawals.


Numerical Reconstruction

On the live DB this difference is ~$140 at S39 end, consistent with the pre-engine committed capital (137.28 RLUSD = 39.27 XRP × 1.3314 + 85.00 RLUSD).

Engine arithmetic (baseline-filtered): - _xrp_balance = 66.82 + 0 = 66.82 ✓ (no post-baseline XRP deposit) - _rlusd_balance = 47.61 + 50 = 97.61 ✓ (Apr 17 injection = post-baseline)

Dashboard arithmetic (unfiltered): - If pre-baseline deposit XRP 39.27 exists → Display XRP ≈ 106.09 (reported: 112.56) - If pre-baseline deposit RLUSD 85 exists → Display RLUSD ≈ 182.61 (reported: 176.03) - Phantom total ≈ 140 RLUSD. Consistent within rounding.


Verification Queries

Run both against neo_live_stage1.db before any code is written.

Query 1 — Capital events vs baseline

WITH baselines AS (
    SELECT 'XRP' AS asset, MIN(created_at) AS first_ledger_ts
    FROM inventory_ledger WHERE asset = 'XRP'
    UNION ALL
    SELECT 'RLUSD', MIN(created_at)
    FROM inventory_ledger WHERE asset = 'RLUSD'
)
SELECT
    ce.created_at,
    ce.event_type,
    ce.asset,
    ce.amount,
    ce.basis_delta_rlusd,
    ce.source_note,
    b.first_ledger_ts,
    CASE
        WHEN b.first_ledger_ts IS NULL THEN 'no-baseline'
        WHEN ce.created_at < b.first_ledger_ts THEN 'PRE-BASELINE'
        ELSE 'post-baseline'
    END AS baseline_status
FROM capital_events ce
JOIN baselines b ON b.asset = ce.asset
ORDER BY ce.asset, ce.created_at;

Expected: at least one PRE-BASELINE row per asset. If none → primary hypothesis is wrong, move to secondary hypotheses.

Query 2 — Decompose the 112.56 / 176.03 directly

-- XRP breakdown
SELECT
    (SELECT new_balance FROM inventory_ledger WHERE asset='XRP'
     ORDER BY id DESC LIMIT 1) AS ledger_last_xrp,
    (SELECT COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount
                              WHEN event_type='withdrawal' THEN -amount
                              ELSE 0 END), 0.0)
     FROM capital_events WHERE asset='XRP') AS unfiltered_overlay_xrp,
    (SELECT COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount
                              WHEN event_type='withdrawal' THEN -amount
                              ELSE 0 END), 0.0)
     FROM capital_events
     WHERE asset='XRP'
       AND created_at >= (SELECT MIN(created_at) FROM inventory_ledger WHERE asset='XRP')
    ) AS filtered_overlay_xrp;

-- RLUSD breakdown
SELECT
    (SELECT new_balance FROM inventory_ledger WHERE asset='RLUSD'
     ORDER BY id DESC LIMIT 1) AS ledger_last_rlusd,
    (SELECT COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount
                              WHEN event_type='withdrawal' THEN -amount
                              ELSE 0 END), 0.0)
     FROM capital_events WHERE asset='RLUSD') AS unfiltered_overlay_rlusd,
    (SELECT COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount
                              WHEN event_type='withdrawal' THEN -amount
                              ELSE 0 END), 0.0)
     FROM capital_events
     WHERE asset='RLUSD'
       AND created_at >= (SELECT MIN(created_at) FROM inventory_ledger WHERE asset='RLUSD')
    ) AS filtered_overlay_rlusd;

ledger_last + unfiltered_overlay must equal the displayed 112.56 / 176.03. The difference between unfiltered and filtered overlays is the double-count magnitude.


Secondary Hypotheses (if Query 1 shows no PRE-BASELINE rows)

  • H2 — Double live-seeding: needs_seed guard ran twice after bad restore, writing a second large new_balance jump in inventory_ledger.
  • H3 — Ghost fills: apply_fill called twice per fill. Check: COUNT(*) FROM fills WHERE session_id = 38 vs COUNT(*) FROM inventory_ledger WHERE fill_id IN (SELECT id FROM fills WHERE session_id = 38) AND asset = 'XRP'. Should be 1:1.
  • H4 — Offer reserves: Not credible — reserves from 15-RLUSD orders can't produce 140 RLUSD phantom, and cancels run before snapshot.
  • H5 — Backup restore inconsistency: S38 DB restored from backup. If backup pre-dated some capital_events rows, those rows become effectively pre-baseline. Query 1 will catch this.

Proposed Branch #2 Scope

Keep branch name fix/summarize-paper-run-capital-overlay. Fix: replace inline SQL in _get_inventory_balance with a call to StateManager.get_capital_delta_total (baseline filter already correct there), or inline the same baseline-filtered SQL.

Tests: 1. Pre-baseline deposit XRP 39.27 + post-baseline deposit RLUSD 50 fixture → assert XRP overlay = 0, RLUSD overlay = 50. 2. Assert _get_inventory_balance equals InventoryManager.rebuild() output on a fixture with both pre- and post-baseline rows. 3. Three-source invariant pinned (engine = summarize = fixture "on-chain").

The full on-chain gateway comparator (Atlas 2A) is a follow-up — including it here would violate Atlas's no-bundling rule.


Vesper's Nit — Orion's Call

String literals in run_paper_session.py — will fold into Branch #2 as a tiny pre-commit if acceptable, since the halt taxonomy is adjacent. Otherwise defer to Branch #5 (audit/config-wiring-pass). Either works.


Next Step

Run Query 1 + Query 2 against neo_live_stage1.db and paste output. If PRE-BASELINE rows exist → Branch #2 fix is confirmed. If not → loop back to H2–H5.

— Orion