mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
refactor: move the extraction fix out (split into a patch PR)
This commit is contained in:
+5
-12
@@ -293,18 +293,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
instead of `%C3%A9`. An accented cwd reached the server as a different
|
||||
path, and Cursor events and the session-start handoff lookup both use the
|
||||
query `cwd`. (#877)
|
||||
- Link extraction no longer mints a permanently unresolved row from a
|
||||
directory target. A `relations:` value whose final component is empty
|
||||
(`sessions/`) had the extension appended to nothing and was stored as the
|
||||
literal `sessions/.md`; the same target in a body link or wikilink
|
||||
(`[notes/](notes/)`) stayed extension-less. No page path can match either —
|
||||
page paths carry `.md` and `latest_page_id_for_link` matches exactly — so
|
||||
both sat in `links` with `to_page_id = NULL` and were visible only as
|
||||
`unresolved:` in `ai-memory status`. Both routes now skip a directory
|
||||
target, or a stem-less `.md`, with the existing warning. Unresolved
|
||||
same-project links are reported by `memory_lint` now too: it only knew
|
||||
cross-project dangling edges, so a broken internal link stayed invisible
|
||||
outside the counter. (#911)
|
||||
- `memory_lint` now reports unresolved same-project links. Only cross-project
|
||||
dangling edges (`DanglingCrossLink`) were reported, so a link to a missing
|
||||
page in the same project stayed invisible outside the status counter;
|
||||
`ReaderPool::dangling_internal_links` now feeds the same `broken_link`
|
||||
findings. (#911)
|
||||
|
||||
## [2.4.0] - 2026-09-21
|
||||
|
||||
|
||||
@@ -190,18 +190,6 @@ fn log_bounded(value: &str) -> String {
|
||||
ai_memory_core::truncate_utf8_bytes(value, RELATION_LOG_FIELD_MAX_BYTES)
|
||||
}
|
||||
|
||||
/// Whether a link target's final path segment can name a page.
|
||||
///
|
||||
/// A directory target (trailing `/`, so an empty last segment) and a
|
||||
/// stem-less `.md` cannot: the first used to normalize to the literal
|
||||
/// `notes/.md` in `relations:` frontmatter, the second to an
|
||||
/// extension-less path — both permanently unresolved `links` rows that no
|
||||
/// page write could ever repoint.
|
||||
fn last_segment_names_a_page(target: &str) -> bool {
|
||||
let last = target.rsplit_once('/').map_or(target, |(_, s)| s);
|
||||
!last.is_empty() && last != ".md"
|
||||
}
|
||||
|
||||
/// Extract typed relation edges from a page's `relations:` frontmatter
|
||||
/// (2.0 item 3):
|
||||
///
|
||||
@@ -237,18 +225,17 @@ pub fn extract_relation_links(frontmatter: &serde_json::Value) -> Vec<LinkTarget
|
||||
},
|
||||
};
|
||||
// Same terminal normalization as wikilinks: extension-less
|
||||
// targets gain `.md`; a directory target, a stem-less `.md`, and
|
||||
// anything with a non-md extension are not pages and are skipped.
|
||||
// targets gain `.md`; anything with a non-md extension is
|
||||
// not a page and is skipped.
|
||||
let raw_path = raw_path.trim();
|
||||
let last = raw_path.rsplit_once('/').map_or(raw_path, |(_, s)| s);
|
||||
let names_a_page = last_segment_names_a_page(raw_path)
|
||||
&& (!last.contains('.') || raw_path.ends_with(".md"));
|
||||
if !names_a_page {
|
||||
tracing::warn!(target = %log_bounded(target), "relation target is not a page; skipping");
|
||||
continue;
|
||||
}
|
||||
let normalized = if last.contains('.') {
|
||||
raw_path.to_string()
|
||||
if raw_path.ends_with(".md") {
|
||||
raw_path.to_string()
|
||||
} else {
|
||||
tracing::warn!(target = %log_bounded(target), "relation target is not a page; skipping");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
format!("{raw_path}.md")
|
||||
};
|
||||
@@ -381,20 +368,13 @@ fn normalize_link_target(raw: &str, page_path: &PagePath, wikilink: bool) -> Opt
|
||||
return None;
|
||||
}
|
||||
|
||||
// A directory target (`notes/`) is not a page: kept, it minted an
|
||||
// extension-less `to_path` (or, for a wikilink, the literal
|
||||
// `notes/.md`) that no page write could ever resolve.
|
||||
if !last_segment_names_a_page(target) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut target = target.to_string();
|
||||
let last_segment = target.rsplit_once('/').map_or(target.as_str(), |(_, s)| s);
|
||||
if last_segment.contains('.') {
|
||||
if !target.ends_with(".md") {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
} else if wikilink || !last_segment.is_empty() {
|
||||
target.push_str(".md");
|
||||
}
|
||||
|
||||
@@ -516,21 +496,6 @@ mod tests {
|
||||
assert_eq!(links[0].path.as_str(), "notes/ok.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_and_stemless_targets_are_not_pages() {
|
||||
// `sessions/` used to normalize to the literal `sessions/.md` — a
|
||||
// link no page write could ever resolve; the same trailing-slash
|
||||
// form in the body stayed extension-less, which cannot match a page
|
||||
// path either. Both are dropped now.
|
||||
let fm = serde_json::json!({
|
||||
"relations": {"fixes": ["sessions/", "sessions/.md"]}
|
||||
});
|
||||
assert!(extract_relation_links(&fm).is_empty());
|
||||
|
||||
assert!(extract_links("- [notes/](notes/)\n", &page()).is_empty());
|
||||
assert!(extract_links("- [[notes/]]\n", &page()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_without_relations_extract_nothing() {
|
||||
assert!(extract_relation_links(&serde_json::json!({})).is_empty());
|
||||
|
||||
+1
-2
@@ -66,8 +66,7 @@ gain `.md`.
|
||||
## Who writes them
|
||||
|
||||
- **You**, in any page's frontmatter (the wiki files are plain
|
||||
markdown — edit them and let the watcher reindex the page; `ai-memory
|
||||
reindex` only rebuilds a clean store from the markdown).
|
||||
markdown — edit and `reindex`, or let the watcher pick it up).
|
||||
- **The consolidator**, sparingly: both single-page consolidation and
|
||||
`memory_consolidate` with `multi_page=true` can preserve a relation
|
||||
when the session's evidence states it plainly (a fix
|
||||
|
||||
Reference in New Issue
Block a user