use std::fs;
use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use anyhow::{Context, bail};
const MAX_CLAIM_RETRIES: u32 = 128;
pub(crate) enum Acquired {
Won,
AlreadyHeld,
}
pub(crate) trait Reservation {
fn acquire(&self, claim: &Path) -> anyhow::Result<Acquired>;
}
pub(crate) struct LocalFs;
impl Reservation for LocalFs {
fn acquire(&self, claim: &Path) -> anyhow::Result<Acquired> {
match fs::create_dir(claim) {
Ok(()) => Ok(Acquired::Won),
Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(Acquired::AlreadyHeld),
Err(e) => Err(e).with_context(|| format!("Failed to claim {}", claim.display())),
}
}
}
pub(crate) enum MaterialiseMode {
AllocateFreshEntity,
CreateInExistingEntity,
}
pub(crate) struct Kind {
pub dir: &'static str,
pub prefix: &'static str,
pub mode: MaterialiseMode,
pub scaffold: fn(&ScaffoldCtx<'_>) -> anyhow::Result<Fileset>,
}
pub(crate) struct ScaffoldCtx<'a> {
pub id: u32,
pub canonical_id: &'a str,
pub slug: &'a str,
pub title: &'a str,
pub date: &'a str,
}
pub(crate) enum Artifact {
File { rel_path: PathBuf, body: String },
Symlink { rel_path: PathBuf, target: String },
}
pub(crate) type Fileset = Vec<Artifact>;
pub(crate) struct Inputs<'a> {
pub existing_id: Option<u32>,
pub slug: &'a str,
pub title: &'a str,
pub date: &'a str,
}
#[derive(Debug)]
pub(crate) struct Materialised {
pub id: u32,
pub dir: PathBuf,
}
pub(crate) fn candidate_id(existing: &[u32]) -> u32 {
existing.iter().copied().max().map_or(1, |m| m + 1)
}
pub(crate) fn derive_slug(title: &str) -> String {
let mut slug = String::new();
let mut pending_dash = false;
for ch in title.chars() {
if ch.is_ascii_alphanumeric() {
if pending_dash && !slug.is_empty() {
slug.push('-');
}
pending_dash = false;
slug.push(ch.to_ascii_lowercase());
} else if ch.is_whitespace() || ch == '-' || ch == '_' {
pending_dash = true;
}
}
slug
}
pub(crate) fn scan_ids(tree_root: &Path) -> anyhow::Result<Vec<u32>> {
let mut ids = Vec::new();
let entries = match fs::read_dir(tree_root) {
Ok(entries) => entries,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(ids),
Err(e) => {
return Err(e).with_context(|| format!("Failed to read {}", tree_root.display()));
}
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
if let Some(name) = entry.file_name().to_str()
&& let Ok(n) = name.parse::<u32>()
{
ids.push(n);
}
}
Ok(ids)
}
pub(crate) fn materialise(
kind: &Kind,
reservation: &dyn Reservation,
project_root: &Path,
inputs: &Inputs<'_>,
) -> anyhow::Result<Materialised> {
let tree_root = project_root.join(kind.dir);
fs::create_dir_all(&tree_root)
.with_context(|| format!("Failed to create {}", tree_root.display()))?;
match kind.mode {
MaterialiseMode::AllocateFreshEntity => {
allocate_fresh(kind, reservation, &tree_root, inputs, || {
scan_ids(&tree_root)
})
}
MaterialiseMode::CreateInExistingEntity => create_in_existing(kind, &tree_root, inputs),
}
}
fn allocate_fresh(
kind: &Kind,
reservation: &dyn Reservation,
tree_root: &Path,
inputs: &Inputs<'_>,
mut scan: impl FnMut() -> anyhow::Result<Vec<u32>>,
) -> anyhow::Result<Materialised> {
for _ in 0..MAX_CLAIM_RETRIES {
let id = candidate_id(&scan()?);
let name = format!("{id:03}");
let dir = tree_root.join(&name);
match reservation.acquire(&dir)? {
Acquired::Won => {
let canonical_id = format!("{}-{name}", kind.prefix);
let ctx = ScaffoldCtx {
id,
canonical_id: &canonical_id,
slug: inputs.slug,
title: inputs.title,
date: inputs.date,
};
return match scaffold_and_write(kind, tree_root, &ctx) {
Ok(()) => Ok(Materialised { id, dir }),
Err(e) => {
drop(fs::remove_dir_all(&dir));
Err(e)
}
};
}
Acquired::AlreadyHeld => {} }
}
bail!("Could not reserve an id after {MAX_CLAIM_RETRIES} attempts");
}
fn create_in_existing(
kind: &Kind,
tree_root: &Path,
inputs: &Inputs<'_>,
) -> anyhow::Result<Materialised> {
let id = inputs
.existing_id
.context("CreateInExistingEntity requires a parent id")?;
let name = format!("{id:03}");
let dir = tree_root.join(&name);
if !dir.is_dir() {
bail!("Parent entity {name} not found at {}", dir.display());
}
let canonical_id = format!("{}-{name}", kind.prefix);
let ctx = ScaffoldCtx {
id,
canonical_id: &canonical_id,
slug: inputs.slug,
title: inputs.title,
date: inputs.date,
};
let fileset = (kind.scaffold)(&ctx)?;
refuse_clobber(tree_root, &fileset)?; write_fileset(tree_root, &fileset)?;
Ok(Materialised { id, dir })
}
fn scaffold_and_write(kind: &Kind, tree_root: &Path, ctx: &ScaffoldCtx<'_>) -> anyhow::Result<()> {
let fileset = (kind.scaffold)(ctx)?;
write_fileset(tree_root, &fileset)
}
fn refuse_clobber(tree_root: &Path, fileset: &Fileset) -> anyhow::Result<()> {
for art in fileset {
let abs = safe_join(tree_root, artifact_rel(art))?;
if abs.exists() {
bail!("Refusing to overwrite existing {}", abs.display());
}
}
Ok(())
}
fn write_fileset(tree_root: &Path, fileset: &Fileset) -> anyhow::Result<()> {
for art in fileset {
let abs = safe_join(tree_root, artifact_rel(art))?;
match art {
Artifact::File { body, .. } => {
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
fs::write(&abs, body)
.with_context(|| format!("Failed to write {}", abs.display()))?;
}
Artifact::Symlink { target, .. } => {
if let Err(e) = std::os::unix::fs::symlink(target, &abs)
&& e.kind() != ErrorKind::AlreadyExists
{
return Err(e).with_context(|| format!("Failed to symlink {}", abs.display()));
}
}
}
}
Ok(())
}
fn artifact_rel(art: &Artifact) -> &Path {
match art {
Artifact::File { rel_path, .. } | Artifact::Symlink { rel_path, .. } => rel_path,
}
}
fn safe_join(tree_root: &Path, rel: &Path) -> anyhow::Result<PathBuf> {
if rel.is_absolute() {
bail!(
"Artifact path {} must be relative to the entity tree",
rel.display()
);
}
if rel.components().any(|c| c == Component::ParentDir) {
bail!(
"Artifact path {} must not escape the entity tree",
rel.display()
);
}
Ok(tree_root.join(rel))
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn candidate_id_empty_is_one() {
assert_eq!(candidate_id(&[]), 1);
}
#[test]
fn candidate_id_is_max_plus_one_ignoring_gaps() {
assert_eq!(candidate_id(&[1, 2, 3]), 4);
assert_eq!(candidate_id(&[1, 3]), 4);
assert_eq!(candidate_id(&[5]), 6);
}
#[test]
fn derive_slug_normalises_title() {
assert_eq!(derive_slug("Add skill removal"), "add-skill-removal");
assert_eq!(derive_slug("Hello, World!"), "hello-world");
assert_eq!(derive_slug(" trim edges "), "trim-edges");
assert_eq!(derive_slug("snake_and-dash"), "snake-and-dash");
}
#[test]
fn scan_ids_collects_numeric_dirs_only() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("001")).unwrap();
fs::create_dir(root.join("002")).unwrap();
fs::create_dir(root.join("not-a-slice")).unwrap();
fs::write(root.join("003"), "a file, not a dir").unwrap();
std::os::unix::fs::symlink("001", root.join("001-some-slug")).unwrap();
let mut ids = scan_ids(root).unwrap();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2]);
}
#[test]
fn scan_ids_missing_dir_is_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(scan_ids(&dir.path().join("nope")).unwrap().is_empty());
}
#[test]
fn safe_join_accepts_a_tree_relative_path() {
let joined = safe_join(Path::new("/tree"), Path::new("003/x.toml")).unwrap();
assert_eq!(joined, Path::new("/tree/003/x.toml"));
}
#[test]
fn safe_join_rejects_absolute_paths() {
let err = safe_join(Path::new("/tree"), Path::new("/etc/passwd")).unwrap_err();
assert!(err.to_string().contains("must be relative"));
}
#[test]
fn safe_join_rejects_parent_escape() {
let err = safe_join(Path::new("/tree"), Path::new("../../etc/passwd")).unwrap_err();
assert!(err.to_string().contains("must not escape"));
}
#[test]
fn local_fs_acquire_wins_then_already_held() {
let dir = tempfile::tempdir().unwrap();
let claim = dir.path().join("001");
assert!(matches!(LocalFs.acquire(&claim).unwrap(), Acquired::Won));
assert!(matches!(
LocalFs.acquire(&claim).unwrap(),
Acquired::AlreadyHeld
));
}
fn one_file(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let name = format!("{:03}", ctx.id);
Ok(vec![Artifact::File {
rel_path: PathBuf::from(format!("{name}/body.md")),
body: format!("{} :: {}", ctx.canonical_id, ctx.title),
}])
}
const TEST_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
mode: MaterialiseMode::AllocateFreshEntity,
scaffold: one_file,
};
fn inputs() -> Inputs<'static> {
Inputs {
existing_id: None,
slug: "s",
title: "T",
date: "2026-06-04",
}
}
#[test]
fn allocate_fresh_writes_then_lands_first_id() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let out =
allocate_fresh(&TEST_KIND, &LocalFs, &tree, &inputs(), || scan_ids(&tree)).unwrap();
assert_eq!(out.id, 1);
let body = fs::read_to_string(tree.join("001/body.md")).unwrap();
assert_eq!(body, "TK-001 :: T");
}
#[test]
fn allocate_fresh_retries_on_collision_through_the_seam() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
fs::create_dir(tree.join("001")).unwrap();
let calls = Cell::new(0u32);
let scan = || {
let n = calls.get();
calls.set(n + 1);
Ok(if n == 0 { vec![] } else { vec![1] })
};
let out = allocate_fresh(&TEST_KIND, &LocalFs, &tree, &inputs(), scan).unwrap();
assert_eq!(out.id, 2);
assert!(tree.join("002/body.md").is_file());
assert_eq!(calls.get(), 2, "expected one collision then success");
}
#[test]
fn allocate_fresh_bails_after_bounded_retries() {
struct AlwaysHeld;
impl Reservation for AlwaysHeld {
fn acquire(&self, _claim: &Path) -> anyhow::Result<Acquired> {
Ok(Acquired::AlreadyHeld)
}
}
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err =
allocate_fresh(&TEST_KIND, &AlwaysHeld, &tree, &inputs(), || Ok(vec![])).unwrap_err();
assert!(err.to_string().contains("Could not reserve an id"));
}
fn doomed_fileset(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let name = format!("{:03}", ctx.id);
Ok(vec![
Artifact::File {
rel_path: PathBuf::from(format!("{name}/a")),
body: "x".to_string(),
},
Artifact::File {
rel_path: PathBuf::from(format!("{name}/a/b")),
body: "y".to_string(),
},
])
}
const DOOMED_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
mode: MaterialiseMode::AllocateFreshEntity,
scaffold: doomed_fileset,
};
#[test]
fn reserved_materialise_write_failure_cleans_up_the_won_directory() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = allocate_fresh(&DOOMED_KIND, &LocalFs, &tree, &inputs(), || scan_ids(&tree))
.unwrap_err();
assert!(err.to_string().contains("Failed to create"));
assert!(!tree.join("001").exists(), "the won dir must be removed");
}
fn escaping_fileset(_ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
Ok(vec![Artifact::File {
rel_path: PathBuf::from("../escape.md"),
body: "x".to_string(),
}])
}
const ESCAPING_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
mode: MaterialiseMode::AllocateFreshEntity,
scaffold: escaping_fileset,
};
#[test]
fn reserved_materialise_rejects_an_escaping_descriptor_and_cleans_up() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = allocate_fresh(&ESCAPING_KIND, &LocalFs, &tree, &inputs(), || {
scan_ids(&tree)
})
.unwrap_err();
assert!(err.to_string().contains("must not escape"));
assert!(!tree.join("001").exists());
assert!(!dir.path().join("escape.md").exists());
}
const SUB_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
mode: MaterialiseMode::CreateInExistingEntity,
scaffold: one_file,
};
#[test]
fn create_in_existing_writes_under_the_parent_without_reserving() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(tree.join("003")).unwrap();
let out = create_in_existing(
&SUB_KIND,
&tree,
&Inputs {
existing_id: Some(3),
slug: "",
title: "Parent",
date: "2026-06-04",
},
)
.unwrap();
assert_eq!(out.id, 3);
let body = fs::read_to_string(tree.join("003/body.md")).unwrap();
assert_eq!(body, "TK-003 :: Parent");
}
#[test]
fn create_in_existing_errors_when_parent_absent() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = create_in_existing(
&SUB_KIND,
&tree,
&Inputs {
existing_id: Some(9),
slug: "",
title: "T",
date: "2026-06-04",
},
)
.unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[test]
fn create_in_existing_refuses_to_clobber() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(tree.join("003")).unwrap();
fs::write(tree.join("003/body.md"), "already here").unwrap();
let err = create_in_existing(
&SUB_KIND,
&tree,
&Inputs {
existing_id: Some(3),
slug: "",
title: "T",
date: "2026-06-04",
},
)
.unwrap_err();
assert!(err.to_string().contains("Refusing to overwrite"));
assert_eq!(
fs::read_to_string(tree.join("003/body.md")).unwrap(),
"already here"
);
}
}