Skip to main content

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;