use super::*;
pub(crate) fn is_settings_family(path: &str) -> bool {
let mut parts = path.rsplit(['/', '\\']);
let base = parts.next().unwrap_or_default();
let parent = parts.next().unwrap_or_default();
matches!(base, "settings.json" | "settings.local.json") && parent == ".claude"
}
pub(crate) fn extract_external_writes(
session_id: &str,
records: &[Record],
line_nos: &[usize],
mutations: &[TaggedMutation],
) -> Vec<TaggedMutation> {
type VersionSeq = Vec<(usize, usize, Option<String>, u64)>;
let mut seqs: BTreeMap<String, VersionSeq> = BTreeMap::new();
let turns = group_turn_indices_deduped(records, |r| r);
let turn_of = |idx: usize| -> usize {
turns
.iter()
.position(|t| t.contains(&idx))
.unwrap_or_default()
};
for (idx, rec) in records.iter().enumerate() {
let Some(snap) = rec.snapshot.as_ref() else {
continue;
};
let Some(tfb) = snap.get("trackedFileBackups").and_then(|v| v.as_object()) else {
continue;
};
let ts = snap
.get("timestamp")
.and_then(serde_json::Value::as_str)
.map(str::to_string);
for (path, entry) in tfb {
if !is_settings_family(path) {
continue;
}
let Some(version) = entry.get("version").and_then(serde_json::Value::as_u64) else {
continue;
};
seqs.entry(path.clone()).or_default().push((
line_nos[idx],
turn_of(idx),
ts.clone(),
version,
));
}
}
let mut out = Vec::new();
for (path, seq) in &seqs {
let mut prev: Option<(usize, u64)> = None; for (line, turn, ts, version) in seq {
if let Some((prev_line, prev_v)) = prev {
if *version > prev_v {
let touched = mutations.iter().any(|m| {
m.line_no > prev_line
&& m.line_no <= *line
&& (crate::recover::path_matches(Some(path), &m.mutation.path)
|| crate::recover::path_matches(Some(&m.mutation.path), path))
});
if !touched {
out.push(TaggedMutation {
session_id: session_id.to_string(),
is_subagent: false,
parent_session_id: session_id.to_string(),
turn_index: *turn,
line_no: *line,
mutation: FileMutation {
path: path.clone(),
op: FileOp::ExternalWrite,
timestamp_utc: ts.clone(),
is_create: false,
path_verbatim: None,
resolution: None,
command_errored: false,
detail: Some(format!(
"inferred: file-history v{prev_v}->v{version} with no \
tool record in L{prev_line}..L{line}"
)),
},
});
}
}
}
prev = Some((*line, *version));
}
}
out
}