feat(neighborhoods): introduce coarse-grained generic moves - #2610
feat(neighborhoods): introduce coarse-grained generic moves#2610triceo wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces coarse-grained generic neighborhood moves for basic and list variables, with supporting preview APIs, migration recipes, tests, and documentation.
Changes:
- Adds pillar, subpillar, mass, list, and sublist moves and providers.
- Adds sampling, dataset, range, and neighborhood-stream infrastructure.
- Adds preview migration support and upgrade documentation.
Four moderate findings remain unresolved.
Reviewed changes
Copilot reviewed 155 out of 155 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Reviewed change |
|---|---|
tools/migration/src/test/java/ai/timefold/solver/migration/preview/NeighborhoodsMigrationRecipeTest.java |
Migration recipe tests |
tools/migration/src/main/java/module-info.java |
Migration module exports |
tools/migration/src/main/java/ai/timefold/solver/migration/v2/ToLatestV2Recipe.java |
V2 migration composition |
tools/migration/src/main/java/ai/timefold/solver/migration/v1/ToLatestV1Recipe.java |
V1 migration composition |
tools/migration/src/main/java/ai/timefold/solver/migration/ToLatestRecipe.java |
Aggregate migration composition |
tools/migration/src/main/java/ai/timefold/solver/migration/preview/PreviewToLatestRecipe.java |
Preview migration aggregation |
tools/migration/src/main/java/ai/timefold/solver/migration/preview/package-info.java |
Preview migration documentation |
tools/migration/src/main/java/ai/timefold/solver/migration/preview/NeighborhoodsMigrationRecipe.java |
Neighborhood API migration |
docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest.adoc |
Latest upgrade guide |
docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc |
V1 upgrade documentation |
docs/src/modules/ROOT/pages/upgrading-timefold-solver/backwards-compatibility.adoc |
Compatibility documentation |
docs/src/modules/ROOT/pages/upgrading-timefold-solver/.upgrading-timefold-solver.adoc |
Upgrade page inclusion |
docs/src/modules/ROOT/nav.adoc |
Documentation navigation |
core/src/test/java/ai/timefold/solver/core/testutil/NeighborhoodTestUtils.java |
Neighborhood test utilities |
core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/multivar/TestdataAllowsUnassignedMultiVarEntityProvidingSolution.java |
Multivariable test data |
core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/SampleTest.java |
Sample API tests |
core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/DatasetTest.java |
Dataset API tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/SwapMoveTest.java |
Swap move tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/SubPillarUnassignMoveProviderTest.java |
Subpillar unassignment tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/SampleValueRangesBiasIT.java |
Value-range bias tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/MassUnassignMoveProviderTest.java |
Mass unassignment tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/MassAssignMoveProviderTest.java |
Mass assignment tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ListSwapMoveProviderTest.java |
List swap provider tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ListChangeMoveProviderTest.java |
List change provider tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMoveTest.java |
Change move tests |
core/src/test/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMoveProviderTest.java |
Change provider tests |
core/src/test/java/ai/timefold/solver/core/impl/util/TriangularNumbersTest.java |
Triangular-number tests |
core/src/test/java/ai/timefold/solver/core/impl/util/TriangleElementFactoryTest.java |
Triangle factory tests |
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/SamplingIteratorBiasIT.java |
Sampling bias tests |
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/PairFairnessBiasIT.java |
Pair fairness tests |
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/DatasetBucketBiasIT.java |
Dataset bucket bias tests |
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/BiasReport.java |
Bias reporting |
core/src/test/java/ai/timefold/solver/core/impl/neighborhood/bias/AbstractBiasIT.java |
Shared bias test support |
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListSwapMoveSelectorTest.java |
Random sublist swap selector tests |
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveSelectorTest.java |
Random sublist change selector tests |
core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelectorTest.java |
Random sublist selector tests |
core/src/main/java/module-info.java |
Core module exports |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/UniMoveConstructor.java |
Unary move construction |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/picking/UniPickingStream.java |
Unary picking stream |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/picking/PickingStream.java |
Picking stream API |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/picking/BiPickingStream.java |
Binary picking stream |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/MoveStreamFactory.java |
Move stream factory |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStream.java |
Unary enumeration stream |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/BiEnumeratingStream.java |
Binary enumeration stream |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/UniDatasetInstance.java |
Unary dataset instances |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/SubListSampler.java |
Sublist sampling |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Sampler.java |
Sampler API |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Sample.java |
Sample API |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Range.java |
Range API |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/package-info.java |
Sample API documentation |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/DefaultSample.java |
Sample implementation |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/DefaultRange.java |
Range implementation |
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/BiDatasetInstance.java |
Binary dataset instances |
core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java |
Mutable solution access |
core/src/main/java/ai/timefold/solver/core/preview/api/move/Move.java |
Move contract |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/UnassignMoveProvider.java |
Unassignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SwapMoveProvider.java |
Swap move provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SwapMove.java |
Swap move implementation |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SubListUnassignMove.java |
Sublist unassignment |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SubListChangeMove.java |
Sublist change move |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/PillarUnassignMoveProvider.java |
Pillar unassignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/package-info.java |
Built-in move documentation |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassUnassignMoveProvider.java |
Mass unassignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListUnassignMoveProvider.java |
Mass list unassignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListDestinationMoveIterator.java |
Mass list destination iteration |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListChangeMoveProvider.java |
Mass list change provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListChangeMove.java |
Mass list change move |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListAssignMoveProvider.java |
Mass list assignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassChangeMoveProvider.java |
Mass change provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassChangeMove.java |
Mass change move |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassAssignMoveProvider.java |
Mass assignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListUnassignMoveProvider.java |
List unassignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListUnassignMove.java |
List unassignment move |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListSwapMoveProvider.java |
List swap provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListAssignMoveProvider.java |
List assignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ListAssignMove.java |
List assignment move |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMoveProvider.java |
Change move provider |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/ChangeMove.java |
Change move implementation |
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/AssignMoveProvider.java |
Assignment provider |
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/VariableMetaModel.java |
Variable metadata |
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/ShadowVariableMetaModel.java |
Shadow variable metadata |
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/ShadowEntityMetaModel.java |
Shadow entity metadata |
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java |
Planning entity metadata |
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/GenuineVariableMetaModel.java |
Genuine variable metadata |
core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/ElementPosition.java |
Element position metadata |
core/src/main/java/ai/timefold/solver/core/impl/util/TriangularNumbers.java |
Triangular-number utilities |
core/src/main/java/ai/timefold/solver/core/impl/util/TriangleElementFactory.java |
Triangle element factory |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/sampling/InnerUniSamplingStream.java |
Unary sampling implementation |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/sampling/InnerSamplingStream.java |
Sampling implementation |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/InnerUniPickingStream.java |
Unary picking implementation |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/InnerPickingStream.java |
Picking implementation |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/DefaultUniPickingStream.java |
Default unary picking |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/picking/DefaultBiPickingStream.java |
Default binary picking |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/common/AbstractLeftDatasetInstance.java |
Dataset join support |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/DefaultMoveStreamFactory.java |
Move stream construction |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/dataset/sample/SampleAssembler.java |
Sample assembly |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/dataset/JustInTimeBiDatasetInstance.java |
Just-in-time binary datasets |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/dataset/DefaultUniDatasetInstance.java |
Default unary datasets |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/DefaultNeighborhoodTestContext.java |
Neighborhood test context |
core/src/main/java/ai/timefold/solver/core/impl/neighborhood/DefaultNeighborhoodProvider.java |
Neighborhood provider |
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/TriangleElementFactory.java |
List selection factory |
core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/RandomSubListSelector.java |
Random sublist selection |
core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/NullAllowingValueRange.java |
Null-allowing value ranges |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java |
Solution metadata |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/InnerPlanningEntityMetaModel.java |
Planning entity metadata construction |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultShadowVariableMetaModel.java |
Shadow variable metadata construction |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultShadowEntityMetaModel.java |
Shadow entity metadata construction |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java |
Planning variable metadata construction |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningListVariableMetaModel.java |
Planning list metadata construction |
core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultGenuineEntityMetaModel.java |
Genuine entity metadata construction |
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/RetiringRandomIterator.java |
Random iterator support |
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/MappingRetiringRandomIterator.java |
Mapped random iterator support |
CONSTITUTION.md |
Repository guidance |
Suppressed comments (7)
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MassListChangeMove.java:79
Sampleexplicitly allows null members, but this move dereferences every member here (and again ingetPlanningValues()), so a validSamplecan make the public list mass-change move fail with an unhelpfulNullPointerException; either reject null members when constructing the move or document a non-null-member precondition.
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/MoveProviderUtil.java:232- The provider groups all entities, including those whose variable is null, but
containsInEveryis called on ranges with nullable wrappers removed, so legal swaps involving an unassigned value are always rejected; handle null as legal whenvariableMetaModel.allowsUnassigned()is true.
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/PillarChangeMoveProvider.java:42 - The new Javadoc contains the truncated word
winstead ofwould, making the explanation grammatically incorrect.
core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/SampleValueRanges.java:126 ValueRange.getSize()is a long, butadmittedCountis an int, sopickExactlyoverflows after 2^31−1 admitted values and eventually passes a non-positive bound tonextInt; use a long counter andnextLong.
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/DefaultSample.java:48Sampledocuments value-based equality for any two samples, but this implementation only comparesDefaultSample, so a valid customSamplewith the same members is considered unequal; compare against theSampleinterface and preserve the same member-based hash contract.
core/src/main/java/ai/timefold/solver/core/preview/api/neighborhood/stream/dataset/sample/Sample.java:45first()is annotated nullable andDefaultSamplereturns null when null is the first member, but this Javadoc promises a never-null result, which gives callers an unsafe contract; document that the result may be null.
docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest.adoc:9- This unconditional promise contradicts the preview API migration documented later on this page and the preview compatibility policy: users on preview APIs can be broken by exactly the sampling-to-picking rename; qualify the guarantee to stable APIs or explicitly exclude preview APIs.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
zepfred
left a comment
There was a problem hiding this comment.
The review of the remaining code is complete and looks great to me. I have some minor comments and questions, but overall, the implementation of the building blocks for the move providers is solid, and the test coverage is adequate.
Please update the list of built-in moves, and we are good to go once the sonar and conflict are resolved.
|
Comments resolved, docs pushed. |
There was a problem hiding this comment.
🟡 Changes recommended
The migration “ToLatest” composition currently runs RemoveUnusedImports redundantly in multiple recipe layers, and the new module export of an internal move package broadens the module API surface in a way that should be explicitly justified or handled via test-only JPMS exports.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 189/231 changed files
- Comments generated: 3
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is large and touches core neighborhood/move-stream internals and preview APIs in ways that warrant careful human validation of behavioral and performance impacts.
Review details
- Files reviewed: 189/231 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
PendingListChangeTracker.clear() does not de-escalate from the overflow map, undermining the intended hot-path allocation/performance optimization after the first multi-entity list move.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
core/src/main/java/ai/timefold/solver/core/impl/move/PendingListChangeTracker.java:69
- PendingListChangeTracker.clear() keeps overflowMap allocated after an escalation, which makes all subsequent single-entity brackets pay the map overhead and defeats the class’s “single-slot common case” optimization.
- Files reviewed: 189/231 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces and refactors core Neighborhoods/move-generation APIs across many modules, requiring deeper correctness and performance validation than can be confidently automated here.
Review details
Suppressed comments (1)
core/src/main/java/ai/timefold/solver/core/api/domain/common/Lookup.java:38
- Public API method Lookup#lookUpNonNullWorkingObject is missing the required @param/@return/@throws Javadoc tags (CONSTITUTION.md:16).
- Files reviewed: 190/232 changed files
- Comments generated: 3
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces broad new Neighborhoods APIs and move-generation behavior while refactoring multiple core subsystems (streams, iterators, metamodel ordering, and migrations), requiring careful human validation of design and correctness.
Review details
Suppressed comments (1)
core/src/main/java/ai/timefold/solver/core/api/domain/common/Lookup.java:38
- The new public API method's Javadoc is missing the required @param/@return tags mandated by CONSTITUTION.md for public methods.
- Files reviewed: 190/232 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
A newly introduced migration recipe class violates the repo’s “classes must be final unless extensible” convention and should be made final before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 190/232 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
It touches a broad set of core Neighborhoods APIs/implementations and introduces new move-generation primitives, requiring careful human validation beyond the few localized issues identified.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/NullAllowingValueRange.java:66
- This iterator can return null (see next() implementation), so its return type should be annotated to avoid leaking nulls through a non-null generic signature.
This issue also appears on line 96 of the same file.
core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/NullAllowingValueRange.java:96
- This iterator can return null (via get(0)), so its return type should be annotated to avoid leaking nulls through a non-null generic signature.
- Files reviewed: 190/232 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces and rewires substantial core move-generation and preview API surface area changes, which are difficult to fully validate for correctness and performance without broader, human-led review.
Review details
- Files reviewed: 195/239 changed files
- Comments generated: 1
- Review effort level: Lite
| void clear() { | ||
| singleEntity = null; | ||
| singleAction = null; | ||
| if (overflowMap != null) { | ||
| overflowMap.clear(); | ||
| } | ||
| } |
Introduces:
Documentation will only be written after we agree on all the things that are in here. The chances of having to redo the docs because of review changes are too big.
My recommendation for review: start with the moves and move providers. Understand them. Before we discuss the implementation, we need to be in agreement that we want these moves, and not some others. (The implementation is dictated by the requirements of the moves - therefore if we decide to change the moves, we will change the implementation.)