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.rebuild → StateManager.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_seedguard ran twice after bad restore, writing a second largenew_balancejump ininventory_ledger. - H3 — Ghost fills:
apply_fillcalled twice per fill. Check:COUNT(*) FROM fills WHERE session_id = 38vsCOUNT(*) 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_eventsrows, 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