git_policy_cli/policy_convergence.rs
1//! What:
2//! The bounded loop that repeats a whole policy pass after corrections changed the
3//! candidate content,
4//! until the content is stable or the loop gives up.
5//! Why:
6//! A correction by one policy can create a finding for another,
7//! so every changed
8//! state restarts the complete order.
9//! The loop must end:
10//! at most eight passes may
11//! change the content,
12//! and returning to an earlier state is a cycle.
13//! This module owns
14//! only that control flow;
15//! running policies,
16//! applying corrections and storing exact
17//! states stay behind a small interface,
18//! so the loop is testable without Git.
19//!
20//! In TS you'd write (pseudocode):
21//! ```ts
22//! // const convergence = await convergeCommitPolicies({ firstPass, firstSnapshot, ... });
23//! ```
24
25/// What:
26/// The most passes that may change candidate content.
27/// `usize` is the unsigned
28/// integer of list positions and counts (siblings `u32`,
29/// `u64`).
30/// Why:
31/// The incumbent stops after eight changed passes;
32/// `usize` is the type the snapshot
33/// numbers below are counted in.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// const MAXIMUM_CHANGED_PASSES = 8;
38/// ```
39pub const MAXIMUM_CHANGED_PASSES: usize = 8;
40
41/// The engine-failure message of a pass that still proposes corrections after the limit.
42pub const FIX_PASS_LIMIT_MESSAGE: &str =
43 "Policy patches did not converge within eight changed passes.";
44
45/// The engine-failure message of corrections that returned to an earlier state.
46pub const FIX_CYCLE_MESSAGE: &str = "Policy patches repeated an exact prior candidate state.";
47
48/// What:
49/// What one whole policy pass over the current state ended in.
50/// An `enum` is a closed
51/// set of named alternatives.
52/// `#[derive(...)]` asks the compiler to generate copying,
53/// debug printing and `==`.
54/// Why:
55/// Only a pass that proposes corrections continues the loop.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// type PassResult = 'stable' | 'proposed' | 'failed';
60/// ```
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum PassResult {
63 /// No policy proposed a correction:
64 /// the findings of this pass are final.
65 Stable,
66 /// At least one policy proposed a correction.
67 Proposed,
68 /// The pass ended in an engine failure.
69 Failed,
70}
71
72/// What:
73/// The work the loop drives.
74/// A `trait` is a named set of methods a type promises to
75/// provide,
76/// like a TS `interface`;
77/// `&mut self` lends the implementer for writing.
78/// Why:
79/// Exact candidate states can be large,
80/// so the implementer stores them (the
81/// incumbent used private files) and answers equality by snapshot number;
82/// the loop
83/// never holds content.
84/// Gotcha:
85/// Snapshot 0 is the state before the first pass and must exist before the loop starts.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// interface FixPasses { runPass(version: number): PassResult; apply(snapshot: number): boolean; snapshotsEqual(a: number, b: number): boolean }
90/// ```
91pub trait FixPasses {
92 /// Run every policy against the current state;
93 /// `candidate_version` counts earlier changes.
94 fn run_pass(&mut self, candidate_version: usize) -> PassResult;
95 /// Apply the last pass's corrections and store the new exact state under `snapshot`.
96 /// Returns false when the corrections could not be applied.
97 fn apply(&mut self, snapshot: usize) -> bool;
98 /// Whether two stored states are equal byte for byte.
99 fn snapshots_equal(&mut self, left: usize, right: usize) -> bool;
100}
101
102/// What:
103/// How the loop ended.
104/// `Settled` carries the number of passes that changed content.
105/// Why:
106/// The caller reports the stable pass,
107/// or one engine failure for each other ending.
108///
109/// In TS you'd write (pseudocode):
110/// ```ts
111/// type Convergence = { kind: 'settled'; changedPasses: number } | { kind: 'blocked' | 'fix-cycle' | 'fix-pass-limit' };
112/// ```
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum Convergence {
115 /// The last pass proposed nothing,
116 /// or its corrections left the state exactly as it was.
117 Settled {
118 /// Passes whose corrections changed the state.
119 changed_passes: usize,
120 },
121 /// A pass failed,
122 /// or its corrections could not be applied.
123 Blocked,
124 /// Corrections reproduced a state from before the preceding one.
125 FixCycle,
126 /// A pass still proposed corrections after eight changed passes.
127 FixPassLimit,
128}
129
130/// What:
131/// The ending forced by the state just stored under `snapshot`,
132/// or nothing when it
133/// differs from every earlier state.
134/// `&mut dyn FixPasses` lends "any implementer" for
135/// writing (`dyn` means the concrete type is chosen at run time,
136/// like a TS interface
137/// value);
138/// `Option<Convergence>` is "an ending or nothing".
139/// Why:
140/// Which earlier state the new one equals decides the ending:
141/// the preceding state
142/// means "nothing changed",
143/// an older one means the corrections are going in circles.
144///
145/// In TS you'd write (pseudocode):
146/// ```ts
147/// const earlier = visited.findIndex(state => equal(state, current));
148/// if (earlier === visited.length - 1) return settled; if (earlier !== -1) return cycle;
149/// ```
150fn repeated_state_ending(passes: &mut dyn FixPasses, snapshot: usize) -> Option<Convergence> {
151 // `0..snapshot` counts every state stored before this one.
152 for earlier in 0..snapshot {
153 if passes.snapshots_equal(earlier, snapshot) {
154 // The preceding state's number is also the number of changes made before it.
155 if earlier + 1 == snapshot {
156 // `Some(x)` is the "present" case of `Option`.
157 return Some(Convergence::Settled {
158 changed_passes: earlier,
159 });
160 }
161 return Some(Convergence::FixCycle);
162 }
163 }
164 // `None` is the "absent" case of `Option`.
165 return None;
166}
167
168/// What:
169/// Drive passes until the state is stable or the loop must give up.
170/// Why:
171/// The bound is structural:
172/// the `for` loop runs at most eight times,
173/// each round
174/// ending or storing exactly one new state,
175/// and one last pass may only confirm
176/// stability.
177///
178/// In TS you'd write (pseudocode):
179/// ```ts
180/// function converge(passes: FixPasses): Convergence;
181/// ```
182pub fn converge(passes: &mut dyn FixPasses) -> Convergence {
183 // `changed_passes` counts the rounds that stored a new state so far.
184 for changed_passes in 0..MAXIMUM_CHANGED_PASSES {
185 // `match` picks one arm per variant; `{}` does nothing and goes on.
186 match passes.run_pass(changed_passes) {
187 PassResult::Stable => return Convergence::Settled { changed_passes },
188 PassResult::Failed => return Convergence::Blocked,
189 PassResult::Proposed => {}
190 }
191 let snapshot: usize = changed_passes + 1;
192 if !passes.apply(snapshot) {
193 return Convergence::Blocked;
194 }
195 // `if let Some(ending) = ...` runs only when the new state equals a stored one.
196 if let Some(ending) = repeated_state_ending(passes, snapshot) {
197 return ending;
198 }
199 }
200 match passes.run_pass(MAXIMUM_CHANGED_PASSES) {
201 PassResult::Stable => {
202 return Convergence::Settled {
203 changed_passes: MAXIMUM_CHANGED_PASSES,
204 };
205 }
206 PassResult::Failed => return Convergence::Blocked,
207 PassResult::Proposed => return Convergence::FixPassLimit,
208 }
209}
210
211/// Stability,
212/// limit and cycle controls stay out of the release executable.
213#[cfg(test)]
214#[path = "policy_convergence_tests.rs"]
215mod tests;