/* ProvSQL Studio: circuit-mode renderer. Lazy-loaded by app.js once the user clicks a UUID/agg_token cell in circuit-mode results. The DOT layout is computed server-side (provsql_studio/circuit.py via `dot -Tjson`); this module just paints, handles zoom/pan/pin, and delegates expansion to the server. */ (function () { if (window.ProvsqlCircuit) return; // idempotent; app.js may call setup repeatedly const NS = 'http://www.w3.org/2000/svg'; const TYPE_SUMMARY = { plus: 'Plus gate (⊕): alternatives (duplicate elimination, multi-derivation)', times: 'Times gate (⊗): combined use (join, cross product)', monus: 'Monus gate (⊖): m-semiring difference (EXCEPT)', project: 'Project gate (Π): column projection (where-provenance only)', eq: 'Eq gate (⋈): equijoin witness (where-provenance only)', agg: 'Aggregation gate: aggregate function', semimod: 'Semimodule scalar (⋆): tensor product of scalar and semiring value', cmp: 'Compare gate: aggregate-value comparison', delta: 'Delta gate (δ): δ-semiring operator', value: 'Value gate: scalar constant', mulinput: 'Multivalued input (⋮)', input: 'Input gate (ι): base tuple', one: 'One (𝟙): semiring ⊗ identity (always true)', zero: 'Zero (𝟘): semiring ⊕ identity (always false)', update: 'Update gate (υ): INSERT / UPDATE / DELETE', rv: 'Random variable: continuous distribution leaf', arith: 'Arithmetic gate: scalar operation over child gates', mixture: 'Mixture (Mix): Bernoulli-weighted choice between two scalar RV branches, or a categorical block (key + N mulinput outcomes)', assumed: 'Assumption marker: the wrapped subcircuit is sound only ' + 'under the named evaluation assumption (boolean / absorptive)', annotation: 'Annotation wrapper: query-level metadata (inversion-free ' + 'certificate / input order key)', conditioned: 'Conditioned gate (∣): "A given B". Children are the ' + 'target A and the evidence B (plus, for discrete events, ' + 'their joint A∧B); P(A|B) = P(A∧B)/P(B). A terminal token: ' + 'evaluate it for a probability or a moment, not a semiring.', mobius: 'Möbius gate (μ): signed Möbius combination Σ_i c_i·P(child_i) ' + 'over certified-independent islands — the safe-UCQ ' + 'inclusion-exclusion route, where the #P-hard term cancels ' + '(coefficient 0). Each child edge is labelled with its integer ' + 'coefficient. A measure-only token: evaluate it for a ' + 'probability, not a semiring.', case: 'Case gate (⇢): guarded selection over RVs (CASE). Children are ' + 'guard/value pairs followed by a default; the value taken is that ' + 'of the first guard (a comparison event) that holds, else the ' + 'default.', observe: 'Observe gate (=): point-observation evidence that a ' + 'random-variable leaf took the datum value (leaf = datum), ' + 'weighted by its density (likelihood weighting).', 'kc-and': 'AND gate', 'kc-or': 'OR gate', 'kc-not': 'NOT gate', 'kc-input': 'Input gate (ι): base tuple', 'kc-bag': 'Bag: tree-decomposition node', }; // gate_arith info1 holds a PROVSQL_ARITH_* tag (src/provsql_utils.h). // Mirror the server-side _ARITH_OP_GLYPH in circuit.py so the // inspector can label the operator without an extra round-trip. const ARITH_OP_NAME = { 0: 'plus (+)', 1: 'times (×)', 2: 'minus (−)', 3: 'divide (÷)', 4: 'negate (−x)', 5: 'max', 6: 'min', 7: 'power (^)', 8: 'ln', 9: 'exp', }; // ─── state ──────────────────────────────────────────────────────────── // Zoom clamp, shared by the toolbar buttons and wheel-to-zoom. The max // is generous so that dense circuits (where fit-to-screen leaves each // node tiny) can still be magnified enough to read individual gates. const ZOOM_MIN = 0.4; const ZOOM_MAX = 8; let state = { scene: null, // {nodes, edges, root, depth} from /api/circuit showUuids: false, zoom: 1, pan: { x: 0, y: 0 }, pinnedNode: null, // Drag-to-move offsets, keyed by node id. Survive frontier expansion // (paint() reads them on every repaint) so the user's manual nudges // aren't undone when new nodes appear; reset on renderCircuit() so a // new circuit always starts from the Graphviz layout. dragOffsets: Object.create(null), // Row context for the current scene: the `__prov` (or `provsql`) // UUID of the result-table row whose click loaded this circuit. // Used by autoPresetConditionInput so the eval strip's "Condition // on" input defaults to the row's provenance gate (i.e. the // canonical conditioning event for `expected(rv, provenance())`), // even when the click target was the row's random_variable cell // (whose scene root is the RV itself, not the row's prov). rowProv: null, // Tracks which row provsql we last auto-preset against, so a row // context change (clicking another row's cell) overwrites the // Condition input even if the user had manually pasted a UUID // for the previous row. Manual edits within the same row are // still respected (the `input` listener clears autoset, and we // don't overwrite while lastAutoPresetRow matches). lastAutoPresetRow: null, }; let svg = null, edgeLayer = null, nodeLayer = null, bannerEl = null; let titleEl = null, subEl = null; let inspectorEl = null, inspectorTitle = null, inspectorBody = null; // Active node-drag session, populated on mousedown over a .node-group // and consumed by the window-level mousemove/mouseup handlers. let _drag = null; // The circuit "vocabulary" -- which wires get positional labels, what // those labels say, and how an oversized node label is fitted -- lives in // the shared circuit-vocab.js so the notebook snapshot painter // (notebook.js) renders identically. It is a plain