From 097ddc35b673c678e8193cb7dd07fa69896cc849 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Tue, 04 Aug 2026 17:39:48 +0000 Subject: [PATCH] Let an entry's filename be its name Mount::open required a name field on every non-tombstone entry and failed the whole world over the first file without one. The scanner's stubs never write name, so a stub the DM scoped under lore/ kept a real world from opening at the next session. The field exists because the SRD's filenames are slugs that lost capitalization and punctuation, like mage-s-sword.md for "Mage's Sword"; a file the engine or the player writes carries its name in the filename. name is now optional and defaults to the address's last segment, which for a directory entity is the directory's own name, and a name that is present but not a string still fails with the path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvctyUUkzw7PcNrjCGG6dF --- src/knowledge/mount.rs | 48 ++++++++++++++++++++++++++++++++++-------------- src/knowledge/mount_tests.rs | 34 +++++++++++++++++++++++++++++++--- 2 file(s) changed, 65 insertion(s)(+), 17 deletion(s)(-) diff --git a/src/knowledge/mount.rs b/src/knowledge/mount.rs --- a/src/knowledge/mount.rs +++ b/src/knowledge/mount.rs @@ -35,10 +35,11 @@ /// `rules/` and `lore/` subtrees recursively, at any depth, and a /// layer missing one or both subtrees contributes nothing from the /// missing side. A directory that holds an `entry.md` mounts that /// file at the directory's own address, and the directory's `log/` - /// stays out of the union. Fails on the first layer root that does - /// not exist or is not a directory, the first file that cannot be - /// read, whose frontmatter is malformed or missing, or whose - /// non-tombstone entry has no `name`, naming the offending path. + /// stays out of the union. An entry with no `name` in its + /// frontmatter takes its filename as its name. Fails on the first + /// layer root that does not exist or is not a directory, or the + /// first file that cannot be read or whose frontmatter is malformed + /// or missing, naming the offending path. pub fn open(layers: &[PathBuf]) -> Result { let mut entries: BTreeMap = BTreeMap::new(); for layer_root in layers { @@ -114,16 +115,24 @@ if deleted { return Ok(Parsed::Tombstone); } - let name = mapping - .get("name") - .and_then(Value::as_str) - .ok_or_else(|| { - format!( - "{}: frontmatter is missing required field 'name'", - path.display() - ) - })? - .to_string(); + // The filename is the canonical name; `name` exists for entries + // whose filename is a slug that lost capitalization or punctuation, + // like the SRD's `mage-s-sword.md` for "Mage's Sword". A file the + // engine or the player writes carries its name in the filename, and + // requiring the field would let one label-less file keep a whole + // world from opening. + let name = match mapping.get("name") { + None => name_segment(address), + Some(value) => value + .as_str() + .ok_or_else(|| { + format!( + "{}: frontmatter field 'name' must be a string", + path.display() + ) + })? + .to_string(), + }; let kind = match mapping.get("type") { None => None, Some(value) => Some( @@ -226,6 +235,17 @@ /// The address a file at `file_path`, found under `root_dir` (a layer's /// `rules/` or `lore/` directory), walks to: `root_name` joined with the /// file's path relative to `root_dir`, `/`-separated, extension stripped. /// An `entry.md` addresses the directory that holds it. +/// The name an entry has when its frontmatter declares none: the last +/// segment of its address, which is the filename without its extension, +/// or the directory's own name for a directory entity. +fn name_segment(address: &str) -> String { + address + .rsplit('/') + .next() + .expect("rsplit always yields at least one segment") + .to_string() +} + fn address_for(root_name: &str, root_dir: &Path, file_path: &Path) -> String { let relative = file_path .strip_prefix(root_dir) diff --git a/src/knowledge/mount_tests.rs b/src/knowledge/mount_tests.rs --- a/src/knowledge/mount_tests.rs +++ b/src/knowledge/mount_tests.rs @@ -257,14 +257,42 @@ assert!(error.contains("must be a YAML mapping")); } #[test] -fn a_missing_name_on_a_non_tombstone_entry_fails_open() { +fn a_missing_name_takes_the_filename_as_the_name() { let layer = Layer::empty(); - layer.write("rules/spells/fireball.md", "---\ntype: spell\n---\nbody\n"); + layer.write( + "lore/places/Blackwater Fen.md", + "---\nkind: lore/places\nstub: true\n---\nbody\n", + ); + + let mount = open(&[&layer]).unwrap(); + let entry = mount.get("lore/places/Blackwater Fen").unwrap(); + + assert_eq!(entry.name, "Blackwater Fen"); +} + +#[test] +fn a_missing_name_on_a_directory_entity_takes_the_directorys_name() { + let layer = Layer::empty(); + layer.write( + "lore/places/weatherford/entry.md", + "---\nkind: lore/places\n---\nbody\n", + ); + + let mount = open(&[&layer]).unwrap(); + let entry = mount.get("lore/places/weatherford").unwrap(); + + assert_eq!(entry.name, "weatherford"); +} + +#[test] +fn a_non_string_name_on_a_non_tombstone_entry_fails_open() { + let layer = Layer::empty(); + layer.write("rules/spells/fireball.md", "---\nname: 5\n---\nbody\n"); let error = open(&[&layer]).unwrap_err(); assert!(error.contains("fireball.md")); - assert!(error.contains("'name'")); + assert!(error.contains("'name' must be a string")); } #[test] -- tangled.sh