Expression-based rules engine rearchitecture - #38
Conversation
Replaces iterative dimension-by-dimension evaluation with single-pass expression-based architecture using mountainash-expressions. Clean break from existing engines — single ExpressionRulesEngine with dual API (metadata convenience + raw expressions). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13-task TDD plan covering: cleanup, constants, context, compiler (EXACT/RANGE/REGEX), result, engine, advanced features, custom expressions, integration tests, public API, and lint pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove all legacy engine implementations (RulesEngine strategies, observer, rule_manager, vectorized_engine, deprecated/) and test files being replaced by the new expression-based architecture. Update pyproject.toml to use polars>=1.35.1, ibis-framework[polars,duckdb]>=11.0.0, narwhals>=1.0.0, and mountainash. Add mountainash-expressions dependency to hatch.toml test environments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ations Rewrites constants.py with module-level sentinel constants and simplified MatchStrategy enum. Rewrites dimension.py with Dimension/DimensionsMetadata pydantic models including model_validator-based strategy validation and resolved_context_field/resolved_rule_field properties. Updates __init__.py to remove all stale references to deleted modules (rule_strategies, observer, rule_manager, vectorized_engine, context) and export only the new symbols. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s function Rewrites context.py as a simple module-level function aligned with the new constants.py (NOT_SET, NOT_SET_NUMERIC) from Task 2. Adds test_context.py with TDD coverage for pydantic model, dict, missing fields, and None values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements DimensionCompiler translating Dimension metadata into backend-agnostic mountainash-expressions templates. EXACT strategy uses t_col with sentinel sets for ternary-aware equality matching (TRUE=1, UNKNOWN=0, FALSE=-1). RANGE and REGEX raise NotImplementedError as stubs for Tasks 5 and 6. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _compile_range (Task 5) using ternary comparison operators with inclusive/exclusive boundary support and sentinel-aware unknown propagation. Add _compile_regex (Task 6) using ma.when/ma.native with polars map_elements for column-based pattern matching and explicit sentinel checking — required because the MA expressions polars backend extracts regex patterns as literals, preventing dynamic column-based matching. All 11 compiler tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…valuated rule DataFrames Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le-pass evaluation Replaces the old ibis-based RulesEngine with ExpressionRulesEngine that uses mountainash-expressions for vectorized single-pass rule evaluation. The new engine compiles dimension metadata into expression templates at init time, binds context values as literal columns, and evaluates all dimensions in one pass with survival filtering and specificity ranking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssions tests Appends TestTopN, TestMinSpecificity, TestDimensionsSubset, TestObservability, and TestCustomExpressions to test_engine.py; adds mountainash.expressions and CTX_PREFIX imports. All 16 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds integration tests covering pricing carve-out hierarchy, entity pool with range/regex rules, no-match, tie handling, and explain breakdown. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrite __init__.py with new public API (ExpressionRulesEngine, DimensionCompiler, RuleResult, etc.) - Rewrite conftest.py with expression-based engine fixtures - Remove 7 leftover old test files that weren't caught in Task 1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Adds 8 new match strategies (NOT_EQUAL, GREATER_THAN, LESS_THAN, PREFIX, SUFFIX, CONTAINS, SET_MEMBERSHIP, SET_EXCLUSION) and rewrites REGEX to be backend-agnostic. All 11 strategies work uniformly across Polars, Ibis, and Narwhals backends with per-row patterns/thresholds. Depends on upstream mountainash-expressions fixes to string API consistency and t_is_in column-reference support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13-task TDD plan covering: enum extension, validation rules, 8 new strategy compile methods (NOT_EQUAL, GT, LT, PREFIX, SUFFIX, CONTAINS, SET_MEMBERSHIP, SET_EXCLUSION), REGEX rewrite, backend agnosticism tests, integration scenario, and documentation updates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend _validate_strategy_fields to enforce type constraints for GREATER_THAN/LESS_THAN (numeric only) and PREFIX/SUFFIX/CONTAINS (string only), and add corresponding test coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…es (Tasks 3 & 4) Implement three new compile methods in DimensionCompiler using the existing ternary expression API (t_ne, t_gt, t_lt), with TDD tests covering true/false/ equal-boundary and unknown-sentinel cases for each strategy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces `_compile_string_match` as a sentinel-aware ternary wrapper for string operations (unknown → 0, match → 1, no-match → -1) and implements the PREFIX strategy via `ctx_col.str.starts_with(rule_col)`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SUFFIX delegates to _compile_string_match("ends_with"). CONTAINS uses
count_substring > 0 rather than str.contains because the polars backend
stringifies column references in contains(), making it unusable for
per-row patterns; count_substring correctly resolves column refs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverse-engineers sp_productpricingmatrix_discretion_combos.sql as the basis for a second rules-engine pattern (accumulator) and contrasts it with the existing filter engine. Frames them as composable stages of a pricing pipeline rather than alternatives. Identifies the minimum gaps in the shared metadata layer and lists open questions for an eventual implementation phase. Also corrects CLAUDE.md: the codebase uses signed-integer ternary encoding (1/0/-1), not the prime-based scheme (PRIME_TRUE=2 etc.) that older planning docs described but never shipped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds test_regex_per_row_different_patterns to prove the existing
map_elements implementation supports per-row regex patterns.
Note: _compile_regex retains the ma.native/map_elements approach rather
than _compile_string_match("regex_contains") because the polars backend
currently compiles regex_contains with literal=True, causing column
references to be treated as literal strings rather than regex patterns.
The per-row test passes with the existing implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Targeted survey of rules-engine patterns adjacent to mountainash-utils-rules' filter and accumulator engines, organised by pattern with systems as exemplars. Identifies the closest precedents — Configit Virtual Tabulation (commercial), DRSA / Greco & Słowiński (academic), Malouf's maximal consistent subsets (vocabulary), skyline queries (filter algorithm) — and argues that the accumulator engine is a novel synthesis of well-precedented elements occupying a gap in the BRE field. Includes a forward-looking roadmap of three candidate future engines (temporal, inverse, probabilistic/learned-rules hybrid), each anchored in a survey gap and validated against a Mountain Ash use case. All roadmap items are EXPLORATORY. Comprehensive references with URLs from exa-verified sources. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With upstream mountainash-expressions fixes (regex_contains routed through new mountainash extension key, contains supports column refs), both CONTAINS and REGEX now use the shared _compile_string_match helper. - CONTAINS: ctx.str.contains(rule_col) - was count_substring workaround - REGEX: ctx.str.regex_contains(rule_col) - was pl.struct.map_elements - Removed: import re, import polars as pl from compiler.py - compiler.py is now pure expressions, fully backend-agnostic Also remove unused mountainash dep from pyproject.toml (provided via hatch.toml test env path; not yet on PyPI). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four corrections after reading the full PMX_DB source: 1. Add a new §0 Framing that positions the accumulator engine as a production pattern from a 2016 Big 4 mortgage pricing system, not as a proposed novel design. Introduces the three-layer separation (data prep / rules engine / governance) so organisational quirks from the SQL precedent are not propagated into the engine layer. 2. §3.1: add a note on the two-tier (shape, cell) rule structure in PMX_DB, and generalise the dual-margin payload as an instance of "rules carry zero-or-more named monoids" rather than architecture. 3. §3.3: correct an embarrassing claim. Prime allocation is already build-scoped per partition in the SQL via ROW_NUMBER, exactly as the principles directory recommends. The Python engine inherits this pattern; it does not improve on it. 4. §7 Q2: add the empirical bound from sp_product_rule_profile — max combination depth was 20 rules in production, smallest 20 primes fit in int64, so int128 and Python-object tiers are safety nets not expected operating modes. Also: tier engine noted as production precedent for the filter pattern alongside the accumulator, confirming the two-engine pipeline shipped in 2016 and was not invented during the brainstorming session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One-page anonymised case study suitable for SMB lender outreach, anchored in the PMX_DB production history and the FICO replacement compression story. Headline: ~200 products, ~2,000 logical rules vs the replacement system's ~500 million enumerated rules (~250,000x compression). Bridges from the Big 4 story to the needs of credit unions, mutuals, and non-bank mortgage lenders. Includes a footnote placeholder for the direct engineer quote, pending attribution decisions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements _compile_set_membership and _compile_set_exclusion in DimensionCompiler using ma.native(pl.col.list.contains()) to handle list-typed rule columns, with sentinel-aware ternary logic. Adds 4 unit tests covering match, non-match, and unknown context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds TestMixedStrategyFraudDetection covering EXACT, SET_MEMBERSHIP, GREATER_THAN, and PREFIX strategies together in a realistic fraud rule scenario. Validates specificity scoring and best-match selection across three test cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests all 9 backend-agnostic match strategies (EXACT, NOT_EQUAL, RANGE, GREATER_THAN, LESS_THAN, PREFIX, SUFFIX, CONTAINS, REGEX) compile cleanly against both Polars and Ibis backends. SET_MEMBERSHIP and SET_EXCLUSION are excluded as they use a Polars-native list.contains workaround. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents all 11 strategies with column formats, data types, backend support notes, and the SET_MEMBERSHIP/SET_EXCLUSION Polars-specific workaround pending upstream t_is_in list-column support. Also includes the pyproject.toml mountainash dependency removal from the earlier compiler refactor (mountainash provided via hatch.toml test env path, not yet on PyPI). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites engine.py and result.py to use mountainash.relations.Relation and mountainash.expressions exclusively. Removes Polars from the engine source tree (with one documented exception for SET_MEMBERSHIP). Updates representation-fits-host-language.md principle to reflect the one-engine-many-backends reality. Promotes status to ENFORCED via a new test_backend_purity.py import-check test. Depends on upstream Relation.count_rows() and Relation.item() additions landed 2026-04-08. Cross-backend test parameterisation deferred to a follow-up spec. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7-task TDD plan: failing backend-purity test first, tag SET_MEMBERSHIP exception in compiler, rewrite engine.py to use mountainash.relations, rewrite result.py to use relation/count_rows/item, full test suite + lint, principle document update, final verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ption Tasks 1+2 of backend-agnostic engine plan: - New tests/test_backend_purity.py asserts engine.py, result.py, and compiler.py have no direct polars/ibis/narwhals imports (with allow comment exceptions) - compiler.py polars import tagged with allow comment for SET_MEMBERSHIP workaround Test currently fails for engine.py only (expected — fixed in Task 3). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…inash.relations.Relation Replace direct polars imports (pl.col, pl.lit, pl.min_horizontal, pl.sum_horizontal) with mountainash.expressions (ma.col, ma.lit, ma.least) and mountainash.relations.Relation for the evaluation pipeline, eliminating the polars coupling that was blocking the backend-purity test for engine.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces Polars-specific idioms (shape[0], df[col]==value, row[col][0]) with relation().count_rows() and relation().item() calls. Terminal accessors (best_match, at_least) use .collect().collect() to match engine.py's materialization pattern. RuleResult source has zero direct DataFrame-library imports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three source fixes exposed by cross-backend testing: 1. REGEX becomes a one-hot context validator. Pattern lives on Dimension.regex_pattern metadata rather than a per-rule column. Upstream mountainash-expressions mandates literal-only regex patterns; the old column-ref form only worked on Polars by accident. Multiple patterns = multiple dimensions. 2. Specificity sum cast to int in engine.py. Polars auto-promoted bool+bool to int; SQLite/DuckDB/pandas/narwhals didn't. 3. Drop double .collect().collect() now that mountainash.relations.Relation.collect() materializes in one call on every backend. Adds tests/__init__.py so tests can import conftest helpers. Adds pytest_collection_modifyitems in conftest to non-strict xfail tests on backends with known upstream bugs (pandas/narwhals-pandas literal-alias collision, ibis-polars missing WindowFunction). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-SET classes auto-parametrize via backend_name fixture. Fraud detection class parametrizes over LIST_CAPABLE_BACKENDS with strict xfail on non-polars backends pending mountainash-io/mountainash#75. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds SET_MEMBERSHIP/SET_EXCLUSION cases with strict xfail on non-Polars backends pending mountainash-io/mountainash#75. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mountainash-io/mountainash#77 (narwhals-pandas literal alias) mountainash-io/mountainash#78 (ibis-polars WindowFunction) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cross-backend test parameterisation landedMirrors the Source fixes
Test suite
Upstream bugs filed and trackedTwo distinct upstream issues surfaced during migration, both marked non-strict xfail via
SET_MEMBERSHIP upstream gap remains tracked as mountainash-io/mountainash#75. Results247 passed, 71 xfailed, 85 xpassed, 0 failed The 85 xpasses are tests on the upstream-broken backends that happen to avoid the broken code paths — non-strict xfail lets them pass cleanly. Once #77, #78, and #75 land, removing entries from Commits in this batch
|
mountainash-io/mountainash#75 landed — t_is_in now accepts list-typed column arguments polymorphically. The rules engine's last direct polars import is gone, and every match strategy compiles cleanly on every list-capable backend. - compiler.py: _compile_set_membership and _compile_set_exclusion use ctx_col.t_is_in(rule_col) / t_is_not_in(rule_col) directly - drop import polars as pl escape hatch - test_compiler.py + test_integration.py: flip SET xfails to pass - conftest.py: remove SET_MEMBERSHIP_XFAIL_REASON constant, drop narwhals-polars from LIST_CAPABLE_BACKENDS (narwhals 2.19.0 list.contains gap), include list_backend in the UPSTREAM_BROKEN xfail hook param names Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The narwhals-pandas literal collision is NOT fixable with a local workaround — mountainash.relations.Relation defers with_columns calls and batches all pending projections into a single narwhals call at collect() time. Per-expression calls in engine.py are cosmetic. Adds test_upstream_regressions.py with a strict-xfail regression test encoding the exact trigger (batched sentinel-aware ternary expressions compiled by DimensionCompiler on narwhals-pandas). When narwhals fixes their intermediate column naming, the xfail flips to xpass and forces a cleanup. Updated #77 with the real diagnosis and reproducer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Blanket non-strict xfails on pandas/narwhals-pandas/ibis-polars produced 91 xpasses (noise) alongside 53 real failures. Replace with curated test×backend lists per upstream issue, using strict=True so CI flags when upstream fixes land. - #77 (pandas, narwhals-pandas): 13 tests that hit batched ternary expressions - #78 (ibis-polars): 27 tests that reach with_row_index in the pipeline - Result: 343 passed, 4 skipped, 54 xfailed, 0 xpassed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ails mountainash-expressions 0.1.1 fixes #77 (narwhals-pandas batched literal collision). Remove all pandas/narwhals-pandas xfails and convert the #77 regression test to a normal passing test. ibis-polars #78 (WindowFunction translation) remains — restore surgical xfails for the 27 affected tests. Result: 370 passed, 4 skipped, 27 xfailed, 0 xpassed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
❌ The last analysis has failed. |
Summary
mountainash-expressionsRulesEngine,HybridRulesEngine,VectorizedRulesEngine) and supporting infrastructure (~12,000 lines deleted)ExpressionRulesEnginewith dual API: convenience (DataFrame + dimension metadata) and advanced (raw expressions)Key changes
compiler.py— Translates dimension metadata into backend-agnostic expression templates (EXACT, RANGE, REGEX)engine.py—ExpressionRulesEnginewith single-passwith_columns()evaluation pipelineresult.py—RuleResultwrapper withbest_match,explain(),at_least(), specificity rankingconstants.py— Simplified:MatchStrategyenum + sentinel values onlydimension.py— Simplified:Dimension+DimensionsMetadataPydantic models, noMetadataManagercontext.py— Singleextract_context_values()function replacingContextHelperclassTest plan
🤖 Generated with Claude Code