\set ECHO none
\pset format unaligned

-- HybridEvaluator simplifier: peephole rewrites on gate_arith
-- subtrees that preserve the gate's scalar value in every world.
-- The probability path runs RangeCheck -> simplifier -> RangeCheck
-- (re-pass picks up newly-folded constants in the joint-conjunction
-- check) -> AnalyticEvaluator, so the assertions below exercise the
-- closure rules end-to-end through probability_evaluate('independent')
-- with the analytical exact tolerance (1e-12).

-- ---------------------------------------------------------------
-- Normal-family closure: linear combinations of independent normals
-- fold to a single normal whose mean and variance are the closed-form
-- combinations.  Independence is tested via the base-RV footprint;
-- the underlying gate_rv UUIDs being distinct (each provsql.normal
-- call mints a fresh v4 UUID) is sufficient.
-- ---------------------------------------------------------------

-- (1) N(0, 1) + N(0, 1) folds to N(0, sqrt(2)); P(sum > 1) =
--     1 - Phi(1/sqrt(2)) ~= 0.2398.  TODO's core 7(a) case.
--     Threshold chosen off the fold's mean so the test discriminates
--     a wrong-variance closure (e.g. an erroneous fold to N(0, 1)
--     would give 0.1587, well outside the 1e-12 tolerance).
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(provsql.normal(0, 1) + provsql.normal(0, 1),
                                1::random_variable),
             'independent') - 0.23975006109347664) < 1e-12
       AS sum_two_indep_normals_exact;

-- (2) 3 * N(0, 1) + N(0, 4) folds to N(0, sqrt(9 + 16)) = N(0, 5);
--     P(... > 5) = 1 - Phi(1) ~= 0.1587.  Parameters chosen so the
--     fold's sigma is integer-clean and the threshold (one sigma
--     above the mean) discriminates a wrong scalar coefficient
--     (e.g. if try_normal_closure misread the TIMES wires and used
--     a = 1 instead of a = 3, the fold would be N(0, sqrt(17))
--     giving P ~= 0.113, outside tolerance).
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(3::random_variable * provsql.normal(0, 1)
                              + provsql.normal(0, 4),
                               5::random_variable),
             'independent') - 0.15865525393145707) < 1e-12
       AS scaled_normal_sum_exact;

-- (3) NEG of a normal inside the linear combination: -N(2, 1) + N(2, 1)
--     folds to N(0, sqrt(2)).  Threshold > 1 again, same truth as (1)
--     by symmetry: a wrong sign on the NEG decomposition (a = +1
--     instead of -1) would fold to N(4, sqrt(2)), giving
--     P(N(4, sqrt(2)) > 1) ~= 0.983, far outside tolerance.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(-provsql.normal(2, 1) + provsql.normal(2, 1),
                                1::random_variable),
             'independent') - 0.23975006109347664) < 1e-12
       AS neg_normal_in_closure_exact;

-- (4) Shared-UUID PLUS: x + x aggregates via try_plus_aggregate to
--     `2 * x`, which then folds via try_times_scalar_rv to N(0, 2).
--     The cmp resolves analytically off that single bare gate_rv:
--     P(2x > 1) = P(x > 0.5) = 1 - Phi(0.5) ≈ 0.3085.  A wrongful
--     independence-assumed fold (independent-normal closure firing
--     before aggregation) would give N(0, sqrt(2)) and P ≈ 0.2398,
--     well outside the 1e-12 tolerance.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(x + x, 1::random_variable),
             'independent') - 0.3085375387259869) < 1e-12
       AS shared_uuid_plus_collapses_to_2X
FROM (SELECT provsql.normal(0, 1) AS x) r;

-- (4b) Mixed coefficients on the same RV: `2*x + x` aggregates to
--     `3*x` (coefficient sum), then folds to N(0, 3) via the scalar
--     closure.  P(3x > 3) = P(x > 1) = 1 - Phi(1) ≈ 0.1587.  A miss of
--     the coefficient sum (e.g. dropping the inner 2*x's coefficient
--     and treating it as just `x`) would fold to N(0, 2) and give
--     P ≈ 0.3085, well outside tolerance.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(2::random_variable * x + x,
                                3::random_variable),
             'independent') - 0.15865525393145707) < 1e-12
       AS shared_uuid_plus_mixed_coeffs
FROM (SELECT provsql.normal(0, 1) AS x) r;

-- (4c) Coefficient cancellation: `x + (-x)` aggregates to coefficient
--     0 on x, collapsing the PLUS to gate_value:0.  The cmp `0 > 0`
--     resolves to P = 0 via RangeCheck on the value gate.  A miss of
--     the NEG sign in the linear-term decomposition would aggregate
--     to 2*x or keep the PLUS unfolded, giving 0.5 / 0.5 respectively.
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(x + (-x), 0::random_variable),
         'independent') = 0.0
       AS shared_uuid_plus_cancels_to_zero
FROM (SELECT provsql.normal(0, 1) AS x) r;

-- (4d) DIV-by-constant canonicalisation: `x / 2` rewrites to
--     `x * (1/2)` in the DIV-to-TIMES canonicalisation pass, then
--     folds via try_times_scalar_rv to N(0, 0.5).
--     P(x/2 > 1) = P(x > 2) = 1 - Phi(2) ≈ 0.0228.  Without the
--     canonicalisation the DIV gate would stay unfolded, the
--     analytic path would fail to match a bare gate_rv, and the
--     evaluator would fall back to MC -- outside the 1e-12
--     analytical tolerance.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(provsql.normal(0, 1) / 2::random_variable,
                                1::random_variable),
             'independent') - 0.022750131948179195) < 1e-12
       AS div_by_constant_folds_through_times;

-- ---------------------------------------------------------------
-- Erlang-family closure: PLUS over k i.i.d. Exp(λ) leaves with the
-- same rate λ and distinct UUIDs folds to a single Erlang(k, λ).
-- ---------------------------------------------------------------

-- (5) Three i.i.d. Exp(1) ~ Erlang(3, 1); P(sum > 3) = e^{-3}(1+3+4.5)
--     = 8.5 / e^3.  Exact via simplifier + analytic CDF.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(provsql.exponential(1)
                              + provsql.exponential(1)
                              + provsql.exponential(1),
                               3::random_variable),
             'independent') - 8.5 * exp(-3.0)) < 1e-12
       AS erlang_fold_exact;

-- (6) Mixed rates: must NOT fold (hypoexponential is outside the
--     closure's scope - the i.i.d.-rate guard inside try_erlang_closure
--     rejects this case).  Falls through to MC; tolerance check
--     verifies the path executed without error.
SET provsql.monte_carlo_seed = 42;
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(provsql.exponential(1) + provsql.exponential(2),
                            1.5::random_variable),
         'monte-carlo', '100000') BETWEEN 0.3 AND 0.7
       AS mixed_rates_not_folded;
RESET provsql.monte_carlo_seed;

-- ---------------------------------------------------------------
-- Constant folding + identity drops.
-- ---------------------------------------------------------------

-- (7) Constant folding of arith(NEG, value:1) -> value:-1.  Before
--     the simplifier, AnalyticEvaluator's "X cmp constant" shortcut
--     would skip a cmp whose constant side was wrapped in NEG,
--     forcing tests to write (-1)::random_variable instead of
--     -1::random_variable.  After the fold the branch fires on the
--     natural form.  P(N(0, 1) > -1) = 1 - Phi(-1) = Phi(1)
--     ~= 0.8413; threshold deliberately within (0, 1) so the test
--     discriminates a wrong sign on the NEG fold (which would give
--     P(N(0, 1) > 1) ~= 0.1587, far outside tolerance).
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(provsql.normal(0, 1),
                                -(1::random_variable)),
             'independent') - 0.8413447460685429) < 1e-12
       AS constant_fold_neg_value;

-- (8) Identity-element drop unlocks the normal closure: the inner
--     `N(0, 1) + 0::random_variable` identity-drops to a singleton
--     PLUS wrapper around N(0, 1); decompose_normal_term recurses
--     through that wrapper, so the outer `+ N(0, 1)` PLUS folds to
--     N(0, sqrt(2)) and the cmp resolves analytically.  Same
--     threshold/truth as (1) so a wrong-variance closure (or a
--     missed identity drop that left the value:0 wire and broke
--     decomposition) would fail the tolerance check.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(provsql.normal(0, 1)
                              + 0::random_variable
                              + provsql.normal(0, 1),
                                1::random_variable),
             'independent') - 0.23975006109347664) < 1e-12
       AS identity_drop_unlocks_normal_closure;

-- (9) Zero absorber on TIMES: arith(TIMES, value:0, X) folds to
--     gate_value:0 regardless of X, via the same identity-drop pass.
--     The resulting cmp `0 > 0.5` is then RangeCheck-decided to 0.0
--     exactly.  Threshold 0.5 deliberately falls between 0 and 1 so
--     a wrong fold to gate_value:1 would flip the cmp to 1 > 0.5
--     = 1.0; the `= 0.0` assertion catches that.
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(0::random_variable * provsql.normal(0, 1),
                            0.5::random_variable),
         'independent') = 0.0
       AS zero_absorber_times;

-- ---------------------------------------------------------------
-- Categorical N-wire mixture lift: push constant scaling / offset
-- inside the [key, mul_1, ..., mul_n] form by minting new mulinputs
-- with transformed value extras.  Shares the key gate with the
-- original so the lifted mixture stays perfectly correlated with the
-- input (FootprintCache flags the shared key as a dependency atom).
-- ---------------------------------------------------------------

-- (10a) TIMES lift on categorical: 3 * cat({0.5, 0.5}, {2, 4}) folds
--      to cat({0.5, 0.5}, {6, 12}).  P(>7) = 0.5 (mass on outcome 12)
--      decided exactly via AnalyticEvaluator's categoricalDecide.  A
--      wrong fold (e.g. missing the scaling, or using factor=1) would
--      give P(>7) = 0 on the original {2, 4} outcomes, far outside
--      the bit-equal 0.5 assertion.
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(
           3::random_variable * provsql.categorical(ARRAY[0.5, 0.5]::float[],
                                                     ARRAY[2.0, 4.0]::float[]),
           7::random_variable),
         'independent') = 0.5
       AS cat_lift_scaled_exact;

-- (10b) PLUS lift on categorical: 10 + cat({0.5, 0.5}, {2, 4}) folds
--      to cat({0.5, 0.5}, {12, 14}).  P(>13) = 0.5 (mass on outcome
--      14) decided exactly.
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(
           10::random_variable + provsql.categorical(ARRAY[0.5, 0.5]::float[],
                                                      ARRAY[2.0, 4.0]::float[]),
           13::random_variable),
         'independent') = 0.5
       AS cat_lift_shifted_exact;

-- (10c) Negative scale on categorical (allowed; flips support but the
--      categorical stays well-formed since outcomes are explicit
--      points).  -2 * cat({0.5, 0.5}, {2, 4}) folds to
--      cat({0.5, 0.5}, {-4, -8}).  P(>-5) = 0.5 (mass on outcome -4).
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(
           (-2)::random_variable * provsql.categorical(ARRAY[0.5, 0.5]::float[],
                                                       ARRAY[2.0, 4.0]::float[]),
           (-5)::random_variable),
         'independent') = 0.5
       AS cat_lift_neg_scale_exact;

-- (10d) Shared-mixture PLUS aggregation: `x+x` where x is a
--      categorical folds via try_plus_aggregate (now treating
--      gate_mixture as a scalar-RV leaf) to `2*x`, which the
--      categorical mixture lift then folds to a new categorical with
--      doubled outcome values.  P(2*x > 3) for x in {1, 2} = mass of
--      outcome 4 = 0.5.  Without the gate_mixture leaf case the
--      aggregator would bail and the PLUS would stay unfolded, falling
--      through to MC.
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(x + x, 3::random_variable),
         'independent') = 0.5
       AS cat_shared_mixture_plus_aggregate
FROM (SELECT provsql.categorical(ARRAY[0.5, 0.5]::float[],
                                  ARRAY[1.0, 2.0]::float[]) AS x) r;

-- ---------------------------------------------------------------
-- Island decomposer: per-cmp MC marginalisation of unresolved
-- continuous-island cmps into Bernoulli gate_input leaves.  Picks
-- up cmps whose shape none of RangeCheck / simplifier /
-- AnalyticEvaluator can resolve - e.g. heterogeneous distributions
-- under arith (the normal closure rejects non-normal wires).
-- Single-cmp islands only; cmps sharing a base RV with another
-- unresolved cmp are detected via footprint overlap and skipped.
-- ---------------------------------------------------------------

-- (12) Disjoint islands.  Two independent N(0,1) + U(-1,1) mixes,
--      neither folded by the simplifier (U isn't normal, so the
--      normal closure bails).  The decomposer MC-marginalises each
--      cmp into its own Bernoulli; 'independent' then computes
--      inclusion-exclusion over them.  By the symmetry of N(0,1)
--      and U(-1,1) around zero, P(N + U > 0) = 0.5 exactly, so the
--      analytical truth of the disjunction is 0.5 + 0.5 - 0.25
--      = 0.75.  Without the decomposer, 'independent' would error
--      on the unresolved gate_arith leaves.
SET provsql.monte_carlo_seed = 42;
SET provsql.rv_mc_samples = 20000;
SELECT abs(provsql.probability_evaluate(
             provsql.provenance_plus(ARRAY[
               provsql.rv_cmp_gt(provsql.normal(0, 1)
                                + provsql.uniform(-1, 1),
                                  0::random_variable),
               provsql.rv_cmp_gt(provsql.normal(0, 1)
                                + provsql.uniform(-1, 1),
                                  0::random_variable)
             ]),
             'independent') - 0.75) < 0.02
       AS disjoint_islands_inclusion_exclusion;
RESET provsql.rv_mc_samples;
RESET provsql.monte_carlo_seed;

-- (13) Shared island, joint-table inline via 'monte-carlo'.  Two
--      cmps share the SAME (N + U) arith gate (via a CTE binding so
--      r.expr is the same gate_t in both cmp_gt calls).  The
--      dependent truth is P(x > 0 OR x > 1) = P(x > 0) = 0.5 by the
--      subset relation `x > 1 implies x > 0`.  The decomposer
--      detects the shared footprint, samples the joint distribution
--      over (cmp_A, cmp_B), and inlines a 4-way mulinput block;
--      cmp_A and cmp_B are rewritten as gate_plus over the mulinputs
--      with their bit set.  After rewriteMultivaluedGates (called
--      inside the legacy 'monte-carlo' path) the Bayesian tree
--      preserves the joint dependence, so the BC monteCarlo over
--      the OR gives ~0.5.  If the decomposer had wrongly treated
--      the two cmps as independent, each marginal Bernoulli (0.5
--      and ~0.2) OR'd independently would give ~0.6 - well outside
--      tolerance.
SET provsql.monte_carlo_seed = 42;
WITH r AS (SELECT provsql.normal(0, 1) + provsql.uniform(-1, 1) AS expr)
SELECT abs(provsql.probability_evaluate(
             provsql.provenance_plus(ARRAY[
               provsql.rv_cmp_gt(r.expr, 0::random_variable),
               provsql.rv_cmp_gt(r.expr, 1::random_variable)
             ]),
             'monte-carlo', '100000') - 0.5) < 0.01
       AS shared_island_joint_table_mc
FROM r;
RESET provsql.monte_carlo_seed;

-- (14) Same shared-island shape via 'tree-decomposition'.  The
--      tree-decomposition method calls rewriteMultivaluedGates
--      on the joint-table mulinputs and then runs the
--      d-DNNF-based evaluator on the resulting Boolean circuit,
--      which handles repeated MULIN references across cmps
--      cleanly (the Bayesian-tree rewrite re-shares the per-block
--      Bernoulli decisions across cmp_A and cmp_B by construction).
--      The result should match the dependent truth (0.5) within
--      the joint-table MC noise.
SET provsql.monte_carlo_seed = 42;
WITH r AS (SELECT provsql.normal(0, 1) + provsql.uniform(-1, 1) AS expr)
SELECT abs(provsql.probability_evaluate(
             provsql.provenance_plus(ARRAY[
               provsql.rv_cmp_gt(r.expr, 0::random_variable),
               provsql.rv_cmp_gt(r.expr, 1::random_variable)
             ]),
             'tree-decomposition') - 0.5) < 0.02
       AS shared_island_joint_table_treedec
FROM r;
RESET provsql.monte_carlo_seed;

-- (15) Multi-cmp shared island where the cmps do NOT share a
--      common scalar lhs.  Two cmps `(X + Y_A) > 0` and
--      `(X + Y_B) > 0` share base RV X (their footprints overlap
--      on X) but use different arith composites (Y_A and Y_B are
--      distinct fresh uniforms, so the lhs gate_arith UUIDs
--      differ).  The monotone-shared-scalar fast path's
--      detect_shared_scalar checks for an identical lhs gate_t
--      and bails on this case; the generic 2^k MC joint table is
--      the only correct handler.  Test pins the dependent truth
--      ~ 0.621 against the wrong-independent ~ 0.75 to detect a
--      regression where detection wrongly accepts disparate
--      scalars (e.g. a future canonicalisation that conflates
--      different arith subtrees with the same base-RV footprint).
--
--      With X ~ N(0, 1) and Y_A, Y_B ~ U(-1, 1) all independent,
--      P(A) = P(B) = 0.5 by symmetry; conditional on X = x the two
--      cmps are independent, so P(A AND B) integrates
--      `clip((1 + x) / 2, 0, 1)^2` against the standard normal
--      density, yielding ~0.379.  P(A OR B) ~= 0.621.  The
--      independent-Bernoulli answer would be 0.5 + 0.5 - 0.25
--      = 0.75 - well outside the 0.05 tolerance.
SET provsql.monte_carlo_seed = 42;
SET provsql.rv_mc_samples = 20000;
WITH r AS (SELECT provsql.normal(0, 1) AS x)
SELECT abs(provsql.probability_evaluate(
             provsql.provenance_plus(ARRAY[
               provsql.rv_cmp_gt(r.x + provsql.uniform(-1, 1),
                                 0::random_variable),
               provsql.rv_cmp_gt(r.x + provsql.uniform(-1, 1),
                                 0::random_variable)
             ]),
             'tree-decomposition') - 0.621) < 0.05
       AS shared_island_disparate_scalars
FROM r;
RESET provsql.rv_mc_samples;
RESET provsql.monte_carlo_seed;

-- (16) Fast-path analytical CDF on a shared bare-RV scalar.  Two
--      cmps `X > 0` and `X > 1` over the SAME N(0, 1) (CTE-bound,
--      so r.x is the same gate_t in both cmps).  The decomposer
--      runs before AnalyticEvaluator so the shared footprint
--      groups the cmps; detect_shared_scalar matches (same lhs
--      gate_t, gate_value rhs) and the fast path computes the 3
--      interval probabilities via cdfAt on the normal CDF -
--      analytically, no MC anywhere:
--        P((-inf, 0])  = Phi(0)      = 0.5
--        P((0, 1])     = Phi(1) - 0.5 ~ 0.34134
--        P((1, +inf))  = 1 - Phi(1)   ~ 0.15866
--      P(X > 0 OR X > 1) = P(X > 0) = 0.5 exactly by the subset
--      relation `x > 1 implies x > 0`, so 'tree-decomposition' over
--      the joint mulinput block recovers 0.5 to float8 precision.
--      Test asserts bit-equal 0.5 (no tolerance) - this assertion
--      is only achievable via the analytical branch; if the fast
--      path bailed to MC binning or the analytical CDF were
--      miscomputed, the result would land off-bit.  The wrong-
--      independent answer (silent AnalyticEvaluator pre-emption
--      before the reorder) would give 0.5 + 0.1587 - 0.5*0.1587
--      ~ 0.579, far from 0.5.
WITH r AS (SELECT provsql.normal(0, 1) AS x)
SELECT provsql.probability_evaluate(
         provsql.provenance_plus(ARRAY[
           provsql.rv_cmp_gt(r.x, 0::random_variable),
           provsql.rv_cmp_gt(r.x, 1::random_variable)
         ]),
         'tree-decomposition') = 0.5
       AS shared_bare_rv_fast_path_exact
FROM r;

-- ---------------------------------------------------------------
-- Sanity: with provsql.hybrid_evaluation = off the simplifier does
-- not run.  The same queries fall through to the original paths:
-- - The normal-sum cmp reaches the BoolExpr semiring without a
--   resolved Bernoulli, which throws (`This semiring does not
--   support value gates.`).
-- - The same query via 'monte-carlo' still works via the RV-aware
--   MC sampler over gate_arith / gate_rv, within sampling tolerance.
-- ---------------------------------------------------------------

SET provsql.hybrid_evaluation = off;

\set VERBOSITY terse
SELECT provsql.probability_evaluate(
         provsql.rv_cmp_gt(provsql.normal(0, 1) + provsql.normal(0, 1),
                            0::random_variable),
         'independent');
\set VERBOSITY default

SET provsql.monte_carlo_seed = 42;
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_gt(provsql.normal(0, 1) + provsql.normal(0, 1),
                                0::random_variable),
             'monte-carlo', '100000') - 0.5) < 0.01
       AS hybrid_off_falls_through_to_mc;
RESET provsql.monte_carlo_seed;

RESET provsql.hybrid_evaluation;

-- ---------------------------------------------------------------
-- Shortest round-trip in double_to_text: scalar parameters written
-- back into a folded gate_rv's extra should print as the shortest
-- decimal that round-trips through std::stod, not the verbose
-- precision-17 form.  E.g. `2 * Exp(0.4)` folds to Exp(0.2); the
-- extra should be "exponential:0.2", not "exponential:0.20000000000000001".
-- This pins the behaviour of the std::to_chars-based formatter.
-- The simplifier runs against the in-memory GenericCircuit returned
-- by getGenericCircuit, so we read the folded extra via
-- simplified_circuit_subgraph (which dispatches through the loader).
-- ---------------------------------------------------------------
WITH q AS (SELECT 2 * provsql.exponential(0.4) AS r),
     j AS (
       SELECT provsql.simplified_circuit_subgraph(
                (r)::uuid, 1)::text AS s
         FROM q
     )
SELECT s LIKE '%"extra": "exponential:0.2"%'
       AS double_to_text_shortest_roundtrip
FROM j;

-- ---------------------------------------------------------------
-- Affine closures: MINUS-to-PLUS canonicalisation, NEG-of-RV on
-- Normal / Uniform, Uniform-family closure with a constant offset.
--
-- The simplifier rewrites @c MINUS(A, B) as @c PLUS(A, NEG(B)) up
-- front, then the existing normal-family / Erlang-family closures
-- and the new uniform-family closure handle the resulting PLUS.
-- @c try_neg_rv (pass 2) folds a standalone @c NEG over a bare
-- Normal / Uniform leaf; it bails on Exp / Erlang (negative support
-- is not those families).
--
-- We pin via @c simplified_circuit_subgraph at depth 1: when a fold
-- succeeds the root collapses to a single bare @c gate_rv, so the
-- depth-1 view's only row is the root itself with the expected
-- gate_type and extra.  Negative cases assert the root stays
-- @c gate_arith.
-- ---------------------------------------------------------------

-- (1) -N(2, 0.5) folds to N(-2, 0.5).
WITH q AS (SELECT - provsql.normal(2, 0.5) AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'normal:-2,0.5'
       AS neg_normal_folds
FROM j;

-- (2) -U(1, 3) folds to U(-3, -1).
WITH q AS (SELECT - provsql.uniform(1, 3) AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'uniform:-3,-1'
       AS neg_uniform_folds
FROM j;

-- (3) N(2, 0.5) - 1 folds to N(1, 0.5).
WITH q AS (SELECT provsql.normal(2, 0.5) - 1::random_variable AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'normal:1,0.5'
       AS normal_minus_constant_folds
FROM j;

-- (4) 1 - N(2, 0.5) folds to N(-1, 0.5).
WITH q AS (SELECT 1::random_variable - provsql.normal(2, 0.5) AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'normal:-1,0.5'
       AS constant_minus_normal_folds
FROM j;

-- (5) U(1, 3) - 0.5 folds to U(0.5, 2.5).
WITH q AS (SELECT provsql.uniform(1, 3) - 0.5::random_variable AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'uniform:0.5,2.5'
       AS uniform_minus_constant_folds
FROM j;

-- (6) 0.5 - U(1, 3) folds to U(-2.5, -0.5).  Exercises the chain
--     MINUS -> PLUS(value, NEG(U)) -> uniform closure with a == -1.
WITH q AS (SELECT 0.5::random_variable - provsql.uniform(1, 3) AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'uniform:-2.5,-0.5'
       AS constant_minus_uniform_folds
FROM j;

-- (7) U(1, 3) + 2 folds to U(3, 5).
WITH q AS (SELECT provsql.uniform(1, 3) + 2::random_variable AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'rv'
       AND (s -> 0 ->> 'extra') = 'uniform:3,5'
       AS uniform_plus_constant_folds
FROM j;

-- Negative (8): -Exp(0.4) keeps a NEG gate (negative support is no
-- longer exponential).
WITH q AS (SELECT - provsql.exponential(0.4) AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'arith'
       AND (s -> 0 ->> 'info1') = '4'  -- PROVSQL_ARITH_NEG
       AS neg_exp_stays
FROM j;

-- Negative (9): Exp + c keeps a PLUS gate (shifted exponential is no
-- longer exponential).
WITH q AS (SELECT provsql.exponential(0.4) + 1::random_variable AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'arith'
       AND (s -> 0 ->> 'info1') = '0'  -- PROVSQL_ARITH_PLUS
       AS exp_plus_constant_stays
FROM j;

-- Negative (10): U + U with distinct uniforms keeps a PLUS gate
-- (sum of two independent uniforms is triangular, not uniform).
WITH q AS (SELECT provsql.uniform(0, 1) + provsql.uniform(0, 1) AS r),
     j AS (SELECT provsql.simplified_circuit_subgraph(
                    (r)::uuid, 1)::jsonb AS s
              FROM q)
SELECT (s -> 0 ->> 'gate_type') = 'arith'
       AND (s -> 0 ->> 'info1') = '0'  -- PROVSQL_ARITH_PLUS
       AS uniform_plus_uniform_stays
FROM j;

-- End-to-end: the new -N fold unlocks an AnalyticEvaluator path.
-- P(-N(2.5, 0.5) < -2) = P(N(2.5, 0.5) > 2) = 1 - Phi((2-2.5)/0.5)
-- = 1 - Phi(-1) = Phi(1) ~= 0.8413.  Before the new -N fold the
-- comparator would have flowed to the MC marginalisation; after the
-- simplifier folds -N(2.5, 0.5) to N(-2.5, 0.5), AnalyticEvaluator
-- resolves the cmp via the closed-form normal CDF.  The 1e-12
-- tolerance is the analytical-exact tolerance, only achievable when
-- the analytical path actually fires.
SELECT abs(provsql.probability_evaluate(
             provsql.rv_cmp_lt(-provsql.normal(2.5, 0.5),
                                (-2)::random_variable),
             'independent') - 0.8413447460685429) < 1e-12
       AS neg_normal_unlocks_analytic;

SELECT 'ok'::text AS continuous_hybrid_done;
