// FR-28 — reject as feedback.
//
// `for_execution()` refuses a result instead of returning a wrong value. That
// refusal is only useful to an agent loop if it can be handed back to the model
// that produced the input, so this file turns the fatal error into a short,
// structured instruction. Nothing here calls a model: `to_prompt()` returns
// text, and the caller decides where to send it.
///|
/// One actionable refusal, ready to be fed back to the model that produced the
/// arguments (FR-28).
pub(all) struct RetryHint {
/// The `E` code that was raised, e.g. `E011`.
error_code : String
/// The `R` code of the refused repair, e.g. `R006`.
repair_code : String
/// Stable name of that repair, e.g. `InsertComma`.
repair_name : String
/// Where the refused repair sits in the input.
position : Position
/// The second reading, when the refusal was an ambiguity (FR-27).
ambiguity : Ambiguity
/// Why the repair could not be accepted.
reason : String
/// The code units around `position`, when the caller passed the input.
context : String
/// Index of `position` inside `context`.
caret : Int32
}
///|
/// Explicit `Eq` implementation for `RetryHint` (structural, for tests).
pub impl Eq for RetryHint with fn equal(self : RetryHint, other : RetryHint) -> Bool {
self.error_code == other.error_code &&
self.repair_code == other.repair_code &&
self.position.offset == other.position.offset
}
///|
/// Explicit `Eq` extension for `RetryHint`.
pub extend RetryHint with Eq::{not_equal, equal}
///|
/// Window of `text` around `offset`: `radius` code units on each side, with
/// the offset itself always inside (FR-28). Never embeds the input in a
/// structured field, so the caller chooses what to show.
pub fn hint_context(
text : String,
offset : Int32,
radius : Int32,
) -> (String, Int32) {
let n = text.length()
let off = if offset < 0 { 0 } else if offset > n { n } else { offset }
let r = if radius < 0 { 0 } else { radius }
let start = if off - r < 0 { 0 } else { off - r }
let end = if off + r + 1 > n { n } else { off + r + 1 }
let sb = StringBuilder()
for i = start; i < end; i = i + 1 {
sb.write_char(Int32::unsafe_to_char(text.at(i).to_int()))
}
(sb.to_string(), off - start)
}
///|
/// Build the hint for a refusal. Returns `None` for every error that is not a
/// refusal of a repair (`EmptyInput`, `SizeLimit`, …), because those are not
/// something the model can fix by re-emitting its arguments.
///
/// `input` is optional: without it the hint still names the position, but it
/// cannot quote the surrounding text.
pub fn RetryHint::of_error(
e : MendError,
input? : String,
radius? : Int32,
) -> RetryHint? {
let r = if radius is Some(v) { v } else { 24 }
let (code, pos, ambiguity, reason) = match e {
StrictViolation(c, p, msg) => (c, p, Unambiguous, msg)
AmbiguousRepair(c, p, a) => (c, p, a, a.message())
_ => return None
}
let (context, caret) = match input {
Some(text) => hint_context(text, pos.offset, r)
None => ("", 0)
}
Some({
error_code: e.code(),
repair_code: code.code(),
repair_name: code.name(),
position: pos,
ambiguity,
reason,
context,
caret,
})
}
///|
/// Render the hint as a prompt fragment (FR-28). Stable across releases; the
/// wording is part of the contract, so it is covered by a test.
///
/// The rendering never contains the whole input, only the window around the
/// refused position, so a caller can put it in a retry message as is.
pub fn RetryHint::to_prompt(self : RetryHint) -> String {
let sb = StringBuilder()
sb.write_string("JSONMend refused the arguments: ")
sb.write_string(self.error_code)
sb.write_string(" ")
sb.write_string(self.repair_code)
sb.write_string(" ")
sb.write_string(self.repair_name)
sb.write_string(" at line ")
sb.write_string(self.position.line.to_string())
sb.write_string(" column ")
sb.write_string(self.position.column.to_string())
sb.write_string(" (offset ")
sb.write_string(self.position.offset.to_string())
sb.write_string(").\nReason: ")
sb.write_string(self.reason)
if self.ambiguity.is_ambiguous() {
sb.write_string("\nAmbiguity: ")
sb.write_string(self.ambiguity.name())
}
if self.context != "" {
sb.write_string("\nContext: ")
sb.write_string(self.context)
sb.write_string("\n")
let mut i = 0
while i < self.caret + 9 {
sb.write_char(' ')
i = i + 1
}
sb.write_char('^')
}
sb.write_string(
"\nRe-emit the arguments as valid JSON with exactly one reading.",
)
sb.to_string()
}
///|
/// One line form, for `--report` and logs.
pub fn RetryHint::to_string(self : RetryHint) -> String {
self.error_code +
" " +
self.repair_code +
" " +
self.repair_name +
" at " +
self.position.line.to_string() +
":" +
self.position.column.to_string() +
" " +
self.reason
}
///|
/// Convenience: the hint of a refusal, or `None`.
pub fn MendError::retry_hint(
self : MendError,
input? : String,
radius? : Int32,
) -> RetryHint? {
match (input, radius) {
(Some(text), Some(r)) => RetryHint::of_error(self, input=text, radius=r)
(Some(text), None) => RetryHint::of_error(self, input=text)
(None, Some(r)) => RetryHint::of_error(self, radius=r)
(None, None) => RetryHint::of_error(self)
}
}