store: strip .md suffix from all keys

Keys were a vestige of the file-based era. resolve_key() added .md
to lookups while upsert() used bare keys, creating phantom duplicate
nodes (the instructions bug: writes went to "instructions", reads
found "instructions.md").

- Remove .md normalization from resolve_key, strip instead
- Update all hardcoded key patterns (journal.md# → journal#, etc)
- Add strip_md_keys() migration to fsck: renames nodes and relations
- Add broken link detection to health report
- Delete redirect table (no longer needed)
- Update config defaults and config.jsonl

Migration: run `poc-memory fsck` to rename existing keys.

Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-03-08 19:41:26 -04:00
parent 77fc533631
commit 46f8fe662e
12 changed files with 289 additions and 132 deletions

View file

@ -110,10 +110,9 @@ const MONTHLY: DigestLevel = DigestLevel {
const LEVELS: &[&DigestLevel] = &[&DAILY, &WEEKLY, &MONTHLY];
/// Store key for a digest node: "daily-2026-03-04.md", "weekly-2026-W09.md", etc.
/// Matches the key format from the old import_file() path.
/// Store key for a digest node: "daily-2026-03-04", "weekly-2026-W09", etc.
fn digest_node_key(level_name: &str, label: &str) -> String {
format!("{}-{}.md", level_name, label)
format!("{}-{}", level_name, label)
}
// --- Input gathering ---
@ -148,12 +147,12 @@ fn gather(level: &DigestLevel, store: &Store, arg: &str) -> Result<(String, Vec<
} else {
// Leaf level: scan store for journal entries matching label
let date_re = Regex::new(&format!(
r"^journal\.md#j-{}", regex::escape(&label)
r"^journal#j-{}", regex::escape(&label)
)).unwrap();
let mut entries: Vec<_> = store.nodes.values()
.filter(|n| date_re.is_match(&n.key))
.map(|n| {
let ts = n.key.strip_prefix("journal.md#j-").unwrap_or(&n.key);
let ts = n.key.strip_prefix("journal#j-").unwrap_or(&n.key);
(ts.to_string(), n.content.clone())
})
.collect();
@ -257,7 +256,7 @@ pub fn digest_auto(store: &mut Store) -> Result<(), String> {
let date_re = Regex::new(r"^\d{4}-\d{2}-\d{2}").unwrap();
let dates: Vec<String> = store.nodes.keys()
.filter_map(|key| {
key.strip_prefix("journal.md#j-")
key.strip_prefix("journal#j-")
.filter(|rest| rest.len() >= 10 && date_re.is_match(rest))
.map(|rest| rest[..10].to_string())
})
@ -320,6 +319,16 @@ fn normalize_link_key(raw: &str) -> String {
let mut key = key.to_string();
// Strip .md suffix if present
if let Some(stripped) = key.strip_suffix(".md") {
key = stripped.to_string();
} else if key.contains('#') {
let (file, section) = key.split_once('#').unwrap();
if let Some(bare) = file.strip_suffix(".md") {
key = format!("{}#{}", bare, section);
}
}
// weekly/2026-W06 → weekly-2026-W06, etc.
if let Some(pos) = key.find('/') {
let prefix = &key[..pos];
@ -329,27 +338,10 @@ fn normalize_link_key(raw: &str) -> String {
}
}
// daily-2026-02-04 → daily-2026-02-04.md
let re = Regex::new(r"^(daily|weekly|monthly)-\d{4}").unwrap();
if re.is_match(&key) && !key.ends_with(".md") {
key.push_str(".md");
}
// Bare date → daily digest
let date_re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
if date_re.is_match(key.strip_suffix(".md").unwrap_or(&key)) {
let date = key.strip_suffix(".md").unwrap_or(&key);
key = format!("daily-{}.md", date);
}
// Ensure .md extension
if key.contains('#') {
let (file, section) = key.split_once('#').unwrap();
if !file.ends_with(".md") {
key = format!("{}.md#{}", file, section);
}
} else if !key.ends_with(".md") && !key.contains('/') && !key.starts_with("NEW:") {
key.push_str(".md");
if date_re.is_match(&key) {
key = format!("daily-{}", key);
}
key