diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83957612..64e92a2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
+## v26.06.18 (2026-06-06)
+
+### Tests
+
+- **Expanded `rule_engine` evaluator coverage** (8 → 44 tests). Validating the
+ `implement-rule-engine` skill found **no bug** — the evaluator is correct — but
+ the module had only ~8 tests for 361 lines, with many paths unexercised. Added
+ `tests/rule_engine/test_evaluator_coverage.py` locking in: every leaf operator
+ (`eq/ne/gt/ge/lt/le/in/not_in/regex`) including None/missing-field safety and a
+ type-mismatch surfacing, composite `and`/`or`/`not` (+ `not`-arity), `then` vs
+ `otherwise`, `set`/`increment`/nested-write/unsupported-action isolation, the
+ loud unknown-operator error, the disabled-rule skip, and `RuleSet` priority
+ ordering + cross-rule error isolation. No framework behavior changed.
+
+---
+
## v26.06.17 (2026-06-06)
### Fixed
diff --git a/README.md b/README.md
index 65ae4275..08132ecb 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
diff --git a/pyproject.toml b/pyproject.toml
index cfddbfd0..a0fbd397 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "pyfly"
# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4);
# git tag, GitHub release and human-readable display use leading-zero form
# (v26.05.04) to match the Java/.NET/Go siblings.
-version = "26.6.17"
+version = "26.6.18"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py
index 5455c4d3..0345ec5b 100644
--- a/src/pyfly/__init__.py
+++ b/src/pyfly/__init__.py
@@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""
-__version__ = "26.06.17"
+__version__ = "26.06.18"
diff --git a/tests/rule_engine/test_evaluator_coverage.py b/tests/rule_engine/test_evaluator_coverage.py
new file mode 100644
index 00000000..38ca36fd
--- /dev/null
+++ b/tests/rule_engine/test_evaluator_coverage.py
@@ -0,0 +1,219 @@
+# Copyright 2026 Firefly Software Foundation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Coverage for the rule evaluator's previously-untested paths (v26.06.18).
+
+The module had ~8 tests for 361 lines; an audit confirmed every path correct but
+many were unexercised. These tests lock in: each leaf operator (+ None/missing
+safety + a type-mismatch surfacing), composite and/or/not (+ not-arity), then vs
+otherwise, set/increment/log/nested-write/unsupported-action isolation, the
+loud unknown-operator error, the disabled-rule skip, and RuleSet priority
+ordering + cross-rule error isolation.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+from pyfly.rule_engine import (
+ Action,
+ Condition,
+ EvaluationResult,
+ Rule,
+ RuleEvaluator,
+ RuleSet,
+ RuleSetEvaluator,
+)
+
+
+def _evaluate(when: Condition, ctx: dict) -> EvaluationResult:
+ return RuleEvaluator().evaluate(Rule(id="r", when=when), ctx)
+
+
+class TestLeafOperators:
+ @pytest.mark.parametrize(
+ ("op", "actual", "expected", "want"),
+ [
+ ("eq", 5, 5, True),
+ ("eq", 5, 6, False),
+ ("ne", 5, 6, True),
+ ("ne", 5, 5, False),
+ ("gt", 10, 5, True),
+ ("gt", 5, 5, False),
+ ("ge", 5, 5, True),
+ ("ge", 4, 5, False),
+ ("lt", 3, 5, True),
+ ("lt", 5, 5, False),
+ ("le", 5, 5, True),
+ ("le", 6, 5, False),
+ ("in", "gold", ["gold", "silver"], True),
+ ("in", "bronze", ["gold"], False),
+ ("not_in", "bronze", ["gold"], True),
+ ("not_in", "gold", ["gold"], False),
+ ],
+ )
+ def test_operator(self, op: str, actual: object, expected: object, want: bool) -> None:
+ assert _evaluate(Condition(operator=op, field="x", value=expected), {"x": actual}).matched is want
+
+ def test_regex(self) -> None:
+ cond = Condition(operator="regex", field="code", value="^PARTNER")
+ assert _evaluate(cond, {"code": "PARTNER-7"}).matched is True
+ assert _evaluate(cond, {"code": "OTHER"}).matched is False
+
+ @pytest.mark.parametrize("op", ["gt", "ge", "lt", "le", "in"])
+ def test_missing_field_is_false_without_error(self, op: str) -> None:
+ value: object = [1, 2] if op == "in" else 5
+ res = _evaluate(Condition(operator=op, field="missing", value=value), {})
+ assert res.matched is False
+ assert res.error is None # None-guarded — no crash
+
+ def test_type_mismatch_is_surfaced_not_silent(self) -> None:
+ # str vs int comparison raises TypeError; it must be captured into the
+ # result (loud), not silently treated as a match or swallowed.
+ res = _evaluate(Condition(operator="gt", field="name", value=5), {"name": "abc"})
+ assert res.matched is False
+ assert res.error is not None
+
+
+class TestCompositeConditions:
+ def test_and(self) -> None:
+ cond = Condition(
+ operator="and",
+ children=[
+ Condition(operator="gt", field="x", value=5),
+ Condition(operator="lt", field="x", value=100),
+ ],
+ )
+ assert _evaluate(cond, {"x": 50}).matched is True
+ assert _evaluate(cond, {"x": 200}).matched is False
+
+ def test_or(self) -> None:
+ cond = Condition(
+ operator="or",
+ children=[
+ Condition(operator="eq", field="tier", value="gold"),
+ Condition(operator="gt", field="spend", value=1000),
+ ],
+ )
+ assert _evaluate(cond, {"tier": "silver", "spend": 2000}).matched is True
+ assert _evaluate(cond, {"tier": "silver", "spend": 10}).matched is False
+
+ def test_not(self) -> None:
+ cond = Condition(operator="not", children=[Condition(operator="eq", field="blocked", value=True)])
+ assert _evaluate(cond, {"blocked": False}).matched is True
+ assert _evaluate(cond, {"blocked": True}).matched is False
+
+ def test_not_with_wrong_arity_is_surfaced(self) -> None:
+ cond = Condition(
+ operator="not",
+ children=[
+ Condition(operator="eq", field="a", value=1),
+ Condition(operator="eq", field="b", value=2),
+ ],
+ )
+ res = _evaluate(cond, {})
+ assert res.matched is False
+ assert "exactly one child" in (res.error or "")
+
+
+class TestThenOtherwise:
+ def test_then_runs_on_match(self) -> None:
+ rule = Rule(
+ id="r",
+ when=Condition(operator="gt", field="x", value=5),
+ then=[Action(type="set", target="hit", value="then")],
+ otherwise=[Action(type="set", target="hit", value="else")],
+ )
+ ctx: dict = {"x": 10}
+ res = RuleEvaluator().evaluate(rule, ctx)
+ assert res.matched is True
+ assert ctx["hit"] == "then"
+
+ def test_otherwise_runs_on_non_match(self) -> None:
+ rule = Rule(
+ id="r",
+ when=Condition(operator="gt", field="x", value=5),
+ then=[Action(type="set", target="hit", value="then")],
+ otherwise=[Action(type="set", target="hit", value="else")],
+ )
+ ctx: dict = {"x": 1}
+ res = RuleEvaluator().evaluate(rule, ctx)
+ assert res.matched is False
+ assert ctx["hit"] == "else"
+
+ def test_disabled_rule_is_skipped(self) -> None:
+ rule = Rule(id="r", when=Condition(operator="eq", field="x", value=1), enabled=False)
+ res = RuleEvaluator().evaluate(rule, {"x": 1})
+ assert res.matched is False
+
+
+class TestActions:
+ def test_increment_defaults_to_one(self) -> None:
+ rule = Rule(id="r", then=[Action(type="increment", target="count")])
+ ctx: dict = {}
+ RuleEvaluator().evaluate(rule, ctx)
+ assert ctx["count"] == 1
+
+ def test_nested_write(self) -> None:
+ rule = Rule(id="r", then=[Action(type="set", target="flags.discount_pct", value=10)])
+ ctx: dict = {}
+ RuleEvaluator().evaluate(rule, ctx)
+ assert ctx["flags"]["discount_pct"] == 10
+
+ def test_unsupported_action_is_isolated(self) -> None:
+ # An unsupported 'call' action raises NotImplementedError; it is recorded
+ # in the result error while the sibling 'set' still runs.
+ rule = Rule(
+ id="r",
+ then=[
+ Action(type="call", target="svc"),
+ Action(type="set", target="ok", value=1),
+ ],
+ )
+ ctx: dict = {}
+ res = RuleEvaluator().evaluate(rule, ctx)
+ assert ctx["ok"] == 1 # sibling executed despite the failing action
+ assert res.error is not None and "call" in res.error
+ assert [a.type for a in res.actions_executed] == ["set"]
+
+ def test_unknown_operator_is_surfaced(self) -> None:
+ res = _evaluate(Condition(operator="between", field="x", value=[1, 5]), {"x": 3})
+ assert res.matched is False
+ assert "unknown operator: between" in (res.error or "")
+
+
+class TestRuleSet:
+ def test_priority_ordering_over_shared_context(self) -> None:
+ # Inserted low-then-high, but evaluated high-then-low (sorted by -priority).
+ # Both write the same path over the shared context, so the LAST writer (low)
+ # wins — proving high ran first.
+ low = Rule(id="low", priority=1, then=[Action(type="set", target="winner", value="low")])
+ high = Rule(id="high", priority=10, then=[Action(type="set", target="winner", value="high")])
+ ruleset = RuleSet(id="rs", rules=[low, high])
+ ctx: dict = {}
+
+ results = RuleSetEvaluator().evaluate(ruleset, ctx)
+
+ assert [r.rule_id for r in results] == ["high", "low"] # priority order
+ assert ctx["winner"] == "low" # high ran first, low overwrote
+
+ def test_one_failing_rule_does_not_abort_the_set(self) -> None:
+ bad = Rule(id="bad", priority=10, then=[Action(type="call")]) # unsupported -> error
+ good = Rule(id="good", priority=1, then=[Action(type="set", target="ran", value=True)])
+ ruleset = RuleSet(id="rs", rules=[bad, good])
+ ctx: dict = {}
+
+ results = RuleSetEvaluator().evaluate(ruleset, ctx)
+
+ assert ctx["ran"] is True # good ran despite bad's error
+ assert any(r.error for r in results)
diff --git a/uv.lock b/uv.lock
index 109335cc..af9738b6 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1967,7 +1967,7 @@ wheels = [
[[package]]
name = "pyfly"
-version = "26.6.17"
+version = "26.6.18"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },