Skip to main content

m1nd_core/
error.rs

1// === crates/m1nd-core/src/error.rs ===
2
3use crate::types::{EdgeIdx, Generation, NodeId};
4
5/// Central error type covering all failure modes from 05-HARDENING-SYNTHESIS.
6/// Each variant references its FM-ID for traceability.
7#[derive(Debug, thiserror::Error)]
8pub enum M1ndError {
9    // --- Graph integrity ---
10    /// FM-ACT-011: Edge references a node index that does not exist.
11    #[error("dangling edge: edge {edge:?} references non-existent node {node:?}")]
12    DanglingEdge { edge: EdgeIdx, node: NodeId },
13
14    /// FM-PL-006: Graph structure changed since engine was initialised.
15    #[error("graph generation mismatch: expected {expected:?}, actual {actual:?}")]
16    GraphGenerationMismatch {
17        expected: Generation,
18        actual: Generation,
19    },
20
21    /// FM-ACT-016: Attempted to add a node whose interned ID already exists.
22    #[error("duplicate node: interned ID {0:?}")]
23    DuplicateNode(NodeId),
24
25    /// Graph not finalised — CSR not built yet.
26    #[error("graph not finalised: call Graph::finalize() before queries")]
27    GraphNotFinalized,
28
29    /// Graph is empty (zero nodes).
30    #[error("graph is empty")]
31    EmptyGraph,
32
33    // --- Numerical safety ---
34    /// FM-PL-001: Non-finite value detected at a NaN firewall boundary.
35    #[error("non-finite value at firewall: node={node:?}, value={value}")]
36    NonFiniteActivation { node: NodeId, value: f32 },
37
38    /// FM-ACT-012: A tuneable parameter is outside its valid range.
39    #[error("parameter out of range: {name} = {value} (expected {range})")]
40    ParameterOutOfRange {
41        name: &'static str,
42        value: f64,
43        range: &'static str,
44    },
45
46    /// FM-RES-001: Zero or negative wavelength/frequency supplied.
47    #[error("non-positive resonance parameter: {name} = {value}")]
48    NonPositiveResonanceParam { name: &'static str, value: f32 },
49
50    // --- Resource exhaustion ---
51    /// FM-RES-004: Pulse propagation exceeded budget.
52    #[error("pulse budget exhausted: {budget} pulses processed")]
53    PulseBudgetExhausted { budget: u64 },
54
55    /// FM-TMP-005: Causal chain DFS exceeded budget.
56    #[error("chain budget exhausted: {budget} chains generated")]
57    ChainBudgetExhausted { budget: u64 },
58
59    /// FM-TMP-001: Co-change sparse matrix exceeded entry budget.
60    #[error("matrix entry budget exhausted: {budget} entries")]
61    MatrixBudgetExhausted { budget: u64 },
62
63    /// FM-ING-002: Ingestion exceeded timeout.
64    #[error("ingestion timeout after {elapsed_s:.1}s")]
65    IngestionTimeout { elapsed_s: f64 },
66
67    /// A supervising runtime cooperatively cancelled ingestion. The error is
68    /// intentionally data-free so parallel workers always surface one stable,
69    /// matchable outcome rather than a race-dependent file or phase name.
70    #[error("ingestion cancelled")]
71    IngestionCancelled,
72
73    /// FM-ING-002: Ingestion exceeded node count budget.
74    #[error("ingestion node budget exhausted: {budget} nodes")]
75    IngestionNodeBudget { budget: u64 },
76
77    /// FM-TOP-014: Fingerprint pair budget exceeded.
78    #[error("fingerprint pair budget exhausted: {budget} pairs")]
79    FingerprintPairBudget { budget: u64 },
80
81    // --- Analysis quality ---
82    /// FM-XLR-010: XLR cancelled all signal — fallback to hot-only.
83    #[error("XLR over-cancellation: all signal cancelled")]
84    XlrOverCancellation,
85
86    /// FM-TOP-003: Louvain community detection did not converge.
87    #[error("Louvain non-convergence after {passes} passes")]
88    LouvainNonConvergence { passes: u32 },
89
90    /// FM-TOP-010: Power iteration may have diverged.
91    #[error("spectral analysis: power iteration divergence suspected")]
92    SpectralDivergence,
93
94    /// FM-RES-020: Division by zero in normalization (max_amp == 0).
95    #[error("resonance normalization: max amplitude is zero")]
96    ResonanceZeroAmplitude,
97
98    /// FM-ACT-019: Atomic CAS retry limit exceeded during concurrent weight update.
99    #[error("CAS retry limit ({limit}) exceeded at edge {edge:?}")]
100    CasRetryExhausted { edge: EdgeIdx, limit: u32 },
101
102    // --- Ingestion ---
103    /// FM-ING-003: File encoding could not be determined.
104    #[error("encoding detection failed for {path} (confidence={confidence:.2})")]
105    EncodingDetectionFailed { path: String, confidence: f32 },
106
107    /// FM-ING-004: Binary file detected and skipped.
108    #[error("binary file skipped: {path}")]
109    BinaryFileSkipped { path: String },
110
111    /// FM-ING-008: Label collision — multiple nodes share a label.
112    #[error("label collision: {label} maps to {count} nodes")]
113    LabelCollision { label: String, count: usize },
114
115    /// A source-scoped refresh cannot prove that its dependency/ownership
116    /// closure is complete. The caller must stage a governed full projection
117    /// rebuild instead of silently applying an incomplete incremental graph.
118    #[error("full reindex required: {reason}")]
119    FullReindexRequired { reason: String },
120
121    // --- Persistence ---
122    /// FM-PL-007: Corrupt state file on load.
123    #[error("corrupt persistence state: {reason}")]
124    CorruptState { reason: String },
125
126    /// FM-PL-009: Schema drift — edge identity mismatch on import.
127    #[error("schema drift on import: {reason}")]
128    SchemaDrift { reason: String },
129
130    /// OPTIONAL `embed` feature: failed to load or run the static embedding model.
131    #[error("embed error: {0}")]
132    EmbedError(String),
133
134    // --- Counterfactual ---
135    /// FM-CF-001: Seed node was in the removal set.
136    #[error("counterfactual seed overlap: seed {node:?} is in the removal set")]
137    CounterfactualSeedOverlap { node: NodeId },
138
139    // --- Perspective / Lock / Navigation (12-PERSPECTIVE-SYNTHESIS Theme 3) ---
140    /// Theme 3: Unknown tool name in dispatch.
141    #[error("unknown tool: {name}")]
142    UnknownTool { name: String },
143
144    /// Theme 3: Invalid parameters for a tool call.
145    #[error("invalid params for {tool}: {detail}")]
146    InvalidParams { tool: String, detail: String },
147
148    /// Theme 3: Perspective does not exist for agent.
149    #[error("perspective not found: {perspective_id} for agent {agent_id}")]
150    PerspectiveNotFound {
151        perspective_id: String,
152        agent_id: String,
153    },
154
155    /// Theme 3: Perspective route set is stale (generation mismatch).
156    #[error(
157        "perspective stale: {perspective_id} expected gen {expected_gen}, actual {actual_gen}"
158    )]
159    PerspectiveStale {
160        perspective_id: String,
161        expected_gen: u64,
162        actual_gen: u64,
163    },
164
165    /// Theme 3: Agent exceeded max perspective count.
166    #[error("perspective limit exceeded for agent {agent_id}: {current}/{limit}")]
167    PerspectiveLimitExceeded {
168        agent_id: String,
169        current: usize,
170        limit: usize,
171    },
172
173    /// Theme 3: Route set version mismatch (stale cached routes).
174    #[error("route set stale: version {route_set_version}, current {current_version}")]
175    RouteSetStale {
176        route_set_version: u64,
177        current_version: u64,
178    },
179
180    /// Theme 3: Route not found in perspective.
181    #[error("route not found: {route_id} in perspective {perspective_id}")]
182    RouteNotFound {
183        route_id: String,
184        perspective_id: String,
185    },
186
187    /// Theme 3: Cannot navigate back — already at root.
188    #[error("navigation at root: perspective {perspective_id}")]
189    NavigationAtRoot { perspective_id: String },
190
191    /// Theme 3: Branch depth limit exceeded.
192    #[error("branch depth exceeded in {perspective_id}: depth {depth}/{limit}")]
193    BranchDepthExceeded {
194        perspective_id: String,
195        depth: usize,
196        limit: usize,
197    },
198
199    /// Theme 3: Lock not found.
200    #[error("lock not found: {lock_id}")]
201    LockNotFound { lock_id: String },
202
203    /// Theme 3: Lock ownership violation.
204    #[error("lock ownership violation: {lock_id} owned by {owner}, called by {caller}")]
205    LockOwnership {
206        lock_id: String,
207        owner: String,
208        caller: String,
209    },
210
211    /// Theme 3: Lock scope too large (BFS budget exceeded).
212    #[error("lock scope too large: {node_count} nodes exceeds cap of {cap}")]
213    LockScopeTooLarge { node_count: usize, cap: usize },
214
215    /// Theme 3: Agent exceeded max lock count.
216    #[error("lock limit exceeded for agent {agent_id}: {current}/{limit}")]
217    LockLimitExceeded {
218        agent_id: String,
219        current: usize,
220        limit: usize,
221    },
222
223    /// Theme 3: Watcher strategy not supported (e.g. Periodic in V1).
224    #[error("watch strategy not supported: {strategy}")]
225    WatchStrategyNotSupported { strategy: String },
226
227    /// Theme 3: Affinity computation exceeded time budget.
228    #[error("affinity timeout: {elapsed_ms:.1}ms exceeded budget of {budget_ms:.1}ms")]
229    AffinityTimeout { elapsed_ms: f64, budget_ms: f64 },
230
231    // --- Antibody ---
232    /// FM-AB-001: Antibody pattern specificity below minimum threshold.
233    #[error("pattern too broad: specificity {specificity:.2} below minimum {minimum:.2}")]
234    PatternTooBroad { specificity: f32, minimum: f32 },
235
236    /// Antibody not found by ID.
237    #[error("antibody not found: {id}")]
238    AntibodyNotFound { id: String },
239
240    /// Antibody storage limit exceeded.
241    #[error("antibody limit exceeded: {current}/{limit}")]
242    AntibodyLimitExceeded { current: usize, limit: usize },
243
244    // --- Epidemic ---
245    /// Epidemic burnout: too many nodes infected too fast.
246    #[error("epidemic burnout: {infected_pct:.1}% infected in {iteration} iterations")]
247    EpidemicBurnout { infected_pct: f32, iteration: u32 },
248
249    /// No valid infected nodes provided for epidemic simulation.
250    #[error("no valid infected nodes")]
251    NoValidInfectedNodes,
252
253    // --- Flow ---
254    /// No entry points found for flow simulation.
255    #[error("no entry points found for flow simulation")]
256    NoEntryPoints,
257
258    // --- Layers ---
259    /// Layer level not found in detection result.
260    #[error("layer not found: level {level}")]
261    LayerNotFound { level: u8 },
262
263    // --- Ingestion (runtime) ---
264    /// Tree-sitter or extractor runtime error.
265    #[error("ingest error: {0}")]
266    IngestError(String),
267
268    // --- I/O ---
269    #[error("I/O error: {0}")]
270    Io(#[from] std::io::Error),
271
272    #[error("serialization error: {0}")]
273    Serde(#[from] serde_json::Error),
274
275    #[error("persistence failed: {0}")]
276    PersistenceFailed(String),
277}
278
279/// Convenience alias used throughout the crate.
280pub type M1ndResult<T> = Result<T, M1ndError>;