Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
30 kB · 840 lines
Rust
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841//! The constellation surface (docs/06): every state a URL. Layout is the//! hypothesis test; comparison never overlays two graphs (that would imply the//! shared coordinate space the theory denies) — the primitive is the//! shared-term contrast. Comparison is consent-gated by pairing, not publicity.
use std::collections::BTreeMap;
use axum::extract::{Path, State};use axum::http::{header, StatusCode};use axum::response::{IntoResponse, Redirect, Response};use axum_extra::extract::cookie::CookieJar;use jacquard::client::AgentSessionExt;use jacquard::identity::resolver::IdentityResolver;use maud::{html, Markup, PreEscaped};use smol_str::SmolStr;
use crate::atproto::records::Snapshot;use crate::infer::{profile_cosine, user_profile, Profile};use crate::render::layout::{self, Layout};use crate::render::svg::{render_sky, SvgOptions};use crate::state::SharedState;use crate::web::auth;use crate::web::pages::shell;
fn internal(e: anyhow::Error) -> Response { tracing::error!("sky route error: {e:#}"); (StatusCode::INTERNAL_SERVER_ERROR, format!("something broke: {e}")).into_response()}
/// Accepts a handle or a DID; returns the DID.async fn resolve_actor(state: &SharedState, actor: &str) -> anyhow::Result<String> { if actor.starts_with("did:") { return Ok(actor.to_string()); } let handle = jacquard::types::string::Handle::new(SmolStr::new(actor)) .map_err(|e| anyhow::anyhow!("invalid handle: {e}"))?; let did = state .oauth .resolve_handle(&handle) .await .map_err(|e| anyhow::anyhow!("could not resolve {actor}: {e}"))?; Ok(did.as_str().to_string())}
/// Best display name we have without a network call: login handle if this DID/// has a web session, else the DID truncated.async fn display_name(state: &SharedState, did: &str) -> String { let row: Option<(Option<String>,)> = sqlx::query_as( "SELECT handle FROM web_sessions WHERE did = ? AND handle IS NOT NULL LIMIT 1", ) .bind(did) .fetch_optional(&state.pool) .await .ok() .flatten(); match row { Some((Some(h),)) => h, _ => { if did.len() > 24 { format!("{}…", &did[..24]) } else { did.to_string() } } }}
fn verdict_blurb(verdict: &str) -> &'static str { match verdict { "quilted" => { "Your sky has a center. A small set of terms organizes the rest; the dark star is \ the term doing the quilting — and the theory's sharpest claim is that it is \ organizing precisely because it carries the least positive content of its own." } "dimensional" => { "Your completions arrange along an arc rather than around a center. On your data, \ the compass people are — embarrassingly for this instrument's theory — right: a \ continuous dimension explains you better than a quilt does." } "deflationary" => { "Your completions don't yet show a stable center. This may mean more sessions — or \ it may mean you don't have one: ideology as situational sampling from ambient \ discourse. Both theoretical camps are organized against this outcome, which is \ exactly why the instrument must be able to return it." } _ => { "Not enough data yet for the geometry to mean anything. The frame below shows \ exactly how little the sky currently rests on." } }}
// ============ /sky/{handle} ============
pub async fn sky_page( State(state): State<SharedState>, jar: CookieJar, Path(actor): Path<String>,) -> Response { match sky_page_inner(&state, &jar, &actor).await { Ok(m) => m.into_response(), Err(e) => internal(e), }}
async fn sky_page_inner( state: &SharedState, jar: &CookieJar, actor: &str,) -> anyhow::Result<Markup> { let did = resolve_actor(state, actor).await?; let viewer = auth::viewer(state, jar).await; let is_self = viewer.as_ref().map(|v| v.did == did).unwrap_or(false); let name = display_name(state, &did).await;
let l = layout::layout_for(&state.pool, &did).await?; let svg = render_sky( &l, &SvgOptions { width: 900, height: 900, focus: None, card: false, title: name.clone() }, );
// Top terms for the tap-list (term-focus fragments). let mut top: Vec<&layout::LayoutNode> = l.nodes.iter().collect(); top.sort_by(|a, b| b.lum.total_cmp(&a.lum)); let top: Vec<&layout::LayoutNode> = top.into_iter().take(24).collect();
// Kin: twin, antipode, mirror (docs/06). let kin = find_kin(state, &did).await.unwrap_or_default();
Ok(shell( &format!("sky of {name}"), viewer.is_some(), html! { h1 { "the sky of " (name) } p { (verdict_blurb(&l.verdict)) } div class="skywrap" id="skyviewport" { (PreEscaped(svg)) } script src="/static/sky.js" defer {} p class="frame-note" { "Layout " (l.version) " — deterministic: same data, same sky. " "Ensemble disagreement renders as shimmer; kinship edges cool, antagonisms warm. " a href={"/sky/" (did) "/card.png"} { "badge" } @if is_self { " · " a href="/export" { "export everything" } } } @if is_self { form method="post" action="/snapshot" { button type="submit" { "publish this reading of myself" } } p class="frame-note" { "Publishing writes an " code { "app.ideonomics.snapshot" } " record to your repo: " "a speech act, citable at a fixed AT-URI. Nothing is ever published automatically." } } h2 { "terms" } div class="termlist" id="termfocus" { @for n in &top { div { a hx-get={"/sky/" (did) "/term/" (n.term)} hx-target="#termdetail" href="#termdetail" { (n.term) } @if n.is_master { " ⭘" } } } } div id="termdetail" {} @if !kin.is_empty() { h2 { "kin" } table { tr { th { "relation" } th { "who" } th {} } @for (label, kdid, kname) in &kin { tr { td { (label) } td { a href={"/sky/" (kdid)} { (kname) } } td { a href={"/compare/" (did) "/" (kdid)} { "compare" } } } } } p class="frame-note" { "Comparison requires pairing: the records are public by protocol, but the social act of juxtaposition is opt-in." } } @if is_self { h2 { "pairing" } (pairing_panel(state, &did).await?) } }, ))}
/// Twin (most similar quilt), antipode (most divergent), mirror (same nodal/// points, opposite valences — the antagonist relation).async fn find_kin( state: &SharedState, did: &str,) -> anyhow::Result<Vec<(&'static str, String, String)>> { let me = user_profile(&state.pool, did).await?; let me_signed = signed_profile(&state.pool, did).await?; if me.is_empty() { return Ok(Vec::new()); } let others: Vec<(String,)> = sqlx::query_as("SELECT DISTINCT did FROM edges WHERE did != ?") .bind(did) .fetch_all(&state.pool) .await?;
let mut twin: Option<(String, f64)> = None; let mut antipode: Option<(String, f64)> = None; let mut mirror: Option<(String, f64)> = None; for (other,) in others { let p = user_profile(&state.pool, &other).await?; if p.is_empty() { continue; } let sim = profile_cosine(&me, &p); if twin.as_ref().map(|(_, s)| sim > *s).unwrap_or(true) { twin = Some((other.clone(), sim)); } if antipode.as_ref().map(|(_, s)| sim < *s).unwrap_or(true) { antipode = Some((other.clone(), sim)); } // Mirror: high similarity of |profile| but sign-flipped valences. let ps = signed_profile(&state.pool, &other).await?; let mut flipped = ps.clone(); for v in flipped.values_mut() { *v = -*v; } let msim = profile_cosine(&me_signed, &flipped); if mirror.as_ref().map(|(_, s)| msim > *s).unwrap_or(true) { mirror = Some((other.clone(), msim)); } }
let mut out = Vec::new(); for (label, k) in [("twin", twin), ("antipode", antipode), ("mirror", mirror)] { if let Some((kdid, _)) = k { let name = display_name(state, &kdid).await; out.push((label, kdid, name)); } } out.dedup_by(|a, b| a.1 == b.1); Ok(out)}
async fn signed_profile(pool: &sqlx::SqlitePool, did: &str) -> anyhow::Result<Profile> { let rows: Vec<(String, i64, f64)> = sqlx::query_as( "SELECT src, relation, SUM(weight * sign) FROM edges WHERE did = ? GROUP BY src, relation", ) .bind(did) .fetch_all(pool) .await?; let mut p = Profile::new(); for (term, rel, w) in rows { *p.entry(format!("{term}|{rel}")).or_default() += w; } crate::infer::normalize_profile(&mut p); Ok(p)}
async fn pairing_panel(state: &SharedState, did: &str) -> anyhow::Result<Markup> { let pairings: Vec<(String, String, String, String)> = sqlx::query_as( "SELECT did_a, did_b, status, requested_by FROM pairings WHERE did_a = ? OR did_b = ?", ) .bind(did) .bind(did) .fetch_all(&state.pool) .await?; let mut rows = Vec::new(); for (a, b, status, requested_by) in pairings { let other = if a == did { b } else { a }; let name = display_name(state, &other).await; rows.push((other, name, status, requested_by)); } Ok(html! { @for (other, name, status, requested_by) in &rows { div class="rival" { span { (name) " — " (status) } @if status == "requested" && requested_by != did { form method="post" action={"/pair/" (other) "/accept"} style="display:inline" { button type="submit" { "accept" } } } } } form method="post" action="/pair/request" onsubmit="this.action='/pair/'+encodeURIComponent(this.handle.value)" { input type="text" name="handle" placeholder="handle to pair with"; button type="submit" { "request pairing" } } })}
// ============ /sky/{handle}/term/{term} ============
pub async fn term_fragment( State(state): State<SharedState>, Path((actor, term)): Path<(String, String)>,) -> Response { match term_fragment_inner(&state, &actor, &term).await { Ok(m) => m.into_response(), Err(e) => internal(e), }}
async fn term_fragment_inner( state: &SharedState, actor: &str, term: &str,) -> anyhow::Result<Markup> { let did = resolve_actor(state, actor).await?; let term = term.trim().to_lowercase();
let info: Option<(f64, f64, f64, bool)> = sqlx::query_as( "SELECT centrality, entropy, gloss_variance, is_master FROM terms WHERE did = ? AND term = ?", ) .bind(&did) .bind(&term) .fetch_optional(&state.pool) .await?; let Some((centrality, entropy, variance, is_master)) = info else { return Ok(html! { p class="err" { "no such star in this sky" } }); }; // The hollow-center pedagogy belongs only to a quilted sky (docs/06). let quilted = layout::layout_for(&state.pool, &did) .await .map(|l| l.verdict == "quilted") .unwrap_or(false); let is_dark_star = is_master && quilted;
// Incident relations, labeled from the discovered vocabulary. let relations: Vec<(String, String, f64, f64)> = sqlx::query_as( "SELECT CASE WHEN e.src = ? THEN e.dst ELSE e.src END, r.label, SUM(e.weight), AVG(e.sign) FROM edges e JOIN relations r ON r.id = e.relation WHERE e.did = ? AND (e.src = ? OR e.dst = ?) GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 12", ) .bind(&term) .bind(&did) .bind(&term) .bind(&term) .fetch_all(&state.pool) .await?;
Ok(html! { h3 { (term) @if is_dark_star { " — the dark star" } } @if is_dark_star { p { "This is your most central term, and it is rendered hollow on purpose: the " "instrument's claim is that what organizes your other meanings has the least " "determinate meaning of its own. Its completion-entropy (" (format!("{entropy:.2}")) ") is the measurement of that emptiness." } } p class="frame-note" { "centrality " (format!("{centrality:.2}")) " · completion-entropy " (format!("{entropy:.2}")) " · ensemble disagreement " (format!("{variance:.2}")) } @if !relations.is_empty() { table { tr { th { "bound to" } th { "as" } th { "weight" } th { "valence" } } @for (other, label, weight, sign) in &relations { tr { td { (other) } td { (label) } td { (format!("{weight:.2}")) } td { (format!("{sign:+.2}")) } } } } } })}
// ============ /sky/{handle}/card.png ============
pub async fn card_png( State(state): State<SharedState>, Path(actor): Path<String>,) -> Response { let inner = async { let did = resolve_actor(&state, &actor).await?; let name = display_name(&state, &did).await; let l = layout::layout_for(&state.pool, &did).await?; crate::render::render_card_png(&l, &name) }; match inner.await { Ok(png) => ([(header::CONTENT_TYPE, "image/png")], png).into_response(), Err(e) => internal(e), }}
// ============ /compare/{a}/{b} ============
async fn pairing_accepted(state: &SharedState, a: &str, b: &str) -> anyhow::Result<bool> { if a == b { return Ok(true); } let row: Option<(i64,)> = sqlx::query_as( "SELECT 1 FROM pairings WHERE status = 'accepted' AND ((did_a = ? AND did_b = ?) OR (did_a = ? AND did_b = ?))", ) .bind(a) .bind(b) .bind(b) .bind(a) .fetch_optional(&state.pool) .await?; Ok(row.is_some())}
#[derive(serde::Deserialize)]pub struct CompareQuery { term: Option<String>,}
pub async fn compare_page( State(state): State<SharedState>, jar: CookieJar, Path((a, b)): Path<(String, String)>, axum::extract::Query(q): axum::extract::Query<CompareQuery>,) -> Response { match compare_inner(&state, &jar, &a, &b, q.term.as_deref()).await { Ok(m) => m.into_response(), Err(e) => internal(e), }}
async fn compare_inner( state: &SharedState, jar: &CookieJar, a: &str, b: &str, term: Option<&str>,) -> anyhow::Result<Markup> { let da = resolve_actor(state, a).await?; let db = resolve_actor(state, b).await?; let viewer = auth::viewer(state, jar).await; let viewer_is_party = viewer.as_ref().map(|v| v.did == da || v.did == db).unwrap_or(false);
if !pairing_accepted(state, &da, &db).await? && !viewer_is_party { return Ok(shell( "compare", viewer.is_some(), html! { h1 { "not paired" } p { "These two skies haven't consented to juxtaposition. The underlying records " "are public by protocol — a ruder AppView could show you this — but here, " "summoning someone's sky beside another is an opt-in social act." } }, )); }
let na = display_name(state, &da).await; let nb = display_name(state, &db).await; let la = layout::layout_for(&state.pool, &da).await?; let lb = layout::layout_for(&state.pool, &db).await?;
// Shared vocabulary, ranked by combined centrality. let lum = |l: &Layout| -> BTreeMap<String, f64> { l.nodes.iter().map(|n| (n.term.clone(), n.lum)).collect() }; let ma = lum(&la); let mb = lum(&lb); let mut shared: Vec<(String, f64)> = ma .iter() .filter_map(|(t, ca)| mb.get(t).map(|cb| (t.clone(), ca + cb))) .collect(); shared.sort_by(|x, y| y.1.total_cmp(&x.1)); shared.truncate(12);
// Explicit ?term= wins; otherwise focus the most jointly central shared term. let focus = term .map(|t| t.trim().to_lowercase()) .filter(|t| !t.is_empty()) .or_else(|| shared.first().map(|(t, _)| t.clone())); let svg_a = render_sky( &la, &SvgOptions { width: 450, height: 450, focus: focus.clone(), card: false, title: na.clone() }, ); let svg_b = render_sky( &lb, &SvgOptions { width: 450, height: 450, focus: focus.clone(), card: false, title: nb.clone() }, );
Ok(shell( &format!("{na} ⇄ {nb}"), viewer.is_some(), html! { h1 { (na) " ⇄ " (nb) } p { "Same stars, different skies. Pick a term you both use: it may be central and dark " "in one sky, peripheral and bright in the other, bound by opposite relations. " "You don't disagree about propositions; you inhabit different quiltings of the same vocabulary." } div class="compare" { div class="skywrap" { (PreEscaped(svg_a)) } div class="skywrap" { (PreEscaped(svg_b)) } } @if !shared.is_empty() { h2 { "shared terms" } div class="termlist" { @for (t, _) in &shared { div { a href={"/compare/" (da) "/" (db) "?term=" (t)} { (t) } } } } div class="compare" { div { (term_fragment_inner(state, &da, focus.as_deref().unwrap_or("")).await.unwrap_or(html!{})) } div { (term_fragment_inner(state, &db, focus.as_deref().unwrap_or("")).await.unwrap_or(html!{})) } } } @else { p { "No shared vocabulary yet." } } }, ))}
// ============ /atlas ============
pub async fn atlas_page(State(state): State<SharedState>, jar: CookieJar) -> Response { match atlas_inner(&state, &jar).await { Ok(m) => m.into_response(), Err(e) => internal(e), }}
async fn atlas_inner(state: &SharedState, jar: &CookieJar) -> anyhow::Result<Markup> { let viewer = auth::viewer(state, jar).await; let dids: Vec<(String,)> = sqlx::query_as("SELECT DISTINCT did FROM edges").fetch_all(&state.pool).await?; let mut profiles: Vec<(String, Profile)> = Vec::new(); for (did,) in dids { let p = user_profile(&state.pool, &did).await?; if !p.is_empty() { profiles.push((did, p)); } }
let positions = mds_2d(&profiles); let mut dots = String::new(); for (i, (did, _)) in profiles.iter().enumerate() { let (x, y) = positions.get(i).copied().unwrap_or((0.5, 0.5)); let name = display_name(state, did).await; dots.push_str(&format!( "<a href='/sky/{did}'><circle cx='{:.1}' cy='{:.1}' r='7' fill='hsl({:.0},70%,65%)' opacity='0.9'><title>{}</title></circle></a>", 60.0 + x * 880.0, 60.0 + y * 880.0, (i as f64 * 47.0) % 360.0, name.replace('<', "").replace('&', "").replace('\'', ""), )); }
Ok(shell( "atlas", viewer.is_some(), html! { h1 { "the atlas" } p { "Every user is a dot placed by relational-profile similarity. Whether this page " "shows a smooth continuum or discrete islands is the manifold-vs-atlas question " "made visible: your neighborhood is your discursive community, discovered rather than declared." } @if profiles.len() < 3 { p class="frame-note" { "The atlas needs at least a few inhabited skies. (" (profiles.len()) " so far.)" } } div class="skywrap" { (PreEscaped(format!( "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000' width='900' height='900'><rect width='1000' height='1000' fill='#05060f'/>{dots}</svg>" ))) } }, ))}
/// Classical MDS to the unit square: double-centered cosine-distance Gram/// matrix, top-2 eigenvectors by power iteration. Deterministic.fn mds_2d(profiles: &[(String, Profile)]) -> Vec<(f64, f64)> { let n = profiles.len(); if n == 0 { return Vec::new(); } if n == 1 { return vec![(0.5, 0.5)]; } let mut d2 = vec![vec![0f64; n]; n]; for i in 0..n { for j in (i + 1)..n { let dist = 1.0 - profile_cosine(&profiles[i].1, &profiles[j].1); d2[i][j] = dist * dist; d2[j][i] = d2[i][j]; } } // Double centering: B = -1/2 J D² J let row_mean: Vec<f64> = d2.iter().map(|r| r.iter().sum::<f64>() / n as f64).collect(); let grand: f64 = row_mean.iter().sum::<f64>() / n as f64; let mut b = vec![vec![0f64; n]; n]; for i in 0..n { for j in 0..n { b[i][j] = -0.5 * (d2[i][j] - row_mean[i] - row_mean[j] + grand); } } let mut coords = vec![(0f64, 0f64); n]; let mut deflated = b; for axis in 0..2 { let mut v: Vec<f64> = (0..n).map(|i| ((i * 2654435761) % 1000) as f64 / 1000.0 - 0.5).collect(); let mut eig = 0.0; for _ in 0..100 { let mut nv = vec![0f64; n]; for i in 0..n { for j in 0..n { nv[i] += deflated[i][j] * v[j]; } } let norm: f64 = nv.iter().map(|x| x * x).sum::<f64>().sqrt(); if norm < 1e-12 { break; } for x in nv.iter_mut() { *x /= norm; } eig = norm; v = nv; } let scale = eig.max(0.0).sqrt(); for i in 0..n { if axis == 0 { coords[i].0 = v[i] * scale; } else { coords[i].1 = v[i] * scale; } } for i in 0..n { for j in 0..n { deflated[i][j] -= eig * v[i] * v[j]; } } } // Normalize into [0,1]². let min_x = coords.iter().map(|c| c.0).fold(f64::MAX, f64::min); let max_x = coords.iter().map(|c| c.0).fold(f64::MIN, f64::max); let min_y = coords.iter().map(|c| c.1).fold(f64::MAX, f64::min); let max_y = coords.iter().map(|c| c.1).fold(f64::MIN, f64::max); coords .into_iter() .map(|(x, y)| { ( if max_x > min_x { (x - min_x) / (max_x - min_x) } else { 0.5 }, if max_y > min_y { (y - min_y) / (max_y - min_y) } else { 0.5 }, ) }) .collect()}
// ============ /export ============
pub async fn export_json(State(state): State<SharedState>, jar: CookieJar) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; match export_inner(&state, &viewer.did).await { Ok(json) => ( [ (header::CONTENT_TYPE, "application/json".to_string()), (header::CONTENT_DISPOSITION, "attachment; filename=ideonomics-export.json".to_string()), ], json, ) .into_response(), Err(e) => internal(e), }}
async fn export_inner(state: &SharedState, did: &str) -> anyhow::Result<String> { let table = |q: &'static str| { let pool = state.pool.clone(); let did = did.to_string(); async move { let rows: Vec<(String,)> = sqlx::query_as(q).bind(&did).fetch_all(&pool).await?; anyhow::Ok( rows.into_iter() .filter_map(|(j,)| serde_json::from_str::<serde_json::Value>(&j).ok()) .collect::<Vec<_>>(), ) } }; // json_object gives us row-as-JSON without per-table structs. let completions = table( "SELECT json_object('uri',uri,'square',square_uri,'slot',slot,'text',text,'mode',mode, 'chainParent',chain_parent,'chainDepth',chain_depth,'latencyBucket',latency_bucket, 'createdAt',created_at) FROM completions WHERE did = ?", ) .await?; let resonances = table( "SELECT json_object('uri',uri,'completion',completion_uri,'reaction',reaction, 'createdAt',created_at) FROM resonances WHERE did = ?", ) .await?; let glosses = table( "SELECT json_object('uri',uri,'completion',completion_uri,'readings',readings, 'interpreter',interpreter,'promptVersion',prompt_version,'ensembleMember',ensemble_member, 'createdAt',created_at) FROM glosses WHERE did = ? AND orphaned = 0", ) .await?; let layout = layout::layout_for(&state.pool, did).await.ok(); let posterior: Option<(String, f64)> = sqlx::query_as("SELECT weights, entropy FROM posteriors WHERE did = ?") .bind(did) .fetch_optional(&state.pool) .await?;
Ok(serde_json::to_string_pretty(&serde_json::json!({ "did": did, "exportedAt": crate::db::now(), "records": { "completions": completions, "resonances": resonances, }, "derived": { "glosses": glosses, "layout": layout, "posterior": posterior.map(|(w, e)| serde_json::json!({ "weights": serde_json::from_str::<serde_json::Value>(&w).unwrap_or_default(), "entropy": e, })), }, "note": "records/* is your testimony (also in your PDS); derived/* is this AppView's reading of it, versioned by interpreter and layout version." }))?)}
// ============ /snapshot ============
pub async fn publish_snapshot(State(state): State<SharedState>, jar: CookieJar) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; match snapshot_inner(&state, &viewer).await { Ok(uri) => shell( "snapshot published", true, html! { h1 { "published" } p { "Your reading of yourself is now a citable record:" } p { code { (uri) } } p { a href={"/sky/" (viewer.did)} { "back to your sky" } } }, ) .into_response(), Err(e) => internal(e), }}
async fn snapshot_inner(state: &SharedState, viewer: &auth::Viewer) -> anyhow::Result<String> { let l = layout::layout_for(&state.pool, &viewer.did).await?; let record = Snapshot { layout_version: SmolStr::new(&l.version), interpreter_versions: l.interpreter_versions.iter().map(|s| SmolStr::new(s)).collect(), input_hash: SmolStr::new(&l.input_hash), layout: serde_json::to_string(&l)?, verdict: SmolStr::new(&l.verdict), created_at: jacquard::types::string::Datetime::now(), }; let record_json = serde_json::to_value(&record)?; let agent = viewer.agent(state).await?; let out = agent .create_record(record, None) .await .map_err(|e| anyhow::anyhow!("PDS write failed: {e}"))?; let uri = out.uri.to_string(); crate::ingest::jetstream::index_record( state, &viewer.did, "app.ideonomics.snapshot", &uri, &out.cid.to_string(), &record_json, ) .await?; Ok(uri)}
// ============ pairing ============
pub async fn request_pairing( State(state): State<SharedState>, jar: CookieJar, Path(actor): Path<String>,) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; let inner = async { let other = resolve_actor(&state, &actor).await?; anyhow::ensure!(other != viewer.did, "cannot pair with yourself"); sqlx::query( "INSERT INTO pairings (did_a, did_b, status, requested_by, created_at) VALUES (?,?,'requested',?,?) ON CONFLICT (did_a, did_b) DO NOTHING", ) .bind(&viewer.did) .bind(&other) .bind(&viewer.did) .bind(crate::db::now()) .execute(&state.pool) .await?; anyhow::Ok(viewer.did.clone()) }; match inner.await { Ok(did) => Redirect::to(&format!("/sky/{did}")).into_response(), Err(e) => internal(e), }}
pub async fn accept_pairing( State(state): State<SharedState>, jar: CookieJar, Path(actor): Path<String>,) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; let inner = async { let other = resolve_actor(&state, &actor).await?; sqlx::query( "UPDATE pairings SET status = 'accepted' WHERE did_a = ? AND did_b = ? AND status = 'requested'", ) .bind(&other) .bind(&viewer.did) .execute(&state.pool) .await?; anyhow::Ok(viewer.did.clone()) }; match inner.await { Ok(did) => Redirect::to(&format!("/sky/{did}")).into_response(), Err(e) => internal(e), }}