Implement Sophia MVP scaffold (stages 0–3 + topology pivot)

Six-crate Rust workspace (core/sim/llm/store/server/bin) backing a
Three.js + WebGL frontend. Live at http://127.0.0.1:7777 via `cargo run`.

Sim
- Event-driven scheduler with min-heap, per-engram tick, staggered Spawn
  events (40 ms apart) so each engram's flight is visually readable.
- Solid-donut torus topology (replaces original spherical density-driven
  shell, see Topology Pivot in docs/system-analysis.md). Configurable
  major/minor radii in config.toml; live `POST /api/galaxy/:id/resize`.
- Physics: Verlet integration + friction; in-hole pull + galactic spin
  (CCW around +z) for spiral-ejection ejection from the donut centre;
  soft tube boundary with velocity-reflecting wall.
- Cosine-weighted gravity (kiddo k-NN within radius 25, threshold 0.50)
  and synapse formation (threshold 0.62) gated to inside-the-tube only.
- LM Studio integration via OpenAI-compatible REST: batched embeddings,
  optional Bearer auth, semaphore-bounded parallel ops per §13.5.

Server
- axum HTTP + WebSocket. Routes: /healthz, /api/galaxy CRUD, /seed,
  /ingest, /resize, /engrams/:id, /ws/galaxy/:id/events.
- Binary 12-byte-aligned position frames at ~20 Hz; JSON for sparse
  events (Hello, EngramCreated, SynapseCreated, TorusUpdated).
- Layered config: config.toml (defaults) + config.local.toml (secrets,
  gitignored) merged on startup.

Frontend
- Vite + vanilla TypeScript + three.js 0.169.
- Engrams render as additive bloom-friendly point sprites with a
  per-engram hash-driven hue rotation and breathing pulse.
- Comet-style velocity-aligned trails; additive ribbon synapses whose
  endpoints track engram positions every frame.
- UnrealBloomPass + ACES tone-mapping for the linked-particles look.
- HUD shows torus dims, engram + synapse counts, LM Studio status;
  controls for seed, ingest, and live torus resize.

Docs
- README replaced with docs/IDEA.md; system-analysis.md updated with
  the topology pivot decisions and Galaxy Ejection refinement notes
  (the implementation plan lives in ~/.claude/plans, gitignored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 08:29:37 +02:00
parent 43c4d270e6
commit 8688f632bf
44 changed files with 7664 additions and 100 deletions

View File

@@ -0,0 +1,24 @@
[package]
name = "sophia-bin"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
authors.workspace = true
[[bin]]
name = "sophia"
path = "src/main.rs"
[dependencies]
sophia-core = { workspace = true }
sophia-server = { workspace = true }
sophia-sim = { workspace = true }
sophia-llm = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }

View File

@@ -0,0 +1,133 @@
//! Sophia binary. Reads `config.toml`, initializes tracing, starts the server.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use sophia_core::GalaxyShape;
use sophia_llm::{LmStudioClient, LmStudioConfig, TokenCapsAll};
use sophia_server::{serve, ServerConfig};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[derive(Debug, Deserialize)]
struct Config {
server: ServerSection,
lm_studio: LmStudioConfig,
#[allow(dead_code)] // wired in to LLM call sites in Stages 4+
token_caps: TokenCapsAll,
#[serde(default)]
galaxy_defaults: GalaxyDefaultsSection,
}
#[derive(Debug, Deserialize)]
struct ServerSection {
host: String,
port: u16,
static_dir: String,
}
#[derive(Debug, Deserialize)]
struct GalaxyDefaultsSection {
major_radius: f32,
minor_radius: f32,
}
impl Default for GalaxyDefaultsSection {
fn default() -> Self {
Self {
major_radius: GalaxyShape::DEFAULT_MAJOR,
minor_radius: GalaxyShape::DEFAULT_MINOR,
}
}
}
fn init_tracing() {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,sophia=debug")))
.with(fmt::layer())
.init();
}
/// Recursively merge `local` into `base`. Tables are deep-merged; scalars
/// and arrays are overwritten by `local`. Used to layer `config.local.toml`
/// (gitignored, secrets) on top of `config.toml` (versioned defaults).
fn merge_toml(base: &mut toml::Value, local: toml::Value) {
match (base, local) {
(toml::Value::Table(b), toml::Value::Table(l)) => {
for (k, v) in l {
match b.get_mut(&k) {
Some(existing) => merge_toml(existing, v),
None => {
b.insert(k, v);
}
}
}
}
(slot, other) => *slot = other,
}
}
fn load_config() -> Result<Config> {
let base_path = "config.toml";
if !Path::new(base_path).exists() {
anyhow::bail!("no config.toml found in cwd");
}
let base_text = std::fs::read_to_string(base_path)
.with_context(|| format!("reading {base_path}"))?;
let mut merged: toml::Value = toml::from_str(&base_text)
.with_context(|| format!("parsing {base_path}"))?;
tracing::info!("loaded config from {base_path}");
let local_path = "config.local.toml";
if Path::new(local_path).exists() {
let local_text = std::fs::read_to_string(local_path)
.with_context(|| format!("reading {local_path}"))?;
let local_value: toml::Value = toml::from_str(&local_text)
.with_context(|| format!("parsing {local_path}"))?;
merge_toml(&mut merged, local_value);
tracing::info!("merged overrides from {local_path}");
}
let cfg: Config = merged.try_into().context("deserializing merged config")?;
Ok(cfg)
}
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
let cfg = load_config()?;
let server_cfg = ServerConfig {
host: cfg.server.host,
port: cfg.server.port,
static_dir: PathBuf::from(cfg.server.static_dir),
};
let llm = LmStudioClient::new(cfg.lm_studio);
// Best-effort startup ping — failure logs but doesn't block boot, so the
// server starts even if LM Studio isn't running yet (the user can launch
// it after, and `/healthz` will report current status).
match llm.list_models().await {
Ok(models) => tracing::info!("LM Studio reachable; {} model(s) loaded", models.len()),
Err(e) => tracing::warn!("LM Studio unreachable at startup: {e}"),
}
let default_shape = GalaxyShape {
major_radius: cfg.galaxy_defaults.major_radius,
minor_radius: cfg.galaxy_defaults.minor_radius,
};
if let Err(msg) = default_shape.validate() {
anyhow::bail!("invalid [galaxy_defaults] in config: {msg}");
}
tracing::info!(
"galaxy defaults: major_radius={} minor_radius={}",
default_shape.major_radius,
default_shape.minor_radius
);
let sim = sophia_sim::spawn_sim(default_shape);
let info = sim.create_galaxy("default".to_string()).await?;
tracing::info!("default galaxy ready: id={:?} name={}", info.id, info.name);
serve(server_cfg, sim, llm).await
}