1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Move trait definition.
use std::borrow::Cow;
use std::fmt::Debug;
use solverforge_core::domain::PlanningSolution;
use solverforge_scoring::Director;
use super::MoveTabuSignature;
use crate::stats::CandidateTraceIdentity;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MoveAffectedEntity<'a> {
pub descriptor_index: usize,
pub entity_index: usize,
pub variable_name: &'a str,
}
/// A move that modifies one or more planning variables.
///
/// Moves are fully monomorphized for maximum performance - no boxing, no virtual dispatch.
/// Undo is handled by move-owned typed data, not boxed director callbacks.
///
/// # Type Parameters
/// * `S` - The planning solution type
///
/// # Implementation Notes
/// - Moves should be lightweight
/// - Moves are NEVER cloned - ownership transfers via arena indices
/// - Methods are generic over D to allow use with concrete directors
pub trait Move<S: PlanningSolution>: Send + Sync + Debug {
type Undo: Send;
/* Returns true if this move can be executed in the current state.
A move is not doable if:
- The source value equals the destination value (no change)
- The move's own structural and value-domain rules reject it
- The move would violate hard constraints that can be detected early
*/
fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool;
/* Executes this move, modifying the working solution.
This method modifies the planning variables through the score director.
Returns concrete undo data that can be passed back to `undo_move`.
*/
fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo;
fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo);
/* Trial path with a retained solve scratch pool of retired undo
journals. The default ignores the pool and delegates to the plain pair;
long-journal families override both to recycle undo storage across
trials, so arbitrary-long edit sets do not grow the heap per trial. */
fn do_move_with_scratch<D: Director<S>>(
&self,
score_director: &mut D,
journal_pool: &mut Vec<Self::Undo>,
) -> Self::Undo {
let _ = journal_pool;
self.do_move(score_director)
}
fn undo_move_with_scratch<D: Director<S>>(
&self,
score_director: &mut D,
undo: Self::Undo,
journal_pool: &mut Vec<Self::Undo>,
) {
let _ = journal_pool;
self.undo_move(score_director, undo)
}
/* Doability check that may run an explicit working preview on the LIVE
director.
Default: identical to `is_doable` with the director reborrowed shared.
Sequential/compound families override this where their later legs read
the earlier legs' effects; the phase trial path calls THIS variant so the
preview mutation is owned by the phase-level orchestration instead of a
clone-out substitute director.
*/
fn is_doable_with_working_preview<D: Director<S>>(&self, score_director: &mut D) -> bool {
self.is_doable(score_director)
}
fn descriptor_index(&self) -> usize;
fn entity_indices(&self) -> &[usize];
fn variable_name(&self) -> &str;
fn telemetry_label(&self) -> &'static str {
"move"
}
fn requires_hard_improvement(&self) -> bool {
false
}
fn requires_score_improvement(&self) -> bool {
false
}
/* This move's tabu signature. Moves that own a prebuilt signature
(static compositions) lend it as `Cow::Borrowed`; moves that derive one
from the working solution return `Cow::Owned`. Wrappers forward this
method, so a borrowed signature survives every layer of wrapping. */
fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> Cow<'_, MoveTabuSignature>;
/// Returns the canonical identity used by opt-in candidate-pull tracing.
///
/// Returns an explicit logical-coordinate identity for candidate tracing.
///
/// This intentionally does *not* derive from tabu metadata: tabu keys may
/// contain representation-specific hashes. A move family that has not
/// supplied a cross-representation coordinate grammar returns `None`, and
/// the resulting trace is explicitly non-qualifying rather than faking a
/// comparable key.
fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
None
}
/// Visits every entity this move changes, passing a concrete visitor by
/// value. `V` stays monomorphized end to end: no `dyn` dispatch, no
/// intermediate collection, every affected entity reported exactly once
/// in the move's own canonical order.
fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
let descriptor_index = self.descriptor_index();
let variable_name = self.variable_name();
for &entity_index in self.entity_indices() {
visitor(MoveAffectedEntity {
descriptor_index,
entity_index,
variable_name,
});
}
}
}