use std::path::Path;
use crate::error::Result;
use crate::session::event::ShoreEvent;
use crate::session::state::{ProjectionDiagnostic, SessionState};
#[cfg(test)]
use crate::session::store::backend::StoreBackend;
use crate::session::store::resolution::{resolve_read_store, resolve_write_store};
use crate::session::{EventStore, SkippedEvent, sweep_stale_temp_files};
use crate::storage::{Durability, LocalStorage};
#[cfg(test)]
fn load_or_rebuild_session_state(repo: impl AsRef<Path>) -> Result<Option<SessionState>> {
let Some((_backend, events)) = list_events_if_store_exists(repo)? else {
return Ok(None);
};
Ok(Some(SessionState::from_events(&events)?))
}
pub fn rebuild_state(repo: impl AsRef<Path>) -> Result<SessionState> {
let write_store = resolve_write_store(repo.as_ref())?;
let store_dir = write_store.store_dir();
let worktree_root = write_store.worktree_root();
let storage = LocalStorage::new(store_dir);
sweep_stale_temp_files(&storage, store_dir)?;
let span = tracing::info_span!("session.rebuild_state", repo = %worktree_root.display());
let _entered = span.enter();
let state =
SessionState::from_events(&EventStore::from_backend(write_store.backend()).list_events()?)?;
storage.write_json_atomic(
&store_dir.join("state.json"),
&state,
Durability::Projection,
)?;
Ok(state)
}
pub fn read_events(repo: impl AsRef<Path>) -> Result<Vec<ShoreEvent>> {
let read_store = resolve_read_store(repo.as_ref())?;
EventStore::from_backend(read_store.backend()).list_events()
}
pub(crate) fn skipped_to_diagnostics(skipped: Vec<SkippedEvent>) -> Vec<ProjectionDiagnostic> {
skipped
.into_iter()
.map(|s| ProjectionDiagnostic {
code: s.code.to_owned(),
message: s.record.to_string(),
})
.collect()
}
pub fn read_events_for_display(
repo: impl AsRef<Path>,
) -> Result<(Vec<ShoreEvent>, Vec<ProjectionDiagnostic>)> {
let read_store = resolve_read_store(repo.as_ref())?;
let (events, skipped) = EventStore::from_backend(read_store.backend()).list_events_lenient()?;
Ok((events, skipped_to_diagnostics(skipped)))
}
#[cfg(test)]
fn list_events_if_store_exists(
repo: impl AsRef<Path>,
) -> Result<Option<(StoreBackend, Vec<ShoreEvent>)>> {
let read_store = resolve_read_store(repo.as_ref())?;
if !read_store.store_dir().exists() {
return Ok(None);
}
let events = EventStore::from_backend(read_store.backend()).list_events()?;
Ok(Some((read_store.backend().clone(), events)))
}
#[cfg(test)]
mod tests {
use std::process::Command;
use super::*;
use crate::error::SchemaBreakRecord;
use crate::model::JournalId;
use crate::session::event::{EventTarget, EventType, ReviewInitializedPayload, Writer};
#[test]
fn skipped_to_diagnostics_maps_code_and_message() {
let skipped = vec![SkippedEvent {
code: "unsupported_event_type",
record: SchemaBreakRecord {
retired: "review_disposition_recorded".to_owned(),
broken_at: "0.1".to_owned(),
anchor: "docs/assessment-model.md#legacy-disposition-events".to_owned(),
},
}];
let diags = skipped_to_diagnostics(skipped);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, "unsupported_event_type");
assert!(diags[0].message.contains("review_disposition_recorded"));
assert!(diags[0].message.contains("#legacy-disposition-events"));
}
#[test]
fn load_or_rebuild_session_state_returns_none_when_shore_dir_absent() {
let repo = tempfile::tempdir().expect("create repo");
Command::new("git")
.args(["init"])
.current_dir(repo.path())
.output()
.unwrap();
let state = load_or_rebuild_session_state(repo.path()).unwrap();
assert!(state.is_none());
}
#[test]
fn load_or_rebuild_session_state_rebuilds_from_events_when_store_present() {
let repo = test_repo_with(vec![review_initialized()]);
let state = load_or_rebuild_session_state(repo.path()).unwrap();
let state = state.expect("state should be present when the resolved store exists");
assert_eq!(state.event_count, 1);
}
fn test_repo_with(events: Vec<ShoreEvent>) -> tempfile::TempDir {
let repo = tempfile::tempdir().expect("create repo");
Command::new("git")
.args(["init"])
.current_dir(repo.path())
.output()
.unwrap();
let store_dir = resolve_read_store(repo.path())
.unwrap()
.store_dir()
.to_path_buf();
std::fs::create_dir_all(store_dir.join("events")).unwrap();
let store = EventStore::open(&store_dir);
for event in events {
store.record_event_once(&event).unwrap();
}
repo
}
fn review_initialized() -> ShoreEvent {
ShoreEvent::new(
EventType::ReviewInitialized,
"review_initialized:session:default:work:default",
EventTarget::for_journal(JournalId::new("journal:default")),
Writer::shore_local("0.1.0"),
ReviewInitializedPayload {},
"2026-05-10T00:00:00Z",
)
.unwrap()
}
}