feat(vault): add Type relationship and type folder inference
- Add "type" folder to infer_type_from_folder() mapping
- When a VaultEntry has isA set (and it's not "Type"), automatically
add a "Type" relationship (e.g., "[[type/project]]") to the
relationships map, making the type property a navigable link
- Handle both plain string isA ("Project") and wikilink isA
("[[type/project]]") correctly
- Add 5 new tests for the Type relationship feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -257,6 +257,7 @@ fn infer_type_from_folder(folder: &str) -> String {
|
||||
"event" => "Event",
|
||||
"topic" => "Topic",
|
||||
"experiment" => "Experiment",
|
||||
"type" => "Type",
|
||||
"note" => "Note",
|
||||
"quarter" => "Quarter",
|
||||
"measure" => "Measure",
|
||||
@@ -319,7 +320,7 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
|
||||
let matter = Matter::<YAML>::new();
|
||||
let parsed = matter.parse(&content);
|
||||
let (frontmatter, relationships) = extract_fm_and_rels(parsed.data);
|
||||
let (frontmatter, mut relationships) = extract_fm_and_rels(parsed.data);
|
||||
|
||||
let title = extract_title(&parsed.content, &filename);
|
||||
let snippet = extract_snippet(&content);
|
||||
@@ -327,6 +328,20 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
let created_at = parse_created_at(&frontmatter);
|
||||
let is_a = resolve_is_a(frontmatter.is_a, path);
|
||||
|
||||
// Add "Type" relationship: isA becomes a navigable link to the type document.
|
||||
// Skip for type documents themselves (isA == "Type") to avoid self-referential links.
|
||||
if let Some(ref type_name) = is_a {
|
||||
if type_name != "Type" {
|
||||
// If isA is already a wikilink (e.g. "[[type/project]]"), use it directly
|
||||
let type_link = if type_name.starts_with("[[") && type_name.ends_with("]]") {
|
||||
type_name.clone()
|
||||
} else {
|
||||
format!("[[type/{}]]", type_name.to_lowercase())
|
||||
};
|
||||
relationships.insert("Type".to_string(), vec![type_link]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(VaultEntry {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
filename, title, is_a, snippet, relationships,
|
||||
@@ -897,7 +912,7 @@ Status: Active
|
||||
create_test_file(dir.path(), "publish-essays.md", content);
|
||||
|
||||
let entry = parse_md_file(&dir.path().join("publish-essays.md")).unwrap();
|
||||
assert_eq!(entry.relationships.len(), 2);
|
||||
assert_eq!(entry.relationships.len(), 3); // Has, Topics, Type
|
||||
assert_eq!(
|
||||
entry.relationships.get("Has").unwrap(),
|
||||
&vec!["[[essay/foo|Foo Essay]]".to_string(), "[[essay/bar|Bar Essay]]".to_string()]
|
||||
@@ -906,6 +921,10 @@ Status: Active
|
||||
entry.relationships.get("Topics").unwrap(),
|
||||
&vec!["[[topic/rust]]".to_string(), "[[topic/wasm]]".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/responsibility]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -952,8 +971,12 @@ Custom Field: just a plain string
|
||||
create_test_file(dir.path(), "plain-note.md", content);
|
||||
|
||||
let entry = parse_md_file(&dir.path().join("plain-note.md")).unwrap();
|
||||
// Tags and Custom Field don't contain wikilinks, so relationships should be empty
|
||||
assert!(entry.relationships.is_empty());
|
||||
// Tags and Custom Field don't contain wikilinks — only the auto-generated "Type" relationship
|
||||
assert_eq!(entry.relationships.len(), 1);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/note]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
const BIG_PROJECT_CONTENT: &str = "---\nIs A: Project\nHas:\n - \"[[deliverable/mvp]]\"\n - \"[[deliverable/v2]]\"\nTopics:\n - \"[[topic/ai]]\"\n - \"[[topic/compilers]]\"\nEvents:\n - \"[[event/launch-day]]\"\nNotes:\n - \"[[note/design-rationale]]\"\n - \"[[note/meeting-2024-01]]\"\n - \"[[note/meeting-2024-02]]\"\nOwner: \"[[person/alice]]\"\nRelated to:\n - \"[[project/sibling-project]]\"\nBelongs to:\n - \"[[area/engineering]]\"\nStatus: Active\n---\n# Big Project\n";
|
||||
@@ -1056,7 +1079,12 @@ Context: "[[area/research]]"
|
||||
fn test_skip_keys_real_relation_included() {
|
||||
let (rels, len) = parse_skip_keys_rels();
|
||||
assert_eq!(rels.get("Real Relation").unwrap(), &vec!["[[note/important]]".to_string()]);
|
||||
assert_eq!(len, 1);
|
||||
// "Real Relation" + auto-generated "Type" (from is_a: "[[type/project]]")
|
||||
assert_eq!(len, 2);
|
||||
assert_eq!(
|
||||
rels.get("Type").unwrap(),
|
||||
&vec!["[[type/project]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1390,5 +1418,57 @@ References:
|
||||
assert_eq!(entry.created_at, Some(1747958400));
|
||||
}
|
||||
|
||||
// --- Type relationship tests ---
|
||||
|
||||
#[test]
|
||||
fn test_type_relationship_added_for_regular_entries() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nIs A: Project\n---\n# My Project\n";
|
||||
let entry = parse_test_entry(&dir, "project/my-project.md", content);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/project]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_relationship_skipped_for_type_documents() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nIs A: Type\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
assert!(entry.relationships.get("Type").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_relationship_from_folder_inference() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "# A Person\n\nSome content.";
|
||||
let entry = parse_test_entry(&dir, "person/someone.md", content);
|
||||
assert_eq!(entry.is_a, Some("Person".to_string()));
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/person]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_relationship_handles_wikilink_is_a() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nIs A: \"[[type/experiment]]\"\n---\n# Test\n";
|
||||
let entry = parse_test_entry(&dir, "test.md", content);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/experiment]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_folder_inferred_as_type() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "# Some Type\n";
|
||||
let entry = parse_test_entry(&dir, "type/some-type.md", content);
|
||||
assert_eq!(entry.is_a, Some("Type".to_string()));
|
||||
}
|
||||
|
||||
// Frontmatter update/delete tests are in frontmatter.rs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user