This is an automated email from the ASF dual-hosted git repository.

Rikkola pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie.git


The following commit(s) were added to refs/heads/main by this push:
     new a0b09cc64fa [incubator-kie-7100] fix(drools): evaluate alpha 
constraints during isAllowedCachedLeft in CombinedConstraint (#7124)
a0b09cc64fa is described below

commit a0b09cc64fa53b67770b4db69fe935df9bfc2220
Author: Toni Rikkola <[email protected]>
AuthorDate: Thu Sep 24 14:01:31 2026 +0300

    [incubator-kie-7100] fix(drools): evaluate alpha constraints during 
isAllowedCachedLeft in CombinedConstraint (#7124)
    
    When a sequence step contains both alpha (intra-fact) and beta (cross-fact 
/ anchor-bound)
    constraints, DynamicFilter takes the beta evaluation path and delegates to
    CombinedConstraint#isAllowedCachedLeft.
    
    Previously, isAllowedCachedLeft iterated over child constraints and only 
evaluated
    those implementing BetaConstraint, completely skipping pure 
AlphaNodeFieldConstraint
    children. This caused facts that satisfied beta constraints but violated 
alpha
    constraints to incorrectly pass evaluation and trigger matches.
    
    Fix this by:
    - Renaming CombinedAlphaConstraint to CombinedConstraint to reflect its 
support for mixed alpha and beta constraints.
    - Evaluating non-BetaConstraint children via isAllowed(FactHandle, 
ValueResolver) inside isAllowedCachedLeft.
    - Adding comprehensive unit and edge case tests for mixed and multiple 
alpha constraints.
    
    Closes: https://github.com/apache/incubator-kie/issues/7100
    
    Co-authored-by: Toni Rikkola <[email protected]>
    Co-authored-by: IBM Bob <[email protected]>
---
 .../rule/constraint/CombinedAlphaConstraint.java   | 110 ------------
 .../base/rule/constraint/CombinedConstraint.java   | 189 +++++++++++++++++++++
 .../rule/constraint/CombinedConstraintTest.java    | 172 +++++++++++++++++++
 .../core/reteoo/builder/PhreakNodeFactory.java     |   4 +-
 .../PatternDSLSequenceEdgeCaseTest.java            |  81 +++++++++
 5 files changed, 444 insertions(+), 112 deletions(-)

diff --git 
a/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedAlphaConstraint.java
 
b/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedAlphaConstraint.java
deleted file mode 100644
index 5344678ea3b..00000000000
--- 
a/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedAlphaConstraint.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- */
-package org.drools.base.rule.constraint;
-
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-import java.util.Arrays;
-
-import org.drools.base.base.ValueResolver;
-import org.drools.base.rule.Declaration;
-import org.kie.api.runtime.rule.FactHandle;
-
-/**
- * ANDs two or more {@link AlphaNodeFieldConstraint}s into a single constraint.
- * Used by the sequencing runtime when a sequence step carries multiple 
expressions:
- * every expression must be satisfied for the step to match.
- */
-public class CombinedAlphaConstraint implements AlphaNodeFieldConstraint {
-
-    private AlphaNodeFieldConstraint[] constraints;
-
-    public CombinedAlphaConstraint(AlphaNodeFieldConstraint[] constraints) {
-        this.constraints = constraints;
-    }
-
-    @Override
-    public boolean isAllowed(FactHandle handle, ValueResolver valueResolver) {
-        for (AlphaNodeFieldConstraint c : constraints) {
-            if (!c.isAllowed(handle, valueResolver)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public Declaration[] getRequiredDeclarations() {
-        return new Declaration[0];
-    }
-
-    @Override
-    public void replaceDeclaration(Declaration oldDecl, Declaration newDecl) {
-        // Sequence step constraints are always self-contained alpha 
constraints —
-        // they carry no cross-pattern declaration references, so 
getRequiredDeclarations()
-        // returns empty and LogicTransformer never calls this method.
-    }
-
-    @Override
-    public CombinedAlphaConstraint clone() {
-        AlphaNodeFieldConstraint[] cloned = new 
AlphaNodeFieldConstraint[constraints.length];
-        for (int i = 0; i < constraints.length; i++) {
-            cloned[i] = (AlphaNodeFieldConstraint) constraints[i].clone();
-        }
-        return new CombinedAlphaConstraint(cloned);
-    }
-
-    @Override
-    public AlphaNodeFieldConstraint cloneIfInUse() {
-        return clone();
-    }
-
-    @Override
-    public ConstraintType getType() {
-        return ConstraintType.ALPHA;
-    }
-
-    @Override
-    public boolean isTemporal() {
-        return false;
-    }
-
-    @Override
-    public void writeExternal(ObjectOutput out) throws IOException {
-        out.writeObject(constraints);
-    }
-
-    @Override
-    public void readExternal(ObjectInput in) throws IOException, 
ClassNotFoundException {
-        constraints = (AlphaNodeFieldConstraint[]) in.readObject();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof CombinedAlphaConstraint)) return false;
-        return Arrays.equals(constraints, ((CombinedAlphaConstraint) 
o).constraints);
-    }
-
-    @Override
-    public int hashCode() {
-        return Arrays.hashCode(constraints);
-    }
-}
diff --git 
a/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedConstraint.java
 
b/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedConstraint.java
new file mode 100644
index 00000000000..34bfd9e4d17
--- /dev/null
+++ 
b/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedConstraint.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.drools.base.rule.constraint;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.BaseTuple;
+import org.drools.base.rule.ContextEntry;
+import org.drools.base.rule.Declaration;
+import org.kie.api.runtime.rule.FactHandle;
+
+/**
+ * ANDs two or more constraints into a single constraint for use in sequence 
steps.
+ * Supports pure-alpha steps (all children have no required declarations) and 
mixed
+ * alpha+beta steps (at least one child references the anchor pattern).
+ *
+ * <p>When the step is pure-alpha, {@link #getRequiredDeclarations()} returns 
an empty
+ * array and {@link org.drools.base.reteoo.DynamicFilter} takes the alpha 
path, calling
+ * {@link #isAllowed(FactHandle, ValueResolver)} directly.
+ *
+ * <p>When the step is mixed (or pure-beta), {@link 
#getRequiredDeclarations()} returns
+ * the aggregate of all children's declarations (non-empty), so
+ * {@link org.drools.base.reteoo.DynamicFilter} takes the beta path and calls
+ * {@link #isAllowedCachedLeft} with the anchor tuple. Alpha children are 
evaluated via
+ * {@link AlphaNodeFieldConstraint#isAllowed} and beta children via
+ * {@link BetaConstraint#isAllowedCachedLeft}, short-circuiting on first 
failure.
+ */
+public class CombinedConstraint
+        implements AlphaNodeFieldConstraint, 
BetaConstraint<CombinedConstraint.CombinedContextEntry> {
+
+    private AlphaNodeFieldConstraint[] constraints;
+
+    private transient Declaration[] requiredDeclarations;
+
+    public CombinedConstraint(AlphaNodeFieldConstraint[] constraints) {
+        this.constraints = constraints;
+    }
+
+    @Override
+    public boolean isAllowed(FactHandle handle, ValueResolver valueResolver) {
+        for (AlphaNodeFieldConstraint c : constraints) {
+            if (!c.isAllowed(handle, valueResolver)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public Declaration[] getRequiredDeclarations() {
+        if (requiredDeclarations == null) {
+            requiredDeclarations = Arrays.stream(constraints)
+                    .flatMap(c -> Stream.of(c.getRequiredDeclarations()))
+                    .distinct()
+                    .toArray(Declaration[]::new);
+        }
+        return requiredDeclarations;
+    }
+
+    @Override
+    public void replaceDeclaration(Declaration oldDecl, Declaration newDecl) {
+    }
+
+    @Override
+    public ConstraintType getType() {
+        return getRequiredDeclarations().length == 0 ? ConstraintType.ALPHA : 
ConstraintType.BETA;
+    }
+
+    @Override
+    public boolean isTemporal() {
+        return false;
+    }
+
+    @Override
+    public CombinedContextEntry createContext() {
+        return new CombinedContextEntry();
+    }
+
+    /**
+     * Evaluates all children against the supplied fact handle and the anchor 
tuple
+     * stored in {@code context}. Alpha children (no required declarations) are
+     * evaluated via {@link AlphaNodeFieldConstraint#isAllowed}; beta children 
are
+     * evaluated via {@link BetaConstraint#isAllowedCachedLeft} with a fresh 
per-child
+     * context populated from the anchor tuple.
+     */
+    @Override
+    public boolean isAllowedCachedLeft(CombinedContextEntry context, 
FactHandle handle) {
+        for (AlphaNodeFieldConstraint c : constraints) {
+            if (c instanceof BetaConstraint) {
+                @SuppressWarnings("unchecked")
+                BetaConstraint<ContextEntry> beta = 
(BetaConstraint<ContextEntry>) c;
+                ContextEntry ctx = beta.createContext();
+                ctx.updateFromTuple(context.valueResolver, context.tuple);
+                if (!beta.isAllowedCachedLeft(ctx, handle)) {
+                    return false;
+                }
+            } else {
+                if (!c.isAllowed(handle, context.valueResolver)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean isAllowedCachedRight(BaseTuple tuple, CombinedContextEntry 
context) {
+        throw new UnsupportedOperationException(
+                "CombinedConstraint does not support right-cached evaluation");
+    }
+
+    @Override
+    public CombinedConstraint clone() {
+        throw new UnsupportedOperationException("CombinedConstraint is not 
cloned");
+    }
+
+    @Override
+    public AlphaNodeFieldConstraint cloneIfInUse() {
+        throw new UnsupportedOperationException("CombinedConstraint is not 
cloned");
+    }
+
+    @Override
+    public void writeExternal(ObjectOutput out) throws IOException {
+        throw new UnsupportedOperationException("CombinedConstraint is not 
serialized");
+    }
+
+    @Override
+    public void readExternal(ObjectInput in) throws IOException, 
ClassNotFoundException {
+        throw new UnsupportedOperationException("CombinedConstraint is not 
serialized");
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof CombinedConstraint)) return false;
+        return Arrays.equals(constraints, ((CombinedConstraint) 
o).constraints);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(constraints);
+    }
+
+    // -----------------------------------------------------------------------
+    // ContextEntry for the beta evaluation path
+    // -----------------------------------------------------------------------
+
+    public static class CombinedContextEntry implements ContextEntry {
+        private BaseTuple     tuple;
+        private ValueResolver valueResolver;
+
+        @Override
+        public void updateFromTuple(ValueResolver valueResolver, BaseTuple 
tuple) {
+            this.valueResolver = valueResolver;
+            this.tuple         = tuple;
+        }
+
+        @Override public void updateFromFactHandle(ValueResolver 
valueResolver, FactHandle handle) { /* not used */ }
+        @Override public void resetTuple()      { tuple = null; valueResolver 
= null; }
+        @Override public void resetFactHandle() { /* not used */ }
+
+        @Override public ContextEntry getNext()                     { throw 
new UnsupportedOperationException(); }
+        @Override public void         setNext(ContextEntry entry)   { throw 
new UnsupportedOperationException(); }
+
+        @Override public void writeExternal(ObjectOutput out) throws 
IOException                          { throw new 
UnsupportedOperationException("CombinedContextEntry is not serialized"); }
+        @Override public void readExternal(ObjectInput in)    throws 
IOException, ClassNotFoundException  { throw new 
UnsupportedOperationException("CombinedContextEntry is not serialized"); }
+    }
+}
diff --git 
a/drools-base/src/test/java/org/drools/base/rule/constraint/CombinedConstraintTest.java
 
b/drools-base/src/test/java/org/drools/base/rule/constraint/CombinedConstraintTest.java
new file mode 100644
index 00000000000..3201b1446b1
--- /dev/null
+++ 
b/drools-base/src/test/java/org/drools/base/rule/constraint/CombinedConstraintTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.drools.base.rule.constraint;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.BaseTuple;
+import org.drools.base.rule.ContextEntry;
+import org.drools.base.rule.Declaration;
+import org.junit.Test;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CombinedConstraintTest {
+
+    @Test
+    public void 
mixedStepWithNegConstraintMustEvaluateAlphaDuringLeftEvaluation() {
+        // Real production AlphaNodeFieldConstraint from drools-base:
+        // NegConstraint(true) checks handle.isNegated() == true.
+        // For a normal (non-negated) fact handle, isAllowed() returns false.
+        NegConstraint failingNegConstraint = new NegConstraint(true);
+        DummyBetaConstraint passingBeta = new DummyBetaConstraint(true);
+
+        CombinedConstraint combined = new CombinedConstraint(new 
AlphaNodeFieldConstraint[]{
+                failingNegConstraint,
+                passingBeta
+        });
+
+        CombinedConstraint.CombinedContextEntry context = 
combined.createContext();
+        context.updateFromTuple(null, null);
+
+        // FactHandle where isNegated() == false
+        FactHandle nonNegatedHandle = new FactHandle() {
+            @Override public Object getObject() { return new Object(); }
+            @Override public boolean isNegated() { return false; }
+            @Override public boolean isEvent() { return false; }
+            @Override public long getId() { return 1L; }
+            @Override public long getRecency() { return 1L; }
+            @Override public <K> K as(Class<K> klass) { return null; }
+            @Override public boolean isValid() { return true; }
+            @Override public String toExternalForm() { return "handle:1"; }
+        };
+
+        // NegConstraint returns false for non-negated handle, so 
isAllowedCachedLeft must return false.
+        // Without the fix, NegConstraint is skipped in isAllowedCachedLeft 
because !(NegConstraint instanceof BetaConstraint).
+        boolean allowed = combined.isAllowedCachedLeft(context, 
nonNegatedHandle);
+        assertThat(allowed)
+                .as("NegConstraint failing inside mixed step must cause 
isAllowedCachedLeft to return false")
+                .isFalse();
+    }
+
+    private static class DummyBetaConstraint implements 
AlphaNodeFieldConstraint, BetaConstraint<DummyContextEntry> {
+
+        private final boolean allowed;
+
+        public DummyBetaConstraint(boolean allowed) {
+            this.allowed = allowed;
+        }
+
+        @Override
+        public boolean isAllowed(FactHandle handle, ValueResolver 
valueResolver) {
+            return allowed;
+        }
+
+        @Override
+        public boolean isAllowedCachedLeft(DummyContextEntry context, 
FactHandle handle) {
+            return allowed;
+        }
+
+        @Override
+        public boolean isAllowedCachedRight(BaseTuple tuple, DummyContextEntry 
context) {
+            return true;
+        }
+
+        @Override
+        public DummyContextEntry createContext() {
+            return new DummyContextEntry();
+        }
+
+        @Override
+        public Declaration[] getRequiredDeclarations() {
+            return new Declaration[]{new Declaration("var", null, null)};
+        }
+
+        @Override
+        public void replaceDeclaration(Declaration oldDecl, Declaration 
newDecl) {
+        }
+
+        @Override
+        public ConstraintType getType() {
+            return ConstraintType.BETA;
+        }
+
+        @Override
+        public boolean isTemporal() {
+            return false;
+        }
+
+        @Override
+        public AlphaNodeFieldConstraint cloneIfInUse() {
+            return this;
+        }
+
+        @Override
+        public AlphaNodeFieldConstraint clone() {
+            return this;
+        }
+
+        @Override
+        public void writeExternal(ObjectOutput out) throws IOException {
+        }
+
+        @Override
+        public void readExternal(ObjectInput in) throws IOException, 
ClassNotFoundException {
+        }
+    }
+
+    private static class DummyContextEntry implements ContextEntry {
+        @Override
+        public void updateFromTuple(ValueResolver valueResolver, BaseTuple 
tuple) {
+        }
+
+        @Override
+        public void updateFromFactHandle(ValueResolver valueResolver, 
FactHandle handle) {
+        }
+
+        @Override
+        public void resetTuple() {
+        }
+
+        @Override
+        public void resetFactHandle() {
+        }
+
+        @Override
+        public ContextEntry getNext() {
+            return null;
+        }
+
+        @Override
+        public void setNext(ContextEntry entry) {
+        }
+
+        @Override
+        public void writeExternal(ObjectOutput out) throws IOException {
+        }
+
+        @Override
+        public void readExternal(ObjectInput in) throws IOException, 
ClassNotFoundException {
+        }
+    }
+}
diff --git 
a/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
 
b/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
index 394434acc54..e58543456ce 100644
--- 
a/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
+++ 
b/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
@@ -34,7 +34,7 @@ import org.drools.base.definitions.rule.impl.RuleImpl;
 import org.drools.base.reteoo.DynamicFilterProto;
 import org.drools.base.rule.Pattern;
 import org.drools.base.rule.constraint.AlphaNodeFieldConstraint;
-import org.drools.base.rule.constraint.CombinedAlphaConstraint;
+import org.drools.base.rule.constraint.CombinedConstraint;
 import org.drools.base.rule.constraint.Constraint;
 import org.drools.base.reteoo.sequencing.Sequence;
 import org.drools.base.reteoo.sequencing.Sequencer;
@@ -195,7 +195,7 @@ public class PhreakNodeFactory implements NodeFactory, 
Serializable {
                 AlphaNodeFieldConstraint[] all = constraints.stream()
                         .map(c -> (AlphaNodeFieldConstraint) c)
                         .toArray(AlphaNodeFieldConstraint[]::new);
-                combined = new CombinedAlphaConstraint(all);
+                combined = new CombinedConstraint(all);
             }
             int adapterIndex = 
objectTypeIndex.get(patterns[i].getObjectType());
             filters[i] = new DynamicFilterProto(combined, adapterIndex);
diff --git 
a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java
 
b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java
index a499e0dd2c8..254d6914fef 100644
--- 
a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java
+++ 
b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java
@@ -511,4 +511,85 @@ public class PatternDSLSequenceEdgeCaseTest {
                 .isInstanceOf(IllegalArgumentException.class)
                 .hasMessageContaining("at least one step");
     }
+
+   @Test
+    public void mixedAlphaAndBetaExprsInOneStepBothMustHold() {
+        Variable<Person> personV = declarationOf(Person.class);
+        Variable<Toy>    toyV    = declarationOf(Toy.class);
+
+        Rule rule = rule("mixed-alpha-beta").build(
+                pattern(personV),
+                sequence(
+                        pattern(toyV)
+                                .expr("alpha", t -> "ball".equals(t.getName()))
+                                .expr("beta", personV, (t, p) -> 
p.getName().equals(t.getOwner()))
+                ),
+                on(personV).execute(p -> results.add("fired:" + p.getName()))
+        );
+
+        KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new 
ModelImpl().addRule(rule));
+        ksession = kbase.newKieSession();
+
+        ksession.insert(new Person("alice"));
+        ksession.fireAllRules();
+
+        // Passes beta (owner == "alice") but fails alpha (name "robot" != 
"ball")
+        // With the isAllowedCachedLeft bug, alpha is skipped because beta 
exists, so this incorrectly fires!
+        Toy robot = new Toy("robot");
+        robot.setOwner("alice");
+        ksession.insert(robot);
+        ksession.fireAllRules();
+        assertThat(results).isEmpty();
+
+        // Passes alpha (name == "ball") but fails beta (owner "bob" != anchor 
"alice")
+        Toy bobBall = new Toy("ball");
+        bobBall.setOwner("bob");
+        ksession.insert(bobBall);
+        ksession.fireAllRules();
+        assertThat(results).isEmpty();
+
+        // Passes both alpha ("ball") and beta (owner == "alice")
+        Toy aliceBall = new Toy("ball");
+        aliceBall.setOwner("alice");
+        ksession.insert(aliceBall);
+        ksession.fireAllRules();
+        assertThat(results).containsExactly("fired:alice");
+    }
+
+    @Test
+    public void twoAlphaExprsInOneStepBothMustHold() {
+        Variable<Person> personV = declarationOf(Person.class);
+        Variable<Toy>    toyV    = declarationOf(Toy.class);
+
+        Rule rule = rule("two-alpha-exprs").build(
+                pattern(personV),
+                sequence(
+                        pattern(toyV)
+                                .expr("starts-with-a", t -> 
t.getName().startsWith("a"))
+                                .expr("ends-with-e",   t -> 
t.getName().endsWith("e"))
+                ),
+                execute(() -> results.add("fired"))
+        );
+
+        KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new 
ModelImpl().addRule(rule));
+        ksession = kbase.newKieSession();
+
+        ksession.insert(new Person("anchor"));
+        ksession.fireAllRules();
+
+        // Fails first alpha ("b…") — must not fire
+        ksession.insert(new Toy("ball"));
+        ksession.fireAllRules();
+        assertThat(results).isEmpty();
+
+        // Passes first alpha but fails second — "arrow" starts with "a" but 
does not end with "e"
+        ksession.insert(new Toy("arrow"));
+        ksession.fireAllRules();
+        assertThat(results).isEmpty();
+
+        // Passes both: "apple" starts with "a" and ends with "e"
+        ksession.insert(new Toy("apple"));
+        ksession.fireAllRules();
+        assertThat(results).containsExactly("fired");
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to