use super::generator::{Manifest, ManifestEntry, BASE_EPOCH_MS};
use areev_core::types::{Fact, Goal, Grain, Observation, Skill, Tool};
use areev_store::Areev;
use std::path::Path;
const DAY_MS: i64 = 86_400_000;
const MIN_MS: i64 = 60_000;
pub const AREEV_LOOP_NOW0_MS: i64 = BASE_EPOCH_MS;
pub fn generate_loop(work_dir: &Path, bundle_path: &Path) -> Manifest {
let db_path = work_dir.join("loop-src.db");
let mut m = Areev::open(db_path.to_str().unwrap()).expect("open loop source store");
let mut grains: Vec<ManifestEntry> = Vec::new();
let push = |grains: &mut Vec<ManifestEntry>, hash: String, gtype: &str, desc: &str| {
grains.push(ManifestEntry {
hash,
gtype: gtype.into(),
ns: "agent".into(),
desc: desc.into(),
superseded: false,
forgotten: false,
});
};
let fact = |m: &mut Areev, s: &str, r: &str, o: &str, ts: i64| -> areev_core::error::Hash {
let mut f = Fact::new(s, r, o).confidence(0.9);
f.common.namespace = Some("agent".to_string());
f.common.created_at = Some(ts);
m.add(&f).unwrap_or_else(|e| panic!("add {s} {r} {o}: {e}"))
};
for (i, (o, ts)) in [
("Enterprise", AREEV_LOOP_NOW0_MS - 40 * DAY_MS),
("enterprise", AREEV_LOOP_NOW0_MS - 30 * DAY_MS),
("Enterprise", AREEV_LOOP_NOW0_MS - 20 * DAY_MS),
]
.iter()
.enumerate()
{
let h = fact(&mut m, "acme", "tier", o, *ts);
push(&mut grains, h.to_hex(), "fact", &format!("dup target {}: acme tier {o}", i + 1));
}
let obs_body = "user asked about pricing tiers refunds billing invoices during onboarding call";
for (i, (body, ts)) in [
(obs_body.to_string(), AREEV_LOOP_NOW0_MS - 15 * DAY_MS),
(format!("{obs_body} please"), AREEV_LOOP_NOW0_MS - 15 * DAY_MS + MIN_MS),
]
.iter()
.enumerate()
{
let mut o = Observation::new("agent-1", "agent").subject("onboarding");
o.common.namespace = Some("agent".to_string());
o.common.created_at = Some(*ts);
o.common.extra_fields.insert("body".into(), serde_json::json!(body));
let h = m.add(&o).expect("add near-dup observation");
push(&mut grains, h.to_hex(), "observation", &format!("near-dup observation {}", i + 1));
}
let h = fact(&mut m, "sam", "lives_in", "berlin", AREEV_LOOP_NOW0_MS - 10 * DAY_MS);
push(&mut grains, h.to_hex(), "fact", "contradiction older: sam lives_in berlin");
let h = fact(&mut m, "sam", "lives_in", "tokyo", AREEV_LOOP_NOW0_MS - 5 * DAY_MS);
push(&mut grains, h.to_hex(), "fact", "contradiction newer: sam lives_in tokyo");
for (s, o, expired_days, desc) in [
("promo-black-friday", "BF25", 100, "stale 100d: promo-black-friday"),
("promo-spring", "SPRING10", 10, "stale 10d: promo-spring"),
] {
let mut f = Fact::new(s, "discount_code", o)
.confidence(1.0)
.valid_to(AREEV_LOOP_NOW0_MS - expired_days * DAY_MS);
f.common.namespace = Some("agent".to_string());
f.common.created_at = Some(AREEV_LOOP_NOW0_MS - 120 * DAY_MS);
let h = m.add(&f).expect("add stale promo");
push(&mut grains, h.to_hex(), "fact", desc);
}
for i in 0..6u32 {
let failed = i < 5;
let mut t = Tool::new("stripe_refund")
.content(if failed { "rate limited 429 retry later" } else { "refund ok" })
.is_error(failed);
t.common.namespace = Some("agent".to_string());
t.common.created_at = Some(AREEV_LOOP_NOW0_MS - 2 * DAY_MS + (i as i64) * 5 * MIN_MS);
let h = m.add(&t).expect("add tool call");
push(
&mut grains,
h.to_hex(),
"tool",
&format!("stripe_refund call {} ({})", i + 1, if failed { "error" } else { "ok" }),
);
}
let mut s = Skill::new("parse_invoices", "extract fields from supplier invoices");
s.practice_count = Some(12);
s.common.confidence = 0.25;
s.common.namespace = Some("agent".to_string());
s.common.created_at = Some(AREEV_LOOP_NOW0_MS - 20 * DAY_MS);
let h = m.add(&s).expect("add stalled skill");
push(&mut grains, h.to_hex(), "skill", "stalled skill: parse_invoices");
let mut g = Goal::new("ship v2 billing");
g.subject = Some("ship-v2".to_string());
g.progress = Some(0.05);
g.common.namespace = Some("agent".to_string());
g.common.created_at = Some(AREEV_LOOP_NOW0_MS - 45 * DAY_MS);
let h = m.add(&g).expect("add stalled goal");
push(&mut grains, h.to_hex(), "goal", "stalled goal seed: ship-v2 (analyzer default-off)");
let h = fact(&mut m, "kai", "prefers", "dark-mode", AREEV_LOOP_NOW0_MS - 3 * DAY_MS);
push(&mut grains, h.to_hex(), "fact", "healthy contrast: kai prefers dark-mode");
let base = fact(&mut m, "deploy", "region", "us-east-1", AREEV_LOOP_NOW0_MS - 30 * DAY_MS);
push(&mut grains, base.to_hex(), "fact", "fork base: deploy region us-east-1");
grains.last_mut().unwrap().superseded = true;
let base_bundle = work_dir.join("loop-base.bundle");
m.bundle_since(0, base_bundle.to_str().unwrap()).expect("export base bundle");
let twin_path = work_dir.join("loop-twin.db");
let mut twin = Areev::open(twin_path.to_str().unwrap()).expect("open twin store");
twin.import_bundle(base_bundle.to_str().unwrap()).expect("twin imports base");
let mut ap = Fact::new("deploy", "region", "ap-south-1").confidence(0.9);
ap.common.namespace = Some("agent".to_string());
ap.common.created_at = Some(AREEV_LOOP_NOW0_MS - 8 * DAY_MS);
let ap_hash = twin.supersede(&base, &mut ap).expect("twin supersede");
let fork_bundle = work_dir.join("loop-fork.bundle");
twin.bundle_since(0, fork_bundle.to_str().unwrap()).expect("export fork bundle");
let mut eu = Fact::new("deploy", "region", "eu-west-1").confidence(0.9);
eu.common.namespace = Some("agent".to_string());
eu.common.created_at = Some(AREEV_LOOP_NOW0_MS - 9 * DAY_MS);
let eu_hash = m.supersede(&base, &mut eu).expect("main supersede");
push(&mut grains, eu_hash.to_hex(), "fact", "fork head A: deploy region eu-west-1");
m.import_bundle(fork_bundle.to_str().unwrap()).expect("import divergent supersession");
push(&mut grains, ap_hash.to_hex(), "fact", "fork head B: deploy region ap-south-1 (imported)");
let forks = m.open_forks().expect("open_forks");
assert_eq!(forks.len(), 1, "expected exactly the deploy/region fork, got {forks:?}");
assert_eq!(forks[0].heads.len(), 2, "expected 2 competing heads: {forks:?}");
m.bundle_since(0, bundle_path.to_str().unwrap()).expect("loop bundle export");
Manifest {
schema: 1,
base_epoch_ms: BASE_EPOCH_MS,
total_grains: grains.len(),
grains,
}
}