+ * IMPORTANT: As this is cached, it is vital the stream does not reference any variables + * (genuine or otherwise), as a score corruption would occur. + *
+ * For example, if employee is a {@link PlanningVariable} on Shift (a {@link PlanningEntity}), + * and start/end are facts on Shift, the following Constraint would cause a score corruption: + * + *
+ * BiConstraintStream<Shift, Shift> overlappingShifts(PrecomputeFactory precomputeFactory) {
+ * return precomputeFactory.forEachUnfiltered(Shift.class)
+ * .join(Shift.class,
+ * Joiners.overlapping(Shift::getStart, Shift::getEnd),
+ * Joiners.equal(Shift::getEmployee))
+ * .filter((left, right) -> left != right);
+ * }
+ *
+ * Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
+ * return constraintFactory.precompute(this::overlappingShifts)
+ * .penalize(HardSoftScore.ONE_HARD)
+ * .asConstraint("Overlapping shifts");
+ * }
+ *
+ * + * You can (and should) use variables after the precompute. So the example above + * can be rewritten correctly like this and would not cause score corruptions: + *
+ * + *
+ * BiConstraintStream<Shift, Shift> overlappingShifts(PrecomputeFactory precomputeFactory) {
+ * return precomputeFactory.forEachUnfiltered(Shift.class)
+ * .join(Shift.class,
+ * Joiners.overlapping(Shift::getStart, Shift::getEnd))
+ * .filter((left, right) -> left != right);
+ * }
+ *
+ * Constraint noOverlappingShifts(ConstraintFactory constraintFactory) {
+ * return constraintFactory.precompute(this::overlappingShifts)
+ * .filter((left, right) -> left.getEmployee() != null && left.getEmployee().equals(right.getEmployee()))
+ * .penalize(HardSoftScore.ONE_HARD)
+ * .asConstraint("Overlapping shifts");
+ * }
+ *
+ */
+ + * For example, + *
+ * + *
+ * precomputeFactory.forEachUnfiltered(Shift.class) + * .join(Shift.class, Joiners.equal(Shift::getLocation)); + *+ *
+ * Would roughly be equivalent to + *
+ * + *
+ * constraintFactory.forEachUnfiltered(Shift.class) + * .join(constraintFactory.forEachUnfiltered(Shift.class), + * Joiners.equal(Shift::getLocation)); + *+ *
+ * Important: no variables can be referenced in any operations performed
+ * by the returned {@link ConstraintStream}, otherwise a score corruption will
+ * occur.
+ * See the note in {@link ConstraintFactory#precompute(Function)} for
+ * more details.
+ *
+ * @param the type of the matched problem fact or {@link PlanningEntity planning entity}
+ */
+ UniConstraintStream forEachUnfiltered(Class sourceClass);
+}
diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java
index f72e84d96a..677bde791f 100644
--- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java
+++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java
@@ -33,6 +33,7 @@
import ai.timefold.solver.core.api.score.stream.tri.TriConstraintStream;
import ai.timefold.solver.core.api.score.stream.tri.TriJoiner;
import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream;
+import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStream;
import ai.timefold.solver.core.impl.util.ConstantLambdaUtils;
import org.jspecify.annotations.NonNull;
@@ -1582,9 +1583,21 @@