Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
8.4 kB · 252 lines
Rust
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253//! Layout as hypothesis test (docs/06). Force-directed layouts are forbidden://! they manufacture organic structure from any graph. Radial placement instead://! centrality maps to radius (most central at the center of the sky), community//! to angular sector — so a quilted person renders as a pole star with orbiting//! families, a deflationary person as scattered static, a dimensional person as//! a smear along an arc. Deterministic and versioned: same data + same layout//! version = same sky, which is what makes snapshots citable.
use serde::{Deserialize, Serialize};use sha2::{Digest, Sha256};use sqlx::SqlitePool;
use crate::graph::{self, StoredGraph};
pub const LAYOUT_VERSION: &str = "radial-v1";
/// Canvas is a unit disk scaled to [0, 1000] × [0, 1000], center (500, 500).pub const SIZE: f64 = 1000.0;
#[derive(Serialize, Deserialize, Clone, Debug)]pub struct LayoutNode { pub term: String, pub x: f64, pub y: f64, /// Luminosity in [0,1] (= centrality). pub lum: f64, pub community: i64, pub is_master: bool, /// Gloss-ensemble disagreement in [0,1]: rendered as chromatic blur. pub blur: f64, /// Completion-entropy at this term (the empty-signifier signature). pub entropy: f64,}
#[derive(Serialize, Deserialize, Clone, Debug)]pub struct LayoutEdge { pub src: String, pub dst: String, pub weight: f64, pub sign: f64, pub variance: f64,}
#[derive(Serialize, Deserialize, Clone, Debug)]pub struct Fit { pub gini: f64, pub empty_corr: f64, pub chain_entropy: f64, pub n_completions: i64, pub n_terms: i64, pub posterior_entropy: f64,}
#[derive(Serialize, Deserialize, Clone, Debug)]pub struct Layout { pub version: String, /// "quilted" | "dimensional" | "deflationary" | "insufficient" pub verdict: String, pub fit: Fit, pub interpreter_versions: Vec<String>, pub input_hash: String, pub nodes: Vec<LayoutNode>, pub edges: Vec<LayoutEdge>,}
pub fn verdict_name(v: i64) -> &'static str { match v { 1 => "quilted", 2 => "dimensional", 3 => "deflationary", _ => "insufficient", }}
fn term_hash(term: &str) -> u64 { let mut h: u64 = 1469598103934665603; for b in term.bytes() { h ^= b as u64; h = h.wrapping_mul(1099511628211); } h}
/// Content-address of the exact input set: sorted completion URIs + gloss count.pub async fn input_hash(pool: &SqlitePool, did: &str) -> anyhow::Result<String> { let uris: Vec<(String,)> = sqlx::query_as("SELECT uri FROM completions WHERE did = ? ORDER BY uri") .bind(did) .fetch_all(pool) .await?; let (glosses,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM glosses WHERE did = ? AND orphaned = 0") .bind(did) .fetch_one(pool) .await?; // The latest refit that touched this user also shapes the sky (verdict, // fit numbers), so it is part of the input set. let refit: (i64,) = sqlx::query_as("SELECT COALESCE(MAX(refit_at), 0) FROM metrics WHERE scope = ?") .bind(did) .fetch_one(pool) .await?; let mut hasher = Sha256::new(); for (u,) in &uris { hasher.update(u.as_bytes()); hasher.update(b"\n"); } hasher.update(glosses.to_le_bytes()); hasher.update(refit.0.to_le_bytes()); hasher.update(LAYOUT_VERSION.as_bytes()); Ok(format!("{:x}", hasher.finalize()))}
/// Compute (or fetch cached) layout for a user.pub async fn layout_for(pool: &SqlitePool, did: &str) -> anyhow::Result<Layout> { let hash = input_hash(pool, did).await?; let cached: Option<(String,)> = sqlx::query_as( "SELECT layout FROM layouts WHERE did = ? AND layout_version = ? AND input_hash = ?", ) .bind(did) .bind(LAYOUT_VERSION) .bind(&hash) .fetch_optional(pool) .await?; if let Some((json,)) = cached { if let Ok(l) = serde_json::from_str(&json) { return Ok(l); } }
let layout = compute(pool, did, &hash).await?; sqlx::query( "INSERT INTO layouts (did, layout_version, input_hash, layout, computed_at) VALUES (?,?,?,?,?) ON CONFLICT (did, layout_version, input_hash) DO UPDATE SET layout = excluded.layout", ) .bind(did) .bind(LAYOUT_VERSION) .bind(&hash) .bind(serde_json::to_string(&layout)?) .bind(crate::db::now()) .execute(pool) .await?; Ok(layout)}
async fn compute(pool: &SqlitePool, did: &str, hash: &str) -> anyhow::Result<Layout> { let g: StoredGraph = graph::load_user_graph(pool, did).await?;
// Latest slow-loop metrics for this user (may predate the newest data; // the frame carries the caveat). let metric = |name: &str| { let name = name.to_string(); async move { let row: Option<(f64,)> = sqlx::query_as( "SELECT value FROM metrics WHERE scope = ? AND name = ? ORDER BY refit_at DESC, id DESC LIMIT 1", ) .bind(did) .bind(name) .fetch_optional(pool) .await .ok() .flatten(); row.map(|(v,)| v) } }; let verdict_v = metric("user.verdict").await; let gini = metric("user.centrality_gini").await; let empty_corr = metric("user.empty_signifier_corr").await; let chain_entropy = metric("user.chain_entropy").await;
let (n_completions,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM completions WHERE did = ?") .bind(did) .fetch_one(pool) .await?; let posterior: Option<(f64,)> = sqlx::query_as("SELECT entropy FROM posteriors WHERE did = ?") .bind(did) .fetch_optional(pool) .await?; let interpreters: Vec<(String,)> = sqlx::query_as( "SELECT DISTINCT ensemble_member || '/' || prompt_version FROM glosses WHERE did = ?", ) .bind(did) .fetch_all(pool) .await?;
// Verdict: slow-loop value if present; 'insufficient' before first refit // over this user unless there is really nothing there at all. let verdict = verdict_name(verdict_v.map(|v| v as i64).unwrap_or(0)).to_string();
// ---- radial placement ---- let communities: Vec<i64> = graph::communities(&g).into_iter().collect(); let n_comm = communities.len().max(1); let sector = std::f64::consts::TAU / n_comm as f64; let center = SIZE / 2.0; let r_max = SIZE * 0.46;
let mut nodes = Vec::with_capacity(g.terms.len()); // Deterministic within-sector ordering: by centrality rank then term hash. for t in &g.terms { let ci = communities.iter().position(|c| *c == t.community).unwrap_or(0); let h = term_hash(&t.term); // Radius: most central at the center of the sky. let radius = (1.0 - t.centrality).clamp(0.0, 1.0) * r_max; // Angle: sector base + deterministic spread within the sector, leaving // a small gutter so sectors read as families. let frac = (h % 10_000) as f64 / 10_000.0; let angle = sector * ci as f64 + sector * (0.08 + 0.84 * frac); nodes.push(LayoutNode { term: t.term.clone(), x: center + radius * angle.cos(), y: center + radius * angle.sin(), lum: t.centrality, community: t.community, is_master: t.is_master, blur: t.gloss_variance.min(1.0), entropy: t.entropy, }); }
let edges = g .edges .iter() .map(|e| LayoutEdge { src: e.src.clone(), dst: e.dst.clone(), weight: e.weight, sign: e.sign, variance: e.variance, }) .collect();
Ok(Layout { version: LAYOUT_VERSION.into(), verdict, fit: Fit { gini: gini.unwrap_or(0.0), empty_corr: empty_corr.unwrap_or(0.0), chain_entropy: chain_entropy.unwrap_or(0.0), n_completions, n_terms: g.terms.len() as i64, posterior_entropy: posterior.map(|(e,)| e).unwrap_or(0.0), }, interpreter_versions: interpreters.into_iter().map(|(s,)| s).collect(), input_hash: hash.to_string(), nodes, edges, })}