use std::path::{Path, PathBuf};
use std::process::Command;
use std::fs;
use std::time::{SystemTime, Duration};
#[derive(Debug, Clone, serde::Serialize)]
pub struct Prediction {
pub command: String,
pub confidence: f64,
pub reason: String,
pub category: String,
}
#[derive(Debug, Default)]
struct WorkspaceState {
root: PathBuf,
git_status: Vec<String>,
has_working_receipt: bool,
working_receipt_size: u64,
recent_receipts: Vec<(PathBuf, SystemTime)>,
cargo_test_failed: bool,
src_modified: bool,
tests_modified: bool,
ontology_modified: bool,
}
trait TelepathyRule {
fn name(&self) -> &str;
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction>;
}
struct AssembleRule;
impl TelepathyRule for AssembleRule {
fn name(&self) -> &str { "Assemble" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if state.has_working_receipt && state.working_receipt_size > 2 {
Some(Prediction {
command: "affi receipt assemble".to_string(),
confidence: 0.95,
reason: "Working receipt contains uncommitted events. Assemble them to finalize the provenance chain.".to_string(),
category: "Chain".to_string(),
})
} else {
None
}
}
}
struct EmitRule;
impl TelepathyRule for EmitRule {
fn name(&self) -> &str { "Emit" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if state.src_modified && !state.has_working_receipt {
Some(Prediction {
command: "affi receipt emit".to_string(),
confidence: 0.85,
reason: "Source code modified. You likely need to emit new events to capture the latest operations.".to_string(),
category: "Emission".to_string(),
})
} else {
None
}
}
}
struct VerifyRule;
impl TelepathyRule for VerifyRule {
fn name(&self) -> &str { "Verify" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if let Some((path, _)) = state.recent_receipts.first() {
let rel_path = path.strip_prefix(&state.root).unwrap_or(path);
Some(Prediction {
command: format!("affi receipt verify {}", rel_path.display()),
confidence: 0.80,
reason: format!("Receipt {} recently modified. Verify it to ensure conformance with the certify pipeline.", rel_path.display()),
category: "Verification".to_string(),
})
} else {
None
}
}
}
struct InspectRule;
impl TelepathyRule for InspectRule {
fn name(&self) -> &str { "Inspect" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if let Some((path, modified)) = state.recent_receipts.first() {
if let Ok(elapsed) = modified.elapsed() {
if elapsed < Duration::from_secs(60) {
let rel_path = path.strip_prefix(&state.root).unwrap_or(path);
return Some(Prediction {
command: format!("affi receipt inspect {}", rel_path.display()),
confidence: 0.70,
reason: "Recently updated receipt detected. Inspect it for a detailed structural report.".to_string(),
category: "QOL".to_string(),
});
}
}
}
None
}
}
struct ModelRule;
impl TelepathyRule for ModelRule {
fn name(&self) -> &str { "Model" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if let Some((path, _)) = state.recent_receipts.first() {
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() > 10000 { let rel_path = path.strip_prefix(&state.root).unwrap_or(path);
return Some(Prediction {
command: format!("affi receipt model {}", rel_path.display()),
confidence: 0.65,
reason: "Large receipt detected. Discovering a process model (DFG/Petri) will help visualize complexity.".to_string(),
category: "Discovery".to_string(),
});
}
}
}
None
}
}
struct CargoTestRule;
impl TelepathyRule for CargoTestRule {
fn name(&self) -> &str { "CargoTest" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if state.tests_modified {
Some(Prediction {
command: "cargo test".to_string(),
confidence: 0.75,
reason: "Test files modified. Running the suite is recommended to verify local invariants.".to_string(),
category: "Development".to_string(),
})
} else {
None
}
}
}
struct StatsRule;
impl TelepathyRule for StatsRule {
fn name(&self) -> &str { "Stats" }
fn evaluate(&self, state: &WorkspaceState) -> Option<Prediction> {
if state.ontology_modified {
Some(Prediction {
command: "affi stats --ontology".to_string(),
confidence: 0.60,
reason: "Ontology modified. Check aggregate statistics to ensure the surface area is consistent.".to_string(),
category: "Analysis".to_string(),
})
} else {
None
}
}
}
pub struct Telepathy {
workspace_root: PathBuf,
rules: Vec<Box<dyn TelepathyRule>>,
}
impl Telepathy {
pub fn new<P: AsRef<Path>>(root: P) -> Self {
Self {
workspace_root: root.as_ref().to_path_buf(),
rules: vec![
Box::new(AssembleRule),
Box::new(EmitRule),
Box::new(VerifyRule),
Box::new(InspectRule),
Box::new(ModelRule),
Box::new(CargoTestRule),
Box::new(StatsRule),
],
}
}
pub fn predict(&self) -> Option<Prediction> {
let state = self.gather_state();
let mut predictions: Vec<Prediction> = self.rules.iter()
.filter_map(|rule| rule.evaluate(&state))
.collect();
predictions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
predictions.into_iter().next()
}
fn gather_state(&self) -> WorkspaceState {
let mut state = WorkspaceState {
root: self.workspace_root.clone(),
..Default::default()
};
let working_path = self.workspace_root.join(".affi/working.json");
if let Ok(metadata) = fs::metadata(&working_path) {
state.has_working_receipt = true;
state.working_receipt_size = metadata.len();
}
if let Ok(output) = Command::new("git")
.arg("status")
.arg("--porcelain")
.current_dir(&self.workspace_root)
.output() {
let status = String::from_utf8_lossy(&output.stdout);
for line in status.lines() {
state.git_status.push(line.to_string());
if line.contains("src/") { state.src_modified = true; }
if line.contains("tests/") { state.tests_modified = true; }
if line.contains("ontology/") { state.ontology_modified = true; }
}
}
state.recent_receipts = self.find_recent_receipts();
state
}
fn find_recent_receipts(&self) -> Vec<(PathBuf, SystemTime)> {
let mut receipts = Vec::new();
let search_paths = vec![
self.workspace_root.clone(),
self.workspace_root.join(".affi"),
];
for dir in search_paths {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
if !name.contains("working") && (name.len() > 32 || path.parent().map(|p| p.ends_with(".affi")).unwrap_or(false)) {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
receipts.push((path, modified));
}
}
}
}
}
}
}
}
receipts.sort_by(|a, b| b.1.cmp(&a.1));
receipts
}
}
pub fn run_cli() {
let args: Vec<String> = std::env::args().collect();
let tele = Telepathy::new(".");
if args.contains(&"--raw".to_string()) {
if let Some(p) = tele.predict() {
print!("{}", p.command);
}
} else if args.contains(&"--init".to_string()) {
println!("{}", shell_integration());
} else {
if let Some(p) = tele.predict() {
println!("AFFI TELEPATHY — PREDICTION ENGINE");
println!("=================================");
println!("Command: \x1b[1;32m{}\x1b[0m", p.command);
println!("Confidence: {:.1}%", p.confidence * 100.0);
println!("Category: {}", p.category);
println!("Reason: {}", p.reason);
println!("\n(Hint: add `eval \"$(affi telepathy --init)\"` to your shell config for Ctrl+T integration)");
} else {
println!("Telepathy: No clear prediction. Try emitting some events.");
}
}
}
pub fn shell_integration() -> &'static str {
r#"
# Affi Telepathy Shell Integration
# Usage: eval "$(affi telepathy --init)"
_affi_telepathy() {
# Fetch prediction from the binary
local prediction
prediction=$(affi telepathy --raw 2>/dev/null)
if [ -n "$prediction" ]; then
if [ -n "$ZSH_VERSION" ]; then
# Zsh: Put command in buffer and keep current line
LBUFFER="$prediction"
zle redisplay
elif [ -n "$BASH_VERSION" ]; then
# Bash: Set the current line to the prediction
READLINE_LINE="$prediction"
READLINE_POINT=${#READLINE_LINE}
fi
fi
}
# Bind to Ctrl+T (Telepathy)
if [ -n "$ZSH_VERSION" ]; then
zle -N _affi_telepathy
bindkey '^T' _affi_telepathy
elif [ -n "$BASH_VERSION" ]; then
bind -x '"\C-t": _affi_telepathy'
fi
echo "[affi] Telepathy integration active (Ctrl+T)" >&2
"#
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_maximalist_telepathy_logic() {
let dir = tempdir().unwrap();
let tele = Telepathy::new(dir.path());
assert!(tele.predict().is_none());
let affi_dir = dir.path().join(".affi");
fs::create_dir(&affi_dir).unwrap();
fs::write(affi_dir.join("working.json"), "[{\"event\": 1}]").unwrap();
let p = tele.predict().expect("Should predict assemble");
assert_eq!(p.command, "affi receipt assemble");
assert!(p.confidence > 0.9);
}
}
fn main() {
run_cli();
}