use std::collections::BTreeMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::{Mutex, MutexGuard};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::index::IndexRecord;
use crate::parser::{parse_db_md, Config, Frontmatter};
use chrono::{DateTime, Datelike, FixedOffset};
const NON_CONTENT_BASENAMES: [&str; 1] = ["index.md"];
const TYPE_INDEX_FILE: &str = "index.jsonl";
#[derive(Debug, thiserror::Error)]
#[error("not a db.md store: {path} has no DB.md")]
pub struct NotAStore {
pub path: PathBuf,
}
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("failed to read type index {path}: {message}")]
BadTypeIndex {
path: PathBuf,
message: String,
},
#[error("cannot compute shard path for {file}: no usable date field")]
NoShardDate {
file: PathBuf,
},
#[error("search failed under {root}: {message}")]
Search {
root: PathBuf,
message: String,
},
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Layer {
Sources,
Records,
}
impl Layer {
pub fn dir_name(self) -> &'static str {
match self {
Layer::Sources => "sources",
Layer::Records => "records",
}
}
pub fn from_dir_name(name: &str) -> Option<Self> {
match name {
"sources" => Some(Layer::Sources),
"records" => Some(Layer::Records),
_ => None,
}
}
pub fn all() -> [Layer; 2] {
[Layer::Sources, Layer::Records]
}
}
#[derive(Debug, Clone)]
pub struct Store {
pub root: PathBuf,
root_locator: PathBuf,
pub config: Config,
root_capability: Arc<File>,
reader: Arc<Mutex<crate::fsx::BoundedDirReader>>,
}
pub struct StoreReader {
reader: crate::fsx::BoundedDirReader,
}
impl StoreReader {
pub fn open(&mut self, path: &Path) -> std::io::Result<File> {
self.reader.open(path)
}
}
pub struct StoreTransaction {
_lock: File,
}
fn absolute_store_locator(path: &Path) -> std::io::Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(std::env::current_dir()?.join(path))
}
}
impl Store {
pub(crate) fn from_held_root_strict(
display_path: &Path,
root_capability: File,
) -> crate::Result<Store> {
if !crate::fsx::directory_contains_exact_regular(&root_capability, "DB.md".as_ref())? {
return Err(NotAStore {
path: display_path.to_path_buf(),
}
.into());
}
let mut reader = crate::fsx::BoundedDirReader::from_root(&root_capability)?;
let bytes = reader.read(Path::new("DB.md"), crate::parser::MAX_DBMD_FILE_BYTES)?;
let text = String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let config = parse_db_md(&text, &display_path.join("DB.md"))?;
Ok(Store {
root: display_path.to_path_buf(),
root_locator: absolute_store_locator(display_path)?,
config,
root_capability: Arc::new(root_capability),
reader: Arc::new(Mutex::new(reader)),
})
}
pub fn is_db_md_store(path: &Path) -> bool {
let Ok(root) = crate::fsx::open_directory_nofollow(path) else {
return false;
};
crate::fsx::directory_contains_exact_regular(&root, "DB.md".as_ref()).unwrap_or(false)
}
pub fn open_strict(path: &Path) -> crate::Result<Store> {
let root_capability = match crate::fsx::open_directory_nofollow(path) {
Ok(root) => root,
Err(_) => {
return Err(NotAStore {
path: path.to_path_buf(),
}
.into())
}
};
if !crate::fsx::directory_contains_exact_regular(&root_capability, "DB.md".as_ref())? {
return Err(NotAStore {
path: path.to_path_buf(),
}
.into());
}
let db_md = path.join("DB.md");
let mut reader = crate::fsx::BoundedDirReader::from_root(&root_capability)?;
let bytes = reader.read(Path::new("DB.md"), crate::parser::MAX_DBMD_FILE_BYTES)?;
let text = String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let config = parse_db_md(&text, &db_md)?;
Ok(Store {
root: path.to_path_buf(),
root_locator: absolute_store_locator(path)?,
config,
root_capability: Arc::new(root_capability),
reader: Arc::new(Mutex::new(reader)),
})
}
pub fn open(path: &Path) -> Result<Store, NotAStore> {
let root_capability = crate::fsx::open_directory_nofollow(path).map_err(|_| NotAStore {
path: path.to_path_buf(),
})?;
if !crate::fsx::directory_contains_exact_regular(&root_capability, "DB.md".as_ref())
.unwrap_or(false)
{
return Err(NotAStore {
path: path.to_path_buf(),
});
}
let db_md = path.join("DB.md");
let mut reader = crate::fsx::BoundedDirReader::from_root(&root_capability)
.expect("held root descriptor can be cloned");
let config = match reader.read(Path::new("DB.md"), crate::parser::MAX_DBMD_FILE_BYTES) {
Ok(bytes) => String::from_utf8(bytes)
.ok()
.and_then(|text| parse_db_md(&text, &db_md).ok())
.unwrap_or_default(),
Err(_) => Config::default(),
};
Ok(Store {
root: path.to_path_buf(),
root_locator: absolute_store_locator(path).map_err(|_| NotAStore {
path: path.to_path_buf(),
})?,
config,
root_capability: Arc::new(root_capability),
reader: Arc::new(Mutex::new(reader)),
})
}
pub fn from_root_and_config(path: &Path, config: Config) -> std::io::Result<Store> {
let root_capability = crate::fsx::open_directory_nofollow(path)?;
let reader = crate::fsx::BoundedDirReader::from_root(&root_capability)?;
Ok(Store {
root: path.to_path_buf(),
root_locator: absolute_store_locator(path)?,
config,
root_capability: Arc::new(root_capability),
reader: Arc::new(Mutex::new(reader)),
})
}
fn cached_reader(&self) -> std::io::Result<MutexGuard<'_, crate::fsx::BoundedDirReader>> {
self.reader
.lock()
.map_err(|_| std::io::Error::other("store reader lock was poisoned"))
}
pub fn open_regular(&self, path: &Path) -> std::io::Result<File> {
let relative = self.capability_relative(path)?;
self.cached_reader()?.open(relative)
}
pub fn regular_reader(&self) -> std::io::Result<StoreReader> {
Ok(StoreReader {
reader: crate::fsx::BoundedDirReader::from_root(&self.root_capability)?,
})
}
pub fn read_bounded(&self, path: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
let relative = self.capability_relative(path)?;
self.cached_reader()?.read(relative, max_bytes)
}
pub fn read_text_bounded(&self, path: &Path, max_bytes: u64) -> std::io::Result<String> {
String::from_utf8(self.read_bounded(path, max_bytes)?)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}
pub fn write_atomic(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::write_atomic_beneath(&self.root_capability, relative, bytes, false, true)
}
pub fn write_atomic_new(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::write_atomic_beneath(&self.root_capability, relative, bytes, true, true)
}
pub(crate) fn write_private_atomic(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::write_private_atomic_beneath(&self.root_capability, relative, bytes, false)
}
pub(crate) fn write_private_atomic_new(
&self,
path: &Path,
bytes: &[u8],
) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::write_private_atomic_beneath(&self.root_capability, relative, bytes, true)
}
pub fn write_atomic_nondurable(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::write_atomic_nondurable_beneath(&self.root_capability, relative, bytes)
}
pub fn read_file(
&self,
path: &Path,
) -> Result<(crate::parser::Frontmatter, String), crate::parser::ParseError> {
let bytes = self
.read_bounded(path, crate::parser::MAX_DBMD_FILE_BYTES)
.map_err(crate::parser::ParseError::Io)?;
let text = String::from_utf8(bytes).map_err(|error| {
crate::parser::ParseError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
error,
))
})?;
let parsed = crate::parser::split_frontmatter(&text, path)?;
let frontmatter = crate::parser::Frontmatter::parse(&parsed.frontmatter_yaml, path)?;
Ok((frontmatter, parsed.body))
}
pub fn write_file(
&self,
path: &Path,
frontmatter: &crate::parser::Frontmatter,
body: &str,
) -> Result<(), crate::parser::ParseError> {
let contents = crate::parser::render_file(frontmatter, body);
self.write_atomic(path, contents.as_bytes())?;
Ok(())
}
pub fn write_file_new(
&self,
path: &Path,
frontmatter: &crate::parser::Frontmatter,
body: &str,
) -> Result<(), crate::parser::ParseError> {
let contents = crate::parser::render_file(frontmatter, body);
self.write_atomic_new(path, contents.as_bytes())?;
Ok(())
}
pub fn regular_metadata(&self, path: &Path) -> std::io::Result<std::fs::Metadata> {
let relative = self.capability_relative(path)?;
self.cached_reader()?.open(relative)?.metadata()
}
pub fn regular_file_exists(&self, path: &Path) -> std::io::Result<bool> {
match self.regular_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
}
}
pub fn directory_exists(&self, path: &Path) -> std::io::Result<bool> {
let relative = self.capability_relative(path)?;
crate::fsx::directory_exists_beneath(&self.root_capability, relative)
}
pub fn path_case_matches(&self, path: &Path) -> std::io::Result<bool> {
let relative = self.capability_relative(path)?;
crate::fsx::path_case_matches_beneath(&self.root_capability, relative)
}
pub fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::open_directory_beneath(&self.root_capability, relative, true).map(drop)
}
pub(crate) fn create_private_dir_all(&self, path: &Path) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
let directory = crate::fsx::open_directory_beneath(&self.root_capability, relative, true)?;
#[cfg(unix)]
{
use std::os::fd::AsRawFd as _;
if unsafe { libc::fchmod(directory.as_raw_fd(), 0o700) } != 0 {
return Err(std::io::Error::last_os_error());
}
directory.sync_all()?;
}
#[cfg(not(unix))]
drop(directory);
Ok(())
}
pub fn directory_names(&self, path: &Path) -> std::io::Result<Vec<std::ffi::OsString>> {
let relative = self.capability_relative(path)?;
crate::fsx::directory_names_beneath(&self.root_capability, relative)
}
pub fn transaction(&self) -> std::io::Result<StoreTransaction> {
Ok(StoreTransaction {
_lock: crate::fsx::lock_exclusive_beneath(
&self.root_capability,
Path::new(".dbmd.transaction.lock"),
)?,
})
}
pub(crate) fn lock_file(&self, path: &Path) -> std::io::Result<File> {
let relative = self.capability_relative(path)?;
crate::fsx::lock_exclusive_beneath(&self.root_capability, relative)
}
pub fn rename_noreplace(&self, old: &Path, new: &Path) -> std::io::Result<()> {
let old = self.capability_relative(old)?;
let new = self.capability_relative(new)?;
crate::fsx::rename_beneath(&self.root_capability, old, new)
}
pub fn remove_file(&self, path: &Path) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::remove_file_beneath(&self.root_capability, relative)
}
pub(crate) fn remove_private_tree(&self, path: &Path) -> std::io::Result<()> {
let relative = self.capability_relative(path)?;
crate::fsx::remove_tree_beneath(&self.root_capability, relative)
}
pub fn regular_file_names(&self, directory: &Path) -> std::io::Result<Vec<std::ffi::OsString>> {
let relative = self.capability_relative(directory)?;
crate::fsx::regular_file_names_beneath(&self.root_capability, relative)
}
pub fn walk_regular_files(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
let relative = self.capability_relative(directory)?;
crate::fsx::walk_regular_files_beneath(&self.root_capability, relative)
}
pub fn capability_relative<'a>(&self, path: &'a Path) -> std::io::Result<&'a Path> {
let relative = if path.is_absolute() {
if let Ok(relative) = path.strip_prefix(&self.root_locator) {
relative
} else {
#[cfg(target_os = "macos")]
{
let mut matched = None;
for (alias, canonical) in [("/var", "/private/var"), ("/tmp", "/private/tmp")] {
if let Ok(suffix) = self.root_locator.strip_prefix(alias) {
let equivalent_root = Path::new(canonical).join(suffix);
if let Ok(relative) = path.strip_prefix(equivalent_root) {
matched = Some(relative);
break;
}
}
if let Ok(suffix) = self.root_locator.strip_prefix(canonical) {
let equivalent_root = Path::new(alias).join(suffix);
if let Ok(relative) = path.strip_prefix(equivalent_root) {
matched = Some(relative);
break;
}
}
}
matched.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} is outside store {}",
path.display(),
self.root_locator.display()
),
)
})?
}
#[cfg(not(target_os = "macos"))]
{
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} is outside store {}",
path.display(),
self.root_locator.display()
),
));
}
}
} else {
path
};
if relative.components().any(|component| {
matches!(
component,
std::path::Component::ParentDir
| std::path::Component::RootDir
| std::path::Component::Prefix(_)
)
}) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"store capability requires a contained relative path",
));
}
Ok(relative)
}
pub fn walk(&self) -> Result<Vec<PathBuf>, StoreError> {
let mut out = Vec::new();
for layer in Layer::all() {
out.extend(self.walk_layer(layer)?);
}
out.sort();
Ok(out)
}
pub fn walk_layer(&self, layer: Layer) -> Result<Vec<PathBuf>, StoreError> {
self.walk_content_md(Path::new(layer.dir_name()))
}
pub fn walk_type_folder(&self, type_folder: &Path) -> Result<Vec<PathBuf>, StoreError> {
let relative = self.capability_relative(type_folder)?;
self.walk_content_md(relative)
}
pub fn nested_store_roots(&self) -> Result<Vec<PathBuf>, StoreError> {
Ok(crate::fsx::ownership_boundaries_beneath(&self.root_capability)?.1)
}
pub fn owns_path(&self, candidate: &Path) -> bool {
self.capability_relative(candidate)
.and_then(|relative| {
if self.regular_file_exists(relative)? || self.directory_exists(relative)? {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"store path does not exist",
))
}
})
.is_ok()
}
pub fn unowned_symlinks(&self) -> Result<Vec<PathBuf>, StoreError> {
Ok(crate::fsx::ownership_boundaries_beneath(&self.root_capability)?.0)
}
pub fn recent_in_type_folder(
&self,
type_folder: &Path,
n: usize,
) -> Result<Vec<PathBuf>, StoreError> {
let files = self.walk_type_folder(type_folder)?;
let mut keyed: Vec<(Option<DateTime<FixedOffset>>, PathBuf)> = files
.into_iter()
.map(|rel| {
let updated = self.read_updated(&rel);
(updated, rel)
})
.collect();
keyed.sort_by(|a, b| {
let by_updated = b.0.cmp(&a.0);
by_updated.then_with(|| a.1.cmp(&b.1))
});
keyed.truncate(n);
Ok(keyed.into_iter().map(|(_, rel)| rel).collect())
}
pub fn type_shards(&self, type_: &str) -> bool {
if let Some(shard) = self.config.schemas.get(type_).and_then(|s| s.shard) {
return shard;
}
matches!(
type_,
"email" | "transcript" | "pdf-source" | "note"
| "expense" | "invoice" | "meeting"
| "order" | "ticket" | "transaction"
)
}
pub fn shard_path_for(
&self,
type_: &str,
frontmatter: &Frontmatter,
name: &str,
) -> Result<PathBuf, StoreError> {
self.shard_path_in(&default_type_folder(type_), type_, frontmatter, name)
}
pub fn shard_path_in(
&self,
folder: &Path,
type_: &str,
frontmatter: &Frontmatter,
name: &str,
) -> Result<PathBuf, StoreError> {
let folder = folder.to_path_buf();
let filename = ensure_md_extension(name);
if !self.type_shards(type_) {
return Ok(folder.join(filename));
}
let (year, month) = self
.primary_shard_segment(type_, frontmatter)
.ok_or_else(|| StoreError::NoShardDate {
file: folder.join(&filename),
})?;
Ok(folder.join(year).join(month).join(filename))
}
pub fn find_links_to(&self, target: &Path) -> Result<Vec<PathBuf>, StoreError> {
self.find_links_to_any(&[target.to_path_buf()])
}
pub fn find_links_to_any(&self, targets: &[PathBuf]) -> Result<Vec<PathBuf>, StoreError> {
let want: std::collections::HashSet<String> = targets
.iter()
.filter_map(|t| {
let canonical = canonical_link_target(&t.to_string_lossy());
if canonical.is_empty() {
None
} else {
Some(link_edge_key(&canonical))
}
})
.collect();
if want.is_empty() {
return Ok(Vec::new());
}
let mut hits = std::collections::BTreeSet::new();
let mut reader =
crate::fsx::BoundedDirReader::from_root(&self.root_capability).map_err(|error| {
StoreError::Search {
root: self.root.clone(),
message: format!("could not hold the store read capability: {error}"),
}
})?;
for rel in self.walk_all_md()? {
let bytes = match reader.read(&rel, crate::parser::MAX_DBMD_FILE_BYTES) {
Ok(bytes) => bytes,
Err(error) => {
return Err(StoreError::Search {
root: self.root.clone(),
message: format!("read failed in {}: {error}", rel.display()),
})
}
};
let text = String::from_utf8_lossy(&bytes);
for target in extract_edge_targets(&text) {
if want.contains(&link_edge_key(&target)) {
hits.insert(rel);
break;
}
}
}
Ok(hits.into_iter().collect())
}
pub fn find_by_type(&self, type_: &str) -> Result<Vec<IndexRecord>, StoreError> {
let canonical_folder = default_type_folder(type_);
let records = self.read_all_type_indexes_in(layer_of_folder(&canonical_folder))?;
Ok(records.into_iter().filter(|r| r.type_ == type_).collect())
}
pub fn find_by_where(&self, key: &str, value: &str) -> Result<Vec<IndexRecord>, StoreError> {
self.find_by_where_in(key, value, None)
}
pub fn find_by_where_in(
&self,
key: &str,
value: &str,
layer: Option<Layer>,
) -> Result<Vec<IndexRecord>, StoreError> {
let records = self.read_all_type_indexes_in(layer)?;
Ok(records
.into_iter()
.filter(|r| record_matches_field(r, key, value))
.collect())
}
pub fn sidecar_records(&self, layer: Option<Layer>) -> Result<Vec<IndexRecord>, StoreError> {
self.read_all_type_indexes_in(layer)
}
pub fn read_type_index(&self, index_jsonl: &Path) -> Result<Vec<IndexRecord>, StoreError> {
let bytes = self
.read_bounded(index_jsonl, crate::parser::MAX_DBMD_FILE_BYTES)
.map_err(|e| StoreError::BadTypeIndex {
path: index_jsonl.to_path_buf(),
message: e.to_string(),
})?;
let text = String::from_utf8(bytes).map_err(|e| StoreError::BadTypeIndex {
path: index_jsonl.to_path_buf(),
message: e.to_string(),
})?;
let mut by_path: BTreeMap<PathBuf, IndexRecord> = BTreeMap::new();
for (i, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let record: IndexRecord =
serde_json::from_str(trimmed).map_err(|e| StoreError::BadTypeIndex {
path: index_jsonl.to_path_buf(),
message: format!("line {}: {e}", i + 1),
})?;
by_path.insert(record.path.clone(), record);
}
Ok(by_path.into_values().collect())
}
pub fn abs_path(&self, store_relative: &Path) -> PathBuf {
self.root.join(store_relative)
}
pub fn rel_path(&self, abs: &Path) -> Option<PathBuf> {
abs.strip_prefix(&self.root).ok().map(|p| p.to_path_buf())
}
fn walk_content_md(&self, root: &Path) -> Result<Vec<PathBuf>, StoreError> {
let mut out = Vec::new();
let files = match crate::fsx::walk_regular_files_beneath(&self.root_capability, root) {
Ok(files) => files,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(error) => return Err(StoreError::Io(error)),
};
for path in files {
if !has_md_extension(&path) {
continue;
}
if is_non_content_basename(&path) {
continue;
}
out.push(path);
}
out.sort();
Ok(out)
}
fn walk_all_md(&self) -> Result<Vec<PathBuf>, StoreError> {
let mut out = Vec::new();
for path in crate::fsx::walk_regular_files_beneath(&self.root_capability, Path::new(""))? {
if !has_md_extension(&path) {
continue;
}
if path.components().next().map(|c| c.as_os_str()) == Some("log".as_ref()) {
continue;
}
out.push(path);
}
out.sort();
Ok(out)
}
fn read_all_type_indexes_in(
&self,
layer: Option<Layer>,
) -> Result<Vec<IndexRecord>, StoreError> {
let mut out = Vec::new();
for sidecar in self.find_type_index_files_in(layer)? {
out.extend(self.read_type_index(&sidecar)?);
}
Ok(out)
}
fn find_type_index_files_in(&self, layer: Option<Layer>) -> Result<Vec<PathBuf>, StoreError> {
let Some(layer) = layer else {
let mut out = Vec::new();
for l in Layer::all() {
out.extend(self.find_type_index_files_in(Some(l))?);
}
out.sort();
return Ok(out);
};
let mut out = Vec::new();
let paths = match crate::fsx::walk_regular_files_beneath(
&self.root_capability,
Path::new(layer.dir_name()),
) {
Ok(paths) => paths,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(error) => return Err(StoreError::Io(error)),
};
for path in paths {
if path.file_name().and_then(|n| n.to_str()) != Some(TYPE_INDEX_FILE) {
continue;
}
out.push(path);
}
out.sort();
Ok(out)
}
fn read_updated(&self, path: &Path) -> Option<DateTime<FixedOffset>> {
let bytes = self
.read_bounded(path, crate::parser::MAX_DBMD_FILE_BYTES)
.ok()?;
let text = String::from_utf8(bytes).ok()?;
let yaml = frontmatter_block(&text)?;
let value: serde_norway::Value = serde_norway::from_str(yaml).ok()?;
let raw = value.get("updated")?;
value_to_datetime(raw)
}
fn primary_shard_segment(&self, type_: &str, fm: &Frontmatter) -> Option<(String, String)> {
if let Some(field) = primary_date_field(type_) {
if let Some(v) = fm.extra.get(field) {
if let Some(seg) = value_to_year_month(v) {
return Some(seg);
}
}
}
fm.created
.map(|dt| (format!("{:04}", dt.year()), format!("{:02}", dt.month())))
}
}
pub fn ensure_path_within_store(store_root: &Path, candidate: &Path) -> std::io::Result<PathBuf> {
reject_parent_components(store_root, candidate)?;
reject_symlink_tail(store_root, candidate)?;
let root = store_root.canonicalize()?;
let resolved = resolve_within(&root, store_root, candidate)?;
reject_nested_store_boundary(&root, &resolved, candidate, store_root)?;
Ok(resolved)
}
fn reject_symlink_tail(store_root: &Path, candidate: &Path) -> std::io::Result<()> {
let lexical_root = platform_lexical_path(store_root);
let lexical_candidate = platform_lexical_path(candidate);
let tail = lexical_candidate
.strip_prefix(&lexical_root)
.unwrap_or(&lexical_candidate);
let mut cursor = if lexical_candidate.starts_with(&lexical_root) {
lexical_root
} else if lexical_candidate.is_absolute() {
PathBuf::from(std::path::MAIN_SEPARATOR.to_string())
} else {
PathBuf::new()
};
for component in tail.components() {
match component {
std::path::Component::Prefix(_) | std::path::Component::RootDir => continue,
std::path::Component::CurDir => continue,
std::path::Component::ParentDir => {
continue;
}
std::path::Component::Normal(name) => cursor.push(name),
}
match std::fs::symlink_metadata(&cursor) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} crosses symlink component {}",
candidate.display(),
cursor.display()
),
));
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => break,
Err(error) => return Err(error),
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn platform_lexical_path(path: &Path) -> PathBuf {
for (alias, canonical) in [("/var", "/private/var"), ("/tmp", "/private/tmp")] {
if let Ok(tail) = path.strip_prefix(alias) {
return Path::new(canonical).join(tail);
}
}
path.to_path_buf()
}
#[cfg(not(target_os = "macos"))]
fn platform_lexical_path(path: &Path) -> PathBuf {
path.to_path_buf()
}
fn reject_nested_store_boundary(
root: &Path,
resolved: &Path,
candidate: &Path,
store_root: &Path,
) -> std::io::Result<()> {
let Ok(rel) = resolved.strip_prefix(root) else {
return Err(outside_store_err(candidate, store_root));
};
if rel != Path::new("DB.md")
&& resolved.file_name().and_then(|name| name.to_str()) == Some("DB.md")
{
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} would create or address a nested db.md store marker",
candidate.display()
),
));
}
let mut cursor = root.to_path_buf();
for component in rel.components() {
cursor.push(component.as_os_str());
if cursor.is_dir() && Store::is_db_md_store(&cursor) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} crosses nested db.md store boundary {}",
candidate.display(),
cursor.display()
),
));
}
}
Ok(())
}
fn reject_parent_components(store_root: &Path, candidate: &Path) -> std::io::Result<()> {
let scrutinized = candidate.strip_prefix(store_root).unwrap_or(candidate);
if scrutinized
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} contains a `..` component beyond the store root {} and cannot be contained",
candidate.display(),
store_root.display()
),
));
}
Ok(())
}
fn resolve_within(root: &Path, store_root: &Path, candidate: &Path) -> std::io::Result<PathBuf> {
let mut existing = candidate.to_path_buf();
let mut tail: Vec<std::ffi::OsString> = Vec::new();
let resolved_prefix = loop {
match existing.canonicalize() {
Ok(p) => break p,
Err(_) => {
match existing.file_name() {
Some(name) => {
tail.push(name.to_os_string());
if !existing.pop() {
break root.to_path_buf();
}
}
None => {
break root.to_path_buf();
}
}
}
}
};
let mut resolved = resolved_prefix;
for name in tail.into_iter().rev() {
resolved.push(name);
}
if resolved.starts_with(root) {
Ok(resolved)
} else {
Err(outside_store_err(candidate, store_root))
}
}
fn outside_store_err(candidate: &Path, store_root: &Path) -> std::io::Error {
std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"path {} resolves outside the store root {}",
candidate.display(),
store_root.display()
),
)
}
pub struct StoreContainment {
store_root: PathBuf,
root: PathBuf,
dirs: BTreeMap<PathBuf, PathBuf>,
}
impl StoreContainment {
pub fn new(store_root: &Path) -> std::io::Result<Self> {
Ok(Self {
store_root: store_root.to_path_buf(),
root: store_root.canonicalize()?,
dirs: BTreeMap::new(),
})
}
pub fn resolve(&mut self, candidate: &Path) -> std::io::Result<PathBuf> {
reject_parent_components(&self.store_root, candidate)?;
if let (Ok(meta), Some(parent), Some(name)) = (
std::fs::symlink_metadata(candidate),
candidate.parent(),
candidate.file_name(),
) {
if !meta.file_type().is_symlink() {
let canon_parent = match self.dirs.get(parent) {
Some(p) => p.clone(),
None => {
reject_symlink_tail(&self.store_root, parent)?;
let p = parent.canonicalize()?;
self.dirs.insert(parent.to_path_buf(), p.clone());
p
}
};
let resolved = canon_parent.join(name);
if !resolved.starts_with(&self.root) {
return Err(outside_store_err(candidate, &self.store_root));
}
reject_nested_store_boundary(&self.root, &resolved, candidate, &self.store_root)?;
return Ok(resolved);
}
}
reject_symlink_tail(&self.store_root, candidate)?;
let resolved = resolve_within(&self.root, &self.store_root, candidate)?;
reject_nested_store_boundary(&self.root, &resolved, candidate, &self.store_root)?;
Ok(resolved)
}
}
pub fn canonical_link_target(raw: &str) -> String {
let mut s = raw.trim().replace('\\', "/");
while let Some(rest) = s.strip_prefix("./") {
s = rest.to_string();
}
let s = s.trim_start_matches('/');
let s = s.strip_suffix(".md").unwrap_or(s);
s.trim().to_string()
}
pub fn link_edge_key(canonical_target: &str) -> String {
use unicode_normalization::UnicodeNormalization;
let nfc: String = canonical_target.nfc().collect();
if fs_is_case_insensitive() {
nfc.to_ascii_lowercase()
} else {
nfc
}
}
pub fn extract_edge_targets(text: &str) -> Vec<String> {
let mut out = Vec::new();
let body = match split_frontmatter_raw(text) {
Some((frontmatter, body)) => {
for line in frontmatter.lines() {
push_edges_in_line(line, &mut out);
}
body
}
None => text,
};
let mut fence: Option<(u8, usize)> = None;
for line in body.lines() {
let content = line.trim_end_matches('\r');
if let Some(f) = fence {
if fence_closes(content, f) {
fence = None;
}
continue;
}
if let Some(opened) = fence_opens(content) {
fence = Some(opened);
continue;
}
push_edges_in_line(line, &mut out);
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdgeSpan {
pub target: String,
pub raw: String,
pub alias: Option<String>,
pub start: usize,
pub end: usize,
}
pub fn extract_edge_spans(body: &str) -> Vec<EdgeSpan> {
let mut out = Vec::new();
let mut fence: Option<(u8, usize)> = None;
let mut base = 0usize;
for line in body.split_inclusive('\n') {
let trimmed_len = line.trim_end_matches('\n').len();
let content = line[..trimmed_len].trim_end_matches('\r');
if let Some(f) = fence {
if fence_closes(content, f) {
fence = None;
}
} else if let Some(opened) = fence_opens(content) {
fence = Some(opened);
} else {
push_edge_spans_in_line(content, base, &mut out);
}
base += line.len();
}
out
}
fn push_edge_spans_in_line(line: &str, base: usize, out: &mut Vec<EdgeSpan>) {
let bytes = line.as_bytes();
let mut i = 0usize;
while i + 1 < bytes.len() {
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
if let Some(close) = line[i + 2..].find("]]") {
let inner = &line[i + 2..i + 2 + close];
let end = i + 2 + close + 2;
let mut parts = inner.splitn(2, '|');
let raw_target = parts.next().unwrap_or(inner).trim();
let alias = parts.next().map(str::trim).filter(|a| !a.is_empty());
if !raw_target.is_empty() && !raw_target.starts_with('[') {
let canonical = canonical_link_target(raw_target);
if !canonical.is_empty() {
out.push(EdgeSpan {
target: canonical,
raw: inner.to_string(),
alias: alias.map(str::to_string),
start: base + i,
end: base + end,
});
}
}
i = end;
continue;
}
}
i += 1;
}
}
fn push_edges_in_line(line: &str, out: &mut Vec<String>) {
let bytes = line.as_bytes();
let mut i = 0usize;
while i + 1 < bytes.len() {
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
if let Some(close) = line[i + 2..].find("]]") {
let inner = &line[i + 2..i + 2 + close];
let raw_target = inner.split('|').next().unwrap_or(inner).trim();
if !raw_target.is_empty() && !raw_target.starts_with('[') {
let canonical = canonical_link_target(raw_target);
if !canonical.is_empty() {
out.push(canonical);
}
}
i = i + 2 + close + 2;
continue;
}
}
i += 1;
}
}
pub fn fence_opens(line: &str) -> Option<(u8, usize)> {
let indent = line.len() - line.trim_start_matches(' ').len();
if indent > 3 {
return None;
}
let rest = &line[indent..];
let byte = rest.bytes().next()?;
if byte != b'`' && byte != b'~' {
return None;
}
let run = rest.len() - rest.trim_start_matches(byte as char).len();
if run < 3 {
return None;
}
if byte == b'`' && rest[run..].contains('`') {
return None;
}
Some((byte, run))
}
pub fn fence_closes(line: &str, fence: (u8, usize)) -> bool {
let (byte, open_len) = fence;
let indent = line.len() - line.trim_start_matches(' ').len();
if indent > 3 {
return false;
}
let rest = &line[indent..];
let run = rest.len() - rest.trim_start_matches(byte as char).len();
if run < open_len {
return false;
}
rest[run..].trim().is_empty()
}
fn fs_is_case_insensitive() -> bool {
use std::sync::OnceLock;
static CASE_INSENSITIVE: OnceLock<bool> = OnceLock::new();
*CASE_INSENSITIVE.get_or_init(|| {
let dir = std::env::temp_dir();
let pid = std::process::id();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let lower = dir.join(format!(".dbmd-case-probe-{pid}-{nanos}"));
let upper = dir.join(format!(".DBMD-CASE-PROBE-{pid}-{nanos}"));
let result = match std::fs::File::create(&lower) {
Ok(_) => upper.is_file(),
Err(_) => false,
};
let _ = std::fs::remove_file(&lower);
result
})
}
fn has_md_extension(path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()) == Some("md")
}
fn is_non_content_basename(path: &Path) -> bool {
match path.file_name().and_then(|n| n.to_str()) {
Some(name) => NON_CONTENT_BASENAMES.contains(&name),
None => false,
}
}
fn ensure_md_extension(name: &str) -> String {
if name.ends_with(".md") {
name.to_string()
} else {
format!("{name}.md")
}
}
fn default_type_folder(type_: &str) -> PathBuf {
let path = match type_ {
"email" => "sources/emails",
"transcript" => "sources/transcripts",
"pdf-source" => "sources/docs",
"note" => "sources/notes",
"contact" => "records/contacts",
"company" => "records/companies",
"expense" => "records/expenses",
"meeting" => "records/meetings",
"decision" => "records/decisions",
"invoice" => "records/invoices",
other => return PathBuf::from("records").join(other),
};
PathBuf::from(path)
}
pub fn layer_for_type(type_: &str) -> Layer {
layer_of_folder(&default_type_folder(type_)).unwrap_or(Layer::Records)
}
fn layer_of_folder(folder: &Path) -> Option<Layer> {
let first = folder.components().next()?.as_os_str().to_str()?;
Layer::from_dir_name(first)
}
pub fn is_content_path(rel: &Path) -> bool {
if layer_of_folder(rel).is_none() {
return false;
}
if rel.extension().and_then(|e| e.to_str()) != Some("md") {
return false;
}
rel.file_name().and_then(|n| n.to_str()) != Some("index.md")
}
pub fn infer_type_from_path(rel: &Path) -> Option<String> {
let mut comps = rel.components().filter_map(|c| c.as_os_str().to_str());
let layer = comps.next()?;
if !matches!(layer, "sources" | "records") {
return None;
}
let folder = comps.next()?;
comps.next()?;
let mapped = match (layer, folder) {
("sources", "emails") => "email",
("sources", "transcripts") => "transcript",
("sources", "docs") => "pdf-source",
("sources", "notes") => "note",
("records", "contacts") => "contact",
("records", "companies") => "company",
("records", "expenses") => "expense",
("records", "meetings") => "meeting",
("records", "decisions") => "decision",
("records", "invoices") => "invoice",
(_, other) => other,
};
Some(mapped.to_string())
}
fn primary_date_field(type_: &str) -> Option<&'static str> {
match type_ {
"email" => Some("date"),
"transcript" => Some("recorded_at"),
"pdf-source" => Some("received_at"),
"note" => Some("told_at"),
"expense" | "invoice" | "meeting" => Some("date"),
_ => None,
}
}
fn value_to_datetime(value: &serde_norway::Value) -> Option<DateTime<FixedOffset>> {
let s = yaml_scalar_string(value)?;
DateTime::parse_from_rfc3339(s.trim()).ok()
}
fn value_to_year_month(value: &serde_norway::Value) -> Option<(String, String)> {
let s = yaml_scalar_string(value)?;
year_month_from_str(s.trim())
}
fn year_month_from_str(s: &str) -> Option<(String, String)> {
let mut parts = s.splitn(3, '-');
let year = parts.next()?;
let month_part = parts.next()?;
if year.len() != 4 || !year.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
if month_part.is_empty()
|| month_part.len() > 2
|| !month_part.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let month: u8 = month_part.parse().ok()?;
if !(1..=12).contains(&month) {
return None;
}
Some((year.to_string(), format!("{month:02}")))
}
fn yaml_scalar_string(value: &serde_norway::Value) -> Option<String> {
if let Some(s) = value.as_str() {
return Some(s.to_string());
}
match value {
serde_norway::Value::Null => None,
serde_norway::Value::Mapping(_) | serde_norway::Value::Sequence(_) => None,
other => serde_norway::to_string(other)
.ok()
.map(|s| s.trim().to_string()),
}
}
fn frontmatter_block(text: &str) -> Option<&str> {
let body = text.strip_prefix('\u{feff}').unwrap_or(text);
let mut rest = body;
let (first, after_first) = split_first_line(rest);
if first.trim_end() != "---" {
return None;
}
rest = after_first;
let block_start = rest;
let mut scanned = 0usize;
loop {
let (line, after) = split_first_line(rest);
if line.trim_end() == "---" {
return Some(&block_start[..scanned]);
}
if after.is_empty() && line.is_empty() {
return None;
}
scanned += line.len() + 1; if after.is_empty() {
return None;
}
rest = after;
}
}
fn split_frontmatter_raw(text: &str) -> Option<(&str, &str)> {
let stripped = text.strip_prefix('\u{feff}').unwrap_or(text);
let (first, after_first) = split_first_line(stripped);
if first.trim_end() != "---" {
return None;
}
let block_start = after_first;
let mut scanned = 0usize;
let mut rest = after_first;
loop {
let (line, after) = split_first_line(rest);
if line.trim_end() == "---" {
return Some((&block_start[..scanned], after));
}
if after.is_empty() && line.is_empty() {
return None; }
scanned += line.len() + 1; if after.is_empty() {
return None; }
rest = after;
}
}
fn split_first_line(s: &str) -> (&str, &str) {
match s.find('\n') {
Some(i) => (&s[..i], &s[i + 1..]),
None => (s, ""),
}
}
fn record_matches_field(record: &IndexRecord, key: &str, value: &str) -> bool {
match key {
"type" => record.type_ == value,
"summary" => record.summary == value,
"path" => record.path.to_string_lossy() == value,
"created" => timestamp_matches(record.created, value),
"updated" => timestamp_matches(record.updated, value),
"tags" => record.tags.iter().any(|t| t == value),
"links" => record.links.iter().any(|l| l == value),
other => record
.fields
.get(other)
.map(|v| json_value_matches(v, value))
.unwrap_or(false),
}
}
fn timestamp_matches(stored: Option<DateTime<FixedOffset>>, value: &str) -> bool {
match (stored, DateTime::parse_from_rfc3339(value)) {
(Some(stored), Ok(queried)) => stored == queried,
_ => false,
}
}
fn number_matches(n: &serde_json::Number, value: &str) -> bool {
if n.to_string() == value {
return true;
}
if n.is_f64() {
if let (Some(stored), Ok(q)) = (n.as_f64(), value.parse::<f64>()) {
return stored == q;
}
}
false
}
fn json_value_matches(v: &serde_json::Value, value: &str) -> bool {
match v {
serde_json::Value::String(s) => s == value,
serde_json::Value::Bool(b) => b.to_string() == value,
serde_json::Value::Number(n) => number_matches(n, value),
serde_json::Value::Array(items) => items.iter().any(|i| json_value_matches(i, value)),
serde_json::Value::Null => false,
serde_json::Value::Object(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::{tempdir, TempDir};
fn write(root: &Path, rel: &str, contents: &str) -> PathBuf {
let abs = root.join(rel);
fs::create_dir_all(abs.parent().unwrap()).unwrap();
fs::write(&abs, contents).unwrap();
PathBuf::from(rel)
}
fn content_md(updated: &str) -> String {
format!(
"---\ntype: note\ncreated: {updated}\nupdated: {updated}\nsummary: a note\n---\n\nbody\n"
)
}
fn empty_store() -> TempDir {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: Test\n---\n\n# Store\n",
)
.unwrap();
dir
}
fn open(dir: &TempDir) -> Store {
Store::open(dir.path()).expect("fixture should be a valid store")
}
fn rels(paths: &[PathBuf]) -> Vec<String> {
paths
.iter()
.map(|p| p.to_string_lossy().replace('\\', "/"))
.collect()
}
#[test]
fn layer_dir_name_and_parse_are_inverse() {
for layer in Layer::all() {
assert_eq!(Layer::from_dir_name(layer.dir_name()), Some(layer));
}
assert_eq!(Layer::Sources.dir_name(), "sources");
assert_eq!(Layer::Records.dir_name(), "records");
assert_eq!(Layer::from_dir_name("wiki"), None);
assert_eq!(Layer::from_dir_name("log"), None);
assert_eq!(Layer::from_dir_name("Sources"), None); }
#[test]
fn layer_order_is_canonical() {
let mut v = [Layer::Records, Layer::Sources];
v.sort();
assert_eq!(v, [Layer::Sources, Layer::Records]);
}
#[test]
fn is_content_path_is_layer_rooted_and_excludes_non_layer_files() {
assert!(is_content_path(Path::new("records/contacts/alice.md")));
assert!(is_content_path(Path::new("sources/emails/2026/05/x.md")));
assert!(!is_content_path(Path::new("DB.md")));
assert!(!is_content_path(Path::new("log.md")));
assert!(!is_content_path(Path::new("NOTES.md")));
assert!(!is_content_path(Path::new("scratch/draft.md")));
assert!(!is_content_path(Path::new("EXPECTED/snapshot.md")));
assert!(!is_content_path(Path::new("archive/old.md")));
assert!(!is_content_path(Path::new(
"EXPECTED/records/contacts/x.md"
)));
assert!(!is_content_path(Path::new("archive/sources/emails/y.md")));
assert!(!is_content_path(Path::new("records/contacts/index.md")));
assert!(!is_content_path(Path::new("records/contacts/index.jsonl")));
}
#[test]
fn is_store_true_only_with_uppercase_marker() {
let dir = tempdir().unwrap();
assert!(
!Store::is_db_md_store(dir.path()),
"no marker → not a store"
);
fs::write(dir.path().join("DB.md"), "---\ntype: db-md\n---\n").unwrap();
assert!(Store::is_db_md_store(dir.path()), "uppercase DB.md → store");
}
#[test]
fn is_store_false_for_lowercase_db_md() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("db.md"), "---\ntype: db-md\n---\n").unwrap();
assert!(
!Store::is_db_md_store(dir.path()),
"lowercase db.md must NOT be treated as a store marker"
);
assert!(Store::open(dir.path()).is_err());
}
#[test]
fn is_store_false_when_db_md_is_a_directory() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("DB.md")).unwrap();
assert!(
!Store::is_db_md_store(dir.path()),
"a directory named DB.md is not the file marker"
);
}
#[cfg(unix)]
#[test]
fn is_store_false_when_db_md_symlink_escapes_root() {
use std::os::unix::fs::symlink;
let dir = tempdir().unwrap();
let external = tempdir().unwrap();
let marker = external.path().join("outside.md");
fs::write(
&marker,
"---\ntype: db-md\nscope: personal\nowner: Outside\n---\n",
)
.unwrap();
symlink(&marker, dir.path().join("DB.md")).unwrap();
assert!(
!Store::is_db_md_store(dir.path()),
"opening a store must not read an external DB.md symlink"
);
}
#[test]
fn open_rejects_non_store_with_path() {
let dir = tempdir().unwrap();
let err = Store::open(dir.path()).unwrap_err();
assert_eq!(err.path, dir.path());
}
#[test]
fn open_succeeds_and_parses_config() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: Test\n---\n\n# Store\n\n\
## Policies\n\n### Frozen pages\n- records/decisions/q1.md\n",
)
.unwrap();
let store = Store::open(dir.path()).unwrap();
assert_eq!(store.root, dir.path());
assert!(
store
.config
.frozen_pages
.iter()
.any(|p| p == Path::new("records/decisions/q1.md")),
"open() must surface DB.md ## Policies, got {:?}",
store.config.frozen_pages
);
}
#[cfg(unix)]
#[test]
fn opened_store_keeps_original_root_capability_after_path_swap() {
use std::os::unix::fs::symlink;
let sandbox = tempdir().unwrap();
let root = sandbox.path().join("store");
fs::create_dir_all(root.join("records/notes")).unwrap();
fs::write(root.join("DB.md"), "---\ntype: db-md\n---\n").unwrap();
fs::write(root.join("records/notes/owned.md"), b"owned").unwrap();
let outside = sandbox.path().join("outside");
fs::create_dir_all(outside.join("records/notes")).unwrap();
fs::write(outside.join("records/notes/owned.md"), b"outside secret").unwrap();
fs::write(
outside.join("records/notes/external-only.md"),
b"must never be enumerated",
)
.unwrap();
let store = Store::open_strict(&root).unwrap();
let detached = sandbox.path().join("detached-store");
fs::rename(&root, &detached).unwrap();
symlink(&outside, &root).unwrap();
assert_eq!(
store
.read_bounded(Path::new("records/notes/owned.md"), 1024)
.unwrap(),
b"owned",
"reads must remain rooted at the directory selected by open()"
);
store
.write_atomic(
Path::new("records/notes/created.md"),
b"created in held store",
)
.unwrap();
assert_eq!(
fs::read(detached.join("records/notes/created.md")).unwrap(),
b"created in held store"
);
assert!(
!outside.join("records/notes/created.md").exists(),
"writes must not follow a swapped store pathname"
);
assert_eq!(
fs::read(outside.join("records/notes/owned.md")).unwrap(),
b"outside secret"
);
let walked = store.walk().unwrap();
assert!(
walked.contains(&PathBuf::from("records/notes/owned.md"))
&& walked.contains(&PathBuf::from("records/notes/created.md")),
"sweeps must enumerate the held original directory"
);
assert!(
!walked.contains(&PathBuf::from("records/notes/external-only.md")),
"sweeps must not enumerate a replacement at the old root pathname"
);
}
#[cfg(unix)]
#[test]
fn transaction_gate_serializes_cooperative_mutators() {
let dir = empty_store();
let first_store = Store::open_strict(dir.path()).unwrap();
let second_store = Store::open_strict(dir.path()).unwrap();
let first = first_store.transaction().unwrap();
let (sent, received) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
let _second = second_store.transaction().unwrap();
sent.send(()).unwrap();
});
assert!(
received
.recv_timeout(std::time::Duration::from_millis(100))
.is_err(),
"a second cooperative mutator must wait while rename owns the gate"
);
drop(first);
received
.recv_timeout(std::time::Duration::from_secs(2))
.expect("waiter acquires immediately after the first transaction ends");
waiter.join().unwrap();
}
#[test]
fn walk_collects_content_across_layers_skipping_meta_and_log() {
let dir = empty_store();
let root = dir.path();
write(
root,
"sources/emails/2026/05/a.md",
&content_md("2026-05-01T00:00:00Z"),
);
write(
root,
"records/contacts/sarah.md",
&content_md("2026-05-02T00:00:00Z"),
);
write(
root,
"records/profiles/sarah.md",
&content_md("2026-05-03T00:00:00Z"),
);
write(root, "sources/emails/index.md", "---\ntype: index\n---\n"); write(root, "index.md", "---\ntype: index\n---\n"); write(root, "log.md", "---\ntype: log\n---\n"); write(root, "log/2026-04.md", "---\ntype: log\n---\n"); write(
root,
"sources/.hidden/secret.md",
&content_md("2026-05-09T00:00:00Z"),
); write(root, "records/contacts/notes.txt", "not markdown");
let store = open(&dir);
let got = rels(&store.walk().unwrap());
assert_eq!(
got,
vec![
"records/contacts/sarah.md".to_string(),
"records/profiles/sarah.md".to_string(),
"sources/emails/2026/05/a.md".to_string(),
]
);
}
#[test]
fn walk_includes_log_md_but_prunes_nested_store() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/configs/log.md",
&content_md("2026-05-01T00:00:00Z"),
);
write(
root,
"sources/docs/DB.md",
"---\ntype: db-md\nscope: research\nowner: Nested\n---\n",
);
write(
root,
"sources/docs/records/notes/secret.md",
&content_md("2026-05-02T00:00:00Z"),
);
write(root, "records/configs/index.md", "---\ntype: index\n---\n");
let store = open(&dir);
let got = rels(&store.walk().unwrap());
assert!(
got.contains(&"records/configs/log.md".to_string()),
"layer-internal log.md is content: {got:?}"
);
assert!(
!got.iter().any(|path| path.starts_with("sources/docs/")),
"nested store content must be pruned: {got:?}"
);
assert!(
!got.iter().any(|p| p.ends_with("index.md")),
"index.md is still skipped: {got:?}"
);
assert_eq!(
store.nested_store_roots().unwrap(),
vec![PathBuf::from("sources/docs")]
);
assert!(
ensure_path_within_store(root, &root.join("sources/docs/records/notes/secret.md"))
.is_err(),
"outer-store containment must reject a nested-store path"
);
let nested = Store::open_strict(&root.join("sources/docs")).unwrap();
assert!(
nested.owns_path(&root.join("sources/docs/records/notes/secret.md")),
"the same path is owned when the nested store is opened directly"
);
}
#[cfg(unix)]
#[test]
fn walk_never_reads_external_file_or_directory_symlinks() {
use std::os::unix::fs::symlink;
let dir = empty_store();
let external = tempdir().unwrap();
fs::write(
external.path().join("secret.md"),
content_md("2026-05-03T00:00:00Z"),
)
.unwrap();
fs::create_dir(external.path().join("folder")).unwrap();
fs::write(
external.path().join("folder").join("deeper.md"),
content_md("2026-05-04T00:00:00Z"),
)
.unwrap();
fs::create_dir_all(dir.path().join("records/notes")).unwrap();
symlink(
external.path().join("secret.md"),
dir.path().join("records/notes/aliased.md"),
)
.unwrap();
symlink(
external.path().join("folder"),
dir.path().join("records/external"),
)
.unwrap();
let store = open(&dir);
assert!(
store.walk().unwrap().is_empty(),
"external symlink targets must be pruned from every store sweep"
);
assert!(!store.owns_path(&dir.path().join("records/notes/aliased.md")));
assert!(!store.owns_path(&dir.path().join("records/external")));
assert_eq!(
rels(&store.unowned_symlinks().unwrap()),
vec![
"records/external".to_string(),
"records/notes/aliased.md".to_string(),
],
"ignored external aliases remain observable without reading targets"
);
}
#[test]
fn walk_layer_is_scoped() {
let dir = empty_store();
let root = dir.path();
write(
root,
"sources/emails/2026/05/a.md",
&content_md("2026-05-01T00:00:00Z"),
);
write(
root,
"records/contacts/sarah.md",
&content_md("2026-05-02T00:00:00Z"),
);
let store = open(&dir);
assert_eq!(
rels(&store.walk_layer(Layer::Sources).unwrap()),
vec!["sources/emails/2026/05/a.md".to_string()]
);
assert_eq!(
rels(&store.walk_layer(Layer::Records).unwrap()),
vec!["records/contacts/sarah.md".to_string()]
);
let only_sources = empty_store();
write(
only_sources.path(),
"sources/emails/2026/05/a.md",
&content_md("2026-05-01T00:00:00Z"),
);
let s2 = open(&only_sources);
assert!(s2.walk_layer(Layer::Records).unwrap().is_empty());
}
#[test]
fn walk_type_folder_recurses_shards_and_accepts_abs_or_rel() {
let dir = empty_store();
let root = dir.path();
write(
root,
"sources/emails/2026/05/a.md",
&content_md("2026-05-01T00:00:00Z"),
);
write(
root,
"sources/emails/2026/06/b.md",
&content_md("2026-06-01T00:00:00Z"),
);
write(root, "sources/emails/index.md", "---\ntype: index\n---\n"); write(
root,
"sources/docs/2026/05/c.md",
&content_md("2026-05-04T00:00:00Z"),
);
let store = open(&dir);
let expected = vec![
"sources/emails/2026/05/a.md".to_string(),
"sources/emails/2026/06/b.md".to_string(),
];
assert_eq!(
rels(&store.walk_type_folder(Path::new("sources/emails")).unwrap()),
expected
);
assert_eq!(
rels(
&store
.walk_type_folder(&root.join("sources/emails"))
.unwrap()
),
expected
);
}
#[test]
fn recent_orders_by_updated_desc_then_path_and_caps() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/meetings/2026/05/c.md",
&content_md("2026-05-03T00:00:00Z"),
);
write(
root,
"records/meetings/2026/05/a.md",
&content_md("2026-05-02T00:00:00Z"),
);
write(
root,
"records/meetings/2026/05/b.md",
&content_md("2026-05-02T00:00:00Z"),
);
write(
root,
"records/meetings/2026/04/z.md",
&content_md("2026-04-01T00:00:00Z"),
);
let store = open(&dir);
let all = rels(
&store
.recent_in_type_folder(Path::new("records/meetings"), 10)
.unwrap(),
);
assert_eq!(
all,
vec![
"records/meetings/2026/05/c.md".to_string(), "records/meetings/2026/05/a.md".to_string(), "records/meetings/2026/05/b.md".to_string(),
"records/meetings/2026/04/z.md".to_string(), ]
);
let top2 = rels(
&store
.recent_in_type_folder(Path::new("records/meetings"), 2)
.unwrap(),
);
assert_eq!(
top2,
vec![
"records/meetings/2026/05/c.md".to_string(),
"records/meetings/2026/05/a.md".to_string(),
]
);
}
#[test]
fn recent_sorts_undated_files_last() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/contacts/dated.md",
&content_md("2026-05-01T00:00:00Z"),
);
write(
root,
"records/contacts/undated.md",
"---\ntype: contact\nsummary: x\n---\nbody\n",
);
let store = open(&dir);
let got = rels(
&store
.recent_in_type_folder(Path::new("records/contacts"), 10)
.unwrap(),
);
assert_eq!(
got,
vec![
"records/contacts/dated.md".to_string(),
"records/contacts/undated.md".to_string(),
],
"a file with a real `updated` must outrank one with none"
);
}
#[test]
fn type_shards_classification() {
let dir = empty_store();
let store = open(&dir);
for t in [
"email",
"transcript",
"pdf-source",
"expense",
"invoice",
"meeting",
"order",
"ticket",
"transaction",
] {
assert!(store.type_shards(t), "{t} should shard");
}
for t in [
"contact", "company", "decision", "profile", "index", "log", "db-md", "proposal",
] {
assert!(!store.type_shards(t), "{t} should stay flat");
}
}
#[test]
fn type_shards_respects_schema_directive_both_directions() {
use crate::parser::{Config, Schema};
let dir = empty_store();
let mut store = open(&dir);
let mut config = Config::default();
config.schemas.insert(
"shipment".to_string(),
Schema {
shard: Some(true),
..Schema::default()
},
);
config.schemas.insert(
"expense".to_string(),
Schema {
shard: Some(false),
..Schema::default()
},
);
config
.schemas
.insert("meeting".to_string(), Schema::default());
store.config = config;
assert!(
store.type_shards("shipment"),
"custom type with `shard: by-date` must shard"
);
assert!(
!store.type_shards("expense"),
"built-in event type with `shard: flat` must go flat"
);
assert!(
store.type_shards("meeting"),
"schema without a `shard:` directive keeps the built-in default"
);
assert!(
!store.type_shards("contact"),
"unconfigured entity type stays flat"
);
}
#[test]
fn year_month_from_str_accepts_unpadded_month() {
let ym = year_month_from_str;
assert_eq!(
ym("2026-1-15"),
Some(("2026".to_string(), "01".to_string())),
);
assert_eq!(
ym("2026-01-15"),
Some(("2026".to_string(), "01".to_string())),
);
assert_eq!(
ym("2026-12-5"),
Some(("2026".to_string(), "12".to_string())),
);
assert_eq!(ym("2026-1"), Some(("2026".to_string(), "01".to_string())));
assert_eq!(
ym("2026-3-22T10:00:00-07:00"),
Some(("2026".to_string(), "03".to_string())),
);
}
#[test]
fn year_month_from_str_rejects_non_dates() {
assert_eq!(year_month_from_str(""), None);
assert_eq!(year_month_from_str("not-a-date"), None);
assert_eq!(year_month_from_str("2026"), None); assert_eq!(year_month_from_str("26-1-15"), None); assert_eq!(year_month_from_str("2026-13-01"), None); assert_eq!(year_month_from_str("2026-0-01"), None); assert_eq!(year_month_from_str("2026-001-01"), None); assert_eq!(year_month_from_str("2026-x-01"), None); assert_eq!(year_month_from_str("20a6-1-15"), None); }
#[test]
fn shard_path_accepts_unpadded_month_same_as_padded() {
let dir = empty_store();
let store = open(&dir);
let padded = store
.shard_path_for("expense", &fm_with_extra("date", "2026-01-15"), "padded")
.unwrap();
assert_eq!(padded, PathBuf::from("records/expenses/2026/01/padded.md"));
let single = store
.shard_path_for("expense", &fm_with_extra("date", "2026-1-15"), "single")
.unwrap();
assert_eq!(single, PathBuf::from("records/expenses/2026/01/single.md"));
}
fn fm_with_extra(key: &str, value: &str) -> Frontmatter {
let mut fm = Frontmatter::default();
fm.extra.insert(
key.to_string(),
serde_norway::Value::String(value.to_string()),
);
fm
}
fn fm_with_created(rfc3339: &str) -> Frontmatter {
Frontmatter {
created: Some(DateTime::parse_from_rfc3339(rfc3339).unwrap()),
..Default::default()
}
}
#[test]
fn shard_path_uses_primary_date_field_per_type() {
let dir = empty_store();
let store = open(&dir);
let p = store
.shard_path_for("expense", &fm_with_extra("date", "2026-05-22"), "lunch")
.unwrap();
assert_eq!(p, PathBuf::from("records/expenses/2026/05/lunch.md"));
let p = store
.shard_path_for(
"email",
&fm_with_extra("date", "2026-11-02T09:00:00-07:00"),
"e1",
)
.unwrap();
assert_eq!(p, PathBuf::from("sources/emails/2026/11/e1.md"));
let p = store
.shard_path_for(
"transcript",
&fm_with_extra("recorded_at", "2025-01-15T12:00:00Z"),
"t1",
)
.unwrap();
assert_eq!(p, PathBuf::from("sources/transcripts/2025/01/t1.md"));
}
#[test]
fn shard_path_falls_back_to_created() {
let dir = empty_store();
let store = open(&dir);
let p = store
.shard_path_for(
"meeting",
&fm_with_created("2024-07-09T08:30:00-04:00"),
"sync",
)
.unwrap();
assert_eq!(p, PathBuf::from("records/meetings/2024/07/sync.md"));
}
#[test]
fn shard_path_primary_field_wins_over_created() {
let dir = empty_store();
let store = open(&dir);
let mut fm = fm_with_created("2020-01-01T00:00:00Z");
fm.extra.insert(
"date".into(),
serde_norway::Value::String("2026-05-22".into()),
);
let p = store.shard_path_for("expense", &fm, "x").unwrap();
assert_eq!(p, PathBuf::from("records/expenses/2026/05/x.md"));
}
#[test]
fn shard_path_flat_types_have_no_shard_segment() {
let dir = empty_store();
let store = open(&dir);
let p = store
.shard_path_for(
"contact",
&fm_with_created("2026-05-22T00:00:00Z"),
"sarah-chen",
)
.unwrap();
assert_eq!(p, PathBuf::from("records/contacts/sarah-chen.md"));
let p = store
.shard_path_for("profile", &Frontmatter::default(), "renewal-theme")
.unwrap();
assert_eq!(p, PathBuf::from("records/profile/renewal-theme.md"));
}
#[test]
fn shard_path_custom_type_is_indexable_three_component_path() {
let dir = empty_store();
let store = open(&dir);
let p = store
.shard_path_for("profile", &Frontmatter::default(), "renewal-theme")
.unwrap();
let comps: Vec<&str> = p.iter().filter_map(|c| c.to_str()).collect();
assert_eq!(
comps.len(),
3,
"custom-type path must be <layer>/<type-folder>/<file>, got {p:?}"
);
assert_eq!(
comps[0], "records",
"first component must be the records layer (a custom type is \
filed under the records fallback)"
);
assert!(
!comps[1].is_empty() && comps[1] != "renewal-theme.md",
"second component must be a real type-folder, not the file: {p:?}"
);
assert!(
comps[2].ends_with(".md"),
"third component must be the .md file: {p:?}"
);
}
#[test]
fn shard_path_preserves_and_adds_md_extension() {
let dir = empty_store();
let store = open(&dir);
let with = store
.shard_path_for("contact", &Frontmatter::default(), "sarah.md")
.unwrap();
let without = store
.shard_path_for("contact", &Frontmatter::default(), "sarah")
.unwrap();
assert_eq!(with, PathBuf::from("records/contacts/sarah.md"));
assert_eq!(without, PathBuf::from("records/contacts/sarah.md"));
}
#[test]
fn shard_path_errors_when_sharding_type_has_no_date() {
let dir = empty_store();
let store = open(&dir);
let err = store
.shard_path_for("expense", &Frontmatter::default(), "mystery")
.unwrap_err();
match err {
StoreError::NoShardDate { file } => {
assert_eq!(file, PathBuf::from("records/expenses/mystery.md"));
}
other => panic!("expected NoShardDate, got {other:?}"),
}
}
#[test]
fn find_links_to_matches_all_accepted_spellings() {
let dir = empty_store();
let root = dir.path();
let target = "records/contacts/sarah-chen";
write(
root,
"records/profiles/sarah.md",
&format!(
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\nSee [[{target}]].\n"
),
);
write(
root,
"records/meetings/2026/05/m.md",
&format!("---\ntype: meeting\nsummary: s\n---\nWith [[{target}|Sarah]].\n"),
);
write(
root,
"records/concepts/t.md",
&format!(
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[{target}.md]]\n"
),
);
write(
root,
"records/contacts/index.md",
&format!("---\ntype: index\n---\n- [[{target}]] — Sarah\n"),
);
write(
root,
"records/profiles/elena.md",
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\nNo links here.\n",
);
write(
root,
"records/profiles/bob.md",
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\n[[sarah-chen]]\n",
);
write(
root,
"records/profiles/jr.md",
&format!(
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\n[[{target}-jr]]\n"
),
);
let store = open(&dir);
let got = rels(&store.find_links_to(Path::new(target)).unwrap());
assert_eq!(
got,
vec![
"records/concepts/t.md".to_string(),
"records/contacts/index.md".to_string(),
"records/meetings/2026/05/m.md".to_string(),
"records/profiles/sarah.md".to_string(),
]
);
}
#[test]
fn find_links_to_distinguishes_sibling_paths() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/concepts/a.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[records/contacts/sarah]]\n",
);
write(
root,
"records/concepts/b.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[records/contacts/sarah-chen]]\n",
);
let store = open(&dir);
assert_eq!(
rels(
&store
.find_links_to(Path::new("records/contacts/sarah"))
.unwrap()
),
vec!["records/concepts/a.md".to_string()]
);
assert_eq!(
rels(
&store
.find_links_to(Path::new("records/contacts/sarah-chen"))
.unwrap()
),
vec!["records/concepts/b.md".to_string()]
);
}
#[test]
fn regression_find_links_to_tolerates_invalid_utf8_on_a_matched_line() {
let dir = empty_store();
let root = dir.path();
let target = "records/contacts/sarah-chen";
write(
root,
"records/profiles/clean.md",
&format!(
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\nSee [[{target}]].\n"
),
);
let mut bytes: Vec<u8> =
b"---\ntype: email\nsummary: s\n---\nSee [[records/contacts/sarah-chen]] \xFF here\n"
.to_vec();
let dirty_abs = root.join("sources/emails/2026/05/raw.md");
fs::create_dir_all(dirty_abs.parent().unwrap()).unwrap();
fs::write(&dirty_abs, &bytes).unwrap();
assert!(
std::str::from_utf8(&bytes).is_err(),
"fixture must contain invalid UTF-8 to exercise the regression"
);
bytes.clear();
let store = open(&dir);
let got = rels(
&store
.find_links_to(Path::new(target))
.expect("a stray non-UTF-8 byte must not abort the backlink scan"),
);
assert_eq!(
got,
vec![
"records/profiles/clean.md".to_string(),
"sources/emails/2026/05/raw.md".to_string(),
],
"both the clean linker and the one with an invalid byte on the link \
line are reported; the scan degrades, it does not fail"
);
}
#[test]
fn find_links_to_any_returns_the_union_with_boundary_correctness() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/concepts/links-sarah.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[records/contacts/sarah-chen]]\n",
);
write(
root,
"records/concepts/links-acme.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\nDeal with [[records/companies/acme|Acme]].\n",
);
write(
root,
"records/meetings/2026/05/m.md",
"---\ntype: meeting\nsummary: s\n---\n[[records/contacts/sarah-chen]] re \
[[records/companies/acme]]\n",
);
write(
root,
"records/concepts/links-jr.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[records/contacts/sarah-chen-jr]]\n",
);
write(
root,
"records/concepts/unrelated.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[records/concepts/spend]]\n",
);
let store = open(&dir);
let targets = vec![
PathBuf::from("records/contacts/sarah-chen"),
PathBuf::from("records/companies/acme"),
];
let got = rels(&store.find_links_to_any(&targets).unwrap());
assert_eq!(
got,
vec![
"records/concepts/links-acme.md".to_string(),
"records/concepts/links-sarah.md".to_string(),
"records/meetings/2026/05/m.md".to_string(),
],
"batch finder must return the deduped union of linkers across all \
targets, excluding the prefix-sibling and the unrelated file"
);
let mut union: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
for t in &targets {
for linker in store.find_links_to(t).unwrap() {
union.insert(linker);
}
}
assert_eq!(
rels(&union.into_iter().collect::<Vec<_>>()),
got,
"find_links_to_any must equal the union of per-target find_links_to"
);
}
#[test]
fn find_links_to_any_empty_targets_matches_nothing() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/concepts/a.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n[[records/contacts/sarah-chen]]\n",
);
let store = open(&dir);
assert!(
store.find_links_to_any(&[]).unwrap().is_empty(),
"no targets ⇒ no linkers (an empty pattern must not match every file)"
);
assert!(
store
.find_links_to_any(&[PathBuf::from(""), PathBuf::from("./")])
.unwrap()
.is_empty(),
"targets that render to empty link text contribute no alternation arm"
);
}
#[test]
fn read_type_index_parses_records_and_flattens_fields() {
let dir = empty_store();
let root = dir.path();
let jsonl = "\
{\"path\":\"records/expenses/2026/05/a.md\",\"type\":\"expense\",\"summary\":\"lunch\",\"tags\":[\"meals\"],\"links\":[\"records/companies/acme\"],\"created\":\"2026-05-01T00:00:00Z\",\"updated\":\"2026-05-01T00:00:00Z\",\"vendor\":\"acme\",\"amount\":42}
{\"path\":\"records/expenses/2026/05/b.md\",\"type\":\"expense\",\"summary\":\"taxi\",\"created\":null,\"updated\":null,\"vendor\":\"yellow\"}
";
let p = write(root, "records/expenses/index.jsonl", jsonl);
let store = open(&dir);
let recs = store.read_type_index(&store.abs_path(&p)).unwrap();
assert_eq!(recs.len(), 2);
assert_eq!(recs[0].path, PathBuf::from("records/expenses/2026/05/a.md"));
assert_eq!(recs[0].type_, "expense");
assert_eq!(recs[0].summary, "lunch");
assert_eq!(recs[0].tags, vec!["meals".to_string()]);
assert_eq!(recs[0].links, vec!["records/companies/acme".to_string()]);
assert!(recs[0].created.is_some());
assert_eq!(
recs[0].fields.get("vendor"),
Some(&serde_json::json!("acme"))
);
assert_eq!(recs[0].fields.get("amount"), Some(&serde_json::json!(42)));
assert!(recs[1].tags.is_empty());
assert!(recs[1].links.is_empty());
}
#[test]
fn read_type_index_last_write_wins_and_skips_blanks() {
let dir = empty_store();
let root = dir.path();
let jsonl = "\
{\"path\":\"records/contacts/sarah.md\",\"type\":\"contact\",\"summary\":\"old\",\"created\":null,\"updated\":null}
{\"path\":\"records/contacts/sarah.md\",\"type\":\"contact\",\"summary\":\"new\",\"created\":null,\"updated\":null}
";
let p = write(root, "records/contacts/index.jsonl", jsonl);
let store = open(&dir);
let recs = store.read_type_index(&store.abs_path(&p)).unwrap();
assert_eq!(recs.len(), 1, "duplicate path collapses to one record");
assert_eq!(recs[0].summary, "new", "later line must win");
}
#[test]
fn read_type_index_errors_on_malformed_line() {
let dir = empty_store();
let root = dir.path();
let p = write(root, "records/contacts/index.jsonl", "{not valid json}\n");
let store = open(&dir);
let err = store.read_type_index(&store.abs_path(&p)).unwrap_err();
assert!(matches!(err, StoreError::BadTypeIndex { .. }));
}
fn jsonl_line(path: &str, type_: &str, summary: &str, extra: &str) -> String {
format!(
"{{\"path\":\"{path}\",\"type\":\"{type_}\",\"summary\":\"{summary}\",\"created\":null,\"updated\":null{extra}}}\n"
)
}
#[test]
fn find_by_type_reads_canonical_folder_sidecar() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/contacts/index.jsonl",
&(jsonl_line("records/contacts/sarah.md", "contact", "Sarah", "")
+ &jsonl_line("records/contacts/elena.md", "contact", "Elena", "")),
);
write(
root,
"records/companies/index.jsonl",
&jsonl_line("records/companies/acme.md", "company", "Acme", ""),
);
let store = open(&dir);
let recs = store.find_by_type("contact").unwrap();
let names: Vec<_> = recs.iter().map(|r| r.summary.clone()).collect();
assert_eq!(names, vec!["Elena".to_string(), "Sarah".to_string()]); assert!(recs.iter().all(|r| r.type_ == "contact"));
}
#[test]
fn regression_find_by_type_includes_non_canonical_folder_when_canonical_exists() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/contacts/index.jsonl",
&jsonl_line("records/contacts/sarah.md", "contact", "Sarah", ""),
);
write(
root,
"records/clients/index.jsonl",
&jsonl_line("records/clients/elena.md", "contact", "Elena", ""),
);
write(
root,
"records/companies/index.jsonl",
&jsonl_line("records/companies/acme.md", "company", "Acme", ""),
);
let store = open(&dir);
let got: std::collections::BTreeSet<String> = store
.find_by_type("contact")
.unwrap()
.into_iter()
.map(|r| r.path.to_string_lossy().into_owned())
.collect();
assert_eq!(
got,
["records/clients/elena.md", "records/contacts/sarah.md"]
.into_iter()
.map(String::from)
.collect::<std::collections::BTreeSet<_>>(),
"both the canonical-folder and the non-canonical-folder contact must \
be returned; the company record must be excluded"
);
}
#[test]
fn regression_find_by_type_profile_spans_multiple_topic_folders() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/profile/index.jsonl",
&jsonl_line("records/profile/billing.md", "profile", "Billing", ""),
);
write(
root,
"records/people/index.jsonl",
&jsonl_line("records/people/sarah-chen.md", "profile", "Sarah Chen", ""),
);
write(
root,
"records/clients/index.jsonl",
&jsonl_line("records/clients/atlas.md", "profile", "Atlas", ""),
);
let store = open(&dir);
let got: std::collections::BTreeSet<String> = store
.find_by_type("profile")
.unwrap()
.into_iter()
.map(|r| r.path.to_string_lossy().into_owned())
.collect();
assert_eq!(
got,
[
"records/clients/atlas.md",
"records/people/sarah-chen.md",
"records/profile/billing.md",
]
.into_iter()
.map(String::from)
.collect::<std::collections::BTreeSet<_>>(),
"a profile query must return records from every topic folder, not \
just the canonical records/profile/"
);
}
#[test]
fn find_by_type_canonical_absent_falls_back_within_the_layer_only() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/proposals/index.jsonl",
&jsonl_line("records/proposals/p1.md", "proposal", "Q3 proposal", ""),
);
write(
root,
"sources/proposals/index.jsonl",
&jsonl_line(
"sources/proposals/leak.md",
"proposal",
"cross-layer decoy",
"",
),
);
let store = open(&dir);
let recs = store.find_by_type("proposal").unwrap();
assert_eq!(
recs.len(),
1,
"only the records-layer proposal, not the sources decoy"
);
assert_eq!(recs[0].summary, "Q3 proposal");
assert_eq!(recs[0].path, PathBuf::from("records/proposals/p1.md"));
}
#[test]
fn find_by_type_canonical_absent_does_not_read_other_layers() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/contacts/index.jsonl",
&jsonl_line("records/contacts/sarah.md", "contact", "Sarah", ""),
);
let store = open(&dir);
assert!(store.find_by_type("email").unwrap().is_empty());
}
#[test]
fn find_by_where_matches_typed_columns_and_flat_fields() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/expenses/index.jsonl",
&(jsonl_line(
"records/expenses/a.md",
"expense",
"lunch",
",\"vendor\":\"acme\",\"tags\":[\"meals\"]",
) + &jsonl_line(
"records/expenses/b.md",
"expense",
"taxi",
",\"vendor\":\"yellow\"",
)),
);
write(
root,
"records/contacts/index.jsonl",
&jsonl_line(
"records/contacts/sarah.md",
"contact",
"Sarah",
",\"tags\":[\"customer\"]",
),
);
let store = open(&dir);
let by_vendor = store.find_by_where("vendor", "acme").unwrap();
assert_eq!(by_vendor.len(), 1);
assert_eq!(by_vendor[0].path, PathBuf::from("records/expenses/a.md"));
assert_eq!(store.find_by_where("type", "expense").unwrap().len(), 2);
let customers = store.find_by_where("tags", "customer").unwrap();
assert_eq!(customers.len(), 1);
assert_eq!(
customers[0].path,
PathBuf::from("records/contacts/sarah.md")
);
assert!(store.find_by_where("vendor", "nobody").unwrap().is_empty());
}
#[test]
fn find_by_where_matches_timestamps_across_rfc3339_spellings() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/meetings/index.jsonl",
"{\"path\":\"records/meetings/kickoff.md\",\"type\":\"meeting\",\
\"summary\":\"kickoff\",\"created\":\"2026-05-01T00:00:00Z\",\
\"updated\":\"2026-05-02T09:30:00-07:00\"}\n",
);
let store = open(&dir);
let by_z = store
.find_by_where("created", "2026-05-01T00:00:00Z")
.unwrap();
assert_eq!(by_z.len(), 1);
assert_eq!(by_z[0].path, PathBuf::from("records/meetings/kickoff.md"));
assert_eq!(
store
.find_by_where("created", "2026-05-01T00:00:00+00:00")
.unwrap()
.len(),
1
);
assert_eq!(
store
.find_by_where("updated", "2026-05-02T09:30:00-07:00")
.unwrap()
.len(),
1
);
assert_eq!(
store
.find_by_where("updated", "2026-05-02T16:30:00Z")
.unwrap()
.len(),
1
);
assert!(store
.find_by_where("created", "2026-05-01T00:00:01Z")
.unwrap()
.is_empty());
assert!(store
.find_by_where("created", "2026-05-01")
.unwrap()
.is_empty());
}
#[test]
fn find_by_where_matches_floats_across_serialized_spellings() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/invoices/index.jsonl",
"{\"path\":\"records/invoices/inv.md\",\"type\":\"invoice\",\
\"summary\":\"inv\",\"amount\":1234.0,\"score\":1000.0,\"count\":42}\n",
);
let store = open(&dir);
for spelling in ["1234.00", "1234.0", "1234"] {
assert_eq!(
store.find_by_where("amount", spelling).unwrap().len(),
1,
"amount spelling `{spelling}` must match the stored 1234.0"
);
}
for spelling in ["1e3", "1000", "1000.0"] {
assert_eq!(
store.find_by_where("score", spelling).unwrap().len(),
1,
"score spelling `{spelling}` must match the stored 1000.0"
);
}
assert!(store.find_by_where("amount", "1234.5").unwrap().is_empty());
assert_eq!(store.find_by_where("count", "42").unwrap().len(), 1);
}
#[test]
fn number_matches_is_numeric_for_floats_but_exact_for_integers() {
use serde_json::Number;
let f: Number = serde_json::from_str("1234.0").unwrap();
assert!(number_matches(&f, "1234.00"));
assert!(number_matches(&f, "1234"));
assert!(number_matches(&f, "1234.0"));
assert!(!number_matches(&f, "1234.5"));
let big: Number = serde_json::from_str("18446744073709551615").unwrap(); assert!(number_matches(&big, "18446744073709551615"));
assert!(!number_matches(&big, "18446744073709551614"));
}
#[test]
fn find_by_where_in_layer_reads_only_that_layers_sidecars() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/companies/index.jsonl",
&jsonl_line(
"records/companies/acme.md",
"company",
"Acme",
",\"domain\":\"acme.com\"",
),
);
write(
root,
"sources/emails/index.jsonl",
"{ this is not valid json and would error if read }\n",
);
let store = open(&dir);
let in_records = store
.find_by_where_in("domain", "acme.com", Some(Layer::Records))
.expect("a records-scoped read must not touch the sources sidecar");
assert_eq!(
rels(
&in_records
.iter()
.map(|r| r.path.clone())
.collect::<Vec<_>>()
),
vec!["records/companies/acme.md".to_string()]
);
let store_wide = store.find_by_where("domain", "acme.com");
assert!(
matches!(store_wide, Err(StoreError::BadTypeIndex { .. })),
"unscoped read walks every layer and hits the corrupt sidecar"
);
let in_sources = store.find_by_where_in("domain", "acme.com", Some(Layer::Sources));
assert!(matches!(in_sources, Err(StoreError::BadTypeIndex { .. })));
}
#[test]
fn find_by_where_in_missing_layer_is_empty_not_an_error() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/contacts/index.jsonl",
&jsonl_line(
"records/contacts/sarah.md",
"contact",
"Sarah",
",\"city\":\"denver\"",
),
);
let store = open(&dir);
let in_sources = store
.find_by_where_in("city", "denver", Some(Layer::Sources))
.expect("missing layer subtree is empty, not an error");
assert!(in_sources.is_empty());
let in_records = store
.find_by_where_in("city", "denver", Some(Layer::Records))
.unwrap();
assert_eq!(in_records.len(), 1);
}
#[test]
fn abs_and_rel_path_roundtrip() {
let dir = empty_store();
let store = open(&dir);
let rel = Path::new("records/contacts/sarah.md");
let abs = store.abs_path(rel);
assert_eq!(abs, dir.path().join(rel));
assert_eq!(store.rel_path(&abs).as_deref(), Some(rel));
assert_eq!(store.abs_path(&abs), abs);
assert_eq!(store.rel_path(Path::new("/somewhere/else.md")), None);
}
#[test]
fn infer_type_maps_every_recognized_folder_back_to_its_type() {
let cases = [
("sources/emails/x.md", "email"),
("sources/transcripts/x.md", "transcript"),
("sources/docs/x.md", "pdf-source"),
("sources/notes/x.md", "note"),
("records/contacts/x.md", "contact"),
("records/companies/x.md", "company"),
("records/expenses/x.md", "expense"),
("records/meetings/x.md", "meeting"),
("records/decisions/x.md", "decision"),
("records/invoices/x.md", "invoice"),
];
for (path, expected) in cases {
assert_eq!(
infer_type_from_path(Path::new(path)).as_deref(),
Some(expected),
"path {path} should infer type {expected}"
);
}
}
#[test]
fn infer_type_round_trips_with_default_type_folder() {
let recognized = [
"email",
"transcript",
"pdf-source",
"contact",
"company",
"expense",
"meeting",
"decision",
"invoice",
];
for type_ in recognized {
let folder = default_type_folder(type_);
let file = folder.join("x.md");
assert_eq!(
infer_type_from_path(&file).as_deref(),
Some(type_),
"recognized type {type_} (folder {folder:?}) must round-trip"
);
}
}
#[test]
fn infer_type_round_trips_custom_types_verbatim_no_singularization() {
for custom in ["task", "tasks", "playbook", "process", "okrs", "ticket"] {
let folder = default_type_folder(custom);
assert_eq!(folder, PathBuf::from("records").join(custom));
let file = folder.join("x.md");
assert_eq!(
infer_type_from_path(&file).as_deref(),
Some(custom),
"custom type {custom} must round-trip verbatim (no singularization)"
);
}
assert_eq!(
infer_type_from_path(Path::new("records/tasks/x.md")).as_deref(),
Some("tasks"),
"records/tasks must infer `tasks`, not `task`"
);
}
#[test]
fn infer_type_requires_three_component_layer_folder_file_shape() {
assert_eq!(infer_type_from_path(Path::new("records/x.md")), None);
assert_eq!(infer_type_from_path(Path::new("sources/x.md")), None);
assert_eq!(infer_type_from_path(Path::new("x.md")), None);
assert_eq!(infer_type_from_path(Path::new("foo/bar/x.md")), None);
assert_eq!(
infer_type_from_path(Path::new("records/expenses/2026/05/x.md")).as_deref(),
Some("expense"),
);
}
#[test]
fn ensure_path_within_store_accepts_in_store_and_rejects_escape() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("records/contacts")).unwrap();
fs::write(root.join("records/contacts/sarah.md"), "x").unwrap();
let inside = root.join("records/contacts/sarah.md");
let got = ensure_path_within_store(root, &inside).expect("in-store path accepted");
assert!(got.starts_with(root.canonicalize().unwrap()));
let new_leaf = root.join("records/contacts/sarah-chen.md");
assert!(
ensure_path_within_store(root, &new_leaf).is_ok(),
"a non-existent in-store leaf must be accepted"
);
let escape = root.join("records/contacts/../../outside/secret.md");
assert!(
ensure_path_within_store(root, &escape).is_err(),
"a `..`-escaping path must be rejected"
);
}
#[test]
fn ensure_path_within_store_rejects_symlink_escape() {
let dir = tempdir().unwrap();
let root = dir.path().join("store");
fs::create_dir_all(&root).unwrap();
let outside_dir = dir.path().join("outside");
fs::create_dir_all(&outside_dir).unwrap();
let secret = outside_dir.join("secret.md");
fs::write(&secret, "TOPSECRET").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let link = root.join("escape.md");
symlink(&secret, &link).unwrap();
assert!(
ensure_path_within_store(&root, &link).is_err(),
"a symlink resolving outside the store must be rejected"
);
}
}
#[test]
fn store_containment_matches_single_shot_gate() {
let dir = tempdir().unwrap();
let root = dir.path().join("store");
fs::create_dir_all(root.join("records/contacts")).unwrap();
fs::write(root.join("records/contacts/sarah.md"), "x").unwrap();
fs::write(root.join("records/contacts/jules.md"), "y").unwrap();
let outside_dir = dir.path().join("outside");
fs::create_dir_all(&outside_dir).unwrap();
fs::write(outside_dir.join("secret.md"), "TOPSECRET").unwrap();
let mut gate = StoreContainment::new(&root).expect("root canonicalizes");
let same = |cand: &Path, label: &str, gate: &mut StoreContainment| {
let single = ensure_path_within_store(&root, cand);
let amortized = gate.resolve(cand);
match (single, amortized) {
(Ok(a), Ok(b)) => assert_eq!(a, b, "{label}: resolved paths differ"),
(Err(_), Err(_)) => {}
(s, a) => panic!("{label}: verdicts differ — single-shot {s:?} vs amortized {a:?}"),
}
};
same(
&root.join("records/contacts/sarah.md"),
"existing file",
&mut gate,
);
same(
&root.join("records/contacts/jules.md"),
"memoized parent",
&mut gate,
);
same(
&root.join("records/contacts/new-leaf.md"),
"missing leaf",
&mut gate,
);
same(
&root.join("records/contacts/../../outside/secret.md"),
"`..` tail",
&mut gate,
);
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let link = root.join("records/contacts/escape.md");
symlink(outside_dir.join("secret.md"), &link).unwrap();
same(&link, "symlink leaf escape", &mut gate);
assert!(
gate.resolve(&link).is_err(),
"symlink leaf must be rejected"
);
let linked_dir = root.join("records/linked");
symlink(&outside_dir, &linked_dir).unwrap();
let through = linked_dir.join("secret.md");
same(&through, "symlinked parent escape", &mut gate);
assert!(
gate.resolve(&through).is_err(),
"a candidate under a symlinked-out parent must be rejected"
);
}
}
#[test]
fn extract_edge_targets_trims_inner_whitespace() {
assert_eq!(
extract_edge_targets("See [[ records/contacts/sarah ]] today."),
vec!["records/contacts/sarah".to_string()]
);
}
#[test]
fn extract_edge_targets_skips_fenced_code_blocks() {
let body = "\
Real [[records/contacts/sarah]] link.
```markdown
[[records/contacts/ghost-example]] is how you link.
```
After fence [[records/companies/acme]].
";
let got = extract_edge_targets(body);
assert_eq!(
got,
vec![
"records/contacts/sarah".to_string(),
"records/companies/acme".to_string(),
],
"fenced example link must not be an edge"
);
}
#[test]
fn edge_spans_agree_with_edge_targets_on_every_body_shape() {
let bodies = [
"Plain [[records/contacts/sarah]] link.",
"See [[ records/contacts/sarah ]] and [[records/companies/acme|Acme Inc]].",
"Fenced:\n\n```markdown\n[[records/ghost]]\n```\n\nAfter [[records/real]].",
"~~~\n[[records/tilde-ghost]]\n~~~\n[[records/after-tilde]]",
" ```\n[[records/indented-fence-ghost]]\n ```\n[[records/after]]",
"````\n```\n[[records/nested-ghost]]\n```\n````\n[[records/after-long]]",
"Mis-encoded [[[a]], [[b]]] and real [[records/x]].",
"Unclosed [[records/never-closed and then [[records/ok]].",
"Empty [[]] and blank [[ ]] and real [[records/y]].",
"Multi [[a]] on [[b]] one [[c]] line.",
"Anchored [[records/x#section]] and aliased [[records/y#s|Label]].",
"Trailing newline body [[records/z]]\n",
"", ];
for body in bodies {
let spans = extract_edge_spans(body);
let targets = extract_edge_targets(body);
assert_eq!(
spans.iter().map(|s| s.target.clone()).collect::<Vec<_>>(),
targets,
"span targets diverged from edge targets for body:\n{body}"
);
for s in &spans {
let slice = &body[s.start..s.end];
assert!(
slice.starts_with("[[") && slice.ends_with("]]"),
"span {}..{} is not a wiki-link token (got {slice:?}) in:\n{body}",
s.start,
s.end
);
assert_eq!(
&slice[2..slice.len() - 2],
s.raw,
"span raw text must be the token's inner text"
);
}
}
}
#[test]
fn edge_spans_carry_alias_and_keep_fragments_in_the_target() {
let spans = extract_edge_spans("Go [[records/notes/x#setup|Read the setup]] now.");
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].alias.as_deref(), Some("Read the setup"));
assert_eq!(spans[0].target, "records/notes/x#setup");
assert_eq!(spans[0].raw, "records/notes/x#setup|Read the setup");
let body = "Go [[records/notes/x#setup|Read the setup]] now.";
let out = format!("{}LINK{}", &body[..spans[0].start], &body[spans[0].end..]);
assert_eq!(out, "Go LINK now.");
}
#[test]
fn extract_edge_targets_frontmatter_fence_does_not_swallow_body_links() {
let file = "\
---
type: note
summary: \"a note\"
ref: \"[[records/contacts/sarah]]\"
snippet: \"```\"
---
Body mentions [[records/companies/acme]].
```
[[records/contacts/ghost-example]] inside a body fence.
```
After fence [[records/contacts/dave]].
";
let got = extract_edge_targets(file);
assert_eq!(
got,
vec![
"records/contacts/sarah".to_string(), "records/companies/acme".to_string(), "records/contacts/dave".to_string(), ],
"a code fence inside frontmatter must not suppress body wiki-links, \
and a real body-fenced link must still be ignored"
);
}
#[test]
fn extract_edge_targets_handles_nested_indented_and_long_run_fences() {
let nested = "\
Doc:
````
```
[[records/contacts/bob]]
```
still fenced [[records/contacts/bob]]
````
Real [[records/companies/acme]].
";
assert_eq!(
extract_edge_targets(nested),
vec!["records/companies/acme".to_string()],
"a nested ``` inside a ````-run fence must not leak the fenced links"
);
let tilde_wraps_backtick = "\
~~~
```
[[records/contacts/ghost]]
```
~~~
After [[records/companies/acme]].
";
assert_eq!(
extract_edge_targets(tilde_wraps_backtick),
vec!["records/companies/acme".to_string()],
"a ``` line inside a ~~~ block must not invert the fence state"
);
let over_indented = " ```\nLive [[records/contacts/sarah]].\n";
assert_eq!(
extract_edge_targets(over_indented),
vec!["records/contacts/sarah".to_string()],
"a >3-space-indented ``` is not a fence opener"
);
}
#[test]
fn canonical_link_target_strips_md_dotslash_and_trims() {
assert_eq!(canonical_link_target(" records/x.md "), "records/x");
assert_eq!(canonical_link_target("./records/y"), "records/y");
assert_eq!(canonical_link_target("/records/z"), "records/z");
}
#[test]
fn link_edge_key_folds_case_only_on_case_insensitive_fs() {
let a = link_edge_key("records/contacts/Sarah-Chen");
let b = link_edge_key("records/contacts/sarah-chen");
if fs_is_case_insensitive() {
assert_eq!(a, b, "case-insensitive FS must fold the key");
} else {
assert_ne!(a, b, "case-sensitive FS must keep the key case-exact");
}
}
#[test]
fn link_edge_key_unifies_nfc_and_nfd_normalization_forms() {
let nfc = "records/contacts/jos\u{00e9}"; let nfd = "records/contacts/jose\u{0301}"; assert_ne!(nfc, nfd, "test inputs must be byte-distinct NFC vs NFD");
assert_eq!(
link_edge_key(nfc),
link_edge_key(nfd),
"NFC and NFD spellings of the same name must produce one edge key"
);
}
#[cfg(unix)]
#[test]
fn walk_skips_symlinked_content_file_and_symlinked_folder() {
use std::os::unix::fs::symlink;
let dir = empty_store();
let root = dir.path();
write(
root,
"records/contacts/sarah.md",
&content_md("2026-05-01T00:00:00Z"),
);
let external_file = root.join("external-elena.md");
fs::write(&external_file, content_md("2026-05-02T00:00:00Z")).unwrap();
symlink(&external_file, root.join("records/contacts/elena.md")).unwrap();
let external_dir = dir.path().join("external-companies");
fs::create_dir_all(&external_dir).unwrap();
fs::write(
external_dir.join("acme.md"),
content_md("2026-05-03T00:00:00Z"),
)
.unwrap();
symlink(&external_dir, root.join("records/companies")).unwrap();
let store = open(&dir);
let got = rels(&store.walk().unwrap());
assert_eq!(
got,
vec!["records/contacts/sarah.md".to_string()],
"store sweeps must not follow symlink leaves or ancestors: {got:?}"
);
}
#[test]
fn find_links_to_matches_whitespace_padded_link() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/profiles/a.md",
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\nSee [[ records/contacts/sarah ]] today.\n",
);
let store = open(&dir);
let got = rels(
&store
.find_links_to(Path::new("records/contacts/sarah"))
.unwrap(),
);
assert_eq!(
got,
vec!["records/profiles/a.md".to_string()],
"a padded `[[ x ]]` link must be found as a backward edge, matching forwardlinks"
);
}
#[test]
fn find_links_to_ignores_fenced_example_link() {
let dir = empty_store();
let root = dir.path();
write(
root,
"records/concepts/howto.md",
"---\ntype: concept\nmeta-type: conclusion\nsummary: s\n---\n```markdown\n[[records/contacts/sarah]]\n```\n",
);
let store = open(&dir);
let got = store
.find_links_to(Path::new("records/contacts/sarah"))
.unwrap();
assert!(
got.is_empty(),
"a `[[...]]` only inside a fenced code block is not a backward edge: {got:?}"
);
}
#[cfg(unix)]
#[test]
fn find_links_to_matches_case_variant_on_case_insensitive_fs() {
if !fs_is_case_insensitive() {
return;
}
let dir = empty_store();
let root = dir.path();
write(
root,
"records/profiles/bio.md",
"---\ntype: profile\nmeta-type: conclusion\nsummary: s\n---\nSee [[records/contacts/Sarah-Chen]].\n",
);
let store = open(&dir);
let got = rels(
&store
.find_links_to(Path::new("records/contacts/sarah-chen"))
.unwrap(),
);
assert_eq!(
got,
vec!["records/profiles/bio.md".to_string()],
"a case-variant link must be found on a case-insensitive filesystem"
);
}
}