@modernrelay/orbit-core 0.2.0 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { DIAGNOSTIC_SAMPLE_CAP, deriveClusters, resolveClusterCenters, clusterCentroids } from './chunk-Z7FOEASL.js';
2
- export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, clusterCentroids, deriveClusters, generateClusterCenters, resolveClusterCenters } from './chunk-Z7FOEASL.js';
1
+ import { DIAGNOSTIC_SAMPLE_CAP, encodeStringTable, collectTransfers, deriveClusters, resolveClusterCenters, clusterCentroids, EnvelopeSequencer, RequestLedger } from './chunk-FG6TQANI.js';
2
+ export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, acceptColumnar, clusterCentroids, collectTransfers, decodeStringTable, deriveClusters, encodeStringTable, generateClusterCenters, judgeEpoch, resolveClusterCenters } from './chunk-FG6TQANI.js';
3
3
  import { createStore } from 'zustand/vanilla';
4
4
 
5
5
  // src/errors.ts
@@ -96,6 +96,10 @@ function validateSnapshot(snapshot) {
96
96
  record(invalidNode, `[${i}]`);
97
97
  continue;
98
98
  }
99
+ if (id.includes("\0")) {
100
+ record(invalidNode, `[${i}]`);
101
+ continue;
102
+ }
99
103
  if (nodeIndex.has(id)) {
100
104
  record(duplicateNode, id);
101
105
  continue;
@@ -154,7 +158,7 @@ function validateSnapshot(snapshot) {
154
158
  "invalid-node",
155
159
  "error",
156
160
  invalidNode,
157
- `${invalidNode.count} node row(s) dropped: missing or non-string id`
161
+ `${invalidNode.count} node row(s) dropped: missing, non-string, or NUL-containing id`
158
162
  );
159
163
  pushDiagnostic(
160
164
  diagnostics,
@@ -1513,6 +1517,375 @@ function neighborsOf(adj, index) {
1513
1517
  }
1514
1518
  return adj.neighbors.subarray(adj.offsets[index], adj.offsets[index + 1]);
1515
1519
  }
1520
+ function buildIncidence(links, pointCount) {
1521
+ if (!Number.isInteger(pointCount) || pointCount < 0) {
1522
+ throw new RangeError(
1523
+ `buildIncidence: pointCount must be a non-negative integer, got ${pointCount}`
1524
+ );
1525
+ }
1526
+ if ((links.length & 1) !== 0) {
1527
+ throw new RangeError(
1528
+ `buildIncidence: links length must be even ([src, tgt] pairs), got ${links.length}`
1529
+ );
1530
+ }
1531
+ const n = links.length;
1532
+ const offsets = new Uint32Array(pointCount + 1);
1533
+ for (let i = 0; i < n; i++) {
1534
+ const p = links[i];
1535
+ if (p >= pointCount) {
1536
+ throw new RangeError(
1537
+ `buildIncidence: link endpoint ${p} out of range (pointCount ${pointCount})`
1538
+ );
1539
+ }
1540
+ offsets[p + 1] = offsets[p + 1] + 1;
1541
+ }
1542
+ for (let i = 1; i <= pointCount; i++) {
1543
+ offsets[i] = offsets[i] + offsets[i - 1];
1544
+ }
1545
+ const edgeSlots = new Uint32Array(n);
1546
+ const cursor = offsets.slice(0, pointCount);
1547
+ for (let i = 0; i < n; i += 2) {
1548
+ const edge = i >>> 1;
1549
+ const a = links[i];
1550
+ const b = links[i + 1];
1551
+ const ca = cursor[a];
1552
+ edgeSlots[ca] = edge;
1553
+ cursor[a] = ca + 1;
1554
+ const cb = cursor[b];
1555
+ edgeSlots[cb] = edge;
1556
+ cursor[b] = cb + 1;
1557
+ }
1558
+ return { offsets, edgeSlots };
1559
+ }
1560
+ function incidentEdgesOf(inc, index) {
1561
+ const pointCount = inc.offsets.length - 1;
1562
+ if (!Number.isInteger(index) || index < 0 || index >= pointCount) {
1563
+ throw new RangeError(`incidentEdgesOf: index ${index} out of range (pointCount ${pointCount})`);
1564
+ }
1565
+ return inc.edgeSlots.subarray(inc.offsets[index], inc.offsets[index + 1]);
1566
+ }
1567
+
1568
+ // src/alphaCompose.ts
1569
+ var IncrementalAlphaComposer = class {
1570
+ bufA = null;
1571
+ bufB = null;
1572
+ staleA = [];
1573
+ staleB = [];
1574
+ /** Which buffer the NEXT nextBuffer() call returns (0 = A, 1 = B). */
1575
+ next = 0;
1576
+ // --- seed key ---
1577
+ seededBase = null;
1578
+ seededCount = -1;
1579
+ seededDimAlpha = NaN;
1580
+ seededEpoch = void 0;
1581
+ /** Slots rewritten since the last resetStats (wave-5 gate instrument). */
1582
+ slotsRewritten = 0;
1583
+ /** Full reseeds performed (each is one O(n) masked pass over the pair). */
1584
+ reseeds = 0;
1585
+ resetStats() {
1586
+ this.slotsRewritten = 0;
1587
+ this.reseeds = 0;
1588
+ }
1589
+ /**
1590
+ * True when the composer is seeded for exactly this (base, count,
1591
+ * dimAlpha, extraEpoch) tuple; otherwise both buffers are rebuilt with a
1592
+ * full masked pass and the stale lists reset. Object.is on the alpha
1593
+ * handles the NaN sentinel.
1594
+ */
1595
+ ensureSeeded(base, count, dimAlpha, extraEpoch, alphaOf) {
1596
+ if (this.seededBase === base && this.seededCount === count && Object.is(this.seededDimAlpha, dimAlpha) && this.seededEpoch === extraEpoch && this.bufA !== null && this.bufB !== null) {
1597
+ return true;
1598
+ }
1599
+ this.reseeds += 1;
1600
+ this.bufA = this.seedOne(base, count, alphaOf, this.bufA);
1601
+ this.bufB = this.seedOne(base, count, alphaOf, this.bufB);
1602
+ this.staleA.length = 0;
1603
+ this.staleB.length = 0;
1604
+ this.seededBase = base;
1605
+ this.seededCount = count;
1606
+ this.seededDimAlpha = dimAlpha;
1607
+ this.seededEpoch = extraEpoch;
1608
+ return false;
1609
+ }
1610
+ /** Record changed slots from one mask drain (call once per drain — the
1611
+ * drain arrays are reused by the mask, so this copies them out). */
1612
+ note(slots) {
1613
+ for (let i = 0; i < slots.length; i += 1) {
1614
+ this.staleA.push(slots[i]);
1615
+ this.staleB.push(slots[i]);
1616
+ }
1617
+ }
1618
+ /**
1619
+ * Replay the target buffer's stale slots against the CURRENT mask state,
1620
+ * swap, and return it. Same output semantics as the naive composer: the
1621
+ * base RGB is untouched, alpha = base alpha × alphaOf(slot).
1622
+ */
1623
+ nextBuffer(base, alphaOf) {
1624
+ const useA = this.next === 0;
1625
+ const buf = useA ? this.bufA : this.bufB;
1626
+ const stale = useA ? this.staleA : this.staleB;
1627
+ for (let i = 0; i < stale.length; i += 1) {
1628
+ const slot = stale[i];
1629
+ buf[4 * slot + 3] = base[4 * slot + 3] * alphaOf(slot);
1630
+ this.slotsRewritten += 1;
1631
+ }
1632
+ stale.length = 0;
1633
+ this.next = useA ? 1 : 0;
1634
+ return buf;
1635
+ }
1636
+ /** Drop the buffers entirely (scene teardown). */
1637
+ reset() {
1638
+ this.bufA = null;
1639
+ this.bufB = null;
1640
+ this.staleA.length = 0;
1641
+ this.staleB.length = 0;
1642
+ this.seededBase = null;
1643
+ this.seededCount = -1;
1644
+ this.seededDimAlpha = NaN;
1645
+ this.seededEpoch = void 0;
1646
+ this.next = 0;
1647
+ }
1648
+ seedOne(base, count, alphaOf, reuse) {
1649
+ const out = reuse !== null && reuse.length === base.length ? reuse : new Float32Array(base.length);
1650
+ out.set(base);
1651
+ for (let i = 0; i < count; i += 1) {
1652
+ const a = alphaOf(i);
1653
+ if (a !== 1) out[4 * i + 3] = base[4 * i + 3] * a;
1654
+ }
1655
+ return out;
1656
+ }
1657
+ };
1658
+
1659
+ // src/perf.ts
1660
+ var PRESSURE_WINDOW_MS = 250;
1661
+ var DROPPED_FRAME_MS = 34;
1662
+ var SLEEP_GAP_MS = 250;
1663
+ var PressureSampler = class {
1664
+ lastFrameAt = NaN;
1665
+ windowStart = NaN;
1666
+ windowSum = 0;
1667
+ windowCount = 0;
1668
+ ewma = NaN;
1669
+ worst = 0;
1670
+ frames = 0;
1671
+ dropped = 0;
1672
+ windows = 0;
1673
+ idleWakeups = 0;
1674
+ /**
1675
+ * Record one onFrame tick. `settled` marks a tick that arrived while the
1676
+ * scene was at rest (sim settled, no pending commit work) — the idle-
1677
+ * wakeup counter, which reads 0 when the ADR-005 gated clock is honest.
1678
+ */
1679
+ noteFrame(timeMs, settled) {
1680
+ this.frames += 1;
1681
+ if (settled) this.idleWakeups += 1;
1682
+ const prev = this.lastFrameAt;
1683
+ this.lastFrameAt = timeMs;
1684
+ if (Number.isNaN(prev)) {
1685
+ this.windowStart = timeMs;
1686
+ return;
1687
+ }
1688
+ const delta = timeMs - prev;
1689
+ if (delta < 0) {
1690
+ this.windowStart = timeMs;
1691
+ this.windowSum = 0;
1692
+ this.windowCount = 0;
1693
+ return;
1694
+ }
1695
+ if (delta >= SLEEP_GAP_MS) {
1696
+ this.closeWindow();
1697
+ this.windowStart = timeMs;
1698
+ return;
1699
+ }
1700
+ if (delta > this.worst) this.worst = delta;
1701
+ if (delta >= DROPPED_FRAME_MS) this.dropped += 1;
1702
+ this.windowSum += delta;
1703
+ this.windowCount += 1;
1704
+ if (timeMs - this.windowStart >= PRESSURE_WINDOW_MS) {
1705
+ this.closeWindow();
1706
+ this.windowStart = timeMs;
1707
+ }
1708
+ }
1709
+ snapshot() {
1710
+ return {
1711
+ frameEwmaMs: this.ewma,
1712
+ worstFrameMs: this.worst,
1713
+ frames: this.frames,
1714
+ droppedFrames: this.dropped,
1715
+ windows: this.windows,
1716
+ idleWakeups: this.idleWakeups
1717
+ };
1718
+ }
1719
+ /** Zero the accumulated counters (EWMA and frame anchor survive — the
1720
+ * smoothing is continuous; the counters are per-sample-period). */
1721
+ resetCounters() {
1722
+ this.worst = 0;
1723
+ this.frames = 0;
1724
+ this.dropped = 0;
1725
+ this.windows = 0;
1726
+ this.idleWakeups = 0;
1727
+ }
1728
+ closeWindow() {
1729
+ if (this.windowCount === 0) return;
1730
+ const mean = this.windowSum / this.windowCount;
1731
+ this.windows += 1;
1732
+ this.ewma = Number.isNaN(this.ewma) ? mean : this.ewma * 0.7 + mean * 0.3;
1733
+ this.windowSum = 0;
1734
+ this.windowCount = 0;
1735
+ }
1736
+ };
1737
+
1738
+ // src/degrade.ts
1739
+ var SCALE_LIMITS_DEFAULTS = Object.freeze({
1740
+ domLabelNodes: 1e5,
1741
+ pickingLinks: 25e4,
1742
+ histogramBatchNodes: 5e5,
1743
+ hysteresis: 0.1,
1744
+ minimumDwellMs: 1e3,
1745
+ resourceDegradationOrder: Object.freeze(["disable-transitions", "defer-images"])
1746
+ });
1747
+ var RESOURCE_STEPS = /* @__PURE__ */ new Set([
1748
+ "disable-transitions",
1749
+ "defer-images",
1750
+ "uniform-link-style"
1751
+ ]);
1752
+ function resolveScaleLimits(input) {
1753
+ const warnings = [];
1754
+ const out = { ...SCALE_LIMITS_DEFAULTS };
1755
+ if (input === void 0) return { limits: out, warnings };
1756
+ const num3 = (key) => {
1757
+ const v = input[key];
1758
+ if (v === void 0) return;
1759
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[key] = v;
1760
+ else warnings.push(`limits.${key} must be a non-negative finite number (got ${String(v)})`);
1761
+ };
1762
+ num3("domLabelNodes");
1763
+ num3("pickingLinks");
1764
+ num3("histogramBatchNodes");
1765
+ num3("minimumDwellMs");
1766
+ if (input.hysteresis !== void 0) {
1767
+ const h = input.hysteresis;
1768
+ if (typeof h === "number" && Number.isFinite(h) && h >= 0 && h < 1) out.hysteresis = h;
1769
+ else warnings.push(`limits.hysteresis must be in [0, 1) (got ${String(h)})`);
1770
+ }
1771
+ if (input.resourceDegradationOrder !== void 0) {
1772
+ const order = input.resourceDegradationOrder;
1773
+ const valid = Array.isArray(order) && order.every((s) => RESOURCE_STEPS.has(s)) && new Set(order).size === order.length;
1774
+ if (valid) out.resourceDegradationOrder = Object.freeze([...order]);
1775
+ else {
1776
+ warnings.push(
1777
+ "limits.resourceDegradationOrder must be unique resource steps ('disable-transitions' | 'defer-images' | 'uniform-link-style')"
1778
+ );
1779
+ }
1780
+ }
1781
+ return { limits: Object.freeze(out), warnings };
1782
+ }
1783
+ var DegradeController = class {
1784
+ constructor(limits, now) {
1785
+ this.limits = limits;
1786
+ this.now = now;
1787
+ }
1788
+ limits;
1789
+ now;
1790
+ steps = /* @__PURE__ */ new Map();
1791
+ activeCache = Object.freeze([]);
1792
+ isEngaged(step) {
1793
+ return this.steps.get(step)?.engaged === true;
1794
+ }
1795
+ /** Stable frozen list for GraphPerfSnapshot.activeDegradations. */
1796
+ activeSteps() {
1797
+ return this.activeCache;
1798
+ }
1799
+ /**
1800
+ * Edge-triggered count evaluation over the CURRENT visible counts.
1801
+ * Returns the events to emit (empty when nothing crossed its band or a
1802
+ * dwell is holding). Count-engaged steps disengage here; pressure- or
1803
+ * resource-engaged steps do NOT auto-disengage on counts — their signal
1804
+ * owns them (`clearPressure` / `releaseResourceSteps`).
1805
+ */
1806
+ evaluateCounts(visible) {
1807
+ const events = [];
1808
+ const counts = [
1809
+ ["cap-dom-labels", visible.nodes, this.limits.domLabelNodes],
1810
+ ["defer-link-picking", visible.edges, this.limits.pickingLinks],
1811
+ ["batch-histograms", visible.nodes, this.limits.histogramBatchNodes]
1812
+ ];
1813
+ for (const [step, value, limit] of counts) {
1814
+ const state = this.stateOf(step);
1815
+ if (!state.engaged) {
1816
+ if (value > limit && this.dwellOver(state)) {
1817
+ events.push(this.transition(step, state, true, "count", visible));
1818
+ }
1819
+ } else if (state.reason === "count") {
1820
+ if (value < limit * (1 - this.limits.hysteresis) && this.dwellOver(state)) {
1821
+ events.push(this.transition(step, state, false, "count", visible));
1822
+ }
1823
+ }
1824
+ }
1825
+ return events;
1826
+ }
1827
+ /**
1828
+ * Pressure trigger (frame or input): engage a step EARLIER than its count
1829
+ * hint. No-op (null) when already engaged or dwell-held.
1830
+ */
1831
+ engageForPressure(step, reason, visible) {
1832
+ const state = this.stateOf(step);
1833
+ if (state.engaged || !this.dwellOver(state)) return null;
1834
+ return this.transition(step, state, true, reason, visible);
1835
+ }
1836
+ /** Release a pressure-engaged step once its signal normalizes. */
1837
+ clearPressure(step, visible) {
1838
+ const state = this.stateOf(step);
1839
+ if (!state.engaged) return null;
1840
+ if (state.reason !== "frame-pressure" && state.reason !== "input-pressure") return null;
1841
+ if (!this.dwellOver(state)) return null;
1842
+ return this.transition(step, state, false, state.reason, visible);
1843
+ }
1844
+ /**
1845
+ * Resource-admission trigger: engage the NEXT not-yet-engaged step in the
1846
+ * declared order. Null when the order is exhausted — the caller must then
1847
+ * REJECT before allocating (§17: never silently erase semantic styling).
1848
+ */
1849
+ engageNextResourceStep(visible) {
1850
+ for (const step of this.limits.resourceDegradationOrder) {
1851
+ const state = this.stateOf(step);
1852
+ if (state.engaged) continue;
1853
+ return this.transition(step, state, true, "resource-estimate", visible);
1854
+ }
1855
+ return null;
1856
+ }
1857
+ /** Release every resource-engaged step (pressure cleared / new budget). */
1858
+ releaseResourceSteps(visible) {
1859
+ const events = [];
1860
+ for (const step of this.limits.resourceDegradationOrder) {
1861
+ const state = this.stateOf(step);
1862
+ if (state.engaged && state.reason === "resource-estimate") {
1863
+ events.push(this.transition(step, state, false, "resource-estimate", visible));
1864
+ }
1865
+ }
1866
+ return events;
1867
+ }
1868
+ stateOf(step) {
1869
+ let state = this.steps.get(step);
1870
+ if (state === void 0) {
1871
+ state = { engaged: false, changedAt: -Infinity, reason: null };
1872
+ this.steps.set(step, state);
1873
+ }
1874
+ return state;
1875
+ }
1876
+ dwellOver(state) {
1877
+ return this.now() - state.changedAt >= this.limits.minimumDwellMs || state.changedAt === -Infinity;
1878
+ }
1879
+ transition(step, state, engaged, reason, visible) {
1880
+ state.engaged = engaged;
1881
+ state.changedAt = this.now();
1882
+ state.reason = engaged ? reason : null;
1883
+ const active = [];
1884
+ for (const [s, st] of this.steps) if (st.engaged) active.push(s);
1885
+ this.activeCache = Object.freeze(active);
1886
+ return { step, engaged, reason, visible: { nodes: visible.nodes, edges: visible.edges } };
1887
+ }
1888
+ };
1516
1889
 
1517
1890
  // src/linkPick.ts
1518
1891
  var MEDIAN_SAMPLE_CAP = 1024;
@@ -2492,8 +2865,8 @@ function buildEntries(nodes, fields) {
2492
2865
  fieldValues = [];
2493
2866
  fieldLowers = [];
2494
2867
  const attrs = node.attrs;
2495
- for (const field of fields) {
2496
- const raw = attrs?.[field];
2868
+ for (const field2 of fields) {
2869
+ const raw = attrs?.[field2];
2497
2870
  if (raw === void 0 || raw === null) continue;
2498
2871
  const value = String(raw);
2499
2872
  fieldValues.push(value);
@@ -2595,11 +2968,408 @@ function createLocalSearchService(getBase) {
2595
2968
  };
2596
2969
  }
2597
2970
 
2971
+ // src/columnar.ts
2972
+ function isColumnarSnapshot(input) {
2973
+ return input.kind === "columnar";
2974
+ }
2975
+ var isTypedLength = (arr, n) => arr !== void 0 && arr.length === n;
2976
+ function checkStringColumn(col, rows, where, isIds, out) {
2977
+ if (col === null || typeof col !== "object") {
2978
+ out.push({ where, problem: "not-a-string-column", detail: "column missing or not an object" });
2979
+ return;
2980
+ }
2981
+ if (col.kind !== "string" || !Array.isArray(col.dictionary) || !(col.codes instanceof Uint32Array)) {
2982
+ out.push({ where, problem: "not-a-string-column", detail: 'expected {kind:"string", dictionary, codes}' });
2983
+ return;
2984
+ }
2985
+ if (col.codes.length !== rows) {
2986
+ const detached = col.codes.length === 0 && col.codes.buffer.byteLength === 0;
2987
+ out.push({
2988
+ where,
2989
+ problem: "length-mismatch",
2990
+ detail: detached ? `codes buffer is DETACHED (a bufferOwnership:'transfer' snapshot is single-use)` : `codes.length ${col.codes.length} !== length ${rows}`
2991
+ });
2992
+ return;
2993
+ }
2994
+ if (col.nulls !== void 0) {
2995
+ if (col.nulls.length !== rows) {
2996
+ out.push({
2997
+ where,
2998
+ problem: "nulls-length-mismatch",
2999
+ detail: `nulls.length ${col.nulls.length} !== length ${rows}`
3000
+ });
3001
+ } else if (isIds) {
3002
+ for (let i = 0; i < rows; i++) {
3003
+ if (col.nulls[i] !== 0) {
3004
+ out.push({ where, problem: "null-id", detail: `row ${i}: ids may not be null` });
3005
+ break;
3006
+ }
3007
+ }
3008
+ }
3009
+ }
3010
+ const dictSize = col.dictionary.length;
3011
+ for (let i = 0; i < rows; i++) {
3012
+ if (col.codes[i] >= dictSize) {
3013
+ out.push({
3014
+ where,
3015
+ problem: "code-out-of-range",
3016
+ detail: `row ${i}: code ${col.codes[i]} >= dictionary size ${dictSize}`
3017
+ });
3018
+ break;
3019
+ }
3020
+ }
3021
+ }
3022
+ function checkColumn(col, rows, where, out) {
3023
+ if (col === null || typeof col !== "object") {
3024
+ out.push({ where, problem: "bad-column-kind", detail: "column missing or not an object" });
3025
+ return;
3026
+ }
3027
+ switch (col.kind) {
3028
+ case "string":
3029
+ checkStringColumn(col, rows, where, false, out);
3030
+ return;
3031
+ case "f64":
3032
+ case "i32":
3033
+ case "u32":
3034
+ case "bool": {
3035
+ if (!isTypedLength(col.data, rows)) {
3036
+ out.push({
3037
+ where,
3038
+ problem: "length-mismatch",
3039
+ detail: `data.length ${col.data?.length ?? "missing"} !== length ${rows}`
3040
+ });
3041
+ }
3042
+ if (col.nulls !== void 0 && col.nulls.length !== rows) {
3043
+ out.push({
3044
+ where,
3045
+ problem: "nulls-length-mismatch",
3046
+ detail: `nulls.length ${col.nulls.length} !== length ${rows}`
3047
+ });
3048
+ }
3049
+ return;
3050
+ }
3051
+ default:
3052
+ out.push({
3053
+ where,
3054
+ problem: "bad-column-kind",
3055
+ detail: `unknown column kind '${String(col.kind)}'`
3056
+ });
3057
+ }
3058
+ }
3059
+ function validateColumnarStructure(snapshot) {
3060
+ const issues = [];
3061
+ const nodeRows = snapshot.nodes?.length;
3062
+ const edgeRows = snapshot.edges?.length;
3063
+ if (!Number.isInteger(nodeRows) || nodeRows < 0) {
3064
+ issues.push({ where: "nodes.length", problem: "bad-length", detail: String(nodeRows) });
3065
+ return issues;
3066
+ }
3067
+ if (!Number.isInteger(edgeRows) || edgeRows < 0) {
3068
+ issues.push({ where: "edges.length", problem: "bad-length", detail: String(edgeRows) });
3069
+ return issues;
3070
+ }
3071
+ if (snapshot.nodes === null || typeof snapshot.nodes !== "object") {
3072
+ issues.push({ where: "nodes", problem: "bad-length", detail: "nodes lane missing" });
3073
+ return issues;
3074
+ }
3075
+ if (snapshot.edges === null || typeof snapshot.edges !== "object") {
3076
+ issues.push({ where: "edges", problem: "bad-length", detail: "edges lane missing" });
3077
+ return issues;
3078
+ }
3079
+ checkStringColumn(snapshot.nodes.ids, nodeRows, "nodes.ids", true, issues);
3080
+ for (const [name, col] of Object.entries(snapshot.nodes.columns ?? {})) {
3081
+ checkColumn(col, nodeRows, `nodes.${name}`, issues);
3082
+ }
3083
+ checkStringColumn(snapshot.edges.ids, edgeRows, "edges.ids", true, issues);
3084
+ for (const [name, col] of Object.entries(snapshot.edges.columns ?? {})) {
3085
+ checkColumn(col, edgeRows, `edges.${name}`, issues);
3086
+ }
3087
+ const { source, target } = snapshot.edges;
3088
+ if (!(source instanceof Uint32Array) || source.length !== edgeRows) {
3089
+ issues.push({
3090
+ where: "edges.source",
3091
+ problem: "endpoint-length-mismatch",
3092
+ detail: `source.length ${source?.length ?? "missing"} !== length ${edgeRows}`
3093
+ });
3094
+ }
3095
+ if (!(target instanceof Uint32Array) || target.length !== edgeRows) {
3096
+ issues.push({
3097
+ where: "edges.target",
3098
+ problem: "endpoint-length-mismatch",
3099
+ detail: `target.length ${target?.length ?? "missing"} !== length ${edgeRows}`
3100
+ });
3101
+ }
3102
+ if (issues.length === 0) {
3103
+ for (let i = 0; i < edgeRows; i++) {
3104
+ if (source[i] >= nodeRows || target[i] >= nodeRows) {
3105
+ issues.push({
3106
+ where: "edges.endpoints",
3107
+ problem: "endpoint-out-of-range",
3108
+ detail: `row ${i}: (${source[i]}, ${target[i]}) with ${nodeRows} nodes`
3109
+ });
3110
+ break;
3111
+ }
3112
+ }
3113
+ }
3114
+ return issues;
3115
+ }
3116
+ function columnValueAt(col, i) {
3117
+ if (col.nulls !== void 0 && col.nulls[i] !== 0) return null;
3118
+ switch (col.kind) {
3119
+ case "string":
3120
+ return col.dictionary[col.codes[i]];
3121
+ case "bool":
3122
+ return col.data[i] !== 0;
3123
+ default:
3124
+ return col.data[i];
3125
+ }
3126
+ }
3127
+ function materializeColumnarSnapshot(snapshot) {
3128
+ const nodeCols = Object.entries(snapshot.nodes.columns ?? {});
3129
+ const nodeIds = snapshot.nodes.ids;
3130
+ const nodes = new Array(snapshot.nodes.length);
3131
+ for (let i = 0; i < snapshot.nodes.length; i++) {
3132
+ const attrs = {};
3133
+ for (const [name, col] of nodeCols) attrs[name] = columnValueAt(col, i);
3134
+ nodes[i] = { id: nodeIds.dictionary[nodeIds.codes[i]], attrs };
3135
+ }
3136
+ const edgeCols = Object.entries(snapshot.edges.columns ?? {});
3137
+ const edgeIds = snapshot.edges.ids;
3138
+ const { source, target } = snapshot.edges;
3139
+ const edges = new Array(snapshot.edges.length);
3140
+ for (let i = 0; i < snapshot.edges.length; i++) {
3141
+ const attrs = {};
3142
+ for (const [name, col] of edgeCols) attrs[name] = columnValueAt(col, i);
3143
+ edges[i] = {
3144
+ id: edgeIds.dictionary[edgeIds.codes[i]],
3145
+ source: nodes[source[i]].id,
3146
+ target: nodes[target[i]].id,
3147
+ attrs
3148
+ };
3149
+ }
3150
+ return {
3151
+ datasetKey: snapshot.datasetKey,
3152
+ sourceRevision: snapshot.sourceRevision,
3153
+ nodes,
3154
+ edges
3155
+ };
3156
+ }
3157
+ function buildAcceptedFromColumnar(snapshot, acceptance) {
3158
+ const nodeCols = Object.entries(snapshot.nodes.columns ?? {});
3159
+ const nodeIds = snapshot.nodes.ids;
3160
+ const nodes = new Array(acceptance.acceptedNodeCount);
3161
+ const nodeIndex = /* @__PURE__ */ new Map();
3162
+ let outN = 0;
3163
+ for (let i = 0; i < snapshot.nodes.length; i++) {
3164
+ if (acceptance.keepNodes[i] !== 1) continue;
3165
+ const attrs = {};
3166
+ for (const [name, col] of nodeCols) attrs[name] = columnValueAt(col, i);
3167
+ const id = nodeIds.dictionary[nodeIds.codes[i]];
3168
+ nodeIndex.set(id, outN);
3169
+ nodes[outN] = { id, attrs };
3170
+ outN += 1;
3171
+ }
3172
+ const edgeCols = Object.entries(snapshot.edges.columns ?? {});
3173
+ const edgeIds = snapshot.edges.ids;
3174
+ const { source, target } = snapshot.edges;
3175
+ const edges = new Array(acceptance.acceptedEdgeCount);
3176
+ let outE = 0;
3177
+ for (let e = 0; e < snapshot.edges.length; e++) {
3178
+ if (acceptance.keepEdges[e] !== 1) continue;
3179
+ const attrs = {};
3180
+ for (const [name, col] of edgeCols) attrs[name] = columnValueAt(col, e);
3181
+ edges[outE] = {
3182
+ id: edgeIds.dictionary[edgeIds.codes[e]],
3183
+ // Endpoint STRINGS resolve through the original row (a dropped
3184
+ // duplicate row shares its survivor's id string by construction).
3185
+ source: nodeIds.dictionary[nodeIds.codes[source[e]]],
3186
+ target: nodeIds.dictionary[nodeIds.codes[target[e]]],
3187
+ attrs
3188
+ };
3189
+ outE += 1;
3190
+ }
3191
+ return {
3192
+ datasetKey: snapshot.datasetKey,
3193
+ sourceRevision: snapshot.sourceRevision,
3194
+ nodes,
3195
+ edges,
3196
+ nodeIndex,
3197
+ diagnostics: acceptance.diagnostics
3198
+ };
3199
+ }
3200
+ function columnarArrayBuffers(snapshot) {
3201
+ const buffers = /* @__PURE__ */ new Set();
3202
+ const add = (view) => {
3203
+ if (view !== void 0 && view.buffer instanceof ArrayBuffer) buffers.add(view.buffer);
3204
+ };
3205
+ const addColumn = (col) => {
3206
+ if (col.kind === "string") add(col.codes);
3207
+ else add(col.data);
3208
+ add(col.nulls);
3209
+ };
3210
+ addColumn(snapshot.nodes.ids);
3211
+ for (const col of Object.values(snapshot.nodes.columns ?? {})) addColumn(col);
3212
+ addColumn(snapshot.edges.ids);
3213
+ for (const col of Object.values(snapshot.edges.columns ?? {})) addColumn(col);
3214
+ add(snapshot.edges.source);
3215
+ add(snapshot.edges.target);
3216
+ return [...buffers];
3217
+ }
3218
+ function detachColumnarBuffers(snapshot) {
3219
+ let detached = 0;
3220
+ for (const buffer of columnarArrayBuffers(snapshot)) {
3221
+ if (buffer.byteLength === 0) continue;
3222
+ const transferable = buffer;
3223
+ if (typeof transferable.transfer === "function") {
3224
+ transferable.transfer();
3225
+ } else {
3226
+ structuredClone(buffer, { transfer: [buffer] });
3227
+ }
3228
+ detached += 1;
3229
+ }
3230
+ return detached;
3231
+ }
3232
+
3233
+ // src/worker/lane.ts
3234
+ function defaultWorkerUrl() {
3235
+ return new URL("./worker/entry.js", import.meta.url);
3236
+ }
3237
+ function transportFromWorker(worker) {
3238
+ return {
3239
+ post: (envelope, transfers) => worker.postMessage(envelope, [...transfers]),
3240
+ onReply: (cb) => {
3241
+ worker.onmessage = (ev) => cb(ev.data);
3242
+ },
3243
+ onError: (cb) => {
3244
+ worker.onerror = (ev) => cb(ev.message !== "" ? ev.message : "worker error");
3245
+ worker.onmessageerror = () => cb("worker messageerror (unclonable reply)");
3246
+ },
3247
+ terminate: () => worker.terminate()
3248
+ };
3249
+ }
3250
+ var WorkerLane = class {
3251
+ options;
3252
+ sequencer = new EnvelopeSequencer();
3253
+ ledger = new RequestLedger();
3254
+ pending = /* @__PURE__ */ new Map();
3255
+ transport = null;
3256
+ /** null = not yet booted; false = boot failed (permanent for this lane). */
3257
+ availableState = null;
3258
+ constructor(options = {}) {
3259
+ this.options = options;
3260
+ }
3261
+ available() {
3262
+ return this.availableState;
3263
+ }
3264
+ /** Synchronous boot probe for callers that must pick a lane NOW (the
3265
+ * instance's sync applyHostUpdate cannot await a rejected request). */
3266
+ ensureBooted() {
3267
+ return this.boot();
3268
+ }
3269
+ /** Boot lazily on first use. A throwing factory (no Worker global, CSP,
3270
+ * missing asset) marks the lane unavailable FOREVER — one diagnostic,
3271
+ * then the caller's main path owns every subsequent request. */
3272
+ boot() {
3273
+ if (this.availableState !== null) return this.availableState;
3274
+ try {
3275
+ let transport = this.options.transport ?? null;
3276
+ if (transport === null) {
3277
+ const factory = this.options.factory;
3278
+ const worker = factory !== void 0 && "create" in factory ? factory.create() : new Worker(
3279
+ factory !== void 0 && "url" in factory ? factory.url : defaultWorkerUrl(),
3280
+ { type: "module" }
3281
+ );
3282
+ transport = transportFromWorker(worker);
3283
+ }
3284
+ transport.onReply((reply) => this.settle(reply));
3285
+ transport.onError?.((reason) => this.fail(reason));
3286
+ this.transport = transport;
3287
+ this.availableState = true;
3288
+ } catch (err) {
3289
+ this.availableState = false;
3290
+ this.options.onUnavailable?.(err instanceof Error ? err.message : String(err));
3291
+ }
3292
+ return this.availableState;
3293
+ }
3294
+ /** Async worker death (P1: a constructed-but-dead worker must strand
3295
+ * NOTHING): every pending request rejects as 'worker-failed' so callers
3296
+ * run their main-lane fallback, the lane goes permanently unavailable,
3297
+ * and the unavailability callback fires (the instance one-shots it). */
3298
+ fail(reason) {
3299
+ if (this.availableState === false) return;
3300
+ this.availableState = false;
3301
+ this.options.onUnavailable?.(reason);
3302
+ for (const [id, entry] of this.pending) {
3303
+ this.pending.delete(id);
3304
+ entry.reject(new Error("worker-failed"));
3305
+ }
3306
+ this.ledger.abortAll();
3307
+ this.transport?.terminate();
3308
+ this.transport = null;
3309
+ }
3310
+ settle(reply) {
3311
+ const original = this.ledger.settle(reply);
3312
+ if (reply.inReplyTo === void 0) return;
3313
+ const entry = this.pending.get(reply.inReplyTo);
3314
+ this.pending.delete(reply.inReplyTo);
3315
+ if (entry === void 0) return;
3316
+ if (original === null) {
3317
+ entry.reject(new Error("superseded"));
3318
+ return;
3319
+ }
3320
+ entry.resolve(reply);
3321
+ }
3322
+ /**
3323
+ * Send one request. Resolves with the reply envelope (op 'result' or
3324
+ * 'error' — protocol errors are DATA to the caller, not exceptions);
3325
+ * rejects only on supersession/abort/unavailability.
3326
+ */
3327
+ request(epoch, entity, op, payload, transfers, klass, lane) {
3328
+ if (!this.boot()) {
3329
+ return Promise.reject(new Error("worker-unavailable"));
3330
+ }
3331
+ const envelope = this.sequencer.make(epoch, entity, op, payload);
3332
+ const signal = this.ledger.track(envelope, klass, lane);
3333
+ return new Promise((resolve, reject) => {
3334
+ if (signal.aborted) {
3335
+ reject(new Error("superseded"));
3336
+ return;
3337
+ }
3338
+ signal.addEventListener(
3339
+ "abort",
3340
+ () => {
3341
+ if (this.pending.delete(envelope.msgId)) reject(new Error("superseded"));
3342
+ },
3343
+ { once: true }
3344
+ );
3345
+ this.pending.set(envelope.msgId, { resolve, reject });
3346
+ this.transport.post(envelope, transfers);
3347
+ });
3348
+ }
3349
+ /** Abort everything in flight (epoch advance / detach / dataset swap).
3350
+ * The ledger's controllers fire each pending promise's abort listener;
3351
+ * the sweep below catches anything tracked before a listener attached —
3352
+ * rejects stay idempotent through the delete guard. */
3353
+ abortAll() {
3354
+ this.ledger.abortAll();
3355
+ for (const [id, entry] of this.pending) {
3356
+ this.pending.delete(id);
3357
+ entry.reject(new Error("aborted"));
3358
+ }
3359
+ }
3360
+ terminate() {
3361
+ this.abortAll();
3362
+ this.transport?.terminate();
3363
+ this.transport = null;
3364
+ this.availableState = null;
3365
+ }
3366
+ };
3367
+
2598
3368
  // src/filter.ts
2599
- function resolveFilterField(item, field) {
2600
- if (field === "id") return item.id;
3369
+ function resolveFilterField(item, field2) {
3370
+ if (field2 === "id") return item.id;
2601
3371
  const attrs = item.attrs;
2602
- return attrs?.[field];
3372
+ return attrs?.[field2];
2603
3373
  }
2604
3374
  function filterValuesEqual(a, b) {
2605
3375
  if (typeof a === "number" && typeof b === "number") {
@@ -2769,7 +3539,7 @@ function compileSelector(selector) {
2769
3539
  };
2770
3540
  }
2771
3541
  let current = null;
2772
- const resolve = (field) => current === null ? void 0 : resolveFilterField(current, field);
3542
+ const resolve = (field2) => current === null ? void 0 : resolveFilterField(current, field2);
2773
3543
  return {
2774
3544
  errors,
2775
3545
  test(item) {
@@ -3047,6 +3817,16 @@ var MetricStore = class {
3047
3817
  /** Admitted async columns by metric name; NaN encodes null. */
3048
3818
  columns = /* @__PURE__ */ new Map();
3049
3819
  degreePasses = 0;
3820
+ /** §17 telemetry: estimated bytes of metric storage held (S13-T07). */
3821
+ estimatedBytes() {
3822
+ let bytes = 0;
3823
+ for (const col of this.columns.values()) bytes += col.byteLength;
3824
+ const dc = this.degreeCache;
3825
+ if (dc !== null) {
3826
+ bytes += dc.degree.byteLength + dc.inDegree.byteLength + dc.outDegree.byteLength;
3827
+ }
3828
+ return bytes;
3829
+ }
3050
3830
  /** Number of combined degree-family compute passes (test observability). */
3051
3831
  get degreeComputePasses() {
3052
3832
  return this.degreePasses;
@@ -3278,6 +4058,7 @@ function resolveEnginePolicy(capabilities, requested) {
3278
4058
  images: imagesNative ? "native" : "placeholder",
3279
4059
  linkPicking: capabilities.linkPicking ? "native" : "cpu-fallback",
3280
4060
  clusterForce: clusterForceNative ? "native" : "inert",
4061
+ quiescence: capabilities.idleFrames === "stops" ? "stops" : "free-running",
3281
4062
  // Defensive snapshot: the policy must not alias the caller's array.
3282
4063
  rangedChannels: new Set(capabilities.rangeUpdates),
3283
4064
  degradations: Object.freeze(degradations)
@@ -3302,12 +4083,27 @@ function assertCapabilityMethodParity(engine) {
3302
4083
  }
3303
4084
  function normalizeCommitForCapabilities(commit, capabilities) {
3304
4085
  const config = commit.config;
4086
+ const declaredRanged = new Set(capabilities.rangeUpdates);
4087
+ const undeclaredPatches = commit.bufferPatches !== void 0 ? Object.keys(commit.bufferPatches).filter(
4088
+ (ch) => !declaredRanged.has(ch)
4089
+ ) : [];
3305
4090
  const dropResources = capabilities.pointImages !== true && commit.resources !== void 0;
3306
4091
  const dropLinkArrows = capabilities.edgeArrows !== true && config !== void 0 && config.linkArrows !== void 0;
3307
4092
  const dropCluster = capabilities.clusterForce !== true && config !== void 0 && config.cluster !== void 0;
3308
- if (!dropResources && !dropLinkArrows && !dropCluster) return { commit, dropped: [] };
4093
+ if (!dropResources && !dropLinkArrows && !dropCluster && undeclaredPatches.length === 0) {
4094
+ return { commit, dropped: [] };
4095
+ }
3309
4096
  const dropped = [];
3310
4097
  const next = { ...commit };
4098
+ if (undeclaredPatches.length > 0) {
4099
+ const kept = { ...next.bufferPatches };
4100
+ for (const ch of undeclaredPatches) {
4101
+ delete kept[ch];
4102
+ dropped.push(`bufferPatches.${ch}`);
4103
+ }
4104
+ if (Object.keys(kept).length > 0) next.bufferPatches = kept;
4105
+ else delete next.bufferPatches;
4106
+ }
3311
4107
  if (dropResources) {
3312
4108
  delete next.resources;
3313
4109
  dropped.push("resources");
@@ -3660,7 +4456,7 @@ var Lane = class {
3660
4456
  }
3661
4457
  };
3662
4458
  function newMembership(capacity) {
3663
- return { flags: new Uint8Array(capacity), list: [] };
4459
+ return { flags: new Uint8Array(capacity), list: [], holes: 0 };
3664
4460
  }
3665
4461
  function growMembership(mem, capacity) {
3666
4462
  if (capacity <= mem.flags.length) return;
@@ -3686,6 +4482,12 @@ var SoftMask = class {
3686
4482
  overflowedFlag = false;
3687
4483
  /** Total memberships currently held across all sources and lanes. */
3688
4484
  totalHeld = 0;
4485
+ /** §17 O(Δ) op counters (F10-02 gate instrumentation). */
4486
+ statsBox = {
4487
+ slotsVisited: 0,
4488
+ zeroCrossings: 0,
4489
+ cascadeEdgesVisited: 0
4490
+ };
3689
4491
  constructor(nodeCapacity, edgeCapacity) {
3690
4492
  checkCapacity(nodeCapacity, "nodeCapacity");
3691
4493
  checkCapacity(edgeCapacity, "edgeCapacity");
@@ -3780,6 +4582,14 @@ var SoftMask = class {
3780
4582
  this.applyMembership(this.edgeDimLane, state.edgeDim, dimIdx);
3781
4583
  }
3782
4584
  },
4585
+ updateNodeFailures: (addHide, removeHide, crossings) => {
4586
+ ensureAlive();
4587
+ this.applyMembershipDelta(this.nodeHideLane, state.nodeHide, addHide, removeHide, crossings);
4588
+ },
4589
+ updateEdgeFailures: (addHide, removeHide) => {
4590
+ ensureAlive();
4591
+ this.applyMembershipDelta(this.edgeHideLane, state.edgeHide, addHide, removeHide);
4592
+ },
3783
4593
  clear: () => {
3784
4594
  ensureAlive();
3785
4595
  this.clearSource(state);
@@ -3833,6 +4643,34 @@ var SoftMask = class {
3833
4643
  this.cascadeSource ??= this.acquire("\xA79.1 node\u2192edge cascade");
3834
4644
  this.cascadeSource.setEdgeFailures(failing);
3835
4645
  }
4646
+ /**
4647
+ * O(incident-edges) delta form of the §9.1 cascade (F10-02): for each node
4648
+ * whose HIDE visibility crossed zero, recompute only its incident edges'
4649
+ * cascade state from the CURRENT node counters and apply the delta through
4650
+ * the same internal cascade source the full form uses — the two compose
4651
+ * freely (the full form re-baselines). Edges shared by two crossed nodes
4652
+ * are visited twice; the membership delta is idempotent, so the second
4653
+ * visit is an O(1) no-op. `incidence` must describe the SAME `links`
4654
+ * buffer (edge slot i ↔ links[2i]/[2i+1]).
4655
+ */
4656
+ applyNodeCascadeToEdgesDelta(links, incidence, crossedNodes) {
4657
+ if (crossedNodes.length === 0) return;
4658
+ const hide = this.nodeHideLane.counters;
4659
+ this.cascadeSource ??= this.acquire("\xA79.1 node\u2192edge cascade");
4660
+ const nowFailing = [];
4661
+ const nowClear = [];
4662
+ for (let k = 0; k < crossedNodes.length; k++) {
4663
+ const edges = incidentEdgesOf(incidence, crossedNodes[k]);
4664
+ for (let j = 0; j < edges.length; j++) {
4665
+ const edge = edges[j];
4666
+ this.statsBox.cascadeEdgesVisited += 1;
4667
+ const failing = hide[links[edge * 2]] !== 0 || hide[links[edge * 2 + 1]] !== 0;
4668
+ if (failing) nowFailing.push(edge);
4669
+ else nowClear.push(edge);
4670
+ }
4671
+ }
4672
+ this.cascadeSource.updateEdgeFailures(nowFailing, nowClear);
4673
+ }
3836
4674
  /**
3837
4675
  * Drains the zero-crossing dirty lists accumulated since the previous
3838
4676
  * drain. Only NET flips are emitted (state compared against the previous
@@ -3848,6 +4686,27 @@ var SoftMask = class {
3848
4686
  edgeVisibleCount: this.edgeHideLane.zeroCount
3849
4687
  };
3850
4688
  }
4689
+ /** §17 telemetry: estimated bytes of mask storage held (S13-T07):
4690
+ * four counter lanes (+pending trackers) and per-source flag columns. */
4691
+ estimatedBytes() {
4692
+ let bytes = 0;
4693
+ for (const lane of [this.nodeHideLane, this.nodeDimLane, this.edgeHideLane, this.edgeDimLane]) {
4694
+ bytes += lane.counters.byteLength + lane.pending.byteLength;
4695
+ }
4696
+ for (const src of this.sources) {
4697
+ bytes += src.nodeHide.flags.byteLength + src.nodeDim.flags.byteLength + src.edgeHide.flags.byteLength + src.edgeDim.flags.byteLength;
4698
+ }
4699
+ return bytes;
4700
+ }
4701
+ /** §17 O(Δ) op counters (live object — snapshot before comparing). */
4702
+ get stats() {
4703
+ return this.statsBox;
4704
+ }
4705
+ resetStats() {
4706
+ this.statsBox.slotsVisited = 0;
4707
+ this.statsBox.zeroCrossings = 0;
4708
+ this.statsBox.cascadeEdgesVisited = 0;
4709
+ }
3851
4710
  visibleNodeCount() {
3852
4711
  return this.nodeHideLane.zeroCount;
3853
4712
  }
@@ -3912,15 +4771,62 @@ var SoftMask = class {
3912
4771
  const prev = mem.list;
3913
4772
  for (let i = 0; i < prev.length; i++) {
3914
4773
  const slot = prev[i];
3915
- if ((flags[slot] & 2) === 0) {
3916
- flags[slot] = 0;
3917
- this.decrement(lane, slot);
3918
- }
4774
+ const f = flags[slot];
4775
+ if ((f & 2) !== 0) continue;
4776
+ if ((f & 1) !== 0) this.decrement(lane, slot);
4777
+ flags[slot] = 0;
3919
4778
  }
3920
4779
  for (let i = 0; i < next.length; i++) flags[next[i]] = 1;
3921
4780
  mem.list = next;
4781
+ mem.holes = 0;
4782
+ }
4783
+ /**
4784
+ * O(Δ) delta ops on one lane membership (F10-02). Adds and removes are
4785
+ * idempotent per slot (adding a member / removing a non-member is a
4786
+ * no-op); removed slots leave HOLES in `list` (compacted past 50%), so
4787
+ * replace/clear passes must honor the bit0 guard above. `crossings`, when
4788
+ * given, is cleared and then receives this CALL's hide zero-crossings.
4789
+ */
4790
+ applyMembershipDelta(lane, mem, add, remove, crossings) {
4791
+ if (crossings !== void 0) {
4792
+ crossings.becameFailing.length = 0;
4793
+ crossings.becameClear.length = 0;
4794
+ }
4795
+ const capacity = lane.counters.length;
4796
+ const flags = mem.flags;
4797
+ const check = (slot) => {
4798
+ if (!Number.isInteger(slot) || slot < 0 || slot >= capacity) {
4799
+ throw new RangeError(`SoftMask: ${lane.label} slot ${slot} out of range [0, ${capacity})`);
4800
+ }
4801
+ };
4802
+ if (add !== null) for (let i = 0; i < add.length; i++) check(add[i]);
4803
+ if (remove !== null) for (let i = 0; i < remove.length; i++) check(remove[i]);
4804
+ if (add !== null) {
4805
+ for (let i = 0; i < add.length; i++) {
4806
+ const slot = add[i];
4807
+ this.statsBox.slotsVisited += 1;
4808
+ if ((flags[slot] & 1) !== 0) continue;
4809
+ flags[slot] = 1;
4810
+ mem.list.push(slot);
4811
+ this.increment(lane, slot, crossings);
4812
+ }
4813
+ }
4814
+ if (remove !== null) {
4815
+ for (let i = 0; i < remove.length; i++) {
4816
+ const slot = remove[i];
4817
+ this.statsBox.slotsVisited += 1;
4818
+ if ((flags[slot] & 1) === 0) continue;
4819
+ flags[slot] = 0;
4820
+ mem.holes += 1;
4821
+ this.decrement(lane, slot, crossings);
4822
+ }
4823
+ }
4824
+ if (mem.holes > mem.list.length >> 1) {
4825
+ mem.list = mem.list.filter((slot) => (flags[slot] & 1) !== 0);
4826
+ mem.holes = 0;
4827
+ }
3922
4828
  }
3923
- increment(lane, slot) {
4829
+ increment(lane, slot, crossings) {
3924
4830
  this.totalHeld += 1;
3925
4831
  const before = lane.counters[slot];
3926
4832
  if (before === COUNTER_MAX) {
@@ -3930,16 +4836,20 @@ var SoftMask = class {
3930
4836
  lane.counters[slot] = before + 1;
3931
4837
  if (before === 0) {
3932
4838
  lane.zeroCount -= 1;
4839
+ this.statsBox.zeroCrossings += 1;
4840
+ crossings?.becameFailing.push(slot);
3933
4841
  this.markDirty(lane, slot, true);
3934
4842
  }
3935
4843
  }
3936
- decrement(lane, slot) {
4844
+ decrement(lane, slot, crossings) {
3937
4845
  this.totalHeld -= 1;
3938
4846
  const before = lane.counters[slot];
3939
4847
  if (before === 0) return;
3940
4848
  lane.counters[slot] = before - 1;
3941
4849
  if (before === 1) {
3942
4850
  lane.zeroCount += 1;
4851
+ this.statsBox.zeroCrossings += 1;
4852
+ crossings?.becameClear.push(slot);
3943
4853
  this.markDirty(lane, slot, false);
3944
4854
  }
3945
4855
  }
@@ -4355,7 +5265,15 @@ function upperBound(values, sorted, target) {
4355
5265
  }
4356
5266
  var TypedColumnCrossfilter = class {
4357
5267
  /** Test instrumentation; see CrossfilterStats. Reset with resetStats(). */
4358
- stats = { slotsWalked: 0, fullSorts: 0, permutationMerges: 0 };
5268
+ stats = {
5269
+ slotsWalked: 0,
5270
+ fullSorts: 0,
5271
+ permutationMerges: 0,
5272
+ binUpdates: 0,
5273
+ filteredRecomputes: 0
5274
+ };
5275
+ /** Count of dims whose filtered layer is live-maintained (F10-02). */
5276
+ liveDims = 0;
4359
5277
  dims = [];
4360
5278
  byKey = /* @__PURE__ */ new Map();
4361
5279
  specs = [];
@@ -4377,6 +5295,29 @@ var TypedColumnCrossfilter = class {
4377
5295
  this.stats.slotsWalked = 0;
4378
5296
  this.stats.fullSorts = 0;
4379
5297
  this.stats.permutationMerges = 0;
5298
+ this.stats.binUpdates = 0;
5299
+ this.stats.filteredRecomputes = 0;
5300
+ }
5301
+ /** §17 telemetry: estimated bytes of typed-column storage held (S13-T07).
5302
+ * Documented components: per-dim value/permutation/bin/code/pass arrays,
5303
+ * the global failure counter, and the external mask. */
5304
+ estimatedBytes() {
5305
+ let bytes = this.failCount.byteLength + (this.externalMask?.byteLength ?? 0);
5306
+ for (const d of this.dims) {
5307
+ bytes += d.pass.byteLength;
5308
+ if (d.kind === "categorical") {
5309
+ bytes += d.codes.byteLength;
5310
+ } else {
5311
+ bytes += d.values.byteLength + d.sorted.byteLength + d.slotBin.byteLength;
5312
+ }
5313
+ }
5314
+ return bytes;
5315
+ }
5316
+ /** F10-02 live-layer bookkeeping — the ONLY writer of `filteredLive`. */
5317
+ setFilteredLive(dim, live) {
5318
+ if (dim.filteredLive === live) return;
5319
+ dim.filteredLive = live;
5320
+ this.liveDims += live ? 1 : -1;
4380
5321
  }
4381
5322
  /**
4382
5323
  * (Re)initialize columns from scratch. Clears all brushes and the external
@@ -4426,7 +5367,9 @@ var TypedColumnCrossfilter = class {
4426
5367
  this.applyRangeTransition(dim, normalized.brush, delta);
4427
5368
  }
4428
5369
  dim.brush = normalized.brush;
4429
- for (const other of this.dims) if (other !== dim) other.filteredDirty = true;
5370
+ for (const other of this.dims) {
5371
+ if (other !== dim && !other.filteredLive) other.filteredDirty = true;
5372
+ }
4430
5373
  this.revision++;
4431
5374
  this.notify();
4432
5375
  return delta;
@@ -4452,7 +5395,10 @@ var TypedColumnCrossfilter = class {
4452
5395
  if (unchanged) return;
4453
5396
  this.externalMask = Uint8Array.from(passSlots);
4454
5397
  }
4455
- for (const d of this.dims) d.filteredDirty = true;
5398
+ for (const d of this.dims) {
5399
+ this.setFilteredLive(d, false);
5400
+ d.filteredDirty = true;
5401
+ }
4456
5402
  this.notify();
4457
5403
  }
4458
5404
  /**
@@ -4466,6 +5412,7 @@ var TypedColumnCrossfilter = class {
4466
5412
  if (dim.filteredDirty) {
4467
5413
  this.recomputeFiltered(dim);
4468
5414
  dim.filteredDirty = false;
5415
+ this.setFilteredLive(dim, true);
4469
5416
  }
4470
5417
  const bins = [];
4471
5418
  const categories = [];
@@ -4530,13 +5477,16 @@ var TypedColumnCrossfilter = class {
4530
5477
  this.externalMask = em;
4531
5478
  }
4532
5479
  this.n = newN;
5480
+ for (const dim of this.dims) {
5481
+ this.setFilteredLive(dim, false);
5482
+ dim.filteredDirty = true;
5483
+ }
4533
5484
  for (const dim of this.dims) {
4534
5485
  const pass = new Uint8Array(newN).fill(1);
4535
5486
  pass.set(dim.pass);
4536
5487
  dim.pass = pass;
4537
5488
  if (dim.kind === "categorical") this.appendCat(dim, newNodes, oldN);
4538
5489
  else this.appendRange(dim, newNodes, oldN);
4539
- dim.filteredDirty = true;
4540
5490
  }
4541
5491
  for (let s = oldN; s < newN; s++) {
4542
5492
  (this.failCount[s] === 0 ? delta.shown : delta.hidden).push(s);
@@ -4600,6 +5550,7 @@ var TypedColumnCrossfilter = class {
4600
5550
  return dim;
4601
5551
  }
4602
5552
  buildDims(nodes, specs) {
5553
+ this.liveDims = 0;
4603
5554
  const seen = /* @__PURE__ */ new Set();
4604
5555
  const dims = [];
4605
5556
  for (const spec of specs) {
@@ -4655,6 +5606,7 @@ var TypedColumnCrossfilter = class {
4655
5606
  invalidSlots,
4656
5607
  brush: null,
4657
5608
  filteredDirty: true,
5609
+ filteredLive: false,
4658
5610
  filteredBins: []
4659
5611
  };
4660
5612
  this.rebinRange(dim);
@@ -4696,6 +5648,7 @@ var TypedColumnCrossfilter = class {
4696
5648
  invalidSlots,
4697
5649
  brush: null,
4698
5650
  filteredDirty: true,
5651
+ filteredLive: false,
4699
5652
  filteredCats: []
4700
5653
  };
4701
5654
  }
@@ -4744,13 +5697,58 @@ var TypedColumnCrossfilter = class {
4744
5697
  const next = this.failCount[slot] - 1;
4745
5698
  this.failCount[slot] = next;
4746
5699
  if (next === 0) delta.shown.push(slot);
5700
+ if (this.liveDims > 0) this.maintainFiltered(dim, slot, next, 1);
4747
5701
  } else {
4748
5702
  if (dim.pass[slot] === 0) return;
4749
5703
  dim.pass[slot] = 0;
4750
5704
  const prev = this.failCount[slot];
4751
5705
  this.failCount[slot] = prev + 1;
4752
5706
  if (prev === 0) delta.hidden.push(slot);
5707
+ if (this.liveDims > 0) this.maintainFiltered(dim, slot, prev, -1);
5708
+ }
5709
+ }
5710
+ /**
5711
+ * F10-02 inline maintenance of LIVE filtered layers, dispatched from the
5712
+ * one place that knows the failCount transition. `boundary` is the
5713
+ * other-failures picture at the interesting side of the flip (after for
5714
+ * shown, before for hidden):
5715
+ * - 0 → the slot crossed the FULLY-VISIBLE boundary: every other live
5716
+ * layer counts it (own layers ignore the own-dim brush, so the brushed
5717
+ * dim's layer is provably unchanged by its own flip);
5718
+ * - 1 → exactly one OTHER dim still fails the slot: only that dim's
5719
+ * what-if-I-cleared-mine layer flips;
5720
+ * - ≥2 → no layer can change.
5721
+ * External-mask-excluded and hygiene-invalid slots contribute nothing
5722
+ * either way and are skipped.
5723
+ */
5724
+ maintainFiltered(brushed, slot, boundary, sign) {
5725
+ if (boundary > 1) return;
5726
+ const ext = this.externalMask;
5727
+ if (ext !== null && ext[slot] === 0) return;
5728
+ if (boundary === 0) {
5729
+ for (const d of this.dims) {
5730
+ if (d === brushed || !d.filteredLive) continue;
5731
+ this.adjustLayer(d, slot, sign);
5732
+ }
5733
+ return;
5734
+ }
5735
+ for (const d of this.dims) {
5736
+ if (d === brushed || d.pass[slot] !== 0) continue;
5737
+ if (d.filteredLive) this.adjustLayer(d, slot, sign);
5738
+ break;
5739
+ }
5740
+ }
5741
+ adjustLayer(d, slot, sign) {
5742
+ if (d.kind === "categorical") {
5743
+ const c = d.codes[slot];
5744
+ if (c < 0) return;
5745
+ d.filteredCats[c] = (d.filteredCats[c] ?? 0) + sign;
5746
+ } else {
5747
+ const b = d.slotBin[slot];
5748
+ if (b < 0) return;
5749
+ d.filteredBins[b] = (d.filteredBins[b] ?? 0) + sign;
4753
5750
  }
5751
+ this.stats.binUpdates += 1;
4754
5752
  }
4755
5753
  walkSorted(dim, from, to, nowPass, delta) {
4756
5754
  for (let i = from; i < to; i++) this.flip(dim, dim.sorted[i], nowPass, delta);
@@ -4919,6 +5917,7 @@ var TypedColumnCrossfilter = class {
4919
5917
  }
4920
5918
  // --- lazy filtered layer -----------------------------------------------------
4921
5919
  recomputeFiltered(dim) {
5920
+ this.stats.filteredRecomputes += 1;
4922
5921
  const ext = this.externalMask;
4923
5922
  const fc = this.failCount;
4924
5923
  if (dim.kind === "categorical") {
@@ -5250,7 +6249,8 @@ var GRAPH_THEME_DARK = Object.freeze({
5250
6249
  edgeDefault: "rgba(255,255,255,0.15)",
5251
6250
  labelFg: "#e6e9f0",
5252
6251
  accent: "#3b82f6",
5253
- mutedAlpha: 0.15
6252
+ mutedAlpha: 0.15,
6253
+ emphasisRing: "#7aa2f7"
5254
6254
  });
5255
6255
  var GRAPH_THEME_LIGHT = Object.freeze({
5256
6256
  background: "#ffffff",
@@ -5258,7 +6258,8 @@ var GRAPH_THEME_LIGHT = Object.freeze({
5258
6258
  edgeDefault: "rgba(15,23,42,0.18)",
5259
6259
  labelFg: "#0f172a",
5260
6260
  accent: "#2563eb",
5261
- mutedAlpha: 0.2
6261
+ mutedAlpha: 0.2,
6262
+ emphasisRing: "#2563eb"
5262
6263
  });
5263
6264
  function resolveTheme(input) {
5264
6265
  const base = input !== void 0 && input.base === "light" ? GRAPH_THEME_LIGHT : GRAPH_THEME_DARK;
@@ -5270,10 +6271,11 @@ function resolveTheme(input) {
5270
6271
  if (input.labelFg !== void 0) out.labelFg = input.labelFg;
5271
6272
  if (input.accent !== void 0) out.accent = input.accent;
5272
6273
  if (input.mutedAlpha !== void 0) out.mutedAlpha = input.mutedAlpha;
6274
+ if (input.emphasisRing !== void 0) out.emphasisRing = input.emphasisRing;
5273
6275
  return out;
5274
6276
  }
5275
6277
  function sameTheme(a, b) {
5276
- return a.background === b.background && a.nodeDefault === b.nodeDefault && a.edgeDefault === b.edgeDefault && a.labelFg === b.labelFg && a.accent === b.accent && a.mutedAlpha === b.mutedAlpha;
6278
+ return a.background === b.background && a.nodeDefault === b.nodeDefault && a.edgeDefault === b.edgeDefault && a.labelFg === b.labelFg && a.accent === b.accent && a.mutedAlpha === b.mutedAlpha && a.emphasisRing === b.emphasisRing;
5277
6279
  }
5278
6280
  var ZOOM_STEP = 1.5;
5279
6281
  var EMPTY_IDS = [];
@@ -5300,6 +6302,9 @@ var TIMELINE_COALESCE_WINDOW_MS = Number.MAX_SAFE_INTEGER;
5300
6302
  var DEV = Boolean(import.meta.env?.DEV);
5301
6303
  var nowMs = () => Date.now();
5302
6304
  var DEFAULT_RGBA = [0.66, 0.66, 0.66, 1];
6305
+ var PERF_SAMPLE_THROTTLE_MS = 1e3;
6306
+ var FRAME_PRESSURE_ENGAGE_MS = 40;
6307
+ var FRAME_PRESSURE_CLEAR_MS = 28;
5303
6308
  var LABEL_RERANK_THROTTLE_MS = 100;
5304
6309
  var SIM_HOT_REFRESH_MS = 500;
5305
6310
  function sameIds(a, b) {
@@ -5372,6 +6377,7 @@ function createGraphInstance(opts) {
5372
6377
  let overlayIdSeq = 0;
5373
6378
  let edgeIndexById = EMPTY_INDEX2;
5374
6379
  let adjacency = null;
6380
+ let sceneIncidence = null;
5375
6381
  let lastLinkWidths = null;
5376
6382
  let scopeSpec = null;
5377
6383
  const scopeExtraIds = /* @__PURE__ */ new Set();
@@ -5505,6 +6511,8 @@ function createGraphInstance(opts) {
5505
6511
  let theme = resolveTheme(void 0);
5506
6512
  let edgeArrows = false;
5507
6513
  let showLinks = true;
6514
+ let emphasisRingOn = true;
6515
+ let emphasizedNodeId = null;
5508
6516
  let nodeImage;
5509
6517
  let labelsConfig;
5510
6518
  const metricStore = new MetricStore();
@@ -5534,11 +6542,140 @@ function createGraphInstance(opts) {
5534
6542
  const positionSubs = /* @__PURE__ */ new Set();
5535
6543
  let currentPlacements = [];
5536
6544
  let labelPositionCache = null;
6545
+ let frameCadence = 0;
6546
+ let quiescenceAsserted = false;
6547
+ let quiescenceTimer = null;
5537
6548
  let lastHotRefreshMs = null;
5538
6549
  let rerankTimer = null;
5539
6550
  let lastOverloadCount = 0;
5540
6551
  const projectScratch = [0, 0];
5541
6552
  let dataDiags = [];
6553
+ let columnarDiags = [];
6554
+ const executionMode = opts.execution ?? "main";
6555
+ let workerLane = null;
6556
+ let workerDiags = [];
6557
+ let workerUnavailableReported = false;
6558
+ let pendingDeriveToken = 0;
6559
+ function workerEligible() {
6560
+ if (executionMode === "main") return false;
6561
+ if (workerLane === null) {
6562
+ workerLane = new WorkerLane({
6563
+ ...opts.workerFactory !== void 0 ? { factory: opts.workerFactory } : {},
6564
+ onUnavailable: (reason) => {
6565
+ if (workerUnavailableReported) return;
6566
+ workerUnavailableReported = true;
6567
+ workerDiags = [
6568
+ {
6569
+ code: "worker-unavailable",
6570
+ severity: executionMode === "worker" ? "error" : "info",
6571
+ count: 1,
6572
+ sampleIds: [],
6573
+ message: `worker lane unavailable (${reason}) \u2014 columnar acceptance runs on the main lane`
6574
+ }
6575
+ ];
6576
+ }
6577
+ });
6578
+ }
6579
+ return workerLane.ensureBooted();
6580
+ }
6581
+ function scheduleWorkerAcceptance(columnar, deferredMetrics) {
6582
+ const lane = workerLane;
6583
+ pendingDeriveToken += 1;
6584
+ const token = pendingDeriveToken;
6585
+ const payload = {
6586
+ nodeIdTable: encodeStringTable(columnar.nodes.ids.dictionary),
6587
+ nodeIdCodes: columnar.nodes.ids.codes.slice(),
6588
+ nodeCount: columnar.nodes.length,
6589
+ edgeIdTable: encodeStringTable(columnar.edges.ids.dictionary),
6590
+ edgeIdCodes: columnar.edges.ids.codes.slice(),
6591
+ edgeSource: columnar.edges.source.slice(),
6592
+ edgeTarget: columnar.edges.target.slice(),
6593
+ edgeCount: columnar.edges.length
6594
+ };
6595
+ const transfers = collectTransfers([
6596
+ payload.nodeIdTable.offsets,
6597
+ payload.nodeIdTable.bytes,
6598
+ payload.nodeIdCodes,
6599
+ payload.edgeIdTable.offsets,
6600
+ payload.edgeIdTable.bytes,
6601
+ payload.edgeIdCodes,
6602
+ payload.edgeSource,
6603
+ payload.edgeTarget
6604
+ ]);
6605
+ void lane.request(token, "scene", "derive-columnar", payload, transfers, "guaranteed", "derive").then((reply) => {
6606
+ if (destroyed || token !== pendingDeriveToken) return;
6607
+ if (reply.op !== "result") {
6608
+ workerDiags = [
6609
+ {
6610
+ code: "worker-unavailable",
6611
+ severity: "error",
6612
+ count: 1,
6613
+ sampleIds: [],
6614
+ message: `worker derive failed (${String(reply.payload.message)}) \u2014 snapshot dropped`
6615
+ }
6616
+ ];
6617
+ publish({ diagnostics: composeDiagnostics() });
6618
+ return;
6619
+ }
6620
+ const acceptance = reply.payload;
6621
+ acceptanceQueue.admit(() => {
6622
+ if (destroyed || token !== pendingDeriveToken) return;
6623
+ const mutated = acceptance.keepNodes.length !== columnar.nodes.length || acceptance.keepEdges.length !== columnar.edges.length || validateColumnarStructure(columnar).length > 0;
6624
+ if (mutated) {
6625
+ columnarDiags = [
6626
+ {
6627
+ code: "invalid-columnar-snapshot",
6628
+ severity: "error",
6629
+ count: 1,
6630
+ sampleIds: [],
6631
+ message: "columnar snapshot mutated while worker acceptance was pending \u2014 rejected whole (\xA75: source coordinates are immutable; publish a new sourceRevision)"
6632
+ }
6633
+ ];
6634
+ publish({ diagnostics: composeDiagnostics() });
6635
+ return;
6636
+ }
6637
+ const preAccepted = buildAcceptedFromColumnar(columnar, acceptance);
6638
+ applyHostUpdateInner(
6639
+ {
6640
+ data: {
6641
+ datasetKey: columnar.datasetKey,
6642
+ sourceRevision: columnar.sourceRevision,
6643
+ nodes: [],
6644
+ edges: []
6645
+ },
6646
+ ...deferredMetrics !== void 0 ? { metrics: deferredMetrics } : {}
6647
+ },
6648
+ preAccepted
6649
+ );
6650
+ if (columnar.bufferOwnership === "transfer") detachColumnarBuffers(columnar);
6651
+ });
6652
+ }).catch((err) => {
6653
+ if (destroyed || token !== pendingDeriveToken) return;
6654
+ const message = err instanceof Error ? err.message : String(err);
6655
+ if (message !== "worker-unavailable" && message !== "worker-failed") return;
6656
+ acceptanceQueue.admit(() => {
6657
+ if (destroyed || token !== pendingDeriveToken) return;
6658
+ if (validateColumnarStructure(columnar).length > 0) {
6659
+ columnarDiags = [
6660
+ {
6661
+ code: "invalid-columnar-snapshot",
6662
+ severity: "error",
6663
+ count: 1,
6664
+ sampleIds: [],
6665
+ message: "columnar snapshot mutated while worker acceptance was pending \u2014 rejected whole (\xA75: source coordinates are immutable; publish a new sourceRevision)"
6666
+ }
6667
+ ];
6668
+ publish({ diagnostics: composeDiagnostics() });
6669
+ return;
6670
+ }
6671
+ applyHostUpdateInner({
6672
+ data: materializeColumnarSnapshot(columnar),
6673
+ ...deferredMetrics !== void 0 ? { metrics: deferredMetrics } : {}
6674
+ });
6675
+ if (columnar.bufferOwnership === "transfer") detachColumnarBuffers(columnar);
6676
+ });
6677
+ });
6678
+ }
5542
6679
  let nodeColorDiags = [];
5543
6680
  let nodeSizeDiags = [];
5544
6681
  let linkColorDiags = [];
@@ -5602,6 +6739,46 @@ function createGraphInstance(opts) {
5602
6739
  }
5603
6740
  pendingHistoryDepths = null;
5604
6741
  store.setState((prev) => ({ ...prev, ...patch }));
6742
+ if (patch.visible !== void 0 || patch.nodeCount !== void 0) {
6743
+ evaluateLadderCounts();
6744
+ }
6745
+ }
6746
+ function evaluateLadderCounts() {
6747
+ if (destroyed) return;
6748
+ const vis = store.getState().visible;
6749
+ const events = degradeController.evaluateCounts({ nodes: vis.nodes, edges: vis.edges });
6750
+ for (const event of events) applyDegradeEvent(event);
6751
+ }
6752
+ function applyDegradeEvent(event) {
6753
+ if (event.step === "cap-dom-labels") {
6754
+ if (event.engaged && !capLabelsNudged) {
6755
+ capLabelsNudged = true;
6756
+ console.warn(
6757
+ "orbit: cap-dom-labels engaged \u2014 DOM labels are hard-capped at the label budget above limits.domLabelNodes visible nodes (\xA717). A GPU label lane (labelStrategy 'sdf') is the planned scale path (S13-T05, deferred)."
6758
+ );
6759
+ }
6760
+ scheduleViewportRerank();
6761
+ }
6762
+ if (event.step === "batch-histograms" && !event.engaged) {
6763
+ flushCrossfilterNotify();
6764
+ }
6765
+ if (event.step === "defer-images" && !event.engaged && imageRefsDeferred) {
6766
+ pushImageRefs();
6767
+ }
6768
+ if (event.step === "disable-transitions") {
6769
+ const eng = engineIfReady();
6770
+ if (eng !== null) {
6771
+ const revisions = { ...store.getState().revisions };
6772
+ revisions.render += 1;
6773
+ commitToEngine(eng, {
6774
+ revision: revisions.render,
6775
+ config: { transitionDurationMs: event.engaged ? 0 : null }
6776
+ });
6777
+ revisions.appliedRender = eng.appliedRevision();
6778
+ store.setState((prev) => ({ ...prev, revisions }));
6779
+ }
6780
+ }
6781
+ emit("degrade", event);
5605
6782
  }
5606
6783
  function emitInstanceError(detail, phase, cause) {
5607
6784
  engineDiags = [
@@ -6019,6 +7196,9 @@ function createGraphInstance(opts) {
6019
7196
  return k === void 0 ? void 0 : accepted.edges[k];
6020
7197
  }
6021
7198
  function applyEdgeHover(edge, transitionsOnly) {
7199
+ if (edge !== null && degradeController.isEngaged("defer-link-picking") && store.getState().simulationRunning) {
7200
+ return;
7201
+ }
6022
7202
  const edgeId = edge === null ? null : edge.id;
6023
7203
  const hover = store.getState().hover;
6024
7204
  const changed = hover.edgeId !== edgeId;
@@ -6061,6 +7241,8 @@ function createGraphInstance(opts) {
6061
7241
  function composeDiagnostics() {
6062
7242
  return [
6063
7243
  ...dataDiags,
7244
+ ...columnarDiags,
7245
+ ...workerDiags,
6064
7246
  ...ingestMergeDiags,
6065
7247
  ...ingestCommitDiags.map((e) => e.diag),
6066
7248
  ...nodeColorDiags,
@@ -6112,7 +7294,60 @@ function createGraphInstance(opts) {
6112
7294
  baseLinkColors = null;
6113
7295
  basePointColorsSynthesized = false;
6114
7296
  baseLinkColorsSynthesized = false;
6115
- }
7297
+ nodeAlphaComposer.reset();
7298
+ edgeAlphaComposer.reset();
7299
+ sceneIncidence = null;
7300
+ }
7301
+ const brushCrossings = { becameFailing: [], becameClear: [] };
7302
+ const brushSceneAdd = [];
7303
+ const brushSceneRemove = [];
7304
+ const nodeAlphaComposer = new IncrementalAlphaComposer();
7305
+ const edgeAlphaComposer = new IncrementalAlphaComposer();
7306
+ const PATCH_FULL_UPLOAD_RATIO = 0.5;
7307
+ function buildAlphaPatches(buf, slotLists, stride) {
7308
+ const slots = [];
7309
+ for (const list of slotLists) for (const slot of list) slots.push(slot);
7310
+ slots.sort((a, b) => a - b);
7311
+ const patches = [];
7312
+ let runStart = -1;
7313
+ let runEnd = -1;
7314
+ for (const slot of slots) {
7315
+ if (slot < runEnd) continue;
7316
+ if (slot === runEnd) {
7317
+ runEnd = slot + 1;
7318
+ continue;
7319
+ }
7320
+ if (runStart >= 0) {
7321
+ patches.push({
7322
+ start: runStart * stride,
7323
+ data: buf.subarray(runStart * stride, runEnd * stride)
7324
+ });
7325
+ }
7326
+ runStart = slot;
7327
+ runEnd = slot + 1;
7328
+ }
7329
+ if (runStart >= 0) {
7330
+ patches.push({
7331
+ start: runStart * stride,
7332
+ data: buf.subarray(runStart * stride, runEnd * stride)
7333
+ });
7334
+ }
7335
+ return patches;
7336
+ }
7337
+ const pressureSampler = new PressureSampler();
7338
+ const resolvedLimits = resolveScaleLimits(opts.limits);
7339
+ const degradeController = new DegradeController(resolvedLimits.limits, () => nowMs());
7340
+ let capLabelsNudged = false;
7341
+ let imageRefsDeferred = false;
7342
+ let lastCommitMs;
7343
+ let lastPerfSampleAt = -Infinity;
7344
+ const perfCounters = {
7345
+ brushSlotsTranslated: 0,
7346
+ fullBrushRefreshes: 0,
7347
+ fullCascades: 0,
7348
+ fullNodeRecomposes: 0,
7349
+ fullEdgeRecomposes: 0
7350
+ };
6116
7351
  function evaluateFilterMembership() {
6117
7352
  if (softMask === null || maskFilterSource === null) return;
6118
7353
  const model = sceneModel();
@@ -6190,6 +7425,7 @@ function createGraphInstance(opts) {
6190
7425
  }
6191
7426
  function refreshBrushMembership() {
6192
7427
  if (softMask === null || maskBrushSource === null || scene === null) return;
7428
+ perfCounters.fullBrushRefreshes += 1;
6193
7429
  const slots = [];
6194
7430
  for (const s of crossfilterHiddenBase) {
6195
7431
  const id = crossfilterRowIds[s];
@@ -6199,8 +7435,45 @@ function createGraphInstance(opts) {
6199
7435
  }
6200
7436
  maskBrushSource.setNodeFailures(slots, null);
6201
7437
  }
7438
+ function refreshBrushMembershipDelta(delta) {
7439
+ if (softMask === null || maskBrushSource === null || scene === null) return false;
7440
+ if (groupRewrite !== null) return false;
7441
+ const identity = accepted !== null && sceneModel() === accepted && crossfilterRowsRef === accepted.nodes;
7442
+ const sceneRef = scene;
7443
+ const translate = (baseSlots, out) => {
7444
+ out.length = 0;
7445
+ for (let i = 0; i < baseSlots.length; i++) {
7446
+ const s = baseSlots[i];
7447
+ perfCounters.brushSlotsTranslated += 1;
7448
+ if (identity) {
7449
+ if (s < sceneRef.count) out.push(s);
7450
+ continue;
7451
+ }
7452
+ const id = crossfilterRowIds[s];
7453
+ if (id === void 0) continue;
7454
+ const idx = sceneRef.indexById.get(id);
7455
+ if (idx !== void 0) out.push(idx);
7456
+ }
7457
+ };
7458
+ translate(delta.hidden, brushSceneAdd);
7459
+ translate(delta.shown, brushSceneRemove);
7460
+ maskBrushSource.updateNodeFailures(brushSceneAdd, brushSceneRemove, brushCrossings);
7461
+ return true;
7462
+ }
6202
7463
  function cascadeNodeMask() {
6203
- if (softMask !== null && scene !== null) softMask.applyNodeCascadeToEdges(scene.links);
7464
+ if (softMask !== null && scene !== null) {
7465
+ perfCounters.fullCascades += 1;
7466
+ softMask.applyNodeCascadeToEdges(scene.links);
7467
+ }
7468
+ }
7469
+ function cascadeNodeMaskDelta() {
7470
+ if (softMask === null || scene === null) return;
7471
+ const failing = brushCrossings.becameFailing;
7472
+ const cleared = brushCrossings.becameClear;
7473
+ if (failing.length === 0 && cleared.length === 0) return;
7474
+ sceneIncidence ??= buildIncidence(scene.links, scene.count);
7475
+ const crossed = failing.length === 0 ? cleared : cleared.length === 0 ? failing : [...failing, ...cleared];
7476
+ softMask.applyNodeCascadeToEdgesDelta(scene.links, sceneIncidence, crossed);
6204
7477
  }
6205
7478
  function refreshGroupMaskMembership(hidden) {
6206
7479
  if (softMask === null || maskGroupSource === null) return;
@@ -6289,6 +7562,8 @@ function createGraphInstance(opts) {
6289
7562
  refreshGroupMaskMembership(hidden);
6290
7563
  cascadeNodeMask();
6291
7564
  softMask.drainDirty();
7565
+ nodeAlphaComposer.reset();
7566
+ edgeAlphaComposer.reset();
6292
7567
  return filterDiags !== before;
6293
7568
  }
6294
7569
  function computeVisibleCounts() {
@@ -6332,6 +7607,7 @@ function createGraphInstance(opts) {
6332
7607
  }
6333
7608
  function composeNodeAlphaBuffer(base) {
6334
7609
  if (softMask === null || scene === null) return base;
7610
+ perfCounters.fullNodeRecomposes += 1;
6335
7611
  const out = new Float32Array(base);
6336
7612
  const n = scene.count;
6337
7613
  const dimAlpha = theme.mutedAlpha;
@@ -6343,6 +7619,7 @@ function createGraphInstance(opts) {
6343
7619
  }
6344
7620
  function composeEdgeAlphaBuffer(base) {
6345
7621
  if (softMask === null && pathDimEdges === null || scene === null) return base;
7622
+ perfCounters.fullEdgeRecomposes += 1;
6346
7623
  const out = new Float32Array(base);
6347
7624
  const n = scene.linkCount;
6348
7625
  const dimAlpha = theme.mutedAlpha;
@@ -6357,6 +7634,36 @@ function createGraphInstance(opts) {
6357
7634
  }
6358
7635
  return out;
6359
7636
  }
7637
+ function composeNodeAlphaIncremental(drain) {
7638
+ const base = basePointColorBuffer();
7639
+ if (softMask === null || scene === null) return base;
7640
+ const mask = softMask;
7641
+ const dimAlpha = theme.mutedAlpha;
7642
+ const alphaOf = (i) => mask.nodeAlpha(i, dimAlpha);
7643
+ const seeded = nodeAlphaComposer.ensureSeeded(base, scene.count, dimAlpha, null, alphaOf);
7644
+ if (seeded) {
7645
+ nodeAlphaComposer.note(drain.nodes);
7646
+ nodeAlphaComposer.note(drain.nodesAlpha);
7647
+ }
7648
+ return nodeAlphaComposer.nextBuffer(base, alphaOf);
7649
+ }
7650
+ function composeEdgeAlphaIncremental(drain) {
7651
+ const base = baseLinkColorBuffer();
7652
+ if (softMask === null || scene === null) return base;
7653
+ if (pathDimEdges !== null) {
7654
+ edgeAlphaComposer.reset();
7655
+ return composeEdgeAlphaBuffer(base);
7656
+ }
7657
+ const mask = softMask;
7658
+ const dimAlpha = theme.mutedAlpha;
7659
+ const alphaOf = (k) => mask.edgeAlpha(k, dimAlpha);
7660
+ const seeded = edgeAlphaComposer.ensureSeeded(base, scene.linkCount, dimAlpha, null, alphaOf);
7661
+ if (seeded) {
7662
+ edgeAlphaComposer.note(drain.edges);
7663
+ edgeAlphaComposer.note(drain.edgesAlpha);
7664
+ }
7665
+ return edgeAlphaComposer.nextBuffer(base, alphaOf);
7666
+ }
6360
7667
  function publishMaskFastPath(extraPatch, diagsChanged) {
6361
7668
  const prev = store.getState();
6362
7669
  const patch = { ...extraPatch };
@@ -6380,13 +7687,48 @@ function createGraphInstance(opts) {
6380
7687
  };
6381
7688
  const buffers = visDirty.nodeColor || visDirty.nodeSize ? { ...projectChannelBuffers(visDirty) ?? {} } : {};
6382
7689
  if (visDirty.nodeColor || visDirty.nodeSize) diagsChanged = true;
7690
+ const maskPhaseT0 = performance.now();
7691
+ const rangedChannels = session !== null && session.policy !== null ? session.policy.rangedChannels : null;
7692
+ const bufferPatches = {};
6383
7693
  if (nodesAffected && buffers.pointColor === void 0) {
6384
- buffers.pointColor = composeNodeAlphaBuffer(basePointColorBuffer());
7694
+ const reseedsBefore = nodeAlphaComposer.reseeds;
7695
+ const composed = composeNodeAlphaIncremental(drain);
7696
+ const changed = drain.nodes.length + drain.nodesAlpha.length;
7697
+ if (rangedChannels !== null && rangedChannels.has("pointColor") && nodeAlphaComposer.reseeds === reseedsBefore && changed < scene.count * PATCH_FULL_UPLOAD_RATIO) {
7698
+ bufferPatches.pointColor = buildAlphaPatches(
7699
+ composed,
7700
+ [drain.nodes, drain.nodesAlpha],
7701
+ 4
7702
+ );
7703
+ } else {
7704
+ buffers.pointColor = composed;
7705
+ }
6385
7706
  }
6386
7707
  if (edgesAffected && buffers.linkColor === void 0) {
6387
- buffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
7708
+ const reseedsBefore = edgeAlphaComposer.reseeds;
7709
+ const composed = composeEdgeAlphaIncremental(drain);
7710
+ const changed = drain.edges.length + drain.edgesAlpha.length;
7711
+ if (rangedChannels !== null && rangedChannels.has("linkColor") && edgeAlphaComposer.reseeds === reseedsBefore && changed < scene.links.length / 2 * PATCH_FULL_UPLOAD_RATIO) {
7712
+ bufferPatches.linkColor = buildAlphaPatches(
7713
+ composed,
7714
+ [drain.edges, drain.edgesAlpha],
7715
+ 4
7716
+ );
7717
+ } else {
7718
+ buffers.linkColor = composed;
7719
+ }
6388
7720
  }
6389
- commitToEngine(eng, { revision: revisions.render, buffers });
7721
+ const maskUploadT0 = performance.now();
7722
+ const maskCommit = { revision: revisions.render, buffers };
7723
+ if (Object.keys(bufferPatches).length > 0) maskCommit.bufferPatches = bufferPatches;
7724
+ commitToEngine(eng, maskCommit);
7725
+ lastCommitMs = {
7726
+ kind: "mask",
7727
+ validate: 0,
7728
+ derive: 0,
7729
+ project: maskUploadT0 - maskPhaseT0,
7730
+ upload: performance.now() - maskUploadT0
7731
+ };
6390
7732
  revisions.appliedRender = eng.appliedRevision();
6391
7733
  }
6392
7734
  patch.revisions = revisions;
@@ -6414,7 +7756,17 @@ function createGraphInstance(opts) {
6414
7756
  crossfilterHiddenBase.clear();
6415
7757
  }
6416
7758
  const crossfilterFacadeListeners = /* @__PURE__ */ new Set();
7759
+ let crossfilterNotifyPending = false;
6417
7760
  function notifyCrossfilterFacade() {
7761
+ if (degradeController.isEngaged("batch-histograms") && engineIfReady() !== null) {
7762
+ crossfilterNotifyPending = true;
7763
+ return;
7764
+ }
7765
+ for (const cb of crossfilterFacadeListeners) cb();
7766
+ }
7767
+ function flushCrossfilterNotify() {
7768
+ if (!crossfilterNotifyPending) return;
7769
+ crossfilterNotifyPending = false;
6418
7770
  for (const cb of crossfilterFacadeListeners) cb();
6419
7771
  }
6420
7772
  function buildCrossfilterEngine() {
@@ -6540,9 +7892,13 @@ function createGraphInstance(opts) {
6540
7892
  for (const s of delta.shown) crossfilterHiddenBase.delete(s);
6541
7893
  if (membershipChanged && scene !== null) {
6542
7894
  ensureMask();
6543
- refreshBrushMembership();
6544
- refreshGroupMaskMembership(store.getState().hiddenNodeIds);
6545
- cascadeNodeMask();
7895
+ if (refreshBrushMembershipDelta(delta)) {
7896
+ cascadeNodeMaskDelta();
7897
+ } else {
7898
+ refreshBrushMembership();
7899
+ refreshGroupMaskMembership(store.getState().hiddenNodeIds);
7900
+ cascadeNodeMask();
7901
+ }
6546
7902
  }
6547
7903
  publishMaskFastPath(extraPatch, false);
6548
7904
  }
@@ -6781,6 +8137,7 @@ function createGraphInstance(opts) {
6781
8137
  const result = reconcileScene(scopedAccepted ?? accepted);
6782
8138
  labelPositionCache = null;
6783
8139
  adjacency = null;
8140
+ sceneIncidence = null;
6784
8141
  structuralChange = result.structuralChange;
6785
8142
  positionChange = result.positionChange;
6786
8143
  }
@@ -6853,6 +8210,8 @@ function createGraphInstance(opts) {
6853
8210
  revisions.scope += 1;
6854
8211
  revisions.render += 1;
6855
8212
  if (eng !== null) {
8213
+ nodeAlphaComposer.reset();
8214
+ edgeAlphaComposer.reset();
6856
8215
  const buffers = {};
6857
8216
  if (nodesAffected) buffers.pointColor = composeNodeAlphaBuffer(basePointColorBuffer());
6858
8217
  if (edgesAffected) buffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
@@ -7047,9 +8406,10 @@ function createGraphInstance(opts) {
7047
8406
  if (eng !== null && scene !== null && labelModel !== null && cfg !== void 0 && cfg.enabled !== false) {
7048
8407
  const sceneRef = scene;
7049
8408
  const vp = store.getState().viewport ?? eng.getViewport() ?? { zoom: 1 };
8409
+ const hardCap = degradeController.isEngaged("cap-dom-labels") ? LABEL_MAX_VISIBLE_DEFAULT : LABEL_MAX_VISIBLE_CAP;
7050
8410
  const k = Math.max(
7051
8411
  0,
7052
- Math.min(Math.floor(cfg.maxVisible ?? LABEL_MAX_VISIBLE_DEFAULT), LABEL_MAX_VISIBLE_CAP)
8412
+ Math.min(Math.floor(cfg.maxVisible ?? LABEL_MAX_VISIBLE_DEFAULT), hardCap)
7053
8413
  );
7054
8414
  const clusterCandidates = clusterLabelCandidates(cfg, vp.zoom, k);
7055
8415
  const nodeLabelsSuppressed = cfg.maxZoom !== void 0 && vp.zoom <= cfg.maxZoom;
@@ -7518,7 +8878,8 @@ function createGraphInstance(opts) {
7518
8878
  defaultPointColor: theme.nodeDefault,
7519
8879
  defaultLinkColor: theme.edgeDefault,
7520
8880
  linkArrows: edgeArrows,
7521
- renderLinks: showLinks
8881
+ renderLinks: showLinks,
8882
+ emphasisRingColor: theme.emphasisRing
7522
8883
  };
7523
8884
  if (simulation !== void 0) c.simulation = simulation;
7524
8885
  if (clusterSpec !== null) {
@@ -7578,6 +8939,11 @@ function createGraphInstance(opts) {
7578
8939
  }
7579
8940
  function pushImageRefs() {
7580
8941
  if (session === null || session.policy === null || session.policy.images !== "native") return;
8942
+ if (degradeController.isEngaged("defer-images")) {
8943
+ imageRefsDeferred = true;
8944
+ return;
8945
+ }
8946
+ imageRefsDeferred = false;
7581
8947
  const model = sceneModel();
7582
8948
  if (model === null) return;
7583
8949
  if (nodeImage === void 0) {
@@ -7691,15 +9057,25 @@ function createGraphInstance(opts) {
7691
9057
  }
7692
9058
  eng.setPinnedIndices(indexSet.size > 0 ? [...indexSet] : null);
7693
9059
  }
9060
+ function applyEmphasis(eng, index) {
9061
+ if (!emphasisRingOn) return;
9062
+ eng.setFocusedIndex(index);
9063
+ }
7694
9064
  function reapplyInteractionState(eng) {
7695
9065
  const { selection, pins, pinnedNodeIds, hover } = store.getState();
7696
9066
  if (selection.nodeIds.length > 0 || selection.groupIds.length > 0) {
7697
9067
  pushSelectionToEngine(eng, selection.nodeIds);
7698
9068
  }
7699
9069
  if (pins.size > 0 || pinnedNodeIds.size > 0) pushPinsToEngine(eng, pins);
9070
+ if (emphasizedNodeId !== null && scene !== null && !scene.indexById.has(emphasizedNodeId)) {
9071
+ emphasizedNodeId = null;
9072
+ }
7700
9073
  if (hover.nodeId !== null && scene !== null) {
7701
9074
  const idx = scene.indexById.get(hover.nodeId);
7702
- if (idx !== void 0) eng.setFocusedIndex(idx);
9075
+ if (idx !== void 0) applyEmphasis(eng, idx);
9076
+ } else if (emphasizedNodeId !== null && scene !== null) {
9077
+ const idx = scene.indexById.get(emphasizedNodeId);
9078
+ if (idx !== void 0) applyEmphasis(eng, idx);
7703
9079
  }
7704
9080
  }
7705
9081
  function maybeFitView(eng) {
@@ -8441,6 +9817,7 @@ function createGraphInstance(opts) {
8441
9817
  invalidateExpansionRecords();
8442
9818
  edgeIndexById = new Map(p.merged.edges.map((e, k) => [e.id, k]));
8443
9819
  adjacency = null;
9820
+ sceneIncidence = null;
8444
9821
  acceptedAdjacency = null;
8445
9822
  acceptedModelSeq += 1;
8446
9823
  scopedAccepted = computeScopedAccepted();
@@ -8610,9 +9987,15 @@ function createGraphInstance(opts) {
8610
9987
  if (pinsChanged || pinnedChanged || structuralChange && (nextPins.size > 0 || nextPinned.size > 0)) {
8611
9988
  pushPinsToEngine(eng, nextPins);
8612
9989
  }
9990
+ if (structuralChange && emphasizedNodeId !== null && !ingestScene.indexById.has(emphasizedNodeId)) {
9991
+ emphasizedNodeId = null;
9992
+ }
8613
9993
  if (structuralChange && !hoverCleared && prev.hover.nodeId !== null) {
8614
9994
  const idx = ingestScene.indexById.get(prev.hover.nodeId);
8615
- if (idx !== void 0) eng.setFocusedIndex(idx);
9995
+ if (idx !== void 0) applyEmphasis(eng, idx);
9996
+ } else if (structuralChange && emphasizedNodeId !== null) {
9997
+ const idx = ingestScene.indexById.get(emphasizedNodeId);
9998
+ if (idx !== void 0) applyEmphasis(eng, idx);
8616
9999
  }
8617
10000
  if (commitNeeded) maybeFitView(eng);
8618
10001
  }
@@ -9373,6 +10756,7 @@ function createGraphInstance(opts) {
9373
10756
  const result = reconcileScene(scopedAccepted ?? accepted);
9374
10757
  labelPositionCache = null;
9375
10758
  adjacency = null;
10759
+ sceneIncidence = null;
9376
10760
  hardScopeGen += 1;
9377
10761
  visibleGen += 1;
9378
10762
  const filterDiagsChanged = rebuildMaskMemberships(prev.hiddenNodeIds);
@@ -9728,10 +11112,14 @@ function createGraphInstance(opts) {
9728
11112
  checkRestoreAcknowledgement(update);
9729
11113
  });
9730
11114
  }
9731
- function applyHostUpdateInner(update) {
11115
+ function applyHostUpdateInner(update, preAccepted) {
11116
+ const phaseT0 = performance.now();
11117
+ let phaseProjectStart = phaseT0;
9732
11118
  const issuedModelSeq = acceptedModelSeq;
9733
11119
  let searchIndexRejected = false;
9734
11120
  let crossfilterRejected = false;
11121
+ let columnarRejected = false;
11122
+ let metricsDeferredToWorker = false;
9735
11123
  const diagnosticsForced = pendingDiagnosticsRefresh;
9736
11124
  pendingDiagnosticsRefresh = false;
9737
11125
  if (update.filter !== void 0 && update.filter !== null) {
@@ -9772,11 +11160,42 @@ function createGraphInstance(opts) {
9772
11160
  let positionChange = false;
9773
11161
  let datasetKeyChanged = false;
9774
11162
  let overlayIdsCleared = false;
9775
- const data = update.data;
11163
+ let data;
11164
+ if (update.data !== void 0 && isColumnarSnapshot(update.data)) {
11165
+ const columnar = update.data;
11166
+ const isColumnarReplay = baseAccepted !== null && baseAccepted.datasetKey === columnar.datasetKey && baseAccepted.sourceRevision === columnar.sourceRevision;
11167
+ if (isColumnarReplay) {
11168
+ data = void 0;
11169
+ } else {
11170
+ const issues = validateColumnarStructure(columnar);
11171
+ if (issues.length > 0) {
11172
+ columnarDiags = [
11173
+ {
11174
+ code: "invalid-columnar-snapshot",
11175
+ severity: "error",
11176
+ count: issues.length,
11177
+ sampleIds: issues.slice(0, DIAGNOSTIC_SAMPLE_CAP).map((i) => i.where),
11178
+ message: `columnar snapshot rejected whole (\xA75.1): ${issues[0].where} \u2014 ${issues[0].detail}`
11179
+ }
11180
+ ];
11181
+ columnarRejected = true;
11182
+ data = void 0;
11183
+ } else if (workerEligible()) {
11184
+ scheduleWorkerAcceptance(columnar, update.metrics);
11185
+ metricsDeferredToWorker = true;
11186
+ data = void 0;
11187
+ } else {
11188
+ data = materializeColumnarSnapshot(columnar);
11189
+ if (columnar.bufferOwnership === "transfer") detachColumnarBuffers(columnar);
11190
+ }
11191
+ }
11192
+ } else {
11193
+ data = update.data;
11194
+ }
9776
11195
  if (data !== void 0) {
9777
11196
  const isReplay = baseAccepted !== null && baseAccepted.datasetKey === data.datasetKey && baseAccepted.sourceRevision === data.sourceRevision;
9778
11197
  if (!isReplay) {
9779
- const nextAccepted = validateSnapshot(data);
11198
+ const nextAccepted = preAccepted ?? validateSnapshot(data);
9780
11199
  abortAllSessions(null, "replaced: a declarative snapshot was applied (\xA77.5)");
9781
11200
  overlayIdsCleared = clearOverlayState();
9782
11201
  datasetKeyChanged = baseAccepted !== null && baseAccepted.datasetKey !== data.datasetKey;
@@ -9809,8 +11228,11 @@ function createGraphInstance(opts) {
9809
11228
  baseSource = "declarative";
9810
11229
  edgeIndexById = new Map(nextAccepted.edges.map((e, k) => [e.id, k]));
9811
11230
  adjacency = null;
11231
+ sceneIncidence = null;
9812
11232
  acceptedAdjacency = null;
9813
11233
  dataDiags = nextAccepted.diagnostics;
11234
+ columnarDiags = [];
11235
+ pendingDeriveToken += 1;
9814
11236
  dataChanged = true;
9815
11237
  }
9816
11238
  }
@@ -9953,6 +11375,7 @@ function createGraphInstance(opts) {
9953
11375
  const result = reconcileScene(scopedAccepted ?? accepted);
9954
11376
  labelPositionCache = null;
9955
11377
  adjacency = null;
11378
+ sceneIncidence = null;
9956
11379
  structuralChange = result.structuralChange;
9957
11380
  positionChange = result.positionChange;
9958
11381
  }
@@ -9966,6 +11389,7 @@ function createGraphInstance(opts) {
9966
11389
  const result = reconcileScene(scopedAccepted ?? accepted);
9967
11390
  labelPositionCache = null;
9968
11391
  adjacency = null;
11392
+ sceneIncidence = null;
9969
11393
  structuralChange = structuralChange || result.structuralChange;
9970
11394
  positionChange = positionChange || result.positionChange;
9971
11395
  }
@@ -10003,7 +11427,7 @@ function createGraphInstance(opts) {
10003
11427
  if (clusterDiags !== clusterDiagsBefore) groupsDiagsChanged = true;
10004
11428
  let metricsAdmitted = null;
10005
11429
  let metricsProcessed = false;
10006
- if (update.metrics !== void 0 && update.metrics.length > 0) {
11430
+ if (!metricsDeferredToWorker && update.metrics !== void 0 && update.metrics.length > 0) {
10007
11431
  metricsProcessed = true;
10008
11432
  if (accepted === null || !ensureMetricModel()) {
10009
11433
  metricDiags = [
@@ -10068,6 +11492,7 @@ function createGraphInstance(opts) {
10068
11492
  let configPatch;
10069
11493
  let themeChanged = false;
10070
11494
  let mutedAlphaChanged = false;
11495
+ let emphasisToggled = false;
10071
11496
  let nodeBaseInvalidated = false;
10072
11497
  let linkBaseInvalidated = false;
10073
11498
  {
@@ -10102,6 +11527,10 @@ function createGraphInstance(opts) {
10102
11527
  linkBaseInvalidated = true;
10103
11528
  }
10104
11529
  }
11530
+ if (nextTheme.emphasisRing !== theme.emphasisRing) {
11531
+ c.emphasisRingColor = nextTheme.emphasisRing;
11532
+ any = true;
11533
+ }
10105
11534
  mutedAlphaChanged = nextTheme.mutedAlpha !== theme.mutedAlpha;
10106
11535
  if (groupRewrite !== null && (nextTheme.accent !== theme.accent || nextTheme.nodeDefault !== theme.nodeDefault)) {
10107
11536
  dirtyNodeColor = true;
@@ -10124,6 +11553,10 @@ function createGraphInstance(opts) {
10124
11553
  c.renderLinks = showLinks;
10125
11554
  any = true;
10126
11555
  }
11556
+ if (update.emphasisRing !== void 0 && update.emphasisRing !== emphasisRingOn) {
11557
+ emphasisRingOn = update.emphasisRing;
11558
+ emphasisToggled = true;
11559
+ }
10127
11560
  if (clusterConfigDirty) {
10128
11561
  c.cluster = clusterConfigPayload();
10129
11562
  any = true;
@@ -10325,6 +11758,8 @@ function createGraphInstance(opts) {
10325
11758
  const edgesAffected = drain.edges.length > 0 || drain.edgesAlpha.length > 0;
10326
11759
  maskAffected = nodesAffected || edgesAffected;
10327
11760
  if (maskAffected) {
11761
+ nodeAlphaComposer.reset();
11762
+ edgeAlphaComposer.reset();
10328
11763
  maskBuffers = {};
10329
11764
  if (nodesAffected) maskBuffers.pointColor = composeNodeAlphaBuffer(basePointColorBuffer());
10330
11765
  if (edgesAffected) maskBuffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
@@ -10344,6 +11779,7 @@ function createGraphInstance(opts) {
10344
11779
  maskBuffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
10345
11780
  }
10346
11781
  }
11782
+ phaseProjectStart = performance.now();
10347
11783
  let buffers = projectChannelBuffers({
10348
11784
  nodeColor: dirtyNodeColor,
10349
11785
  nodeSize: dirtyNodeSize,
@@ -10393,7 +11829,15 @@ function createGraphInstance(opts) {
10393
11829
  simRestarted = true;
10394
11830
  }
10395
11831
  }
11832
+ const phaseUploadStart = performance.now();
10396
11833
  commitToEngine(eng, commit);
11834
+ lastCommitMs = {
11835
+ kind: structure !== void 0 ? scopeChanged && !dataChanged ? "scope" : "model" : buffers !== void 0 ? "mask" : "config",
11836
+ validate: 0,
11837
+ derive: phaseProjectStart - phaseT0,
11838
+ project: phaseUploadStart - phaseProjectStart,
11839
+ upload: performance.now() - phaseUploadStart
11840
+ };
10397
11841
  revisions.appliedRender = eng.appliedRevision();
10398
11842
  const facade = session !== null ? session.edgePicking : null;
10399
11843
  if (facade !== null) {
@@ -10433,7 +11877,7 @@ function createGraphInstance(opts) {
10433
11877
  patch.timeline = { playingKey: null };
10434
11878
  changed = true;
10435
11879
  }
10436
- if (dataChanged || datasetKeyChanged || buffers !== void 0 || labelRerank.diagsChanged || filterDiagsChanged || metricsProcessed || searchIndexRejected || crossfilterRejected || parallelRejected || groupsDiagsChanged || diagnosticsForced) {
11880
+ if (dataChanged || datasetKeyChanged || buffers !== void 0 || labelRerank.diagsChanged || filterDiagsChanged || metricsProcessed || searchIndexRejected || crossfilterRejected || columnarRejected || parallelRejected || groupsDiagsChanged || diagnosticsForced) {
10437
11881
  patch.diagnostics = composeDiagnostics();
10438
11882
  changed = true;
10439
11883
  }
@@ -10502,9 +11946,26 @@ function createGraphInstance(opts) {
10502
11946
  if (pinsChanged || pinnedChanged || structuralChange && (nextPins.size > 0 || nextPinned.size > 0)) {
10503
11947
  pushPinsToEngine(eng, nextPins);
10504
11948
  }
11949
+ if (structuralChange && emphasizedNodeId !== null && scene !== null && !scene.indexById.has(emphasizedNodeId)) {
11950
+ emphasizedNodeId = null;
11951
+ }
10505
11952
  if (structuralChange && !hoverCleared && prev.hover.nodeId !== null && scene !== null) {
10506
11953
  const idx = scene.indexById.get(prev.hover.nodeId);
10507
- if (idx !== void 0) eng.setFocusedIndex(idx);
11954
+ if (idx !== void 0) applyEmphasis(eng, idx);
11955
+ } else if (structuralChange && emphasizedNodeId !== null && scene !== null) {
11956
+ const idx = scene.indexById.get(emphasizedNodeId);
11957
+ if (idx !== void 0) applyEmphasis(eng, idx);
11958
+ }
11959
+ if (emphasisToggled) {
11960
+ if (!emphasisRingOn) {
11961
+ eng.setFocusedIndex(null);
11962
+ } else if (!hoverCleared && prev.hover.nodeId !== null && scene !== null) {
11963
+ const idx = scene.indexById.get(prev.hover.nodeId);
11964
+ if (idx !== void 0) applyEmphasis(eng, idx);
11965
+ } else if (emphasizedNodeId !== null && scene !== null) {
11966
+ const idx = scene.indexById.get(emphasizedNodeId);
11967
+ if (idx !== void 0) applyEmphasis(eng, idx);
11968
+ }
10508
11969
  }
10509
11970
  if (commitNeeded) maybeFitView(eng);
10510
11971
  }
@@ -10554,7 +12015,8 @@ function createGraphInstance(opts) {
10554
12015
  const nodeId = node === null ? null : node.id;
10555
12016
  const hover = store.getState().hover;
10556
12017
  if (hover.nodeId !== nodeId) publish({ hover: { nodeId, edgeId: hover.edgeId } });
10557
- s.engine.setFocusedIndex(node === null ? null : index);
12018
+ emphasizedNodeId = null;
12019
+ applyEmphasis(s.engine, node === null ? null : index);
10558
12020
  emit("nodeHover", { node });
10559
12021
  },
10560
12022
  // §13 native-route edge events (§7.4 mapping). Host onLink* events are
@@ -10575,6 +12037,9 @@ function createGraphInstance(opts) {
10575
12037
  emit("edgeClick", { edge });
10576
12038
  },
10577
12039
  onLinkHover(linkIndex) {
12040
+ if (linkIndex !== null && degradeController.isEngaged("defer-link-picking") && store.getState().simulationRunning) {
12041
+ return;
12042
+ }
10578
12043
  if (!active()) return;
10579
12044
  if (linkIndex !== null && !maskEdgeVisibleAt(linkIndex)) linkIndex = null;
10580
12045
  const edge = linkIndex === null ? null : edgeAtLinkIndex(linkIndex) ?? null;
@@ -10616,6 +12081,32 @@ function createGraphInstance(opts) {
10616
12081
  */
10617
12082
  onFrame(timeMs) {
10618
12083
  if (!active()) return;
12084
+ frameCadence += 1;
12085
+ pressureSampler.noteFrame(timeMs, !store.getState().simulationRunning);
12086
+ flushCrossfilterNotify();
12087
+ if (timeMs - lastPerfSampleAt >= PERF_SAMPLE_THROTTLE_MS) {
12088
+ lastPerfSampleAt = timeMs;
12089
+ const ewma = pressureSampler.snapshot().frameEwmaMs;
12090
+ if (Number.isFinite(ewma)) {
12091
+ const vis = store.getState().visible;
12092
+ const visible = { nodes: vis.nodes, edges: vis.edges };
12093
+ if (ewma > FRAME_PRESSURE_ENGAGE_MS) {
12094
+ for (const step of ["cap-dom-labels", "defer-link-picking"]) {
12095
+ const event = degradeController.engageForPressure(step, "frame-pressure", visible);
12096
+ if (event !== null) applyDegradeEvent(event);
12097
+ }
12098
+ } else if (ewma < FRAME_PRESSURE_CLEAR_MS) {
12099
+ for (const step of ["cap-dom-labels", "defer-link-picking"]) {
12100
+ const event = degradeController.clearPressure(step, visible);
12101
+ if (event !== null) applyDegradeEvent(event);
12102
+ }
12103
+ }
12104
+ }
12105
+ if ((listeners.get("perfSample")?.size ?? 0) > 0) {
12106
+ emit("perfSample", getPerfSnapshot());
12107
+ }
12108
+ pressureSampler.resetCounters();
12109
+ }
10619
12110
  if (store.getState().simulationRunning && labelsEnabled()) {
10620
12111
  if (lastHotRefreshMs === null || timeMs - lastHotRefreshMs >= SIM_HOT_REFRESH_MS || timeMs < lastHotRefreshMs) {
10621
12112
  lastHotRefreshMs = timeMs;
@@ -10662,6 +12153,7 @@ function createGraphInstance(opts) {
10662
12153
  notifyLabelSubs(positionSubs);
10663
12154
  }
10664
12155
  emit("simulationEnd", {});
12156
+ armQuiescenceAssertion();
10665
12157
  },
10666
12158
  onError(error) {
10667
12159
  if (!active()) return;
@@ -10739,11 +12231,13 @@ function createGraphInstance(opts) {
10739
12231
  }
10740
12232
  function buildAndCommitFullReplay(s, restartAlpha) {
10741
12233
  const eng = s.engine;
12234
+ const phaseT0 = performance.now();
10742
12235
  const renderRevision = store.getState().revisions.render;
10743
12236
  const replayModel = renderModel();
10744
12237
  if (replayModel !== null) {
10745
12238
  reconcileScene(replayModel);
10746
12239
  const replayScene = scene;
12240
+ const phaseProjectStart = performance.now();
10747
12241
  const commit = {
10748
12242
  revision: renderRevision,
10749
12243
  structure: {
@@ -10767,7 +12261,15 @@ function createGraphInstance(opts) {
10767
12261
  commit.resources = { ...commit.resources ?? {}, pointImageIndex: syncIndex };
10768
12262
  }
10769
12263
  if (restartAlpha !== null) commit.restart = { alpha: restartAlpha };
12264
+ const phaseUploadStart = performance.now();
10770
12265
  commitToEngine(eng, commit);
12266
+ lastCommitMs = {
12267
+ kind: "model",
12268
+ validate: 0,
12269
+ derive: phaseProjectStart - phaseT0,
12270
+ project: phaseUploadStart - phaseProjectStart,
12271
+ upload: performance.now() - phaseUploadStart
12272
+ };
10771
12273
  if (s.edgePicking !== null) {
10772
12274
  if (restartAlpha !== null) s.edgePicking.disarm();
10773
12275
  else s.edgePicking.arm(replayScene.positions, replayScene.links);
@@ -10804,6 +12306,7 @@ function createGraphInstance(opts) {
10804
12306
  const restartAlpha = layout === "force" ? 1 : null;
10805
12307
  const committed = buildAndCommitFullReplay(s, restartAlpha);
10806
12308
  const restarted = committed && accepted !== null && restartAlpha !== null;
12309
+ if (layout === "fixed") eng.pause();
10807
12310
  publish({
10808
12311
  status: "ready",
10809
12312
  revisions: { ...store.getState().revisions, appliedRender: eng.appliedRevision() },
@@ -10859,6 +12362,11 @@ function createGraphInstance(opts) {
10859
12362
  session = null;
10860
12363
  mountPromise = null;
10861
12364
  cancelRerankTimer();
12365
+ flushCrossfilterNotify();
12366
+ if (quiescenceTimer !== null) {
12367
+ clearTimeout(quiescenceTimer);
12368
+ quiescenceTimer = null;
12369
+ }
10862
12370
  const timelinePatch = resetTimelineForPatch();
10863
12371
  s.edgePicking?.destroy();
10864
12372
  s.edgePicking = null;
@@ -10871,10 +12379,48 @@ function createGraphInstance(opts) {
10871
12379
  });
10872
12380
  rerankAndNotify();
10873
12381
  }
12382
+ function armQuiescenceAssertion() {
12383
+ if (quiescenceAsserted || quiescenceTimer !== null || destroyed) return;
12384
+ const env = globalThis.process?.env?.NODE_ENV;
12385
+ if (env !== "development") return;
12386
+ const g = globalThis;
12387
+ if (typeof g.requestAnimationFrame !== "function") return;
12388
+ const armedRender = store.getState().revisions.render;
12389
+ quiescenceTimer = setTimeout(() => {
12390
+ quiescenceTimer = null;
12391
+ const state = store.getState();
12392
+ if (destroyed || state.simulationRunning || state.revisions.render !== armedRender) {
12393
+ return;
12394
+ }
12395
+ quiescenceAsserted = true;
12396
+ const native = g.requestAnimationFrame;
12397
+ if (typeof native !== "function") return;
12398
+ let registrations = 0;
12399
+ const wrapper = (cb) => {
12400
+ registrations += 1;
12401
+ return native.call(globalThis, cb);
12402
+ };
12403
+ g.requestAnimationFrame = wrapper;
12404
+ setTimeout(() => {
12405
+ if (g.requestAnimationFrame === wrapper) g.requestAnimationFrame = native;
12406
+ if (registrations > 0 && !destroyed) {
12407
+ console.warn(
12408
+ `orbit: ${registrations} requestAnimationFrame registration(s) observed over 500ms while this instance is quiescent (\xA717/S13-T06 one-frame-loop). If no app-owned animation is running, a second frame loop is leaking.`
12409
+ );
12410
+ }
12411
+ }, 500);
12412
+ }, 2e3);
12413
+ }
10874
12414
  function destroy() {
10875
12415
  if (destroyed) return;
10876
12416
  destroyed = true;
12417
+ workerLane?.terminate();
12418
+ workerLane = null;
10877
12419
  stopTimelineTimer();
12420
+ if (quiescenceTimer !== null) {
12421
+ clearTimeout(quiescenceTimer);
12422
+ quiescenceTimer = null;
12423
+ }
10878
12424
  timelinePlayingKey = null;
10879
12425
  imagePipeline?.dispose();
10880
12426
  imagePipeline = null;
@@ -11251,6 +12797,9 @@ function createGraphInstance(opts) {
11251
12797
  } else if (wantsManualGroups && !groupsControlled) {
11252
12798
  commitManualGroups(manualGroups.length > 0 ? manualGroups : null);
11253
12799
  }
12800
+ if (state.layout.kind !== layout) {
12801
+ applyHostUpdateInner({ layout: state.layout.kind });
12802
+ }
11254
12803
  if (state.styling !== void 0 && !needsHost) {
11255
12804
  const patch = {};
11256
12805
  if (state.styling.showLinks !== void 0) patch.showLinks = state.styling.showLinks;
@@ -11513,14 +13062,17 @@ function createGraphInstance(opts) {
11513
13062
  }
11514
13063
  function captureExportPin() {
11515
13064
  if (accepted === null) return null;
11516
- return { accepted, scene, mask: softMask };
13065
+ let visibleIds = null;
13066
+ if (scene !== null) {
13067
+ const slots = visibleSlotsOf(scene, softMask);
13068
+ const ids = /* @__PURE__ */ new Set();
13069
+ for (const i of slots) ids.add(scene.idByIndex[i]);
13070
+ visibleIds = ids;
13071
+ }
13072
+ return { accepted, scene, visibleIds };
11517
13073
  }
11518
13074
  function pinnedVisibleIds(pin) {
11519
- if (pin.scene === null) return null;
11520
- const slots = visibleSlotsOf(pin.scene, pin.mask);
11521
- const ids = /* @__PURE__ */ new Set();
11522
- for (const i of slots) ids.add(pin.scene.idByIndex[i]);
11523
- return ids;
13075
+ return pin.visibleIds;
11524
13076
  }
11525
13077
  function pinnedRowCount(pin, scope) {
11526
13078
  if (scope === "accepted") return pin.accepted.nodes.length + pin.accepted.edges.length;
@@ -11622,7 +13174,7 @@ function createGraphInstance(opts) {
11622
13174
  if (eng === null || scene === null) return EMPTY_IDS;
11623
13175
  const idx = scene.indexById.get(id);
11624
13176
  if (idx === void 0) return EMPTY_IDS;
11625
- eng.setFocusedIndex(idx);
13177
+ applyEmphasis(eng, idx);
11626
13178
  if (effectiveReducedMotion()) eng.zoomToIndex?.(idx, 0);
11627
13179
  else eng.zoomToIndex?.(idx);
11628
13180
  const neighbors = neighborIdsOf(id);
@@ -11637,6 +13189,67 @@ function createGraphInstance(opts) {
11637
13189
  }
11638
13190
  return neighbors;
11639
13191
  }
13192
+ function emphasizeNode(id) {
13193
+ const eng = engineIfReady();
13194
+ if (eng === null) return;
13195
+ if (id === null) {
13196
+ emphasizedNodeId = null;
13197
+ applyEmphasis(eng, null);
13198
+ return;
13199
+ }
13200
+ if (scene === null) return;
13201
+ const idx = scene.indexById.get(id);
13202
+ if (idx !== void 0) {
13203
+ emphasizedNodeId = id;
13204
+ applyEmphasis(eng, idx);
13205
+ }
13206
+ }
13207
+ function getPerfSnapshot() {
13208
+ const st = store.getState();
13209
+ const p = pressureSampler.snapshot();
13210
+ const snap = {
13211
+ at: Date.now(),
13212
+ nodeCount: st.nodeCount,
13213
+ edgeCount: st.edgeCount,
13214
+ visibleNodeCount: st.visible.nodes,
13215
+ visibleEdgeCount: st.visible.edges,
13216
+ estimatedCpuBytes: estimateCpuBytes(),
13217
+ // The acceptance queue is synchronous (depth is 0 outside a job, and a
13218
+ // reader inside a job observes 1); async ingestion depth arrives with
13219
+ // the worker lane (PR-E).
13220
+ queueDepth: acceptanceQueue.active ? 1 : 0,
13221
+ modelRevision: st.revisions.model,
13222
+ scopeRevision: st.revisions.scope,
13223
+ renderRevision: st.revisions.render,
13224
+ appliedRenderRevision: st.revisions.appliedRender,
13225
+ activeDegradations: degradeController.activeSteps(),
13226
+ // 'worker' iff the lane actually boots — a configured-but-unavailable
13227
+ // lane reports the honest 'main' (plus its worker-unavailable diag).
13228
+ execution: workerLane !== null && workerLane.available() === true ? "worker" : "main",
13229
+ rangeUpdates: session !== null && session.policy !== null ? [...session.policy.rangedChannels] : [],
13230
+ pressure: {
13231
+ frameEwmaMs: p.frameEwmaMs,
13232
+ droppedFrames: p.droppedFrames,
13233
+ idleWakeups: p.idleWakeups
13234
+ }
13235
+ };
13236
+ if (lastCommitMs !== void 0) snap.lastCommitMs = { ...lastCommitMs };
13237
+ if (scene !== null) {
13238
+ snap.estimatedGpuBytes = scene.count * (2 + 4 + 1) * 4 + scene.linkCount * (4 + 1 + 2) * 4;
13239
+ }
13240
+ return snap;
13241
+ }
13242
+ function estimateCpuBytes() {
13243
+ let bytes = 0;
13244
+ if (scene !== null) bytes += scene.positions.byteLength + scene.links.byteLength;
13245
+ if (basePointColors !== null) bytes += basePointColors.byteLength;
13246
+ if (baseLinkColors !== null) bytes += baseLinkColors.byteLength;
13247
+ if (lastLinkWidths !== null) bytes += lastLinkWidths.byteLength;
13248
+ bytes += metricStore.estimatedBytes();
13249
+ if (crossfilterEngine !== null) bytes += crossfilterEngine.estimatedBytes();
13250
+ if (softMask !== null) bytes += softMask.estimatedBytes();
13251
+ return bytes;
13252
+ }
11640
13253
  function cameraZoom(factor) {
11641
13254
  const eng = engineIfReady();
11642
13255
  if (eng === null) return;
@@ -11709,6 +13322,7 @@ function createGraphInstance(opts) {
11709
13322
  else eng.setViewport(v);
11710
13323
  },
11711
13324
  focusNode,
13325
+ emphasizeNode,
11712
13326
  requestNodeContextMenu,
11713
13327
  getViewState,
11714
13328
  setViewState,
@@ -11729,6 +13343,9 @@ function createGraphInstance(opts) {
11729
13343
  pickEdgeAt,
11730
13344
  sampleEdgeHover,
11731
13345
  sampleEdgeClick,
13346
+ getFrameCadence: () => frameCadence,
13347
+ getPerfCounters: () => perfCounters,
13348
+ getPerfSnapshot,
11732
13349
  hideNodes,
11733
13350
  showNodes,
11734
13351
  showAll,
@@ -11933,6 +13550,73 @@ var OverviewController = class {
11933
13550
  }
11934
13551
  };
11935
13552
 
11936
- export { ATLAS_MAX_CONCURRENT_DEFAULT, ATLAS_MAX_ENTRIES_DEFAULT, ATLAS_MAX_RETRIES_DEFAULT, AcceptanceQueue, BASE_PENDING_KEY, BRUSH_HISTORY_COALESCE_MS, CATEGORICAL_PALETTE, DEFAULT_BIN_COUNT, DIM_ALPHA_DEFAULT, DIVERGING_RANGE_DEFAULT, DomainStore, EDGE_PICK_TOLERANCE_PX, EdgePickingFacade, GRAPH_THEME_DARK, GRAPH_THEME_LIGHT, HISTORY_LIMIT_DEFAULT, HistoryKernel, INGEST_MAX_FLUSH_LATENCY_MS_DEFAULT, INGEST_MAX_PENDING_BYTES_DEFAULT, INGEST_OVERFLOW_FACTOR, ImageAtlasPipeline, LABEL_MAX_VISIBLE_CAP, LABEL_MAX_VISIBLE_DEFAULT, LinkPickIndex, META_EDGE_MAX_WIDTH, MetricStore, OVERVIEW_HOT_INTERVAL_MS, OVERVIEW_IDLE_INTERVAL_MS, OVERVIEW_SIZE_DEFAULT, OrbitOperationError, OverviewController, PHYSICAL_DEFAULT_LINK_WIDTH, PHYSICAL_DEFAULT_POINT_SIZE, PendingExpansions, Reconciler, SEARCH_CACHE_LIMIT, SEARCH_LIMIT_DEFAULT, SEARCH_SCAN_CHUNK, SEARCH_SCORE_EXACT_ID, SEARCH_SCORE_ID_PREFIX, SEARCH_SCORE_SUBSTRING, SEARCH_SCORE_TOKEN_START_BONUS, SEQUENTIAL_RANGE_DEFAULT, SUPER_NODE_MAX_SIZE, SVG_MAX_ELEMENTS_DEFAULT, SoftMask, SvgBudgetError, TIMELINE_STEP_DEFAULT, TIMELINE_TICK_MS_DEFAULT, TypedColumnCrossfilter, VIEW_STATE_VERSION, admitServiceResult, assertCapabilityMethodParity, baseFromAccepted, baseFromContribution, buildAcceptedAdjacency, buildAdjacency, canonicalFilterKey, canonicalJson2 as canonicalJson, canonicalScaleKey, cascadeEdges, categoricalIndex, categoricalRows, coerceNumeric, coerceNumericInto, collapseParallelEdges, compileEdgeFilter, compileNodeFilter, computeNumericDomain, createGraphInstance, createLocalExpansionService, createLocalSearchService, createRequestContext, deriveGroupsByKey, divergingColor, escapeXml, estimateBatchBytes, evaluateFilterExpr, graphErrorToError, groupByDerivedId, groupSceneKey, interpolateColor, isFatalGraphError, medianLinkWidthPx, mergeDiagnostics, mergeModel, metaEdgePublicId, metaEdgeSceneKey, metaEdgeWidthFor, neighborsOf, newContribution, newStagingTallies, nextRequestId, normalizeCommitForCapabilities, parseColor, pointSegmentDistanceSquared, projectColors, projectSizes, renderSvg, resolveEnginePolicy, resolveFilterField, resolveManualGroups, resolveScope, resolveTheme, resourceLimitFatal, rewriteGroups, sameDataRef, sameGroupBySpec, sameGroupSpecArrays, sceneGroupsOf, sceneLinkRefAt, scenePointRefAt, selectLabelCandidates, sequentialColor, sequentialSize, serviceCacheKey, sessionCommitDiagnostics, stageBatch, superNodeSizeFor, validateFilterExpr, validateGroupBySpec, validateGroupSpecs, validateSnapshot, validateViewState };
13553
+ // src/descriptors.ts
13554
+ function field(path) {
13555
+ return path;
13556
+ }
13557
+ var TRANSFORM_OPS = /* @__PURE__ */ new Set(["identity", "number", "lowercase", "date-to-epoch-ms", "coalesce"]);
13558
+ function validateFieldAccessor(value) {
13559
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
13560
+ return { kind: "not-an-object" };
13561
+ }
13562
+ const candidate = value;
13563
+ if (typeof candidate.field !== "string") return { kind: "field-not-a-string" };
13564
+ if (candidate.transform !== void 0) {
13565
+ const t = candidate.transform;
13566
+ if (t === null || typeof t !== "object" || Array.isArray(t)) {
13567
+ return { kind: "transform-not-an-object" };
13568
+ }
13569
+ const op = t.op;
13570
+ if (typeof op !== "string" || !TRANSFORM_OPS.has(op)) {
13571
+ return { kind: "unknown-transform-op", op: String(op) };
13572
+ }
13573
+ if (op === "coalesce" && !("value" in t)) {
13574
+ return { kind: "coalesce-missing-value" };
13575
+ }
13576
+ }
13577
+ return null;
13578
+ }
13579
+ function isFieldAccessor(value) {
13580
+ return validateFieldAccessor(value) === null;
13581
+ }
13582
+ function descriptorKey(accessor) {
13583
+ const t = accessor.transform;
13584
+ if (t === void 0) return `f:${accessor.field}|identity`;
13585
+ if (t.op === "coalesce") return `f:${accessor.field}|coalesce:${JSON.stringify(t.value ?? null)}`;
13586
+ return `f:${accessor.field}|${t.op}`;
13587
+ }
13588
+ function coerceNumber(value) {
13589
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
13590
+ if (typeof value === "string" && value.trim() !== "") {
13591
+ const parsed = Number(value);
13592
+ return Number.isFinite(parsed) ? parsed : null;
13593
+ }
13594
+ return null;
13595
+ }
13596
+ function coerceEpochMs(value) {
13597
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
13598
+ if (typeof value === "string") {
13599
+ const parsed = Date.parse(value);
13600
+ return Number.isNaN(parsed) ? null : parsed;
13601
+ }
13602
+ return null;
13603
+ }
13604
+ function evaluateFieldAccessor(accessor, id, attrs) {
13605
+ const raw = accessor.field === "id" ? id : attrs?.[accessor.field];
13606
+ const t = accessor.transform;
13607
+ if (t === void 0 || t.op === "identity") return raw;
13608
+ switch (t.op) {
13609
+ case "number":
13610
+ return coerceNumber(raw);
13611
+ case "lowercase":
13612
+ return typeof raw === "string" ? raw.toLowerCase() : null;
13613
+ case "date-to-epoch-ms":
13614
+ return coerceEpochMs(raw);
13615
+ case "coalesce":
13616
+ return raw === null || raw === void 0 ? t.value : raw;
13617
+ }
13618
+ }
13619
+
13620
+ export { ATLAS_MAX_CONCURRENT_DEFAULT, ATLAS_MAX_ENTRIES_DEFAULT, ATLAS_MAX_RETRIES_DEFAULT, AcceptanceQueue, BASE_PENDING_KEY, BRUSH_HISTORY_COALESCE_MS, CATEGORICAL_PALETTE, DEFAULT_BIN_COUNT, DIM_ALPHA_DEFAULT, DIVERGING_RANGE_DEFAULT, DomainStore, EDGE_PICK_TOLERANCE_PX, EdgePickingFacade, GRAPH_THEME_DARK, GRAPH_THEME_LIGHT, HISTORY_LIMIT_DEFAULT, HistoryKernel, INGEST_MAX_FLUSH_LATENCY_MS_DEFAULT, INGEST_MAX_PENDING_BYTES_DEFAULT, INGEST_OVERFLOW_FACTOR, ImageAtlasPipeline, LABEL_MAX_VISIBLE_CAP, LABEL_MAX_VISIBLE_DEFAULT, LinkPickIndex, META_EDGE_MAX_WIDTH, MetricStore, OVERVIEW_HOT_INTERVAL_MS, OVERVIEW_IDLE_INTERVAL_MS, OVERVIEW_SIZE_DEFAULT, OrbitOperationError, OverviewController, PHYSICAL_DEFAULT_LINK_WIDTH, PHYSICAL_DEFAULT_POINT_SIZE, PendingExpansions, Reconciler, SEARCH_CACHE_LIMIT, SEARCH_LIMIT_DEFAULT, SEARCH_SCAN_CHUNK, SEARCH_SCORE_EXACT_ID, SEARCH_SCORE_ID_PREFIX, SEARCH_SCORE_SUBSTRING, SEARCH_SCORE_TOKEN_START_BONUS, SEQUENTIAL_RANGE_DEFAULT, SUPER_NODE_MAX_SIZE, SVG_MAX_ELEMENTS_DEFAULT, SoftMask, SvgBudgetError, TIMELINE_STEP_DEFAULT, TIMELINE_TICK_MS_DEFAULT, TypedColumnCrossfilter, VIEW_STATE_VERSION, admitServiceResult, assertCapabilityMethodParity, baseFromAccepted, baseFromContribution, buildAcceptedAdjacency, buildAcceptedFromColumnar, buildAdjacency, canonicalFilterKey, canonicalJson2 as canonicalJson, canonicalScaleKey, cascadeEdges, categoricalIndex, categoricalRows, coerceNumeric, coerceNumericInto, collapseParallelEdges, columnarArrayBuffers, compileEdgeFilter, compileNodeFilter, computeNumericDomain, createGraphInstance, createLocalExpansionService, createLocalSearchService, createRequestContext, deriveGroupsByKey, descriptorKey, detachColumnarBuffers, divergingColor, escapeXml, estimateBatchBytes, evaluateFieldAccessor, evaluateFilterExpr, field, graphErrorToError, groupByDerivedId, groupSceneKey, interpolateColor, isColumnarSnapshot, isFatalGraphError, isFieldAccessor, materializeColumnarSnapshot, medianLinkWidthPx, mergeDiagnostics, mergeModel, metaEdgePublicId, metaEdgeSceneKey, metaEdgeWidthFor, neighborsOf, newContribution, newStagingTallies, nextRequestId, normalizeCommitForCapabilities, parseColor, pointSegmentDistanceSquared, projectColors, projectSizes, renderSvg, resolveEnginePolicy, resolveFilterField, resolveManualGroups, resolveScope, resolveTheme, resourceLimitFatal, rewriteGroups, sameDataRef, sameGroupBySpec, sameGroupSpecArrays, sceneGroupsOf, sceneLinkRefAt, scenePointRefAt, selectLabelCandidates, sequentialColor, sequentialSize, serviceCacheKey, sessionCommitDiagnostics, stageBatch, superNodeSizeFor, validateColumnarStructure, validateFieldAccessor, validateFilterExpr, validateGroupBySpec, validateGroupSpecs, validateSnapshot, validateViewState };
11937
13621
  //# sourceMappingURL=index.js.map
11938
13622
  //# sourceMappingURL=index.js.map