app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
John Lauer
0.2.5–0.2.8 source: verified foreground + visible demo bars; reaper reads ab's matches[] (was blind since 0.2.0), class…
195ea60
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
//! The MEASURED-progress contract (wiki issue #32), ported from `progress.py`. John's rule:
//! every slow verb reports measured progress, never "can take a couple of minutes". Estimates come
//! from THIS machine's own history — the store paths and JSON shapes are byte-compatible with the
//! Python bridge's, so a machine's measured history survives the engine swap.
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
pub const PREFIX: &str = "altium";
const KEEP: usize = 10; // rolling window per key (fusion parity)
const RUNLOG_MAX_BYTES: u64 = 4 * 1024 * 1024;
static LOCK: Mutex<()> = Mutex::new(());
/// Shipped defaults, used ONLY to seed a machine with no history of its own — each a measured
/// figure from this fleet, not a guess.
fn default_seconds(phase: &str) -> Option<f64> {
Some(match phase {
"cold_start" => 60.0,
"warm_start" => 1.0,
"agent_start" => 8.0,
"show_symbol" => 12.0,
"show_footprint" => 20.0,
"show_3d_chip" => 25.0,
"show_library" => 12.0,
"show_schematic" => 15.0,
"show_2d_board" => 15.0,
"show_3d_board" => 30.0,
"show_project" => 10.0,
"install_plugin" => 6.0,
"run_outjob" => 60.0,
"export" => 8.0,
"demo" => 210.0,
"demo_live_stage" => 12.0,
_ => return None,
})
}
fn store_dir() -> PathBuf {
let base = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_else(|_| ".".into());
let d = PathBuf::from(base).join(".adom");
let _ = std::fs::create_dir_all(&d);
d
}
fn timings_path() -> PathBuf {
store_dir().join("altium-bridge-timings.json")
}
fn runlog_path() -> PathBuf {
store_dir().join("altium-verb-times.jsonl")
}
fn load() -> BTreeMap<String, Vec<f64>> {
std::fs::read_to_string(timings_path())
.ok()
.and_then(|s| serde_json::from_str::<BTreeMap<String, Vec<f64>>>(&s).ok())
.unwrap_or_default()
}
fn save(data: &BTreeMap<String, Vec<f64>>) {
if let Ok(s) = serde_json::to_string_pretty(data) {
let _ = std::fs::write(timings_path(), s);
}
}
fn key(phase: &str, start_kind: &str) -> String {
format!("{phase}:{}", if start_kind.is_empty() { "warm" } else { start_kind })
}
fn pct(samples: &[f64], q: f64) -> f64 {
if samples.is_empty() {
return 0.0;
}
let mut s = samples.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let i = ((q * (s.len() as f64 - 1.0)).round() as usize).min(s.len() - 1);
(s[i] * 100.0).round() / 100.0
}
/// Add one real measurement. This is what makes confidence "measured".
pub fn record(phase: &str, seconds: f64, start_kind: &str) {
if !(0.0..=7200.0).contains(&seconds) {
return;
}
let _g = LOCK.lock();
let mut data = load();
let k = key(phase, start_kind);
let samples = data.entry(k).or_default();
samples.push((seconds * 100.0).round() / 100.0);
let len = samples.len();
if len > KEEP {
samples.drain(..len - KEEP);
}
save(&data);
}
/// (estimatedSec, confidence) for this phase ON THIS MACHINE.
/// 3+ samples -> p50 "measured"; 1-2 -> blend with the shipped seed, "typical";
/// a bare seed -> "typical"; nothing at all -> (0, "unknown") = indeterminate marquee, never a
/// guessed number wearing a measurement's clothes.
pub fn estimate(phase: &str, start_kind: &str) -> (f64, String) {
let data = load();
let samples = data.get(&key(phase, start_kind)).cloned().unwrap_or_default();
if samples.len() >= 3 {
return (pct(&samples, 0.5), "measured".into());
}
let default = default_seconds(phase);
if !samples.is_empty() {
let base = default.unwrap_or(*samples.last().unwrap());
let avg = samples.iter().sum::<f64>() / samples.len() as f64;
return ((((avg + base) / 2.0) * 100.0).round() / 100.0, "typical".into());
}
if let Some(d) = default {
return (d, "typical".into());
}
(0.0, "unknown".into())
}
/// Everything we know, for describe/status: p50 + p90 + sample counts.
pub fn history() -> Value {
let mut out = serde_json::Map::new();
for (k, samples) in load() {
if samples.is_empty() {
continue;
}
let (phase, kind) = k.split_once(':').unwrap_or((k.as_str(), "warm"));
let entry = out.entry(phase.to_string()).or_insert_with(|| json!({}));
entry[kind] = json!({"samples": samples.len(), "p50": pct(&samples, 0.5), "p90": pct(&samples, 0.9)});
}
Value::Object(out)
}
fn now_secs() -> f64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0)
}
/// Build the progress block in the shape ab consumes verbatim. Formulas adopted from fusion's
/// timings.py (John: copy the method, do not redesign): 0->80 across the estimate, 80->95
/// asymptotic when overdue, the two meeting at elapsed == estimate so percent never walks
/// backwards at the crossover.
pub fn block(phase: &str, step_label: &str, started_at: f64, percent: Option<i64>,
start_kind: &str, blocked_by: Option<&str>, done: bool) -> Value {
let elapsed = (now_secs() - started_at).max(0.0);
let (est0, mut confidence) = estimate(phase, start_kind);
let mut est = est0;
let overdue = est0 > 0.0 && elapsed > est0;
if overdue {
est = (elapsed * 1.25 * 10.0).round() / 10.0;
if confidence == "measured" {
confidence = "typical".into();
}
if elapsed > 2.0 * est0 {
confidence = "unknown".into();
}
}
let mut percent = percent;
if done {
percent = Some(100);
est = (elapsed * 100.0).round() / 100.0;
} else if percent.is_none() {
percent = if est0 <= 0.0 {
None
} else if !overdue {
Some((80.0 * elapsed / est0) as i64)
} else {
let over = elapsed - est0;
Some(80 + (15.0 * (over / (over + est0))) as i64)
};
}
let eta = percent.map(|_| ((est - elapsed).max(0.0) * 10.0).round() / 10.0);
let mut step_label = step_label.to_string();
if !done && est0 > 0.0 && overdue && confidence == "unknown" {
step_label = format!("{step_label} (taking far longer than this machine ever has)");
}
if let Some(b) = blocked_by {
step_label = format!("waiting for you: {b}");
}
let mut out = json!({
"phase": format!("{PREFIX}.{phase}"),
"stepLabel": step_label,
"percent": percent,
"elapsedSec": (elapsed * 10.0).round() / 10.0,
"estimatedSec": (est * 10.0).round() / 10.0,
"etaSec": eta,
"confidence": confidence,
"startKind": start_kind,
});
if !done && est0 > 0.0 {
out["overdue"] = json!(overdue);
}
if let Some(b) = blocked_by {
out["blockedBy"] = json!(b);
}
if done {
out["measured"] = json!({"phase": format!("{PREFIX}.{phase}"), "seconds": (elapsed * 100.0).round() / 100.0, "startKind": start_kind});
}
out
}
// ---- live, in-flight progress (in-memory on purpose: a stale frame surviving a restart lies) --
struct Live {
started_at: f64,
start_kind: String,
step_label: String,
percent: Option<i64>,
blocked_by: Option<String>,
seq: u64,
}
static LIVE: Mutex<Vec<((String, String), Live)>> = Mutex::new(Vec::new());
static LIVE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn begin(phase: &str, start_kind: &str, step_label: &str, caller: &str) {
let seq = LIVE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
if let Ok(mut g) = LIVE.lock() {
g.retain(|(k, _)| !(k.0 == caller && k.1 == phase));
g.push(((caller.to_string(), phase.to_string()),
Live { started_at: now_secs(), start_kind: start_kind.into(),
step_label: step_label.into(), percent: None, blocked_by: None, seq }));
}
}
/// Advance the visible sub-step of an in-flight phase — the live streaming contract. Kept as a
/// public API for verbs that report sub-steps; not every verb does yet.
#[allow(dead_code)]
pub fn step(phase: &str, step_label: &str, percent: Option<i64>, blocked_by: Option<&str>, caller: &str) {
if let Ok(mut g) = LIVE.lock() {
let hit = g.iter_mut().find(|(k, _)| k.0 == caller && k.1 == phase).map(|(_, v)| v);
let hit = match hit {
Some(v) => Some(v),
None => {
// fall back to the only in-flight entry for this phase; never guess among several
let mut it = g.iter_mut().filter(|(k, _)| k.1 == phase);
match (it.next(), it.next()) {
(Some((_, v)), None) => Some(v),
_ => None,
}
}
};
if let Some(v) = hit {
v.step_label = step_label.into();
v.blocked_by = blocked_by.map(str::to_string);
if let Some(p) = percent {
// never let a bar go backwards: a bar that retreats reads as broken
v.percent = Some(p.max(v.percent.unwrap_or(0)));
}
}
}
}
pub fn end(phase: &str, caller: &str) {
if let Ok(mut g) = LIVE.lock() {
g.retain(|(k, _)| !(k.0 == caller && k.1 == phase));
}
}
/// Every phase currently in flight, as full progress blocks (percent/ETA recomputed at read time;
/// a step's explicit percent is a monotonic FLOOR, so the bar always advances).
pub fn live(mine_only: bool, asker: &str) -> Vec<Value> {
let snapshot: Vec<(String, String, f64, String, String, Option<i64>, Option<String>, u64)> =
LIVE.lock()
.map(|g| {
g.iter()
.map(|((who, phase), v)| {
(who.clone(), phase.clone(), v.started_at, v.start_kind.clone(),
v.step_label.clone(), v.percent, v.blocked_by.clone(), v.seq)
})
.collect()
})
.unwrap_or_default();
let mut out = Vec::new();
for (who, phase, started_at, start_kind, step_label, floor, blocked_by, seq) in snapshot {
if mine_only && who != asker {
continue;
}
let mut b = block(&phase, &step_label, started_at, None, &start_kind, blocked_by.as_deref(), false);
if let Some(f) = floor {
let derived = b.get("percent").and_then(|p| p.as_i64());
b["percent"] = json!(derived.map(|d| d.max(f)).unwrap_or(f));
}
b["seq"] = json!(seq);
b["caller"] = json!(who);
out.push(b);
}
out
}
// ---- per-verb run times (fusion parity) --------------------------------------
/// Append one verb run to the historical log AND the rolling window. Called at the dispatch
/// chokepoint for EVERY verb. Never fails loudly.
pub fn log_run(verb: &str, seconds: f64, ok: bool) {
if !(0.0..=7200.0).contains(&seconds) {
return; // a clock jump is not a sample
}
record(&format!("verb:{verb}"), seconds, "warm");
let p = runlog_path();
if let Ok(m) = std::fs::metadata(&p) {
if m.len() > RUNLOG_MAX_BYTES {
let _ = std::fs::rename(&p, p.with_extension("jsonl.1"));
}
}
let line = json!({"at": now_secs() as i64, "verb": verb, "seconds": (seconds * 100.0).round() / 100.0, "ok": ok, "note": ""});
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&p) {
let _ = writeln!(f, "{line}");
}
}
/// Aggregate the historical log: per verb count / p50 / p90 / max / failures, p90 descending —
/// p90, not p50, is the column for hunting slow paths.
pub fn verb_report(limit: usize) -> Value {
let raw = match std::fs::read_to_string(runlog_path()) {
Ok(s) => s,
Err(_) => return json!({}),
};
let mut runs: BTreeMap<String, (Vec<f64>, u64)> = BTreeMap::new();
for line in raw.lines() {
if let Ok(r) = serde_json::from_str::<Value>(line) {
let verb = r.get("verb").and_then(|v| v.as_str()).unwrap_or("?").to_string();
let e = runs.entry(verb).or_default();
e.0.push(r.get("seconds").and_then(|s| s.as_f64()).unwrap_or(0.0));
if r.get("ok").and_then(|b| b.as_bool()) != Some(true) {
e.1 += 1;
}
}
}
let mut rows: Vec<(String, Value, f64)> = runs
.into_iter()
.map(|(verb, (secs, fails))| {
let p90 = pct(&secs, 0.9);
let row = json!({"count": secs.len(), "p50": pct(&secs, 0.5), "p90": p90,
"max": secs.iter().cloned().fold(f64::NAN, f64::max), "failures": fails});
(verb, row, p90)
})
.collect();
rows.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
let mut out = serde_json::Map::new();
for (verb, row, _) in rows.into_iter().take(limit) {
out.insert(verb, row);
}
Value::Object(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentile_and_estimate_ladder() {
assert_eq!(pct(&[3.0, 1.0, 2.0], 0.5), 2.0);
assert_eq!(pct(&[], 0.5), 0.0);
// a phase with a seed but no samples is "typical", never "unknown"
let (e, c) = estimate("show_symbol_never_recorded_xyz", "warm");
assert_eq!((e, c.as_str()), (0.0, "unknown"));
}
#[test]
fn block_percent_meets_at_crossover() {
// with no estimate the bar is indeterminate, never 0%
let b = block("no_such_phase_zz", "working", now_secs() - 5.0, None, "warm", None, false);
assert!(b["percent"].is_null());
// done carries the measured actuals
let b = block("no_such_phase_zz", "done", now_secs() - 5.0, None, "warm", None, true);
assert_eq!(b["percent"], json!(100));
assert!(b["measured"]["seconds"].as_f64().unwrap() >= 4.9);
}
#[test]
fn live_floor_is_monotonic() {
begin("t_phase", "warm", "starting", "tester");
step("t_phase", "half way", Some(50), None, "tester");
step("t_phase", "later", Some(30), None, "tester"); // lower explicit % must not retreat
let l = live(true, "tester");
assert_eq!(l.len(), 1);
assert!(l[0]["percent"].as_i64().unwrap() >= 50);
end("t_phase", "tester");
assert!(live(true, "tester").is_empty());
}
}