@@ -132,6 +132,9 @@ def __repr__(self):
132132# StatFunc — a registered stat computation
133133# ---------------------------------------------------------------------------
134134
135+ VALID_COSTS = ("scalar" , "aggregate" )
136+
137+
135138@dataclass
136139class StatFunc :
137140 """A registered stat computation.
@@ -149,6 +152,11 @@ class StatFunc:
149152 engines can compute this stat via aggregation push-down without
150153 in-process materialization. Empty default means pandas-only /
151154 requires materialization.
155+ cost: cost class — ``"scalar"`` (cheap, ships in the initial
156+ state_change response) or ``"aggregate"`` (slow path that the
157+ JS orchestrator fetches via a separate round-trip after a
158+ debounce). Histograms, value_counts and other per-column
159+ queries belong in ``"aggregate"``.
152160 """
153161 name : str
154162 func : Callable
@@ -159,6 +167,7 @@ class StatFunc:
159167 quiet : bool = False
160168 default : Any = field (default_factory = lambda : MISSING )
161169 pushdown : Tuple [str , ...] = ()
170+ cost : str = "scalar"
162171 spread_dict_result : bool = False # v1 compat: spread all dict keys into accumulator
163172 v1_computed : bool = False # v1 compat: pass full accumulator as single dict arg
164173
@@ -255,7 +264,7 @@ def _get_requires_from_params(sig: inspect.Signature, hints: dict) -> tuple:
255264# @stat decorator
256265# ---------------------------------------------------------------------------
257266
258- def stat (column_filter = None , quiet = False , default = MISSING , pushdown = ()):
267+ def stat (column_filter = None , quiet = False , default = MISSING , pushdown = (), cost = "scalar" ):
259268 """Decorator that converts a function into a StatFunc.
260269
261270 The function signature IS the contract:
@@ -274,6 +283,12 @@ def stat(column_filter=None, quiet=False, default=MISSING, pushdown=()):
274283 identifiers: ``"xorq"``, ``"polars"``. Normalised to a tuple so callers
275284 may pass a list.
276285
286+ ``cost=`` declares the compute-cost class. ``"scalar"`` (default)
287+ means cheap — ships in the initial state_change response. ``"aggregate"``
288+ means slow (histograms, value_counts, per-column queries) — the JS
289+ orchestrator fetches these via a separate round-trip after a debounce.
290+ See plans/js-driven-stat-debounce.md.
291+
277292 Usage::
278293
279294 @stat()
@@ -292,6 +307,10 @@ def safe_ratio(a: int, b: int) -> float:
292307 def mean(col):
293308 return col.mean()
294309
310+ @stat(cost="aggregate")
311+ def histogram(...) -> list:
312+ ...
313+
295314 class TypingResult(MultipleProvides):
296315 is_numeric: bool
297316 is_integer: bool
@@ -300,6 +319,10 @@ class TypingResult(MultipleProvides):
300319 def typing_stats(dtype: str) -> TypingResult:
301320 ...
302321 """
322+ if cost not in VALID_COSTS :
323+ raise ValueError (
324+ f"@stat(cost={ cost !r} ): invalid cost class. "
325+ f"Must be one of { VALID_COSTS } ." )
303326 def decorator (func ):
304327 sig = inspect .signature (func )
305328 try :
@@ -317,7 +340,7 @@ def decorator(func):
317340 pushdown_norm = (pushdown ,) if isinstance (pushdown , str ) else tuple (pushdown )
318341 stat_func = StatFunc (name = func .__name__ , func = func , requires = requires , provides = provides_keys ,
319342 needs_raw = needs_raw , column_filter = column_filter , quiet = quiet , default = default ,
320- pushdown = pushdown_norm )
343+ pushdown = pushdown_norm , cost = cost )
321344
322345 # Attach metadata to the function so pipeline can find it
323346 func ._stat_func = stat_func
0 commit comments