#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Intervention {
pub position: usize,
pub original_token: String,
pub injected_token: String,
pub timestamp_ms: u64,
}
impl Intervention {
pub fn new(
position: usize,
original_token: impl Into<String>,
injected_token: impl Into<String>,
) -> Self {
let timestamp_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
Self {
position,
original_token: original_token.into(),
injected_token: injected_token.into(),
timestamp_ms,
}
}
pub fn is_effective(&self) -> bool {
self.original_token != self.injected_token
}
}
#[derive(Debug, Clone)]
pub struct InterventionHistory {
pub original_stream: Vec<String>,
pub interventions: Vec<Intervention>,
pub final_stream: Vec<String>,
}
impl InterventionHistory {
pub fn new(original_stream: Vec<String>) -> Self {
let final_stream = original_stream.clone();
Self {
original_stream,
interventions: Vec::new(),
final_stream,
}
}
pub fn apply(&mut self, intervention: Intervention, counterfactual_stream: Vec<String>) {
self.interventions.push(intervention);
self.final_stream = counterfactual_stream;
}
pub fn patch_token(&mut self, position: usize, new_token: impl Into<String>) {
let new_token = new_token.into();
if position < self.final_stream.len() {
let old = self.final_stream[position].clone();
let iv = Intervention::new(position, old, new_token.clone());
self.interventions.push(iv);
self.final_stream[position] = new_token;
}
}
pub fn causal_effect(&self) -> f64 {
if self.interventions.is_empty() {
return 0.0;
}
let first_pos = self
.interventions
.iter()
.map(|iv| iv.position)
.min()
.unwrap_or(0);
let orig = &self.original_stream[first_pos.min(self.original_stream.len())..];
let final_ = &self.final_stream[first_pos.min(self.final_stream.len())..];
if orig.is_empty() && final_.is_empty() {
return 0.0;
}
let max_len = orig.len().max(final_.len());
let differing = orig
.iter()
.zip(final_.iter())
.filter(|(a, b)| a != b)
.count()
+ orig.len().abs_diff(final_.len());
differing as f64 / max_len as f64
}
pub fn summary(&self) -> String {
let mut s = String::new();
s.push_str(&format!(
"Interventions: {}, causal_effect: {:.4}\n",
self.interventions.len(),
self.causal_effect()
));
for (i, iv) in self.interventions.iter().enumerate() {
s.push_str(&format!(
" [{}] pos={} {:?} → {:?}\n",
i, iv.position, iv.original_token, iv.injected_token
));
}
s
}
pub fn original_text(&self) -> String {
self.original_stream.join("")
}
pub fn final_text(&self) -> String {
self.final_stream.join("")
}
}
pub struct TokenEditor {
tokens: Vec<String>,
cursor: usize,
edit_mode: bool,
edit_buffer: String,
edits: Vec<(usize, String, String)>,
}
impl TokenEditor {
pub fn new(tokens: Vec<String>) -> Self {
Self {
tokens,
cursor: 0,
edit_mode: false,
edit_buffer: String::new(),
edits: Vec::new(),
}
}
pub fn cursor_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn cursor_right(&mut self) {
if self.cursor + 1 < self.tokens.len() {
self.cursor += 1;
}
}
pub fn cursor_home(&mut self) {
self.cursor = 0;
}
pub fn cursor_end(&mut self) {
if !self.tokens.is_empty() {
self.cursor = self.tokens.len() - 1;
}
}
pub fn enter_edit_mode(&mut self) {
if self.cursor < self.tokens.len() {
self.edit_mode = true;
self.edit_buffer = self.tokens[self.cursor].clone();
}
}
pub fn edit_push(&mut self, c: char) {
if self.edit_mode {
self.edit_buffer.push(c);
}
}
pub fn edit_pop(&mut self) {
if self.edit_mode {
self.edit_buffer.pop();
}
}
pub fn commit_edit(&mut self) -> Option<(usize, String, String)> {
if !self.edit_mode {
return None;
}
self.edit_mode = false;
let new_token = self.edit_buffer.clone();
let original = self.tokens[self.cursor].clone();
if new_token != original {
self.tokens[self.cursor] = new_token.clone();
let edit = (self.cursor, original, new_token);
self.edits.push(edit.clone());
Some(edit)
} else {
None
}
}
pub fn cancel_edit(&mut self) {
self.edit_mode = false;
self.edit_buffer.clear();
}
pub fn render(&self) -> String {
let mut out = String::new();
for (i, token) in self.tokens.iter().enumerate() {
if i == self.cursor {
if self.edit_mode {
out.push_str(&format!("\x1b[43m{}\x1b[0m", self.edit_buffer));
} else {
out.push_str(&format!("\x1b[4;36m{}\x1b[0m", token));
}
} else {
out.push_str(token);
}
}
out
}
pub fn tokens(&self) -> &[String] {
&self.tokens
}
pub fn edits(&self) -> &[(usize, String, String)] {
&self.edits
}
pub fn to_interventions(&self) -> Vec<Intervention> {
self.edits
.iter()
.map(|(pos, orig, new_tok)| Intervention::new(*pos, orig.clone(), new_tok.clone()))
.collect()
}
pub fn text(&self) -> String {
self.tokens.join("")
}
pub fn len(&self) -> usize {
self.tokens.len()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn is_editing(&self) -> bool {
self.edit_mode
}
}
pub fn per_position_agreement(stream_a: &[String], stream_b: &[String]) -> Vec<bool> {
let len = stream_a.len().max(stream_b.len());
(0..len)
.map(|i| {
match (stream_a.get(i), stream_b.get(i)) {
(Some(a), Some(b)) => a == b,
_ => false,
}
})
.collect()
}
pub fn causal_influence_map(
original: &[String],
counterfactual: &[String],
) -> HashMap<usize, f64> {
let len = original.len().max(counterfactual.len());
let mut map = HashMap::new();
for p in 0..len {
let downstream_len = len.saturating_sub(p + 1);
if downstream_len == 0 {
map.insert(p, 0.0);
continue;
}
let changed: usize = ((p + 1)..len)
.filter(|&i| {
original.get(i).map(String::as_str) != counterfactual.get(i).map(String::as_str)
})
.count();
map.insert(p, changed as f64 / downstream_len as f64);
}
map
}
#[cfg(test)]
mod tests {
use super::*;
fn tok(s: &str) -> String {
s.to_string()
}
fn toks(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn intervention_is_effective() {
let iv = Intervention::new(0, "cat", "dog");
assert!(iv.is_effective());
let noop = Intervention::new(0, "cat", "cat");
assert!(!noop.is_effective());
}
#[test]
fn history_causal_effect_zero_when_no_change() {
let orig = toks(&["a", "b", "c"]);
let mut history = InterventionHistory::new(orig.clone());
let counterfactual = orig.clone();
history.apply(Intervention::new(0, "a", "a"), counterfactual);
assert!((history.causal_effect() - 0.0).abs() < 1e-10);
}
#[test]
fn history_causal_effect_full_change() {
let orig = toks(&["a", "b", "c"]);
let mut history = InterventionHistory::new(orig);
let counterfactual = toks(&["X", "Y", "Z"]);
history.apply(Intervention::new(0, "a", "X"), counterfactual);
let effect = history.causal_effect();
assert!(effect > 0.0, "full token change should produce non-zero effect: {effect}");
}
#[test]
fn history_patch_token() {
let orig = toks(&["hello", " world"]);
let mut history = InterventionHistory::new(orig);
history.patch_token(1, " rust");
assert_eq!(history.final_stream[1], " rust");
assert_eq!(history.interventions.len(), 1);
}
#[test]
fn history_no_interventions_returns_zero_effect() {
let orig = toks(&["a", "b"]);
let history = InterventionHistory::new(orig);
assert_eq!(history.causal_effect(), 0.0);
}
#[test]
fn token_editor_cursor_navigation() {
let mut ed = TokenEditor::new(toks(&["a", "b", "c"]));
assert_eq!(ed.cursor(), 0);
ed.cursor_right();
assert_eq!(ed.cursor(), 1);
ed.cursor_left();
assert_eq!(ed.cursor(), 0);
ed.cursor_end();
assert_eq!(ed.cursor(), 2);
ed.cursor_home();
assert_eq!(ed.cursor(), 0);
}
#[test]
fn token_editor_cursor_clamps_at_bounds() {
let mut ed = TokenEditor::new(toks(&["a"]));
ed.cursor_left(); assert_eq!(ed.cursor(), 0);
ed.cursor_right(); assert_eq!(ed.cursor(), 0);
}
#[test]
fn token_editor_commit_edit() {
let mut ed = TokenEditor::new(toks(&["cat", " sat"]));
ed.enter_edit_mode();
ed.edit_push('d');
ed.edit_push('o');
ed.edit_push('g');
ed.edit_buffer = "dog".to_string();
let result = ed.commit_edit();
assert!(result.is_some());
let (pos, orig, new_tok) = result.unwrap();
assert_eq!(pos, 0);
assert_eq!(orig, "cat");
assert_eq!(new_tok, "dog");
assert_eq!(ed.tokens()[0], "dog");
}
#[test]
fn token_editor_cancel_edit() {
let mut ed = TokenEditor::new(toks(&["cat"]));
ed.enter_edit_mode();
ed.edit_buffer = "something_else".to_string();
ed.cancel_edit();
assert!(!ed.is_editing());
assert_eq!(ed.tokens()[0], "cat"); }
#[test]
fn token_editor_to_interventions() {
let mut ed = TokenEditor::new(toks(&["a", "b"]));
ed.edits.push((0, "a".to_string(), "X".to_string()));
let ivs = ed.to_interventions();
assert_eq!(ivs.len(), 1);
assert_eq!(ivs[0].position, 0);
}
#[test]
fn per_position_agreement_identical() {
let s = toks(&["a", "b", "c"]);
let ag = per_position_agreement(&s, &s);
assert!(ag.iter().all(|&a| a));
}
#[test]
fn per_position_agreement_all_different() {
let s1 = toks(&["a", "b"]);
let s2 = toks(&["x", "y"]);
let ag = per_position_agreement(&s1, &s2);
assert!(ag.iter().all(|&a| !a));
}
#[test]
fn causal_influence_map_no_change() {
let s = toks(&["a", "b", "c"]);
let map = causal_influence_map(&s, &s);
for (_, (_, v)) in map.iter().enumerate() {
assert_eq!(*v, 0.0);
}
}
#[test]
fn causal_influence_map_full_change_at_zero() {
let orig = toks(&["a", "b", "c"]);
let counter = toks(&["X", "Y", "Z"]);
let map = causal_influence_map(&orig, &counter);
assert!((map[&0] - 1.0).abs() < 1e-10, "map[0]={}", map[&0]);
}
#[test]
fn render_contains_token_text() {
let ed = TokenEditor::new(toks(&["hello", " world"]));
let rendered = ed.render();
assert!(rendered.contains("hello"));
assert!(rendered.contains("world"));
}
#[test]
fn history_summary_not_empty() {
let mut history = InterventionHistory::new(toks(&["a"]));
history.apply(Intervention::new(0, "a", "b"), toks(&["b"]));
let s = history.summary();
assert!(!s.is_empty());
assert!(s.contains("Interventions: 1"));
}
}