1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/// Serializable error envelope emitted as a single NDJSON line.
#[derive(Debug, Serialize, JsonSchema)]
pub struct ErrorJson {
/// Always true, marks this line as an error event.
pub error: bool,
/// Machine-readable error code string.
pub code: &'static str,
/// Suggested process exit code.
pub exit: u8,
/// Human-readable error message.
pub message: String,
/// Filesystem path related to the error, if applicable.
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// Error class: transient, conflict, `precondition_failed`, or permanent.
pub error_class: &'static str,
/// Whether a retry may resolve this error.
pub retryable: bool,
/// Optional actionable suggestion for the caller.
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
/// Workspace root used for jail validation, if applicable.
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
/// 1-based index of the failed `--old`/`--new` pair (multi-pair edit, G117).
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_pair_index: Option<u64>,
/// Total number of `--old`/`--new` pairs in the invocation (multi-pair edit, G117).
#[serde(skip_serializing_if = "Option::is_none")]
pub pairs_total: Option<u64>,
/// Per-pair diagnostics up to and including the failed pair (multi-pair edit, G117).
/// Pairs after the failure were never attempted and are absent.
#[serde(skip_serializing_if = "Option::is_none")]
pub pair_results: Option<Vec<PairResult>>,
/// Closest near-miss when a match cascade fails (v0.1.29 P0-2).
#[serde(skip_serializing_if = "Option::is_none")]
pub best_candidate: Option<BestCandidate>,
/// Additional near-miss candidates for `did_you_mean` (v0.1.30).
#[serde(skip_serializing_if = "Option::is_none")]
pub candidates: Option<Vec<BestCandidate>>,
/// Match count when ambiguity is reported (v0.1.30).
#[serde(skip_serializing_if = "Option::is_none")]
pub match_count: Option<u64>,
/// Similar basenames when path is missing (v0.1.30).
#[serde(skip_serializing_if = "Option::is_none")]
pub similar_paths: Option<Vec<String>>,
}
impl ErrorJson {
/// Build an [`ErrorJson`] from a domain error with default empty context.
///
/// Equivalent to `from_error_with_context(err, &ErrorContext::default())`.
/// Use [`Self::from_error_with_context`] when workspace provenance is known
/// so the suggestion for `WorkspaceJail` is precise.
#[cold]
#[track_caller]
pub fn from_error(err: &AtomwriteError) -> Self {
Self::from_error_with_context(err, &ErrorContext::default())
}
/// Build an [`ErrorJson`] from a domain error and a diagnostic context.
///
/// The context allows the suggestion text to be precise. In particular,
/// `WorkspaceJail` errors report different remediation depending on
/// whether the user already supplied a workspace root via `--workspace`
/// or CLI `--workspace` (GAP 13 fix).
#[cold]
#[track_caller]
pub fn from_error_with_context(err: &AtomwriteError, ctx: &ErrorContext) -> Self {
let workspace = match err {
AtomwriteError::WorkspaceJail { workspace, .. } => {
Some(workspace.display().to_string())
}
_ => None,
};
// Cold error path: clone out of `Box` / `Option` fields for the
// public JSON surface. Source remains borrowed (`&AtomwriteError`).
let (failed_pair_index, pairs_total, pair_results) = match err {
AtomwriteError::EditPairFailed {
index,
total,
pair_results,
..
} => (
Some(*index),
Some(*total),
Some(pair_results.as_ref().clone()),
),
_ => (None, None, None),
};
let best_candidate = match err {
AtomwriteError::MatchFailed { best_candidate, .. }
| AtomwriteError::MatchAmbiguous { best_candidate, .. }
| AtomwriteError::EditPairFailed { best_candidate, .. } => {
best_candidate.as_deref().cloned()
}
_ => None,
};
let candidates = match err {
AtomwriteError::MatchFailed { candidates, .. } => candidates.clone(),
_ => None,
};
let match_count = match err {
AtomwriteError::MatchAmbiguous { count, .. } => Some(*count),
_ => None,
};
Self {
error: true,
code: err.error_code(),
exit: err.exit_code(),
message: err.to_string(),
path: err.path().map(|p| p.display().to_string()),
error_class: err.error_class().as_str(),
retryable: err.is_retryable(),
suggestion: suggestion_for(err, ctx),
workspace,
failed_pair_index,
pairs_total,
pair_results,
best_candidate,
candidates,
match_count,
similar_paths: similar_paths_for(err, ctx),
}
}
}