use crate::boundary::{BoundaryRow, Provenance as BoundaryProvenance};
use crate::dispatch::funnel::LandingVerdict;
use crate::dispatch::{
CommitOutcome, Identity, Provenance, VerifyOutcome, dispatch_identity, dispatch_ref, funnel,
funnel_message, land_boundary_row, land_conclude, verify_phase,
};
use crate::funnel_machine::{self, PhaseRow, Position, Transition, TransitionFacts};
use crate::git::{self, WorktreeRecord};
use crate::ledger::Boundaries;
use crate::worktree::{
Apply, ForkExpect, GcOutcome, GcRefusal, classify_import, reap_fork, require_binding,
resolve_agent,
};
use anyhow::Context;
use serde::Serialize;
use std::io::Write as _;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CoordRefusal {
UnknownSlice,
Ambiguous,
Stale,
}
impl CoordRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
CoordRefusal::UnknownSlice => "unknown-slice",
CoordRefusal::Ambiguous => "ambiguous",
CoordRefusal::Stale => "stale",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CoordFacts {
pub(crate) branch_hits: usize,
pub(crate) path: Option<PathBuf>,
pub(crate) prunable: bool,
pub(crate) dir_exists: bool,
pub(crate) tip: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CoordTarget {
pub(crate) root: PathBuf,
pub(crate) tip: String,
}
pub(crate) fn classify_coord(facts: CoordFacts) -> Result<CoordTarget, CoordRefusal> {
if facts.branch_hits == 0 {
return Err(CoordRefusal::UnknownSlice);
}
if facts.branch_hits > 1 {
return Err(CoordRefusal::Ambiguous);
}
let Some(root) = facts.path else {
return Err(CoordRefusal::Stale);
};
if facts.prunable || !facts.dir_exists {
return Err(CoordRefusal::Stale);
}
let Some(tip) = facts.tip else {
return Err(CoordRefusal::Stale);
};
Ok(CoordTarget { root, tip })
}
pub(crate) fn resolve_coord(root: &Path, slice: u32) -> Result<CoordTarget, CoordRefusal> {
let branch_ref = format!("refs/heads/dispatch/{slice:03}");
let records = git::list_worktrees(root).unwrap_or_default();
let hits: Vec<&WorktreeRecord> = records
.iter()
.filter(|r| r.branch.as_deref() == Some(branch_ref.as_str()))
.collect();
let branch_hits = hits.len();
let single = hits.first().copied();
let path = single.map(|r| r.path.clone());
let prunable = single.is_some_and(|r| r.prunable);
let dir_exists = single.is_some_and(|r| r.path.exists());
let tip = git::git_opt(
root,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("{branch_ref}^{{commit}}"),
],
)
.ok()
.flatten();
classify_coord(CoordFacts {
branch_hits,
path,
prunable,
dir_exists,
tip,
})
}
pub(crate) use crate::dispatch::{
TOOL_DISPATCH_CONCLUDE_PHASE, TOOL_DISPATCH_IMPORT, TOOL_DISPATCH_REAP,
};
pub(crate) const TOOL_DISPATCH_VERIFY: &str = "dispatch_verify";
const MERGE_CONFLICT: &str = "merge-conflict";
const GC_INCOMPLETE: &str = "gc-incomplete";
const CLAIM_BUSY: &str = "claim-busy";
const AMBIGUOUS_FORK_ROW: &str = "ambiguous-fork-row";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) enum FunnelOutcome {
Imported { coord_tip: String },
Concluded { coord_tip: String },
Reaped { fork: String },
ReapedRowPending { fork: String, detail: String },
Verified { coord_tip: String, suite: String },
VerifyFailed { suite: String, detail: String },
Refused { reason: String, detail: String },
}
fn funnel_refused(reason: &str, detail: String) -> FunnelOutcome {
FunnelOutcome::Refused {
reason: reason.to_owned(),
detail,
}
}
fn commit_author(root: &Path, rev: &str) -> anyhow::Result<Identity> {
let raw = git::git_text(root, &["log", "-1", "--format=%an%n%ae", rev])
.with_context(|| format!("read author of {rev}"))?;
let mut lines = raw.lines();
Ok(Identity {
name: lines.next().unwrap_or_default().to_owned(),
email: lines.next().unwrap_or_default().to_owned(),
})
}
pub(crate) fn dispatch_import(
root: &Path,
slice: u32,
name: &str,
) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
let fork = match resolve_agent(&coord.root, fork_agent(name), ForkExpect::Advanced)
.and_then(|record| require_binding(&record).map(|binding| (record, binding)))
{
Ok((record, binding)) if binding.slice == slice => (record, binding),
Ok((_, binding)) => {
return Ok(funnel_refused(
crate::worktree::ResolveRefusal::UnprovableFork.token(),
format!(
"{name} is bound to SL-{:03}, not the SL-{slice:03} funnel",
binding.slice
),
));
}
Err(refusal) => return Ok(funnel_refused(refusal.token(), name.to_owned())),
};
let (record, binding) = fork;
let fork_tip = git::git_text(&coord.root, &["rev-parse", &format!("{name}^{{commit}}")])
.with_context(|| format!("resolve fork tip {name}"))?;
let funnel_record = funnel::read_funnel_at(&coord.root, &coord.tip, slice)?;
let stored = funnel_record.row(&binding.phase).cloned();
let ts = import_ladder(
stored.as_ref().map(|r| r.position),
&record.branch,
&record.base,
&fork_tip,
&coord.tip,
);
let at = crate::clock::now_timestamp()?;
let fold = match funnel_machine::fold_transitions(
stored.as_ref(),
&binding.phase,
&ts,
&coord.tip,
None,
&at,
) {
Ok(Some(fold)) => fold,
Ok(None) => return Ok(funnel_refused(MISSING_LADDER, name.to_owned())),
Err(illegal) => return Ok(funnel_refused(illegal.reason, illegal.to_string())),
};
if fold.replay {
return Ok(FunnelOutcome::Imported {
coord_tip: coord.tip,
});
}
let selectors = crate::slice::selectors(
&coord.root,
slice,
Some(crate::slice::SelectorIntent::DesignTarget),
)?;
let plan = match import_plan(&coord, slice, name, &selectors)? {
Composed::Refused(outcome) => return Ok(outcome),
Composed::Ready(plan) => plan,
};
match funnel::land_funnel_transitions(
&coord.root,
&dispatch_ref(slice),
&coord.tip,
slice,
&binding.phase,
&ts,
None,
Some(&plan.tree),
&import_message(slice, name),
&at,
&plan.prov,
)? {
funnel::FunnelLanding::Landed { oid, .. } => Ok(FunnelOutcome::Imported { coord_tip: oid }),
funnel::FunnelLanding::Replayed { .. } => Ok(FunnelOutcome::Imported {
coord_tip: coord.tip.clone(),
}),
funnel::FunnelLanding::Illegal(illegal) => {
Ok(funnel_refused(illegal.reason, illegal.to_string()))
}
funnel::FunnelLanding::CommitRefused(refusal) => {
Ok(funnel_refused(refusal.token(), String::new()))
}
}
}
const MISSING_LADDER: &str = "missing-ladder";
fn fork_agent(name: &str) -> &str {
name.rsplit('/').next().unwrap_or(name)
}
fn import_message(slice: u32, name: &str) -> String {
funnel_message(slice, &format!("import {name}"))
}
fn import_ladder(
position: Option<Position>,
fork: &str,
base_oid: &str,
fork_tip: &str,
onto: &str,
) -> Vec<Transition> {
let worker_commit = Transition::RecordWorkerCommit {
fork_tip: fork_tip.to_owned(),
};
let import = Transition::Import {
fork_tip: fork_tip.to_owned(),
onto: onto.to_owned(),
};
match position {
None => vec![
Transition::Spawn {
fork: fork.to_owned(),
base_oid: base_oid.to_owned(),
},
worker_commit,
import,
],
Some(Position::Spawned) => vec![worker_commit, import],
Some(_) => vec![import],
}
}
struct ImportPlan {
tree: String,
prov: Provenance,
}
enum Composed {
Ready(ImportPlan),
Refused(FunnelOutcome),
}
fn import_plan(
coord: &CoordTarget,
slice: u32,
name: &str,
selectors: &[String],
) -> anyhow::Result<Composed> {
let fork_tip = git::git_text(&coord.root, &["rev-parse", &format!("{name}^{{commit}}")])
.with_context(|| format!("resolve fork tip {name}"))?;
let base = git::git_text(&coord.root, &["merge-base", &coord.tip, &fork_tip])
.with_context(|| format!("merge-base {} {fork_tip}", coord.tip))?;
let fork_parent = git::git_opt(
&coord.root,
&["rev-parse", "--verify", &format!("{name}^^{{commit}}")],
)?;
let single_commit = fork_parent.as_deref() == Some(base.as_str());
let diff = git::git_text(
&coord.root,
&[
"-c",
"core.quotePath=false",
"diff",
"--name-only",
"--no-renames",
&format!("{base}..{fork_tip}"),
],
)?;
let delta_paths: Vec<String> = diff.lines().map(str::to_owned).collect();
match classify_import(true, true, single_commit, &delta_paths, selectors) {
Ok(Apply::Ok) => {}
Err(refusal) => {
return Ok(Composed::Refused(funnel_refused(
refusal.token(),
refusal.scope_detail(slice, selectors, &delta_paths),
)));
}
}
let tree = if coord.tip == base {
git::git_text(&coord.root, &["rev-parse", &format!("{fork_tip}^{{tree}}")])
.with_context(|| format!("resolve fork tree {fork_tip}"))?
} else {
match git::merge_tree(&coord.root, &base, &coord.tip, &fork_tip)? {
git::MergeTree::Clean { tree } => tree,
git::MergeTree::Conflict { .. } => {
return Ok(Composed::Refused(funnel_refused(
MERGE_CONFLICT,
String::new(),
)));
}
}
};
Ok(Composed::Ready(ImportPlan {
tree,
prov: Provenance::Import {
author: commit_author(&coord.root, &fork_tip)?,
committer: dispatch_identity(),
},
}))
}
#[cfg(test)]
fn import_compose(
coord: &CoordTarget,
slice: u32,
name: &str,
selectors: &[String],
) -> anyhow::Result<FunnelOutcome> {
let plan = match import_plan(coord, slice, name, selectors)? {
Composed::Refused(outcome) => return Ok(outcome),
Composed::Ready(plan) => plan,
};
match crate::dispatch::commit_on_behalf(
&coord.root,
&dispatch_ref(slice),
&coord.tip,
&plan.tree,
&import_message(slice, name),
&plan.prov,
)? {
CommitOutcome::Landed { oid } => Ok(FunnelOutcome::Imported { coord_tip: oid }),
CommitOutcome::Refused(refusal) => Ok(funnel_refused(refusal.token(), String::new())),
}
}
pub(crate) fn dispatch_conclude_phase(
root: &Path,
slice: u32,
phase: &str,
code_start: &str,
code_end: &str,
note: Option<&str>,
) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
let now = crate::clock::now_timestamp()?;
let funnel_record = funnel::read_funnel_at(&coord.root, &coord.tip, slice)?;
let landed = match funnel_record.row(phase) {
Some(row) => conclude_funnel_commit(&coord, slice, phase, code_start, code_end, row, &now)?,
None => {
conclude_boundary_commit(&coord.root, &coord.tip, slice, phase, code_start, code_end)?
}
};
if matches!(landed, FunnelOutcome::Concluded { .. }) {
crate::state::set_phase_status(
&coord.root,
slice,
phase,
crate::state::PhaseStatus::Completed,
note,
&now,
)?;
}
Ok(landed)
}
fn conclude_funnel_commit(
coord: &CoordTarget,
slice: u32,
phase: &str,
code_start: &str,
code_end: &str,
row: &PhaseRow,
now: &str,
) -> anyhow::Result<FunnelOutcome> {
match land_conclude(
&coord.root,
&coord.tip,
slice,
row,
boundary_row(phase, code_start, code_end),
now,
)? {
funnel::FunnelLanding::Landed { oid, .. } => {
Ok(FunnelOutcome::Concluded { coord_tip: oid })
}
funnel::FunnelLanding::Replayed { .. } => Ok(FunnelOutcome::Concluded {
coord_tip: coord.tip.clone(),
}),
funnel::FunnelLanding::Illegal(illegal) => {
Ok(funnel_refused(illegal.reason, illegal.to_string()))
}
funnel::FunnelLanding::CommitRefused(refusal) => {
Ok(funnel_refused(refusal.token(), String::new()))
}
}
}
fn boundary_row(phase: &str, code_start: &str, code_end: &str) -> BoundaryRow {
BoundaryRow {
phase: phase.to_owned(),
code_start_oid: code_start.to_owned(),
code_end_oid: code_end.to_owned(),
provenance: BoundaryProvenance::Funnel,
}
}
fn conclude_boundary_commit(
coord_root: &Path,
tip: &str,
slice: u32,
phase: &str,
code_start: &str,
code_end: &str,
) -> anyhow::Result<FunnelOutcome> {
let row = boundary_row(phase, code_start, code_end);
let prov = Provenance::Conclude {
who: dispatch_identity(),
};
match land_boundary_row(coord_root, &dispatch_ref(slice), tip, slice, row, &prov)? {
CommitOutcome::Landed { oid } => Ok(FunnelOutcome::Concluded { coord_tip: oid }),
CommitOutcome::Refused(refusal) => Ok(funnel_refused(refusal.token(), String::new())),
}
}
pub(crate) fn dispatch_reap(root: &Path, slice: u32, name: &str) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
let reaped = FunnelOutcome::Reaped {
fork: name.to_owned(),
};
let funnel_landed = match funnel::resolve_landing(&coord.root, &coord.tip, slice, name)? {
LandingVerdict::Landed => true,
LandingVerdict::NotProven | LandingVerdict::NoRow => false,
LandingVerdict::Ambiguous => {
return Ok(funnel_refused(
AMBIGUOUS_FORK_ROW,
format!(
"two or more SL-{slice:03} funnel rows record fork {name}: no single import certifies it — repair the funnel record before reaping"
),
));
}
};
let funnel_record = funnel::read_funnel_at(&coord.root, &coord.tip, slice)?;
let Some((phase, row)) = row_for_fork(&funnel_record, name) else {
let outcome = reap_fork(Some(coord.root), name, funnel_landed)?;
return Ok(match reap_verdict(outcome, name) {
Ok(_) => reaped,
Err(refusal) => refusal,
});
};
let facts = TransitionFacts {
row: Some(row),
coord_tip: &coord.tip,
paths_since_verify: None,
};
if let Err(illegal) =
funnel_machine::attempt_advance(Some(row.position), &Transition::Reap, &facts)
{
return Ok(funnel_refused(illegal.reason, illegal.to_string()));
}
let outcome = reap_fork(Some(coord.root.clone()), name, funnel_landed)?;
let leftover = match reap_verdict(outcome, name) {
Ok(leftover) => leftover,
Err(refusal) => return Ok(refusal),
};
if let Some(pending) = land_reap_row(&coord.root, &coord.tip, slice, &phase, name)? {
return Ok(pending);
}
if !leftover.is_empty() {
drop(writeln!(
std::io::stderr(),
"warning: reap of {name} left administrative residue: {}",
leftover.join(", ")
));
}
Ok(reaped)
}
fn land_reap_row(
coord_root: &Path,
tip: &str,
slice: u32,
phase: &str,
fork: &str,
) -> anyhow::Result<Option<FunnelOutcome>> {
let at = crate::clock::now_timestamp()?;
let landing = funnel::land_funnel_transition(
coord_root,
&dispatch_ref(slice),
tip,
slice,
phase,
&Transition::Reap,
None,
&at,
&Provenance::Conclude {
who: dispatch_identity(),
},
);
let detail = match landing {
Ok(funnel::FunnelLanding::Landed { .. } | funnel::FunnelLanding::Replayed { .. }) => {
return Ok(None);
}
Ok(other) => format!("{other:?}"),
Err(cause) => format!("{cause:#}"),
};
Ok(Some(FunnelOutcome::ReapedRowPending {
fork: fork.to_owned(),
detail: format!(
"the fork {fork} IS reaped; the SL-{slice:03} {phase} reap row did not land ({detail}) — re-drive `dispatch_reap` to complete it"
),
}))
}
fn reap_verdict(outcome: GcOutcome, fork: &str) -> Result<Vec<String>, FunnelOutcome> {
match outcome {
GcOutcome::Reaped | GcOutcome::AlreadyAbsent => Ok(Vec::new()),
GcOutcome::NotLanded => Err(funnel_refused(
GcRefusal::NotLanded.token(),
format!(
"fork {fork} has not provably landed: {}",
GcRefusal::NotLanded.remedy()
),
)),
GcOutcome::Busy => Err(funnel_refused(
CLAIM_BUSY,
format!(
"an active claimant holds the claim lock for {fork} (a spawn is mid claim→bind→act); nothing was classified and nothing deleted — retry once it finishes"
),
)),
GcOutcome::Residual(residue) => {
let members = residue.members(fork, None);
if GcOutcome::Residual(residue).advances() {
Ok(members)
} else {
Err(funnel_refused(
GC_INCOMPLETE,
format!(
"reap of {fork} left leftover(s) needing manual cleanup: {}",
members.join(", ")
),
))
}
}
}
}
fn row_for_fork<'a>(
record: &'a funnel::FunnelRecord,
fork: &'a str,
) -> Option<(String, &'a PhaseRow)> {
record.rows_for_fork(fork).next().map(|r| (r.id.clone(), r))
}
pub(crate) fn dispatch_verify(
root: &Path,
slice: u32,
phase: &str,
) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
Ok(match verify_phase(&coord.root, &coord.tip, slice, phase)? {
VerifyOutcome::Verified { coord_tip, suite } => {
FunnelOutcome::Verified { coord_tip, suite }
}
VerifyOutcome::VerifyFailed { suite, detail } => {
FunnelOutcome::VerifyFailed { suite, detail }
}
VerifyOutcome::Refused { reason, detail } => funnel_refused(&reason, detail),
})
}
pub(crate) const TOOL_DISPATCH_PHASE_RECEIPT: &str = "dispatch_phase_receipt";
pub(crate) const TOOL_DISPATCH_NEXT_READY: &str = "dispatch_next_ready";
pub(crate) const TOOL_DISPATCH_AUTHORED_DIVERGENCE: &str = "dispatch_authored_divergence";
pub(crate) const TOOL_DISPATCH_TREE_STATE: &str = "dispatch_tree_state";
pub(crate) const TOOL_DISPATCH_NEXT: &str = "dispatch_next";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) enum ReadOutcome<T> {
Resolved(T),
CoordRefused { reason: String },
}
pub(crate) type PhaseReceiptResult = ReadOutcome<PhaseReceiptCore>;
pub(crate) type NextReadyResult = ReadOutcome<NextReadyCore>;
pub(crate) type DivergenceResult = ReadOutcome<DivergenceCore>;
fn coord_refused<T>(refusal: CoordRefusal) -> ReadOutcome<T> {
ReadOutcome::CoordRefused {
reason: refusal.token().to_owned(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct PhaseReceiptCore {
pub(crate) slice: u32,
pub(crate) phase: String,
pub(crate) status: String,
pub(crate) dispatch_tip: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) code_start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) code_end: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) position: Option<String>,
}
pub(crate) fn dispatch_phase_receipt(
root: &Path,
slice: u32,
phase: &str,
) -> anyhow::Result<PhaseReceiptResult> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(coord_refused(refusal)),
};
let boundaries = read_boundaries_at(&coord.root, &coord.tip, slice)?;
let boundary = boundaries.rows.iter().find(|r| r.phase == phase);
let has_boundary = boundary.is_some();
let position = funnel::read_funnel_at(&coord.root, &coord.tip, slice)?.position(phase);
let state_dir = crate::state::phases_dir(&coord.root, slice);
let status = crate::dispatch::phase_receipt_status(&state_dir, phase, has_boundary, position);
Ok(ReadOutcome::Resolved(PhaseReceiptCore {
slice,
phase: phase.to_owned(),
status: status.as_str().to_owned(),
dispatch_tip: coord.tip,
code_start: boundary.map(|b| b.code_start_oid.clone()),
code_end: boundary.map(|b| b.code_end_oid.clone()),
position: position.map(|p| p.as_str().to_owned()),
}))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct NextPhaseRow {
pub(crate) id: String,
pub(crate) status: String,
pub(crate) name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct NextReadyCore {
pub(crate) next: Vec<String>,
pub(crate) phases: Vec<NextPhaseRow>,
}
pub(crate) fn dispatch_next_ready(root: &Path, slice: u32) -> anyhow::Result<NextReadyResult> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(coord_refused(refusal)),
};
let rows = crate::dispatch::plan_next_rows(&coord.root, slice)?;
let next = crate::dispatch::compute_next_phases(&rows);
let phases = rows
.into_iter()
.map(|(id, status, name)| NextPhaseRow { id, status, name })
.collect();
Ok(ReadOutcome::Resolved(NextReadyCore { next, phases }))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct DivergenceCore {
pub(crate) diverged: bool,
pub(crate) compared_ref: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) drifted_paths: Vec<String>,
}
pub(crate) fn dispatch_authored_divergence(
root: &Path,
slice: u32,
) -> anyhow::Result<DivergenceResult> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(coord_refused(refusal)),
};
let compared_ref = git::trunk_commit(&coord.root)?.context(
"trunk ref not found (no DOCTRINE_TRUNK_REF / origin/HEAD / main / master resolves)",
)?;
let drifted_paths = git::diff_doctrine_paths(
&coord.root,
&compared_ref,
&coord.tip,
crate::corpus_guard::DOCTRINE_PATHSPEC,
)?;
Ok(ReadOutcome::Resolved(DivergenceCore {
diverged: !drifted_paths.is_empty(),
compared_ref,
drifted_paths,
}))
}
pub(crate) type TreeStateResult = ReadOutcome<crate::dispatch::TreeStateCore>;
pub(crate) fn dispatch_tree_state(root: &Path, slice: u32) -> anyhow::Result<TreeStateResult> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(coord_refused(refusal)),
};
let core = crate::dispatch::tree_state_core(slice, git::tree_clean_untracked(&coord.root)?);
Ok(ReadOutcome::Resolved(core))
}
pub(crate) type NextResult = ReadOutcome<crate::dispatch::NextCore>;
pub(crate) fn dispatch_next(root: &Path, slice: u32) -> anyhow::Result<NextResult> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(coord_refused(refusal)),
};
let core = crate::dispatch::next_core(&coord.root, &coord.tip, slice)?;
Ok(ReadOutcome::Resolved(core))
}
fn read_boundaries_at(root: &Path, tip: &str, slice: u32) -> anyhow::Result<Boundaries> {
let path = format!(".doctrine/dispatch/{slice:03}/boundaries.toml");
match git::read_path_at(root, tip, &path)? {
Some(text) => Boundaries::parse(&text)
.with_context(|| format!("parse committed boundaries.toml at {tip}")),
None => Ok(Boundaries::default()),
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests: fail-fast unwrap on fixture setup is idiomatic"
)]
mod tests {
use super::*;
use crate::worktree::Refusal;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
fn git_run(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn primary_with_coord(slice: u32) -> (tempfile::TempDir, PathBuf, PathBuf, String, String) {
let tmp = tempfile::tempdir().unwrap();
let primary = fs::canonicalize(tmp.path()).unwrap().join("primary");
fs::create_dir_all(&primary).unwrap();
git_run(&primary, &["init", "-q", "-b", "main"]);
git_run(&primary, &["config", "user.email", "t@t"]);
git_run(&primary, &["config", "user.name", "t"]);
fs::write(primary.join("seed"), "base\n").unwrap();
git_run(&primary, &["add", "-A"]);
git_run(&primary, &["commit", "-q", "-m", "base"]);
let base = git_run(&primary, &["rev-parse", "HEAD^{commit}"]);
let base_tree = git_run(&primary, &["rev-parse", "HEAD^{tree}"]);
let coord = fs::canonicalize(tmp.path()).unwrap().join("coord");
git_run(
&primary,
&[
"worktree",
"add",
"-q",
"-b",
&format!("dispatch/{slice:03}"),
coord.to_str().unwrap(),
&base,
],
);
let coord = fs::canonicalize(&coord).unwrap();
(tmp, primary, coord, base, base_tree)
}
#[test]
fn classify_coord_happy_returns_the_live_target() {
let facts = CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: true,
tip: Some("abc".to_string()),
};
assert_eq!(
classify_coord(facts),
Ok(CoordTarget {
root: PathBuf::from("/x/coord"),
tip: "abc".to_string(),
})
);
}
#[test]
fn classify_coord_zero_hits_is_unknown_slice() {
let facts = CoordFacts {
branch_hits: 0,
path: None,
prunable: false,
dir_exists: false,
tip: None,
};
assert_eq!(classify_coord(facts), Err(CoordRefusal::UnknownSlice));
assert_eq!(CoordRefusal::UnknownSlice.token(), "unknown-slice");
}
#[test]
fn classify_coord_multi_hit_is_ambiguous() {
let facts = CoordFacts {
branch_hits: 2,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: true,
tip: Some("abc".to_string()),
};
assert_eq!(classify_coord(facts), Err(CoordRefusal::Ambiguous));
assert_eq!(CoordRefusal::Ambiguous.token(), "ambiguous");
}
#[test]
fn classify_coord_single_hit_inconsistencies_are_stale() {
let stale_variants = [
CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: false,
tip: Some("abc".to_string()),
},
CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: true,
dir_exists: true,
tip: Some("abc".to_string()),
},
CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: true,
tip: None,
},
];
for facts in stale_variants {
assert_eq!(classify_coord(facts), Err(CoordRefusal::Stale));
}
assert_eq!(CoordRefusal::Stale.token(), "stale");
}
#[test]
fn resolve_coord_finds_the_live_coord_and_refuses_an_absent_slice() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
assert_eq!(
resolve_coord(&primary, 199),
Ok(CoordTarget {
root: coord,
tip: base,
})
);
assert_eq!(
resolve_coord(&primary, 200),
Err(CoordRefusal::UnknownSlice)
);
}
fn ct(coord: &Path, tip: &str) -> CoordTarget {
CoordTarget {
root: coord.to_path_buf(),
tip: tip.to_string(),
}
}
fn add_fork(dir: &Path, base: &str, branch: &str, file: &str, content: &str) -> String {
let base_tree = git_run(dir, &["rev-parse", &format!("{base}^{{tree}}")]);
let tree = git::tree_with_file(dir, &base_tree, file, content).unwrap();
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(["commit-tree", &tree, "-p", base, "-m", "worker work"])
.env("GIT_AUTHOR_NAME", "worker-x")
.env("GIT_AUTHOR_EMAIL", "worker-x@doctrine")
.env("GIT_COMMITTER_NAME", "worker-x")
.env("GIT_COMMITTER_EMAIL", "worker-x@doctrine")
.output()
.unwrap();
assert!(
out.status.success(),
"commit-tree: {}",
String::from_utf8_lossy(&out.stderr)
);
let commit = String::from_utf8_lossy(&out.stdout).trim().to_string();
git_run(
dir,
&["update-ref", &format!("refs/heads/{branch}"), &commit],
);
commit
}
mod funnel_gates {
use super::super::{
dispatch_conclude_phase, dispatch_import, dispatch_phase_receipt, dispatch_reap,
};
use super::{add_fork, git_run, primary_with_coord};
use crate::dispatch::{
Identity, Provenance, dispatch_ref, funnel, funnel_message, verify_phase,
};
use crate::funnel_machine::{
Position, Transition, VerifyEvidence, VerifyStatus, funnel_record_path,
};
use crate::mcp_server::dispatch::{FunnelOutcome, ReadOutcome};
use crate::worktree::{ForkBinding, provision_dispatch_record};
use std::fs;
use std::path::{Path, PathBuf};
const SLICE: u32 = 199;
const PHASE: &str = "PHASE-01";
const FORK: &str = "dispatch/wk";
const AGENT: &str = "wk";
const AT: &str = "2026-07-25T09:00:00Z";
fn disp() -> Provenance {
Provenance::Conclude {
who: Identity {
name: "dispatch".to_string(),
email: "dispatch@doctrine".to_string(),
},
}
}
fn land(coord: &Path, tip: &str, ts: &[Transition], paths: Option<&[String]>) -> String {
match funnel::land_funnel_transitions(
coord,
&dispatch_ref(SLICE),
tip,
SLICE,
PHASE,
ts,
paths,
None,
&funnel_message(SLICE, PHASE),
AT,
&disp(),
)
.unwrap()
{
funnel::FunnelLanding::Landed { oid, .. } => oid,
other => panic!("expected Landed, got {other:?}"),
}
}
fn tip(coord: &Path) -> String {
git_run(coord, &["rev-parse", &dispatch_ref(SLICE)])
}
fn position(coord: &Path, at: &str) -> Option<Position> {
funnel::read_funnel_at(coord, at, SLICE)
.unwrap()
.position(PHASE)
}
fn live_fork(coord: &Path, base: &str, binding: Option<&ForkBinding>) -> PathBuf {
let dir = coord.join(".worktrees").join(AGENT);
git_run(
coord,
&[
"worktree",
"add",
"-q",
"-b",
FORK,
dir.to_str().unwrap(),
base,
],
);
add_fork(coord, base, FORK, "src/f.rs", "fn f() {}\n");
provision_dispatch_record(coord, AGENT, base, &dir, FORK, binding).unwrap();
dir
}
fn bound() -> ForkBinding {
ForkBinding {
slice: SLICE,
phase: PHASE.to_owned(),
}
}
fn seed_slice(coord: &Path, selectors: &[&str]) {
let dir = coord.join(format!(".doctrine/slice/{SLICE:03}"));
fs::create_dir_all(&dir).unwrap();
let rows: String = selectors
.iter()
.map(|s| {
format!("\n[[selector]]\nselector = \"{s}\"\nintent = \"design-target\"\n")
})
.collect();
fs::write(
dir.join(format!("slice-{SLICE:03}.toml")),
format!(
"id = {SLICE}\nslug = \"fixture\"\ntitle = \"Fixture\"\n\
status = \"proposed\"\ncreated = \"2026-07-25\"\nupdated = \"2026-07-25\"\n\
\n[relationships]\nneeds = []\nafter = []\n{rows}"
),
)
.unwrap();
fs::write(
dir.join(format!("slice-{SLICE:03}.md")),
"# Fixture\n\n## Context\n\n## Scope & Objectives\n",
)
.unwrap();
}
#[test]
fn import_heals_the_whole_prefix_in_exactly_one_commit() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
seed_slice(&coord, &["src/**"]);
live_fork(&coord, &base, Some(&bound()));
let out = dispatch_import(&primary, SLICE, FORK).unwrap();
let landed = match out {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
assert_eq!(
git_run(
&coord,
&["rev-list", "--count", &format!("{base}..{landed}")]
),
"1",
"delta ⊕ healed prefix in a single commit"
);
let row = funnel::read_funnel_at(&coord, &landed, SLICE)
.unwrap()
.row(PHASE)
.cloned()
.expect("the healed row");
assert_eq!(row.position, Position::Imported);
assert_eq!(row.spawn.expect("spawn").fork, FORK);
assert!(
row.worker_commit.is_some(),
"the lagging Class-2 row healed"
);
assert!(row.import.is_some());
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &landed]);
assert!(names.lines().any(|l| l == "src/f.rs"), "{names}");
assert!(names.contains(&funnel_record_path(SLICE)), "{names}");
assert_eq!(
git_run(&coord, &["log", "-1", "--format=%an", &landed]),
"worker-x",
"IMPORT provenance keeps the worker author"
);
}
#[test]
fn position_never_durably_rests_at_worker_committed_via_import() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
seed_slice(&coord, &["src/**"]);
live_fork(&coord, &base, Some(&bound()));
dispatch_import(&primary, SLICE, FORK).unwrap();
let landed = tip(&coord);
let walk = git_run(&coord, &["rev-list", &format!("{base}..{landed}")]);
for commit in walk.lines() {
assert_ne!(
position(&coord, commit),
Some(Position::WorkerCommitted),
"no commit in the import run rests at worker-committed"
);
}
}
#[test]
fn a_lost_response_retry_is_a_no_op_replay_despite_a_fresh_onto() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
seed_slice(&coord, &["src/**"]);
live_fork(&coord, &base, Some(&bound()));
let first = match dispatch_import(&primary, SLICE, FORK).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
assert_eq!(
dispatch_import(&primary, SLICE, FORK).unwrap(),
FunnelOutcome::Imported {
coord_tip: first.clone()
}
);
assert_eq!(tip(&coord), first, "the retry landed nothing");
assert_eq!(
git_run(
&coord,
&["rev-list", "--count", &format!("{base}..{first}")]
),
"1",
"still exactly one import commit"
);
}
#[test]
fn an_unbound_fork_refuses_unprovable_fork_and_lands_nothing() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
seed_slice(&coord, &["src/**"]);
live_fork(&coord, &base, None);
match dispatch_import(&primary, SLICE, FORK).unwrap() {
FunnelOutcome::Refused { reason, detail } => {
assert_eq!(reason, "unprovable-fork");
assert_eq!(detail, FORK);
}
other => panic!("expected Refused, got {other:?}"),
}
assert_eq!(tip(&coord), base, "nothing landed");
}
#[test]
fn a_fork_bound_to_another_slice_is_refused() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
seed_slice(&coord, &["src/**"]);
live_fork(
&coord,
&base,
Some(&ForkBinding {
slice: SLICE + 1,
phase: PHASE.to_owned(),
}),
);
match dispatch_import(&primary, SLICE, FORK).unwrap() {
FunnelOutcome::Refused { reason, detail } => {
assert_eq!(reason, "unprovable-fork");
assert!(detail.contains("SL-200"), "names the other slice: {detail}");
}
other => panic!("expected Refused, got {other:?}"),
}
assert_eq!(tip(&coord), base, "nothing landed");
}
fn verified_phase(coord: &Path, base: &str) -> String {
let imported = land(
coord,
base,
&[
Transition::Spawn {
fork: FORK.to_owned(),
base_oid: base.to_owned(),
},
Transition::RecordWorkerCommit {
fork_tip: "forktip".to_owned(),
},
Transition::Import {
fork_tip: "forktip".to_owned(),
onto: base.to_owned(),
},
],
None,
);
land(
coord,
&imported,
&[Transition::Verify {
evidence: VerifyEvidence {
status: VerifyStatus::Pass,
verified_oid: imported.clone(),
suite: "gate".to_owned(),
at: AT.to_owned(),
},
}],
None,
)
}
fn seed_sheets(coord: &Path, primary: &Path, status: &str) {
for root in [coord, primary] {
let dir = crate::state::phases_dir(root, SLICE);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("phase-01.toml"),
format!("status = \"{status}\"\n"),
)
.unwrap();
}
}
#[test]
fn a_funnel_managed_conclude_lands_boundary_and_position_in_one_commit() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let verified = verified_phase(&coord, &base);
seed_sheets(&coord, &primary, "in_progress");
let out =
dispatch_conclude_phase(&primary, SLICE, PHASE, &base, &verified, Some("done"))
.unwrap();
let landed = match out {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("expected Concluded, got {other:?}"),
};
assert_eq!(
git_run(
&coord,
&["rev-list", "--count", &format!("{verified}..{landed}")]
),
"1",
"boundary ⊕ position in ONE commit"
);
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &landed]);
assert!(names.contains("boundaries.toml"), "{names}");
assert_eq!(position(&coord, &landed), Some(Position::Concluded));
assert_eq!(
crate::state::read_phase_status(
&crate::state::phases_dir(&coord, SLICE),
"phase-01"
)
.unwrap(),
Some("completed".to_string())
);
}
#[test]
fn the_conclude_kill_window_still_reads_as_completed() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let verified = verified_phase(&coord, &base);
seed_sheets(&coord, &primary, "in_progress");
let row = funnel::read_funnel_at(&coord, &verified, SLICE)
.unwrap()
.row(PHASE)
.cloned()
.expect("the verified row");
let landed = match super::super::conclude_funnel_commit(
&super::ct(&coord, &verified),
SLICE,
PHASE,
&base,
&verified,
&row,
AT,
)
.unwrap()
{
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("expected Concluded, got {other:?}"),
};
assert_eq!(position(&coord, &landed), Some(Position::Concluded));
assert_eq!(
crate::state::read_phase_status(
&crate::state::phases_dir(&coord, SLICE),
"phase-01"
)
.unwrap(),
Some("in_progress".to_string()),
"the sheet is deliberately BEHIND — the kill window"
);
match dispatch_phase_receipt(&primary, SLICE, PHASE).unwrap() {
ReadOutcome::Resolved(core) => {
assert_eq!(
core.status, "completed",
"a lagging sheet must not mask durable completion"
);
assert_eq!(core.position.as_deref(), Some("concluded"));
}
other => panic!("expected Resolved, got {other:?}"),
}
}
#[test]
fn conclude_refuses_when_a_non_record_path_changed_since_the_verified_tip() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let verified = verified_phase(&coord, &base);
let drifted_tree = crate::git::tree_with_file(
&coord,
&format!("{verified}^{{tree}}"),
"seed",
"drift\n",
)
.unwrap();
let drifted = git_run(
&coord,
&["commit-tree", &drifted_tree, "-p", &verified, "-m", "drift"],
);
git_run(&coord, &["update-ref", &dispatch_ref(SLICE), &drifted]);
seed_sheets(&coord, &primary, "in_progress");
match dispatch_conclude_phase(&primary, SLICE, PHASE, &base, &drifted, None).unwrap() {
FunnelOutcome::Refused { reason, .. } => {
assert_eq!(reason, "conclude-verify-stale");
}
other => panic!("expected Refused, got {other:?}"),
}
assert_eq!(tip(&coord), drifted, "nothing landed");
assert_eq!(
crate::state::read_phase_status(
&crate::state::phases_dir(&coord, SLICE),
"phase-01"
)
.unwrap(),
Some("in_progress".to_string()),
"a refused conclude never flips the sheet"
);
}
#[test]
fn a_fresh_pass_can_always_conclude_because_evidence_never_self_stales() {
let (_tmp, primary, coord, _base, _bt) = primary_with_coord(SLICE);
fs::create_dir_all(coord.join(".doctrine")).unwrap();
fs::write(
coord.join(".doctrine/doctrine.toml"),
"[verification]\ngate = [\"true\"]\n\n[dispatch]\nverify-suite = \"gate\"\n",
)
.unwrap();
git_run(&coord, &["add", "."]);
git_run(&coord, &["commit", "-q", "-m", "config"]);
let configured = git_run(&coord, &["rev-parse", "HEAD"]);
let imported = land(
&coord,
&configured,
&[
Transition::Spawn {
fork: FORK.to_owned(),
base_oid: configured.clone(),
},
Transition::RecordWorkerCommit {
fork_tip: "forktip".to_owned(),
},
Transition::Import {
fork_tip: "forktip".to_owned(),
onto: configured.clone(),
},
],
None,
);
verify_phase(&coord, &imported, SLICE, PHASE).unwrap();
let verified = tip(&coord);
seed_sheets(&coord, &primary, "in_progress");
match dispatch_conclude_phase(&primary, SLICE, PHASE, &configured, &verified, None)
.unwrap()
{
FunnelOutcome::Concluded { .. } => {}
other => panic!("a fresh pass must conclude, got {other:?}"),
}
assert_eq!(position(&coord, &tip(&coord)), Some(Position::Concluded));
}
#[test]
fn reap_refuses_before_conclude_when_the_phase_is_funnel_managed() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let dir = live_fork(&coord, &base, Some(&bound()));
land(
&coord,
&base,
&[Transition::Spawn {
fork: FORK.to_owned(),
base_oid: base.clone(),
}],
None,
);
match dispatch_reap(&primary, SLICE, FORK).unwrap() {
FunnelOutcome::Refused { reason, .. } => assert_eq!(reason, "worker-not-committed"),
other => panic!("expected Refused, got {other:?}"),
}
assert!(dir.exists(), "the gate refused BEFORE the gc belt ran");
assert!(
crate::git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", FORK])
.unwrap()
.is_some(),
"the fork branch survives the refusal"
);
}
#[test]
fn reap_at_concluded_with_the_fork_already_absent_completes_idempotently() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let verified = verified_phase(&coord, &base);
let since = vec![funnel_record_path(SLICE)];
let concluded = land(&coord, &verified, &[Transition::Conclude], Some(&since));
assert_eq!(position(&coord, &concluded), Some(Position::Concluded));
let out = dispatch_reap(&primary, SLICE, FORK).unwrap();
assert_eq!(
out,
FunnelOutcome::Reaped {
fork: FORK.to_owned()
}
);
assert_eq!(
position(&coord, &tip(&coord)),
Some(Position::Reaped),
"the run completed: the Reap row landed without a gc"
);
}
#[test]
fn a_fork_with_no_funnel_row_keeps_the_legacy_ungated_reap() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
seed_slice(&coord, &["src/**"]);
live_fork(&coord, &base, Some(&bound()));
dispatch_import(&primary, SLICE, FORK).unwrap();
let other = "dispatch/legacy";
add_fork(&coord, &base, other, "src/legacy.rs", "l\n");
match dispatch_reap(&primary, SLICE, other).unwrap() {
FunnelOutcome::Refused { reason, detail } => {
assert_eq!(reason, "not-landed");
assert!(detail.contains(other), "names the fork: {detail}");
assert!(
detail.contains("dispatch_reap"),
"carries the single-sourced remedy: {detail}"
);
}
other => panic!("expected Refused, got {other:?}"),
}
assert!(
crate::git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", other])
.unwrap()
.is_some(),
"the unlanded fork survives the refusal"
);
}
fn concluded_with_live_fork(primary: &Path, coord: &Path, base: &str) -> PathBuf {
seed_slice(coord, &["src/**"]);
let dir = live_fork(coord, base, Some(&bound()));
let imported = match dispatch_import(primary, SLICE, FORK).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
let verified = land(
coord,
&imported,
&[Transition::Verify {
evidence: VerifyEvidence {
status: VerifyStatus::Pass,
verified_oid: imported.clone(),
suite: "gate".to_owned(),
at: AT.to_owned(),
},
}],
None,
);
let since = vec![funnel_record_path(SLICE)];
land(coord, &verified, &[Transition::Conclude], Some(&since));
dir
}
#[test]
fn a_funnel_managed_fork_is_reaped_without_force_despite_the_patch_id_oracle() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let dir = concluded_with_live_fork(&primary, &coord, &base);
assert!(dir.exists(), "precondition: the fork worktree is live");
let cherry = git_run(&coord, &["cherry", "HEAD", FORK]);
assert!(
cherry.lines().any(|l| l.starts_with('+')),
"precondition (ISS-245): the atomic import defeats patch-id; got {cherry:?}"
);
assert_eq!(
dispatch_reap(&primary, SLICE, FORK).unwrap(),
FunnelOutcome::Reaped {
fork: FORK.to_owned()
}
);
assert!(
crate::git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", FORK])
.unwrap()
.is_none(),
"the fork branch is reaped"
);
assert!(!dir.exists(), "the fork worktree is reaped");
assert_eq!(
position(&coord, &tip(&coord)),
Some(Position::Reaped),
"the funnel position advanced"
);
}
#[test]
fn a_branch_advanced_past_the_imported_tip_refuses_and_keeps_the_branch() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
concluded_with_live_fork(&primary, &coord, &base);
let fork_tip = git_run(&coord, &["rev-parse", FORK]);
let advanced = git_run(
&coord,
&[
"commit-tree",
&format!("{fork_tip}^{{tree}}"),
"-p",
&fork_tip,
"-m",
"work the funnel never imported",
],
);
git_run(
&coord,
&["update-ref", &format!("refs/heads/{FORK}"), &advanced],
);
match dispatch_reap(&primary, SLICE, FORK).unwrap() {
FunnelOutcome::Refused { reason, .. } => assert_eq!(reason, "not-landed"),
other => panic!("expected Refused, got {other:?}"),
}
assert_eq!(
crate::git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", FORK])
.unwrap()
.as_deref(),
Some(advanced.as_str()),
"the branch — and the uncertified work on it — survives"
);
assert_eq!(
position(&coord, &tip(&coord)),
Some(Position::Concluded),
"position did not advance"
);
}
#[test]
fn a_lost_reap_row_cas_is_row_pending_never_a_bare_reaped() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
concluded_with_live_fork(&primary, &coord, &base);
let concluded = tip(&coord);
let sibling = git_run(
&coord,
&[
"commit-tree",
&format!("{concluded}^{{tree}}"),
"-p",
&concluded,
"-m",
"a sibling advance",
],
);
git_run(&coord, &["update-ref", &dispatch_ref(SLICE), &sibling]);
assert_ne!(tip(&coord), concluded, "precondition: the ref moved");
match super::super::land_reap_row(&coord, &concluded, SLICE, PHASE, FORK).unwrap() {
Some(FunnelOutcome::ReapedRowPending { fork, detail }) => {
assert_eq!(fork, FORK);
assert!(detail.contains("IS reaped"), "{detail}");
assert!(detail.contains("re-drive"), "{detail}");
}
other => panic!("expected ReapedRowPending, got {other:?}"),
}
assert_eq!(
position(&coord, &tip(&coord)),
Some(Position::Concluded),
"the reap row did NOT land"
);
assert_eq!(tip(&coord), sibling, "and nothing was committed");
assert_eq!(
super::super::land_reap_row(&coord, &sibling, SLICE, PHASE, FORK).unwrap(),
None
);
assert_eq!(position(&coord, &tip(&coord)), Some(Position::Reaped));
}
#[test]
fn two_rows_naming_one_fork_refuse_before_any_act() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(SLICE);
let dir = concluded_with_live_fork(&primary, &coord, &base);
let concluded = tip(&coord);
let second = crate::dispatch::funnel::land_funnel_transition(
&coord,
&dispatch_ref(SLICE),
&concluded,
SLICE,
"PHASE-02",
&Transition::Spawn {
fork: FORK.to_owned(),
base_oid: base.clone(),
},
None,
AT,
&disp(),
)
.unwrap();
assert!(matches!(
second,
crate::dispatch::funnel::FunnelLanding::Landed { .. }
));
match dispatch_reap(&primary, SLICE, FORK).unwrap() {
FunnelOutcome::Refused { reason, detail } => {
assert_eq!(reason, "ambiguous-fork-row");
assert!(detail.contains(FORK), "names the fork: {detail}");
}
other => panic!("expected Refused, got {other:?}"),
}
assert!(dir.exists(), "nothing was deleted");
assert!(
crate::git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", FORK])
.unwrap()
.is_some(),
"the branch survives an ambiguous record"
);
}
}
#[test]
fn dispatch_import_happy_advances_the_coord_tip_by_one() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk", "src/f.rs", "fn f() {}\n");
let out = import_compose(&ct(&coord, &base), 199, "dispatch/wk", &[]).unwrap();
let coord_tip = match out {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
coord_tip
);
assert_ne!(coord_tip, base, "coord tip advanced");
let parents = git_run(&coord, &["rev-list", "--parents", "-n", "1", &coord_tip]);
let cols: Vec<&str> = parents.split_whitespace().collect();
assert_eq!(cols.len(), 2, "exactly one parent: {parents}");
assert_eq!(cols[1], base, "parent == coord tip (B)");
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &coord_tip]);
assert!(
names.lines().any(|l| l == "src/f.rs"),
"worker file imported: {names}"
);
let raw = git_run(&coord, &["log", "-1", "--format=%an%n%ae", &coord_tip]);
let mut it = raw.lines();
assert_eq!(it.next().unwrap(), "worker-x", "worker author preserved");
assert_eq!(it.next().unwrap(), "worker-x@doctrine");
}
#[test]
fn dispatch_import_undeclared_scope_refuses_before_compose_and_leaves_the_tip() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk", "docs/readme.md", "hi\n");
let selectors = vec!["src/**".to_string()];
let out = import_compose(&ct(&coord, &base), 199, "dispatch/wk", &selectors).unwrap();
match out {
FunnelOutcome::Refused { reason, detail } => {
assert_eq!(reason, Refusal::UndeclaredScope.token());
assert!(
detail.contains("docs/readme.md"),
"detail names the offending path: {detail}"
);
assert!(
detail.contains("doctrine slice selector add SL-199 docs/readme.md"),
"detail carries a runnable remediation: {detail}"
);
}
other => panic!("expected Refused, got {other:?}"),
}
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
base
);
}
#[test]
fn dispatch_import_composes_a_second_disjoint_fork_via_merge_tree() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk1", "src/a.rs", "a\n");
let tip1 = match import_compose(&ct(&coord, &base), 199, "dispatch/wk1", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
add_fork(&coord, &base, "dispatch/wk2", "src/b.rs", "b\n");
let tip2 = match import_compose(&ct(&coord, &tip1), 199, "dispatch/wk2", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
tip2
);
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &tip2]);
assert!(
names.lines().any(|l| l == "src/a.rs"),
"first delta: {names}"
);
assert!(
names.lines().any(|l| l == "src/b.rs"),
"second delta: {names}"
);
}
#[test]
fn dispatch_import_unknown_slice_refuses_via_resolve_coord() {
let (_tmp, primary, _coord, _base, _bt) = primary_with_coord(199);
let out = dispatch_import(&primary, 200, "dispatch/wk").unwrap();
assert_eq!(out, funnel_refused("unknown-slice", String::new()));
}
#[test]
fn dispatch_import_is_transport_agnostic_routed_only_by_declared_args() {
let (_tmp_a, primary_a, _coord_a, _base_a, _bt_a) = primary_with_coord(199);
let (_tmp_b, primary_b, _coord_b, base_b, _bt_b) = primary_with_coord(200);
let found = dispatch_import(&primary_a, 199, "dispatch/wk");
assert!(
!matches!(&found, Ok(o) if *o == funnel_refused("unknown-slice", String::new())),
"resolve_coord matched the live dispatch/199 coord at the declared root, \
not the process cwd — must not short-circuit unknown-slice: {found:?}",
);
assert_eq!(
dispatch_import(&primary_b, 199, "dispatch/wk").unwrap(),
funnel_refused("unknown-slice", String::new()),
"the root arg keys resolve_coord — not cwd/agent_id/payload",
);
assert_eq!(
dispatch_import(&primary_a, 200, "dispatch/wk").unwrap(),
funnel_refused("unknown-slice", String::new()),
"the slice arg keys resolve_coord — not cwd/agent_id/payload",
);
assert_eq!(
git_run(&primary_b, &["rev-parse", "refs/heads/dispatch/200"]),
base_b,
"sibling fixture untouched — resolution never keyed off ambient state",
);
}
#[test]
fn conclude_boundary_commit_happy_lands_the_true_range_row() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk", "src/f.rs", "x\n");
let code_end = match import_compose(&ct(&coord, &base), 199, "dispatch/wk", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let out =
conclude_boundary_commit(&coord, &code_end, 199, "PHASE-01", &base, &code_end).unwrap();
let tip = match out {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("expected Concluded, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
tip
);
let committed = git_run(
&coord,
&[
"show",
&format!("{tip}:.doctrine/dispatch/199/boundaries.toml"),
],
);
let parsed = Boundaries::parse(&committed).unwrap();
assert_eq!(parsed.rows.len(), 1);
assert_eq!(parsed.rows[0].phase, "PHASE-01");
assert_eq!(parsed.rows[0].code_start_oid, base, "code_start == B");
assert_eq!(
parsed.rows[0].code_end_oid, code_end,
"code_end == coord_tip"
);
}
#[test]
fn conclude_boundary_commit_upserts_by_phase_never_duplicates() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
let tip1 =
match conclude_boundary_commit(&coord, &base, 199, "PHASE-01", &base, &base).unwrap() {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let tip2 =
match conclude_boundary_commit(&coord, &tip1, 199, "PHASE-01", &base, &tip1).unwrap() {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let committed = git_run(
&coord,
&[
"show",
&format!("{tip2}:.doctrine/dispatch/199/boundaries.toml"),
],
);
let parsed = Boundaries::parse(&committed).unwrap();
assert_eq!(parsed.rows.len(), 1, "upsert replaces, never appends");
assert_eq!(parsed.rows[0].code_end_oid, tip1, "the row was updated");
}
#[test]
fn conclude_boundary_commit_fault_lands_no_boundary_and_leaves_porcelain_clean() {
let (_tmp, _p, coord, base, base_tree) = primary_with_coord(199);
let other = git_run(
&coord,
&["commit-tree", &base_tree, "-p", &base, "-m", "other"],
);
git_run(&coord, &["update-ref", "refs/heads/dispatch/199", &other]);
let status_before = git_run(&coord, &["status", "--porcelain"]);
let out = conclude_boundary_commit(&coord, &base, 199, "PHASE-01", &base, &base).unwrap();
assert_eq!(out, funnel_refused("lost-ref-race", String::new()));
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
other
);
assert!(
git::read_path_at(&coord, &other, ".doctrine/dispatch/199/boundaries.toml")
.unwrap()
.is_none(),
"no boundary committed on the fault"
);
assert_eq!(
git_run(&coord, &["status", "--porcelain"]),
status_before,
"porcelain byte-unchanged"
);
}
#[test]
fn dispatch_conclude_phase_flips_the_gitignored_sheet_and_lands_the_boundary() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
let phases_dir = crate::state::phases_dir(&coord, 199);
fs::create_dir_all(&phases_dir).unwrap();
fs::write(
phases_dir.join("phase-01.toml"),
"status = \"in_progress\"\n",
)
.unwrap();
add_fork(&coord, &base, "dispatch/wk", "src/f.rs", "x\n");
let code_end = match import_compose(&ct(&coord, &base), 199, "dispatch/wk", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let out =
dispatch_conclude_phase(&primary, 199, "PHASE-01", &base, &code_end, Some("done"))
.unwrap();
let tip = match out {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("expected Concluded, got {other:?}"),
};
let committed = git_run(
&coord,
&[
"show",
&format!("{tip}:.doctrine/dispatch/199/boundaries.toml"),
],
);
assert!(
committed.contains("PHASE-01"),
"boundary committed: {committed}"
);
assert_eq!(
crate::state::read_phase_status(&phases_dir, "phase-01").unwrap(),
Some("completed".to_string()),
"sheet flipped to completed"
);
let committed_names = git_run(&coord, &["ls-tree", "-r", "--name-only", &tip]);
assert!(
!committed_names
.lines()
.any(|l| l.starts_with(".doctrine/state/")),
"the phase sheet never enters committed history"
);
}
#[test]
fn dispatch_conclude_phase_mirrors_the_completed_flip_into_the_primary_tree() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
for root in [&coord, &primary] {
let dir = crate::state::phases_dir(root, 199);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("phase-01.toml"), "status = \"in_progress\"\n").unwrap();
}
dispatch_conclude_phase(&primary, 199, "PHASE-01", &base, &base, Some("done")).unwrap();
assert_eq!(
crate::state::read_phase_status(&crate::state::phases_dir(&coord, 199), "phase-01")
.unwrap(),
Some("completed".to_string()),
"coord sheet flipped",
);
assert_eq!(
crate::state::read_phase_status(&crate::state::phases_dir(&primary, 199), "phase-01")
.unwrap(),
Some("completed".to_string()),
"primary sheet mirrored (IMP-272)",
);
}
#[test]
fn dispatch_reap_reaps_a_landed_fork_and_refuses_an_unlanded_one() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
let landed_branch = "dispatch/landed";
add_fork(&coord, &base, landed_branch, "src/landed.rs", "l\n");
let landed_wt = coord.join(".worktrees").join("landed");
git_run(
&coord,
&[
"worktree",
"add",
"-q",
landed_wt.to_str().unwrap(),
landed_branch,
],
);
import_compose(&ct(&coord, &base), 199, landed_branch, &[]).unwrap();
let out = dispatch_reap(&primary, 199, landed_branch).unwrap();
assert_eq!(
out,
FunnelOutcome::Reaped {
fork: landed_branch.to_string()
}
);
assert!(
git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", landed_branch])
.unwrap()
.is_none(),
"the landed fork branch was reaped"
);
let unlanded_branch = "dispatch/unlanded";
add_fork(&coord, &base, unlanded_branch, "src/unlanded.rs", "u\n");
match dispatch_reap(&primary, 199, unlanded_branch).unwrap() {
FunnelOutcome::Refused { reason, detail } => {
assert_eq!(reason, "not-landed", "gc refuses an unlanded fork");
assert!(
detail.contains(unlanded_branch) && detail.contains("--force"),
"the detail names the fork and the remedy: {detail}"
);
}
other => panic!("expected Refused, got {other:?}"),
}
assert!(
git::git_opt(
&coord,
&["rev-parse", "--verify", "--quiet", unlanded_branch]
)
.unwrap()
.is_some(),
"the unlanded fork branch survives the refusal"
);
}
#[test]
fn dispatch_reap_unknown_slice_refuses_via_resolve_coord() {
let (_tmp, primary, _coord, _base, _bt) = primary_with_coord(199);
let out = dispatch_reap(&primary, 200, "dispatch/wk").unwrap();
assert_eq!(out, funnel_refused("unknown-slice", String::new()));
}
fn write_phase_sheet(coord: &Path, slice: u32, phase: &str, status: &str) {
let dir = crate::state::phases_dir(coord, slice);
fs::create_dir_all(&dir).unwrap();
let stem = phase.to_lowercase();
fs::write(
dir.join(format!("{stem}.toml")),
format!("status = \"{status}\"\n"),
)
.unwrap();
}
#[test]
fn dispatch_phase_receipt_resolved_carries_real_tip_distinct_from_code_end() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(206);
fs::create_dir_all(coord.join(".doctrine/dispatch/206")).unwrap();
fs::write(
coord.join(".doctrine/dispatch/206/boundaries.toml"),
format!(
"[[boundary]]\nphase = \"PHASE-01\"\ncode_start_oid = \"{base}\"\ncode_end_oid = \"{base}\"\n"
),
)
.unwrap();
git_run(&coord, &["add", ".doctrine/dispatch/206/boundaries.toml"]);
git_run(&coord, &["commit", "-q", "-m", "boundary"]);
let tip = git_run(&coord, &["rev-parse", "refs/heads/dispatch/206"]);
write_phase_sheet(&coord, 206, "PHASE-01", "completed");
match dispatch_phase_receipt(&primary, 206, "PHASE-01").unwrap() {
ReadOutcome::Resolved(core) => {
assert_eq!(core.dispatch_tip, tip, "carries the REAL live coord tip");
assert_eq!(
core.code_end.as_deref(),
Some(base.as_str()),
"the committed boundary's code_end"
);
assert_ne!(
core.dispatch_tip,
core.code_end.clone().unwrap(),
"the live dispatch_tip is DISTINCT from the boundary code_end (EX-1)"
);
assert_eq!(core.status, "completed");
}
other => panic!("expected Resolved, got {other:?}"),
}
}
#[test]
fn dispatch_phase_receipt_no_boundary_omits_oids_and_reflects_sheet() {
let (_tmp, primary, coord, _base, _bt) = primary_with_coord(206);
write_phase_sheet(&coord, 206, "PHASE-02", "completed");
match dispatch_phase_receipt(&primary, 206, "PHASE-02").unwrap() {
ReadOutcome::Resolved(core) => {
assert_eq!(core.status, "conclude-incomplete");
assert!(core.code_start.is_none() && core.code_end.is_none());
}
other => panic!("expected Resolved, got {other:?}"),
}
}
#[test]
fn read_tools_refuse_unknown_slice_with_no_fabricated_tip() {
let (_tmp, primary, _coord, _base, _bt) = primary_with_coord(206);
assert_eq!(
dispatch_phase_receipt(&primary, 207, "PHASE-01").unwrap(),
ReadOutcome::CoordRefused {
reason: "unknown-slice".to_string()
}
);
assert_eq!(
dispatch_next_ready(&primary, 207).unwrap(),
ReadOutcome::CoordRefused {
reason: "unknown-slice".to_string()
}
);
assert_eq!(
dispatch_authored_divergence(&primary, 207).unwrap(),
ReadOutcome::CoordRefused {
reason: "unknown-slice".to_string()
}
);
}
#[test]
fn dispatch_next_ready_agrees_with_compute_next_phases() {
let (_tmp, primary, coord, _base, _bt) = primary_with_coord(206);
fs::create_dir_all(coord.join(".doctrine/slice/206")).unwrap();
fs::write(
coord.join(".doctrine/slice/206/plan.toml"),
"[[phase]]\nid = \"PHASE-01\"\nname = \"one\"\n\n\
[[phase]]\nid = \"PHASE-02\"\nname = \"two\"\n\n\
[[phase]]\nid = \"PHASE-03\"\nname = \"three\"\n",
)
.unwrap();
write_phase_sheet(&coord, 206, "PHASE-01", "completed");
let rows = crate::dispatch::plan_next_rows(&coord, 206).unwrap();
let expected = crate::dispatch::compute_next_phases(&rows);
assert_eq!(expected, vec!["PHASE-02", "PHASE-03"], "fixture sanity");
match dispatch_next_ready(&primary, 206).unwrap() {
ReadOutcome::Resolved(core) => {
assert_eq!(core.next, expected, "next == compute_next_phases VERBATIM");
let ids: Vec<&str> = core.phases.iter().map(|p| p.id.as_str()).collect();
assert_eq!(ids, vec!["PHASE-01", "PHASE-02", "PHASE-03"]);
assert_eq!(core.phases[0].status, "completed");
assert_eq!(core.phases[1].status, "pending");
}
other => panic!("expected Resolved, got {other:?}"),
}
}
#[test]
fn dispatch_authored_divergence_true_iff_doctrine_differs_trunk_to_tip() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(206);
match dispatch_authored_divergence(&primary, 206).unwrap() {
ReadOutcome::Resolved(core) => {
assert!(!core.diverged, "clean coord has no divergence: {core:?}");
assert_eq!(
core.compared_ref, base,
"compared_ref = git::trunk_commit (the resolved main tip)"
);
assert!(core.drifted_paths.is_empty());
}
other => panic!("expected Resolved, got {other:?}"),
}
fs::create_dir_all(coord.join(".doctrine/slice/206")).unwrap();
fs::write(coord.join(".doctrine/slice/206/notes.md"), "drift\n").unwrap();
git_run(&coord, &["add", ".doctrine/slice/206/notes.md"]);
git_run(&coord, &["commit", "-q", "-m", "authored drift"]);
match dispatch_authored_divergence(&primary, 206).unwrap() {
ReadOutcome::Resolved(core) => {
assert!(
core.diverged,
"authored `.doctrine` change diverges: {core:?}"
);
assert_eq!(
core.compared_ref, base,
"still compared to the RESOLVED trunk, never edge/hardcode"
);
assert!(
core.drifted_paths
.iter()
.any(|p| p == ".doctrine/slice/206/notes.md"),
"the drifted authored path is reported: {:?}",
core.drifted_paths
);
}
other => panic!("expected Resolved, got {other:?}"),
}
}
}