//! The demo seed: replays the jac-demo arc against the server's own state,
//! plus one extra live blocked gate so a visitor lands with something to do.
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use jac_core::{Author, Clock as _, Provenance, SystemClock};
use jac_decision::{Decision, DecisionScope, RationaleFamily, Statement};
use jac_rendezvous::{Publication, PublishedSketch, Registry as _};
use jac_repo::RefName;
use serde_json::{Value, json};
use crate::dto::{self, VerdictDto};
use crate::error::ApiError;
use crate::routes::repos::{
AgentReq, FileReq, FounderReq, InitRepoRequest, InitialCommitReq, create_repo,
};
use crate::routes::{with_repo, with_repo_mut};
use crate::state::{AppState, SharedState};
const LOGIN_RS: &str =
"pub fn login(user: &str, token: &str) -> Result<Session, AuthError> { /* ... */ }";
const BACKOFF_RS: &str = "pub fn retry(op: impl Fn() -> Outcome) -> Outcome { /* exponential backoff; 401/403 are terminal */ }";
const SESSION_RS: &str =
"pub struct SessionCache { /* time-based eviction only; see the governing decision */ }";
/// `POST /api/demo` — seed the walkthrough. Naturally run-once: the second
/// call collides on the slug and returns `409`.
/// Eight modules that all reach the retry helper the same way. The bulk
/// rename below rewrites the call in every one of them, which is the shape a
/// grouped diff exists to collapse.
const FANOUT: [(&str, &str); 8] = [
("src/net/fetch.rs", "fetch"),
("src/net/push.rs", "push"),
("src/net/poll.rs", "poll"),
("src/net/stream.rs", "stream"),
("src/net/upload.rs", "upload"),
("src/net/probe.rs", "probe"),
("src/net/resolve.rs", "resolve"),
("src/net/handshake.rs", "handshake"),
];
/// Three more modules whose rename means the same thing but is spelled
/// differently. No hash can fold these in with the others; a judge can see
/// that they are one change.
const VARIANT: [(&str, &str); 3] = [
("src/net/subscribe.rs", "subscribe"),
("src/net/publish.rs", "publish"),
("src/net/reconnect.rs", "reconnect"),
];
/// The module body before and after the rename. One line differs.
fn fanout_body(op: &str, jittered: bool) -> String {
fanout_body_with(
op,
if jittered {
"retry_with_jitter"
} else {
"retry"
},
)
}
fn fanout_body_with(op: &str, call: &str) -> String {
format!(
"pub async fn {op}(req: Request) -> Result<Response, NetError> {{\n let policy = Policy::default();\n {call}(policy, || transport::{op}(&req)).await\n }}\n"
)
}
pub(crate) async fn seed_route(
State(state): State<SharedState>,
) -> Result<(StatusCode, Json<Value>), ApiError> {
let body = seed_inner(&state)?;
Ok((StatusCode::CREATED, Json(body)))
}
/// Seeds the demo arc, for the binary's `--seed-demo` flag.
///
/// # Errors
///
/// Fails if the demo repos already exist or any engine step refuses.
pub fn seed(state: &AppState) -> anyhow::Result<Value> {
seed_inner(state).map_err(|e| anyhow::anyhow!("{e}"))
}
/// Seeds Meridian Systems and Halcyon Works, the blocked→attested→admitted
/// arc, the rendezvous, and one still-unsettled decision with a parked
/// branch (`feature/session-cache`) whose gate a visitor can walk.
#[expect(
clippy::too_many_lines,
reason = "the demo arc is one linear story; scene order is the structure"
)]
fn seed_inner(state: &AppState) -> Result<Value, ApiError> {
// -- Scene 1-2: Meridian, Ada, loom-bot, and the first human commit.
let meridian = create_repo(
state,
InitRepoRequest {
name: "Meridian Systems".to_owned(),
founder: FounderReq {
display_name: "Ada".to_owned(),
},
default_ref: None,
agents: vec![AgentReq {
model: "loom-bot".to_owned(),
}],
initial_commit: Some(InitialCommitReq {
message: "auth: initial login flow".to_owned(),
files: vec![FileReq {
path: "src/auth/login.rs".to_owned(),
content: LOGIN_RS.to_owned(),
}],
provenance: None,
}),
founding_decision: None,
},
)?;
let slug = meridian.slug.clone();
let ada_id = meridian.founder.id.clone();
let loom_bot_id = meridian
.agents
.first()
.map(|a| a.id.clone())
.ok_or_else(|| ApiError::internal("seed lost its agent"))?;
let main = RefName::parse("main")?;
let feature = RefName::parse("feature/backoff")?;
// -- Scene 3: loom-bot branches and adds the retry layer (provenance: agent).
// -- Scene 4: loom-bot proposes the governing decision.
let (decision_id, decision_hex) = with_repo_mut(state, &slug, |entry| {
let base = entry
.repo
.head(&main)
.ok_or_else(|| ApiError::internal("main is unbound after seed commit"))?;
entry.repo.branch(&feature, base);
let loom_bot = *entry
.agents
.keys()
.next()
.ok_or_else(|| ApiError::internal("seed lost its agent"))?;
entry.repo.commit(
&feature,
&[
("src/auth/login.rs", LOGIN_RS.as_bytes()),
("src/auth/backoff.rs", BACKOFF_RS.as_bytes()),
],
Author::Agent(loom_bot),
Provenance::Agent,
"auth: retry with exponential backoff",
)?;
let at = entry.repo.now();
let id = entry.repo.propose_decision(Decision {
title: "auth retry fails closed on 401/403".to_owned(),
rationale: "On 401 and 403 responses the retry layer fails closed. Exponential \
backoff applies to transient network errors only. Cached credentials \
are never reused for an unauthorized request."
.to_owned(),
families: vec![
RationaleFamily::Constraints,
RationaleFamily::ConfidenceRisk,
],
proposed_by: Author::Agent(loom_bot),
scope: DecisionScope {
path_prefixes: vec![dto::parse_repo_path("src/auth")?],
},
at,
});
Ok((id, id.digest().to_hex().as_str().to_owned()))
})?;
// -- Scene 5: promotion blocked. The work parks; nobody is punished.
let blocked = with_repo_mut(state, &slug, |entry| {
let verdict = entry.repo.promote(&feature, &main)?;
Ok(VerdictDto::of(&verdict, false, None))
})?;
// -- Scene 6: Ada walks the change and answers in her own words.
with_repo_mut(state, &slug, |entry| {
let ada = entry
.humans
.values()
.next()
.cloned()
.ok_or_else(|| ApiError::internal("seed lost Ada"))?;
let statement = Statement::new(
"I traced the 401 path by hand: the retry loop treats it as terminal and \
surfaces the error instead of replaying stale credentials. The backoff \
bound is what I'd have picked. I'd revisit if we ever add token refresh \
inside the retry.",
)?;
entry.repo.attest(decision_id, &ada, statement)?;
Ok(())
})?;
// -- Scene 7: the same promotion, admitted.
let admitted = with_repo_mut(state, &slug, |entry| {
let verdict = entry.repo.promote(&feature, &main)?;
let into_head = entry
.repo
.head(&main)
.map(|id| id.digest().to_hex().as_str().to_owned());
Ok(VerdictDto::of(&verdict, verdict.is_admitted(), into_head))
})?;
// -- Scene 8: Meridian publishes sketches — and only sketches.
let (meridian_org, content_sketch, decision_sketch) = with_repo(state, &slug, |entry| {
let head = entry
.repo
.head(&main)
.ok_or_else(|| ApiError::internal("main is unbound after admit"))?;
Ok((
entry.org_id,
PublishedSketch::Content(entry.repo.sketch_snapshot(head)?),
PublishedSketch::Decision(entry.repo.sketch_decision(decision_id)?),
))
})?;
{
let mut registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
let at = SystemClock.now();
registry.publish(Publication {
org: meridian_org,
sketch: content_sketch,
at,
});
registry.publish(Publication {
org: meridian_org,
sketch: decision_sketch,
at,
});
}
// -- Scene 9: Halcyon, independently, has the same problem.
let halcyon = create_repo(
state,
InitRepoRequest {
name: "Halcyon Works".to_owned(),
founder: FounderReq {
display_name: "Grace".to_owned(),
},
default_ref: None,
agents: vec![AgentReq {
model: "shuttle-bot".to_owned(),
}],
initial_commit: Some(InitialCommitReq {
message: "net: retry scaffolding".to_owned(),
files: vec![FileReq {
path: "lib/net/retry/mod.rs".to_owned(),
content: "pub mod backoff;".to_owned(),
}],
provenance: None,
}),
founding_decision: None,
},
)?;
let halcyon_slug = halcyon.slug;
let (halcyon_org, their_sketch) = with_repo_mut(state, &halcyon_slug, |entry| {
let bot = *entry
.agents
.keys()
.next()
.ok_or_else(|| ApiError::internal("seed lost Halcyon's agent"))?;
let at = entry.repo.now();
let their_decision = entry.repo.propose_decision(Decision {
title: "retry layer: authentication errors are terminal".to_owned(),
rationale: "The retry layer fails closed on 401 and 403 responses. Exponential \
backoff is for transient network errors only. A request that was \
unauthorized is never replayed with cached credentials."
.to_owned(),
families: vec![RationaleFamily::Constraints],
proposed_by: Author::Agent(bot),
scope: DecisionScope {
path_prefixes: vec![dto::parse_repo_path("lib/net/retry")?],
},
at,
});
Ok((
entry.org_id,
PublishedSketch::Decision(entry.repo.sketch_decision(their_decision)?),
))
})?;
let (matches, introduction) = {
let mut registry = state
.registry
.lock()
.map_err(|_| ApiError::internal("registry lock poisoned"))?;
let at = SystemClock.now();
registry.publish(Publication {
org: halcyon_org,
sketch: their_sketch,
at,
});
let matches: Vec<Value> = registry
.find_similar(
&their_sketch,
jac_sketch::Similarity::from_permille(150),
halcyon_org,
)
.iter()
.map(|m| {
json!({
"org": m.org.to_string(),
"similarity_permille": m.similarity.permille(),
"similarity_percent": m.similarity.to_string(),
})
})
.collect();
let introduction = registry.broker(halcyon_org, meridian_org, at);
(
matches,
json!({
"token": introduction.token.to_string(),
"parties": [
introduction.parties.0.to_string(),
introduction.parties.1.to_string(),
],
}),
)
};
// -- The live gate: a fresh unsettled decision and a parked branch, so
// the visitor experiences a block instead of only reading about one.
let session_branch = RefName::parse("feature/session-cache")?;
let live_decision_hex = with_repo_mut(state, &slug, |entry| {
let base = entry
.repo
.head(&main)
.ok_or_else(|| ApiError::internal("main is unbound"))?;
entry.repo.branch(&session_branch, base);
let loom_bot = *entry
.agents
.keys()
.next()
.ok_or_else(|| ApiError::internal("seed lost its agent"))?;
entry.repo.commit(
&session_branch,
&[
("src/auth/login.rs", LOGIN_RS.as_bytes()),
("src/auth/backoff.rs", BACKOFF_RS.as_bytes()),
("src/auth/session.rs", SESSION_RS.as_bytes()),
],
Author::Agent(loom_bot),
Provenance::Agent,
"auth: session cache with time-based eviction",
)?;
let at = entry.repo.now();
let id = entry.repo.propose_decision(Decision {
title: "session cache: eviction is time-based only".to_owned(),
rationale: "Entries expire on a fixed clock, never on capacity pressure: a \
size-based evictor would let one busy tenant silently log out \
another. Capacity is handled by refusing new sessions instead."
.to_owned(),
families: vec![
RationaleFamily::Alternatives,
RationaleFamily::ConfidenceRisk,
],
proposed_by: Author::Agent(loom_bot),
scope: DecisionScope {
path_prefixes: vec![dto::parse_repo_path("src/auth")?],
},
at,
});
Ok(id.digest().to_hex().as_str().to_owned())
})?;
// -- A bulk rename. The transport modules land on `main` first, so the
// branch's diff is eight identical substitutions rather than eight new
// files. One of the eight also gains a timeout, which a grouped diff must
// refuse to fold away.
let jitter_branch = RefName::parse("feature/retry-jitter")?;
with_repo_mut(state, &slug, |entry| {
let loom_bot = *entry
.agents
.keys()
.next()
.ok_or_else(|| ApiError::internal("seed lost its agent"))?;
// Commits carry the whole tree in this milestone, so main's existing
// files travel with the new ones.
let mut base_tree: Vec<(String, String)> = vec![
("src/auth/login.rs".to_owned(), LOGIN_RS.to_owned()),
("src/auth/backoff.rs".to_owned(), BACKOFF_RS.to_owned()),
];
base_tree.extend(
FANOUT
.iter()
.chain(VARIANT.iter())
.map(|(path, op)| ((*path).to_owned(), fanout_body(op, false))),
);
let base_refs: Vec<(&str, &[u8])> = base_tree
.iter()
.map(|(p, c)| (p.as_str(), c.as_bytes()))
.collect();
entry.repo.commit(
&main,
&base_refs,
Author::Agent(loom_bot),
Provenance::Agent,
"net: transport modules",
)?;
let base = entry
.repo
.head(&main)
.ok_or_else(|| ApiError::internal("main is unbound"))?;
entry.repo.branch(&jitter_branch, base);
// The rename everywhere, plus one module that also grows a timeout.
let mut after: Vec<(String, String)> = vec![
("src/auth/login.rs".to_owned(), LOGIN_RS.to_owned()),
("src/auth/backoff.rs".to_owned(), BACKOFF_RS.to_owned()),
];
after.extend(
FANOUT
.iter()
.map(|(path, op)| ((*path).to_owned(), fanout_body(op, true))),
);
// The same intent, spelled differently — deterministically a separate
// fold, and exactly what a judge is asked to reconcile.
after.extend(
VARIANT
.iter()
.map(|(path, op)| ((*path).to_owned(), fanout_body_with(op, "retry_jittered"))),
);
if let Some(odd) = after.iter_mut().find(|(p, _)| p == "src/net/upload.rs") {
odd.1 = odd.1.replace(
"let policy = Policy::default();",
"let policy = Policy::default().timeout(Duration::from_secs(90));",
);
}
let after_refs: Vec<(&str, &[u8])> = after
.iter()
.map(|(p, c)| (p.as_str(), c.as_bytes()))
.collect();
entry.repo.commit(
&jitter_branch,
&after_refs,
Author::Agent(loom_bot),
Provenance::Agent,
"net: jitter every retry to avoid a thundering herd",
)?;
Ok(())
})?;
state.telemetry.emit(jac_telemetry::Envelope::event(
"jac.demo.seeded",
SystemClock.now().as_millis(),
));
Ok(json!({
"repos": [slug, halcyon_slug],
"meridian": {
"slug": "meridian-systems",
"founder": ada_id,
"agent": loom_bot_id,
"decision": decision_hex,
},
"arc": {
"first_promotion": blocked,
"after_attestation": admitted,
},
"rendezvous": {
"matches": matches,
"introduction": introduction,
},
"bulk": {
"branch": "feature/retry-jitter",
"note": "eight modules share one substitution; a ninth also changes its policy",
},
"live": {
"branch": "feature/session-cache",
"decision": live_decision_hex,
"note": "this decision is unsettled; the branch parks at the gate until a human attests",
},
}))
}