• The pure auto-adapt decision predicate.

    Parameters

    • stats: GraphStats

      The current commit's EngineTelemetry-shaped snapshot. Read-only; the predicate does not mutate.

    • thresholds: AdaptThresholds

      The merged threshold object for the active engine instance (typically DEFAULT_THRESHOLDS merged with per-engine and env-var overrides).

    • history: readonly GraphStats[]

      Snapshots from prior commits, oldest first. The predicate inspects only the tail (the last HYSTERESIS_TRIP_COUNT entries for the consecutive-trip gate, plus the full sequence for the node-count EWMA gate).

    • commitTimings: readonly number[] = []

      Optional rolling window of recent per-commit wall-times (ms), oldest first. This is the #1048 / Option B surface for the commit-shape axis: the auto-adapt wrapper's commit() shim (landing later when createCausl({ backend: 'auto' }) integration ships per #685 / #687) records performance.now() - commitStart after each commit and passes a bounded slice (length ≤ thresholds.rollingCommitWindow, default 100) into this parameter. When commitTimings.length > 0 its median takes precedence over stats.medianCommitMs (which remains accepted for backends that pre-compute the median internally — e.g. a future WASM backend that surfaces it directly via EngineTelemetry). Default [] for callers that have not yet wired the wrapper, in which case the commit-shape axis falls back to stats.medianCommitMs ?? 0 and degrades cleanly to the pre-#1048 behaviour.

    Returns boolean

    true iff the active backend SHOULD migrate to the WASM engine on the next commit boundary; false otherwise.

    The body is two gates AND'd:

    1. Consecutive-trip gate: the multi-axis OR (see AdaptThresholds) must fire on each of the last HYSTERESIS_TRIP_COUNT commits — the current stats plus the HYSTERESIS_TRIP_COUNT - 1 most recent entries in history. With fewer than 3 historical snapshots the predicate short-circuits to false.

    2. EWMA gate: the EWMA of inputs + deriveds (alpha=0.1) over the entire history (with stats appended as the newest observation) must exceed thresholds.nodeCount. This is the spike-rejection band — a single 60k-node commit followed by a 30k-node steady state has consecutive-trip count 1 and EWMA well below 50k, so it does not migrate.

    Pure: no I/O, no clock reads, no process.env. The success path allocates one array slice for the consecutive-trip window and (when commitTimings is non-empty) one slice-and-sort for the median — still O(n log n) on a bounded window of typically 100. Safe to call from commit() once the wiring lands in #687.

    The wrapper-side capture of commitTimings (the actual commit() shim that records performance.now() - commitStart and bounds the ring) is intentionally deferred to the integration PR that ships createCausl({ backend: 'auto' }) per #685 / #687. This module lands the decision-side signature only; the production capture lives in the wrapper layer alongside the backend-selection state.