diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc
index 7e75cc46a8e..cbd6ac2d898 100644
--- a/docs/src/modules/ROOT/nav.adoc
+++ b/docs/src/modules/ROOT/nav.adoc
@@ -1,12 +1,12 @@
-* xref:introduction.adoc[leveloffset=+1]
-* Getting started
-** xref:quickstart/overview.adoc[Overview]
-** xref:quickstart/service/getting-started.adoc[Build as a service]
-** Embed as a library
-*** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World guide]
-*** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus guide]
-*** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot guide]
+.Getting started
+* xref:quickstart/overview.adoc[Overview]
+* xref:quickstart/service/getting-started.adoc[Build as a service]
+* Embed as a library
+** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World guide]
+** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus guide]
+** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot guide]
+.Build with Timefold
* Domain modeling
** xref:domain-modeling/domain-modeling.adoc[Guide]
** xref:domain-modeling/modeling-planning-problems.adoc[Building blocks]
@@ -25,7 +25,6 @@
*** xref:running-timefold-solver/service/rest-api.adoc[leveloffset=+1]
*** xref:running-timefold-solver/service/model-config-overrides.adoc[Model configuration overrides]
*** xref:running-timefold-solver/service/model-enrichment.adoc[leveloffset=+1]
-
*** xref:running-timefold-solver/service/demo-data.adoc[leveloffset=+1]
*** xref:running-timefold-solver/service/exposing-metrics.adoc[leveloffset=+1]
*** xref:running-timefold-solver/service/consumer-guide.adoc[Service consumer guide]
@@ -37,58 +36,63 @@
*** xref:running-timefold-solver/library/spring-boot.adoc[leveloffset=+1]
*** xref:running-timefold-solver/library/jpa-jaxb-json-integration.adoc[leveloffset=+1]
+* Responding to change
+** xref:responding-to-change/continuous-planning.adoc[leveloffset=+1]
+** xref:responding-to-change/real-time-planning.adoc[leveloffset=+1]
+** xref:responding-to-change/non-disruptive-replanning.adoc[leveloffset=+1]
+** xref:responding-to-change/recommendation-api.adoc[leveloffset=+1]
+
* Diagnosing the Solver
** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1]
** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1]
-* Deploying to the Timefold Platform
-** xref:deploying-to-platform/introduction.adoc[Overview]
-** xref:deploying-to-platform/guide.adoc[Guide]
-** xref:deploying-to-platform/model-metadata.adoc[leveloffset=+1]
-** xref:deploying-to-platform/metrics.adoc[leveloffset=+1]
+* Use cases
+** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle routing (guide)]
+** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^]
* Optimization algorithms
** xref:optimization-algorithms/overview.adoc[Overview]
** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1]
** xref:optimization-algorithms/local-search.adoc[leveloffset=+1]
** xref:optimization-algorithms/exhaustive-search.adoc[leveloffset=+1]
-** Custom moves
-*** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API]
-*** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1]
-* Responding to change
-** xref:responding-to-change/continuous-planning.adoc[leveloffset=+1]
-** xref:responding-to-change/real-time-planning.adoc[leveloffset=+1]
-** xref:responding-to-change/non-disruptive-replanning.adoc[leveloffset=+1]
-** xref:responding-to-change/recommendation-api.adoc[leveloffset=+1]
-
-* Example use cases
-** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle routing (guide)]
-** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^]
+* Moves
+** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1]
+** Custom Moves
+*** xref:optimization-algorithms/custom-moves.adoc[Move definition]
+*** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API]
-* xref:frequently-asked-questions.adoc[leveloffset=+1]
-* https://github.com/TimefoldAI/timefold-solver/releases[New and noteworthy][leveloffset=+1]
+.Deploy to Timefold Platform
+* xref:deploying-to-platform/introduction.adoc[Overview]
+* xref:deploying-to-platform/guide.adoc[Guide]
+* xref:deploying-to-platform/model-metadata.adoc[leveloffset=+1]
+* xref:deploying-to-platform/metrics.adoc[leveloffset=+1]
-* Upgrading Timefold Solver
+.Upgrading
+* https://github.com/TimefoldAI/timefold-solver/releases[New and noteworthy]
+* Upgrading
** xref:upgrading-timefold-solver/overview.adoc[leveloffset=+1]
-** xref:upgrading-timefold-solver/upgrade-to-latest.adoc[leveloffset=+1]
** xref:upgrading-timefold-solver/upgrade-from-v1.adoc[leveloffset=+1]
-** https://docs.timefold.ai/timefold-solver/1.x/upgrading-timefold-solver/upgrade-from-optaplanner[Upgrading from OptaPlanner][leveloffset=+1]
-** xref:upgrading-timefold-solver/backwards-compatibility.adoc[leveloffset=+1]
-** Migration guides
-*** xref:upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc[leveloffset=+1]
-*** xref:upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc[leveloffset=+1]
-* Commercial editions
-** xref:commercial-editions/commercial-editions.adoc[Overview]
-** xref:commercial-editions/installation.adoc[Installation]
-** xref:commercial-editions/performance-improvements.adoc[leveloffset=+1]
-** xref:constraints-and-score/understanding-the-score.adoc[Score analysis]
-** xref:responding-to-change/recommendation-api.adoc#assignmentRecommendationAPI[Recommendation API]
-** xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection]
-** xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[Multithreaded solving]
-** xref:running-timefold-solver/multithreaded-solving.adoc#partitionedSearch[Partitioned search]
-** xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling]
-** xref:commercial-editions/multistage-moves.adoc[leveloffset=+1]
-** xref:running-timefold-solver/library/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events]
-** xref:commercial-editions/license-management.adoc[License management]
+** xref:upgrading-timefold-solver/upgrade-from-optaplanner.adoc[leveloffset=+1]
+* Migration guides
+** xref:upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc[leveloffset=+1]
+** xref:upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc[leveloffset=+1]
+
+.Commercial editions
+* xref:commercial-editions/commercial-editions.adoc[Overview]
+* xref:commercial-editions/installation.adoc[Installation]
+* xref:commercial-editions/performance-improvements.adoc[leveloffset=+1]
+* xref:constraints-and-score/understanding-the-score.adoc[Score analysis]
+* xref:responding-to-change/recommendation-api.adoc#assignmentRecommendationAPI[Recommendation API]
+* xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection]
+* xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[Multithreaded solving]
+* xref:running-timefold-solver/multithreaded-solving.adoc#partitionedSearch[Partitioned search]
+* xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling]
+* xref:commercial-editions/multistage-moves.adoc[leveloffset=+1]
+* xref:running-timefold-solver/library/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events]
+* xref:commercial-editions/license-management.adoc[License management]
+
+.Additional resources
+* xref:frequently-asked-questions.adoc[leveloffset=+1]
+* link:https://github.com/TimefoldAI/timefold-solver[GitHub]
diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc
index 8dac6b805ad..76d4ebfc65b 100644
--- a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc
+++ b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc
@@ -234,7 +234,7 @@ In that case, use `ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT` instead of the de
== Solution Diff: What changed between now and then?
IMPORTANT: The solution diff is exclusive to Timefold Solver Enterprise Edition.
-It is also only available as a xref:upgrading-timefold-solver/backwards-compatibility.adoc#previewFeatures[preview feature].
+It is also only available as a xref:upgrading-timefold-solver/overview.adoc#previewFeatures[preview feature].
It may be subject to change and must be enabled in the solver configuration by setting: `PLANNING_SOLUTION_DIFF`
Using the `SolutionManager` API, you can compare two solutions provided by the solver, and find out what changed between them:
diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/.optimization-algorithms.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/.optimization-algorithms.adoc
index ddc55f0ef47..dc21935ee71 100644
--- a/docs/src/modules/ROOT/pages/optimization-algorithms/.optimization-algorithms.adoc
+++ b/docs/src/modules/ROOT/pages/optimization-algorithms/.optimization-algorithms.adoc
@@ -3,10 +3,13 @@
:doctype: book
:sectnums:
:icons: font
+// Hides the compatibility anchors in move-selector-reference.adoc, which would otherwise duplicate the IDs in custom-moves.adoc.
+:custom-moves-included:
include::overview.adoc[leveloffset=+1]
include::construction-heuristics.adoc[leveloffset=+1]
include::local-search.adoc[leveloffset=+1]
include::exhaustive-search.adoc[leveloffset=+1]
include::neighborhoods.adoc[leveloffset=+1]
-include::move-selector-reference.adoc[leveloffset=+1]
\ No newline at end of file
+include::move-selector-reference.adoc[leveloffset=+1]
+include::custom-moves.adoc[leveloffset=+1]
diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/custom-moves.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/custom-moves.adoc
new file mode 100644
index 00000000000..b07999b6a97
--- /dev/null
+++ b/docs/src/modules/ROOT/pages/optimization-algorithms/custom-moves.adoc
@@ -0,0 +1,241 @@
+[#customMovesIntroduction]
+= Custom moves
+:doctype: book
+:sectnums:
+:icons: font
+
+Instead of using the generic ``Move``s (such as ``ChangeMove``) you can also implement your own ``Move``.
+Generic and custom ``MoveSelector``s can be xref:optimization-algorithms/move-selector-reference.adoc#combiningMultipleMoveSelectors[combined] as desired.
+
+A custom `Move` can be tailored to work to the advantage of your constraints.
+For example, in examination scheduling, changing the period of an exam A
+would also change the period of all the other exams that need to coincide with exam A.
+
+A custom `Move` is far more work to implement and much harder to avoid bugs than a generic ``Move``.
+After implementing a custom ``Move``, turn on `environmentMode` ``TRACED_FULL_ASSERT`` to check for score corruptions.
+
+For information on the `Move` interface, check out the
+xref:optimization-algorithms/neighborhoods.adoc#neighborhoodsMove[Move Anatomy] section of the Neighborhoods chapter.
+Even though Neighborhoods is an entirely different mechanism than move selectors,
+they share the `Move` interface and therefore the same custom `Move` can be used in both mechanisms.
+
+
+[#generatingCustomMoves]
+== Generating custom moves
+
+Now, let's generate instances of this custom ``Move`` class.
+There are 2 ways:
+
+[#moveListFactory]
+=== `MoveListFactory`: the easy way to generate custom moves
+
+The easiest way to generate custom moves is by implementing the interface ``MoveListFactory``:
+
+[source,java,options="nowrap"]
+----
+public interface MoveListFactory {
+
+ List createMoveList(Solution_ solution);
+
+}
+----
+
+Simple configuration (which can be nested in a `unionMoveSelector` just like any other ``MoveSelector``):
+
+[source,xml,options="nowrap"]
+----
+
+ ...MyMoveFactory
+
+----
+
+Advanced configuration:
+
+[source,xml,options="nowrap"]
+----
+
+ ...
+ ...MyMoveFactory
+
+ ...
+
+
+----
+
+Because the `MoveListFactory` generates all moves at once in a ``List``,
+it does not support `cacheType` ``JUST_IN_TIME``.
+Therefore, `moveListFactory` uses `cacheType` ``STEP`` by default and it scales badly.
+
+To configure values of a `MoveListFactory` dynamically in the solver configuration
+(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters),
+add the `moveListFactoryCustomProperties` element and use xref:running-timefold-solver/library/configuration.adoc#customPropertiesConfiguration[custom properties].
+
+[WARNING]
+====
+A custom `MoveListFactory` implementation must ensure that it does not move xref:domain-modeling/modeling-planning-problems.adoc#pinnedPlanningEntities[pinned entities].
+====
+
+
+[#moveIteratorFactory]
+=== ``MoveIteratorFactory``: generate Custom moves just in time
+
+Use this advanced form to generate custom moves Just In Time
+by implementing the `MoveIteratorFactory` interface:
+
+[source,java,options="nowrap"]
+----
+public interface MoveIteratorFactory {
+
+ long getSize(ScoreDirector scoreDirector);
+
+ Iterator createOriginalMoveIterator(ScoreDirector scoreDirector);
+
+ Iterator createRandomMoveIterator(ScoreDirector scoreDirector, Random workingRandom);
+
+}
+----
+
+The `getSize()` method must return an estimation of the size.
+It doesn't need to be correct, but it's better too big than too small.
+The `createOriginalMoveIterator` method is called if the `selectionOrder` is `ORIGINAL` or if it is cached.
+The `createRandomMoveIterator` method is called for `selectionOrder` ``RANDOM`` combined with cacheType ``JUST_IN_TIME``.
+
+[IMPORTANT]
+====
+Don't create a collection (array, list, set or map) of ``Move``s when creating the ``Iterator``:
+the whole purpose of `MoveIteratorFactory` over `MoveListFactory` is to create a `Move` just in time
+in a custom ``Iterator.next()``.
+====
+
+For example:
+
+[source,java,options="nowrap"]
+----
+public class PossibleAssignmentsOnlyMoveIteratorFactory implements MoveIteratorFactory {
+ @Override
+ public long getSize(ScoreDirector scoreDirector) {
+ // In this case, we return the exact size, but an estimate can be used
+ // if it too expensive to calculate or unknown
+ long totalSize = 0L;
+ var solution = scoreDirector.getWorkingSolution();
+ for (MyEntity entity : solution.getEntities()) {
+ for (MyPlanningValue value : solution.getValues()) {
+ if (entity.canBeAssigned(value)) {
+ totalSize++;
+ }
+ }
+ }
+ return totalSize;
+ }
+
+ @Override
+ public Iterator createOriginalMoveIterator(ScoreDirector scoreDirector) {
+ // Only needed if selectionOrder is ORIGINAL or if it is cached
+ var solution = scoreDirector.getWorkingSolution();
+ var entities = solution.getEntities();
+ var values = solution.getValues();
+ // Assumes each entity has at least one assignable value
+ var firstEntityIndex = 0;
+ var firstValueIndex = 0;
+ while (!entities.get(firstEntityIndex).canBeAssigned(values.get(firstValueIndex))) {
+ firstValueIndex++;
+ }
+
+
+ return new Iterator<>() {
+ int nextEntityIndex = firstEntityIndex;
+ int nextValueIndex = firstValueIndex;
+
+ @Override
+ public boolean hasNext() {
+ return nextEntityIndex < entities.size();
+ }
+
+ @Override
+ public MyChangeMove next() {
+ var selectedEntity = entities.get(nextEntityIndex);
+ var selectedValue = values.get(nextValueIndex);
+ nextValueIndex++;
+ while (nextValueIndex < values.size() && !selectedEntity.canBeAssigned(values.get(nextValueIndex))) {
+ nextValueIndex++;
+ }
+ if (nextValueIndex >= values.size()) {
+ // value list exhausted, go to next entity
+ nextEntityIndex++;
+ if (nextEntityIndex < entities.size()) {
+ nextValueIndex = 0;
+ while (nextValueIndex < values.size() && !entities.get(nextEntityIndex).canBeAssigned(values.get(nextValueIndex))) {
+ // Assumes each entity has at least one assignable value
+ nextValueIndex++;
+ }
+ }
+ }
+ return new MyChangeMove(selectedEntity, selectedValue);
+ }
+ };
+ }
+
+ @Override
+ public Iterator createRandomMoveIterator(ScoreDirector scoreDirector,
+ Random workingRandom) {
+ // Not needed if selectionOrder is ORIGINAL or if it is cached
+ var solution = scoreDirector.getWorkingSolution();
+ var entities = solution.getEntities();
+ var values = solution.getValues();
+
+ return new Iterator<>() {
+ @Override
+ public boolean hasNext() {
+ return !entities.isEmpty();
+ }
+
+ @Override
+ public MyChangeMove next() {
+ var selectedEntity = entities.get(workingRandom.nextInt(entities.size()));
+ var selectedValue = values.get(workingRandom.nextInt(values.size()));
+ while (!selectedEntity.canBeAssigned(selectedValue)) {
+ // This assumes there at least one value that can be assigned to the selected entity
+ selectedValue = values.get(workingRandom.nextInt(values.size()));
+ }
+ return new MyChangeMove(selectedEntity, selectedValue);
+ }
+ };
+ }
+}
+----
+
+[NOTE]
+====
+The same effect can also be achieved using xref:optimization-algorithms/move-selector-reference.adoc#filteredSelection[filtered selection].
+====
+
+Simple configuration (which can be nested in a `unionMoveSelector` just like any other ``MoveSelector``):
+
+[source,xml,options="nowrap"]
+----
+
+ ...
+
+----
+
+Advanced configuration:
+
+[source,xml,options="nowrap"]
+----
+
+ ...
+ ...
+
+ ...
+
+
+----
+
+To configure values of a `MoveIteratorFactory` dynamically in the solver configuration
+(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters),
+add the `moveIteratorFactoryCustomProperties` element and use xref:running-timefold-solver/library/configuration.adoc#customPropertiesConfiguration[custom properties].
+
+[WARNING]
+====
+A custom `MoveIteratorFactory` implementation must ensure that it does not move xref:domain-modeling/modeling-planning-problems.adoc#pinnedPlanningEntities[pinned entities].
+====
\ No newline at end of file
diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc
index 4eca347e6a1..1735bba84d5 100644
--- a/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc
+++ b/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc
@@ -562,7 +562,7 @@ Advanced configuration:
[IMPORTANT]
====
-The new acceptor is available as a xref:upgrading-timefold-solver/backwards-compatibility.adoc#previewFeatures[preview feature]
+The new acceptor is available as a xref:upgrading-timefold-solver/overview.adoc#previewFeatures[preview feature]
It may be subject to change and must be enabled in the solver configuration by setting: `DIVERSIFIED_LATE_ACCEPTANCE`
====
diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc
index 052303f5e0b..1022a670cd4 100644
--- a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc
+++ b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc
@@ -1774,241 +1774,19 @@ xref:optimization-algorithms/move-selector-reference.adoc#listSwapMoveSelector[S
xref:optimization-algorithms/move-selector-reference.adoc#kOptListMoveSelector[K-OPT].
====
-[#customMovesIntroduction]
-== Custom moves
-
-Instead of using the generic ``Move``s (such as ``ChangeMove``) you can also implement your own ``Move``.
-Generic and custom ``MoveSelector``s can be <> as desired.
-
-A custom `Move` can be tailored to work to the advantage of your constraints.
-For example, in examination scheduling, changing the period of an exam A
-would also change the period of all the other exams that need to coincide with exam A.
-
-A custom `Move` is far more work to implement and much harder to avoid bugs than a generic ``Move``.
-After implementing a custom ``Move``, turn on `environmentMode` ``TRACED_FULL_ASSERT`` to check for score corruptions.
-
-For information on the `Move` interface, check out the
-xref:optimization-algorithms/neighborhoods.adoc#neighborhoodsMove[Move Anatomy] section of the Neighborhoods chapter.
-Even though Neighborhoods is an entirely different mechanism than move selectors,
-they share the `Move` interface and therefore the same custom `Move` can be used in both mechanisms.
-
-
-[#generatingCustomMoves]
-=== Generating custom moves
-
-Now, let's generate instances of this custom ``Move`` class.
-There are 2 ways:
-
-[#moveListFactory]
-==== `MoveListFactory`: the easy way to generate custom moves
-
-The easiest way to generate custom moves is by implementing the interface ``MoveListFactory``:
-
-[source,java,options="nowrap"]
-----
-public interface MoveListFactory {
-
- List createMoveList(Solution_ solution);
-
-}
-----
-
-Simple configuration (which can be nested in a `unionMoveSelector` just like any other ``MoveSelector``):
-
-[source,xml,options="nowrap"]
-----
-
- ...MyMoveFactory
-
-----
-
-Advanced configuration:
-
-[source,xml,options="nowrap"]
-----
-
- ...
- ...MyMoveFactory
-
- ...
-
-
-----
-
-Because the `MoveListFactory` generates all moves at once in a ``List``,
-it does not support `cacheType` ``JUST_IN_TIME``.
-Therefore, `moveListFactory` uses `cacheType` ``STEP`` by default and it scales badly.
-
-To configure values of a `MoveListFactory` dynamically in the solver configuration
-(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters),
-add the `moveListFactoryCustomProperties` element and use xref:running-timefold-solver/library/configuration.adoc#customPropertiesConfiguration[custom properties].
-
-[WARNING]
-====
-A custom `MoveListFactory` implementation must ensure that it does not move xref:domain-modeling/modeling-planning-problems.adoc#pinnedPlanningEntities[pinned entities].
-====
-
-
-[#moveIteratorFactory]
-==== ``MoveIteratorFactory``: generate Custom moves just in time
-
-Use this advanced form to generate custom moves Just In Time
-by implementing the `MoveIteratorFactory` interface:
-
-[source,java,options="nowrap"]
-----
-public interface MoveIteratorFactory {
-
- long getSize(ScoreDirector scoreDirector);
-
- Iterator createOriginalMoveIterator(ScoreDirector scoreDirector);
-
- Iterator createRandomMoveIterator(ScoreDirector scoreDirector, Random workingRandom);
-}
-----
-
-The `getSize()` method must return an estimation of the size.
-It doesn't need to be correct, but it's better too big than too small.
-The `createOriginalMoveIterator` method is called if the `selectionOrder` is `ORIGINAL` or if it is cached.
-The `createRandomMoveIterator` method is called for `selectionOrder` ``RANDOM`` combined with cacheType ``JUST_IN_TIME``.
-
-[IMPORTANT]
-====
-Don't create a collection (array, list, set or map) of ``Move``s when creating the ``Iterator``:
-the whole purpose of `MoveIteratorFactory` over `MoveListFactory` is to create a `Move` just in time
-in a custom ``Iterator.next()``.
-====
-
-For example:
-
-[source,java,options="nowrap"]
-----
-public class PossibleAssignmentsOnlyMoveIteratorFactory implements MoveIteratorFactory {
- @Override
- public long getSize(ScoreDirector scoreDirector) {
- // In this case, we return the exact size, but an estimate can be used
- // if it too expensive to calculate or unknown
- long totalSize = 0L;
- var solution = scoreDirector.getWorkingSolution();
- for (MyEntity entity : solution.getEntities()) {
- for (MyPlanningValue value : solution.getValues()) {
- if (entity.canBeAssigned(value)) {
- totalSize++;
- }
- }
- }
- return totalSize;
- }
-
- @Override
- public Iterator createOriginalMoveIterator(ScoreDirector scoreDirector) {
- // Only needed if selectionOrder is ORIGINAL or if it is cached
- var solution = scoreDirector.getWorkingSolution();
- var entities = solution.getEntities();
- var values = solution.getValues();
- // Assumes each entity has at least one assignable value
- var firstEntityIndex = 0;
- var firstValueIndex = 0;
- while (!entities.get(firstEntityIndex).canBeAssigned(values.get(firstValueIndex))) {
- firstValueIndex++;
- }
+ifndef::custom-moves-included[]
+// Compatibility anchors: this content moved to custom-moves.adoc.
+// Keep these IDs so that existing deep links into this page still land somewhere useful.
+[#customMovesIntroduction]
+== Custom moves
- return new Iterator<>() {
- int nextEntityIndex = firstEntityIndex;
- int nextValueIndex = firstValueIndex;
-
- @Override
- public boolean hasNext() {
- return nextEntityIndex < entities.size();
- }
-
- @Override
- public MyChangeMove next() {
- var selectedEntity = entities.get(nextEntityIndex);
- var selectedValue = values.get(nextValueIndex);
- nextValueIndex++;
- while (nextValueIndex < values.size() && !selectedEntity.canBeAssigned(values.get(nextValueIndex))) {
- nextValueIndex++;
- }
- if (nextValueIndex >= values.size()) {
- // value list exhausted, go to next entity
- nextEntityIndex++;
- if (nextEntityIndex < entities.size()) {
- nextValueIndex = 0;
- while (nextValueIndex < values.size() && !entities.get(nextEntityIndex).canBeAssigned(values.get(nextValueIndex))) {
- // Assumes each entity has at least one assignable value
- nextValueIndex++;
- }
- }
- }
- return new MyChangeMove(selectedEntity, selectedValue);
- }
- };
- }
-
- @Override
- public Iterator createRandomMoveIterator(ScoreDirector scoreDirector,
- Random workingRandom) {
- // Not needed if selectionOrder is ORIGINAL or if it is cached
- var solution = scoreDirector.getWorkingSolution();
- var entities = solution.getEntities();
- var values = solution.getValues();
-
- return new Iterator<>() {
- @Override
- public boolean hasNext() {
- return !entities.isEmpty();
- }
-
- @Override
- public MyChangeMove next() {
- var selectedEntity = entities.get(workingRandom.nextInt(entities.size()));
- var selectedValue = values.get(workingRandom.nextInt(values.size()));
- while (!selectedEntity.canBeAssigned(selectedValue)) {
- // This assumes there at least one value that can be assigned to the selected entity
- selectedValue = values.get(workingRandom.nextInt(values.size()));
- }
- return new MyChangeMove(selectedEntity, selectedValue);
- }
- };
- }
-}
-----
-
-[NOTE]
-====
-The same effect can also be accomplished using <>.
-====
-
-Simple configuration (which can be nested in a `unionMoveSelector` just like any other ``MoveSelector``):
-
-[source,xml,options="nowrap"]
-----
-
- ...
-
-----
-
-Advanced configuration:
-
-[source,xml,options="nowrap"]
-----
-
- ...
- ...
-
- ...
-
-
-----
-
-To configure values of a `MoveIteratorFactory` dynamically in the solver configuration
-(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters),
-add the `moveIteratorFactoryCustomProperties` element and use xref:running-timefold-solver/library/configuration.adoc#customPropertiesConfiguration[custom properties].
+[[generatingCustomMoves]][[moveListFactory]][[moveIteratorFactory]]
+Custom moves, including generating them with a `MoveListFactory` or a `MoveIteratorFactory`,
+are described in xref:optimization-algorithms/custom-moves.adoc[Custom moves]:
-[WARNING]
-====
-A custom `MoveIteratorFactory` implementation must ensure that it does not move xref:domain-modeling/modeling-planning-problems.adoc#pinnedPlanningEntities[pinned entities].
-====
+* xref:optimization-algorithms/custom-moves.adoc#generatingCustomMoves[Generating custom moves]
+* xref:optimization-algorithms/custom-moves.adoc#moveListFactory[`MoveListFactory`]
+* xref:optimization-algorithms/custom-moves.adoc#moveIteratorFactory[`MoveIteratorFactory`]
+endif::[]
diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/.upgrading-timefold-solver.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/.upgrading-timefold-solver.adoc
index 04724062baf..ab872b524c7 100644
--- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/.upgrading-timefold-solver.adoc
+++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/.upgrading-timefold-solver.adoc
@@ -5,6 +5,5 @@
:icons: font
include::overview.adoc[leveloffset=+1]
-include::upgrade-to-latest.adoc[leveloffset=+1]
include::upgrade-from-v1.adoc[leveloffset=+1]
-include::backwards-compatibility.adoc[leveloffset=+1]
\ No newline at end of file
+include::upgrade-from-optaplanner.adoc[leveloffset=+1]
\ No newline at end of file
diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc
deleted file mode 100644
index b700c4b1e4d..00000000000
--- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc
+++ /dev/null
@@ -1,55 +0,0 @@
-[#backwardsCompatibility]
-= Backwards compatibility
-:doctype: book
-:icons: font
-
-Timefold Solver separates its API from its implementation:
-
-* **Public API**: All classes under these `api` and `config` namespaces are 100% *backwards compatible* in future minor and hotfix releases:
-** `ai.timefold.solver.core.api`
-** `ai.timefold.solver.benchmark.api`
-** `ai.timefold.solver.test.api`
-** `ai.timefold.solver.*.api`
-** `ai.timefold.solver.core.config`
-** `ai.timefold.solver.benchmark.config`
-* **Implementation classes**: All other classes are _not_ backwards compatible.
-They will change in future major or minor releases,
-but probably not in hotfix releases.
-
-Backwards incompatible changes for a new major version are clearly documented in xref:upgrading-timefold-solver/upgrade-from-v1.adoc#manualUpgrade1to2[the upgrade recipe].
-
-
-[#previewFeatures]
-== Preview features
-
-Timefold Solver includes several components which are only available as preview features.
-We use preview features as means of sharing our work with the community early
-and to get feedback on how to develop them further,
-without being hamstrung by our strict backwards compatibility guarantees.
-
-We deliver preview features to the same standard of quality as the rest of Timefold Solver.
-However, their APIs and behavior are not yet considered stable, pending user feedback.
-Any class, method or field related to these features may change or be removed without prior notice,
-although we strive to avoid this as much as possible.
-
-Preview features need to be activated in the solverConfig.xml file.
-
-[source,xml,options="nowrap"]
-----
-
- DIVERSIFIED_LATE_ACCEPTANCE
- PLANNING_SOLUTION_DIFF
-
-----
-
-Preview features often live in the `preview.api` package, and they are:
-
-- xref:optimization-algorithms/local-search.adoc#diversifiedLateAcceptance[Diversified Late Acceptance] acceptor,
-- xref:constraints-and-score/understanding-the-score.adoc#solutionDiff[Solution diff API]
-in the `ai.timefold.solver.core.preview.api.domain.solution.diff` package and in the `SolutionManager`,
-- Neighborhoods API.
-
-We encourage you to try these preview features and give us feedback on your experience with them.
-Please direct your feedback to
-https://github.com/TimefoldAI/timefold-solver/discussions[Timefold Solver GitHub]
-or to https://discord.com/channels/1413420192213631086/1414521616955605003[Timefold Discord]
\ No newline at end of file
diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/overview.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/overview.adoc
index adb83cbafcf..ce447ffb741 100644
--- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/overview.adoc
+++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/overview.adoc
@@ -15,3 +15,54 @@ in our GitHub repository,
read https://timefold.ai/blog[our blog],
or subscribe to our newsletter.
+== Backwards compatibility
+
+Timefold Solver separates its API from its implementation:
+
+* **Public API**: All classes under these `api` and `config` namespaces are 100% *backwards compatible* in future minor and hotfix releases:
+** `ai.timefold.solver.core.api`
+** `ai.timefold.solver.benchmark.api`
+** `ai.timefold.solver.test.api`
+** `ai.timefold.solver.*.api`
+** `ai.timefold.solver.core.config`
+** `ai.timefold.solver.benchmark.config`
+* **Implementation classes**: All other classes are _not_ backwards compatible.
+They will change in future major or minor releases,
+but probably not in hotfix releases.
+
+Backwards incompatible changes for a new major version are clearly documented in xref:upgrading-timefold-solver/upgrade-from-v1.adoc#manualUpgrade[the upgrade recipe].
+
+[#previewFeatures]
+== Preview features
+
+Timefold Solver includes several components which are only available as preview features.
+We use preview features as means of sharing our work with the community early
+and to get feedback on how to develop them further,
+without being hamstrung by our strict backwards compatibility guarantees.
+
+We deliver preview features to the same standard of quality as the rest of Timefold Solver.
+However, their APIs and behavior are not yet considered stable, pending user feedback.
+Any class, method or field related to these features may change or be removed without prior notice,
+although we strive to avoid this as much as possible.
+
+Preview features need to be activated in the solverConfig.xml file.
+
+[source,xml,options="nowrap"]
+----
+
+ DIVERSIFIED_LATE_ACCEPTANCE
+ PLANNING_SOLUTION_DIFF
+
+----
+
+Preview features often live in the `preview.api` package, and they are:
+
+- xref:optimization-algorithms/local-search.adoc#diversifiedLateAcceptance[Diversified Late Acceptance] acceptor,
+- xref:constraints-and-score/understanding-the-score.adoc#solutionDiff[Solution diff API]
+in the `ai.timefold.solver.core.preview.api.domain.solution.diff` package and in the `SolutionManager`,
+- Neighborhoods API.
+
+We encourage you to try these preview features and give us feedback on your experience with them.
+Please direct your feedback to
+https://github.com/TimefoldAI/timefold-solver/discussions[Timefold Solver GitHub]
+or to https://discord.com/channels/1413420192213631086/1414521616955605003[Timefold Discord]
\ No newline at end of file
diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-optaplanner.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-optaplanner.adoc
new file mode 100644
index 00000000000..4aaff21942a
--- /dev/null
+++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-optaplanner.adoc
@@ -0,0 +1,71 @@
+[#upgradeFromOptaPlanner]
+= Upgrade from OptaPlanner
+:page-aliases: upgrade-and-migration/migrate-from-optaplanner.adoc
+:doctype: book
+:sectnums:
+:icons: font
+
+In spring of 2024, Red Hat announced https://access.redhat.com/articles/7060671[end of life for OptaPlanner].
+Timefold Solver is a faster, feature-rich, and actively developed fork of OptaPlanner by the same team.
+
+== Automatic upgrade
+
+Upgrading from OptaPlanner to Timefold Solver only takes two minutes.
+Run the command below to upgrade your java, build and other code automatically.
+
+NOTE: The script below upgrades your OptaPlanner project to the latest Timefold Solver version, which is {timefold-solver-version}.
+This is a significant jump and might require additional migrations. Check out our other guide for more details on how to upgrade to the xref:upgrading-timefold-solver/upgrade-from-v1.adoc[2.x range].
+
+[tabs]
+====
+Maven::
++
+--
+[source,shell,subs=attributes+]
+----
+mvn org.openrewrite.maven:rewrite-maven-plugin:{rewrite-maven-plugin-version}:run -Drewrite.recipeArtifactCoordinates=ai.timefold.solver:timefold-solver-migration:{timefold-solver-version} -Drewrite.activeRecipes=ai.timefold.solver.migration.ToLatest
+----
+--
+
+Gradle::
++
+--
+[source,shell,subs=attributes+]
+----
+curl https://raw.githubusercontent.com/TimefoldAI/timefold-solver/refs/tags/v{timefold-solver-version}/tools/migration/upgrade-timefold.gradle > upgrade-timefold.gradle ; gradle -Dorg.gradle.jvmargs=-Xmx2G --init-script upgrade-timefold.gradle rewriteRun -DtimefoldSolverVersion={timefold-solver-version} ; rm upgrade-timefold.gradle
+----
+--
+====
+
+include::framework-version-warning.adoc[leveloffset=+1]
+
+Having done that, do a test run of the solver and commit the changes.
+If it doesn't work, just revert it instead and
+https://github.com/timefoldai/timefold-solver/issues[submit an issue].
+We'll fix it with the highest priority.
+
+Timefold Solver 1.x does not support `scoreDRL`, nor is it upgraded automatically.
+If you're still using `scoreDRL` from OptaPlanner 7.x,
+please link:https://timefold.ai/blog/migrating-score-drl-to-constraint-streams[upgrade to Constraint Streams first].
+
+== Manual upgrade
+
+Timefold Solver 1.x is backward compatible with OptaPlanner 8.x,
+except for the following changes:
+
+* Java 17 is the minimum, and Java 21 and 25 are also supported.
+* The Maven/Gradle GAVs changed:
+** The groupId changed from `org.optaplanner` to `ai.timefold.solver`.
+** The artifactIds changed from `optaplanner-\*` to `timefold-solver-*`.
+** ArtifactIds containing `persistence-` changed from `optaplanner-persistence-\*` to `timefold-solver-*`.
+*** For example, `optaplanner-persistence-jackson` changed to `timefold-solver-jackson`.
+* The import statements changed accordingly:
+** `import org.optaplanner...;` changed to `import ai.timefold.solver...;`.
+** `import org.optaplanner.persistence...;` changed to `import ai.timefold.solver...;` too.
+* The JEE dependencies changed from `javax` to `jakarta` to accommodate Spring 3 and Quarkus 3.
+** This is the same difference as between OptaPlanner 8.x and OptaPlanner 9.x.
+* The `OptaPlannerJacksonModule` class is now called `TimefoldJacksonModule`.
+* The deprecated `scoreDRL` support is removed, because Drools with its transitive dependencies have been removed entirely.
+* The unsecure module `persistence-xstream` is removed, because of old, unresolved CVEs in XStream.
+* The deprecated, undocumented `ScoreHibernateType` has been removed because of Jakarta.
+Use JPA's `ScoreConverter` instead.