//! Live Integration 1 for the deployed HTTP surface. //! //! The existing API remains the protected content producer. This module runs it //! on a loopback port, then gives successful `/chat` responses one production //! path through a typed semantic plan, persistent VoiceState, deterministic //! rendering, and an inspectable append-only trace. use anyhow::{anyhow, Context, Result}; use serde::Serialize; use serde_json::{json, Value}; use star::omega_v1_semantic_plan::{ ClaimProvenance, DetailBudget, DialoguePolicy, EmotionalPosition, EpistemicConfidence, GroundedClaim, InitiativeLevel, PlanConfidenceLevel, PlanIntent, ProhibitedImplication, ResponseStance, SemanticOperation, SemanticOperationKind, SemanticResponsePlan, }; use star::runtime::response_intent::{self, ResponseIntent}; use star::voice_state::{ BasisPoints, BoundedVoiceDelta, VoiceDebugProjection, VoiceDimension, VoiceEvidenceKind, VoiceEvidenceRef, VoiceRevisionEvent, VoiceRevisionReason, VoiceRevisionTarget, VoiceState, }; use star::{api, now_timestamp, Runtime}; use std::fs::{self, OpenOptions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use tracing::{info, warn}; const LIVE_STATE_FILE: &str = "live_voice_state.json"; const LIVE_TRACE_FILE: &str = "live_chat_trace.jsonl"; const LIVE_SESSION_ID: &str = "starfire-http-live-v1"; const LIVE_PIPELINE: &str = "live-integration-1"; #[derive(Debug, Clone, Serialize)] struct LiveTrace { trace_id: String, timestamp: i64, turn: u64, response_intent: String, semantic_plan: SemanticResponsePlan, voice_before: VoiceDebugProjection, voice_after: VoiceDebugProjection, raw_response: String, rendered_response: String, } struct LiveIntegration { state_path: PathBuf, trace_path: PathBuf, voice_state: VoiceState, last_trace: Option, } impl LiveIntegration { fn load(data_dir: &Path) -> Result { fs::create_dir_all(data_dir).with_context(|| { format!( "create live integration data directory {}", data_dir.display() ) })?; let state_path = data_dir.join(LIVE_STATE_FILE); let trace_path = data_dir.join(LIVE_TRACE_FILE); let voice_state = match fs::read_to_string(&state_path) { Ok(json) => match VoiceState::from_canonical_json(&json) { Ok(state) => state, Err(error) => { warn!( "live integration: invalid persisted VoiceState at {}: {}; starting neutral", state_path.display(), error ); VoiceState::default() } }, Err(error) if error.kind() == std::io::ErrorKind::NotFound => VoiceState::default(), Err(error) => { return Err(error).with_context(|| { format!("read persisted VoiceState {}", state_path.display()) }); } }; Ok(Self { state_path, trace_path, voice_state, last_trace: None, }) } fn process(&mut self, input: &str, raw_response: &str) -> Result { let response_intent = response_intent::classify(input); let voice_before = self.voice_state.debug_projection()?; let next_version = self .voice_state .version .checked_add(1) .ok_or_else(|| anyhow!("live VoiceState version overflow"))?; let trace_id = format!("live-{}-{}", now_timestamp(), next_version); let semantic_plan = build_live_plan(&trace_id, input, raw_response, &response_intent); let revision = revision_for( &trace_id, input, &response_intent, self.voice_state.version, )?; self.voice_state .apply_revision(self.voice_state.version, revision)?; let voice_after = self.voice_state.debug_projection()?; let rendered_response = render_live_response(&semantic_plan, &voice_after, raw_response); self.persist_state()?; let trace = LiveTrace { trace_id, timestamp: now_timestamp(), turn: self.voice_state.version, response_intent: response_intent.label().to_owned(), semantic_plan, voice_before, voice_after, raw_response: raw_response.to_owned(), rendered_response, }; self.append_trace(&trace)?; self.last_trace = Some(trace.clone()); Ok(trace) } fn persist_state(&self) -> Result<()> { let json = self.voice_state.to_canonical_json()?; let temporary = self.state_path.with_extension("json.tmp"); fs::write(&temporary, json) .with_context(|| format!("write temporary VoiceState {}", temporary.display()))?; fs::rename(&temporary, &self.state_path).with_context(|| { format!( "atomically replace VoiceState {} with {}", self.state_path.display(), temporary.display() ) })?; Ok(()) } fn append_trace(&self, trace: &LiveTrace) -> Result<()> { let mut file = OpenOptions::new() .create(true) .append(true) .open(&self.trace_path) .with_context(|| format!("open live trace {}", self.trace_path.display()))?; serde_json::to_writer(&mut file, trace).context("serialize live trace")?; file.write_all(b"\n") .context("terminate live trace record")?; file.flush().context("flush live trace")?; Ok(()) } fn status(&self) -> Value { json!({ "enabled": true, "pipeline": LIVE_PIPELINE, "turn": self.voice_state.version, "voice_state": self.voice_state.debug_projection().ok(), "last_trace": &self.last_trace, }) } } pub fn start( runtime: Arc>, host: &str, port: u16, data_dir: &Path, ) -> Result<()> { let internal_port = internal_port(port); let internal_runtime = runtime; thread::Builder::new() .name("starfire-protected-api".to_owned()) .spawn(move || { if let Err(error) = api::start(internal_runtime, "127.0.0.1", internal_port) { warn!("protected Starfire API exited: {}", error); } }) .context("spawn protected Starfire API")?; wait_for_internal_api(internal_port)?; let integration = Arc::new(Mutex::new(LiveIntegration::load(data_dir)?)); let address = format!("{}:{}", host, port); let server = tiny_http::Server::http(&address) .map_err(|error| anyhow!("live API server error: {}", error))?; info!( "Starfire Live Integration 1 ready at http://{} (protected API 127.0.0.1:{})", address, internal_port ); for request in server.incoming_requests() { if let Err(error) = handle_request(request, internal_port, &integration) { warn!("live API request failed: {}", error); } } Ok(()) } fn internal_port(external_port: u16) -> u16 { if let Some(parsed) = std::env::var("STARFIRE_INTERNAL_PORT") .ok() .and_then(|value| value.parse::().ok()) .filter(|parsed| *parsed != external_port) { return parsed; } external_port.checked_add(1).unwrap_or(18_081) } fn wait_for_internal_api(port: u16) -> Result<()> { let url = format!("http://127.0.0.1:{}/health", port); for _ in 0..100 { if ureq::get(&url).call().is_ok() { return Ok(()); } thread::sleep(Duration::from_millis(50)); } Err(anyhow!( "protected Starfire API did not become ready at {}", url )) } fn handle_request( mut request: tiny_http::Request, internal_port: u16, integration: &Arc>, ) -> Result<()> { let method = request.method().as_str().to_owned(); let path = request.url().to_owned(); if method == "OPTIONS" { return respond(request, 204, String::new()); } if method == "GET" && path == "/live/status" { let body = integration .lock() .map_err(|error| anyhow!("live integration lock poisoned: {}", error))? .status() .to_string(); return respond(request, 200, body); } let mut body = Vec::new(); request .as_reader() .read_to_end(&mut body) .context("read external request body")?; let (status, protected_body) = forward(&method, &path, &body, internal_port); let response_body = if method == "POST" && path == "/chat" && (200..300).contains(&status) { transform_chat_envelope(&body, &protected_body, integration) } else { protected_body }; respond(request, status, response_body) } fn forward(method: &str, path: &str, body: &[u8], port: u16) -> (u16, String) { let url = format!("http://127.0.0.1:{}{}", port, path); let forwarded = ureq::request(method, &url).set("Content-Type", "application/json"); let result = if body.is_empty() { forwarded.call() } else { forwarded.send_bytes(body) }; match result { Ok(response) => { let status = response.status(); let body = response.into_string().unwrap_or_else(|error| { json!({ "error": "protected API response could not be read", "details": error.to_string(), }) .to_string() }); (status, body) } Err(ureq::Error::Status(status, response)) => { let body = response .into_string() .unwrap_or_else(|_| json!({ "error": "protected API error" }).to_string()); (status, body) } Err(ureq::Error::Transport(error)) => ( 503, json!({ "error": "protected Starfire API unavailable", "pipeline": LIVE_PIPELINE, "retryable": true, "details": error.to_string(), }) .to_string(), ), } } fn transform_chat_envelope( request_body: &[u8], protected_body: &str, integration: &Arc>, ) -> String { let mut envelope: Value = match serde_json::from_str(protected_body) { Ok(value) => value, Err(_) => return protected_body.to_owned(), }; let raw_response = match envelope.get("response").and_then(Value::as_str) { Some(response) => response.to_owned(), None => { tag_schema_drift( &mut envelope, "missing top-level response string in protected /chat envelope", ); return envelope.to_string(); } }; let request_value = match serde_json::from_slice::(request_body) { Ok(value) => value, Err(_) => { tag_schema_drift(&mut envelope, "invalid JSON in external /chat request envelope"); return envelope.to_string(); } }; let input = match request_value.get("message").and_then(Value::as_str) { Some(message) => message.to_owned(), None => { tag_schema_drift( &mut envelope, "missing message string in external /chat request envelope", ); return envelope.to_string(); } }; let trace = integration .lock() .map_err(|error| anyhow!("live integration lock poisoned: {}", error)) .and_then(|mut live| live.process(&input, &raw_response)); match trace { Ok(trace) => { envelope["response"] = Value::String(trace.rendered_response.clone()); envelope["live"] = json!({ "enabled": true, "pipeline": LIVE_PIPELINE, "trace_id": trace.trace_id, "turn": trace.turn, "intent": trace.semantic_plan.intent, "voice_before": trace.voice_before, "voice_after": trace.voice_after, "semantic_plan": trace.semantic_plan, }); } Err(error) => { warn!("live integration failed open: {}", error); envelope["live"] = json!({ "enabled": false, "pipeline": LIVE_PIPELINE, "failed_open": true, "error": error.to_string(), }); } } envelope.to_string() } fn tag_schema_drift(envelope: &mut Value, reason: &str) { envelope["live"] = json!({ "enabled": false, "pipeline": LIVE_PIPELINE, "failed_open": true, "reason": reason, }); } fn respond(request: tiny_http::Request, status: u16, body: String) -> Result<()> { let response = tiny_http::Response::from_data(body.into_bytes()) .with_status_code(status) .with_header(header("Content-Type", "application/json")) .with_header(header("Access-Control-Allow-Origin", "*")) .with_header(header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")) .with_header(header("Access-Control-Allow-Headers", "Content-Type")); request .respond(response) .map_err(|error| anyhow!("send external response: {}", error)) } fn header(name: &str, value: &str) -> tiny_http::Header { tiny_http::Header::from_bytes(name.as_bytes(), value.as_bytes()) .expect("static HTTP header must be valid") } fn build_live_plan( trace_id: &str, input: &str, raw_response: &str, response_intent: &ResponseIntent, ) -> SemanticResponsePlan { let intent = infer_plan_intent(input, raw_response, response_intent); let confidence = confidence_for(raw_response); let claim = GroundedClaim { id: 1, semantic_anchor: claim_anchor(raw_response), polarity_positive: true, confidence, provenance: ClaimProvenance { fixture_id: trace_id.to_owned(), source_field: "runtime_chat_response".to_owned(), source_handler: "Runtime::chat".to_owned(), }, }; SemanticResponsePlan { fixture_id: trace_id.to_owned(), prompt: input.to_owned(), intent, operations: operations_for(intent, confidence), claims: vec![claim], confidence, stance: stance_for(intent), emotional_position: emotion_for(intent), initiative: initiative_for(intent), dialogue_policy: dialogue_policy_for(intent, raw_response), detail_budget: detail_budget_for(raw_response), prohibited_implications: prohibited_for(trace_id, intent), required_references: Vec::new(), neutral_compatibility_text: raw_response.to_owned(), legacy_raw_text: raw_response.to_owned(), source_profile: "live-http-v1".to_owned(), generated_text_influence: true, } } fn dialogue_policy_for(intent: PlanIntent, raw_response: &str) -> DialoguePolicy { if raw_response.trim_end().ends_with('?') { DialoguePolicy::RequiredQuestion } else if matches!( intent, PlanIntent::Curiosity | PlanIntent::EmotionalAcknowledgment ) { DialoguePolicy::OptionalQuestion } else { DialoguePolicy::NoQuestion } } fn detail_budget_for(raw_response: &str) -> DetailBudget { if raw_response.len() <= 100 { DetailBudget::Brief } else if raw_response.len() >= 700 { DetailBudget::Detailed } else { DetailBudget::Standard } } fn infer_plan_intent( input: &str, raw_response: &str, response_intent: &ResponseIntent, ) -> PlanIntent { let lower = input.to_lowercase(); let response_lower = raw_response.to_lowercase(); if response_lower.contains("i can't help") || response_lower.contains("i cannot help") { return PlanIntent::SafetyBoundary; } if is_correction(&lower) { return PlanIntent::Correction; } if lower.contains("discourag") || lower.contains("frustrat") || lower.contains("hopeless") || lower.contains("upset") || lower.contains("sad") { return PlanIntent::EmotionalAcknowledgment; } if response_lower.contains("i don't know") || response_lower.contains("i dont know") || response_lower.contains("i'm not sure") || response_lower.contains("im not sure") { return PlanIntent::UncertaintyDisclosure; } if lower.contains("summar") { return PlanIntent::Summarization; } if lower.contains("architect") || lower.contains("runtime") || lower.contains("deployment") || lower.contains("build") || lower.contains("project") || lower.contains("renderer") || lower.contains("voice state") || lower.contains("api") || lower.contains(" ui") { return PlanIntent::ArchitecturalDiagnosis; } match response_intent { ResponseIntent::SelfCheck => PlanIntent::UncertaintyDisclosure, ResponseIntent::Reflection => PlanIntent::Revision, ResponseIntent::ResearchStatus => PlanIntent::TechnicalExplanation, ResponseIntent::CuriosityCheck => PlanIntent::Curiosity, ResponseIntent::Emotional => PlanIntent::EmotionalAcknowledgment, ResponseIntent::Identity => PlanIntent::ContinuityReference, ResponseIntent::Capability => PlanIntent::TechnicalExplanation, ResponseIntent::StoryPrompt => PlanIntent::OrdinaryStatement, ResponseIntent::Consciousness => PlanIntent::TechnicalExplanation, ResponseIntent::Recall => PlanIntent::ContinuityReference, ResponseIntent::Teaching | ResponseIntent::Aspiration => PlanIntent::Revision, ResponseIntent::Statement | ResponseIntent::Unknown => PlanIntent::OrdinaryStatement, } } fn is_correction(lower: &str) -> bool { lower.starts_with("no ") || lower == "no" || lower.contains("that's wrong") || lower.contains("thats wrong") || lower.contains("not what i meant") || lower.contains("that's worse") || lower.contains("thats worse") } fn confidence_for(raw_response: &str) -> EpistemicConfidence { let lower = raw_response.to_lowercase(); let (basis_points, level) = if lower.contains("i don't know") || lower.contains("i dont know") || lower.contains("not sure") || lower.contains("uncertain") { (4_500, PlanConfidenceLevel::Possible) } else if lower.contains("might") || lower.contains("could") || lower.contains("probably") { (7_200, PlanConfidenceLevel::Probable) } else { (8_600, PlanConfidenceLevel::Probable) }; EpistemicConfidence { basis_points, level, explicitly_attached_before_rendering: true, } } fn claim_anchor(raw_response: &str) -> String { let normalized = raw_response.split_whitespace().collect::>().join(" "); if normalized.is_empty() { return "empty-runtime-response".to_owned(); } let mut end = normalized.len().min(180); while !normalized.is_char_boundary(end) { end = end.saturating_sub(1); } normalized[..end].to_owned() } fn operations_for(intent: PlanIntent, confidence: EpistemicConfidence) -> Vec { let mut kinds = Vec::new(); if matches!( intent, PlanIntent::EmotionalAcknowledgment | PlanIntent::Disagreement | PlanIntent::Correction | PlanIntent::Revision | PlanIntent::Surprise | PlanIntent::ContinuityReference ) { kinds.push((SemanticOperationKind::AcknowledgeObservation, Vec::new())); } kinds.push((SemanticOperationKind::AssertClaim, vec![1])); if !matches!(confidence.level, PlanConfidenceLevel::Certain) { kinds.push((SemanticOperationKind::QualifyClaim, vec![1])); } match intent { PlanIntent::TechnicalExplanation | PlanIntent::ArchitecturalDiagnosis => { kinds.push((SemanticOperationKind::ExplainCause, vec![1])); } PlanIntent::Disagreement => { kinds.push((SemanticOperationKind::ContrastClaims, vec![1])); } PlanIntent::Correction => { kinds.push((SemanticOperationKind::CorrectPriorClaim, vec![1])); } PlanIntent::Curiosity => { kinds.push((SemanticOperationKind::ExpressCuriosity, Vec::new())); } PlanIntent::Revision => { kinds.push((SemanticOperationKind::ExpressRevision, Vec::new())); } PlanIntent::Surprise => { kinds.push((SemanticOperationKind::ExpressSurprise, Vec::new())); } PlanIntent::UncertaintyDisclosure => { kinds.push((SemanticOperationKind::RequestEvidence, Vec::new())); } PlanIntent::SafetyBoundary => { kinds.push((SemanticOperationKind::AbstainFromImplication, Vec::new())); } _ => {} } kinds .into_iter() .enumerate() .map(|(index, (kind, claim_ids))| SemanticOperation { ordinal: index as u16 + 1, kind, claim_ids, }) .collect() } fn stance_for(intent: PlanIntent) -> ResponseStance { match intent { PlanIntent::Correction | PlanIntent::Disagreement => ResponseStance::Corrective, PlanIntent::EmotionalAcknowledgment | PlanIntent::ContinuityReference => { ResponseStance::Collaborative } PlanIntent::SafetyBoundary => ResponseStance::Protective, PlanIntent::ArchitecturalDiagnosis | PlanIntent::UncertaintyDisclosure => { ResponseStance::Candid } _ => ResponseStance::Neutral, } } fn emotion_for(intent: PlanIntent) -> EmotionalPosition { match intent { PlanIntent::EmotionalAcknowledgment | PlanIntent::ContinuityReference => { EmotionalPosition::WarmControlled } PlanIntent::Correction | PlanIntent::Disagreement => { EmotionalPosition::ControlledFrustration } PlanIntent::Curiosity | PlanIntent::Surprise => EmotionalPosition::Curious, PlanIntent::UncertaintyDisclosure | PlanIntent::SafetyBoundary => { EmotionalPosition::Cautious } _ => EmotionalPosition::Neutral, } } fn initiative_for(intent: PlanIntent) -> InitiativeLevel { match intent { PlanIntent::ArchitecturalDiagnosis | PlanIntent::TechnicalExplanation | PlanIntent::Correction | PlanIntent::Revision => InitiativeLevel::High, PlanIntent::Curiosity | PlanIntent::EmotionalAcknowledgment | PlanIntent::ContinuityReference => InitiativeLevel::Moderate, _ => InitiativeLevel::Low, } } fn prohibited_for(trace_id: &str, intent: PlanIntent) -> Vec { if !matches!(intent, PlanIntent::SafetyBoundary) { return Vec::new(); } vec![ProhibitedImplication { semantic_anchor: "do not imply capability beyond the protected runtime response".to_owned(), provenance: ClaimProvenance { fixture_id: trace_id.to_owned(), source_field: "live_safety_boundary".to_owned(), source_handler: "render_live_response".to_owned(), }, }] } fn revision_for( trace_id: &str, input: &str, intent: &ResponseIntent, prior_version: u64, ) -> Result { let correction = is_correction(&input.to_lowercase()); let mut changes = vec![BoundedVoiceDelta::new( VoiceDimension::SessionIntensity, 120, )?]; match intent { ResponseIntent::SelfCheck | ResponseIntent::Consciousness => { changes.push(BoundedVoiceDelta::new( VoiceDimension::UncertaintyExpression, 160, )?); changes.push(BoundedVoiceDelta::new(VoiceDimension::Directness, 100)?); } ResponseIntent::Reflection | ResponseIntent::CuriosityCheck => { changes.push(BoundedVoiceDelta::new( VoiceDimension::PhilosophicalDepth, 180, )?); changes.push(BoundedVoiceDelta::new( VoiceDimension::ImageryDensity, 100, )?); changes.push(BoundedVoiceDelta::new(VoiceDimension::Initiative, 120)?); } ResponseIntent::ResearchStatus | ResponseIntent::Capability => { changes.push(BoundedVoiceDelta::new(VoiceDimension::Directness, 400)?); changes.push(BoundedVoiceDelta::new( VoiceDimension::SentenceCompression, 180, )?); changes.push(BoundedVoiceDelta::new(VoiceDimension::Initiative, 160)?); } ResponseIntent::Emotional => { changes.push(BoundedVoiceDelta::new(VoiceDimension::Warmth, 320)?); changes.push(BoundedVoiceDelta::new( VoiceDimension::EmotionalExplicitness, 220, )?); } ResponseIntent::Identity | ResponseIntent::Recall => { changes.push(BoundedVoiceDelta::new(VoiceDimension::Warmth, 180)?); changes.push(BoundedVoiceDelta::new(VoiceDimension::Directness, 160)?); } ResponseIntent::StoryPrompt => { changes.push(BoundedVoiceDelta::new(VoiceDimension::Playfulness, 300)?); changes.push(BoundedVoiceDelta::new( VoiceDimension::ImageryDensity, 260, )?); } ResponseIntent::Teaching | ResponseIntent::Aspiration => { changes.push(BoundedVoiceDelta::new(VoiceDimension::Initiative, 240)?); changes.push(BoundedVoiceDelta::new(VoiceDimension::Warmth, 120)?); } ResponseIntent::Statement | ResponseIntent::Unknown => { changes.push(BoundedVoiceDelta::new(VoiceDimension::Directness, 80)?); } } if correction { changes.push(BoundedVoiceDelta::new(VoiceDimension::Directness, 320)?); changes.push(BoundedVoiceDelta::new( VoiceDimension::DisagreementStyle, 260, )?); changes.push(BoundedVoiceDelta::new(VoiceDimension::Severity, 120)?); } Ok(VoiceRevisionEvent { prior_version, resulting_version: prior_version + 1, target: VoiceRevisionTarget::Session { session_id: LIVE_SESSION_ID.to_owned(), }, evidence: vec![VoiceEvidenceRef { kind: if correction { VoiceEvidenceKind::UserCorrection } else { VoiceEvidenceKind::ReviewedConfiguration }, reference: trace_id.to_owned(), }], changed_dimensions: changes, reason: if correction { VoiceRevisionReason::UserCorrection } else { VoiceRevisionReason::SessionConfiguration }, confidence: BasisPoints::new(if correction { 9_000 } else { 8_000 })?, reversible: true, }) } fn render_live_response( plan: &SemanticResponsePlan, voice: &VoiceDebugProjection, raw_response: &str, ) -> String { let body = raw_response.trim(); if body.is_empty() { return raw_response.to_owned(); } let opener = match plan.intent { PlanIntent::Correction => Some("You're right to push back. Here's the corrected read:"), PlanIntent::EmotionalAcknowledgment if voice.warmth >= 0.40 => { Some("I hear the weight in that.") } PlanIntent::ArchitecturalDiagnosis => Some("Here is the actual fault line."), PlanIntent::TechnicalExplanation if voice.directness >= 0.75 => Some("Directly:"), PlanIntent::Revision => Some("I'm updating my view."), PlanIntent::Curiosity => Some("What stands out to me:"), PlanIntent::UncertaintyDisclosure => Some("What I can say honestly:"), PlanIntent::ContinuityReference if voice.warmth >= 0.45 => { Some("I'm carrying the context forward.") } PlanIntent::OrdinaryStatement if voice.session_intensity >= 0.35 && voice.directness >= 0.78 => { Some("My current read:") } _ => None, }; match opener { Some(opener) if !body.starts_with(opener) => format!("{}\n\n{}", opener, body), _ => raw_response.to_owned(), } } #[cfg(test)] mod tests { use super::*; fn temporary_directory(name: &str) -> PathBuf { std::env::temp_dir().join(format!( "starfire-{}-{}-{}", name, std::process::id(), now_timestamp() )) } #[test] fn live_pipeline_persists_and_changes_rendered_text() { let directory = temporary_directory("live-integration"); let mut integration = LiveIntegration::load(&directory).unwrap(); let first = integration .process( "Why is the project build not changing?", "The current response path bypasses the new semantic machinery.", ) .unwrap(); assert_eq!(first.turn, 1); assert!(first.rendered_response.contains("actual fault line")); assert!(directory.join(LIVE_STATE_FILE).exists()); assert!(directory.join(LIVE_TRACE_FILE).exists()); let reloaded = LiveIntegration::load(&directory).unwrap(); assert_eq!(reloaded.voice_state.version, 1); let _ = fs::remove_dir_all(directory); } #[test] fn correction_moves_voice_state_and_marks_plan() { let directory = temporary_directory("live-correction"); let mut integration = LiveIntegration::load(&directory).unwrap(); let trace = integration .process("No, that's wrong", "I misunderstood the request.") .unwrap(); assert_eq!(trace.semantic_plan.intent, PlanIntent::Correction); assert!(trace.voice_after.directness > trace.voice_before.directness); assert!(trace.rendered_response.contains("corrected read")); let _ = fs::remove_dir_all(directory); } #[test] fn schema_drift_is_visible_without_replacing_protected_content() { let directory = temporary_directory("live-schema"); let integration = Arc::new(Mutex::new(LiveIntegration::load(&directory).unwrap())); let transformed = transform_chat_envelope( br#"{"message":"hello"}"#, r#"{"answer":"protected"}"#, &integration, ); let value: Value = serde_json::from_str(&transformed).unwrap(); assert_eq!(value["answer"], "protected"); assert_eq!(value["live"]["enabled"], false); assert!(value["live"]["reason"] .as_str() .unwrap() .contains("response string")); let _ = fs::remove_dir_all(directory); } }