Initial commit: aether-tools-rs workspace
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
//! aec-atom — minimal REPL over aether-atom.
|
||||
//!
|
||||
//! Replaces the 1088-line aec interpreter with a thin shell. Six
|
||||
//! builtins: lookup, remember, stats, replay, fingerprint, canonicalize.
|
||||
//! The primitive is a single-token command followed by quoted args.
|
||||
//!
|
||||
//! Usage:
|
||||
//! aec-atom # interactive REPL on stdin
|
||||
//! AEC_ATOM_DATA_DIR=/path aec-atom # custom data dir
|
||||
|
||||
use aether_atom::{data_dir, fingerprint, rust_canonicalize, Lattice};
|
||||
use std::env;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
|
||||
fn extract_quoted(s: &str) -> Vec<String> {
|
||||
// Split on whitespace; treat `"x y"` as a single token.
|
||||
let mut out = Vec::new();
|
||||
let mut rest = s;
|
||||
while !rest.is_empty() {
|
||||
rest = rest.trim_start();
|
||||
if rest.is_empty() { break; }
|
||||
if rest.starts_with('"') {
|
||||
if let Some(end) = rest[1..].find('"') {
|
||||
out.push(rest[1..end+1].to_string());
|
||||
rest = &rest[end + 2..];
|
||||
} else {
|
||||
out.push(rest[1..].to_string());
|
||||
rest = "";
|
||||
}
|
||||
} else {
|
||||
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
|
||||
out.push(rest[..end].to_string());
|
||||
rest = &rest[end..];
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn handle(line: &str, lattice: &mut Lattice) -> String {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() { return String::new(); }
|
||||
if trimmed.starts_with('#') { return String::new(); }
|
||||
let parts = extract_quoted(trimmed);
|
||||
let cmd = parts.first().map(String::as_str).unwrap_or("");
|
||||
let args = &parts[1..];
|
||||
|
||||
match cmd {
|
||||
"lookup" | "l" => {
|
||||
let f = args.first().map(String::as_str).unwrap_or("");
|
||||
if f.is_empty() {
|
||||
return "lookup: missing argument".into();
|
||||
}
|
||||
let sig = fingerprint(f.as_bytes());
|
||||
match lattice.lookup(&sig, "global") {
|
||||
Some(hit) => format!("[recall hit] {} -> {}", sig, String::from_utf8_lossy(hit.value.as_ref())),
|
||||
None => format!("[recall miss] {} (work was never recorded)", sig),
|
||||
}
|
||||
}
|
||||
"remember" | "r" => {
|
||||
if args.len() < 2 {
|
||||
return "remember: need <failure> <resolution>".into();
|
||||
}
|
||||
let sig = fingerprint(args[0].as_bytes());
|
||||
let bytes = args[1].as_bytes().to_vec();
|
||||
lattice.record(sig, "global", &bytes, Some(true));
|
||||
format!("[recorded] {} -> {} bytes", sig, bytes.len())
|
||||
}
|
||||
"demote" | "d" => {
|
||||
if args.is_empty() {
|
||||
return "demote: need <failure>".into();
|
||||
}
|
||||
let sig = fingerprint(args[0].as_bytes());
|
||||
lattice.record(sig, "global", b"[demoted]", Some(false));
|
||||
format!("[demoted] {}", sig)
|
||||
}
|
||||
"stats" | "s" => {
|
||||
format!(
|
||||
"records: {} lookups: {} hits: {} misses: {} outcomes: {} hit_rate: {:.1}%",
|
||||
lattice.record_count(),
|
||||
lattice.lookups,
|
||||
lattice.hits,
|
||||
lattice.misses,
|
||||
lattice.outcomes,
|
||||
lattice.hit_rate() * 100.0
|
||||
)
|
||||
}
|
||||
"compact" | "c" => {
|
||||
let retention_secs = env::var("AEC_COMPACT_DAYS")
|
||||
.ok().and_then(|v| v.parse().ok())
|
||||
.unwrap_or(30u64) * 86_400;
|
||||
match lattice.compact(retention_secs) {
|
||||
Ok(dropped) => format!("compacted: dropped {} old lookups", dropped),
|
||||
Err(e) => format!("compact error: {}", e),
|
||||
}
|
||||
}
|
||||
"replay" | "R" => {
|
||||
let path = env::var("AEC_REPLAY_PATH").unwrap_or_else(|_| {
|
||||
format!("{}/events.jsonl", data_dir("AETHER_RECALL_DIR").display())
|
||||
});
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(text) => {
|
||||
let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
|
||||
let start = lines.len().saturating_sub(20);
|
||||
let mut out = format!("last 20 of {} events:\n", lines.len());
|
||||
for l in &lines[start..] {
|
||||
out.push_str(l);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
Err(e) => format!("replay error: {}", e),
|
||||
}
|
||||
}
|
||||
"fingerprint" | "fp" => {
|
||||
let input = args.first().map(String::as_str).unwrap_or("");
|
||||
let sig = fingerprint(input.as_bytes());
|
||||
format!("{}", sig)
|
||||
}
|
||||
"canonicalize" | "c14" => {
|
||||
let input = args.first().map(String::as_str).unwrap_or("");
|
||||
let can = rust_canonicalize(input);
|
||||
format!("canonical: {}", String::from_utf8_lossy(&can))
|
||||
}
|
||||
"help" | "h" | "?" => {
|
||||
"commands:\n lookup / l <failure> — atom.lookup\n remember / r <f> <res> — atom.record (worked=true)\n demote / d <f> — atom.record (worked=false)\n stats / s — hit_rate + counters\n compact / c — drop lookups older than AEC_COMPACT_DAYS\n replay / R — last 20 jsonl events\n fingerprint / fp <text> — sha3 hex\n canonicalize / c14 <text> — Rust canonical form\n help / h / ? — this\n quit / q — exit".into()
|
||||
}
|
||||
"quit" | "q" | "exit" => "::quit::".into(),
|
||||
other => format!("unknown: {} (try `help`)", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dir = data_dir("AETHER_RECALL_DIR");
|
||||
let mut lattice = Lattice::open(&dir)?;
|
||||
eprintln!("aec-atom ready (memory: {:?})", dir);
|
||||
eprintln!("type 'help' for commands, 'quit' to exit");
|
||||
|
||||
let stdin = std::io::stdin();
|
||||
let mut stdout = std::io::stdout();
|
||||
let reader = BufReader::new(stdin.lock());
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(_) => break,
|
||||
};
|
||||
let out = handle(&line, &mut lattice);
|
||||
if out == "::quit::" { break; }
|
||||
if out.is_empty() { continue; }
|
||||
writeln!(stdout, "{}", out)?;
|
||||
stdout.flush()?;
|
||||
write!(stdout, "ae> ")?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn extract_quoted_simple() {
|
||||
let p = extract_quoted(r#"hello "world foo" "#);
|
||||
assert_eq!(p, vec!["hello", "world foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_lists_all_commands() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let resp = handle("help", &mut lat);
|
||||
assert!(resp.contains("lookup"));
|
||||
assert!(resp.contains("remember"));
|
||||
assert!(resp.contains("canonicalize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remember_then_lookup_round_trip() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let _ = handle(r#"remember "error[E0308]: foo" "fix this""#, &mut lat);
|
||||
let resp = handle(r#"lookup "error[E0308]: foo""#, &mut lat);
|
||||
assert!(resp.contains("[recall hit]"));
|
||||
assert!(resp.contains("fix this"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_miss_after_no_record() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let resp = handle(r#"lookup "never seen""#, &mut lat);
|
||||
assert!(resp.contains("[recall miss]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_returns_counters() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let _ = handle(r#"remember "x" "y""#, &mut lat);
|
||||
let _ = handle(r#"lookup "x""#, &mut lat);
|
||||
let resp = handle("stats", &mut lat);
|
||||
assert!(resp.contains("records:"));
|
||||
assert!(resp.contains("outcomes: 1"));
|
||||
assert!(resp.contains("lookups: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_command_matches_atom() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let a = handle(r#"fingerprint "hello""#, &mut lat);
|
||||
let b = format!("{}", fingerprint(b"hello"));
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_command_produces_canonical() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let a = handle(r#"canonicalize "error[E0308]: expected i32, found &str""#, &mut lat);
|
||||
assert!(a.contains("error[E0308]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_command_handled() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let resp = handle("nonsense", &mut lat);
|
||||
assert!(resp.starts_with("unknown:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_line_no_output() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
assert_eq!(handle("", &mut lat), "");
|
||||
assert_eq!(handle(" ", &mut lat), "");
|
||||
assert_eq!(handle("# comment", &mut lat), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quit_signal() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
assert_eq!(handle("quit", &mut lat), "::quit::");
|
||||
assert_eq!(handle("q", &mut lat), "::quit::");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user