# Implementation Plan: Math Core v3 Engine ## Overview Incremental upgrade of the Stonks Oracle signal processing engine from dual-mode heuristic/probabilistic to the v3 Calibrated Evidence Engine. Implementation follows the v3 math doc section 20 order: EvidenceUnit → reliability → LLR → clustering → posterior → contradiction → confidence → macro/competitive layers → EV gate → Kelly sizing → stop-defined heat → retire heuristic scoring. All v3 code operates behind the `v3_engine_enabled` feature flag with heuristic fallback. ## Tasks - [x] 1. EvidenceUnit and LLR conversion behind feature flag - [x] 1.1 Implement EvidenceUnit dataclass and normalization functions in `services/aggregation/scoring.py` - Add frozen dataclass `EvidenceUnit` with all 16 fields (symbol, layer, event_type, source_id, source_group, timestamp, horizon, direction, sentiment_strength, impact, extraction_conf, source_cred, novelty, event_base_rate, cluster_id) - Implement `normalize_company_signal()`, `normalize_macro_signal()`, `normalize_competitive_signal()` - Validate required fields (symbol, timestamp, source_id) — reject with warning on missing - Substitute 0.5 for missing optional numeric fields - Map direction from sentiment/impact_direction strings (+1/-1/0) - Assign event_base_rate from EVENT_TYPE_BASE_RATES lookup (default 0.10) - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8_ - [x] 1.2 Implement calibrated reliability pipeline (`compute_v3_reliability`) in `services/aggregation/scoring.py` - Add `SourceStats` dataclass (source_id, hits, misses, alpha_0=3, beta_0=3) - Add `ReliabilityComponents` dataclass (q_ext, q_source, q_recency, q_uniqueness, q_i) - Implement q_ext = sigmoid(8.0 × (extraction_conf - 0.55)) - Implement q_source via Bayesian shrinkage: E[theta_s] = (alpha_0 + hits) / (alpha_0 + beta_0 + hits + misses), then clamp((E - 0.50) / 0.35, 0, 1) - Implement q_recency = 2^(-age_hours / tau_adaptive) with adaptive half-life formula - Implement q_uniqueness = clamp(0.5 + 0.5 × novelty, 0.5, 1.0) × (1 / sqrt(1 + dup_count)) - Implement q_i = clamp(q_ext × q_source × source_cred × q_recency × q_uniqueness, 0, 1) - Apply floor of 0.01 on q_recency only for explainability display - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9_ - [x] 1.3 Implement LLR conversion (`compute_llr`) in `services/aggregation/scoring.py` - Compute p_correct = clamp(0.50 + 0.35 × q_i × impact × sentiment_strength, 0.501, 0.85) - Compute LLR_i = direction × ln(p_correct / (1 - p_correct)) - Return 0.0 for neutral signals (direction == 0) - Ensure LLR sign always matches direction for directional signals - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_ - [x] 1.4 Add feature flag routing in `services/aggregation/worker.py` - Read `v3_engine_enabled` from risk_configs table at start of each aggregation cycle - Route to v3 pipeline when True, heuristic when False - Default to heuristic mode if DB read fails (log warning) - Wrap v3 pipeline in try/except — fall back to heuristic on unhandled error (log ERROR with traceback) - Store `pipeline_mode` field ("v3" or "heuristic") in output metadata - _Requirements: 19.1, 19.2, 19.3, 19.4, 19.5, 19.6_ - [x] 1.5 Write property tests for EvidenceUnit, reliability, and LLR (`tests/test_pbt_v3_reliability.py`) - **Property 1: Reliability q_i is bounded in [0, 1]** - **Property 2: p_correct is bounded in [0.501, 0.85]** - **Property 3: LLR sign matches direction and magnitude is bounded** - **Property 4: Neutral signals produce zero LLR** - **Property 21: EvidenceUnit normalization preserves field ranges** - **Validates: Requirements 1.1–1.8, 2.1–2.9, 3.1–3.6, 21.1–21.3** - Use Hypothesis with `@settings(max_examples=100)` - [x] 1.6 Write unit tests for EvidenceUnit normalization and LLR conversion (`tests/test_v3_evidence_unit.py`) - Test company signal → EvidenceUnit with correct field mapping - Test macro signal → EvidenceUnit with correct horizon mapping (short_term→7d, medium_term→30d, long_term→90d) - Test competitive signal → EvidenceUnit with correct direction mapping - Test missing required fields → rejection with warning - Test missing optional fields → default 0.5 substitution - Test known inputs through full q_i pipeline → expected outputs - Test LLR boundary cases: p_correct at clamp boundaries - _Requirements: 1.1–1.8, 2.1–2.9, 3.1–3.6_ - [x] 2. Evidence clustering and n_eff - [x] 2.1 Implement correlation-aware clustering in `services/aggregation/worker.py` - Add `EvidenceCluster` dataclass (cluster_id, units, llrs, n_eff, cluster_llr) - Implement `cluster_evidence()` — group EvidenceUnits by (symbol, horizon, event_type, source_group, time_bucket) - Compute cluster_id as hash of grouping key - Define time_bucket resolution per horizon (intraday=1h, 1d=4h, 7d=24h, 30d=72h, 90d=168h) - _Requirements: 4.1_ - [x] 2.2 Implement n_eff computation in `services/aggregation/worker.py` - Implement `compute_n_eff(llrs, correlations)` using formula: (sum w_i)² / (sum w_i² + 2 × sum_{i 1.5 OR |trend_z| > 2.5), trend_following (|trend_z| >= 0.75 AND vol_ratio < 1.3), mean_reversion (|trend_z| < 0.50 AND vol_ratio < 1.0), uncertainty (default) - Assign regime parameters: gamma, confidence_mult, phi, ATR_mult, min_edge - Default to uncertainty when data insufficient - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8_ - [x] 4.2 Implement posterior assembly via log-odds in `services/aggregation/bayesian.py` - Add `V3Posterior` dataclass (p_up, p_down, log_odds, strength, direction, n_eff_total, regime) - Implement `compute_v3_posterior(clusters, regime, p_prior=0.50)` - Compute logit(P_up) = logit(P_prior) + sum(gamma_regime × LLR_c) - Compute P_up = sigmoid(log_odds), clamp to [1e-10, 1 - 1e-10] - Compute strength = abs(2 × P_up - 1) - Classify direction using regime-specific thresholds (panic: 0.68/0.32, trend_following: 0.60/0.40, mean_reversion: 0.63/0.37, uncertainty: 0.65/0.35) - Load calibrated prior from risk_configs if available (clamp to [0.40, 0.60]), fall back to 0.50 on error - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7_ - [x] 4.3 Write property tests for posterior and regime (`tests/test_pbt_v3_posterior.py`) - **Property 7: Posterior P_up is in open interval (0, 1)** - **Property 20: Regime classification is exhaustive and deterministic** - **Validates: Requirements 5.3, 6.2–6.5, 21.5** - Use Hypothesis with `@settings(max_examples=100)` - [x] 4.4 Write unit tests for posterior assembly (`tests/test_v3_posterior.py`) - Test empty clusters → P_up = 0.50 (neutral prior) - Test all bullish clusters → P_up > 0.50 - Test regime direction thresholds at boundary values - Test prior clamp [0.40, 0.60] - Test regime classification with known inputs - _Requirements: 5.1–5.7, 6.1–6.8_ - [x] 5. Replace contradiction with LLR entropy - [x] 5.1 Implement LLR entropy contradiction in `services/aggregation/contradiction.py` - Add `compute_v3_contradiction(clusters: list[EvidenceCluster]) -> float` - Compute E_pos = sum(max(LLR_c, 0)), E_neg = sum(max(-LLR_c, 0)), E_total = E_pos + E_neg - When E_total == 0 → return 0.0 - When only one direction exists (E_pos == 0 or E_neg == 0) → return 0.0 - Compute f_pos = E_pos / E_total, f_neg = E_neg / E_total - Compute H_conflict = -f_pos × log2(f_pos) - f_neg × log2(f_neg), treating 0×log2(0) = 0 - Compute volume_factor = 1 - exp(-E_total / 3.0) - Return H_conflict × volume_factor, bounded in [0.0, 1.0] - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7_ - [x] 5.2 Write property tests for contradiction (`tests/test_pbt_v3_posterior.py`) - **Property 8: Contradiction is zero when evidence is unidirectional** - **Property 9: Contradiction score is bounded in [0, 1]** - **Validates: Requirements 7.6, 7.7, 21.7** - Use Hypothesis with `@settings(max_examples=100)` - [x] 5.3 Write unit tests for LLR entropy contradiction (`tests/test_v3_contradiction.py`) - Test all bullish clusters → contradiction = 0.0 - Test equal split of evidence → high contradiction near 1.0 - Test E_total = 0 → contradiction = 0.0 - Test volume_factor growth: small evidence mass → suppressed score - _Requirements: 7.1–7.7_ - [x] 6. Replace confidence formula - [x] 6.1 Implement multiplicative confidence v3 in `services/aggregation/worker.py` - Add `compute_v3_confidence(n_eff_total, q_values, llrs, strength, regime_confidence_mult, contradiction, data_quality) -> float` - Compute C_evidence = 1 - exp(-n_eff_total / 5.0) - Compute C_quality = weighted_mean(q_i, weight=|LLR_i|) - Compute confidence = clamp(C_evidence × sqrt(C_quality) × sqrt(max(strength, 0.05)) × regime_confidence_mult × (1 - contradiction) × data_quality, 0, 1) - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_ - [x] 6.2 Implement data quality v3 in `services/aggregation/worker.py` - Add `compute_v3_data_quality(units, extraction_failure_rate, age_newest_hours, n_source_types) -> float` - Q_parse = 1 - extraction_failure_rate - Q_conf = weighted_mean(extraction_conf_i, weight=impact_i) - Q_fresh = exp(-age_newest_hours / 168) - Q_coverage = 1 - exp(-N_valid / 5) - Q_diversity = min(1, log2(1 + N_source_types) / log2(4)) - data_quality_score = clamp(Q_parse × sqrt(Q_conf) × Q_fresh × Q_coverage × Q_diversity, 0, 1) - Force informational mode when data_quality < 0.50 OR N_valid < 2 OR Q_parse < 0.50 - Force informational when only macro/competitive evidence unless macro_only_enabled - _Requirements: 17.1, 17.2, 17.3, 17.4, 17.5, 17.6, 17.7, 17.8_ - [x] 6.3 Write property tests for confidence and data quality (`tests/test_pbt_v3_posterior.py`) - **Property 10: Multiplicative confidence is bounded in [0, 1] and suppressed by weak dimensions** - **Property 17: Data quality score is bounded in [0, 1]** - **Validates: Requirements 8.3, 8.5, 17.6, 21.6** - Use Hypothesis with `@settings(max_examples=100)` - [x] 6.4 Write unit tests for confidence and data quality (`tests/test_v3_confidence.py`) - Test zero data quality → near-zero confidence - Test full contradiction (1.0) → zero confidence - Test low n_eff → suppressed C_evidence - Test data quality boundary cases (Q_parse < 0.50 → informational) - _Requirements: 8.1–8.5, 17.1–17.8_ - [x] 7. Checkpoint - Verify core pipeline - Ensure all tests pass for posterior, contradiction, confidence, and data quality. - Ensure all tests pass, ask the user if questions arise. - [x] 8. Convert macro and competitive layers to emit LLR - [x] 8.1 Implement noisy-OR macro exposure and LLR emission in `services/aggregation/interpolation.py` - Add `compute_normalized_macro_exposure(overlaps: dict[str, float]) -> float` - E_raw = 1 - product(1 - w_k × O_k) with weights: w_geo=0.35, w_supply=0.25, w_commodity=0.25, w_sector=0.15 - E_max = 1 - product(1 - w_k) - E_macro = E_raw / E_max (normalized to [0, 1]) - Apply resilience dampener per tier: global_leader=0.70, multinational=0.85, regional=1.00, domestic=1.20 - Add `compute_macro_llr(macro_impact, event_confidence, q_recency, macro_direction) -> float` - p_macro = clamp(0.50 + 0.30 × macro_impact × event_confidence × q_recency, 0.501, 0.80) - LLR_macro = macro_direction × ln(p_macro / (1 - p_macro)) - Feed macro LLR into shared posterior without separate post-hoc modifier - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5_ - [x] 8.2 Implement correlation-shrunk competitive propagation in `services/aggregation/signal_propagation.py` - Add `compute_shrunk_correlation(rho_rolling, n_observations, same_sector) -> float` - rho_prior = 0.30 (same_sector) or 0.10 (cross_sector) - rho_shrunk = (n/(n+30)) × rho_rolling + (30/(n+30)) × rho_prior - rho_effective = max(rho_shrunk, 0) - Add `compute_competitive_llr(llr_source, rho_effective, d_network, pattern_confidence) -> float` - attenuation = rho_effective × exp(-0.85 × d_network), max_distance = 3 - LLR_competitive = clamp(llr_source × attenuation × pattern_confidence, -1.25, 1.25) - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_ - [x] 8.3 Write property tests for macro and competitive layers (`tests/test_pbt_v3_layers.py`) - **Property 13: Noisy-OR normalized exposure is bounded in [0, 1]** - **Property 14: Competitive LLR is clamped to [-1.25, 1.25]** - **Property 15: Graph attenuation is zero beyond max distance** - **Validates: Requirements 9.1, 9.2, 10.3, 10.4, 10.5** - Use Hypothesis with `@settings(max_examples=100)` - [x] 8.4 Write unit tests for macro and competitive layers (`tests/test_v3_layers.py`) - Test noisy-OR: all O_k = 1.0 → E_macro = 1.0; all O_k = 0 → E_macro = 0 - Test resilience dampener per tier - Test macro LLR at boundary values - Test shrunk correlation convergence (n → ∞ approaches rho_rolling) - Test competitive LLR clamp at ±1.25 - Test distance > 3 → zero attenuation - _Requirements: 9.1–9.5, 10.1–10.5_ - [x] 9. Replace EV gate with expected-return distribution - [x] 9.1 Implement posterior state projection in `services/aggregation/projection.py` - Add `V3ProjectionState` dataclass (a_t, p_up_projected, projected_strength, diverges, phi_regime) - Implement `compute_v3_projection(a_prev, cluster_llrs, regime, p_prior, projection_horizon, known_catalyst_llr=0.0)` - Evidence state: A_t = phi_regime × A_{t-1} + sum(LLR_c), init A_0 = 0.0 - Regime decay: panic=0.35, trend_following=0.80, mean_reversion=0.55, uncertainty=0.50 - Projected alpha: A_projected = phi^h × A_t + known_catalyst_LLR - P_up_projected = sigmoid(logit(P_prior) + A_projected) - Projected strength = abs(2 × P_up_projected - 1) - Flag divergence when sign(P_up_projected - 0.5) ≠ sign(P_up_t - 0.5) - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_ - [x] 9.2 Implement return distribution and EV gate in `services/recommendation/eligibility.py` - Add `ReturnDistribution` dataclass (sigma_h, mu_h, ev_long, min_edge, eligible) - Add `compute_return_distribution(a_projected, confidence, realized_vol_20d, horizon_days, costs, regime)` - sigma_h = realized_vol_20d × sqrt(horizon_days / 252); default vol = 0.25 if unavailable - mu_h = tanh(A_projected / 3.0) × confidence × sigma_h - CVaR_5 = sigma_h × 1.645 × 1.4 - EV_long = mu_h - costs - 0.10 × CVaR_5 - Regime min_edge: panic=0.0100, trend_following=0.0035, mean_reversion=0.0050, uncertainty=0.0075 - Eligibility: EV_long > min_edge AND EV_long > max(0.0025, 0.25 × costs) - Also require: confidence >= regime_confidence_min, contradiction <= regime_contradiction_max, n_eff_total >= 2.0, data_quality >= 0.50 - _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7_ - [x] 9.3 Implement regime-aware eligibility and mode escalation in `services/recommendation/eligibility.py` - Add regime-specific eligibility thresholds: panic (conf≥0.70, contra≤0.25, str≥0.36), trend_following (conf≥0.55, contra≤0.40, str≥0.20), mean_reversion (conf≥0.60, contra≤0.35, str≥0.26), uncertainty (conf≥0.65, contra≤0.30, str≥0.30) - Action mapping: BUY when P_up >= bullish_threshold and EV > min_edge; SELL when existing position and EV_exit > EV_hold; HOLD when existing; WATCH otherwise - live_eligible: BUY/SELL + conf >= 0.75 + contra <= 0.20 + n_eff >= 5 + EV > 2×min_edge + risk_passed - paper_eligible: BUY/SELL + conf >= 0.60 + EV > min_edge + risk_passed - Otherwise: informational - _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5_ - [x] 9.4 Write property tests for projection (`tests/test_pbt_v3_projection.py`) - **Property 16: Projection evidence state decays toward zero** - **Validates: Requirements 11.1, 11.3** - Use Hypothesis with `@settings(max_examples=100)` - [x] 9.5 Write unit tests for EV gate and eligibility (`tests/test_v3_eligibility.py`) - Test EV_long positive → eligible - Test EV_long negative → ineligible - Test regime-specific min_edge thresholds - Test mode escalation: live vs paper vs informational - Test projection decay convergence - Test divergence flag behavior - _Requirements: 11.1–11.7, 12.1–12.7, 13.1–13.5_ - [x] 10. Checkpoint - Verify decision layer - Ensure all tests pass for projection, EV gate, eligibility, and layer integrations. - Ensure all tests pass, ask the user if questions arise. - [x] 11. Replace sizing with fractional Kelly under existing risk caps - [x] 11.1 Implement fractional Kelly position sizing in `services/trading/position_sizer.py` - Add `compute_kelly_sizing(p_win, b, confidence, data_quality, contradiction, max_position_pct, available_caps) -> float` - Compute reward ratio b = clamp(1.2 + 2.0 × confidence + 1.0 × strength - contradiction, 1.2, 3.0) - Compute f_kelly = (p_win × b - (1 - p_win)) / b - portfolio_pct = clamp(max(0, f_kelly) × 0.25 × confidence × data_quality × (1 - contradiction), 0, max_position_pct) - Apply min of: max_position_pct, sector_capacity, correlation_capacity (0 if avg corr > 0.80), heat_capacity - If portfolio_pct < 0.005 → downgrade to WATCH (reason: position_below_minimum) - If f_kelly <= 0 → portfolio_pct = 0, downgrade to WATCH (reason: negative_edge) - _Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7_ - [x] 11.2 Implement regime-aware stop loss and take profit in `services/trading/stop_loss_manager.py` - Add v3 stop computation: stop_distance_pct = max(ATR_pct × regime_ATR_mult, sigma_h × 1.25, 0.005) - stop_loss = entry_price × (1 - stop_distance_pct) - take_profit = entry_price × (1 + b × stop_distance_pct) where b = reward ratio - Activate trailing stop when unrealized_gain >= 0.50 × TP distance - trailing_stop = max(existing_stop, current_price × (1 - trailing_distance_pct)) - trailing_distance_pct = max(ATR_pct × trailing_ATR_mult, sigma_h × 0.75) - Trailing stop must be monotonically non-decreasing - _Requirements: 16.1, 16.2, 16.3, 16.4, 16.5_ - [x] 11.3 Write property tests for Kelly sizing and stops (`tests/test_pbt_v3_decision.py`) - **Property 11: Fractional Kelly sizing is bounded and respects negative edge** - **Property 18: Stop loss is below entry price and take profit is above** - **Property 19: Trailing stop never decreases** - **Validates: Requirements 14.4, 14.7, 16.2, 16.3, 16.5, 21.8, 21.9** - Use Hypothesis with `@settings(max_examples=100)` - [x] 11.4 Write unit tests for Kelly sizing and stops (`tests/test_v3_sizing.py`) - Test p_win=0.3, b=2 → f_kelly < 0 → size = 0 - Test p_win=0.7, b=2 → positive size within caps - Test cap enforcement (sector, correlation, heat) - Test position_below_minimum downgrade - Test stop/TP computation with known inputs - Test trailing stop monotonicity over a price sequence - _Requirements: 14.1–14.7, 16.1–16.5_ - [x] 12. Replace portfolio heat with stop-defined risk dollars - [x] 12.1 Implement stop-defined portfolio heat in `services/risk/engine.py` - Add `compute_portfolio_heat(positions, stop_distances) -> float` - risk_dollars = position_value × stop_distance_pct for each position - portfolio_heat = sum(risk_dollars) - Add `check_heat_capacity(current_heat, new_risk_dollars, max_heat_pct, portfolio_value) -> bool` - Reject new entry when current_heat + new_risk_dollars > max_heat_pct × portfolio_value - Integrate available_heat_capacity into Kelly sizing pipeline - _Requirements: 15.1, 15.2, 15.3, 15.4, 15.5_ - [x] 12.2 Implement risk tier auto-adjustment v3 in `services/risk/engine.py` - Add `TierMetrics` dataclass (profit_factor_30d, max_drawdown_30d, calibration_error, realized_sharpe_30d, n_trades_30d, reserve_pool_pct) - Add `evaluate_tier_adjustment(metrics) -> str` returning 'upgrade'|'downgrade'|'hold' - Downgrade if ANY: profit_factor < 1.0 OR drawdown > 0.12 OR calibration_error > 0.20 OR sharpe < 0 - Upgrade only if ALL: profit_factor > 1.35 AND drawdown < 0.05 AND calibration_error < 0.12 AND reserve > 0.20 AND N_trades >= 20 - Apply downgrade immediately; enforce 7-day upgrade cooldown - Evaluate once per calendar day after session close - _Requirements: 18.1, 18.2, 18.3, 18.4, 18.5, 18.6_ - [x] 12.3 Write property tests for heat and tier adjustment (`tests/test_pbt_v3_decision.py`) - **Property 22: Portfolio heat rejection is correct** - **Property 23: Tier auto-adjustment obeys downgrade-any, upgrade-all logic** - **Validates: Requirements 15.3, 15.5, 18.3, 18.4** - Use Hypothesis with `@settings(max_examples=100)` - [x] 12.4 Write unit tests for heat and tier (`tests/test_v3_risk.py`) - Test heat computation: 3 positions with known stops → expected heat - Test heat rejection: heat at limit → new entry blocked - Test tier downgrade: single bad metric triggers downgrade - Test tier upgrade: all metrics good → upgrade - Test tier upgrade: one metric bad → hold (not upgrade) - Test 7-day cooldown enforcement - _Requirements: 15.1–15.5, 18.1–18.6_ - [x] 13. Checkpoint - Verify sizing and risk layer - Ensure all tests pass for Kelly sizing, stops, heat, and tier adjustment. - Ensure all tests pass, ask the user if questions arise. - [x] 14. Retire heuristic scoring to explainability-only mode - [x] 14.1 Wire v3 pipeline end-to-end in `services/aggregation/worker.py` - Orchestrate full pipeline: EvidenceUnit → q_i → LLR → cluster → posterior → contradiction → confidence → data_quality - Store v3 posterior in TrendSummary JSONB metadata (p_up, log_odds, strength, confidence, contradiction, n_eff, data_quality, regime) - Store return model in Recommendation JSONB metadata (mu_h, sigma_h, ev_long, min_edge) - Store explainability payload (top_positive_clusters, top_negative_clusters, suppression_reasons, risk_adjustments) - Preserve WeightedSignal as intermediate representation before LLR conversion - _Requirements: 20.1, 20.2, 20.3, 20.4, 20.5_ - [x] 14.2 Retain heuristic pipeline as fallback with explainability overlay - Keep existing heuristic scoring path fully functional (no removal) - Mark heuristic outputs with `pipeline_mode: "heuristic"` in metadata - Ensure heuristic mode still produces valid TrendSummary and Recommendation objects - Test feature flag toggle: v3 → heuristic → v3 round-trip - _Requirements: 19.1, 19.2, 19.5, 20.4_ - [x] 14.3 Write property test for JSON round-trip (`tests/test_pbt_v3_posterior.py`) - **Property 12: Posterior state JSON round-trip** - **Validates: Requirements 20.1, 21.10** - Serialize V3Posterior to JSON and deserialize, verify equivalence within 1e-10 - Use Hypothesis with `@settings(max_examples=100)` - [x] 14.4 Write integration tests for full pipeline (`tests/test_v3_integration.py`) - Test full path: raw signals → EvidenceUnit → q_i → LLR → cluster → posterior → recommendation - Test feature flag false → heuristic path, flag true → v3 path - Test v3 exception → heuristic fallback + error logged - Test output JSONB contains expected v3 fields - _Requirements: 19.1–19.6, 20.1–20.5_ - [x] 15. Final checkpoint - All tests green - Ensure all tests pass, ask the user if questions arise. - Run full test suite: `.venv/bin/python -m pytest tests/test_pbt_v3_*.py tests/test_v3_*.py -x --tb=short -q` - Verify no regressions in existing heuristic pipeline tests ## Notes - Tasks marked with `*` are optional and can be skipped for faster MVP - Each task references specific requirements for traceability - Checkpoints ensure incremental validation after each logical phase - Property tests validate the 23 correctness properties defined in the design - Unit tests validate specific examples, edge cases, and error handling - The implementation preserves the existing heuristic pipeline as a fully functional fallback - All v3 code is gated behind `v3_engine_enabled` — no changes to production behavior until flag is flipped - Run tests with: `.venv/bin/python -m pytest tests/ -x --tb=short -q` - Property tests use Hypothesis: `@settings(max_examples=100)` ## Task Dependency Graph ```json { "waves": [ { "id": 0, "tasks": ["1.1"] }, { "id": 1, "tasks": ["1.2", "1.4"] }, { "id": 2, "tasks": ["1.3"] }, { "id": 3, "tasks": ["1.5", "1.6"] }, { "id": 4, "tasks": ["2.1"] }, { "id": 5, "tasks": ["2.2"] }, { "id": 6, "tasks": ["2.3"] }, { "id": 7, "tasks": ["2.4", "2.5"] }, { "id": 8, "tasks": ["4.1"] }, { "id": 9, "tasks": ["4.2"] }, { "id": 10, "tasks": ["4.3", "4.4"] }, { "id": 11, "tasks": ["5.1"] }, { "id": 12, "tasks": ["5.2", "5.3"] }, { "id": 13, "tasks": ["6.1", "6.2"] }, { "id": 14, "tasks": ["6.3", "6.4"] }, { "id": 15, "tasks": ["8.1", "8.2"] }, { "id": 16, "tasks": ["8.3", "8.4"] }, { "id": 17, "tasks": ["9.1"] }, { "id": 18, "tasks": ["9.2", "9.3"] }, { "id": 19, "tasks": ["9.4", "9.5"] }, { "id": 20, "tasks": ["11.1", "11.2"] }, { "id": 21, "tasks": ["11.3", "11.4"] }, { "id": 22, "tasks": ["12.1", "12.2"] }, { "id": 23, "tasks": ["12.3", "12.4"] }, { "id": 24, "tasks": ["14.1"] }, { "id": 25, "tasks": ["14.2"] }, { "id": 26, "tasks": ["14.3", "14.4"] } ] } ```