Skip to main content

lsp_max_protocol/
repair.rs

1//! Read-only `max/repairPlanSynth` and `max/whatIf` protocol surface.
2//!
3//! A diagnostic tells an agent *that* a law axis is unsatisfied. This module
4//! projects each known diagnostic family onto a structured, bounded **repair
5//! plan**: an ordered list of [`RepairStep`]s an agent or CI runner can act on.
6//! It also exposes a [`simulate_admission`] *what-if* gate that answers, without
7//! touching any file, what admission verdict a hypothetical world-state would
8//! produce.
9//!
10//! This surface is **read-only**. It emits plans and intents; it never mutates
11//! files. A [`RepairPlanResult`] is guidance, not an applied change — any actual
12//! mutation must route through the `CodeAction -> clap-noun-verb -> Receipt`
13//! chain, never through this observation surface. A `verb` named on a step is a
14//! *suggested* `clap-noun-verb` actuation, not an executed one.
15//!
16//! All status fields carry **bounded statuses only** (`ADMITTED`, `PARTIAL`,
17//! `UNKNOWN`, `REFUSED`, `BLOCKED`). The three-state law holds throughout:
18//! `UNKNOWN` is never coerced into `ADMITTED` or `REFUSED`. An unrecognized
19//! diagnostic id yields an `UNKNOWN` plan with **no** fabricated steps, and a
20//! what-if with any unknown axis yields `UNKNOWN` regardless of how many axes
21//! are admitted.
22
23use serde::{Deserialize, Serialize};
24
25// ---------------------------------------------------------------------------
26// Method name constants (read-only surface — the LSP never mutates files)
27// ---------------------------------------------------------------------------
28
29/// max/repairPlanSynth — Synthesize a bounded, ordered repair plan for a diagnostic
30/// id. Read-only: returns a [`RepairPlanResult`]; never applies the repair.
31pub const METHOD_REPAIR_PLAN: &str = "max/repairPlanSynth";
32
33/// max/whatIf — Simulate the admission verdict for a hypothetical world-state.
34/// Read-only: returns a [`WhatIfResult`]; observes nothing on disk and changes
35/// no state.
36pub const METHOD_WHAT_IF: &str = "max/whatIf";
37
38// ---------------------------------------------------------------------------
39// Bounded status constants
40// ---------------------------------------------------------------------------
41
42/// Bounded status: the plan/axis cleared the admission bar.
43pub const STATUS_ADMITTED: &str = "ADMITTED";
44/// Bounded status: a partial path exists but does not clear the bar on its own.
45pub const STATUS_PARTIAL: &str = "PARTIAL";
46/// Bounded status: admissibility cannot be determined from what is known.
47pub const STATUS_UNKNOWN: &str = "UNKNOWN";
48/// Bounded status: an explicit refusal by law.
49pub const STATUS_REFUSED: &str = "REFUSED";
50/// Bounded status: an active ANDON signal blocks progress until it clears.
51pub const STATUS_BLOCKED: &str = "BLOCKED";
52
53// ---------------------------------------------------------------------------
54// max/repairPlanSynth params / result
55// ---------------------------------------------------------------------------
56
57/// One ordered step in a repair plan. A step describes an intent, never an
58/// applied mutation.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct RepairStep {
61    /// 1-based position of this step within the plan.
62    pub order: usize,
63    /// The action an agent or CI runner should take.
64    pub action: String,
65    /// Why this step is part of the bounded path toward admission.
66    pub rationale: String,
67    /// Suggested `clap-noun-verb` actuation, when one applies. This is a
68    /// suggestion only; the LSP surface never invokes it.
69    pub verb: Option<String>,
70}
71
72/// Parameters for `max/repairPlanSynth`.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct RepairPlanParams {
75    /// The diagnostic id (or law id) to synthesize a plan for.
76    pub diagnostic_id: String,
77}
78
79/// Result of `max/repairPlanSynth`.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct RepairPlanResult {
82    /// Echo of the requested diagnostic id.
83    pub diagnostic_id: String,
84    /// Bounded status string (`ADMITTED`, `PARTIAL`, `UNKNOWN`, `REFUSED`,
85    /// `BLOCKED`).
86    pub status: String,
87    /// Ordered, bounded repair steps. Empty when the id is unrecognized; the
88    /// surface never fabricates steps for an `UNKNOWN` plan.
89    pub steps: Vec<RepairStep>,
90    /// Repairability of the diagnostic, mirroring
91    /// [`crate::diagnostics::Repairability`] as a bounded string.
92    pub repairability: String,
93    /// Human-readable summary of the plan's intent.
94    pub summary: String,
95}
96
97impl RepairPlanResult {
98    /// The `UNKNOWN` plan for an unrecognized diagnostic id: empty steps, no
99    /// fabrication, and a repairability of `Unknown`. This is the negative
100    /// control — an absent catalog entry must never present as `ADMITTED` or
101    /// `REFUSED`.
102    fn unknown(diagnostic_id: &str) -> Self {
103        Self {
104            diagnostic_id: diagnostic_id.to_string(),
105            status: STATUS_UNKNOWN.to_string(),
106            steps: Vec::new(),
107            repairability: "Unknown".to_string(),
108            summary: format!(
109                "no repair catalog entry for `{diagnostic_id}`; status UNKNOWN — \
110                 admissibility cannot be determined and is not coerced to REFUSED or ADMITTED"
111            ),
112        }
113    }
114}
115
116/// Build an ordered list of [`RepairStep`]s from `(action, rationale, verb)`
117/// triples, assigning 1-based `order` positions.
118fn steps_from(raw: &[(&str, &str, Option<&str>)]) -> Vec<RepairStep> {
119    raw.iter()
120        .enumerate()
121        .map(|(i, (action, rationale, verb))| RepairStep {
122            order: i + 1,
123            action: (*action).to_string(),
124            rationale: (*rationale).to_string(),
125            verb: verb.map(str::to_string),
126        })
127        .collect()
128}
129
130/// Synthesize a bounded repair plan for a diagnostic id.
131///
132/// Read-only: this consults a small static catalog and reports a plan; it does
133/// not read or change any file.
134///
135/// Status handling, three-state law preserved:
136/// - An ANDON-gated `WASM4PM-*` code -> `BLOCKED`, with steps that route the
137///   agent to resolve the gate (`gate check`). The ANDON must clear before work
138///   proceeds; the plan reflects that floor rather than claiming admission.
139/// - `TPOT2-NONCONVERGENCE` -> `PARTIAL`: a result exists but is below the bar;
140///   the steps widen the search (more generations / larger population).
141/// - `TPOT2-EMPTY-POOL` -> `REFUSED`: with no breeds there is nothing to admit;
142///   the steps direct the agent to repopulate / check the catalog.
143/// - Any unrecognized id -> `UNKNOWN` with an empty plan (negative control).
144pub fn repair_plan_for(diagnostic_id: &str) -> RepairPlanResult {
145    match diagnostic_id {
146        // ANDON-gated process-mining family. The gate is a floor: while it is
147        // set, no shell-side action proceeds, so the bounded status is BLOCKED.
148        "WASM4PM-ANDON" | "WASM4PM-GATE" | "WASM4PM-GATE-BLOCKED" => RepairPlanResult {
149            diagnostic_id: diagnostic_id.to_string(),
150            status: STATUS_BLOCKED.to_string(),
151            steps: steps_from(&[
152                (
153                    "Run the gate check to read the current ANDON state.",
154                    "The ANDON signal is a one-byte gate; the check reports whether it is set \
155                     before any further shell-side action is attempted.",
156                    Some("gate check"),
157                ),
158                (
159                    "Resolve every active WASM4PM-* and GGEN-* Error-severity diagnostic.",
160                    "Lambda_CD blocks while any governed Error is present in the diagnostic \
161                     context; the gate clears only when that set drains.",
162                    None,
163                ),
164                (
165                    "Re-run the gate check and confirm it reports clear before proceeding.",
166                    "Re-reading the gate confirms the BLOCKED floor has lifted; status remains \
167                     BLOCKED until the gate is observed clear.",
168                    Some("gate check"),
169                ),
170            ]),
171            repairability: "Repairable".to_string(),
172            summary: "ANDON gate is active; status BLOCKED — resolve governed diagnostics and \
173                      confirm the gate clears before any build, test, or release action"
174                .to_string(),
175        },
176
177        // Search produced a below-threshold result: a partial path exists.
178        "TPOT2-NONCONVERGENCE" => RepairPlanResult {
179            diagnostic_id: diagnostic_id.to_string(),
180            status: STATUS_PARTIAL.to_string(),
181            steps: steps_from(&[
182                (
183                    "Increase the number of generations the breed search evolves.",
184                    "More generations give the optimizer additional rounds to climb toward the \
185                     admission threshold the previous run fell short of.",
186                    None,
187                ),
188                (
189                    "Increase the population size per generation.",
190                    "A larger population widens the explored breed space, reducing the chance \
191                     the search stalls in a below-threshold local optimum.",
192                    None,
193                ),
194                (
195                    "Re-run the search and compare best observed fitness against the threshold.",
196                    "The status stays PARTIAL until a re-run clears the bar; the comparison is \
197                     what distinguishes PARTIAL from ADMITTED.",
198                    None,
199                ),
200            ]),
201            repairability: "Repairable".to_string(),
202            summary: "breed search ended below the admission threshold; status PARTIAL — widen \
203                      the search (more generations / larger population) and re-run"
204                .to_string(),
205        },
206
207        // Empty breed pool: an explicit refusal — nothing exists to admit.
208        "TPOT2-EMPTY-POOL" => RepairPlanResult {
209            diagnostic_id: diagnostic_id.to_string(),
210            status: STATUS_REFUSED.to_string(),
211            steps: steps_from(&[
212                (
213                    "Inspect the breed catalog to confirm whether any breeds are registered.",
214                    "An empty pool is a REFUSED outcome; the first step is to verify the catalog \
215                     is the cause rather than a filter excluding every entry.",
216                    None,
217                ),
218                (
219                    "Repopulate the breed pool with at least one candidate before searching.",
220                    "With no breeds there is nothing to evaluate; admission is impossible until \
221                     the pool is non-empty.",
222                    None,
223                ),
224            ]),
225            repairability: "Repairable".to_string(),
226            summary: "breed pool is empty; status REFUSED — check the catalog and repopulate the \
227                      pool before a search can yield any admissible breed"
228                .to_string(),
229        },
230
231        // Negative control: anything outside the catalog stays UNKNOWN.
232        _ => RepairPlanResult::unknown(diagnostic_id),
233    }
234}
235
236// ---------------------------------------------------------------------------
237// max/whatIf params / result
238// ---------------------------------------------------------------------------
239
240/// Parameters for `max/whatIf`: a hypothetical world-state to evaluate.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct WhatIfParams {
243    /// Whether an ANDON signal is active in the hypothetical state.
244    pub andon_active: bool,
245    /// Count of law axes whose admissibility is unknown.
246    pub unknown_axes: usize,
247    /// Count of law axes explicitly refused.
248    pub refused_axes: usize,
249}
250
251/// Result of `max/whatIf`.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct WhatIfResult {
254    /// Bounded admission verdict (`BLOCKED`, `REFUSED`, `UNKNOWN`, `ADMITTED`).
255    pub admission: String,
256    /// Why the verdict was reached, naming the governing precedence.
257    pub rationale: String,
258}
259
260/// Simulate the admission verdict for a hypothetical world-state.
261///
262/// Read-only: this is a pure function over the supplied counts; it observes no
263/// file and changes no state.
264///
265/// Strict precedence, three-state law preserved (`UNKNOWN` never collapses):
266/// 1. `andon_active` -> `BLOCKED`. An active ANDON is a hard floor.
267/// 2. `refused_axes > 0` -> `REFUSED`. An explicit refusal dominates remaining
268///    polarities.
269/// 3. `unknown_axes > 0` -> `UNKNOWN`. An undetermined axis is reported as
270///    `UNKNOWN`; it is never coerced to `ADMITTED` (or to `REFUSED`) merely
271///    because other axes are admitted.
272/// 4. Otherwise -> `ADMITTED`.
273pub fn simulate_admission(p: &WhatIfParams) -> WhatIfResult {
274    if p.andon_active {
275        return WhatIfResult {
276            admission: STATUS_BLOCKED.to_string(),
277            rationale: "an ANDON signal is active; status BLOCKED — it is the highest-precedence \
278                        floor and no admission verdict is reached while it is set"
279                .to_string(),
280        };
281    }
282    if p.refused_axes > 0 {
283        return WhatIfResult {
284            admission: STATUS_REFUSED.to_string(),
285            rationale: format!(
286                "{} law axis/axes are explicitly refused; status REFUSED — refusal dominates \
287                 any unknown or admitted axes",
288                p.refused_axes
289            ),
290        };
291    }
292    if p.unknown_axes > 0 {
293        return WhatIfResult {
294            admission: STATUS_UNKNOWN.to_string(),
295            rationale: format!(
296                "{} law axis/axes are undetermined; status UNKNOWN — this is never coerced to \
297                 ADMITTED or REFUSED, even when every other axis is admitted",
298                p.unknown_axes
299            ),
300        };
301    }
302    WhatIfResult {
303        admission: STATUS_ADMITTED.to_string(),
304        rationale: "no ANDON signal, no refused axes, and no unknown axes; status ADMITTED"
305            .to_string(),
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    /// A known code yields an ordered, non-empty plan with a bounded status.
314    #[test]
315    fn known_code_yields_ordered_bounded_plan() {
316        let plan = repair_plan_for("TPOT2-NONCONVERGENCE");
317        assert!(!plan.steps.is_empty(), "a known code must produce steps");
318        assert_eq!(plan.status, STATUS_PARTIAL);
319        // Steps carry 1-based, contiguous, ascending order indices.
320        for (i, step) in plan.steps.iter().enumerate() {
321            assert_eq!(step.order, i + 1, "step order must be 1-based contiguous");
322        }
323        // Status is drawn from the bounded set, never an ad-hoc string.
324        assert!([
325            STATUS_ADMITTED,
326            STATUS_PARTIAL,
327            STATUS_UNKNOWN,
328            STATUS_REFUSED,
329            STATUS_BLOCKED,
330        ]
331        .contains(&plan.status.as_str()));
332    }
333
334    /// The ANDON-gated family is BLOCKED and suggests the gate-check verb.
335    #[test]
336    fn andon_code_is_blocked_and_routes_to_gate() {
337        let plan = repair_plan_for("WASM4PM-ANDON");
338        assert_eq!(plan.status, STATUS_BLOCKED);
339        assert!(!plan.steps.is_empty());
340        assert!(
341            plan.steps
342                .iter()
343                .any(|s| s.verb.as_deref() == Some("gate check")),
344            "an ANDON plan must route the agent to `gate check`"
345        );
346    }
347
348    /// The empty-pool family is an explicit REFUSED with a non-empty plan.
349    #[test]
350    fn empty_pool_code_is_refused() {
351        let plan = repair_plan_for("TPOT2-EMPTY-POOL");
352        assert_eq!(plan.status, STATUS_REFUSED);
353        assert!(!plan.steps.is_empty());
354    }
355
356    /// NEGATIVE CONTROL: an unrecognized code yields UNKNOWN with no fabricated
357    /// steps, and never collapses to REFUSED or ADMITTED.
358    #[test]
359    fn unknown_code_yields_unknown_and_empty_steps() {
360        let plan = repair_plan_for("NOT-A-REAL-DIAGNOSTIC-CODE");
361        assert_eq!(plan.status, STATUS_UNKNOWN);
362        assert!(
363            plan.steps.is_empty(),
364            "an unrecognized code must not fabricate any repair step"
365        );
366        assert_ne!(plan.status, STATUS_REFUSED);
367        assert_ne!(plan.status, STATUS_ADMITTED);
368        assert_eq!(plan.repairability, "Unknown");
369    }
370
371    /// What-if with an unknown axis stays UNKNOWN — not ADMITTED, not REFUSED —
372    /// even when no axis is refused.
373    #[test]
374    fn what_if_unknown_axis_stays_unknown() {
375        let r = simulate_admission(&WhatIfParams {
376            andon_active: false,
377            unknown_axes: 1,
378            refused_axes: 0,
379        });
380        assert_eq!(r.admission, STATUS_UNKNOWN);
381        assert_ne!(r.admission, STATUS_ADMITTED);
382        assert_ne!(r.admission, STATUS_REFUSED);
383    }
384
385    /// What-if precedence: BLOCKED > REFUSED > UNKNOWN > ADMITTED.
386    #[test]
387    fn what_if_precedence_holds() {
388        // ANDON dominates everything else, including refused and unknown axes.
389        let blocked = simulate_admission(&WhatIfParams {
390            andon_active: true,
391            unknown_axes: 3,
392            refused_axes: 3,
393        });
394        assert_eq!(blocked.admission, STATUS_BLOCKED);
395
396        // With no ANDON, a refusal dominates unknown axes.
397        let refused = simulate_admission(&WhatIfParams {
398            andon_active: false,
399            unknown_axes: 3,
400            refused_axes: 1,
401        });
402        assert_eq!(refused.admission, STATUS_REFUSED);
403
404        // With no ANDON and no refusal, an unknown axis dominates admission.
405        let unknown = simulate_admission(&WhatIfParams {
406            andon_active: false,
407            unknown_axes: 1,
408            refused_axes: 0,
409        });
410        assert_eq!(unknown.admission, STATUS_UNKNOWN);
411
412        // Only a fully clean state admits.
413        let admitted = simulate_admission(&WhatIfParams {
414            andon_active: false,
415            unknown_axes: 0,
416            refused_axes: 0,
417        });
418        assert_eq!(admitted.admission, STATUS_ADMITTED);
419    }
420}