56b1ff898a
model/types.rs carried four copy-pasted blocks across its two evaluation paths (cached and uncached). All four copies were byte-identical — no drift yet, but any change to aggregation or item lookup had to be made twice to stay correct. - apply_agg(values, func) — cell aggregation, shared by evaluate_aggregated and aggregate_raw. Absorbs the callers' emptiness guards so "empty means absent, not zero" is stated once. - eval_agg(...) — formula-level SUM/AVG/MIN/MAX/COUNT, shared by eval_expr_cached and eval_expr. This pair was a second instance of the same defect, not named in either ticket but the same 27 duplicated lines. It keeps its own emptiness rule (0 where an identity exists, error otherwise) and now expresses that by delegating to apply_agg. - Model::find_item_category — was a nested fn defined identically in both eval paths. Filter joins the improvise-formula re-exports so eval_agg can name it. Tests cover each helper directly, including the deliberate emptiness asymmetry between the two aggregation paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>