Skip to content

refactor: ScoreDirector is no longer public#2129

Open
triceo wants to merge 3 commits intoTimefoldAI:mainfrom
triceo:scoredirector
Open

refactor: ScoreDirector is no longer public#2129
triceo wants to merge 3 commits intoTimefoldAI:mainfrom
triceo:scoredirector

Conversation

@triceo
Copy link
Collaborator

@triceo triceo commented Feb 19, 2026

No description provided.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the ScoreDirector from public API to internal implementation as part of Timefold Solver 2.0. The changes involve moving ScoreDirector from ai.timefold.solver.core.api.score.director to ai.timefold.solver.core.impl.score.director, and introducing a new PhaseCommandContext interface to replace the direct use of ScoreDirector in the public API. This is a significant breaking change that improves API encapsulation.

Changes:

  • ScoreDirector moved from public API to internal implementation package
  • PhaseCommand interface refactored to use PhaseCommandContext instead of ScoreDirector and BooleanSupplier parameters
  • SolutionPartitioner.splitWorkingSolution() signature changed to accept Solution_ instead of ScoreDirector<Solution_>
  • Documentation updated to reflect new API patterns and remove deprecated content
  • Test domain classes updated (comparator/factory naming consistency improvements)
  • MoveDirector enhanced with new assignValuesAndAdd method and executeTemporary variants

Reviewed changes

Copilot reviewed 123 out of 123 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirector.java Moved from api package to impl package, marked as non-public API
core/src/main/java/ai/timefold/solver/core/api/solver/phase/PhaseCommand.java Interface refactored to use PhaseCommandContext parameter
core/src/main/java/ai/timefold/solver/core/api/solver/phase/PhaseCommandContext.java New interface providing access to working solution and move execution
core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultPhaseCommandContext.java Implementation of PhaseCommandContext
core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/partitioner/SolutionPartitioner.java Interface updated to remove ScoreDirector dependency
core/src/main/java/ai/timefold/solver/core/impl/move/MoveDirector.java Enhanced with new assignValuesAndAdd method and executeTemporary variants
docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc Updated PhaseCommand documentation with new API
docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc Removed deprecated PinningFilter section, updated ProblemChange references
docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc Removed deprecated constraint configuration section
Multiple test files Updated PhaseCommand implementations to use new API, comparator/factory naming improvements
Multiple impl files Updated imports from api.score.director to impl.score.director

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 166 out of 166 changed files in this pull request and generated 2 comments.

@sonarqubecloud
Copy link

Copy link
Contributor

@Christopher-Chianelli Christopher-Chianelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike renaming Rebaser to Lookup; the methods are more awkward, and I believe some changes should be made; in particular, the lookup/rebase method should not have "return null if does not exist" method, and instead I propose these two methods:

@NullMarked
interface Rebaser {
    @Nullable
    <Type_> rebase(@Nullable Type_ toRebase);
    <Type_> rebaseExisting(Type_ toRebase);
}

Both methods fail fast if toRebase does not exist in the ScoreDirector. They only differ in one accepts null (and thus return a nullable), the other does not (and thus always return non-null).

* As defined by {@link #execute(Move, boolean)},
* but with the guarantee of a fresh score.
*/
default void execute(Move<Solution_> move) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the execute commands should return the score (that way, users don't need to go through getWorkingSolution).

isInitializedPredicate = this::isInitialized;
if (entityClass.getPackage() == null) {
LOGGER.warn("The entityClass ({}) should be in a proper java package.", entityClass);
LOGGER.warn("The entityClass ({}) is in an unnamed package.", entityClass);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we warn about this?


@Override
public SelectorBasedKOptListMove<Solution_> rebase(Rebaser rebaser) {
public SelectorBasedKOptListMove<Solution_> rebase(Lookup lookup) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike replacing Rebaser with Lookup. First, when rebasing, you always want to fail-fast (whereas with Lookup, you might want to return null if the entity does not exist for problem changes). Second, the method name is long and awkward; rebase is much nicer.

public SelectorBasedListSwapMove<Solution_> rebase(Lookup lookup) {
return new SelectorBasedListSwapMove<>(variableDescriptor,
Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(leftEntity)),
leftIndex, Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(rightEntity)), rightIndex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objects.requireNonNull is worthless, since lookUpWorkingObjectOrFail will fail-fast if there no corresponding object, and leftEntity/rightEntity are guarantee to not be null.

sourceIndex, Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex);
public SelectorBasedListChangeMove<Solution_> rebase(Lookup lookup) {
return new SelectorBasedListChangeMove<>(variableDescriptor,
Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(sourceEntity)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

return new ListAssignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(planningValue)),
Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex);
public Move<Solution_> rebase(Lookup lookup) {
return new ListAssignMove<>(variableMetaModel, Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(planningValue)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

return new ListChangeMove<>(variableMetaModel,
Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex,
Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex);
Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(sourceEntity)), sourceIndex,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

public Move<Solution_> rebase(Rebaser rebaser) {
return new ListUnassignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex);
public Move<Solution_> rebase(Lookup lookup) {
return new ListUnassignMove<>(variableMetaModel, Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(sourceEntity)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

assertThat(entity.getValueList()).hasSize(1);
assertThat(TestdataObjectSortableDescendingFactory.extractCode(entity.getValueList().get(0).getCode()))
assertThat(
TestdataObjectSortableDescendingComparator.extractCode(entity.getValueList().get(0).getCode()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
TestdataObjectSortableDescendingComparator.extractCode(entity.getValueList().get(0).getCode()))
TestdataObjectSortableDescendingComparator.extractCode(entity.getValueList().getFirst().getCode()))

}

@Test
void assignValuesAndAddToEmptyList() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these duplicates of the tests above?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments