Continuous Distributions
========================

This page describes the architecture of ProvSQL's continuous
random-variable surface: the on-disk gates, the per-family
``Distribution`` class hierarchy and its rule registries, the SQL
composite type, the planner-hook rewriter's classifier, the Monte
Carlo sampler, the RangeCheck / AnalyticEvaluator / Expectation
chain, the HybridEvaluator's simplifier and island decomposer, the
conditional-inference path, the aggregate dispatch, and the Studio
rendering hooks. The user-facing description lives in
:doc:`../user/continuous-distributions`.

Gate Types
----------

Four gate types described below back the continuous surface, plus
the ``gate_observe`` evidence gate covered in *Latent variables*
further down; all are appended to the
:cfunc:`gate_type` enum in :cfile:`provsql_utils.h` before the
``gate_invalid`` sentinel, with no renumbering of the existing
values. The companion ``provenance_gate`` SQL enum in
``sql/provsql.common.sql`` mirrors the C enum identically. (The
conditioning marker ``gate_conditioned`` is shared with the
Boolean surface and described in :doc:`probability-evaluation`;
its interaction with the moment evaluators is covered in
*Conditional Evaluation* below.)

``gate_rv``
    Random-variable leaf. The gate's ``extra`` blob carries the
    distribution as text, ``<family>:<p1>[,<p2>]`` – e.g.
    ``"normal:2.5,0.5"`` for ``normal(2.5, 0.5)`` or
    ``"exponential:2"`` for ``exponential(2)``. The family token
    is *not* an enum: it is resolved at parse time against the
    distribution registry (see *The Distribution Class Hierarchy*
    below), so the set of valid blobs grows with the registered
    families – currently ``normal``, ``uniform``, ``exponential``,
    ``erlang``, ``gamma``, ``lognormal``, ``weibull``, ``pareto``,
    ``beta``, ``logistic``, ``inverse_gamma``, ``inverse_gaussian``,
    plus the discrete count families ``poisson``, ``binomial``,
    ``geometric``, ``negative_binomial`` (whose blobs appear as
    ``gate_rv`` leaves only in the parametric/latent form – see
    *Latent variables* below).

    Categorical random variables share no ``gate_rv`` encoding;
    they are encoded as a block of ``gate_mulinput`` gates
    under a ``gate_mixture`` (see below). The *literal* discrete
    count constructors (Poisson, Binomial, …) are SQL-level
    constructors over that categorical encoding, not gate types.

``gate_arith``
    ``N``-ary arithmetic over scalar children. The operator tag
    lives in ``info1`` of the gate's ``GateInformation``:
    ``provsql_arith_op`` is ``PLUS = 0``, ``TIMES = 1``,
    ``MINUS = 2``, ``DIV = 3``, ``NEG = 4``, ``MAX = 5``,
    ``MIN = 6``, ``POW = 7``, ``LN = 8``, ``EXP = 9``,
    ``PERCENTILE = 10`` (the ``percentile_cont`` order statistic,
    see *Statistic aggregates* below).
    ``MAX`` / ``MIN`` are the n-ary order statistics behind
    ``greatest`` / ``least`` and the ``max`` /
    ``min`` aggregates. ``POW`` (binary, real branch only) and
    ``LN`` carry evaluation-time domain guards (see the sampler
    section); ``EXP`` is total. The enum is append-only: the
    values are persisted on disk and must not be renumbered.

``gate_mixture``
    Probabilistic mixture. The wire vector is
    ``[p, x, y]`` for a Bernoulli mixture (with ``p`` a Boolean
    gate and ``x``, ``y`` scalar RV roots) or
    ``[key, mul_1, …, mul_n]`` for a categorical block (with
    ``key`` a fresh ``gate_input`` anchoring the block and
    each ``mul_i`` a ``gate_mulinput`` carrying the
    outcome value in its ``extra`` and its probability via
    :sqlfunc:`set_prob`).

``gate_case``
    N-ary guarded selection over scalar children, with first-match
    semantics: the wire vector is
    ``[guard_1, value_1, …, guard_k, value_k, default]`` (odd
    length ``2k + 1``), and the gate's value is the ``value_i`` of
    the first guard (a Boolean event, typically a ``gate_cmp``)
    that holds, else the ``default``. It carries data only in its
    wires – no ``info`` / ``extra``, following the
    ``gate_conditioned`` precedent. Minted by
    ``provenance_case`` (and its ``random_variable``
    wrapper ``rv_case``), the target of the planner hook's
    lowering of SQL ``CASE`` over ``random_variable`` branches.
    It is a real arm in the MC sampler, in RangeCheck (support =
    union of the value branches), and in the Expectation evaluator
    (closed-form moments via the guard-partition integrator below,
    Monte Carlo otherwise); like every measure-carrier gate it is
    refused by the general ``sr_*`` semirings (a guarded selection
    is not a semiring operation).

In addition, ``gate_value`` gains a *float8 mode*: the
``extra`` blob is parsed as a double by the RV evaluators
(``parseDoubleStrict`` in the MC sampler,
``extract_finite_double`` in RangeCheck), coexisting with
:cfile:`having_semantics.cpp`'s string-based
``extract_constant_string`` path, so ``gate_value`` covers both
the deterministic mode used in HAVING sub-circuits and the
random-variable-constant mode used by :sqlfunc:`as_random`.

The Distribution Class Hierarchy
--------------------------------

``src/distributions/`` holds the per-family class hierarchy that
every evaluator dispatches through; no evaluator names a family.
The abstract interface is ``distributions/Distribution.h``;
the registries live in ``distributions/Distribution.cpp``;
each family is one self-contained implementation file
(``normal.cpp``, ``uniform.cpp``, ``exponential.cpp``,
``erlang.cpp``, ``gamma.cpp``, ``lognormal.cpp``,
``weibull.cpp``, ``pareto.cpp``, ``beta.cpp``, ``logistic.cpp``,
``inverse_gamma.cpp``, ``inverse_gaussian.cpp``, and the discrete
``poisson.cpp``, ``binomial.cpp``, ``geometric.cpp``,
``negative_binomial.cpp``) sharing only the
internal header ``DistributionCommon.h``. The ``logistic`` family
is the location-scale ``logistic(μ, s)`` whose CDF is the logistic
sigmoid :math:`F(x) = \sigma((x - \mu)/s)` and whose quantile is
the logit :math:`\mu + s\,\ln(p/(1-p))`, so it realises the
logit-link selection noise exactly; its mean is affine in ``μ``
(``meanIsAffine``).

A ``Distribution`` is a transient per-family view constructed
from a parsed spec by ``makeDistribution``. Its interface
groups into:

- **identity**: ``family()`` returns the interned
  ``DistributionFamily`` descriptor (name token, parameter count,
  display label, parameter symbols, factory) – descriptor-pointer
  equality *is* family identity; there is no family enum anywhere;
- **closed-form moments**: ``mean()``, ``variance()``,
  ``rawMoment(k)``, plus the optional ``truncatedRawMoment(lo,
  hi, k)`` and ``iidOrderStatMean(n, isMax)``;
- **density / distribution**: ``pdf(x)``, ``cdf(x)`` – a family
  that has no closed form returns NaN (the *NaN-as-undecided*
  contract: callers treat NaN as "fall back", never as a value);
  the optional ``quantile(p)`` returns ``nullopt`` when there is
  no elementary inverse;
- **ranges**: ``support()``, ``integrationRange()`` (a finite
  quadrature window), ``plotRange()`` (for Studio's density
  previews);
- **sampling**: ``sample(rng)``, plus the optional
  ``sampleTruncated(rng, lo, hi, n)`` for rejection-free
  conditioned draws;
- **structure**: ``affine(a, b)`` (the family's image under
  ``a·X + b``, or ``nullptr`` when the image leaves the family),
  ``asDirac()`` (degenerate point masses), and ``serialise()``
  (the on-disk ``extra`` text, inverse of
  :cfunc:`parse_distribution_spec`).

Alongside the family table, four *rule registries* capture the
pairwise closed forms, all keyed by family-name tokens and all
populated at static initialisation:

- the **comparator registry** maps a family pair to a
  ``P(X < Y)`` closed form, consulted by ``comparatorPairLess``
  (used by the AnalyticEvaluator; a miss falls through to the
  quadrature described below);
- the **sum-closure registry** maps a family pair to a rule
  folding a list of ``a·Z + b`` terms into a single distribution,
  consulted by ``closePlusTerms`` (used by the HybridEvaluator;
  this is how a same-rate Exponential / Erlang chain folds into
  one Erlang, and any linear combination of independent normals
  into one normal);
- the **product-closure registry** does the same for TIMES wires
  via ``closeProductFactors`` (independent lognormals multiply in
  log space);
- the **transform registry** maps a ``(transform, family)`` pair
  – transform names are the opcode-free strings ``"ln"`` /
  ``"exp"`` – to the image distribution, consulted by
  ``closeTransform`` (``exp(Normal) → Lognormal``,
  ``ln(Lognormal) → Normal``).

``numericQuantile`` (same file) is the family-agnostic fallback
inverse CDF: a monotone bisection of ``cdf()`` over the family's
``integrationRange``, used whenever ``quantile()`` declines
(Erlang, Gamma, Beta, Inverse-Gamma, Inverse-Gaussian).

A family file self-registers: it defines one static
``DistributionFamily`` descriptor plus a
``DistributionFamilyRegistrar`` (and any
comparator/closure/product/transform registrar objects), all of
which run at static initialisation. **Adding a family is one new
self-registering** ``src/distributions/<name>.cpp`` **plus its
SQL constructor** in ``sql/provsql.common.sql`` – no shared
header, enum, parser, or evaluator is touched, and every readout
(moments, quantiles, sampling, comparisons, Studio rendering)
picks the family up through the registries.

``DistributionSpec`` (:cfile:`RandomVariable.h`) is the parsed
form of a ``gate_rv``'s ``extra`` blob: an interned
family-descriptor pointer plus up to two parameters.
:cfunc:`parse_distribution_spec` (:cfile:`RandomVariable.cpp`)
splits the blob on ``:``, resolves the token through the family
registry, and parses the comma-separated parameters per the
descriptor's arity; ``serialise()`` is its inverse. The
:cfunc:`analytical_mean` / ``analytical_variance`` /
``analytical_raw_moment`` helpers there are thin wrappers over
``makeDistribution(spec)->…``.

Not every family implements every optional capability, and the
evaluators degrade per capability, not per family: exact
truncated moments need ``truncatedRawMoment`` (Normal, Uniform,
Exponential, Lognormal, Weibull, Pareto, Beta); rejection-free
conditioned sampling needs ``sampleTruncated`` (the same list
minus Beta, plus Logistic); elementary quantiles need
``quantile()`` (Normal –
a Beasley-Springer-Moro start polished to machine precision by
two Newton steps – Uniform, Exponential, Lognormal, Weibull,
Pareto, Logistic, and the discrete families; the rest bisect);
closed-form i.i.d. order-statistic
means need ``iidOrderStatMean`` (Uniform, Exponential, Weibull,
Pareto). A missing capability falls through to quadrature,
bisection, or Monte Carlo as appropriate.

The guard-partition integrator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``gate_case`` moments and conjunction conditioning are evaluated in
closed form (:cfile:`Expectation.cpp`), not sampled, whenever the shape
reduces to a **one-dimensional pivot-conjunction integral**
:math:`\int x^k f_X(x)\,\prod_j W_j(x)\,dx`, where each factor
:math:`W_j` is a partner variable's CDF :math:`F_{Y_j}(x)` (for
:math:`X > Y_j`) or its complement, and constant comparisons clip the
integration window (``pivotConjunctionIntegral``, composite Simpson,
exact for the polynomial uniform integrands). The shared partner
variables are marginalised analytically because distinct bare
``gate_rv`` leaves are independent. Three ``gate_case`` tiers feed it:

- **single-pivot piecewise** (``singlePivotCaseRawMoment``): every guard
  compares one pivot RV to a constant and every branch is affine in it
  (``abs`` / ``clamp`` / ReLU); partitions the pivot's support at the
  thresholds and accumulates ``truncatedRawMoment`` over each interval;
- **two-arm two-RV** (``twoArmCaseRawMoment``): a single guard between
  two distinct RVs, each branch affine in one operand (min / max pair);
- **order statistic** (``orderStatCaseRawMoment``): a first-match
  tournament recognised as ``max`` / ``min`` of the branch RVs by
  simulating the selection over every strict ordering (``n!``, capped),
  then summed as one pivot-conjunction integral per RV-is-extremum term.

Conjunction conditioning ``E[X^k | ∧_j (X op Y_j)]`` is the ratio
:math:`I_k / I_0` of two such integrals
(``matchPivotConjunctionConditional`` /
``try_pivotConjunction_conditional_moment``), slotting into the
conditional-moment chain after the single-comparison truncation and
rv-vs-rv paths.

The **probability** side of the same shape -- a correlated island of
comparisons all sharing one pivot RV, e.g. ``probability((x>y)|(x>z))``
-- is resolved by an analytic :math:`2^k` joint table in
:cfile:`HybridEvaluator.cpp` (``detect_shared_pivot_rv`` /
``inline_analytic_pivot_joint_table``): each outcome word's cell
probability is one pivot-conjunction integral, installed as mulinputs
under a shared key exactly like the Monte-Carlo ``inline_joint_table``.
This is the RV-vs-RV analogue of the monotone-shared-scalar fast path
(``detect_shared_scalar``), which handles the all-constant-threshold
case; both keep correlated comparison joints exact at
``rv_mc_samples = 0`` instead of collapsing to a product of marginals.

SQL Surface
-----------

The type ``random_variable`` is a thin wrapper around the UUID of
the provenance gate behind the variable: the UUID is the single
source of truth, and every downstream evaluator (``MonteCarloSampler``,
``AnalyticEvaluator``, ``Expectation``, ``RangeCheck``,
``HybridEvaluator``) dispatches on the gate it points at, parsing
the distribution from the gate's ``extra`` blob. Its IO functions
live in :cfile:`random_variable_type.c`; the C++-side parsing
helpers live in :cfile:`RandomVariable.cpp` (see the previous
section).

Constructors are PL/pgSQL functions in
``sql/provsql.common.sql``: :sqlfunc:`normal`,
:sqlfunc:`uniform`, :sqlfunc:`exponential`, :sqlfunc:`erlang`,
:sqlfunc:`gamma` (with :sqlfunc:`chi_squared` sugar; an integer
shape routes through :sqlfunc:`erlang`), :sqlfunc:`lognormal`,
:sqlfunc:`weibull` (``k = 1`` routes through
:sqlfunc:`exponential`), :sqlfunc:`pareto`, :sqlfunc:`beta`
(``beta(1,1)`` routes through :sqlfunc:`uniform`),
:sqlfunc:`logistic`, :sqlfunc:`inverse_gamma`,
:sqlfunc:`inverse_gaussian`,
:sqlfunc:`categorical`, :sqlfunc:`mixture` (two overloads), and
:sqlfunc:`as_random` (three numeric overloads via the
``double precision`` form). They validate parameters and mint
the appropriate gate via :sqlfunc:`create_gate`, ``set_extra``,
:sqlfunc:`set_prob`, ``set_infos``.

The discrete count families are pure-SQL constructors over the
categorical encoding: :sqlfunc:`poisson`, :sqlfunc:`binomial`,
:sqlfunc:`geometric`, :sqlfunc:`hypergeometric`, and
:sqlfunc:`negative_binomial` each enumerate their pmf by a
log-space recurrence (numerically stable at large parameters, no
``lgamma`` dependency) into
:sqlfunc:`categorical_from_log_pmf`, which subtracts the maximum
log-mass, drops outcomes below a ``1e-15`` relative tail,
renormalises, and calls :sqlfunc:`categorical`.
``categorical_from_log_pmf`` is itself public – the "arbitrary
user-defined discrete density" surface. Infinite supports are
truncated at the same relative tail; a 10000-outcome cap raises
with the suggested continuous approximation; degenerate
parameters route through :sqlfunc:`as_random`.

Three further constructors package common fitted-density shapes,
each minting **no new gate** – they decompose into the existing
mixture / categorical machinery so every evaluator handles them
for free:

- :sqlfunc:`gmm` ``(weights, means, stddevs)`` is a Gaussian
  mixture: a stick-breaking cascade of Bernoulli ``gate_mixture``
  nodes over ``gate_rv`` Normal leaves (component ``i`` selected
  with conditional probability ``w_i / (w_i + … + w_n)`` so the
  joint selection probabilities are exactly ``weights``). Moments
  are closed-form through the mixture recursion, sampling is
  exact; zero-weight components are skipped and a single
  positive-weight component returns its Normal directly.
  Validation mirrors :sqlfunc:`categorical` (same-length non-empty
  arrays, weights in ``[0, 1]`` summing to ``1`` within ``1e-9``).
- :sqlfunc:`empirical_samples` ``(samples)`` loads a sample bundle
  (Monte Carlo / MCMC / bootstrap draws) as its ecdf: the discrete
  distribution putting mass ``1/n`` on each draw (duplicates
  merge). It reduces entirely to :sqlfunc:`categorical`, so the
  exact discrete surface applies – sample moments, analytic
  "fraction below ``c``" comparisons, and exact empirical
  quantiles – subject to the same 10000-distinct-value cap.
- :sqlfunc:`empirical_cdf` ``(grid, cdf)`` loads a tabulated
  piecewise-linear CDF (percentile tables, risk models,
  elicited forecasts) as a stick-breaking cascade of Bernoulli
  :sqlfunc:`mixture` nodes over :sqlfunc:`uniform` components –
  mass ``cdf[i+1] − cdf[i]`` spread uniformly over
  ``(grid[i], grid[i+1])`` – plus an optional :sqlfunc:`as_random`
  atom of mass ``cdf[1]`` at ``grid[1]`` when ``cdf[1] > 0``.
  Moments and sampling are exact through the mixture machinery;
  comparisons ride Monte Carlo.

:sqlfunc:`rv_families` (:cfile:`rv_families.cpp`) exposes the
family registry as a SRF – one row per registered family with
its on-disk name token, parameter count, parameter-symbol array,
and display label. UI clients (Studio) read it so newly added
families render without a client release.

The fresh-randomness constructors (every distribution
constructor that mints an anonymous gate) are ``VOLATILE`` to
prevent constant-folding under ``STABLE`` or ``IMMUTABLE`` from
collapsing two independent draws into a single shared gate. The
deterministic constructors (:sqlfunc:`as_random` and the
three-argument ``mixture(p uuid, x random_variable,
y random_variable)`` overload, both of which mint a v5-derived
UUID keyed on their inputs) are ``IMMUTABLE``.

Arithmetic operators ``+ - * / -`` on
``(random_variable, random_variable)`` resolve to
``random_variable_plus`` and siblings, each a one-line SQL
function that calls ``provenance_arith`` with the appropriate
``provsql_arith_op`` tag. The transform surface follows the same
pattern: the ``^`` operator and :sqlfunc:`pow` / ``power``
resolve to ``random_variable_pow`` (opcode ``POW``),
:sqlfunc:`ln` and :sqlfunc:`exp` to their opcodes, and
:sqlfunc:`sqrt` is pure ``x ^ 0.5`` sugar with no opcode of its
own. ``greatest`` / ``least`` (quoted – they shadow
keywords) build ``MAX`` / ``MIN`` gates over their variadic
arguments, de-duplicating identical child gates first and
collapsing a single survivor to itself. Comparison operators
``< <= = <> >= >`` resolve to placeholder procedures that
*raise* if executed; the planner hook intercepts every such
``OpExpr`` and rewrites it before the executor sees it (see the
classifier section below).

Implicit casts ``integer → random_variable``,
``numeric → random_variable``, ``double precision →
random_variable`` are declared explicitly so that
``WHERE rv > 2`` and ``WHERE 2.5 > rv`` resolve uniformly via the
``(rv, rv)`` operator declarations – and so a scalar exponent in
``x ^ 0.5`` lifts to the ``(rv, rv)`` operator.

Planner-Hook Rewriting
----------------------

The transformation that lifts ``WHERE`` and join predicates on
``random_variable`` columns into the row's provenance circuit
lives in the same :cfunc:`provsql_planner` hook in
:cfile:`provsql.c` that handles deterministic
provenance tracking and the :cfunc:`agg_token` HAVING surface.

The central walker is :cfunc:`migrate_probabilistic_quals`. It
walks every qual in the input query and routes each into one of
four mutually-exclusive classes (the ``qual_class`` enum):

- ``QUAL_PURE_AGG``: the qual is built only from ``agg_token``
  comparators (the pre-existing HAVING pathway).
- ``QUAL_PURE_RV``: the qual is built only from
  ``random_variable`` comparators.
- ``QUAL_DETERMINISTIC``: the qual contains no probabilistic
  comparator and stays in the WHERE clause as ordinary SQL.
- A short tail of *mixed-error* classes flagged so the rewriter
  raises a clean diagnostic rather than producing a malformed
  circuit (e.g. a qual that conjoins a ``random_variable``
  comparator and an ``agg_token`` comparator in the same node).

For ``QUAL_PURE_RV`` quals, the rewriter mints a
``gate_cmp`` per comparator and conjoins its UUID into the
row's provenance via ``provenance_times``. The
comparator's float8-comparator OID is recovered via
``random_variable_cmp_oid``. The original
``OpExpr`` is dropped from the WHERE so the executor never reaches
the placeholder procedure.

For ``QUAL_PURE_AGG`` quals, the existing HAVING pathway
(:cfunc:`make_aggregation_expression`, dispatched on
``aggtype``) is reused with one extension: when the aggregate's
result type is ``OID_TYPE_RANDOM_VARIABLE``, the rewriter routes
through ``make_rv_aggregate_expression`` so the per-row
argument is wrapped in ``rv_aggregate_semimod`` (a
:sqlfunc:`mixture` over the row's provenance and the
identity for the aggregate, see *Aggregate Dispatch* below).

Two further rewrites piggyback on the same hook: an SQL ``CASE``
whose result branches are ``random_variable`` lowers to
``rv_case`` (a ``gate_case`` whose guards are the lifted
comparison events), and a call to the Boolean
:sqlfunc:`probability` overload whose argument carries a
probabilistic comparison is rewritten to
:sqlfunc:`probability_evaluate` over the comparison's event
token (a purely deterministic argument falls through to the SQL
body, ``predicate::integer::double precision``).

Conditioning two comparison events, ``(A) | (B)``, is handled by
the same ``rewrite_cond_predicate_mutator`` that lowers the
``carrier | (predicate)`` placeholders. An ``random_variable`` /
``agg_token`` comparison is statically ``boolean``-typed, so
``(A) | (B)`` is a ``boolean | boolean`` operator (backed by the
raising placeholder ``predicate_cond_predicate``, registered as
``OID_FUNCTION_PREDICATE_COND_PREDICATE``); unlike the
carrier-passthrough ``cond_predicate`` shape, *both* operands are
predicates. The mutator lowers each through
``predicate_to_condition_gate`` and emits
``cond(target_gate, evidence_gate)``, whose
:sqlfunc:`probability_evaluate` is the correlation-aware
:math:`\Pr(A \wedge B) / \Pr(B)`. Because the operator returns
``uuid``, ``(A) | (B)`` is a first-class event token in every
position -- a :sqlfunc:`probability` argument, a projected
column, or the left operand of a further ``|``.

A short-cut handles the corner case of ``WHERE rv > 2`` on a
FROM-less query (which touches no provenance-tracked relation):
there is nothing to conjoin into, so the rewriter runs only the
qual migration and splices a synthesised provenance column onto
the single result row, and the
result is a circuit that :sqlfunc:`probability_evaluate` reads
directly.

Monte Carlo Sampler
-------------------

The sampler implementation lives in
:cfile:`MonteCarloSampler.cpp`. The entry point
:cfunc:`monteCarloRV` runs ``N`` iterations over a
:cfunc:`GenericCircuit`; per iteration it draws every reachable
``gate_rv`` leaf once (memoised in ``scalar_cache_``) and
evaluates every reachable ``gate_input`` Bernoulli once
(memoised in ``bool_cache_``). The two per-iteration caches
ensure that shared leaves are correctly coupled within an
iteration. A third, cross-iteration cache (``dist_cache_``)
holds the per-gate ``Distribution`` object, constructed once via
``makeDistribution`` and reused for every draw, so the
blob-parsing and factory cost is paid once per gate rather than
once per sample.

The RNG is ``std::mt19937_64``, seeded from the
``provsql.monte_carlo_seed`` GUC: ``-1`` seeds from
``std::random_device``; any other value (including ``0``) is
used as a literal seed for reproducibility. The same RNG drives
the Bernoulli and continuous (gate_rv) sampling paths, so a
pinned seed reproduces both the discrete and continuous
components of a circuit's randomness.

``Sampler::evalScalar`` is the scalar dispatcher: it knows
how to sample ``gate_rv`` (via ``Distribution::sample``),
``gate_value`` (float8 mode parsed via
``parseDoubleStrict``), ``gate_arith`` (recursing on
children and combining per ``info1``, including ``MAX`` /
``MIN`` folds – shared base RVs stay coupled through the caches,
so ``max(x, y)`` with correlated ``x``, ``y`` is sampled
jointly), ``gate_mixture`` (sampling the Boolean selector once
via ``evalBool``, then recursing into the chosen branch), and
``gate_case`` (evaluating guards via ``evalBool`` and values via
``evalScalar`` in the same iteration; the first true guard wins,
else the default wire). The ``gate_agg`` arm calls back into the
aggregate evaluator with the per-iteration sampled values; this
is what unlocks HAVING+RV under Monte Carlo.

The transform opcodes carry *evaluation-time domain guards*: a
negative draw flowing into ``LN``, or a negative base drawn
together with a non-integer exponent under ``POW``, raises an
actionable error naming the ``greatest(x, 0)`` clamp – never a
silently dropped NaN, which would bias the estimate. Integer
exponents are total; ``ln(0)`` legitimately yields ``-∞``; NaN
operands (from an upstream guard-free source) propagate.

``Sampler::evalBool`` is the Boolean dispatcher: it walks the
Boolean wrappers (plus / times / monus / cmp / input / mulinput /
project / eq), and treats ``gate_delta`` as transparent: the
gate exists for the structural δ-semiring algebra but adds
no event to the rv_* event walker. The same transparency is
asserted in ``walkAndConjunctIntervals`` so the AND-conjunct
pass that backs RangeCheck behaves consistently.

RangeCheck
----------

:cfile:`RangeCheck.cpp` propagates support intervals
through ``gate_arith`` and tests every ``gate_cmp``
against the propagated interval. A comparator that is decidable
from the support alone (e.g. a Normal restricted to
``x > μ + 10σ``: the support of the LHS is
:math:`(-\infty, +\infty)` but every realisation is
overwhelmingly to the right of the RHS; or a bounded uniform
``x > b``: the support cap is ``b``, so the cmp is identically
false) collapses to a Bernoulli ``gate_input`` with
probability ``0`` or ``1``, transparent to every downstream
consumer.

Interval propagation covers all eleven arith opcodes: ``MAX`` /
``MIN`` take the corner max / min of the child intervals;
``EXP`` and ``LN`` map interval endpoints through the monotone
function (``LN`` clamping the interval at zero from below);
``POW`` over a non-negative base does corner analysis on the
``(base, exponent)`` box, and widens to all-real when the base
interval crosses zero. This keeps :sqlfunc:`support` readouts
and interval-decidable comparisons exact through the transform
surface. A ``gate_case``'s interval is the union of its value
branches (odd wires plus the default).

The AND-conjunction pass ``walkAndConjunctIntervals`` walks a
``WHERE`` clause's conjunction and intersects the per-RV intervals
across conjuncts before running the per-comparator decision.
``reading > 1 AND reading < 3`` thus constrains a single normal
once, with the analytic CDF call evaluating both endpoints in a
single pass.

``compute_support`` is exposed as ``rv_support`` for
SQL-side use (:sqlfunc:`support` polymorphically dispatches on
type and routes ``random_variable`` here).

AnalyticEvaluator
-----------------

:cfile:`AnalyticEvaluator.cpp` computes the exact probability of
a decidable ``gate_cmp``. :cfunc:`runAnalyticEvaluator` snapshots
every ``gate_cmp`` in the circuit, calls ``tryAnalyticDecide``
per comparator, and on a non-NaN result collapses the gate to a
Bernoulli ``gate_input`` with that probability.
``tryAnalyticDecide`` recognises three shapes:

- **RV vs constant** (either order): ``cdfDecide`` evaluates the
  family CDF at the constant – ``F(c)`` for ``<`` / ``<=``,
  ``1 − F(c)`` for the mirrored operators. Equality on a
  continuous distribution is RangeCheck's job (below).
- **categorical vs constant**: ``categoricalDecide`` sums
  :sqlfunc:`get_prob` over the mulinputs satisfying the
  predicate; being discrete, ``=`` / ``<>`` are decided here,
  exactly.
- **RV vs RV** (two bare leaves): ``rvVsRvDecide`` consults the
  comparator registry through ``comparatorPairLess``. Same-family
  closed forms are registered for Normal (via the difference
  normal), Uniform, Exponential, Lognormal, Weibull (same
  shape), and Pareto (any parameter pair – the comparison the
  heavy-tailed quadrature grid handles worst). On a registry
  miss – including every *mixed-family* pair – the generic
  fallback ``quadraturePairLess`` computes
  :math:`P(X < Y) = \int (1 - F_Y(t))\, f_X(t)\, dt` by
  composite Simpson quadrature (4000 panels) over X's
  ``integrationRange``. A NaN anywhere (no finite integration
  window, a declined pdf/cdf) leaves the comparator to the Monte
  Carlo net.

Equality and inequality on continuous distributions collapse in
``RangeCheck``: ``X = X`` is identically true (a zero-width
interval identity), ``X = Y`` for any two sub-circuits of which
at least one has purely continuous support is identically false.
``hasOnlyContinuousSupport`` (in :cfile:`RangeCheck.cpp`) is the
predicate behind the second case: a recursive walk that returns
true on ``gate_rv`` leaves, on ``gate_arith`` whose every wire is
continuous, and on Bernoulli mixtures whose two branches are both
continuous; false on ``gate_value`` (Dirac), on categorical
mixtures (point masses at each outcome), and on Boolean / agg
gates. The widened test catches heterogeneous-rate exponential
sums (``Exp(λ_1) + Exp(λ_2)`` with ``λ_1 ≠ λ_2``, no Erlang
closure), products of two independent continuous RVs, and mixed
``gate_arith`` composites that the simplifier cannot fold to a
single ``gate_rv`` -- their equality predicate would otherwise
have flowed all the way down to the MC marginalisation only to
return 0 in finite precision anyway.

When neither side is purely continuous, a second analytical path
in ``RangeCheck`` fires: ``collectDiracMassMap`` extracts the
``(value → mass)`` map from each side (recursing into
categoricals and Bernoulli mixtures of ``as_random`` /
``gate_value`` branches), and the cmp resolves exactly via the
independent-Dirac sum-product

  ``P(X = Y) = Σ_{v ∈ M_X ∩ M_Y} M_X(v) · M_Y(v)``.

Continuous components on either side contribute zero by
measure-zero arguments (continuous vs Dirac and continuous vs
continuous), so they need not appear in the sum. Independence is
required for the factoring to hold; ``collectRandomLeaves`` walks
both sides for the union of ``gate_rv`` + ``gate_input`` leaves
and the shortcut bails on any overlap (e.g. two mixtures sharing
a Bernoulli ``p_token``). Bernoulli mixtures whose ``p_token`` is
a compound Boolean (whose static probability would require a
recursive :sqlfunc:`probability_evaluate` call) also bail. The
sum-product subsumes the disjoint-Dirac case as its boundary
(empty intersection ⇒ ``P(X = Y) = 0``).

Analytical Moment Evaluator
---------------------------

:cfile:`Expectation.cpp` implements the closed-form moment
evaluator for continuous-RV circuits. It is *not* a
``Semiring`` subclass: the
:cfunc:`provenance_evaluate_compiled_internal` dispatcher
special-cases ``semiring == "expectation"`` and calls
:cfunc:`compute_expectation` directly on the
``GenericCircuit``, bypassing the template-based
``GenericCircuit::evaluate<S>`` machinery used by the proper
semirings. The same entry point is reached by
:sqlfunc:`expected` over a ``random_variable`` and by the
``rv_moment`` C helper.

The algorithm runs analytical moment computation per distribution
at leaves (``Distribution::mean`` / ``variance`` /
``rawMoment``), then propagates through ``gate_arith`` by
closed-form rules:

- ``E[X + Y] = E[X] + E[Y]`` (always),
- ``E[X − Y] = E[X] − E[Y]`` (always),
- ``E[a · X] = a · E[X]`` (when one operand is a constant),
- ``E[X · Y] = E[X] · E[Y]`` (only when ``X`` and ``Y`` are
  structurally independent),
- ``Var[X + Y] = Var[X] + Var[Y]`` (independent),
- etc.

Divergent moments are reported honestly: a family whose k-th
moment does not exist (a Pareto with ``α ≤ k``) returns
``Infinity``, never an estimate.

The ``MAX`` / ``MIN`` order statistics get their own mean rules:
when the children are independent bare RVs of the same family
and parameters, ``Distribution::iidOrderStatMean`` supplies the
closed form (Uniform, Exponential, Weibull, Pareto implement
it); otherwise ``mixedOrderStatMean`` integrates the layer-cake
identities :math:`E[\max] = lo + \int (1 - \prod_i F_i)` /
:math:`E[\min] = lo + \int \prod_i (1 - F_i)` by composite
Simpson quadrature over a window covering every child's support
– still exact-grade for independent bare-RV children of *any*
family mix. Shared leaves, non-leaf children, or an undefined
CDF fall through to MC, as do order-statistic variances and
higher moments.

Two read-only registry consultations extend the closed-form
reach without rewriting the circuit (safe under shared-RV
identity): ``transform_image`` maps ``LN`` / ``EXP`` gates over
a single bare RV through the transform registry, and
``product_image`` maps a TIMES over independent same-family
leaves through the product registry; both the moment and the
quantile evaluators then read the image distribution's closed
forms directly.

Structural independence is detected via a per-evaluation
``FootprintCache`` that memoises, per gate, the set of base
``gate_rv`` leaves reachable from it (a ``gate_case``
contributes all its wires, like a ``gate_arith``). Two gates
whose footprints are disjoint are independent; the cache speeds
up the check from quadratic to linear by sharing the leaf-set
computation across the recursion.

When no closed form applies, :cfunc:`compute_expectation` falls
back to a Monte-Carlo estimate using ``MonteCarloSampler``. The
sample count is ``provsql.rv_mc_samples``; setting it to ``0``
turns the fallback into an exception so callers that need
analytical answers can detect the silent fallback.

Quantiles
~~~~~~~~~

The polymorphic :sqlfunc:`quantile` dispatcher routes a
``random_variable`` to ``rv_quantile``, whose C implementation
``compute_quantile`` (:cfile:`Expectation.cpp`) dispatches on
the root shape: a ``gate_value`` returns its constant; a bare
``gate_rv`` goes to ``analytic_dist_quantile``; a categorical
mixture takes the generalised inverse
:math:`F^{-1}(p) = \min\{v : F(v) \ge p\}` over its enumerated
outcomes; an ``LN`` / ``EXP`` / foldable ``TIMES`` root reads
its registry image (see above); anything else – and any
conditioned shape outside the truncated closed form – draws
``provsql.rv_mc_samples`` samples and interpolates the empirical
quantile with the same type-7 linear interpolation PostgreSQL's
``percentile_cont`` uses.

``analytic_dist_quantile`` handles the edges and truncation
uniformly: ``p ≤ 0`` / ``p ≥ 1`` return the (possibly truncated)
support edges; under interval conditioning the probability is
rescaled to :math:`u = F(lo) + p\,(F(hi) - F(lo))` before
inverting (a conditioning mass below ``1e-12`` falls to MC); the
inversion itself tries ``Distribution::quantile`` first and
``numericQuantile`` bisection second.

Covariance, correlation, standard deviation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:sqlfunc:`covariance` ``(x, y [, prov])`` and
:sqlfunc:`correlation` are C readouts
(:cfile:`RvCovariance.cpp`) over a joint circuit loaded through
the multi-root ``getJointCircuit`` (the three roots ``x``, ``y``,
``prov`` share one ``gate_t`` per common leaf).
:sqlfunc:`stddev` stays pure SQL, ``sqrt(variance(x))``.
Exact tiers first: two identical roots route to the variance
evaluator; structurally independent arguments (disjoint
stochastic-leaf footprints, given the event) give an exact ``0``;
and when ``E[XY]``, ``E[X]``, ``E[Y]`` (plus both variances, for
``correlation``) all decompose analytically -- probed with the MC
fallback temporarily disabled -- the closed-form
``E[XY] − E[X]·E[Y]`` subtraction is returned. Otherwise a
**single coupled Monte-Carlo pass** draws ``(x, y)`` pairs (a
leaf shared between the roots and/or the event produces one draw
per iteration that all observe; conditioning is
rejection-sampled through ``monteCarloConditionalScalarPairSamples``)
and the sample covariance :math:`\tfrac1n \sum (x_i - \bar
x)(y_i - \bar y)` is returned, with ``correlation`` reading
:math:`\sigma_X, \sigma_Y` off the same pass. The naive
three-run ``expected(x·y) − expected(x)·expected(y)``
subtraction is deliberately avoided on the MC path: its noise
scales with :math:`E[X]\,E[Y]` (a catastrophic cancellation when
the means dominate the coupling), whereas the paired estimator's
scales with :math:`\sqrt{(\sigma_X^2\sigma_Y^2 + \mathrm{Cov}^2)/n}`.
``correlation`` returns ``NULL`` on a degenerate (zero-variance)
argument in every tier.

Collapsed aggregate moments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:cfile:`CollapsedAggMoment.cpp` is the Rao-Blackwellised fast
path for the recurring latent-variable relational shape: an
aggregate over probabilistically-selected rows whose per-row
selection events are coupled through **one** shared continuous
latent. Conditional on that latent the row indicators are
independent, so the aggregate's moments collapse to a **1-D
quadrature over the shared latent** instead of an :math:`n^k`
tuple enumeration or a degenerating importance sampler. It is
**exact** up to the quadrature grid and works at
``provsql.rv_mc_samples = 0``; complexity :math:`O(n^2 + G\,K)`
(``G`` grid points, ``K`` per-point cost). This is distinct from
the ``foldDegenerateMixtures`` Bernoulli-arm collapse (see
*HybridEvaluator*): that pass folds a certain (:math:`\pi \in
\{0, 1\}`) Bernoulli selector to its surviving arm, whereas this
one marginalises a *fractional*, shared continuous latent
analytically.

Two entry points, both consulted by :cfile:`Expectation.cpp`
before the generic MC fallback and exposed as SQL:

- :cfunc:`aggCollapsedRawMoment` (SQL
  :sqlfunc:`agg_collapsed_moment` ``(token, k)`` and the paired
  :sqlfunc:`agg_collapsed_moments` ``(token)`` that returns both
  ``{E[C], E[C²]}`` from a single circuit load) computes the
  raw moment :math:`E[C^k]` (``k`` in ``{1, 2}``) of a correlated
  COUNT / SUM by a Poisson-binomial 1-D quadrature over the shared
  latent, with the closed-form per-row CDF given it. It returns
  ``std::nullopt`` / SQL ``NULL`` when the circuit does not match
  the shared-latent shape, so the caller falls back to the exact
  :math:`n^k` enumeration. :sqlfunc:`variance` uses the paired
  form so a mean-plus-variance readout traverses the circuit once.
- :cfunc:`collapsedConditionalMoment` computes the **exact
  posterior** raw moment :math:`E[R^k \mid Y = C]` of a latent
  ``R`` conditioned (through an equality ``event``) on a discrete
  rv ``Y`` – parametrised by ``R`` – equalling a correlated COUNT
  ``C``. The ``event`` must be a ``gate_cmp`` with ``=`` whose
  operands are a parametric discrete ``gate_rv`` over ``R`` and a
  ``gate_agg`` count; the count's pmf :math:`P(C = j)` comes from
  the collapse, and the posterior is the second 1-D quadrature
  :math:`E[R^k \mid C] = \int r^k f_R(r)\,L(r)\,dr /
  \int f_R(r)\,L(r)\,dr` with likelihood
  :math:`L(r) = \sum_j P(C = j)\,\mathrm{pmf}_Y(j; \theta(r))`.
  A shape mismatch returns ``nullopt`` and the caller falls back
  to importance sampling.

HybridEvaluator
---------------

:cfile:`HybridEvaluator.cpp` is the orchestrator. Given a
circuit, it runs:

1. **Universal peephole pass** (:cfunc:`runHybridSimplifier`)
   that folds family-preserving combinations into a single leaf.
   The fold rules are registry-driven: ``try_sum_closure``
   collects a PLUS gate's wires as ``a·Z + b`` terms and consults
   the sum-closure registry (any linear combination of
   independent normals and constants into a single normal; a
   same-rate Exponential / Erlang chain into ``erlang(Σk, λ)``;
   same-rate Gammas into a Gamma); ``try_product_closure``
   consults the product registry (independent lognormals fold in
   log space); ``try_transform_closure`` consults the transform
   registry (``exp(Normal) → Lognormal``, ``ln(Lognormal) →
   Normal`` – so a chain like ``exp(N_1 + N_2)`` collapses to
   one lognormal leaf). Scalar shifts, scalings, and negations
   of a single RV fold through ``Distribution::affine``; a
   family whose affine image leaves the family declines the fold
   and the shape stays a ``gate_arith`` (shifting or negating an
   exponential, for instance, flips or displaces its support).
   Around the registry rules sit the structural
   canonicalisations: MINUS-to-PLUS (so subtraction shapes flow
   through the same PLUS pipeline), DIV-to-TIMES for division by
   a constant, shift-and-scale pushed through mixtures and
   categoricals, single-child arith roots, semiring-identity
   drops (``gate_one`` in TIMES, ``gate_zero`` in PLUS…), and
   constant folding of deterministic subtrees – including
   ``POW`` / ``LN`` / ``EXP`` / ``MAX`` / ``MIN`` over
   constants, with domain-violating constants deliberately left
   unfolded so the sampler's guard fires. The pass is
   invariant-preserving: every transformation produces a
   semantically equivalent circuit. Out of scope: combinations
   with no registered rule – the sum of two *distinct* uniforms
   (triangular, not uniform), differing-rate exponential sums,
   min / max of RVs (only their means have closed forms, see
   above); these shapes stay as ``gate_arith`` and the MC
   sampler handles them per-iteration.

   *Peephole* is borrowed from compiler engineering (McKeeman,
   CACM 8(7), 1965): a small sliding window over consecutive
   instructions / gates, a fixed list of local pattern ->
   replacement rules, iterated to a fixed point. Each rule here
   looks at one ``gate_arith`` plus its immediate children,
   never further, matching the original scope. Contrast with
   ``RangeCheck`` (:cfile:`RangeCheck.cpp`), which propagates a
   data-flow fact (the support interval) through the whole
   circuit, and with the island decomposer below, which uses a
   global union-find over base-RV footprints.

2. **Island decomposition** (:cfunc:`runHybridDecomposer`)
   that splits a multi-cmp query into independent islands
   (connected components on a union-find over base-RV
   footprints). A single-cmp island marginalises to a Bernoulli
   ``gate_input`` via ``AnalyticEvaluator``. A multi-cmp
   island whose cmps share base RVs is enumerated via the joint
   table (the joint distribution of the shared base RVs is
   evaluated explicitly).

3. **Monotone-shared-scalar fast path** for the common shape of
   a single ``gate_rv`` shared across multiple monotone
   comparators (typical of range queries on a single column):
   the joint event reduces to an interval on the underlying
   scalar and one analytical CDF call per endpoint.

   This fast path is **analytical** when the shared scalar is a
   bare ``gate_rv`` with a closed-form CDF, so the decomposer
   deliberately does **not** short-circuit on
   ``provsql.rv_mc_samples = 0``: skipping it would leave the
   correlated cmps for ``AnalyticEvaluator`` to collapse one at a
   time, silently returning the *product of the marginals* for
   events that share a leaf (e.g. ``Pr(x ≥ 2000 ∧ x ≥ 1000)``
   coming back as ``Pr(x ≥ 2000)·Pr(x ≥ 1000)`` rather than
   ``Pr(x ≥ 2000)``). Only the genuinely MC-bound arms -- a
   composite (non-``gate_rv``) shared scalar, an RV-vs-RV joint
   table, a non-analytic singleton -- are gated on
   ``rv_mc_samples > 0``; under ``0`` a correlated island with no
   closed form **raises** (a ``CircuitException`` surfaced as a
   clean error) rather than falling back to the independent
   product. This is what makes ``probability((A) | (B))`` over
   two shared-leaf comparisons exact even with the MC fallback
   disabled.

4. **Universal semiring-identity collapse** after RangeCheck
   has decided every decidable cmp.

The simplifier is gated by ``provsql.simplify_on_load`` for the
universal pass run at load time, and by the debug-only
``provsql.hybrid_evaluation`` (``GUC_NO_SHOW_ALL``) for the
in-evaluator hybrid path. End users have no reason to flip
``hybrid_evaluation``; it exists for developer A/B against the
unfolded path and as a bisection knob.

Conditional Evaluation
----------------------

:sqlfunc:`expected`, :sqlfunc:`variance`, :sqlfunc:`moment`,
:sqlfunc:`central_moment`, :sqlfunc:`quantile`,
:sqlfunc:`support`, :sqlfunc:`rv_sample`, and
:sqlfunc:`rv_histogram` all accept an optional ``prov uuid
DEFAULT gate_one()`` argument. When ``prov`` resolves to anything
other than ``gate_one()``, evaluation routes through the
joint-circuit loader.

``getJointCircuit`` (:cfile:`MMappedCircuit.cpp`)
builds a multi-rooted BFS over the union of the reachable gates
from both ``input`` and ``prov`` so shared ``gate_rv``
leaves between the two are loaded into a single
:cfunc:`GenericCircuit` and consequently couple correctly in the
Monte Carlo sampler's per-iteration caches. This is the
*shared-atom coupling invariant*: a conditioning event ``prov``
is only meaningful relative to the random variables it
references, and those must be the same leaves the moment's
evaluator sees.

The closed-form table for truncated (interval-conditioned)
moments is a per-family capability, not a hard-coded list: a
family implementing ``Distribution::truncatedRawMoment`` gets
exact conditional moments (Normal via the Mills-ratio formula
and integration by parts, Uniform trivially on the intersected
interval, Exponential by memorylessness plus the lower
incomplete gamma, Lognormal, Weibull, Pareto – the latter via
tail self-similarity – and Beta via the incomplete-beta ratio).
Families that decline (Erlang, Gamma) fall through to the MC
path below. Extending the truncated surface to a new family
means implementing ``truncatedRawMoment`` (and, for exact
conditioned sampling, ``sampleTruncated``) in that family's
file – no detection or dispatch site changes.

For shapes outside the closed-form table, the conditional moment
is estimated by rejection sampling at ``provsql.rv_mc_samples``;
:sqlfunc:`rv_sample` emits a ``NOTICE`` (and
:sqlfunc:`rv_histogram` / :sqlfunc:`expected` raise) when the
acceptance rate drops below the requested ``n`` within the
budget, so the caller can either widen the budget or loosen the
conditioning.

``matchTruncatedSingleRv`` (in :cfile:`RangeCheck.cpp`) is the
single-RV shape-detection helper used by the moment surface
(``try_truncated_closed_form`` in :cfile:`Expectation.cpp`), the
quantile evaluator, and the rejection-free sampler
(:cfunc:`try_truncated_closed_form_sample`). It
runs the four common gates -- ``gate_rv`` root check,
``parse_distribution_spec``, ``collectRvConstraints``, and the
empty-intersection / ``gate_zero`` event guards -- so the
supported-shape set stays in sync between moments, quantiles,
and sampling.

``matchClosedFormDistribution`` (same file) generalises the
single-RV matcher to the four-arm variant
``std::variant<TruncatedSingleRv, DiracShape, CategoricalShape,
BernoulliMixtureShape>`` consumed by
:sqlfunc:`rv_analytical_curves`. The variant covers, in addition
to the bare-RV case, ``as_random(c)`` Diracs (``gate_value``
roots), categorical-form ``gate_mixture`` roots, and classic
Bernoulli ``gate_mixture`` roots over any recursively-matched
shape. A bare-RV root under conjugate observe-evidence resolves
first through ``conjugatePosterior`` (the posterior is itself a
bare distribution, returned as an untruncated single-RV shape --
this is what makes :sqlfunc:`rv_histogram` and the curve
renderers exact for recognised posteriors).
Conditioning is honoured uniformly across all four arms:
non-trivial events are routed through ``collectRvConstraints``
to extract a ``[lo, hi]`` interval on the root variable, then
``truncateShape`` is applied recursively -- bare RVs intersect
their bounds and renormalise via the truncated CDF; Diracs are
kept iff the value falls inside the interval (otherwise the
event is infeasible); categoricals drop outcomes outside the
interval and renormalise surviving masses; Bernoulli mixtures
recursively truncate their arms and reweight the Bernoulli by
the ratio of arm masses
(:math:`\pi' = \pi Z_L / (\pi Z_L + (1-\pi) Z_R)`), with the
arm masses computed by ``shape_mass`` (a parallel recursive
pass that integrates the unconditional CDF over the interval).
An arm with zero post-truncation mass is eliminated and the
mixture degenerates to the surviving arm.

``eventIsProvablyInfeasible`` (also :cfile:`RangeCheck.cpp`) is
the conditional-moment dispatcher's pre-MC short-circuit:
``gate_zero`` events are detected for every root type, and
``gate_rv`` roots additionally surface
``collectRvConstraints``-empty intersections. The dispatcher in
``conditional_raw_moment`` / ``conditional_central_moment``
(:cfile:`Expectation.cpp`) calls it after
``try_truncated_closed_form`` returns ``nullopt`` and raises a
"conditioning event is infeasible" error directly when the
predicate fires, avoiding a full ``rv_mc_samples`` MC round whose
acceptance probability is exactly zero.

:sqlfunc:`rv_sample` and :sqlfunc:`rv_histogram` share
``MonteCarloSampler::try_truncated_closed_form_sample``: a
direct exact-sampling fast path that fires on a bare ``gate_rv``
with an interval-extractable event whenever the family
implements ``Distribution::sampleTruncated`` (Uniform draws
``U(lo, hi)`` on the intersected truncation; Exponential
one-sided uses memorylessness (``X | X > c = c + Exp(λ)``),
two-sided uses inverse-CDF via ``std::log1p`` / ``std::expm1``
for numerical accuracy near the support boundary; Normal uses
the inverse-CDF transform; Lognormal, Weibull, and Pareto invert
their exact quantiles). The fast path delivers exactly ``n``
samples with 100% acceptance even for tight tail events that
would otherwise starve the rejection budget. Families without
``sampleTruncated`` (Erlang, Gamma, Beta, Inverse-Gamma,
Inverse-Gaussian) and ``gate_arith``
composite roots fall through to MC rejection unchanged.

:sqlfunc:`rv_analytical_curves` (in :cfile:`RvAnalyticalCurves.cpp`)
exposes the closed-form PDF, CDF, and discrete stems as sampled
data for ProvSQL Studio's Distribution profile overlay. Returns
NULL when the root sub-circuit is not a closed-form shape, so
callers can dispatch it in parallel with :sqlfunc:`rv_histogram`
without a structural pre-check. The payload has three optional
fields:

* ``pdf`` -- evenly-spaced ``{x, p}`` samples of the continuous
  density. Absent when the shape has no continuous component
  (pure Dirac, pure categorical, or nested mixture of those).
* ``cdf`` -- same grid as ``pdf``, cumulative probability.
  Always emitted (well-defined for any supported shape; a pure-
  discrete shape produces a staircase, a continuous shape a
  smooth curve, and a mixed shape a smooth curve with jumps at
  the stem positions).
* ``stems`` -- ``{x, p}`` point masses produced by Dirac roots,
  categorical roots, or Dirac / categorical arms inside a
  Bernoulli mixture. Bernoulli weights propagate down the path
  (a Dirac inside ``mixture(0.3, X, c)`` appears at ``(c, 0.7)``).

The supported shape set is the union of
``matchClosedFormDistribution`` 's variant arms (see above):
a bare ``gate_rv`` of *any* registered family – the plot window,
density, and distribution come from ``Distribution::plotRange``
/ ``pdf`` / ``cdf``, so a new family gets curves for free; a
family that declines pdf/cdf on the grid (NaN) returns NULL –
plus ``as_random(c)`` Diracs, categorical mixtures, and
Bernoulli mixtures over any recursively-matched shape, all four
arms accepting a non-trivial conditioning event. ``gate_arith``
composites return NULL; the frontend renders histogram-only in
those cases.

Before matching, :sqlfunc:`rv_analytical_curves` runs
:cfunc:`runHybridSimplifier` so the curves see the same folded tree
that :sqlfunc:`simplified_circuit_subgraph` exposes to Studio's circuit
view: ``c·Exp(λ)`` folded to ``Exp(λ/c)``, sums of independent
normals folded to a single normal, ``exp(Normal)`` folded to its
lognormal image, ``c + mixture(p, X, Y)``
pushed inside the mixture, etc. Without this pass a circuit
that displays as a single ``Exp(0.5)`` node would still be
seen by the matcher as a ``gate_arith`` composite of
``value(2)`` and ``gate_rv:Exp(1)`` and would silently fall
back to histogram-only.

Truncation under a bare RV normalises the PDF by
:math:`Z = \text{CDF}(\text{hi}) - \text{CDF}(\text{lo})` and
rescales the CDF to ``[0, 1]`` over the conditioning interval.
Under a Bernoulli mixture the truncated PDF is
:math:`f_{M|A}(x) = (\pi \cdot Z_L \cdot f_{L|A}(x) +
(1-\pi) \cdot Z_R \cdot f_{R|A}(x)) / (\pi Z_L + (1-\pi) Z_R)`
with the per-arm normalisers
:math:`Z_L, Z_R` computed by ``shape_mass``. Under a categorical
the conditional masses are :math:`p_i \cdot
\mathbb{1}\{v_i \in A\} / \sum_j p_j \cdot \mathbb{1}\{v_j \in A\}`.
A Dirac is invariant under any feasible event.

The load-time pass ``runConstantFold`` (in ``HybridEvaluator.cpp``,
invoked from :cfunc:`CircuitFromMMap::applyLoadTimeSimplification`
alongside ``runRangeCheck`` and ``foldSemiringIdentities``)
folds deterministic ``gate_arith`` subtrees to ``gate_value``
at load time. This lifts the common parser shape
``arith(NEG, value:c)`` (produced when SQL parses
``-c::random_variable`` as ``-(c::random_variable)``) into a
clean ``value:-c``, so ``asRvVsConstCmp`` and friends recognise
the comparator's constant side without callers having to
parenthesise. The pass runs only the constant-fold rule from the
hybrid simplifier, never the family closures or identity drops,
because the result is always a ``gate_value`` that carries no
random identity, so no shared-RV coupling is decoupled by the
rewrite. The family closures stay behind the separate
``provsql.hybrid_evaluation`` GUC, which gates :cfunc:`runHybridSimplifier`
inside the probability and view paths where the simplifier owns
the rewritten subtree.

Immediately before ``runConstantFold`` in the same load-time
sequence, ``foldDegenerateMixtures`` collapses a classic Bernoulli
``mixture(p, X, Y)`` whose selector ``p`` is *certainly* true or
false to the surviving arm: ``X`` when :math:`\Pr(p) = 1`, ``Y``
when :math:`\Pr(p) = 0`. The weight is read only where it is known
without a probability computation – a resolved ``gate_one`` /
``gate_zero`` selector, or a bare ``gate_input`` whose pinned
probability is exactly ``1`` or ``0`` (the default ``1`` of a
non-probabilistic tuple included). The collapse rewrites the
mixture as a single-wire ``gate_arith`` ``PLUS`` passthrough to
the survivor (``liftConditionedToTarget``), which *references*
rather than copies it, so a shared survivor keeps its single gate
identity and single Monte-Carlo draw; ``foldSemiringIdentities``
then unwraps the passthrough. Ordering it before the constant fold
is what lets a provenance-weighted count of certain tuples (the
denominator of the ``avg`` rewrite, see *Aggregate Dispatch*)
reduce to a ``gate_value``. The fold is exact and
correlation-safe precisely at :math:`\pi \in \{0, 1\}`: a
deterministic selector shared across several mixtures couples
none of them, whereas a fractional selector genuinely does, so
the pass admits only the two endpoints and leaves every
fractional Bernoulli for the Monte-Carlo sampler. A compound
Boolean selector – whose :math:`\Pr(p)` would need a (possibly
#P-hard) evaluation and depends on mutable input probabilities –
is left intact for the probability-aware evaluators.

Information Theory
------------------

:cfile:`InformationTheory.cpp` implements the entropy /
Kullback-Leibler / mutual-information readouts, all in **nats**,
over scalar RV sub-circuits. The exact paths resolve a gate to a
closed **density view** – a bare ``gate_rv`` (its family ``pdf``
over the integration range), a ``gate_value`` / categorical
mixture (a finite pmf), or a Bernoulli mixture tree over
independent such arms (e.g. the :sqlfunc:`gmm` cascade) – and
evaluate the defining integral or sum directly. Entropy of a
discrete view is Shannon entropy (a point mass has entropy ``0``);
of a continuous view, differential entropy (quadrature of
:math:`-f \ln f` over the family's integration range). The three
C entry points are :cfunc:`computeEntropy`, :cfunc:`computeKL`,
:cfunc:`computeMutualInformation`, bound to the SQL
:sqlfunc:`entropy` ``(x [, prov])``, :sqlfunc:`kl` ``(p, q)``, and
:sqlfunc:`mutual_information` ``(x, y)``.

- **Entropy.** Shapes with no closed density (arithmetic
  composites) and the conditional form (``prov`` other than
  ``gate_one()``) fall back to a Monte Carlo histogram plug-in
  estimate at the ``provsql.rv_mc_samples`` budget.
- **KL divergence.** Exact only: the defining sum for two discrete
  views (outcomes matched by value) and the defining integral
  (quadrature over ``P``'s integration window) for two continuous
  ones, including independent-arm mixture trees. Returns
  ``Infinity`` when ``P`` is not absolutely continuous with
  respect to ``Q`` (mismatched kinds, an atom or region of ``P``
  outside ``Q``'s support). KL has no density-free estimator, so
  an arithmetic composite or conditioned argument **raises**
  rather than falling back to Monte Carlo.
- **Mutual information.** Exactly ``0`` for structurally
  independent roots (disjoint stochastic-leaf footprints, the same
  ``FootprintCache`` test the moment evaluators use); ``H(X)`` for
  a discrete variable paired with itself and ``Infinity`` for a
  continuous one (``I(X; X)`` diverges); a genuinely correlated
  pair (shared leaves) is the 2-D histogram plug-in over coupled
  joint draws – both roots evaluated against the same
  per-iteration cache so shared leaves keep their joint law – at
  the ``provsql.rv_mc_samples`` budget.

Aggregate Dispatch
------------------

The :sqlfunc:`sum`, :sqlfunc:`avg`, :sqlfunc:`product`,
:sqlfunc:`max`, and :sqlfunc:`min` aggregates over
``random_variable`` all share ``sum_rv_sfunc`` as their
state-transition function (a ``uuid[]`` accumulator) and an
``INITCOND = '{}'`` so the FFUNC runs even on an empty group.

The row-absence identity is baked into each per-row contribution
*upstream*, by the planner hook: ``make_rv_aggregate_expression``
wraps the per-row argument in ``rv_aggregate_semimod`` – a
:sqlfunc:`mixture` ``(prov_i, X_i, as_random(identity))`` – with
the identity dispatched per aggregate: ``0`` for :sqlfunc:`sum`
(the two-argument form), and, through the three-argument
identity-parameterised form, ``1`` for :sqlfunc:`product`,
``-Infinity`` for :sqlfunc:`max`, ``+Infinity`` for
:sqlfunc:`min` – a row absent in a world must not perturb the
fold, so it contributes the fold's identity. :sqlfunc:`avg` is
rewritten at the same site into the "AVG = SUM / COUNT"
identity, ``rv_sum_or_null(rv_aggregate_semimod(prov, x)) /
sum(rv_aggregate_indicator(prov))``: the numerator is the usual
provenance-weighted sum (``rv_sum_or_null`` differs from
:sqlfunc:`sum` only in returning ``NULL`` on an empty group so
the division propagates standard ``AVG`` semantics), and the
denominator sums per-row ``mixture(prov_i, 1, 0)`` indicators –
the count of *included* rows as a random variable.

That denominator is a genuine random variable only when some row
is *uncertainly* present. When every contributing tuple is
certain – the default for a provenance-tracked but
non-probabilistic table, where each input gate carries the
default probability 1 – the count is deterministic, and
``expected(avg(x))`` is simply ``E[SUM] / n``. The load-time
``foldDegenerateMixtures`` pass (see *HybridEvaluator*) turns
this into an analytic result: a Bernoulli
``mixture`` whose selector is certainly true (:math:`\pi = 1`) or
false (:math:`\pi = 0`) is collapsed to the surviving arm, so
each denominator ``mixture(prov_i, 1, 0)`` becomes the constant
``1`` and constant folding reduces the whole sum to the
``gate_value`` :math:`n` that the analytic DIV-by-constant arm
divides by (each numerator ``mixture(prov_i, X_i, 0)`` likewise
unwraps to ``X_i``, leaving a plain sum of the row RVs). A
fractional presence probability genuinely couples the numerator's
random sum to the random count, so its mixtures are left intact
and the ratio is estimated by Monte Carlo – the fold admits only
the exact :math:`\pi \in \{0, 1\}` cases, where a deterministic
selector carries no coupling to lose.

With the identities pre-baked, the FFUNCs are plain folds with
no gate inspection: a single ``gate_arith`` root (``PLUS`` /
``TIMES`` / ``MAX`` / ``MIN``, the extremum pair through the
shared ``extremum_rv_ffunc``) over the accumulated UUIDs, a
singleton group collapsing to its single child, and per-aggregate
empty-group identities (``as_random(0)`` for :sqlfunc:`sum`,
``as_random(1)`` for :sqlfunc:`product`, ``as_random(∓∞)`` for
the extrema, SQL ``NULL`` for :sqlfunc:`avg`). On an
*untracked* call (no provenance to weight by) the direct
aggregates still work: every row is unconditionally present and
the raw per-row RVs are folded as-is.

The dispatch is keyed on ``aggtype`` (the aggregate's result
type OID) rather than ``aggfnoid`` so the same routing works for
any future RV-returning aggregate.

An earlier design considered an *M-polymorphic* ``gate_agg``
that would carry the full semimodule lift directly. We rejected
it because the mixture-of-mixtures shape composes through every
existing gate_arith / gate_mixture rule, while a new
M-polymorphic gate would have required a parallel evaluation
path in every analytical evaluator. The semimodule-of-mixtures
shape reuses what's there.

Statistic aggregates
~~~~~~~~~~~~~~~~~~~~

The SQL-standard statistic aggregates (``covar_pop`` /
``covar_samp`` / ``corr`` two-argument, ``stddev_pop`` /
``stddev_samp`` one-argument, and the ordered-set
``percentile_cont``) use a different wrap: instead of baking a
fold identity into each row's mixture, they carry the row's
*presence indicator* explicitly. The public aggregates append
the certain indicator ``as_random(1)`` per row; a
provenance-tracked query is rewritten by
``make_rv_aggregate_expression`` to the internal ``rv_*_impl``
aggregates, whose extra leading argument is
``rv_aggregate_indicator(prov)`` (the ``mixture(prov, 1, 0)``
indicator ``avg`` already uses), so absent rows drop out of
every sum, the count, and the percentile member set.

The moment statistics are then pure circuit *arithmetic* over
indicator-weighted power sums – ``rv_stat_sum_tokens`` mints
:math:`N = \sum \mathbf{1}_i`, :math:`S_X`, :math:`S_{XX}` (and
:math:`S_Y, S_{XY}, S_{YY}` for the two-argument forms) as
``gate_arith`` ``PLUS``-of-``TIMES`` trees, sharing each row's
indicator gate between :math:`N` and every product it weighs so
the Monte Carlo per-iteration cache keeps presence coupled
across the sums (and a repeated wire ``[ind, x, x]`` reuses the
same draw of ``x``, giving :math:`x^2`). No new opcode is
needed: e.g. ``covar_pop`` is ``MINUS(DIV(SXY, N),
TIMES(DIV(SX, N), DIV(SY, N)))``, and the stddevs clamp the
variance with ``MAX(v, 0)`` before ``POW(·, 0.5)`` so
floating-point error can never trip the ``pow`` domain guard.
Undefined worlds (:math:`N = 0`; :math:`N = 1` for the sample
forms; zero variance for ``corr``) evaluate to ``NaN``, the
established convention the moment estimators skip.

``percentile_cont`` is the one statistic arithmetic cannot
express: it mints the appended ``PROVSQL_ARITH_PERCENTILE``
(``= 10``) ``gate_arith`` whose wires are the interleaved
``[ind_1, x_1, ..., ind_n, x_n]`` pairs and whose fraction is
text-encoded in ``extra`` (and participates in the token UUID,
so two fractions over the same group are distinct gates). The
Monte Carlo sampler arm collects the values whose indicator
draws 1, sorts, and linearly interpolates; ``RangeCheck``
propagates the hull of the value wires' supports; the moment
evaluators route it straight to the sampler (no closed form).
Because the planner rewrite replaces the ordered-set ``Aggref``
with the *normal* three-argument ``rv_percentile_impl`` before
planning, the executor never sorts ``random_variable`` rows –
the sort order is irrelevant to the gate – which is also why
the untracked public form is unusable: its input sort funnels
into ``random_variable_btree_cmp`` and raises. The fraction
travels as the leading impl argument into a composite
transition state (``rv_percentile_state``), since a normal
aggregate's final function sees only the state.

Studio Rendering
----------------

ProvSQL Studio's circuit canvas is class-based: every node
carries a ``node--<type>`` CSS class derived from the gate kind,
and the stylesheet at ``studio/provsql_studio/static/app.css``
gives each class its colour, glyph, and inline-text layout.
The continuous gate types map to:

- one entry per type in the server-side JSON serialiser at
  ``studio/provsql_studio/circuit.py`` (the ``label`` and
  ``children`` fields, plus the distribution-blob parsing for
  the per-leaf inline glyph);
- one branch in the client renderer at
  ``studio/provsql_studio/static/circuit.js`` to pick the
  ``node--rv`` / ``node--arith`` / ``node--mixture`` /
  ``node--case`` class and to emit the correct edge labels
  (``p`` / ``x`` / ``y`` for mixtures; the ``provsql_arith_op``
  glyph for arith, with ``pow(x, 0.5)`` rendered as ``√``;
  guard / value / default for ``gate_case``, whose node glyph is
  ``⇢``);
- a Circuit-mode fetch that consumes the
  :sqlfunc:`simplified_circuit_subgraph` function
  (:cfile:`SimplifiedSubgraph.cpp`), which runs
  the universal peephole pass on a sub-BFS and returns the
  result as a single ``jsonb`` value, with the ``provsql.simplify_on_load``
  Config-panel toggle switching between the raw and folded
  views.

The RV-family rendering is registry-driven end to end: Studio
reads :sqlfunc:`rv_families` for each family's display label and
parameter symbols, and fetches its density preview as a
server-computed grid from :sqlfunc:`rv_analytical_curves` – so a
newly registered family renders correctly with no Studio change.

The eval-strip *Distribution profile*, *Sample*, *Moment*, and
*Support* entries call :sqlfunc:`rv_histogram`,
:sqlfunc:`rv_sample`, ``rv_moment`` and
``rv_support`` directly. The *Condition on* row-prov
auto-preset is a client-side feature: clicking a result cell
stamps the row's provenance UUID into the input and toggles the
*Conditioned by* badge active. Manual edits stick within a row;
row navigation resets the input to the new row's prov.

Latent variables and posterior inference
----------------------------------------

A distribution parameter may be a scalar provenance token instead of a
literal double, making the leaf a compound (hierarchical) distribution;
conditioning such a leaf on observed data is likelihood-weighting
posterior inference. Two mechanisms implement this.

**Parameter wires on** ``gate_rv``. A ``gate_rv`` gained the ability to
carry wires (it never did before, so no mmap format bump is needed). A
parameter slot in the ``extra`` text is either a literal or a wire
reference ``$i`` (0-based index into the gate's wire vector), e.g.
``"normal:$0,1.0"``. ``DistributionSpec`` (the resolved
``{family, double, double}`` POD every analytic call site consumes) is
**unchanged**; a parallel template parser
(``parse_distribution_template`` in ``src/RandomVariable.cpp``, returning
``DistributionTemplate`` with per-slot ``DistributionParam``) keeps the
literal-or-wire distinction, and ``parse_distribution_spec`` is now that
template parse followed by an all-literal check -- so it *declines* a
parametric leaf and every analytic path (``Expectation.cpp``,
``AnalyticEvaluator.cpp``) falls through to Monte Carlo unchanged. The
sampler's ``gate_rv`` arm (``src/MonteCarloSampler.cpp``) resolves wired
parameters per iteration through ``evalScalar`` (so a shared latent lands
in ``scalar_cache_`` and couples the leaves) and builds the family
instance for that draw; ``integrationRange()`` is the family-agnostic
domain guard for a drawn-out-of-support parameter. The
``FootprintCache`` unions a latent leaf's parameter-wire footprints into
its own, so two leaves sharing a latent are flagged dependent and the
independence shortcuts defeat correctly. The ``DistributionFamily``
factory and the ``Distribution`` interface are untouched -- only the
parameters' *source* (sampled wire vs literal) changes.

**The** ``gate_observe`` **evidence gate and importance sampling.**
``gate_observe`` (append-only enum addition; one wire → the observed
``gate_rv`` leaf, the datum in ``extra``) is an *evidence* node: it
composes into an evidence circuit by ``gate_times`` exactly like a
Boolean conditioning event, but contributes a continuous density factor.
``Sampler::evalWeight`` walks the evidence circuit to a **weight** rather
than a bool or scalar: a ``gate_observe`` returns its leaf's pdf at the
datum (resolving the leaf's latent parameters through ``evalScalar``, so
the weight and the value couple), a ``gate_times`` multiplies its
children's weights, and any other subtree falls to ``evalBool`` for a
``0/1`` weight -- so a purely Boolean evidence tree reproduces the
rejection conditioning. ``importanceSampleConditional`` draws latents
from the prior, weights each by ``evalWeight(evidence)``, and returns a
``WeightedPosterior`` of ``(value, weight)`` particles plus the marginal
likelihood ``P(data)`` (mean weight) and the effective sample size
``(Σw)² / Σw²``. The moment / quantile / sample dispatchers route to it
whenever ``circuitHasObserve`` finds a ``gate_observe`` in the evidence
and the exact conjugate recogniser below has declined, computing
weighted posterior statistics (``rv_sample`` resamples the particles,
SIR). The whole readout goes through ``getJointCircuit`` so a latent
shared between the root and the evidence is a single ``gate_t``.

**Exact conjugate posteriors.** :cfile:`ConjugatePosterior.cpp` is the
closed-form fast path over the same evidence surface: when the target is
a bare **all-literal** ``gate_rv`` (the prior), the evidence flattens
through the ``gate_times`` spine into ``gate_observe`` atoms only
(``gate_one`` factors are skipped; any other factor declines), and every
observed leaf's ``DistributionTemplate`` has exactly **one** wired slot
whose wire is the target gate itself, the posterior is folded one
observation at a time through the **conjugate-update registry**
(``registerConjugateRule`` in ``distributions/Distribution.h``,
keyed on *(likelihood family, wired parameter position, running
posterior family)* -- the same self-registering name-token pattern as
the comparator / closure / transform registries, one rule per likelihood
family file).  The result is a first-class ``DistributionSpec`` of the
prior's family, so ONE recognition upgrades every readout at once:

- ``conditional_raw_moment`` / ``conditional_central_moment``
  (:cfile:`Expectation.cpp`) return the family's closed-form moments --
  the attempt slots between ``collapsedConditionalMoment`` and the
  ``circuitHasObserve`` importance-sampling fallback, preserving the
  invisible-fallback guarantee (any ``nullopt`` leaves the ladder
  unchanged);
- ``compute_quantile`` inverts the posterior CDF exactly;
- :sqlfunc:`rv_sample` draws i.i.d. from the posterior distribution
  (no weighted-particle resampling), seeded through the shared
  ``seedRng``;
- ``matchClosedFormDistribution`` (:cfile:`RangeCheck.cpp`) returns the
  posterior as an untruncated ``TruncatedSingleRv`` shape, making
  :sqlfunc:`rv_histogram` and :sqlfunc:`rv_analytical_curves` exact;
- ``computeEntropy`` (:cfile:`InformationTheory.cpp`) integrates the
  posterior pdf exactly;
- :sqlfunc:`evidence` returns ``exp(Σ log m(dᵢ | ...))``, the exact
  marginal likelihood from each rule's ``log_predictive`` (the
  sequential chain-rule factorisation, accumulated in log space); a rule
  without a predictive declines the whole evidence recognition.

Everything works at ``provsql.rv_mc_samples = 0``.  Each rule's
``update`` guards its own domain (an out-of-support datum, an invalid
literal slot, a non-integer count outside the family pmf's 1e-9
rounding tolerance) and declines rather than raises -- the
zero-weight / ESS diagnostics of importance sampling remain the UX for
contradictory evidence.  The fold canonicalises an ``erlang`` or
``exponential`` prior into the ``gamma`` carrier (identical
distributions; the SQL ``gamma`` constructor stores an integer-shape
prior as an erlang leaf).  Because conjugacy is checked per observation
against the *running* posterior family, mixed likelihoods sharing one
conjugate prior compose (Poisson counts and Exponential gaps over one
Gamma-prior rate).  Correctness is by construction the importance-
sampling estimand: the IS weight is exactly ``∏ f(dᵢ | θ)``
(``evalWeight``) and the conjugate posterior is the prior times that
product renormalised, so recognition changes the method, never the
semantics.  The MVP rule table covers Normal-Normal (mean slot),
Normal-LogNormal (log-location), Gamma-Exponential / -Poisson /
-Gamma / -Erlang / -Pareto (rate and tail-shape slots),
Beta-Binomial / -Geometric / -NegativeBinomial (success-probability
slots, in each family's own support convention), and Pareto-Uniform
(upper bound, zero lower bound only -- ``1/(θ−a₀)`` is Pareto-shaped in
``θ`` only for ``a₀ = 0``).  Deliberately absent, because the exact
posterior leaves the registered families (so it cannot ride the
``DistributionSpec`` carrier): a prior on a Normal's ``σ`` slot
(conjugacy is on the precision, not σ), a Beta *likelihood* with a
latent shape (the ``1/B(α, β)`` normaliser puts Gamma-function factors
in the posterior kernel), Weibull's scale slot, and multi-latent /
hierarchical posteriors.  All decline to importance sampling.
The regression file is ``continuous_conjugate.sql`` (exact pairs at
``rv_mc_samples = 0``, plus decline coverage validated against IS).

The **discrete** families (``poisson``, ``binomial``, ``geometric``,
``negative_binomial``) are ordinary self-registering ``Distribution``
subclasses (``isDiscrete() == true``) that happen to be integer-valued:
``sample()`` draws from the matching ``std::…_distribution``, ``pdf()``
is the pmf (the ``observe`` likelihood weight), and
``integrationRange()`` doubles as the parameter-domain gate (``λ > 0``;
``0 ≤ p ≤ 1``; ``0 < p ≤ 1``).  They are only ever instantiated for a
*latent* leaf -- ``poisson(random_variable)``,
``binomial(integer, random_variable)``,
``geometric(random_variable)``,
``negative_binomial(r, random_variable)`` -- because the literal
constructors still enumerate an exact categorical; and a parametric leaf
is declined by ``parse_distribution_spec``, so no continuous-analytic
path ever integrates their pmf as a density.  They reuse the parametric-
leaf mechanism wholesale, which is why the discrete conjugate posteriors
(Gamma-Poisson, Beta-Binomial) fall out with no evaluator change.
``hypergeometric`` stays literal-only: its three parameters do not fit
the two-parameter ``Distribution`` ABI, so there is no parametric-leaf
form -- the constructor always enumerates its exact categorical.

**The equality-form surface.** The user writes the natural
conditional-equality ``X | (Y = c)`` (single) or ``given(Y = c)`` folded
by :sqlfunc:`and_agg` (a table); ``observe`` is the internal primitive.
The bridge is ``evidence_as_observation`` (SQL): when a conditioning event
is a ``gate_cmp`` with the ``=`` operator, one side a bare ``gate_rv`` leaf
and the other a constant, it is rewritten to a ``gate_observe`` at
*construction* time -- so the stored evidence never reaches the load-time
measure-zero fold. ``random_variable_cond`` (the ``|`` operator) and
``given`` both apply it; ``given(boolean)`` (the renamed ``given_predicate``
placeholder) is planner-rewritten from both the ``| (predicate)`` operator
and the ``given(predicate)`` function call.

This is where the ``Distribution::isDiscrete()`` flag earns its keep. A
point event ``Y = c`` is measure-zero for a *continuous* leaf (folded to
``gate_zero`` by ``RangeCheck``'s continuous EQ/NE shortcut and Dirac
sum-product) but *positive-mass* for a discrete one; ``isDiscrete()`` (an
authoritative per-family flag, propagated through transforms by
``hasOnlyContinuousSupport`` / ``collectDiracMassMap``) keeps the discrete
equality from folding, so a discrete *selection* (``probability(poisson =
5)``) stays exact while a discrete/continuous *conditioning* ``Y = c`` is
routed to likelihood weighting through the observe rewrite.

The rest of the SQL surface is :sqlfunc:`evidence` /
:sqlfunc:`shapley_observe`, the token-accepting constructor overloads, and
the ``provsql.ess_warn_fraction`` GUC.  :sqlfunc:`shapley_observe` is
connecting code: it enumerates the observation subsets, calls ``rv_moment``
for each coalition's posterior value, and combines them into the Shapley
attribution -- exact, so capped at 12 observations. Sequential Monte Carlo (for the many-observations
regime where the ESS collapses) and MCMC are deferred: the sampler is
stateless (``resetIteration()`` wipes state each draw) and has no
joint-density-at-an-assignment evaluation mode, which MCMC's acceptance
ratio needs.
