use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use anyhow::Context;
use serde::Serialize;
use crate::conformance::{self, Status};
use crate::dtoml;
use crate::entity::{
self, Artifact, Fileset, Inputs, Kind, LocalFs, MaterialiseRequest, ScaffoldCtx,
};
use crate::lifecycle::{Transition, classify, crosses_closure_seam};
use crate::listing::{self, Format, ListArgs};
use crate::meta::{self, Meta};
use crate::plan::Plan;
use crate::tomlfmt::toml_string;
use std::str::FromStr;
use clap::Subcommand;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, Serialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum SelectorIntent {
ScopeRelevant,
DesignTarget,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, Serialize)]
pub(crate) struct Selector {
#[expect(
clippy::struct_field_names,
reason = "`selector` is the canonical noun from RFC-004"
)]
pub(crate) selector: String,
pub(crate) intent: SelectorIntent,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub(crate) note: Option<String>,
}
#[derive(Subcommand)]
pub(crate) enum SelectorCommand {
Add {
id: u32,
#[arg(long, value_parser = clap::value_parser!(SelectorIntent))]
intent: SelectorIntent,
globs: Vec<String>,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Note {
id: u32,
selector: String,
text: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Rm {
id: u32,
globs: Vec<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
pub(crate) enum SliceCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Design {
#[arg(value_parser = parse_cli_id)]
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Plan {
#[arg(value_parser = parse_cli_id)]
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Phases {
#[arg(value_parser = parse_cli_id)]
id: u32,
#[arg(long)]
prune: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Notes {
#[arg(value_parser = parse_cli_id)]
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Phase {
#[arg(value_parser = parse_cli_id)]
id: u32,
phase_id: String,
#[arg(long)]
status: crate::state::PhaseStatus,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
#[arg(value_parser = parse_cli_id)]
id: u32,
state: SliceStatus,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: crate::CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Paths {
refs: Vec<String>,
#[arg(short = 't', long)]
toml: bool,
#[arg(short = 'm', long)]
md: bool,
#[arg(short = 'e', long)]
entity: bool,
#[arg(short = 's', long)]
single: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Selector {
#[command(subcommand)]
command: SelectorCommand,
},
Conformance {
#[arg(value_parser = parse_cli_id)]
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
RecordDelta {
#[arg(value_parser = parse_cli_id)]
id: u32,
phase: String,
#[arg(long)]
start: String,
#[arg(long)]
end: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
VerifyVt {
#[arg(value_parser = parse_cli_id)]
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
pub(crate) fn dispatch(cmd: SliceCommand, color: bool) -> anyhow::Result<()> {
match cmd {
SliceCommand::New { title, slug, path } => run_new(path, title, slug),
SliceCommand::Design { id, path } => run_design(path, id),
SliceCommand::Plan { id, path } => run_plan(path, id),
SliceCommand::Phases { id, prune, path } => run_phases(path, id, prune),
SliceCommand::Notes { id, path } => run_notes(path, id),
SliceCommand::Phase {
id,
phase_id,
status,
note,
path,
} => run_phase(path, id, &phase_id, status, note.as_deref()),
SliceCommand::Status {
id,
state,
note,
path,
} => run_status(path, id, state, note.as_deref()),
SliceCommand::List { list, path } => run_list(path, list.into_list_args(color)),
SliceCommand::Show {
reference,
format,
json,
path,
} => run_show(path, &reference, if json { Format::Json } else { format }),
SliceCommand::Paths {
refs,
toml,
md,
entity,
single,
path,
} => {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let sel = crate::paths::PathSelection {
toml,
md,
entity,
single,
};
let mut all_lines: Vec<String> = Vec::new();
for r in &refs {
let id = parse_ref(r)?;
let name = format!("{id:03}");
let entity_dir = slice_root.join(&name);
let toml_name = format!("slice-{name}.toml");
let md_name = format!("slice-{name}.md");
let set = crate::paths::scan_entity_dir(
&entity_dir,
&entity_dir.join(&toml_name),
Some(&entity_dir.join(&md_name)),
&root,
)?;
let lines = crate::paths::select_paths(&set, &sel)?;
all_lines.extend(lines);
}
write!(io::stdout(), "{}", all_lines.join("\n"))?;
Ok(())
}
SliceCommand::Selector { command } => dispatch_selector(command),
SliceCommand::Conformance { id, path } => run_conformance(path, id),
SliceCommand::RecordDelta {
id,
phase,
start,
end,
path,
} => run_record_delta(path, id, &phase, &start, &end),
SliceCommand::VerifyVt { id, path } => run_verify_vt(path, id),
}
}
const SLICE_DIR: &str = ".doctrine/slice";
pub(crate) const SLICE_KIND: Kind = Kind {
dir: SLICE_DIR,
prefix: crate::kinds::SL,
stem: "slice",
scaffold: slice_scaffold,
};
const DESIGN_KIND: Kind = Kind {
dir: SLICE_DIR,
prefix: crate::kinds::SL,
stem: "",
scaffold: design_scaffold,
};
const PLAN_KIND: Kind = Kind {
dir: SLICE_DIR,
prefix: crate::kinds::SL,
stem: "",
scaffold: plan_scaffold,
};
const NOTES_KIND: Kind = Kind {
dir: SLICE_DIR,
prefix: crate::kinds::SL,
stem: "",
scaffold: notes_scaffold,
};
fn render_toml(id: u32, slug: &str, title: &str, date: &str) -> anyhow::Result<String> {
Ok(crate::install::asset_text("templates/slice.toml")?
.replace("{{id}}", &id.to_string())
.replace("{{slug}}", &toml_string(slug))
.replace("{{title}}", &toml_string(title))
.replace("{{date}}", date))
}
fn render_md(title: &str) -> anyhow::Result<String> {
Ok(crate::install::asset_text("templates/slice.md")?.replace("{{title}}", title))
}
fn render_design(canonical_id: &str, title: &str) -> anyhow::Result<String> {
Ok(crate::install::asset_text("templates/design.md")?
.replace("{{ref}}", canonical_id)
.replace("{{title}}", title))
}
fn slice_scaffold(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let id = ctx.id;
let name = format!("{id:03}");
Ok(vec![
Artifact::File {
rel_path: entity::rel_path(&SLICE_KIND, id, entity::Ext::Toml),
body: render_toml(id, ctx.slug, ctx.title, ctx.date)?,
},
Artifact::File {
rel_path: entity::rel_path(&SLICE_KIND, id, entity::Ext::Md),
body: render_md(ctx.title)?,
},
Artifact::Symlink {
rel_path: PathBuf::from(format!("{name}-{}", ctx.slug)),
target: name,
},
])
}
fn design_scaffold(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let (id, canonical) = (ctx.id, ctx.canonical);
let name = format!("{id:03}");
Ok(vec![Artifact::File {
rel_path: PathBuf::from(format!("{name}/design.md")),
body: render_design(canonical, ctx.title)?,
}])
}
fn render_plan_toml(canonical_id: &str) -> anyhow::Result<String> {
Ok(crate::install::asset_text("templates/plan.toml")?.replace("{{ref}}", canonical_id))
}
fn render_plan_md(canonical_id: &str, title: &str) -> anyhow::Result<String> {
Ok(crate::install::asset_text("templates/plan.md")?
.replace("{{ref}}", canonical_id)
.replace("{{title}}", title))
}
fn plan_scaffold(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let (id, canonical) = (ctx.id, ctx.canonical);
let name = format!("{id:03}");
Ok(vec![
Artifact::File {
rel_path: PathBuf::from(format!("{name}/plan.toml")),
body: render_plan_toml(canonical)?,
},
Artifact::File {
rel_path: PathBuf::from(format!("{name}/plan.md")),
body: render_plan_md(canonical, ctx.title)?,
},
])
}
fn render_notes(canonical_id: &str, title: &str) -> anyhow::Result<String> {
Ok(crate::install::asset_text("templates/notes.md")?
.replace("{{ref}}", canonical_id)
.replace("{{title}}", title))
}
fn notes_scaffold(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let (id, canonical) = (ctx.id, ctx.canonical);
let name = format!("{id:03}");
Ok(vec![Artifact::File {
rel_path: PathBuf::from(format!("{name}/notes.md")),
body: render_notes(canonical, ctx.title)?,
}])
}
pub(crate) fn read_plan(slice_root: &Path, id: u32) -> anyhow::Result<Plan> {
let name = format!("{id:03}");
let path = slice_root.join(&name).join("plan.toml");
let text = fs::read_to_string(&path)
.with_context(|| format!("Plan for slice {name} not found at {}", path.display()))?;
Plan::parse(&text)
}
pub(crate) fn run_new(
path: Option<PathBuf>,
title: Option<String>,
slug: Option<String>,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let trunk_ids = crate::git::trunk_entity_ids(&root, SLICE_KIND.dir)?;
let (backend, mut reserved) =
crate::reserve::backend(&root, SLICE_KIND.prefix, crate::install::prompt_confirm)?;
let title = crate::input::resolve_title(title)?;
let slug = crate::input::resolve_slug(&title, slug)?;
let date = crate::clock::today();
let out = entity::materialise(
&SLICE_KIND,
&*backend,
&root,
&MaterialiseRequest::Fresh,
&Inputs {
slug: &slug,
title: &title,
date: &date,
},
&trunk_ids,
&mut reserved,
)?;
let id = out
.eid
.numeric_id()
.context("slice kind must yield a numeric id")?;
writeln!(io::stdout(), "Created slice {id:03}: {}", out.dir.display())?;
Ok(())
}
pub(crate) fn run_design(path: Option<PathBuf>, id: u32) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let meta = meta::read_meta(&slice_root, "slice", id, "SL")?;
let date = crate::clock::today();
let out = entity::materialise(
&DESIGN_KIND,
&LocalFs,
&root,
&MaterialiseRequest::InExisting { id },
&Inputs {
slug: "",
title: &meta.title,
date: &date,
},
&[], &mut entity::local_reserved(), )?;
writeln!(
io::stdout(),
"Created design doc: {}",
out.dir.join("design.md").display()
)?;
Ok(())
}
pub(crate) fn run_plan(path: Option<PathBuf>, id: u32) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let meta = meta::read_meta(&slice_root, "slice", id, "SL")?;
let date = crate::clock::today();
let out = entity::materialise(
&PLAN_KIND,
&LocalFs,
&root,
&MaterialiseRequest::InExisting { id },
&Inputs {
slug: "",
title: &meta.title,
date: &date,
},
&[], &mut entity::local_reserved(), )?;
writeln!(
io::stdout(),
"Created implementation plan: {}",
out.dir.join("plan.toml").display()
)?;
Ok(())
}
pub(crate) fn run_phases(path: Option<PathBuf>, id: u32, prune: bool) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let plan = read_plan(&slice_root, id)?;
let report = crate::state::init_phases(&root, id, &plan, prune)?;
let mut out = io::stdout();
for phase_id in &report.created {
writeln!(out, " materialised {phase_id}")?;
}
for phase_id in &report.orphan {
writeln!(
out,
" orphan {phase_id} (plan phase gone; --prune to remove)"
)?;
}
for phase_id in &report.pruned {
writeln!(out, " pruned {phase_id}")?;
}
if report.created.is_empty() && report.orphan.is_empty() && report.pruned.is_empty() {
writeln!(out, "Phases up to date.")?;
}
Ok(())
}
pub(crate) fn run_verify_vt(path: Option<PathBuf>, id: u32) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let plan = read_plan(&slice_root, id)?;
let read_file = |rel: &str| fs::read_to_string(root.join(rel)).ok();
let reports = crate::vtgate::check_phases(&plan, &read_file);
write!(io::stdout(), "{}", crate::vtgate::render_summary(&reports))?;
if crate::vtgate::has_failure(&reports) {
#[expect(
clippy::disallowed_methods,
reason = "verify-vt forwards its gate verdict as the terminal exit code (INV-4)"
)]
{
std::process::exit(1);
}
}
Ok(())
}
pub(crate) fn run_notes(path: Option<PathBuf>, id: u32) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let meta = meta::read_meta(&slice_root, "slice", id, "SL")?;
let date = crate::clock::today();
let out = entity::materialise(
&NOTES_KIND,
&LocalFs,
&root,
&MaterialiseRequest::InExisting { id },
&Inputs {
slug: "",
title: &meta.title,
date: &date,
},
&[], &mut entity::local_reserved(), )?;
writeln!(
io::stdout(),
"Created notes: {}",
out.dir.join("notes.md").display()
)?;
Ok(())
}
pub(crate) fn run_phase(
path: Option<PathBuf>,
id: u32,
phase_id: &str,
status: crate::state::PhaseStatus,
note: Option<&str>,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let now = crate::clock::now_timestamp()?;
crate::state::set_phase_status(&root, id, phase_id, status, note, &now)?;
writeln!(io::stdout(), "Updated {phase_id}: {}", status.as_str())?;
Ok(())
}
pub(crate) fn run_status(
path: Option<PathBuf>,
id: u32,
state: SliceStatus,
note: Option<&str>,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let from = read_status(&slice_root, id)?;
let to = state.as_str();
let kind = classify(&from, to);
if crosses_closure_seam(&from, to) {
let blockers = crate::review::unresolved_blockers_for(&root, &canonical_id(id))?;
if !blockers.is_empty() {
let listed = blockers
.iter()
.map(|b| format!("{}/{}", b.rv, b.finding))
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"slice {} → {to}: refused — unresolved blocker review finding(s): {listed} \
(resolve via `review verify`/`review withdraw`, then retry)",
canonical_id(id)
);
}
}
if from == "reconcile" && to == "done" {
let undischarged = undischarged_drift(&root, id)?;
if !undischarged.is_empty() {
anyhow::bail!(
"slice {} → {to}: refused — undischarged residual drift on \
requirement(s): {} (reconcile each via an `accept` REC whose evidence \
covers the current drift, or resolve the drift, then retry)",
canonical_id(id),
undischarged.join(", "),
);
}
}
if from == "reconcile" && to == "done" {
let deliver_to = crate::dtoml::load_doctrine_toml(&root)?.dispatch.deliver_to;
match crate::ledger::trunk_integration(&root, id, &deliver_to)? {
crate::ledger::TrunkIntegration::NotDispatched
| crate::ledger::TrunkIntegration::Integrated => {}
crate::ledger::TrunkIntegration::Blocked(reason) => anyhow::bail!(
"slice {} → {to}: refused — dispatched code not integrated to trunk: \
{reason} (run close step-3a `dispatch sync --integrate`, verify, retry)",
canonical_id(id)
),
}
}
set_slice_status(&slice_root, id, &from, state, &crate::clock::today())?;
let cfg = load_conduct(&root)?;
let posture = crate::conduct::resolve(&cfg, &from);
writeln!(
io::stdout(),
"{}",
status_line(&from, to, kind, posture, note)
)?;
Ok(())
}
fn load_conduct(root: &Path) -> anyhow::Result<crate::conduct::ConductConfig> {
Ok(crate::dtoml::load_doctrine_toml(root)?.conduct)
}
fn status_line(
from: &str,
to: &str,
kind: Transition,
posture: crate::conduct::Conduct,
note: Option<&str>,
) -> String {
let suffix = note.map(|n| format!(" — {n}")).unwrap_or_default();
format!(
"{from} → {to} [{}] [{}]{suffix}",
transition_label(kind),
posture.label()
)
}
fn transition_label(kind: Transition) -> &'static str {
match kind {
Transition::Advance => "advance",
Transition::BackEdge => "back-edge",
Transition::Skip => "skip",
Transition::Abandon => "abandon",
Transition::Noop => "no-op",
Transition::FromTerminal => "from-terminal",
Transition::SeamBreach => "seam-breach",
}
}
fn read_status(slice_root: &Path, id: u32) -> anyhow::Result<String> {
let name = format!("{id:03}");
let path = slice_root.join(&name).join(format!("slice-{name}.toml"));
let text = fs::read_to_string(&path)
.with_context(|| format!("slice {name} not found at {}", path.display()))?;
let doc = text
.parse::<toml_edit::DocumentMut>()
.with_context(|| format!("Failed to parse {}", path.display()))?;
doc.get("status")
.and_then(toml_edit::Item::as_str)
.map(str::to_string)
.with_context(|| format!("malformed slice {name}: missing `status`"))
}
fn set_slice_status(
slice_root: &Path,
id: u32,
from: &str,
state: SliceStatus,
today: &str,
) -> anyhow::Result<()> {
let to = state.as_str();
let name = format!("{id:03}");
match classify(from, to) {
Transition::FromTerminal => anyhow::bail!(
"slice {name}: refusing to leave terminal status `{from}` (reopening is deferred)"
),
Transition::SeamBreach => anyhow::bail!(
"slice {name}: `{to}` is reachable only across the closure seam \
(→ reconcile from audit, → done from reconcile), not from `{from}`"
),
_ => {}
}
let path = slice_root.join(&name).join(format!("slice-{name}.toml"));
let hint = format!(
"malformed slice {name}: missing `status`/`updated` — restore the missing keys and retry; the file is left untouched"
);
crate::dep_seq::set_authored_status(&path, &[("status", to), ("updated", today)], &hint)?;
Ok(())
}
pub(crate) const SLICE_STATUSES: &[&str] = &[
"proposed",
"design",
"plan",
"ready",
"started",
"audit",
"reconcile",
"done",
"abandoned",
];
fn is_hidden(status: &str) -> bool {
matches!(status, "done" | "abandoned")
}
fn is_drifted(status: &str) -> bool {
!SLICE_STATUSES.contains(&status)
}
fn is_terminal_status(authored: &str) -> bool {
authored == "done"
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum SliceStatus {
Proposed,
Design,
Plan,
Ready,
Started,
Audit,
Reconcile,
Done,
Abandoned,
}
impl SliceStatus {
fn as_str(self) -> &'static str {
match self {
Self::Proposed => "proposed",
Self::Design => "design",
Self::Plan => "plan",
Self::Ready => "ready",
Self::Started => "started",
Self::Audit => "audit",
Self::Reconcile => "reconcile",
Self::Done => "done",
Self::Abandoned => "abandoned",
}
}
}
fn is_divergent(authored: &str, rollup: Option<&crate::state::PhaseRollup>) -> bool {
let Some(r) = rollup else { return false };
if r.anomalies() > 0 {
return false;
}
let terminal = is_terminal_status(authored);
(terminal && r.completed < r.total())
|| (!terminal && r.total() > 0 && r.completed == r.total())
}
fn phases_cell(rollup: Option<&crate::state::PhaseRollup>) -> String {
let Some(r) = rollup else {
return "—".to_string();
};
let blocked = if r.blocked > 0 {
format!(" !{}", r.blocked)
} else {
String::new()
};
let anomalies = if r.anomalies() > 0 {
format!(" ?{}", r.anomalies())
} else {
String::new()
};
format!("{}/{}{blocked}{anomalies}", r.completed, r.total())
}
fn decorated_status(status: &str, rollup: Option<&crate::state::PhaseRollup>) -> String {
let drift = if is_drifted(status) { "?" } else { "" };
let divergence = if is_divergent(status, rollup) {
" ⚠"
} else {
""
};
format!("{status}{drift}{divergence}")
}
type SliceRowTuple = (Meta, Option<crate::state::PhaseRollup>);
const SLICE_COLUMNS: [listing::Column<SliceRowTuple>; 6] = [
listing::Column {
name: "id",
header: "id",
cell: |(m, _)| canonical_id(m.id),
paint: listing::ColumnPaint::Fixed(owo_colors::DynColors::Ansi(
owo_colors::AnsiColors::Cyan,
)),
},
listing::Column {
name: "status",
header: "status",
cell: |(m, r)| decorated_status(&m.status, r.as_ref()),
paint: listing::ColumnPaint::ByValue(|(m, _)| listing::status_hue(&m.status)),
},
listing::Column {
name: "phases",
header: "phases",
cell: |(_, r)| phases_cell(r.as_ref()),
paint: listing::ColumnPaint::None,
},
listing::Column {
name: "tags",
header: "tags",
cell: |(m, _)| m.tags.join(", "),
paint: listing::ColumnPaint::PerToken {
split: |(m, _)| m.tags.clone(),
render: listing::paint_tag,
},
},
listing::Column {
name: "slug",
header: "slug",
cell: |(m, _)| m.slug.clone(),
paint: listing::ColumnPaint::None,
},
listing::Column {
name: "title",
header: "title",
cell: |(m, _)| m.title.clone(),
paint: listing::ColumnPaint::Alternate([listing::TITLE_EVEN, listing::TITLE_ODD]),
},
];
const SLICE_DEFAULT: &[&str] = &["id", "status", "phases", "title"];
#[derive(Debug, Serialize)]
struct SliceRow {
id: String,
status: String,
slug: String,
title: String,
tags: Vec<String>,
phases: Option<PhasesJson>,
}
#[derive(Debug, Serialize)]
struct PhasesJson {
completed: u32,
total: u32,
blocked: u32,
}
fn key(m: &Meta) -> listing::FilterFields {
listing::FilterFields {
canonical: canonical_id(m.id),
slug: m.slug.clone(),
title: m.title.clone(),
status: m.status.clone(),
tags: m.tags.clone(),
}
}
fn canonical_id(id: u32) -> String {
listing::canonical_id(SLICE_KIND.prefix, id)
}
fn read_gate_extra_reqs(slice_root: &Path, id: u32) -> anyhow::Result<Vec<String>> {
let name = format!("{id:03}");
let path = slice_root.join(&name).join(format!("slice-{name}.toml"));
let text = fs::read_to_string(&path)
.with_context(|| format!("slice {name} not found at {}", path.display()))?;
let doc: SliceDoc =
toml::from_str(&text).with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(doc.gate.extra_reqs)
}
fn gate_requirement_set(
root: &Path,
id: u32,
owned_recs: &[crate::rec::RecDoc],
) -> anyhow::Result<Vec<String>> {
let canonical = canonical_id(id);
let slice_root = root.join(SLICE_DIR);
let mut set = std::collections::BTreeSet::new();
set.extend(crate::coverage_scan::slice_local_covered_reqs(
root, id, &canonical,
)?);
set.extend(read_gate_extra_reqs(&slice_root, id)?);
set.extend(
owned_recs
.iter()
.flat_map(|rec| rec.status_delta.iter().map(|d| d.requirement.clone())),
);
Ok(set.into_iter().collect())
}
fn undischarged_drift(root: &Path, id: u32) -> anyhow::Result<Vec<String>> {
let canonical = canonical_id(id);
let owned_recs = crate::rec::recs_owned_by(root, &canonical)?;
let mut undischarged = Vec::new();
for req in gate_requirement_set(root, id, &owned_recs)? {
let entries = crate::coverage_scan::scan_coverage(root, &req);
let composite = crate::coverage::composite(&entries);
let authored = crate::requirement::load(root, &req)
.with_context(|| format!("closure gate: requirement {req} not found"))?
.status;
if matches!(
crate::coverage::drift(authored, &composite),
crate::coverage::Verdict::Coherent
) {
continue;
}
let residual_keys = crate::coverage::distinct_keys(entries.into_iter().map(|(e, _)| e.key));
let latest = latest_owning_rec_for(&owned_recs, &req);
if !rec_discharges(latest, &req, authored, &residual_keys) {
undischarged.push(req);
}
}
Ok(undischarged)
}
fn latest_owning_rec_for<'a>(
owned_recs: &'a [crate::rec::RecDoc],
req: &str,
) -> Option<&'a crate::rec::RecDoc> {
owned_recs
.iter()
.filter(|rec| rec.status_delta.iter().any(|d| d.requirement == req))
.max_by_key(|rec| rec.id)
}
fn rec_discharges(
latest: Option<&crate::rec::RecDoc>,
req: &str,
authored: crate::requirement::ReqStatus,
residual_keys: &[crate::coverage::CoverageKey],
) -> bool {
let Some(rec) = latest else { return false };
if rec.rec.r#move != "accept" {
return false;
}
let affirmed_at_current = rec
.status_delta
.iter()
.any(|d| d.requirement == req && d.to == authored.as_str());
if !affirmed_at_current {
return false;
}
residual_keys
.iter()
.all(|k| rec.evidence_ref.iter().any(|e| e == k))
}
fn validate_statuses(given: &[String], known: &[&str]) -> anyhow::Result<()> {
listing::validate_statuses(given, known)
}
pub(crate) fn list_rows(root: &Path, mut args: ListArgs) -> anyhow::Result<String> {
validate_statuses(&args.status, SLICE_STATUSES)?;
let render = args.render;
let columns = args.columns.take();
let (filter, format) = listing::build(args)?;
let slice_root = root.join(SLICE_DIR);
let mut metas = listing::retain(
meta::read_metas(&slice_root, "slice", "SL")?,
&filter,
is_hidden,
key,
);
metas.sort_by_key(|m| m.id);
let rows: Vec<(Meta, Option<crate::state::PhaseRollup>)> = metas
.into_iter()
.map(|m| {
let rollup = crate::state::phase_rollup(root, m.id)?;
Ok((m, rollup))
})
.collect::<anyhow::Result<_>>()?;
match format {
Format::Table => {
let any_tagged = rows.iter().any(|(m, _)| !m.tags.is_empty());
let effective_default = listing::default_with_tags(SLICE_DEFAULT, any_tagged);
let sel =
listing::select_columns(&SLICE_COLUMNS, &effective_default, columns.as_deref())?;
Ok(listing::render_columns(&rows, &sel, render))
}
Format::Json => listing::json_envelope("slice", &json_rows(&rows)),
}
}
fn json_rows(rows: &[(Meta, Option<crate::state::PhaseRollup>)]) -> Vec<SliceRow> {
rows.iter()
.map(|(m, rollup)| SliceRow {
id: canonical_id(m.id),
status: m.status.clone(),
slug: m.slug.clone(),
title: m.title.clone(),
tags: m.tags.clone(),
phases: rollup.as_ref().map(|r| PhasesJson {
completed: r.completed,
total: r.total(),
blocked: r.blocked,
}),
})
.collect()
}
pub(crate) fn run_list(path: Option<PathBuf>, args: ListArgs) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let mut out = io::stdout();
write!(out, "{}", list_rows(&root, args)?)?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize, Serialize)]
struct SliceDoc {
id: u32,
slug: String,
title: String,
status: String,
created: String,
updated: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
gate: Gate,
#[serde(default)]
estimate: Option<crate::estimate::EstimateFacet>,
#[serde(default)]
value: Option<crate::value::ValueFacet>,
#[serde(default, rename = "selector")]
selectors: Vec<Selector>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Deserialize, Serialize)]
struct Gate {
#[serde(default)]
extra_reqs: Vec<String>,
}
pub(crate) fn parse_ref(reference: &str) -> anyhow::Result<u32> {
crate::governance::parse_entity_ref("SL", "a slice", reference)
}
fn parse_cli_id(s: &str) -> Result<u32, String> {
parse_ref(s).map_err(|e| {
format!("{e:#}")
})
}
pub(crate) fn run_show(
path: Option<PathBuf>,
reference: &str,
format: Format,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let id = parse_ref(reference)?;
let (doc, toml_text, body) = read_slice(&root.join(SLICE_DIR), id)?;
let tier1 = crate::relation::tier1_edges(&SLICE_KIND, &toml_text)?;
let dep_seq = crate::dep_seq::read(&slice_toml_path(&root.join(SLICE_DIR), id))?;
let out = match format {
Format::Table => {
let cfg = crate::dtoml::load_doctrine_toml(&root)?;
let posture = crate::conduct::resolve(&cfg.conduct, &doc.status);
let estimation_unit = crate::estimate::resolve_unit(&cfg.estimation);
let value_unit = crate::value::resolve_unit(&cfg.value);
let (lower_pct, upper_pct) = crate::estimate::resolve_confidence(&cfg.estimation)?;
let facets = crate::facet::EntityFacets {
estimate: doc.estimate.clone(),
value: doc.value.clone(),
risk: None,
tags: doc.tags.clone(),
};
format_show(
&doc,
&tier1,
&dep_seq,
&body,
posture,
&facets,
&estimation_unit,
&value_unit,
lower_pct,
upper_pct,
)
}
Format::Json => show_json(&doc, &tier1, &dep_seq, &body)?,
};
write!(io::stdout(), "{out}")?;
Ok(())
}
fn slice_toml_path(slice_root: &Path, id: u32) -> PathBuf {
let name = format!("{id:03}");
slice_root.join(&name).join(format!("slice-{name}.toml"))
}
fn read_slice(slice_root: &Path, id: u32) -> anyhow::Result<(SliceDoc, String, String)> {
let name = format!("{id:03}");
let dir = slice_root.join(&name);
let toml_path = slice_toml_path(slice_root, id);
let text = fs::read_to_string(&toml_path)
.with_context(|| format!("slice {name} not found at {}", toml_path.display()))?;
let doc: SliceDoc = dtoml::parse_entity_toml(&text, "SL", id)
.with_context(|| format!("Failed to parse {}", toml_path.display()))?;
let md_path = dir.join(format!("slice-{name}.md"));
let body = fs::read_to_string(&md_path)
.with_context(|| format!("Failed to read {}", md_path.display()))?;
Ok((doc, text, body))
}
pub(crate) fn relation_edges(
root: &Path,
id: u32,
) -> anyhow::Result<Vec<crate::relation::RelationEdge>> {
let (_doc, toml_text, _body) = read_slice(&root.join(SLICE_DIR), id)?;
crate::relation::tier1_edges(&SLICE_KIND, &toml_text)
}
pub(crate) fn selector_paths(root: &Path, id: u32) -> anyhow::Result<Vec<String>> {
let (doc, _toml, _body) = read_slice(&root.join(SLICE_DIR), id)?;
let mut set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for sel in &doc.selectors {
set.insert(sel.selector.clone());
}
Ok(set.into_iter().collect())
}
#[expect(
clippy::too_many_arguments,
reason = "format_show consolidates all rendering inputs; splitting into a struct adds indirection without reducing coupling"
)]
fn format_show(
doc: &SliceDoc,
tier1: &[crate::relation::RelationEdge],
dep_seq: &crate::dep_seq::DepSeq,
body: &str,
posture: crate::conduct::Conduct,
facets: &crate::facet::EntityFacets,
estimation_unit: &str,
value_unit: &str,
lower_pct: f64,
upper_pct: f64,
) -> String {
use crate::relation::{RelationLabel, Role, targets_for, targets_for_role};
let mut parts: Vec<String> = Vec::new();
parts.push(format!("{} — {}\n", canonical_id(doc.id), doc.title));
parts.push(format!("{} · {}\n", doc.slug, doc.status));
parts.push(format!("conduct: {}\n", posture.label()));
if !doc.tags.is_empty() {
parts.push(format!("tags: {}\n", doc.tags.join(", ")));
}
parts.push(format!(
"created {} · updated {}\n",
doc.created, doc.updated
));
if let Some(ref est) = facets.estimate {
parts.push(format!(
"{}\n",
crate::estimate::display::format_estimate_confidence(
est,
lower_pct,
upper_pct,
estimation_unit,
)
));
}
if let Some(ref val) = facets.value {
parts.push(format!(
"{}\n",
crate::value::format_value_normal(val, value_unit)
));
}
let axes = [
("supersedes", targets_for(tier1, RelationLabel::Supersedes)),
("governed_by", targets_for(tier1, RelationLabel::GovernedBy)),
(
"references(implements)",
targets_for_role(tier1, RelationLabel::References, Role::Implements),
),
(
"references(originates_from)",
targets_for_role(tier1, RelationLabel::References, Role::OriginatesFrom),
),
(
"references(concerns)",
targets_for_role(tier1, RelationLabel::References, Role::Concerns),
),
];
let after_line = dep_seq
.after
.iter()
.map(|e| {
if e.rank == 0 {
e.to.clone()
} else {
format!("{} (rank {})", e.to, e.rank)
}
})
.collect::<Vec<_>>();
let any_tier1 = axes.iter().any(|(_, refs)| !refs.is_empty());
let any_dep_seq = !dep_seq.needs.is_empty() || !after_line.is_empty();
if any_tier1 || any_dep_seq {
parts.push("\nrelationships:\n".to_string());
for (label, refs) in &axes {
if !refs.is_empty() {
parts.push(format!(" {label}: {}\n", refs.join(", ")));
}
}
if !dep_seq.needs.is_empty() {
parts.push(format!(" needs: {}\n", dep_seq.needs.join(", ")));
}
if !after_line.is_empty() {
parts.push(format!(" after: {}\n", after_line.join(", ")));
}
}
parts.push(format!("\n{body}"));
parts.concat()
}
fn show_json(
doc: &SliceDoc,
tier1: &[crate::relation::RelationEdge],
dep_seq: &crate::dep_seq::DepSeq,
body: &str,
) -> anyhow::Result<String> {
use crate::relation::{RelationLabel, Role, targets_for, targets_for_role};
let mut relationships = serde_json::Map::new();
relationships.insert(
"supersedes".to_string(),
serde_json::json!(targets_for(tier1, RelationLabel::Supersedes)),
);
let governed_by = targets_for(tier1, RelationLabel::GovernedBy);
if !governed_by.is_empty() {
relationships.insert("governed_by".to_string(), serde_json::json!(governed_by));
}
relationships.insert(
"references".to_string(),
serde_json::json!({
"implements": targets_for_role(tier1, RelationLabel::References, Role::Implements),
"originates_from": targets_for_role(tier1, RelationLabel::References, Role::OriginatesFrom),
"concerns": targets_for_role(tier1, RelationLabel::References, Role::Concerns),
}),
);
if !dep_seq.needs.is_empty() {
relationships.insert("needs".to_string(), serde_json::json!(dep_seq.needs));
}
if !dep_seq.after.is_empty() {
relationships.insert("after".to_string(), serde_json::json!(dep_seq.after));
}
let mut slice = serde_json::to_value(doc).context("failed to serialize slice doc")?;
if let Some(obj) = slice.as_object_mut() {
obj.insert(
"relationships".to_string(),
serde_json::Value::Object(relationships),
);
}
let value = serde_json::json!({ "kind": "slice", "slice": slice, "body": body });
serde_json::to_string_pretty(&value).context("failed to serialize slice show JSON")
}
fn dispatch_selector(cmd: SelectorCommand) -> anyhow::Result<()> {
match cmd {
SelectorCommand::Add {
id,
intent,
globs,
note,
path,
} => run_selector_add(path, id, intent, &globs, note.as_deref()),
SelectorCommand::Note {
id,
selector,
text,
path,
} => run_selector_note(path, id, &selector, &text),
SelectorCommand::List { id, path } => run_selector_list(path, id),
SelectorCommand::Rm { id, globs, path } => run_selector_rm(path, id, &globs),
}
}
fn open_selector_doc(root: &Path, id: u32) -> anyhow::Result<(PathBuf, toml_edit::DocumentMut)> {
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, id);
let text = fs::read_to_string(&toml_path)
.with_context(|| format!("slice {} not found at {}", id, toml_path.display()))?;
let doc = text
.parse::<toml_edit::DocumentMut>()
.with_context(|| format!("Failed to parse {}", toml_path.display()))?;
Ok((toml_path, doc))
}
fn run_selector_add(
path: Option<PathBuf>,
id: u32,
intent: SelectorIntent,
globs: &[String],
note: Option<&str>,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let (toml_path, mut doc) = open_selector_doc(&root, id)?;
let intent_str = serde_rename_kebab(intent);
for glob in globs {
selector_upsert(&mut doc, glob, intent_str, note)?;
}
crate::fsutil::write_atomic(&toml_path, doc.to_string().as_bytes())?;
let cid = canonical_id(id);
writeln!(
io::stdout(),
"{cid}: upserted {} selector(s)\n",
globs.len()
)?;
Ok(())
}
fn run_selector_note(
path: Option<PathBuf>,
id: u32,
selector: &str,
text: &str,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let (toml_path, mut doc) = open_selector_doc(&root, id)?;
let found = selector_set_note(&mut doc, selector, text);
if !found {
let cid = canonical_id(id);
anyhow::bail!("{cid}: no selector `{selector}` — add it first");
}
crate::fsutil::write_atomic(&toml_path, doc.to_string().as_bytes())?;
writeln!(io::stdout(), "{selector}: note set")?;
Ok(())
}
fn run_selector_list(path: Option<PathBuf>, id: u32) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let slice_root = root.join(SLICE_DIR);
let (doc, _toml_text, _body) = read_slice(&slice_root, id)?;
if doc.selectors.is_empty() {
writeln!(io::stdout(), "(no selectors)")?;
return Ok(());
}
for s in &doc.selectors {
let note = s.note.as_deref().unwrap_or("-");
writeln!(
io::stdout(),
"{} {: <16} {}",
s.selector,
serde_rename_kebab(s.intent),
note
)?;
}
Ok(())
}
fn run_selector_rm(path: Option<PathBuf>, id: u32, globs: &[String]) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let (toml_path, mut doc) = open_selector_doc(&root, id)?;
let removed = selector_remove_many(&mut doc, globs);
if removed > 0 {
crate::fsutil::write_atomic(&toml_path, doc.to_string().as_bytes())?;
}
writeln!(io::stdout(), "Removed {removed} selector(s)")?;
Ok(())
}
fn fold_name_status_line(line: &str, actual: &mut std::collections::BTreeMap<String, Vec<Status>>) {
let mut cols = line.split('\t');
let Some(code) = cols.next() else { return };
let letter = code.chars().next().unwrap_or(' ');
match letter {
'A' | 'M' | 'D' => {
if let Some(p) = cols.next() {
let st = match letter {
'A' => Status::Added,
'D' => Status::Deleted,
_ => Status::Modified,
};
actual.entry(p.to_string()).or_default().push(st);
}
}
'R' | 'C' => {
let src = cols.next();
if let Some(dst) = cols.next() {
if letter == 'R'
&& let Some(src) = src
{
actual
.entry(src.to_string())
.or_default()
.push(Status::Deleted);
}
actual
.entry(dst.to_string())
.or_default()
.push(Status::Added);
}
}
_ => {}
}
}
#[derive(Debug)]
enum ConformanceOutcome {
Unavailable,
Incomplete(Vec<crate::state::CompletenessGap>),
Computed(conformance::Conformance),
}
fn conformance_outcome(root: &Path, id: u32) -> anyhow::Result<ConformanceOutcome> {
let slice_root = root.join(SLICE_DIR);
let (doc, _toml, _body) = read_slice(&slice_root, id)?;
let selectors: Vec<String> = doc
.selectors
.iter()
.filter(|s| s.intent == SelectorIntent::DesignTarget)
.map(|s| s.selector.clone())
.collect();
let rows = crate::state::read_source_deltas(root, id)?;
if rows.is_empty() {
return Ok(ConformanceOutcome::Unavailable);
}
if let crate::state::Completeness::Incomplete { gaps } =
crate::state::registry_completeness(root, root, id)?
{
return Ok(ConformanceOutcome::Incomplete(gaps));
}
let mut actual: std::collections::BTreeMap<String, Vec<Status>> =
std::collections::BTreeMap::new();
for row in &rows {
let diff = crate::git::git_text(
root,
&[
"diff",
"--name-status",
&format!("{}..{}", row.code_start_oid, row.code_end_oid),
],
)?;
for line in diff.lines().filter(|l| !l.trim().is_empty()) {
fold_name_status_line(line, &mut actual);
}
}
Ok(ConformanceOutcome::Computed(conformance::compute(
&selectors, &actual,
)))
}
fn run_conformance(path: Option<PathBuf>, id: u32) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let cid = canonical_id(id);
let mut out = io::stdout();
match conformance_outcome(&root, id)? {
ConformanceOutcome::Unavailable => {
writeln!(
out,
"{cid}: conformance unavailable — no recorded source deltas"
)?;
Ok(())
}
ConformanceOutcome::Incomplete(gaps) => {
writeln!(out, "{cid}: conformance incomplete — partial coverage")?;
for gap in &gaps {
writeln!(out, " - {}", gap.describe())?;
}
Ok(())
}
ConformanceOutcome::Computed(result) => render_conformance(&cid, &result),
}
}
fn run_record_delta(
path: Option<PathBuf>,
id: u32,
phase: &str,
start: &str,
end: &str,
) -> anyhow::Result<()> {
let root = crate::root::find(path, &crate::root::default_markers())?;
let resolve = |refish: &str| -> anyhow::Result<String> {
crate::git::resolve_ref(&root, refish)?
.with_context(|| format!("record-delta: {refish} does not resolve to a commit"))
};
crate::state::record_source_delta(
&root,
id,
crate::boundary::BoundaryRow {
phase: phase.to_string(),
code_start_oid: resolve(start)?,
code_end_oid: resolve(end)?,
provenance: crate::boundary::Provenance::Manual,
},
)?;
writeln!(
io::stdout(),
"{}: recorded source delta for {phase}",
canonical_id(id)
)?;
Ok(())
}
fn render_conformance(cid: &str, result: &conformance::Conformance) -> anyhow::Result<()> {
let mut out = io::stdout();
writeln!(out, "{cid}: conformance")?;
writeln!(out, "undeclared ({}):", result.undeclared.len())?;
for u in &result.undeclared {
writeln!(out, " {} {}", u.verb.marker(), u.path)?;
}
writeln!(out, "undelivered ({}):", result.undelivered.len())?;
for sel in &result.undelivered {
writeln!(out, " {sel}")?;
}
writeln!(out, "conformant ({}):", result.conformant.len())?;
for c in &result.conformant {
writeln!(out, " {} ⟵ {}", c.path, c.matched_selector)?;
}
Ok(())
}
fn serde_rename_kebab(intent: SelectorIntent) -> &'static str {
match intent {
SelectorIntent::ScopeRelevant => "scope-relevant",
SelectorIntent::DesignTarget => "design-target",
}
}
fn selector_upsert(
doc: &mut toml_edit::DocumentMut,
glob: &str,
intent: &str,
note: Option<&str>,
) -> anyhow::Result<()> {
let array = doc
.as_table_mut()
.entry("selector")
.or_insert_with(|| toml_edit::Item::ArrayOfTables(toml_edit::ArrayOfTables::new()))
.as_array_of_tables_mut()
.ok_or_else(|| {
anyhow::anyhow!("`selector` key exists but is not an array-of-tables (corrupt file)")
})?;
for row in array.iter_mut() {
if row
.get("selector")
.and_then(toml_edit::Item::as_str)
.is_some_and(|s| s == glob)
{
row.insert("intent", toml_edit::value(intent));
if let Some(n) = note {
row.insert("note", toml_edit::value(n));
}
return Ok(());
}
}
let mut row = toml_edit::Table::new();
row.insert("selector", toml_edit::value(glob));
row.insert("intent", toml_edit::value(intent));
if let Some(n) = note {
row.insert("note", toml_edit::value(n));
}
array.push(row);
Ok(())
}
fn selector_set_note(doc: &mut toml_edit::DocumentMut, selector: &str, text: &str) -> bool {
let Some(array) = doc
.as_table_mut()
.get_mut("selector")
.and_then(toml_edit::Item::as_array_of_tables_mut)
else {
return false;
};
for row in array.iter_mut() {
if row
.get("selector")
.and_then(toml_edit::Item::as_str)
.is_some_and(|s| s == selector)
{
row.insert("note", toml_edit::value(text));
return true;
}
}
false
}
fn selector_remove_many(doc: &mut toml_edit::DocumentMut, globs: &[String]) -> usize {
let Some(array) = doc
.as_table_mut()
.get_mut("selector")
.and_then(toml_edit::Item::as_array_of_tables_mut)
else {
return 0;
};
let before = array.len();
array.retain(|row| {
let s = row
.get("selector")
.and_then(toml_edit::Item::as_str)
.unwrap_or("");
!globs.iter().any(|g| g == s)
});
before - array.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lifecycle::is_transition_terminal;
use crate::meta::Meta;
use crate::test_support::SCHEMA_PLAN_OVERVIEW;
fn meta(id: u32, status: &str, slug: &str, title: &str) -> Meta {
Meta {
id,
slug: slug.to_string(),
title: title.to_string(),
status: status.to_string(),
tags: Vec::new(),
}
}
use crate::state::PhaseRollup;
fn rollup(completed: u32, planned: u32) -> PhaseRollup {
PhaseRollup {
completed,
planned,
..Default::default()
}
}
#[test]
fn divergence_flags_the_two_unambiguous_mismatches() {
assert!(is_divergent("done", Some(&rollup(2, 4))));
assert!(is_divergent("proposed", Some(&rollup(6, 0))));
}
#[test]
fn divergence_is_quiet_when_consistent_untracked_or_anomalous() {
assert!(!is_divergent("done", Some(&rollup(6, 0))));
assert!(!is_divergent("proposed", Some(&rollup(2, 4))));
assert!(!is_divergent("done", None));
let anomalous = PhaseRollup {
completed: 2,
planned: 3,
unknown: 1,
..Default::default()
};
assert!(!is_divergent("done", Some(&anomalous)));
}
#[test]
fn phases_cell_renders_markers() {
assert_eq!(phases_cell(None), "—");
assert_eq!(phases_cell(Some(&rollup(4, 2))), "4/6");
let blocked = PhaseRollup {
completed: 2,
planned: 3,
blocked: 1,
..Default::default()
};
assert_eq!(phases_cell(Some(&blocked)), "2/6 !1");
let anomalous = PhaseRollup {
completed: 3,
planned: 2,
unknown: 1,
..Default::default()
};
assert_eq!(phases_cell(Some(&anomalous)), "3/6 ?1");
}
fn render_default(rows: &[SliceRowTuple]) -> String {
let sel = listing::select_columns(&SLICE_COLUMNS, SLICE_DEFAULT, None).unwrap();
listing::render_columns(rows, &sel, listing::RenderOpts::default())
}
fn render_cols(rows: &[SliceRowTuple], cols: &[&str]) -> String {
let owned: Vec<String> = cols.iter().map(|s| (*s).to_string()).collect();
let sel = listing::select_columns(&SLICE_COLUMNS, SLICE_DEFAULT, Some(&owned)).unwrap();
listing::render_columns(rows, &sel, listing::RenderOpts::default())
}
#[test]
fn slice_list_empty_suppresses_the_header() {
assert_eq!(render_default(&[]), "");
}
#[test]
fn slice_list_default_renders_prefixed_ids_rollup_and_divergence() {
let rows = vec![
(
meta(1, "done", "entity-v1", "Entity v1"),
Some(rollup(6, 0)),
),
(
meta(7, "done", "anchoring", "Anchoring"),
Some(rollup(2, 4)),
),
(meta(9, "proposed", "rollup", "Rollup"), None),
];
let out = render_default(&rows);
let lines: Vec<&str> = out.lines().collect();
assert!(lines[0].starts_with("id"), "header: {:?}", lines[0]);
assert!(lines[0].contains("phases"), "phases column: {:?}", lines[0]);
assert!(lines[1].starts_with("SL-001 │ done"), "{:?}", lines[1]);
assert!(lines[1].contains("6/6"));
assert!(lines[2].starts_with("SL-007 │ done ⚠"), "{:?}", lines[2]);
assert!(lines[2].contains("2/6"));
assert!(lines[3].starts_with("SL-009 │ proposed"), "{:?}", lines[3]);
assert!(lines[3].contains("—"));
assert!(!out.contains("\n001 "), "no bare numeric id: {out}");
}
#[test]
fn slice_list_default_omits_slug() {
let rows = vec![(meta(1, "proposed", "entity-v1", "Entity v1"), None)];
let out = render_default(&rows);
let header = out.lines().next().unwrap();
assert!(
!header.contains("slug"),
"default header omits slug: {header:?}"
);
assert!(
!out.contains("entity-v1"),
"slug value hidden by default: {out}"
);
assert!(header.contains("title"), "default keeps title: {header:?}");
}
#[test]
fn slice_list_columns_reveals_slug_and_preserves_markers() {
let rows = vec![(
meta(7, "done", "anchoring", "Anchoring"),
Some(rollup(2, 4)),
)];
let out = render_cols(&rows, &["id", "status", "phases", "slug"]);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines[0].split_whitespace().collect::<Vec<_>>(),
vec!["id", "│", "status", "│", "phases", "│", "slug"]
);
assert!(
out.contains("anchoring"),
"slug revealed by --columns: {out}"
);
assert!(
lines[1].contains("done ⚠"),
"⚠ marker preserved: {:?}",
lines[1]
);
assert!(
lines[1].contains("2/6"),
"phases cell intact: {:?}",
lines[1]
);
}
#[test]
fn decorated_status_composes_drift_and_divergence() {
assert_eq!(decorated_status("proposed", None), "proposed");
assert_eq!(decorated_status("done", Some(&rollup(2, 4))), "done ⚠");
assert_eq!(decorated_status("bogus", None), "bogus?");
assert_eq!(decorated_status("bogus", Some(&rollup(6, 0))), "bogus? ⚠");
assert_eq!(
decorated_status("abandoned", Some(&rollup(2, 4))),
"abandoned"
);
}
#[test]
fn is_drifted_flags_only_out_of_vocab() {
for s in SLICE_STATUSES {
assert!(!is_drifted(s), "in-vocab `{s}` is not drift");
}
assert!(is_drifted("bogus"));
assert!(is_drifted("superseded")); }
#[test]
fn is_hidden_is_the_terminal_presentation_set_not_divergence() {
assert!(is_hidden("done"));
assert!(is_hidden("abandoned"));
assert!(!is_hidden("proposed"));
assert!(!is_hidden("bogus"));
assert!(is_terminal_status("done"));
assert!(!is_terminal_status("abandoned"));
}
fn make_slice(root: &Path, slug: &str, title: &str, date: &str) -> entity::Materialised {
entity::materialise(
&SLICE_KIND,
&LocalFs,
root,
&MaterialiseRequest::Fresh,
&Inputs { slug, title, date },
&[],
&mut entity::local_reserved(),
)
.unwrap()
}
#[test]
fn render_toml_round_trips_to_metadata() {
let body = render_toml(7, "my-slug", "My Title", "2026-06-03").unwrap();
let parsed: Meta = toml::from_str(&body).unwrap();
assert_eq!(parsed, meta(7, "proposed", "my-slug", "My Title"));
assert!(body.contains("created = \"2026-06-03\""));
}
#[test]
fn render_toml_escapes_hostile_title_and_slug() {
let title = crate::tomlfmt::HOSTILE_TITLE;
let slug = crate::tomlfmt::HOSTILE_SLUG;
let body = render_toml(7, slug, title, "2026-06-03").unwrap();
let parsed: Meta = toml::from_str(&body).unwrap();
assert_eq!(parsed.slug, slug);
assert_eq!(parsed.title, title);
}
#[test]
fn render_md_substitutes_title() {
let body = render_md("My Title").unwrap();
assert!(body.contains("My Title"));
assert!(!body.contains("{{title}}"));
}
#[test]
fn render_design_substitutes_ref_and_title() {
let body = render_design("SL-003", "My Title").unwrap();
assert!(body.contains("Design SL-003: My Title"));
assert!(!body.contains("{{ref}}"));
assert!(!body.contains("{{title}}"));
}
#[test]
fn slice_scaffold_lays_out_two_files_and_a_symlink() {
let ctx = ScaffoldCtx {
id: 3,
canonical: "SL-003",
slug: "vendor-skills",
title: "Vendor skills",
date: "2026-06-03",
};
let fileset = slice_scaffold(&ctx).unwrap();
assert_eq!(fileset.len(), 3);
assert!(matches!(&fileset[0],
Artifact::File { rel_path, body }
if rel_path == Path::new("003/slice-003.toml") && body.contains("2026-06-03")));
assert!(matches!(&fileset[1],
Artifact::File { rel_path, body }
if rel_path == Path::new("003/slice-003.md") && body.contains("Vendor skills")));
assert!(matches!(&fileset[2],
Artifact::Symlink { rel_path, target }
if rel_path == Path::new("003-vendor-skills") && target == "003"));
}
#[test]
fn render_plan_toml_substitutes_ref_and_parses() {
let body = render_plan_toml("SL-004").unwrap();
assert!(body.contains("slice = \"SL-004\""));
assert!(!body.contains("{{ref}}"));
let doc: toml::Value = toml::from_str(&body).unwrap();
assert_eq!(doc["schema"].as_str(), Some(SCHEMA_PLAN_OVERVIEW));
assert_eq!(doc["version"].as_integer(), Some(1));
assert_eq!(doc["phase"][0]["id"].as_str(), Some("PHASE-01"));
}
#[test]
fn render_plan_md_substitutes_ref_and_title() {
let body = render_plan_md("SL-004", "My Title").unwrap();
assert!(body.contains("Implementation Plan SL-004: My Title"));
assert!(!body.contains("{{ref}}"));
assert!(!body.contains("{{title}}"));
}
#[test]
fn plan_scaffold_lays_out_toml_and_md() {
let ctx = ScaffoldCtx {
id: 4,
canonical: "SL-004",
slug: "",
title: "Plan title",
date: "2026-06-04",
};
let fileset = plan_scaffold(&ctx).unwrap();
assert_eq!(fileset.len(), 2);
assert!(matches!(&fileset[0],
Artifact::File { rel_path, body }
if rel_path == Path::new("004/plan.toml") && body.contains("SL-004")));
assert!(matches!(&fileset[1],
Artifact::File { rel_path, body }
if rel_path == Path::new("004/plan.md") && body.contains("Plan title")));
}
#[test]
fn plan_parse_accepts_the_scaffold_template() {
let body = render_plan_toml("SL-004").unwrap();
let plan = Plan::parse(&body).unwrap();
assert_eq!(plan.phases.len(), 1);
assert_eq!(plan.phases[0].id, "PHASE-01");
}
#[test]
fn design_scaffold_is_a_single_file_no_symlink() {
let ctx = ScaffoldCtx {
id: 3,
canonical: "SL-003",
slug: "",
title: "Vendor skills",
date: "2026-06-03",
};
let fileset = design_scaffold(&ctx).unwrap();
assert_eq!(fileset.len(), 1);
assert!(matches!(&fileset[0],
Artifact::File { rel_path, body }
if rel_path == Path::new("003/design.md") && body.contains("Design SL-003: Vendor skills")));
}
#[test]
fn materialise_writes_well_formed_slice() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let s = make_slice(root, "my-slug", "My Title", "2026-06-03");
let slice_root = root.join(SLICE_DIR);
assert_eq!(s.eid.numeric_id(), Some(1));
assert!(slice_root.join("001").is_dir());
assert!(slice_root.join("001/slice-001.toml").is_file());
assert!(slice_root.join("001/slice-001.md").is_file());
assert_eq!(
fs::read_link(slice_root.join("001-my-slug")).unwrap(),
Path::new("001")
);
let toml_body = fs::read_to_string(slice_root.join("001/slice-001.toml")).unwrap();
assert!(toml_body.contains("id = 1"));
assert!(toml_body.contains("2026-06-03"));
}
#[test]
fn meta_read_metas_round_trips_a_created_slice() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-03");
let metas = meta::read_metas(&root.join(SLICE_DIR), "slice", "SL").unwrap();
assert_eq!(metas, vec![meta(1, "proposed", "my-slug", "My Title")]);
}
#[test]
fn design_materialises_under_an_existing_slice_with_no_symlink() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-03");
let slice_root = root.join(SLICE_DIR);
let out = entity::materialise(
&DESIGN_KIND,
&LocalFs,
root,
&MaterialiseRequest::InExisting { id: 1 },
&Inputs {
slug: "",
title: "My Title",
date: "2026-06-03",
},
&[],
&mut entity::local_reserved(),
)
.unwrap();
assert_eq!(out.eid.numeric_id(), Some(1));
let body = fs::read_to_string(slice_root.join("001/design.md")).unwrap();
assert!(body.contains("Design SL-001: My Title"));
assert!(!slice_root.join("002").exists());
}
#[test]
fn design_refuses_to_clobber_an_existing_doc() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-03");
let slice_root = root.join(SLICE_DIR);
fs::write(slice_root.join("001/design.md"), "hand-written").unwrap();
let err = entity::materialise(
&DESIGN_KIND,
&LocalFs,
root,
&MaterialiseRequest::InExisting { id: 1 },
&Inputs {
slug: "",
title: "My Title",
date: "2026-06-03",
},
&[],
&mut entity::local_reserved(),
)
.unwrap_err();
assert!(err.to_string().contains("Refusing to overwrite"));
assert_eq!(
fs::read_to_string(slice_root.join("001/design.md")).unwrap(),
"hand-written"
);
}
#[test]
fn plan_materialises_two_files_under_an_existing_slice() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let slice_root = root.join(SLICE_DIR);
let out = entity::materialise(
&PLAN_KIND,
&LocalFs,
root,
&MaterialiseRequest::InExisting { id: 1 },
&Inputs {
slug: "",
title: "My Title",
date: "2026-06-04",
},
&[],
&mut entity::local_reserved(),
)
.unwrap();
assert_eq!(out.eid.numeric_id(), Some(1));
let toml_body = fs::read_to_string(slice_root.join("001/plan.toml")).unwrap();
assert!(toml_body.contains("slice = \"SL-001\""));
let md_body = fs::read_to_string(slice_root.join("001/plan.md")).unwrap();
assert!(md_body.contains("Implementation Plan SL-001: My Title"));
assert!(!slice_root.join("002").exists());
}
#[test]
fn plan_refuses_to_clobber_an_existing_plan() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let slice_root = root.join(SLICE_DIR);
fs::write(slice_root.join("001/plan.toml"), "hand-written").unwrap();
let err = entity::materialise(
&PLAN_KIND,
&LocalFs,
root,
&MaterialiseRequest::InExisting { id: 1 },
&Inputs {
slug: "",
title: "My Title",
date: "2026-06-04",
},
&[],
&mut entity::local_reserved(),
)
.unwrap_err();
assert!(err.to_string().contains("Refusing to overwrite"));
assert_eq!(
fs::read_to_string(slice_root.join("001/plan.toml")).unwrap(),
"hand-written"
);
assert!(!slice_root.join("001/plan.md").exists());
}
#[test]
fn notes_materialises_under_an_existing_slice() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let slice_root = root.join(SLICE_DIR);
entity::materialise(
&NOTES_KIND,
&LocalFs,
root,
&MaterialiseRequest::InExisting { id: 1 },
&Inputs {
slug: "",
title: "My Title",
date: "2026-06-04",
},
&[],
&mut entity::local_reserved(),
)
.unwrap();
let body = fs::read_to_string(slice_root.join("001/notes.md")).unwrap();
assert!(body.contains("Notes SL-001: My Title"));
}
#[test]
fn notes_refuses_to_clobber() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let slice_root = root.join(SLICE_DIR);
fs::write(slice_root.join("001/notes.md"), "hand-written").unwrap();
let err = entity::materialise(
&NOTES_KIND,
&LocalFs,
root,
&MaterialiseRequest::InExisting { id: 1 },
&Inputs {
slug: "",
title: "My Title",
date: "2026-06-04",
},
&[],
&mut entity::local_reserved(),
)
.unwrap_err();
assert!(err.to_string().contains("Refusing to overwrite"));
assert_eq!(
fs::read_to_string(slice_root.join("001/notes.md")).unwrap(),
"hand-written"
);
}
fn slice_root(root: &Path) -> PathBuf {
root.join(SLICE_DIR)
}
fn set_status_raw(root: &Path, id: u32, status: &str) {
let name = format!("{id:03}");
let p = slice_root(root)
.join(&name)
.join(format!("slice-{name}.toml"));
let flipped = fs::read_to_string(&p)
.unwrap()
.replace("status = \"proposed\"", &format!("status = \"{status}\""));
fs::write(&p, flipped).unwrap();
}
fn list_args() -> ListArgs {
ListArgs::default()
}
#[test]
fn list_rows_emits_prefixed_ids_and_a_header() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "first", "First", "2026-06-04");
make_slice(root, "second", "Second", "2026-06-04");
let out = list_rows(root, list_args()).unwrap();
let lines: Vec<&str> = out.lines().collect();
assert!(lines[0].starts_with("id"), "header row: {:?}", lines[0]);
assert!(lines[0].contains("phases"), "phases column named");
assert!(out.contains("SL-001 │ proposed"), "prefixed id: {out}");
assert!(out.contains("SL-002"), "second slice present: {out}");
assert!(!out.contains("\n001 "), "no bare numeric id: {out}");
}
#[test]
fn list_rows_hide_set_drops_done_and_abandoned_by_default() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "live", "Live", "2026-06-04");
make_slice(root, "shipped", "Shipped", "2026-06-04");
make_slice(root, "dropped", "Dropped", "2026-06-04");
set_status_raw(root, 2, "done");
set_status_raw(root, 3, "abandoned");
let out = list_rows(root, list_args()).unwrap();
assert!(out.contains("SL-001"), "live slice kept: {out}");
assert!(!out.contains("SL-002"), "done hidden by default: {out}");
assert!(
!out.contains("SL-003"),
"abandoned hidden by default: {out}"
);
}
#[test]
fn list_rows_all_and_explicit_status_reveal_the_hide_set() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "live", "Live", "2026-06-04");
make_slice(root, "dropped", "Dropped", "2026-06-04");
set_status_raw(root, 2, "abandoned");
let all = list_rows(
root,
ListArgs {
all: true,
..Default::default()
},
)
.unwrap();
assert!(all.contains("SL-002"), "--all reveals abandoned: {all}");
let by_status = list_rows(
root,
ListArgs {
status: vec!["abandoned".into()],
..Default::default()
},
)
.unwrap();
assert!(
by_status.contains("SL-002"),
"explicit status reveals: {by_status}"
);
assert!(
!by_status.contains("SL-001"),
"and filters to it: {by_status}"
);
}
#[test]
fn list_rows_out_of_vocab_stored_status_is_never_hidden_and_drift_marked() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "weird", "Weird", "2026-06-04");
set_status_raw(root, 1, "bogus");
let out = list_rows(root, list_args()).unwrap();
assert!(out.contains("SL-001"), "drifted slice not hidden: {out}");
assert!(out.contains("bogus?"), "drift `?` marker present: {out}");
assert!(!out.contains("⚠"), "no spurious divergence marker: {out}");
}
#[test]
fn list_rows_filter_matches_slug_and_title() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "use-rust", "Use Rust", "2026-06-04");
make_slice(root, "adopt-ci", "Adopt CI", "2026-06-04");
let out = list_rows(
root,
ListArgs {
substr: Some("adopt".into()),
..Default::default()
},
)
.unwrap();
assert!(out.contains("SL-002"), "substr matches adopt-ci: {out}");
assert!(!out.contains("SL-001"), "use-rust filtered out: {out}");
}
#[test]
fn list_rows_regexp_matches_canonical_id() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "one", "One", "2026-06-04");
make_slice(root, "two", "Two", "2026-06-04");
let out = list_rows(
root,
ListArgs {
regexp: Some("SL-002".into()),
..Default::default()
},
)
.unwrap();
assert!(out.contains("SL-002"), "regex matches canonical: {out}");
assert!(!out.contains("SL-001"), "non-matching dropped: {out}");
}
#[test]
fn list_rows_json_is_the_shared_envelope_with_structured_phases() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "first", "First", "2026-06-04");
let out = list_rows(
root,
ListArgs {
json: true,
..Default::default()
},
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(parsed["kind"], "slice");
let rows = parsed["rows"].as_array().unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["id"], "SL-001");
assert_eq!(rows[0]["status"], "proposed");
assert_eq!(rows[0]["slug"], "first");
assert!(
rows[0]["phases"].is_null(),
"untracked phases → null: {out}"
);
assert!(!out.contains("4/6"), "no rendered phase cell in json");
}
#[test]
fn list_rows_empty_tree_is_the_empty_string() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(list_rows(dir.path(), list_args()).unwrap(), "");
}
#[test]
fn list_rows_rejects_an_unknown_status_with_the_uniform_error() {
let dir = tempfile::tempdir().unwrap();
let err = list_rows(
dir.path(),
ListArgs {
status: vec!["bogus".into()],
..Default::default()
},
)
.unwrap_err()
.to_string();
assert!(err.contains("bogus"), "names the bad value: {err}");
assert!(err.contains("abandoned"), "lists the known set: {err}");
}
#[test]
fn list_rows_accepts_every_known_status() {
let dir = tempfile::tempdir().unwrap();
for s in SLICE_STATUSES {
assert!(
list_rows(
dir.path(),
ListArgs {
status: vec![(*s).to_string()],
..Default::default()
},
)
.is_ok(),
"known status `{s}` accepted"
);
}
}
#[test]
fn list_rows_accepts_abandoned_and_rejects_superseded() {
let dir = tempfile::tempdir().unwrap();
assert!(
list_rows(
dir.path(),
ListArgs {
status: vec!["abandoned".into()],
..Default::default()
},
)
.is_ok()
);
let err = list_rows(
dir.path(),
ListArgs {
status: vec!["superseded".into()],
..Default::default()
},
)
.unwrap_err()
.to_string();
assert!(err.contains("superseded"), "superseded rejected: {err}");
}
#[test]
fn slice_statuses_matches_the_spec_vocabulary() {
assert_eq!(
SLICE_STATUSES,
&[
"proposed",
"design",
"plan",
"ready",
"started",
"audit",
"reconcile",
"done",
"abandoned"
]
);
}
fn slice_at(root: &Path, id: u32, status: &str, slug: &str, title: &str) {
let name = format!("{id:03}");
let dir = slice_root(root).join(&name);
fs::create_dir_all(&dir).unwrap();
let toml = format!(
"id = {id}\nslug = \"{slug}\"\ntitle = \"{title}\"\nstatus = \"{status}\"\ncreated = \"2026-06-04\"\nupdated = \"2026-06-04\"\n"
);
fs::write(dir.join(format!("slice-{name}.toml")), toml).unwrap();
}
#[test]
fn list_rows_orders_by_id_ascending_regardless_of_creation_order() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
slice_at(root, 3, "proposed", "gamma", "Gamma");
slice_at(root, 1, "proposed", "alpha", "Alpha");
slice_at(root, 2, "proposed", "beta", "Beta");
let out = list_rows(root, list_args()).unwrap();
let off = |id: &str| {
out.find(id)
.unwrap_or_else(|| panic!("{id} present: {out}"))
};
assert!(
off("SL-001") < off("SL-002") && off("SL-002") < off("SL-003"),
"slice rows must render in ascending id order (sort, not read order): {out}"
);
}
#[test]
fn parse_ref_accepts_prefixed_padded_and_bare_ids() {
assert_eq!(parse_ref("SL-025").unwrap(), 25);
assert_eq!(parse_ref("sl-25").unwrap(), 25);
assert_eq!(parse_ref("25").unwrap(), 25);
assert_eq!(parse_ref("002").unwrap(), 2);
assert!(parse_ref("nope").is_err());
}
#[test]
fn read_slice_reassembles_toml_as_data_and_md_scope_body() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let (doc, toml_text, body) = read_slice(&slice_root(root), 1).unwrap();
assert_eq!(doc.id, 1);
assert_eq!(doc.slug, "my-slug");
assert_eq!(doc.status, "proposed");
let edges = crate::relation::tier1_edges(&SLICE_KIND, &toml_text).unwrap();
assert!(edges.is_empty());
assert!(body.contains("My Title"));
}
#[test]
fn format_show_renders_identity_and_scope_body() {
use crate::relation::{RelationEdge, RelationLabel, Role};
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let tier1 = vec![RelationEdge::with_role(
RelationLabel::References,
Some(Role::Implements),
"PRD-010".into(),
)];
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let out = format_show(
&doc,
&tier1,
&crate::dep_seq::DepSeq::default(),
"# Scope\n\nthe scope body.\n",
posture,
&crate::facet::EntityFacets::default(),
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(out.contains("SL-025 — Uniform CLI"), "identity: {out}");
assert!(out.contains("uniform-cli · started"), "flat fields: {out}");
assert!(out.contains("conduct: self/auto"), "conduct posture: {out}");
assert!(out.contains("created 2026-06-01 · updated 2026-06-08"));
assert!(
out.contains("references(implements): PRD-010"),
"relationships axis: {out}"
);
assert!(
out.contains("the scope body."),
"scope body appended: {out}"
);
}
#[test]
fn vt5_format_show_absent_facets_is_byte_identical() {
use crate::relation::{RelationEdge, RelationLabel, Role};
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let tier1 = vec![RelationEdge::with_role(
RelationLabel::References,
Some(Role::Implements),
"PRD-010".into(),
)];
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let out = format_show(
&doc,
&tier1,
&crate::dep_seq::DepSeq::default(),
"# Scope\n\nthe scope body.\n",
posture,
&crate::facet::EntityFacets::default(),
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
let expected = concat!(
"SL-025 — Uniform CLI\n",
"uniform-cli · started\n",
"conduct: self/auto\n",
"created 2026-06-01 · updated 2026-06-08\n",
"\n",
"relationships:\n",
" references(implements): PRD-010\n",
"\n",
"# Scope\n",
"\n",
"the scope body.\n",
);
assert_eq!(
out, expected,
"absent facets must be byte-identical to pre-change format_show output"
);
}
#[test]
fn vt1_format_show_estimate_present_renders_confidence_row() {
use crate::relation::{RelationEdge, RelationLabel, Role};
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let tier1 = vec![RelationEdge::with_role(
RelationLabel::References,
Some(Role::Implements),
"PRD-010".into(),
)];
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let facets = crate::facet::EntityFacets {
estimate: Some(crate::estimate::EstimateFacet {
lower: 3.0,
upper: 5.0,
}),
value: None,
risk: None,
tags: vec![],
};
let out = format_show(
&doc,
&tier1,
&crate::dep_seq::DepSeq::default(),
"# Scope\n",
posture,
&facets,
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(
out.contains("estimate: 3.2–4.8 espresso_shots (80% confidence)"),
"VT-1 estimate row: {out}"
);
}
#[test]
fn vt2_format_show_estimate_absent_no_row() {
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let out = format_show(
&doc,
&[],
&crate::dep_seq::DepSeq::default(),
"# Scope\n",
posture,
&crate::facet::EntityFacets::default(),
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(
!out.contains("estimate:"),
"VT-2: estimate row must not appear when absent: {out}"
);
}
#[test]
fn vt3_format_show_value_present_renders_row() {
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let facets = crate::facet::EntityFacets {
estimate: None,
value: Some(crate::value::ValueFacet { value: 5.0 }),
risk: None,
tags: vec![],
};
let out = format_show(
&doc,
&[],
&crate::dep_seq::DepSeq::default(),
"# Scope\n",
posture,
&facets,
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(
out.contains("value: 5.0 magic_beans"),
"VT-3 value row: {out}"
);
}
#[test]
fn vt4_format_show_value_absent_no_row() {
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let out = format_show(
&doc,
&[],
&crate::dep_seq::DepSeq::default(),
"# Scope\n",
posture,
&crate::facet::EntityFacets::default(),
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(
!out.contains("value:"),
"VT-4: value row must not appear when absent: {out}"
);
}
#[test]
fn vt9_format_show_custom_confidence_bounds() {
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let facets = crate::facet::EntityFacets {
estimate: Some(crate::estimate::EstimateFacet {
lower: 3.0,
upper: 5.0,
}),
value: None,
risk: None,
tags: vec![],
};
let out = format_show(
&doc,
&[],
&crate::dep_seq::DepSeq::default(),
"# Scope\n",
posture,
&facets,
"espresso_shots",
"magic_beans",
0.25,
0.75,
);
assert!(
out.contains("estimate: 3.5–4.5 espresso_shots (50% confidence)"),
"VT-9 custom bounds: {out}"
);
}
#[test]
fn vt10_format_show_zero_width_estimate() {
let doc = SliceDoc {
id: 25,
slug: "uniform-cli".into(),
title: "Uniform CLI".into(),
status: "started".into(),
created: "2026-06-01".into(),
updated: "2026-06-08".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let facets = crate::facet::EntityFacets {
estimate: Some(crate::estimate::EstimateFacet {
lower: 5.0,
upper: 5.0,
}),
value: None,
risk: None,
tags: vec![],
};
let out = format_show(
&doc,
&[],
&crate::dep_seq::DepSeq::default(),
"# Scope\n",
posture,
&facets,
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(
out.contains("estimate: 5.0–5.0 espresso_shots (80% confidence)"),
"VT-10 zero-width: {out}"
);
}
#[test]
fn show_does_not_fold_in_design_plan_or_notes() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let sr = slice_root(root);
fs::write(sr.join("001/design.md"), "DESIGN_SECRET").unwrap();
fs::write(sr.join("001/plan.md"), "PLAN_SECRET").unwrap();
fs::write(sr.join("001/notes.md"), "NOTES_SECRET").unwrap();
let (doc, toml_text, body) = read_slice(&sr, 1).unwrap();
let tier1 = crate::relation::tier1_edges(&SLICE_KIND, &toml_text).unwrap();
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let ds = crate::dep_seq::DepSeq::default();
let table = format_show(
&doc,
&tier1,
&ds,
&body,
posture,
&crate::facet::EntityFacets::default(),
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
let json = show_json(&doc, &tier1, &ds, &body).unwrap();
for needle in ["DESIGN_SECRET", "PLAN_SECRET", "NOTES_SECRET"] {
assert!(!table.contains(needle), "table leaked {needle}: {table}");
assert!(!json.contains(needle), "json leaked {needle}: {json}");
}
}
#[test]
fn show_json_is_faithful_toml_as_data_plus_scope_body() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "my-slug", "My Title", "2026-06-04");
let (doc, toml_text, body) = read_slice(&slice_root(root), 1).unwrap();
let tier1 = crate::relation::tier1_edges(&SLICE_KIND, &toml_text).unwrap();
let out = show_json(&doc, &tier1, &crate::dep_seq::DepSeq::default(), &body).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(parsed["kind"], "slice");
assert_eq!(parsed["slice"]["id"], 1);
assert_eq!(parsed["slice"]["slug"], "my-slug");
assert_eq!(parsed["slice"]["status"], "proposed");
let rel = &parsed["slice"]["relationships"];
assert!(rel.get("specs").is_none());
assert!(rel.get("requirements").is_none());
assert!(rel["supersedes"].is_array());
assert!(rel["references"]["implements"].is_array());
assert!(parsed["body"].as_str().unwrap().contains("My Title"));
}
fn doc_for_json(id: u32) -> SliceDoc {
SliceDoc {
id,
slug: "refs".into(),
title: "Refs".into(),
status: "proposed".into(),
created: "2026-06-01".into(),
updated: "2026-06-01".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
}
}
#[test]
fn show_json_groups_references_by_role() {
use crate::relation::{RelationEdge, RelationLabel, Role};
let tier1 = vec![
RelationEdge::with_role(
RelationLabel::References,
Some(Role::Implements),
"SPEC-018".into(),
),
RelationEdge::with_role(
RelationLabel::References,
Some(Role::OriginatesFrom),
"IMP-012".into(),
),
RelationEdge::with_role(
RelationLabel::References,
Some(Role::Concerns),
"RFC-003".into(),
),
];
let json = show_json(
&doc_for_json(149),
&tier1,
&crate::dep_seq::DepSeq::default(),
"# b\n",
)
.unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let rel = &v["slice"]["relationships"];
assert_eq!(
rel["references"]["implements"],
serde_json::json!(["SPEC-018"])
);
assert_eq!(
rel["references"]["originates_from"],
serde_json::json!(["IMP-012"])
);
assert_eq!(
rel["references"]["concerns"],
serde_json::json!(["RFC-003"])
);
assert!(rel.get("specs").is_none(), "legacy specs key removed");
assert!(
rel.get("requirements").is_none(),
"legacy requirements key removed"
);
}
#[test]
fn show_json_references_implements_carries_spec_and_req() {
use crate::relation::{RelationEdge, RelationLabel, Role};
let tier1 = vec![
RelationEdge::with_role(
RelationLabel::References,
Some(Role::Implements),
"SPEC-018".into(),
),
RelationEdge::with_role(
RelationLabel::References,
Some(Role::Implements),
"REQ-002".into(),
),
];
let json = show_json(
&doc_for_json(149),
&tier1,
&crate::dep_seq::DepSeq::default(),
"# b\n",
)
.unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let rel = &v["slice"]["relationships"];
assert_eq!(
rel["references"]["implements"],
serde_json::json!(["SPEC-018", "REQ-002"])
);
assert!(rel.get("specs").is_none());
assert!(rel.get("requirements").is_none());
}
#[test]
fn show_surfaces_dep_seq_axes_in_table_and_json() {
use crate::dep_seq::{AfterEdge, DepSeq};
let doc = SliceDoc {
id: 60,
slug: "dep-seq".into(),
title: "Dep Seq".into(),
status: "proposed".into(),
created: "2026-06-01".into(),
updated: "2026-06-01".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let tier1 = Vec::new();
let ds = DepSeq {
needs: vec!["SL-047".into()],
after: vec![
AfterEdge {
to: "SL-002".into(),
rank: 0,
},
AfterEdge {
to: "SL-003".into(),
rank: 5,
},
],
};
let posture =
crate::conduct::resolve(&crate::conduct::ConductConfig::default(), &doc.status);
let table = format_show(
&doc,
&tier1,
&ds,
"# body\n",
posture,
&crate::facet::EntityFacets::default(),
"espresso_shots",
"magic_beans",
0.1,
0.9,
);
assert!(table.contains("relationships:"), "block renders: {table}");
assert!(table.contains("needs: SL-047"), "needs axis: {table}");
assert!(
table.contains("after: SL-002, SL-003 (rank 5)"),
"after axis with rank suffix: {table}"
);
let json = show_json(&doc, &tier1, &ds, "# body\n").unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(
v["slice"]["relationships"]["needs"],
serde_json::json!(["SL-047"])
);
assert_eq!(
v["slice"]["relationships"]["after"],
serde_json::json!([
{ "to": "SL-002", "rank": 0 },
{ "to": "SL-003", "rank": 5 },
])
);
}
#[test]
fn show_omits_dep_seq_keys_when_unauthored() {
let doc = SliceDoc {
id: 1,
slug: "v".into(),
title: "V".into(),
status: "proposed".into(),
created: "2026-06-01".into(),
updated: "2026-06-01".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let json = show_json(&doc, &[], &crate::dep_seq::DepSeq::default(), "# body\n").unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let rel = &v["slice"]["relationships"];
assert!(rel.get("needs").is_none(), "no needs key when unauthored");
assert!(rel.get("after").is_none(), "no after key when unauthored");
}
#[test]
fn slice_doc_round_trips_estimate_facet() {
let doc = SliceDoc {
id: 17,
slug: "estimate-facet".into(),
title: "Estimate Facet".into(),
status: "proposed".into(),
created: "2026-06-18".into(),
updated: "2026-06-18".into(),
tags: vec![],
gate: Gate::default(),
estimate: Some(crate::estimate::EstimateFacet {
lower: 2.0,
upper: 8.0,
}),
value: None,
selectors: vec![],
};
let toml = toml::to_string_pretty(&doc).unwrap();
let parsed: SliceDoc = toml::from_str(&toml).unwrap();
let estimate = parsed.estimate.expect("estimate facet present");
assert_eq!(estimate.lower, 2.0);
assert_eq!(estimate.upper, 8.0);
assert_eq!(parsed.value, None);
}
#[test]
fn slice_doc_round_trips_value_facet() {
let doc = SliceDoc {
id: 17,
slug: "value-facet".into(),
title: "Value Facet".into(),
status: "proposed".into(),
created: "2026-06-18".into(),
updated: "2026-06-18".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: Some(crate::value::ValueFacet { value: 5.0 }),
selectors: vec![],
};
let toml = toml::to_string_pretty(&doc).unwrap();
let parsed: SliceDoc = toml::from_str(&toml).unwrap();
assert_eq!(parsed.estimate, None);
assert_eq!(
parsed.value.expect("value facet present"),
crate::value::ValueFacet { value: 5.0 }
);
}
#[test]
fn slice_doc_serde_omits_absent_facets() {
let doc = SliceDoc {
id: 18,
slug: "no-facets".into(),
title: "No Facets".into(),
status: "proposed".into(),
created: "2026-06-18".into(),
updated: "2026-06-18".into(),
tags: vec![],
gate: Gate::default(),
estimate: None,
value: None,
selectors: vec![],
};
let toml = toml::to_string_pretty(&doc).unwrap();
assert!(
!toml.contains("[estimate]"),
"unexpected estimate table: {toml}"
);
assert!(!toml.contains("[value]"), "unexpected value table: {toml}");
let parsed: SliceDoc = toml::from_str(&toml).unwrap();
assert_eq!(parsed.estimate, None);
assert_eq!(parsed.value, None);
}
#[test]
fn slice_doc_malformed_facet_errors_at_parse() {
let text = "id = 1\nslug = \"x\"\ntitle = \"X\"\nstatus = \"proposed\"\n\
created = \"2026-06-18\"\nupdated = \"2026-06-18\"\n[value]\nvalue = nan\n";
let err = toml::from_str::<SliceDoc>(text).unwrap_err().to_string();
assert!(err.contains("must be finite"), "got: {err}");
}
#[test]
fn load_conduct_absent_file_is_baked_defaults() {
let dir = tempfile::tempdir().unwrap();
let cfg = load_conduct(dir.path()).unwrap();
assert_eq!(cfg, crate::conduct::ConductConfig::default());
assert_eq!(crate::conduct::resolve(&cfg, "plan").label(), "self/gate");
assert_eq!(
crate::conduct::resolve(&cfg, "started").label(),
"self/auto"
);
}
#[test]
fn load_conduct_reflects_a_root_override() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join(crate::dtoml::DOCTRINE_TOML);
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
fs::write(
&config,
"[conduct]\ndefault-actor = \"agent\"\n[conduct.ready]\nautonomy = \"gate\"\n",
)
.unwrap();
let cfg = load_conduct(dir.path()).unwrap();
assert_eq!(crate::conduct::resolve(&cfg, "ready").label(), "agent/gate");
}
#[test]
fn load_conduct_refuses_malformed_doctrine_toml() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join(crate::dtoml::DOCTRINE_TOML);
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
fs::write(&config, "[conduct\nbroken =").unwrap();
assert!(load_conduct(dir.path()).is_err());
}
#[test]
fn run_show_on_a_missing_slice_errors() {
let dir = tempfile::tempdir().unwrap();
let err = run_show(Some(dir.path().to_path_buf()), "SL-009", Format::Table).unwrap_err();
assert!(err.to_string().contains("not found"), "got: {err}");
}
#[test]
fn vt11_run_show_fixture_with_estimate_renders_confidence_row() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let config = root.join(crate::dtoml::DOCTRINE_TOML);
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
fs::write(
&config,
"[estimation]\nunit = \"story_points\"\nlower_confidence = 0.1\nupper_confidence = 0.9\n",
)
.unwrap();
let sr = slice_root(root);
fs::create_dir_all(sr.join("001")).unwrap();
fs::write(
sr.join("001/slice-001.toml"),
"id = 1\nslug = \"test\"\ntitle = \"Test\"\nstatus = \"proposed\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\n[estimate]\nlower = 3\nupper = 8\n",
)
.unwrap();
fs::write(sr.join("001/slice-001.md"), "# Test slice\n").unwrap();
let (doc, toml_text, body) = read_slice(&sr, 1).unwrap();
let cfg = crate::dtoml::load_doctrine_toml(root).unwrap();
let posture = crate::conduct::resolve(&cfg.conduct, &doc.status);
let estimation_unit = crate::estimate::resolve_unit(&cfg.estimation);
let value_unit = crate::value::resolve_unit(&cfg.value);
let (lower_pct, upper_pct) = crate::estimate::resolve_confidence(&cfg.estimation).unwrap();
let facets = crate::facet::EntityFacets {
estimate: doc.estimate.clone(),
value: doc.value.clone(),
risk: None,
tags: doc.tags.clone(),
};
let tier1 = crate::relation::tier1_edges(&SLICE_KIND, &toml_text).unwrap();
let dep_seq = crate::dep_seq::DepSeq::default();
let out = format_show(
&doc,
&tier1,
&dep_seq,
&body,
posture,
&facets,
&estimation_unit,
&value_unit,
lower_pct,
upper_pct,
);
assert!(
out.contains("estimate: 3.5–7.5 story_points (80% confidence)"),
"VT-11: {out}"
);
}
#[test]
fn vt12_run_show_malformed_doctrine_toml_propagates_error() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let config = root.join(crate::dtoml::DOCTRINE_TOML);
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
fs::write(&config, "[estimation\nunit = broken").unwrap();
let sr = slice_root(root);
fs::create_dir_all(sr.join("001")).unwrap();
fs::write(
sr.join("001/slice-001.toml"),
"id = 1\nslug = \"test\"\ntitle = \"Test\"\nstatus = \"proposed\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n",
)
.unwrap();
fs::write(sr.join("001/slice-001.md"), "# Test\n").unwrap();
let err = run_show(Some(root.to_path_buf()), "SL-001", Format::Table).unwrap_err();
assert!(
err.to_string().contains("Failed to parse"),
"VT-12: error must propagate, got: {err}"
);
}
#[test]
fn classify_forward_chain_is_advance() {
for (from, to) in [
("proposed", "design"),
("design", "plan"),
("plan", "ready"),
("ready", "started"),
("started", "audit"),
] {
assert_eq!(classify(from, to), Transition::Advance, "{from} → {to}");
}
}
#[test]
fn classify_legit_closure_seam_path_is_advance() {
assert_eq!(classify("audit", "reconcile"), Transition::Advance);
assert_eq!(classify("reconcile", "done"), Transition::Advance);
}
#[test]
fn classify_named_back_edges() {
for (from, to) in [
("audit", "started"),
("audit", "design"),
("reconcile", "audit"),
("reconcile", "design"),
] {
assert_eq!(classify(from, to), Transition::BackEdge, "{from} → {to}");
}
}
#[test]
fn classify_abandon_from_each_non_terminal() {
for from in [
"proposed",
"design",
"plan",
"ready",
"started",
"audit",
"reconcile",
] {
assert_eq!(
classify(from, "abandoned"),
Transition::Abandon,
"{from} → abandoned"
);
}
}
#[test]
fn classify_noop_when_unchanged() {
assert_eq!(classify("started", "started"), Transition::Noop);
assert_eq!(classify("done", "done"), Transition::Noop);
}
#[test]
fn classify_from_terminal_refused() {
for from in ["done", "abandoned"] {
assert_eq!(
classify(from, "design"),
Transition::FromTerminal,
"{from} → design"
);
}
}
#[test]
fn classify_seam_breach_to_reconcile_from_non_audit() {
for from in ["proposed", "design", "plan", "ready", "started"] {
assert_eq!(
classify(from, "reconcile"),
Transition::SeamBreach,
"{from} → reconcile"
);
}
}
#[test]
fn classify_seam_breach_to_done_from_non_reconcile() {
for from in ["proposed", "design", "plan", "ready", "started", "audit"] {
assert_eq!(
classify(from, "done"),
Transition::SeamBreach,
"{from} → done"
);
}
}
#[test]
fn classify_seam_binds_even_from_a_drifted_source() {
assert_eq!(classify("bogus", "reconcile"), Transition::SeamBreach);
assert_eq!(classify("bogus", "done"), Transition::SeamBreach);
}
#[test]
fn classify_move_out_of_drift_is_skip_not_refused() {
assert_eq!(classify("bogus", "started"), Transition::Skip);
}
#[test]
fn classify_non_chain_move_is_skip() {
assert_eq!(classify("proposed", "started"), Transition::Skip);
assert_eq!(classify("design", "started"), Transition::Skip);
}
#[test]
fn is_transition_terminal_is_a_distinct_third_predicate() {
assert!(is_transition_terminal("done"));
assert!(is_transition_terminal("abandoned"));
assert!(!is_transition_terminal("started"));
assert!(is_transition_terminal("abandoned") && !is_terminal_status("abandoned"));
assert_eq!(is_transition_terminal("done"), is_hidden("done"));
}
#[test]
fn slice_status_enum_matches_the_vocabulary() {
let variants = [
SliceStatus::Proposed,
SliceStatus::Design,
SliceStatus::Plan,
SliceStatus::Ready,
SliceStatus::Started,
SliceStatus::Audit,
SliceStatus::Reconcile,
SliceStatus::Done,
SliceStatus::Abandoned,
];
let from_variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
assert_eq!(from_variants, SLICE_STATUSES.to_vec());
}
fn slice_text(root: &Path, id: u32) -> String {
let name = format!("{id:03}");
fs::read_to_string(
slice_root(root)
.join(&name)
.join(format!("slice-{name}.toml")),
)
.unwrap()
}
#[test]
fn set_slice_status_advances_and_preserves_comments_and_relationships() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
let before = slice_text(root, 1);
assert!(
before.contains("[relationships]"),
"fixture has relationships"
);
let comment = before
.lines()
.find(|l| l.trim_start().starts_with('#'))
.is_some();
set_slice_status(
&slice_root(root),
1,
"proposed",
SliceStatus::Design,
"2099-01-01",
)
.unwrap();
let after = slice_text(root, 1);
assert!(
after.contains("status = \"design\""),
"status written: {after}"
);
assert!(
after.contains("updated = \"2099-01-01\""),
"date stamped: {after}"
);
assert!(
after.contains("[relationships]"),
"relationships survive: {after}"
);
if comment {
assert!(after.contains('#'), "comments survive: {after}");
}
}
#[test]
fn set_slice_status_noop_holds_content_and_mtime() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
let p = slice_root(root).join("001").join("slice-001.toml");
let before = fs::read_to_string(&p).unwrap();
let mtime_before = fs::metadata(&p).unwrap().modified().unwrap();
set_slice_status(
&slice_root(root),
1,
"proposed",
SliceStatus::Proposed,
"2099-01-01",
)
.unwrap();
assert_eq!(fs::read_to_string(&p).unwrap(), before, "content held");
assert_eq!(
fs::metadata(&p).unwrap().modified().unwrap(),
mtime_before,
"mtime held"
);
}
#[test]
fn set_slice_status_refuses_from_terminal() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
for from in [SliceStatus::Done, SliceStatus::Abandoned] {
let err = set_slice_status(
&slice_root(root),
1,
from.as_str(),
SliceStatus::Design,
"x",
)
.unwrap_err()
.to_string();
assert!(err.contains("terminal"), "{}: {err}", from.as_str());
}
assert!(slice_text(root, 1).contains("status = \"proposed\""));
}
#[test]
fn set_slice_status_refuses_seam_breach() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
let err = set_slice_status(&slice_root(root), 1, "started", SliceStatus::Done, "x")
.unwrap_err()
.to_string();
assert!(err.contains("closure seam"), "skip-to-done refused: {err}");
let err2 = set_slice_status(&slice_root(root), 1, "design", SliceStatus::Reconcile, "x")
.unwrap_err()
.to_string();
assert!(
err2.contains("closure seam"),
"non-audit → reconcile refused: {err2}"
);
assert!(
slice_text(root, 1).contains("status = \"proposed\""),
"disk untouched"
);
}
#[test]
fn set_slice_status_seam_breach_from_a_drifted_source() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "bogus");
let err = set_slice_status(&slice_root(root), 1, "bogus", SliceStatus::Done, "x")
.unwrap_err()
.to_string();
assert!(
err.contains("closure seam"),
"drifted → done refused: {err}"
);
}
#[test]
fn set_slice_status_refuses_malformed_toml() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let d = slice_root(root).join("001");
fs::create_dir_all(&d).unwrap();
fs::write(
d.join("slice-001.toml"),
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"started\"\n",
)
.unwrap();
let err = set_slice_status(&slice_root(root), 1, "started", SliceStatus::Audit, "x")
.unwrap_err()
.to_string();
assert!(err.contains("malformed"), "missing key refused: {err}");
}
#[test]
fn run_status_prints_classification_with_note() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "started");
run_status(
Some(root.to_path_buf()),
1,
SliceStatus::Audit,
Some("done impl"),
)
.unwrap();
assert!(
slice_text(root, 1).contains("status = \"audit\""),
"write landed"
);
}
#[test]
fn status_line_carries_the_source_exit_posture() {
let cfg = crate::conduct::ConductConfig::default();
let line = status_line(
"reconcile",
"done",
classify("reconcile", "done"),
crate::conduct::resolve(&cfg, "reconcile"),
None,
);
assert_eq!(line, "reconcile → done [advance] [self/gate]");
}
#[test]
fn status_line_appends_the_note_after_the_posture() {
let cfg = crate::conduct::ConductConfig::default();
let line = status_line(
"started",
"audit",
classify("started", "audit"),
crate::conduct::resolve(&cfg, "started"),
Some("done impl"),
);
assert_eq!(line, "started → audit [advance] [self/auto] — done impl");
}
#[test]
fn read_status_surfaces_the_current_authored_status() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "reconcile");
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt4_crosses_closure_seam_is_only_the_two_terminal_advances() {
assert!(crosses_closure_seam("audit", "reconcile"));
assert!(crosses_closure_seam("reconcile", "done"));
for (from, to) in [
("started", "audit"),
("ready", "started"),
("plan", "ready"),
("audit", "started"), ("reconcile", "audit"), ("started", "abandoned"),
("audit", "audit"), ] {
assert!(
!crosses_closure_seam(from, to),
"{from} → {to} must NOT be a closure-seam crossing"
);
}
}
fn raise_blocker_rv(root: &Path, target_id: u32) {
let target = canonical_id(target_id);
crate::review::run_new(
Some(root.to_path_buf()),
&crate::review::NewArgs {
facet: crate::review::Facet::Reconciliation,
target: target.clone(),
phase: None,
title: None,
raiser: None,
responder: None,
},
)
.unwrap();
crate::review::run_raise(
Some(root.to_path_buf()),
&crate::review::RaiseArgs {
reference: "RV-001".to_owned(),
severity: crate::review::Severity::Blocker,
title: "must fix".to_owned(),
detail: "d".to_owned(),
},
crate::review::Role::Raiser,
)
.unwrap();
}
#[test]
fn vt2_close_seam_refused_on_an_unresolved_blocker() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "audit");
raise_blocker_rv(root, 1);
let err = run_status(Some(root.to_path_buf()), 1, SliceStatus::Reconcile, None)
.unwrap_err()
.to_string();
assert!(err.contains("RV-001/F-1"), "names the blocker: {err}");
assert!(err.contains("refused"), "refusal wording: {err}");
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "audit");
}
#[test]
fn vt2_close_seam_passes_after_the_blocker_is_verified() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "audit");
raise_blocker_rv(root, 1);
crate::review::run_dispose(
Some(root.to_path_buf()),
&crate::review::DisposeArgs {
reference: "RV-001".to_owned(),
finding: "F-1".to_owned(),
disposition: "fixed".to_owned(),
response: "done".to_owned(),
},
crate::review::Role::Responder,
)
.unwrap();
crate::review::run_verify(
Some(root.to_path_buf()),
"RV-001",
"F-1",
None,
crate::review::Role::Raiser,
)
.unwrap();
run_status(Some(root.to_path_buf()), 1, SliceStatus::Reconcile, None).unwrap();
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt2_close_seam_passes_after_the_blocker_is_withdrawn() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "audit");
raise_blocker_rv(root, 1);
crate::review::run_withdraw(
Some(root.to_path_buf()),
"RV-001",
"F-1",
crate::review::Role::Raiser,
)
.unwrap();
run_status(Some(root.to_path_buf()), 1, SliceStatus::Reconcile, None).unwrap();
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt4_non_seam_transition_is_not_gated() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-04");
set_status_raw(root, 1, "started");
raise_blocker_rv(root, 1);
run_status(Some(root.to_path_buf()), 1, SliceStatus::Audit, None).unwrap();
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "audit");
}
#[test]
fn vt5_close_shell_is_the_sole_seam_crossing_caller_of_set_slice_status() {
let src = include_str!("slice.rs");
let production = src.split_once("#[cfg(test)]").map_or(src, |(head, _)| head);
let call_sites = production
.match_indices("set_slice_status(")
.filter(|(i, _)| {
!production.get(..*i).unwrap_or("").ends_with("fn ")
})
.count();
assert_eq!(
call_sites, 1,
"exactly ONE production caller may cross the closure seam (the close \
shell `run_status`); a second `set_slice_status(` call site bypasses \
the close-gate (design §7 Charge VIII — re-invoke the gate, or move \
it into the FSM writer)"
);
}
use crate::coverage::CoverageKey;
use crate::rec::{RecDoc, RecMeta, StatusDelta};
use crate::requirement::{self, ReqKind, ReqStatus};
fn git(root: &Path, args: &[&str]) -> String {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args([
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"-c",
"commit.gpgsign=false",
])
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap().trim().to_owned()
}
fn drift_repo() -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
git(root, &["init", "-q", "-b", "main"]);
std::fs::write(root.join("src.rs"), "fn a() {}\n").unwrap();
git(root, &["add", "src.rs"]);
git(root, &["commit", "-q", "-m", "seed"]);
let anchor = git(root, &["rev-parse", "HEAD"]);
(dir, anchor)
}
fn mint_req(root: &Path, status: ReqStatus) -> String {
let id = requirement::reserve(root, "fast-boot", "Fast boot", "2026-06-12")
.unwrap()
.eid
.numeric_id()
.unwrap();
requirement::set_kind(root, id, ReqKind::Functional).unwrap();
requirement::set_status(root, id, status).unwrap();
requirement::canonical_id(id)
}
fn write_own_coverage(root: &Path, dir_id: u32, cov_slice: &str, req: &str, anchor: &str) {
let d = root.join(SLICE_DIR).join(format!("{dir_id:03}"));
fs::create_dir_all(&d).unwrap();
let body = format!(
"[[entry]]\nslice = \"{cov_slice}\"\nrequirement = \"{req}\"\n\
contributing_change = \"{cov_slice}\"\nmode = \"VT\"\n\
status = \"verified\"\ngit_anchor = \"{anchor}\"\n\
touched_paths = [\"src.rs\"]\n"
);
fs::write(d.join("coverage.toml"), body).unwrap();
}
fn cov_key(cov_slice: &str, req: &str) -> CoverageKey {
CoverageKey {
slice: cov_slice.to_owned(),
requirement: req.to_owned(),
contributing_change: cov_slice.to_owned(),
mode: "VT".to_owned(),
}
}
fn mint_rec(
root: &Path,
owning: &str,
r#move: &str,
req: &str,
from: ReqStatus,
to: ReqStatus,
evidence: Vec<CoverageKey>,
) -> u32 {
let doc = RecDoc {
tags: Vec::new(),
id: 0,
slug: format!("{move}-{}", req.to_lowercase()),
title: format!("{move} {req}"),
rec: RecMeta {
r#move: r#move.to_owned(),
owning_slice: Some(owning.to_owned()),
decision_ref: None,
},
status_delta: vec![StatusDelta {
requirement: req.to_owned(),
from: from.as_str().to_owned(),
to: to.as_str().to_owned(),
}],
evidence_ref: evidence,
};
crate::rec::materialise_populated(root, &doc).unwrap()
}
fn slice_at_reconcile(root: &Path) {
make_slice(root, "s", "S", "2026-06-12");
set_status_raw(root, 1, "reconcile");
}
fn expect_close_refused(root: &Path) -> String {
run_status(Some(root.to_path_buf()), 1, SliceStatus::Done, None)
.expect_err("reconcile → done should be refused")
.to_string()
}
#[test]
fn vt1_covered_req_residual_drift_refuses_close() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
let err = expect_close_refused(root);
assert!(err.contains("undischarged residual drift"), "{err}");
assert!(err.contains(&req), "names the offending req: {err}");
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt1_f12_topology_refuses_out_of_seam_independent_of_drift() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
make_slice(root, "s", "S", "2026-06-12");
set_status_raw(root, 1, "started");
let err = run_status(Some(root.to_path_buf()), 1, SliceStatus::Done, None)
.unwrap_err()
.to_string();
assert!(
err.contains("reconcile") && !err.contains("residual drift"),
"F12 topology refusal, not the drift gate: {err}"
);
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "started");
}
#[test]
fn vt2_declared_extra_req_blocks_on_residual_drift() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 999, "SL-999", &req, &anchor);
let p = slice_root(root).join("001").join("slice-001.toml");
let mut toml = fs::read_to_string(&p).unwrap();
toml.push_str(&format!("\n[gate]\nextra_reqs = [\"{req}\"]\n"));
fs::write(&p, toml).unwrap();
let err = expect_close_refused(root);
assert!(err.contains(&req), "declared req gates: {err}");
}
#[test]
fn vt2_reconciled_req_blocks_on_residual_drift() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 999, "SL-999", &req, &anchor);
mint_rec(
root,
"SL-001",
"revise",
&req,
ReqStatus::Pending,
ReqStatus::Pending,
vec![cov_key("SL-999", &req)],
);
let err = expect_close_refused(root);
assert!(err.contains(&req), "reconciled req gates: {err}");
}
#[test]
fn vt2_no_gate_table_runs_on_covered_union_reconciled() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
let toml = fs::read_to_string(slice_root(root).join("001").join("slice-001.toml")).unwrap();
assert!(!toml.contains("[gate]"), "fixture has no [gate] table");
let err = expect_close_refused(root);
assert!(err.contains(&req), "{err}");
}
#[test]
fn vt3_slice_local_reader_returns_distinct_reqs() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let d = root.join(SLICE_DIR).join("001");
fs::create_dir_all(&d).unwrap();
let body = "\
[[entry]]\nslice = \"SL-001\"\nrequirement = \"REQ-001\"\n\
contributing_change = \"SL-001\"\nmode = \"VT\"\nstatus = \"planned\"\n\
git_anchor = \"a\"\n\
[[entry]]\nslice = \"SL-001\"\nrequirement = \"REQ-001\"\n\
contributing_change = \"SL-001\"\nmode = \"VA\"\nstatus = \"planned\"\n\
git_anchor = \"a\"\n\
[[entry]]\nslice = \"SL-001\"\nrequirement = \"REQ-002\"\n\
contributing_change = \"SL-001\"\nmode = \"VT\"\nstatus = \"planned\"\n\
git_anchor = \"a\"\n";
fs::write(d.join("coverage.toml"), body).unwrap();
let reqs = crate::coverage_scan::slice_local_covered_reqs(root, 1, "SL-001").unwrap();
assert_eq!(reqs, vec!["REQ-001".to_owned(), "REQ-002".to_owned()]);
}
#[test]
fn vt3_foreign_slice_in_own_coverage_is_an_integrity_error() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let d = root.join(SLICE_DIR).join("001");
fs::create_dir_all(&d).unwrap();
let body = "[[entry]]\nslice = \"SL-042\"\nrequirement = \"REQ-001\"\n\
contributing_change = \"SL-042\"\nmode = \"VT\"\nstatus = \"planned\"\n\
git_anchor = \"a\"\n";
fs::write(d.join("coverage.toml"), body).unwrap();
let err = crate::coverage_scan::slice_local_covered_reqs(root, 1, "SL-001")
.unwrap_err()
.to_string();
assert!(err.contains("integrity error"), "{err}");
assert!(err.contains("SL-042"), "names the foreign slice: {err}");
}
#[test]
fn vt3_absent_coverage_is_empty_not_error() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
assert!(
crate::coverage_scan::slice_local_covered_reqs(root, 1, "SL-001")
.unwrap()
.is_empty()
);
}
#[test]
fn multi_delta_rec_does_not_discharge_via_foreign_requirement() {
let rec = RecDoc {
id: 1,
slug: "accept-req-001".to_owned(),
title: "accept REQ-001".to_owned(),
rec: RecMeta {
r#move: "accept".to_owned(),
owning_slice: Some("SL-001".to_owned()),
decision_ref: None,
},
tags: Vec::new(),
status_delta: vec![
StatusDelta {
requirement: "REQ-001".to_owned(),
from: "pending".to_owned(),
to: "active".to_owned(),
},
StatusDelta {
requirement: "REQ-002".to_owned(),
from: "active".to_owned(),
to: "pending".to_owned(),
},
],
evidence_ref: Vec::new(),
};
assert!(
!rec_discharges(Some(&rec), "REQ-001", ReqStatus::Pending, &[]),
"a foreign requirement's coinciding `to` laundered R1's drift"
);
let affirm = RecDoc {
status_delta: vec![StatusDelta {
requirement: "REQ-001".to_owned(),
from: "pending".to_owned(),
to: "pending".to_owned(),
}],
..rec
};
assert!(
rec_discharges(Some(&affirm), "REQ-001", ReqStatus::Pending, &[]),
"an accept REC affirming R1 at its current status should discharge"
);
}
#[test]
fn vt4_matching_accept_rec_discharges_the_drift() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
mint_rec(
root,
"SL-001",
"accept",
&req,
ReqStatus::Pending,
ReqStatus::Pending,
vec![cov_key("SL-001", &req)],
);
run_status(Some(root.to_path_buf()), 1, SliceStatus::Done, None).unwrap();
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "done");
}
#[test]
fn vt5_post_rec_fresh_evidence_undischarges() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
mint_rec(
root,
"SL-001",
"accept",
&req,
ReqStatus::Pending,
ReqStatus::Pending,
vec![cov_key("SL-001", &req)],
);
let d = root.join(SLICE_DIR).join("001");
let extra = format!(
"\n[[entry]]\nslice = \"SL-001\"\nrequirement = \"{req}\"\n\
contributing_change = \"SL-002\"\nmode = \"VT\"\nstatus = \"verified\"\n\
git_anchor = \"{anchor}\"\ntouched_paths = [\"src.rs\"]\n"
);
let p = d.join("coverage.toml");
let mut body = fs::read_to_string(&p).unwrap();
body.push_str(&extra);
fs::write(&p, body).unwrap();
let err = expect_close_refused(root);
assert!(
err.contains(&req),
"stale REC cannot excuse fresh evidence: {err}"
);
}
#[test]
fn vt6_revise_rec_does_not_discharge() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
mint_rec(
root,
"SL-001",
"revise",
&req,
ReqStatus::Pending,
ReqStatus::Pending,
vec![cov_key("SL-001", &req)],
);
let err = expect_close_refused(root);
assert!(err.contains(&req), "revise REC does not discharge: {err}");
}
#[test]
fn vt6_foreign_owning_slice_rec_does_not_discharge() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
mint_rec(
root,
"SL-777",
"accept",
&req,
ReqStatus::Pending,
ReqStatus::Pending,
vec![cov_key("SL-001", &req)],
);
let err = expect_close_refused(root);
assert!(
err.contains(&req),
"foreign-slice REC does not discharge: {err}"
);
}
#[test]
fn vt7_blocker_and_drift_each_independently_refuse() {
let (dir, anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
let req = mint_req(root, ReqStatus::Pending);
write_own_coverage(root, 1, "SL-001", &req, &anchor);
let rec_id = mint_rec(
root,
"SL-001",
"accept",
&req,
ReqStatus::Pending,
ReqStatus::Pending,
vec![cov_key("SL-001", &req)],
);
let _ = rec_id;
raise_blocker_rv(root, 1);
let err_blocker = expect_close_refused(root);
assert!(
err_blocker.contains("blocker review finding"),
"blocker gate refuses independently: {err_blocker}"
);
let (dir2, anchor2) = drift_repo();
let root2 = dir2.path();
slice_at_reconcile(root2);
let req2 = mint_req(root2, ReqStatus::Pending);
write_own_coverage(root2, 1, "SL-001", &req2, &anchor2);
let err_drift = expect_close_refused(root2);
assert!(
err_drift.contains("residual drift"),
"drift gate refuses independently: {err_drift}"
);
}
#[test]
fn nf001_gate_destructure_is_exhaustive_and_facet_free() {
let Gate { extra_reqs: _ } = Gate::default();
}
fn dispatch_row_toml(target_ref: &str, planned_new_oid: &str) -> String {
format!(
"[[row]]\n\
source_oid = \"src\"\n\
target_ref = \"{target_ref}\"\n\
expected_old_oid = \"{zero}\"\n\
planned_new_oid = \"{planned_new_oid}\"\n\
status = \"pending\"\n",
zero = "0".repeat(40),
)
}
fn commit_dispatch_journal(root: &Path, slice: u32, body: &str) {
let branch = format!("dispatch/{slice:03}");
git(root, &["checkout", "-q", "--orphan", &branch]);
git(root, &["rm", "-rf", "--cached", "--ignore-unmatch", "."]);
let rel = format!(".doctrine/dispatch/{slice:03}/journal.toml");
let full = root.join(&rel);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(&full, body).unwrap();
git(root, &["add", &rel]);
git(root, &["commit", "-q", "-m", "coordinate: journal"]);
git(root, &["checkout", "-f", "main"]);
}
fn commit_dispatch_no_journal(root: &Path, slice: u32) {
let branch = format!("dispatch/{slice:03}");
git(root, &["checkout", "-q", "--orphan", &branch]);
git(root, &["rm", "-rf", "--cached", "--ignore-unmatch", "."]);
fs::write(root.join("placeholder.txt"), "x").unwrap();
git(root, &["add", "placeholder.txt"]);
git(root, &["commit", "-q", "-m", "coordinate: no journal"]);
git(root, &["checkout", "-f", "main"]);
}
fn expect_close_succeeds(root: &Path) {
run_status(Some(root.to_path_buf()), 1, SliceStatus::Done, None)
.expect("reconcile → done should succeed");
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "done");
}
#[test]
fn vt1_close_integration_not_dispatched_succeeds() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
slice_at_reconcile(root);
expect_close_succeeds(root);
}
#[test]
fn vt1b_close_integration_dispatched_empty_journal_succeeds() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
commit_dispatch_journal(root, 1, "");
slice_at_reconcile(root);
expect_close_succeeds(root);
}
#[test]
fn vt2_close_integration_planned_on_trunk_succeeds() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
std::fs::write(root.join("src.rs"), "fn a() {}\nfn b() {}\n").unwrap();
git(root, &["add", "src.rs"]);
git(root, &["commit", "-q", "-m", "landed"]);
let landed = git(root, &["rev-parse", "HEAD"]);
std::fs::write(root.join("src.rs"), "fn a() {}\nfn b() {}\nfn c() {}\n").unwrap();
git(root, &["add", "src.rs"]);
git(root, &["commit", "-q", "-m", "advance trunk"]);
commit_dispatch_journal(root, 1, &dispatch_row_toml("refs/heads/main", &landed));
slice_at_reconcile(root);
expect_close_succeeds(root);
}
#[test]
fn vt3_close_integration_planned_off_trunk_refuses() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
git(root, &["checkout", "-q", "-b", "side"]);
std::fs::write(root.join("side.rs"), "fn x() {}\n").unwrap();
git(root, &["add", "side.rs"]);
git(root, &["commit", "-q", "-m", "divergent"]);
let orphaned = git(root, &["rev-parse", "HEAD"]);
git(root, &["checkout", "-f", "main"]);
commit_dispatch_journal(root, 1, &dispatch_row_toml("refs/heads/main", &orphaned));
slice_at_reconcile(root);
let err = expect_close_refused(root);
assert!(
err.contains("not integrated to trunk"),
"names the integration anomaly: {err}"
);
assert!(
err.contains("planned tip not on trunk"),
"carries the leaf reason token: {err}"
);
assert!(
err.contains("dispatch sync --integrate"),
"carries the retry guidance: {err}"
);
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt4_close_integration_no_trunk_row_refuses_fail_closed() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
let oid = git(root, &["rev-parse", "HEAD"]);
commit_dispatch_journal(root, 1, &dispatch_row_toml("refs/heads/edge", &oid));
slice_at_reconcile(root);
let err = expect_close_refused(root);
assert!(
err.contains("no trunk row") && err.contains("integrate --trunk never completed"),
"fail-closed on a missing trunk row: {err}"
);
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt5_close_integration_does_not_fire_off_the_reconcile_done_crossing() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
git(root, &["checkout", "-q", "-b", "side"]);
std::fs::write(root.join("side.rs"), "fn x() {}\n").unwrap();
git(root, &["add", "side.rs"]);
git(root, &["commit", "-q", "-m", "divergent"]);
let orphaned = git(root, &["rev-parse", "HEAD"]);
git(root, &["checkout", "-f", "main"]);
commit_dispatch_journal(root, 1, &dispatch_row_toml("refs/heads/main", &orphaned));
make_slice(root, "s", "S", "2026-06-12");
set_status_raw(root, 1, "audit");
run_status(Some(root.to_path_buf()), 1, SliceStatus::Reconcile, None)
.expect("audit → reconcile is not gated by the integration check");
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt6_close_integration_composes_with_the_blocker_gate() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
git(root, &["checkout", "-q", "-b", "side"]);
std::fs::write(root.join("side.rs"), "fn x() {}\n").unwrap();
git(root, &["add", "side.rs"]);
git(root, &["commit", "-q", "-m", "divergent"]);
let orphaned = git(root, &["rev-parse", "HEAD"]);
git(root, &["checkout", "-f", "main"]);
commit_dispatch_journal(root, 1, &dispatch_row_toml("refs/heads/main", &orphaned));
slice_at_reconcile(root);
raise_blocker_rv(root, 1);
let err = expect_close_refused(root);
assert!(
err.contains("blocker review finding"),
"an unresolved blocker independently refuses: {err}"
);
assert_eq!(read_status(&slice_root(root), 1).unwrap(), "reconcile");
}
#[test]
fn vt6_close_integration_refuses_independently_of_the_blocker() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
git(root, &["checkout", "-q", "-b", "side"]);
std::fs::write(root.join("side.rs"), "fn x() {}\n").unwrap();
git(root, &["add", "side.rs"]);
git(root, &["commit", "-q", "-m", "divergent"]);
let orphaned = git(root, &["rev-parse", "HEAD"]);
git(root, &["checkout", "-f", "main"]);
commit_dispatch_journal(root, 1, &dispatch_row_toml("refs/heads/main", &orphaned));
slice_at_reconcile(root);
let err = expect_close_refused(root);
assert!(
err.contains("not integrated to trunk"),
"integration gate refuses with no blocker present: {err}"
);
}
#[test]
fn vt1c_close_integration_dispatch_ref_present_no_journal_succeeds() {
let (dir, _anchor) = drift_repo();
let root = dir.path();
commit_dispatch_no_journal(root, 1);
slice_at_reconcile(root);
expect_close_succeeds(root);
}
fn paths_slice_fixture(root: &Path, id: u32, extra: &[&str]) {
let slice_root = root.join(SLICE_DIR);
let name = format!("{id:03}");
let dir = slice_root.join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(format!("slice-{name}.toml")), "toml").unwrap();
std::fs::write(dir.join(format!("slice-{name}.md")), "md").unwrap();
for e in extra {
std::fs::write(dir.join(e), e).unwrap();
}
}
#[test]
fn paths_slice_full_output() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
paths_slice_fixture(root, 1, &["notes.md"]);
let sel = crate::paths::PathSelection {
toml: false,
md: false,
entity: false,
single: false,
};
let slice_root = root.join(SLICE_DIR);
let entity_dir = slice_root.join("001");
let set = crate::paths::scan_entity_dir(
&entity_dir,
&entity_dir.join("slice-001.toml"),
Some(&entity_dir.join("slice-001.md")),
root,
)
.unwrap();
let lines = crate::paths::select_paths(&set, &sel).unwrap();
assert_eq!(
lines,
vec![
".doctrine/slice/001/slice-001.toml",
".doctrine/slice/001/slice-001.md",
".doctrine/slice/001/notes.md"
]
);
}
#[test]
fn paths_slice_single() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
paths_slice_fixture(root, 1, &["design.md"]);
let sel = crate::paths::PathSelection {
toml: false,
md: false,
entity: false,
single: true,
};
let slice_root = root.join(SLICE_DIR);
let entity_dir = slice_root.join("001");
let set = crate::paths::scan_entity_dir(
&entity_dir,
&entity_dir.join("slice-001.toml"),
Some(&entity_dir.join("slice-001.md")),
root,
)
.unwrap();
let lines = crate::paths::select_paths(&set, &sel).unwrap();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], ".doctrine/slice/001/slice-001.toml");
}
#[test]
fn paths_slice_missing_entity_errors() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let slice_root = root.join(SLICE_DIR);
let result = crate::paths::scan_entity_dir(
&slice_root.join("999"),
&slice_root.join("999/slice-999.toml"),
Some(&slice_root.join("999/slice-999.md")),
root,
);
assert!(result.is_err());
}
#[test]
fn selector_intent_serde_kebab_case_round_trip() {
let selector: Selector =
toml::from_str("selector = \"src/x.rs\"\nintent = \"scope-relevant\"\n").unwrap();
assert_eq!(selector.intent, SelectorIntent::ScopeRelevant);
let selector: Selector =
toml::from_str("selector = \"src/y.rs\"\nintent = \"design-target\"\nnote = \"hi\"\n")
.unwrap();
assert_eq!(selector.intent, SelectorIntent::DesignTarget);
assert_eq!(selector.note.as_deref(), Some("hi"));
let err = toml::from_str::<Selector>("selector = \"x\"\nintent = \"bogus\"\n");
assert!(err.is_err(), "unknown intent should be rejected");
}
#[test]
fn slice_doc_without_selectors_deserializes_empty() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "no-selectors", "No Selectors", "2026-06-24");
let (doc, _toml_text, _body) = read_slice(&slice_root(root), 1).unwrap();
assert!(doc.selectors.is_empty());
}
#[test]
fn selector_add_and_read_back() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "test", "Test", "2026-06-24");
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, 1);
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
selector_upsert(&mut doc, "src/x.rs", "design-target", None).unwrap();
selector_upsert(&mut doc, "src/y.rs", "design-target", Some("shared note")).unwrap();
selector_upsert(&mut doc, "docs/*.md", "scope-relevant", None).unwrap();
fs::write(&toml_path, doc.to_string()).unwrap();
let (doc, _toml_text, _body) = read_slice(&slice_root, 1).unwrap();
assert_eq!(doc.selectors.len(), 3);
assert_eq!(doc.selectors[0].selector, "src/x.rs");
assert_eq!(doc.selectors[0].intent, SelectorIntent::DesignTarget);
assert_eq!(doc.selectors[0].note, None);
assert_eq!(doc.selectors[1].selector, "src/y.rs");
assert_eq!(doc.selectors[1].note.as_deref(), Some("shared note"));
assert_eq!(doc.selectors[2].selector, "docs/*.md");
assert_eq!(doc.selectors[2].intent, SelectorIntent::ScopeRelevant);
}
#[test]
fn selector_upsert_is_idempotent() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "test", "Test", "2026-06-24");
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, 1);
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
selector_upsert(&mut doc, "src/x.rs", "scope-relevant", Some("first")).unwrap();
fs::write(&toml_path, doc.to_string()).unwrap();
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
selector_upsert(&mut doc, "src/x.rs", "design-target", Some("updated")).unwrap();
fs::write(&toml_path, doc.to_string()).unwrap();
let (doc, _toml_text, _body) = read_slice(&slice_root, 1).unwrap();
assert_eq!(doc.selectors.len(), 1, "upsert should not duplicate");
assert_eq!(doc.selectors[0].selector, "src/x.rs");
assert_eq!(doc.selectors[0].intent, SelectorIntent::DesignTarget);
assert_eq!(doc.selectors[0].note.as_deref(), Some("updated"));
}
#[test]
fn selector_note_sets_per_file_note() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "test", "Test", "2026-06-24");
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, 1);
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
selector_upsert(&mut doc, "src/x.rs", "design-target", Some("shared")).unwrap();
selector_upsert(&mut doc, "src/y.rs", "design-target", Some("shared")).unwrap();
fs::write(&toml_path, doc.to_string()).unwrap();
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
assert!(selector_set_note(&mut doc, "src/x.rs", "per-file override"));
fs::write(&toml_path, doc.to_string()).unwrap();
let (doc, _toml_text, _body) = read_slice(&slice_root, 1).unwrap();
assert_eq!(doc.selectors.len(), 2);
assert_eq!(doc.selectors[0].note.as_deref(), Some("per-file override"));
assert_eq!(doc.selectors[1].note.as_deref(), Some("shared"));
}
#[test]
fn selector_note_missing_fails() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "test", "Test", "2026-06-24");
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, 1);
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
assert!(!selector_set_note(&mut doc, "nonexistent", "note"));
}
#[test]
fn selector_rm_variadic() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "test", "Test", "2026-06-24");
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, 1);
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
selector_upsert(&mut doc, "src/a.rs", "design-target", None).unwrap();
selector_upsert(&mut doc, "src/b.rs", "design-target", None).unwrap();
selector_upsert(&mut doc, "src/c.rs", "design-target", None).unwrap();
fs::write(&toml_path, doc.to_string()).unwrap();
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
let removed = selector_remove_many(&mut doc, &["src/a.rs".into(), "src/b.rs".into()]);
assert_eq!(removed, 2);
fs::write(&toml_path, doc.to_string()).unwrap();
let (doc, _toml_text, _body) = read_slice(&slice_root, 1).unwrap();
assert_eq!(doc.selectors.len(), 1);
assert_eq!(doc.selectors[0].selector, "src/c.rs");
}
#[test]
fn selector_rm_noop_on_missing() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
make_slice(root, "test", "Test", "2026-06-24");
let slice_root = root.join(SLICE_DIR);
let toml_path = slice_toml_path(&slice_root, 1);
let mut doc = fs::read_to_string(&toml_path)
.unwrap()
.parse::<toml_edit::DocumentMut>()
.unwrap();
let removed = selector_remove_many(&mut doc, &["nonexistent".into()]);
assert_eq!(removed, 0);
}
#[test]
fn fold_name_status_parses_each_class_and_renames() {
let mut actual: std::collections::BTreeMap<String, Vec<Status>> =
std::collections::BTreeMap::new();
fold_name_status_line("A\tsrc/new.rs", &mut actual);
fold_name_status_line("M\tsrc/state.rs", &mut actual);
fold_name_status_line("D\tsrc/gone.rs", &mut actual);
fold_name_status_line("R100\tsrc/old.rs\tsrc/moved.rs", &mut actual);
assert_eq!(actual["src/new.rs"], vec![Status::Added]);
assert_eq!(actual["src/state.rs"], vec![Status::Modified]);
assert_eq!(actual["src/gone.rs"], vec![Status::Deleted]);
assert_eq!(actual["src/old.rs"], vec![Status::Deleted]);
assert_eq!(actual["src/moved.rs"], vec![Status::Added]);
}
fn commit_file(repo: &Path, path: &str, content: &str) -> String {
let full = repo.join(path);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(&full, content).unwrap();
git(repo, &["add", path]);
git(repo, &["commit", "-q", "-m", &format!("touch {path}")]);
git(repo, &["rev-parse", "HEAD"])
}
fn write_phase_completed(repo: &Path, slice_id: u32, stem: &str) {
let dir = crate::state::phases_dir(repo, slice_id);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join(format!("{stem}.toml")), "status = \"completed\"\n").unwrap();
}
#[test]
fn conformance_partitions_into_the_three_cells_with_matched_selectors() {
let dir = tempfile::tempdir().unwrap();
let repo = fs::canonicalize(dir.path()).unwrap();
git(&repo, &["init", "-q", "-b", "main"]);
git(&repo, &["commit", "-q", "--allow-empty", "-m", "root"]);
let base = git(&repo, &["rev-parse", "HEAD"]);
make_slice(&repo, "conf", "Conformance Fixture", "2026-06-24");
run_selector_add(
Some(repo.clone()),
1,
SelectorIntent::DesignTarget,
&["src/**".to_string(), "docs/missing.md".to_string()],
None,
)
.unwrap();
run_selector_add(
Some(repo.clone()),
1,
SelectorIntent::ScopeRelevant,
&["Cargo.toml".to_string()],
None,
)
.unwrap();
let p1 = commit_file(&repo, "src/feature.rs", "fn f() {}\n");
let p2 = commit_file(&repo, "README.md", "surprise\n");
write_phase_completed(&repo, 1, "phase-01");
write_phase_completed(&repo, 1, "phase-02");
crate::state::record_source_delta(
&repo,
1,
crate::boundary::BoundaryRow {
phase: "PHASE-01".into(),
code_start_oid: base.clone(),
code_end_oid: p1.clone(),
provenance: crate::boundary::Provenance::Manual,
},
)
.unwrap();
crate::state::record_source_delta(
&repo,
1,
crate::boundary::BoundaryRow {
phase: "PHASE-02".into(),
code_start_oid: p1.clone(),
code_end_oid: p2.clone(),
provenance: crate::boundary::Provenance::Manual,
},
)
.unwrap();
let ConformanceOutcome::Computed(result) = conformance_outcome(&repo, 1).unwrap() else {
panic!("expected a computed outcome");
};
assert_eq!(result.conformant.len(), 1);
assert_eq!(result.conformant[0].path, "src/feature.rs");
assert_eq!(result.conformant[0].matched_selector, "src/**");
assert_eq!(result.undeclared.len(), 1);
assert_eq!(result.undeclared[0].path, "README.md");
assert_eq!(result.undeclared[0].verb, conformance::Verb::Added);
assert_eq!(result.undelivered, vec!["docs/missing.md".to_string()]);
}
#[test]
fn conformance_is_unavailable_with_no_recorded_deltas() {
let dir = tempfile::tempdir().unwrap();
let repo = fs::canonicalize(dir.path()).unwrap();
git(&repo, &["init", "-q", "-b", "main"]);
git(&repo, &["commit", "-q", "--allow-empty", "-m", "root"]);
make_slice(&repo, "conf", "Conf", "2026-06-24");
assert!(matches!(
conformance_outcome(&repo, 1).unwrap(),
ConformanceOutcome::Unavailable
));
}
#[test]
fn conformance_is_incomplete_on_partial_coverage() {
let dir = tempfile::tempdir().unwrap();
let repo = fs::canonicalize(dir.path()).unwrap();
git(&repo, &["init", "-q", "-b", "main"]);
git(&repo, &["commit", "-q", "--allow-empty", "-m", "root"]);
let base = git(&repo, &["rev-parse", "HEAD"]);
make_slice(&repo, "conf", "Conf", "2026-06-24");
let p1 = commit_file(&repo, "src/a.rs", "x\n");
write_phase_completed(&repo, 1, "phase-01");
write_phase_completed(&repo, 1, "phase-02"); crate::state::record_source_delta(
&repo,
1,
crate::boundary::BoundaryRow {
phase: "PHASE-01".into(),
code_start_oid: base,
code_end_oid: p1,
provenance: crate::boundary::Provenance::Manual,
},
)
.unwrap();
let ConformanceOutcome::Incomplete(gaps) = conformance_outcome(&repo, 1).unwrap() else {
panic!("expected incomplete");
};
assert!(
gaps.iter().any(|g| g.describe().contains("PHASE-02")),
"names the uncovered phase: {gaps:?}"
);
}
}