Skip to main content

sim_incremental_core/projection/
assay_repair.rs

1use std::collections::BTreeSet;
2
3use sim_kernel::{ContentId, Datum, Symbol};
4
5use super::{
6    ConclusionId, FactId,
7    assay::{AssayContract, AssayError},
8};
9
10/// One graph location requiring narrower or more complete projection sensitivity.
11#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub enum ProjectionRepairItem {
13    /// A conclusion declaration is absent or has the wrong sensitivity.
14    Declaration(ConclusionId),
15    /// A declared conclusion-to-fact edge is too broad.
16    GraphEdge {
17        /// Affected conclusion.
18        conclusion: ConclusionId,
19        /// Changed fact responsible for the false prediction.
20        fact: FactId,
21    },
22    /// A non-graph assay contract was violated.
23    Contract(AssayContract),
24}
25
26/// Canonical bounded set of projection declarations or graph edges to repair.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct ProjectionRepairSet {
29    /// Controlled delta that exposed the finding.
30    pub delta: String,
31    /// Canonically ordered, deduplicated repair items.
32    pub items: BTreeSet<ProjectionRepairItem>,
33    id: ContentId,
34}
35
36impl ProjectionRepairSet {
37    pub(super) fn new(
38        delta: &str,
39        items: BTreeSet<ProjectionRepairItem>,
40    ) -> Result<Self, AssayError> {
41        let datum = Datum::Node {
42            tag: Symbol::qualified("projection", "repair-set-v1"),
43            fields: vec![
44                (Symbol::new("delta"), Datum::String(delta.to_owned())),
45                (
46                    Symbol::new("items"),
47                    Datum::Vector(items.iter().map(repair_item_datum).collect()),
48                ),
49            ],
50        };
51        let id = datum
52            .content_id()
53            .map_err(|error| AssayError::Canonical(error.to_string()))?;
54        Ok(Self {
55            delta: delta.to_owned(),
56            items,
57            id,
58        })
59    }
60
61    /// Returns the stable identity used to recognize an unchanged finding.
62    #[must_use]
63    pub fn id(&self) -> &ContentId {
64        &self.id
65    }
66}
67
68/// Architecture review emitted after three unchanged failed repair epochs.
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct ArchitectureFaultReview {
71    /// Unchanged finding identity.
72    pub finding: ContentId,
73    /// Exact number of consecutive epochs.
74    pub epochs: u8,
75}
76
77/// Repair loop disposition. NeedDirection is the terminal third epoch.
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub enum RepairDisposition {
80    /// One bounded repair packet remains allowed.
81    RepairEpoch(u8),
82    /// Architecture review is required before more work or a higher rung.
83    NeedDirection(ArchitectureFaultReview),
84}
85
86/// Tracks consecutive unchanged projection findings without silently waiving one.
87#[derive(Clone, Debug, Default)]
88pub struct ProjectionRepairTracker {
89    finding: Option<ContentId>,
90    epochs: u8,
91}
92
93impl ProjectionRepairTracker {
94    /// Records a failed repair epoch, resetting the count when the finding changes.
95    #[must_use]
96    pub fn record(&mut self, repairs: &ProjectionRepairSet) -> RepairDisposition {
97        if self.finding.as_ref() == Some(repairs.id()) {
98            self.epochs = self.epochs.saturating_add(1).min(3);
99        } else {
100            self.finding = Some(repairs.id().clone());
101            self.epochs = 1;
102        }
103        if self.epochs >= 3 {
104            RepairDisposition::NeedDirection(ArchitectureFaultReview {
105                finding: repairs.id().clone(),
106                epochs: self.epochs,
107            })
108        } else {
109            RepairDisposition::RepairEpoch(self.epochs)
110        }
111    }
112
113    /// Clears consecutive failure state after a passing delta.
114    pub fn clear(&mut self) {
115        self.finding = None;
116        self.epochs = 0;
117    }
118}
119
120fn repair_item_datum(item: &ProjectionRepairItem) -> Datum {
121    match item {
122        ProjectionRepairItem::Declaration(conclusion) => Datum::Node {
123            tag: Symbol::qualified("projection", "repair-declaration-v1"),
124            fields: vec![(
125                Symbol::new("conclusion"),
126                Datum::String(conclusion.as_str().to_owned()),
127            )],
128        },
129        ProjectionRepairItem::GraphEdge { conclusion, fact } => Datum::Node {
130            tag: Symbol::qualified("projection", "repair-edge-v1"),
131            fields: vec![
132                (
133                    Symbol::new("conclusion"),
134                    Datum::String(conclusion.as_str().to_owned()),
135                ),
136                (Symbol::new("fact"), Datum::String(fact.as_str().to_owned())),
137            ],
138        },
139        ProjectionRepairItem::Contract(contract) => Datum::Node {
140            tag: Symbol::qualified("projection", "repair-contract-v1"),
141            fields: vec![(
142                Symbol::new("name"),
143                Datum::String(assay_contract_name(*contract).into()),
144            )],
145        },
146    }
147}
148
149const fn assay_contract_name(contract: AssayContract) -> &'static str {
150    match contract {
151        AssayContract::DeltaClass => "delta-class",
152        AssayContract::UnknownChangedFact => "unknown-changed-fact",
153        AssayContract::Denominator => "denominator",
154        AssayContract::AffectedCeiling => "affected-ceiling",
155        AssayContract::RevisionDelta => "revision-delta",
156        AssayContract::CarriedState => "carried-state",
157        AssayContract::D1UnaffectedFloor => "d1-unaffected-floor",
158        AssayContract::D2UnaffectedFloor => "d2-unaffected-floor",
159        AssayContract::D4UnaffectedFloor => "d4-unaffected-floor",
160        AssayContract::D6CarriedState => "d6-carried-state",
161        AssayContract::SemanticNoOpWork => "semantic-no-op-work",
162    }
163}