use std::collections::BTreeSet;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::LibrarianError;
pub const QUORUM_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuorumStore {
root: PathBuf,
}
impl QuorumStore {
#[must_use]
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
}
}
pub fn create_episode(&self, episode: &QuorumEpisode) -> Result<PathBuf, LibrarianError> {
let path = self.episode_path(&episode.id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let bytes = serde_json::to_vec_pretty(episode)?;
fs::write(&path, bytes)?;
Ok(path)
}
pub fn load_episode(&self, id: &str) -> Result<QuorumEpisode, LibrarianError> {
let bytes = fs::read(self.episode_path(id))?;
Ok(serde_json::from_slice(&bytes)?)
}
pub fn save_result(&self, result: &QuorumResult) -> Result<PathBuf, LibrarianError> {
let path = self.result_path(&result.episode_id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let bytes = serde_json::to_vec_pretty(result)?;
fs::write(&path, bytes)?;
Ok(path)
}
pub fn load_result(&self, episode_id: &str) -> Result<QuorumResult, LibrarianError> {
let bytes = fs::read(self.result_path(episode_id))?;
Ok(serde_json::from_slice(&bytes)?)
}
#[must_use]
pub fn result_artifact_path(&self, episode_id: &str) -> PathBuf {
self.result_path(episode_id)
}
pub fn append_participant_output(
&self,
output: &QuorumParticipantOutput,
) -> Result<PathBuf, LibrarianError> {
let episode = self.load_episode(&output.episode_id)?;
self.validate_participant_output(&episode, output)?;
let path = self.output_path(output);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let bytes = serde_json::to_vec_pretty(output)?;
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut file) => file.write_all(&bytes)?,
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
return Err(LibrarianError::QuorumOutputAlreadyExists {
episode_id: output.episode_id.clone(),
round: output.round.as_str().to_string(),
participant_id: output.participant_id.clone(),
});
}
Err(err) => return Err(err.into()),
}
Ok(path)
}
pub fn load_round_outputs(
&self,
episode_id: &str,
round: QuorumRound,
) -> Result<Vec<QuorumParticipantOutput>, LibrarianError> {
let dir = self.round_dir(episode_id, round);
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
let mut outputs = Vec::new();
for entry in entries {
let path = entry?.path();
if path.extension().is_some_and(|ext| ext == "json") {
let bytes = fs::read(path)?;
let output: QuorumParticipantOutput = serde_json::from_slice(&bytes)?;
if output.round == round {
outputs.push(output);
}
}
}
outputs.sort_by(|left, right| {
left.participant_id
.cmp(&right.participant_id)
.then(left.output_id.cmp(&right.output_id))
});
Ok(outputs)
}
pub fn visible_outputs_for_round(
&self,
episode_id: &str,
round: QuorumRound,
) -> Result<Vec<QuorumParticipantOutput>, LibrarianError> {
let episode = self.load_episode(episode_id)?;
match round {
QuorumRound::Independent => Ok(Vec::new()),
QuorumRound::Critique => {
self.require_round_complete(&episode, QuorumRound::Independent)
}
QuorumRound::Revision => {
let mut outputs =
self.require_round_complete(&episode, QuorumRound::Independent)?;
outputs.extend(self.require_round_complete(&episode, QuorumRound::Critique)?);
Ok(outputs)
}
}
}
pub fn build_adapter_request(
&self,
episode_id: &str,
participant_id: &str,
round: QuorumRound,
) -> Result<QuorumAdapterRequest, LibrarianError> {
let episode = self.load_episode(episode_id)?;
let participant = episode
.participants
.iter()
.find(|candidate| candidate.id == participant_id)
.cloned()
.ok_or_else(|| {
quorum_protocol_violation(
&episode.id,
format!("unknown participant {participant_id}"),
)
})?;
let visible_prior_outputs = self.visible_outputs_for_round(episode_id, round)?;
let visible_prior_output_ids = visible_prior_outputs
.iter()
.map(|output| output.output_id.clone())
.collect();
Ok(QuorumAdapterRequest {
schema_version: QUORUM_SCHEMA_VERSION,
episode_id: episode.id,
participant,
round,
question: episode.question,
target_project: episode.target_project,
target_scope: episode.target_scope,
evidence_policy: episode.evidence_policy,
visible_prior_output_ids,
visible_prior_outputs,
})
}
fn episode_path(&self, id: &str) -> PathBuf {
self.episode_dir(id).join("episode.json")
}
fn result_path(&self, episode_id: &str) -> PathBuf {
self.episode_dir(episode_id).join("result.json")
}
fn episode_dir(&self, id: &str) -> PathBuf {
self.root.join("episodes").join(quorum_id_slug(id))
}
fn round_dir(&self, episode_id: &str, round: QuorumRound) -> PathBuf {
self.episode_dir(episode_id)
.join("outputs")
.join(round.as_str())
}
fn output_path(&self, output: &QuorumParticipantOutput) -> PathBuf {
self.round_dir(&output.episode_id, output.round)
.join(format!("{}.json", quorum_id_slug(&output.participant_id)))
}
fn validate_participant_output(
&self,
episode: &QuorumEpisode,
output: &QuorumParticipantOutput,
) -> Result<(), LibrarianError> {
if output.schema_version != QUORUM_SCHEMA_VERSION {
return Err(quorum_protocol_violation(
&episode.id,
format!(
"unsupported output schema version {}; expected {QUORUM_SCHEMA_VERSION}",
output.schema_version
),
));
}
if output.output_id.trim().is_empty() {
return Err(quorum_protocol_violation(
&episode.id,
"participant output id must not be empty",
));
}
if !episode
.participants
.iter()
.any(|participant| participant.id == output.participant_id)
{
return Err(quorum_protocol_violation(
&episode.id,
format!("unknown participant {}", output.participant_id),
));
}
match output.round {
QuorumRound::Independent => {
if !output.visible_prior_output_ids.is_empty() {
return Err(quorum_protocol_violation(
&episode.id,
"independent outputs must not reference prior visible outputs",
));
}
}
QuorumRound::Critique => {
let visible = self.require_round_complete(episode, QuorumRound::Independent)?;
require_exact_visible_prior_ids(output, &visible)?;
}
QuorumRound::Revision => {
let mut visible = self.require_round_complete(episode, QuorumRound::Independent)?;
visible.extend(self.require_round_complete(episode, QuorumRound::Critique)?);
require_exact_visible_prior_ids(output, &visible)?;
}
}
Ok(())
}
fn require_round_complete(
&self,
episode: &QuorumEpisode,
round: QuorumRound,
) -> Result<Vec<QuorumParticipantOutput>, LibrarianError> {
let outputs = self.load_round_outputs(&episode.id, round)?;
let expected: BTreeSet<&str> = episode
.participants
.iter()
.map(|participant| participant.id.as_str())
.collect();
let actual: BTreeSet<&str> = outputs
.iter()
.map(|output| output.participant_id.as_str())
.collect();
let missing: Vec<&str> = expected.difference(&actual).copied().collect();
if !missing.is_empty() {
return Err(quorum_protocol_violation(
&episode.id,
format!(
"{} round is incomplete; missing participant outputs: {}",
round.as_str(),
missing.join(", ")
),
));
}
Ok(outputs)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuorumEpisodeState {
Requested,
Enlisted,
IndependentRound,
CritiqueRound,
RevisionRound,
VoteRound,
Synthesized,
SubmittedToLibrarian,
Archived,
Quarantined,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuorumParticipant {
pub id: String,
pub adapter: String,
pub model: Option<String>,
pub persona: String,
pub prompt_template_version: String,
pub runtime_surface: String,
pub tool_permissions: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuorumRound {
Independent,
Critique,
Revision,
}
impl QuorumRound {
fn as_str(self) -> &'static str {
match self {
Self::Independent => "independent",
Self::Critique => "critique",
Self::Revision => "revision",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuorumEpisode {
pub schema_version: u32,
pub id: String,
pub requested_at_unix_ms: u64,
pub requester: String,
pub question: String,
pub target_project: Option<String>,
pub target_scope: Option<String>,
pub evidence_policy: String,
pub state: QuorumEpisodeState,
pub participants: Vec<QuorumParticipant>,
pub provenance_uri: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuorumParticipantOutput {
pub schema_version: u32,
pub episode_id: String,
pub output_id: String,
pub participant_id: String,
pub round: QuorumRound,
pub submitted_at_unix_ms: u64,
pub prompt: String,
pub response: String,
pub visible_prior_output_ids: Vec<String>,
pub evidence_used: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuorumAdapterRequest {
pub schema_version: u32,
pub episode_id: String,
pub participant: QuorumParticipant,
pub round: QuorumRound,
pub question: String,
pub target_project: Option<String>,
pub target_scope: Option<String>,
pub evidence_policy: String,
pub visible_prior_output_ids: Vec<String>,
pub visible_prior_outputs: Vec<QuorumParticipantOutput>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionStatus {
Recommend,
Split,
NeedsEvidence,
Reject,
Unsafe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsensusLevel {
Unanimous,
StrongMajority,
WeakMajority,
Contested,
Abstained,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VoteChoice {
Agree,
Disagree,
Abstain,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParticipantVote {
pub participant_id: String,
pub vote: VoteChoice,
pub confidence: f32,
pub rationale: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuorumResult {
pub schema_version: u32,
pub episode_id: String,
pub question: String,
pub recommendation: String,
pub decision_status: DecisionStatus,
pub consensus_level: ConsensusLevel,
pub confidence: f32,
pub supporting_points: Vec<String>,
pub dissenting_points: Vec<String>,
pub unresolved_questions: Vec<String>,
pub evidence_used: Vec<String>,
pub participant_votes: Vec<ParticipantVote>,
pub proposed_memory_drafts: Vec<String>,
}
fn quorum_id_slug(id: &str) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let digest = Sha256::digest(id.as_bytes());
let mut suffix = String::with_capacity(16);
for byte in digest.iter().take(8) {
suffix.push(char::from(HEX[usize::from(byte >> 4)]));
suffix.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
let prefix: String = id
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
ch
} else {
'_'
}
})
.take(48)
.collect();
if prefix.is_empty() {
suffix
} else {
format!("{prefix}-{suffix}")
}
}
fn require_exact_visible_prior_ids(
output: &QuorumParticipantOutput,
visible: &[QuorumParticipantOutput],
) -> Result<(), LibrarianError> {
let expected: BTreeSet<&str> = visible.iter().map(|item| item.output_id.as_str()).collect();
let actual: BTreeSet<&str> = output
.visible_prior_output_ids
.iter()
.map(String::as_str)
.collect();
if expected != actual || actual.len() != output.visible_prior_output_ids.len() {
return Err(quorum_protocol_violation(
&output.episode_id,
format!(
"{} output from {} must reference exactly the visible prior output ids",
output.round.as_str(),
output.participant_id
),
));
}
Ok(())
}
fn quorum_protocol_violation(episode_id: &str, message: impl Into<String>) -> LibrarianError {
LibrarianError::QuorumProtocolViolation {
episode_id: episode_id.to_string(),
message: message.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn participant(name: &str, persona: &str) -> QuorumParticipant {
QuorumParticipant {
id: name.to_string(),
adapter: name.to_string(),
model: Some(format!("{name}-model")),
persona: persona.to_string(),
prompt_template_version: "v1".to_string(),
runtime_surface: name.to_string(),
tool_permissions: vec!["read_memory".to_string()],
}
}
fn episode() -> QuorumEpisode {
QuorumEpisode {
schema_version: QUORUM_SCHEMA_VERSION,
id: "qr-2026-04-24-001".to_string(),
requested_at_unix_ms: 1_772_000_000_000,
requester: "operator:AlainDor".to_string(),
question: "Should Mimir keep remote sync explicit?".to_string(),
target_project: Some("buildepicshit/Mimir".to_string()),
target_scope: Some("project".to_string()),
evidence_policy: "source_backed_when_claiming_external_facts".to_string(),
state: QuorumEpisodeState::Requested,
participants: vec![
participant("claude", "architect"),
participant("codex", "implementation_engineer"),
],
provenance_uri: "quorum://episode/qr-2026-04-24-001".to_string(),
}
}
fn output(
output_id: &str,
participant_id: &str,
round: QuorumRound,
visible_prior_output_ids: Vec<String>,
) -> QuorumParticipantOutput {
QuorumParticipantOutput {
schema_version: QUORUM_SCHEMA_VERSION,
episode_id: "qr-2026-04-24-001".to_string(),
output_id: output_id.to_string(),
participant_id: participant_id.to_string(),
round,
submitted_at_unix_ms: 1_772_000_001_000,
prompt: format!("Prompt for {participant_id}"),
response: format!("Response from {participant_id}"),
visible_prior_output_ids,
evidence_used: vec!["docs/concepts/consensus-quorum.md".to_string()],
}
}
#[test]
fn quorum_store_creates_and_loads_episode() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let store = QuorumStore::new(tmp.path());
let episode = episode();
let path = store.create_episode(&episode)?;
assert!(path.ends_with("episode.json"));
let loaded = store.load_episode(&episode.id)?;
assert_eq!(loaded, episode);
Ok(())
}
#[test]
fn quorum_store_saves_result_with_dissent_and_votes() -> Result<(), Box<dyn std::error::Error>>
{
let tmp = tempfile::tempdir()?;
let store = QuorumStore::new(tmp.path());
let result = QuorumResult {
schema_version: QUORUM_SCHEMA_VERSION,
episode_id: "qr-2026-04-24-001".to_string(),
question: "Should Mimir keep remote sync explicit?".to_string(),
recommendation: "Keep sync explicit and expose refresh status.".to_string(),
decision_status: DecisionStatus::Recommend,
consensus_level: ConsensusLevel::StrongMajority,
confidence: 0.82,
supporting_points: vec!["Launch/capture stay transparent.".to_string()],
dissenting_points: vec!["Operator may forget to push.".to_string()],
unresolved_questions: vec!["Service adapter protocol remains open.".to_string()],
evidence_used: vec![
".planning/planning/2026-04-24-transparent-agent-harness.md".to_string()
],
participant_votes: vec![
ParticipantVote {
participant_id: "claude".to_string(),
vote: VoteChoice::Agree,
confidence: 0.86,
rationale: "Explicit sync protects native launch flow.".to_string(),
},
ParticipantVote {
participant_id: "codex".to_string(),
vote: VoteChoice::Disagree,
confidence: 0.42,
rationale: "A reminder surface may still be needed.".to_string(),
},
],
proposed_memory_drafts: vec![
"Remote sync must remain explicit during launch and capture.".to_string(),
],
};
store.save_result(&result)?;
let loaded = store.load_result(&result.episode_id)?;
assert_eq!(loaded, result);
assert_eq!(loaded.dissenting_points.len(), 1);
assert_eq!(loaded.participant_votes[1].vote, VoteChoice::Disagree);
Ok(())
}
#[test]
fn quorum_store_blocks_critique_until_independent_outputs_complete(
) -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let store = QuorumStore::new(tmp.path());
let episode = episode();
store.create_episode(&episode)?;
store.append_participant_output(&output(
"out-independent-claude",
"claude",
QuorumRound::Independent,
Vec::new(),
))?;
let critique_before_complete = output(
"out-critique-claude",
"claude",
QuorumRound::Critique,
vec!["out-independent-claude".to_string()],
);
let err = match store.append_participant_output(&critique_before_complete) {
Ok(path) => {
return Err(std::io::Error::other(format!(
"critique must wait for every independent first pass, wrote {}",
path.display()
))
.into());
}
Err(err) => err,
};
assert!(matches!(
err,
LibrarianError::QuorumProtocolViolation { .. }
));
assert!(
store
.visible_outputs_for_round(&episode.id, QuorumRound::Critique)
.is_err(),
"critique visibility must stay closed until every independent output is present",
);
store.append_participant_output(&output(
"out-independent-codex",
"codex",
QuorumRound::Independent,
Vec::new(),
))?;
let visible = store.visible_outputs_for_round(&episode.id, QuorumRound::Critique)?;
let visible_ids: Vec<_> = visible.iter().map(|item| item.output_id.clone()).collect();
assert_eq!(
visible_ids,
vec!["out-independent-claude", "out-independent-codex"]
);
store.append_participant_output(&output(
"out-critique-claude",
"claude",
QuorumRound::Critique,
visible_ids,
))?;
let critiques = store.load_round_outputs(&episode.id, QuorumRound::Critique)?;
assert_eq!(critiques.len(), 1);
assert_eq!(critiques[0].participant_id, "claude");
Ok(())
}
#[test]
fn quorum_store_rejects_independent_output_with_prior_visibility(
) -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let store = QuorumStore::new(tmp.path());
store.create_episode(&episode())?;
let output = output(
"out-independent-claude",
"claude",
QuorumRound::Independent,
vec!["out-independent-codex".to_string()],
);
let err = match store.append_participant_output(&output) {
Ok(path) => {
return Err(std::io::Error::other(format!(
"independent output cannot see prior answers, wrote {}",
path.display()
))
.into());
}
Err(err) => err,
};
assert!(matches!(
err,
LibrarianError::QuorumProtocolViolation { .. }
));
Ok(())
}
#[test]
fn quorum_store_builds_adapter_request_with_visible_outputs(
) -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let store = QuorumStore::new(tmp.path());
let episode = episode();
store.create_episode(&episode)?;
store.append_participant_output(&output(
"out-independent-claude",
"claude",
QuorumRound::Independent,
Vec::new(),
))?;
store.append_participant_output(&output(
"out-independent-codex",
"codex",
QuorumRound::Independent,
Vec::new(),
))?;
let request = store.build_adapter_request(&episode.id, "codex", QuorumRound::Critique)?;
assert_eq!(request.episode_id, episode.id);
assert_eq!(request.participant.id, "codex");
assert_eq!(request.round, QuorumRound::Critique);
assert_eq!(
request.visible_prior_output_ids,
vec!["out-independent-claude", "out-independent-codex"]
);
assert_eq!(request.visible_prior_outputs.len(), 2);
Ok(())
}
}