render: deduplicate footer links against inline references

Render now detects neighbor keys that already appear in the node's
content and omits them from the footer link list. Inline references
serve as the node's own navigation structure; the footer catches
only neighbors not mentioned in prose.

Also fixes PEG query parser to accept hyphens in field names
(content-len was rejected).

memory-instructions-core updated to v12: documents canonical inline
link format (→ `key`), adds note about normalizing references when
updating nodes, and guidance on splitting oversized nodes.

Content is never modified for display — render is round-trippable.
Agents can read rendered output and write it back without artifacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-20 13:37:29 -04:00
parent 9517b1b310
commit 601a072cfd
2 changed files with 30 additions and 12 deletions

View file

@ -198,23 +198,41 @@ pub fn cmd_render(key: &[String]) -> Result<(), String> {
let node = store.nodes.get(&bare) let node = store.nodes.get(&bare)
.ok_or_else(|| format!("Node not found: {}", bare))?; .ok_or_else(|| format!("Node not found: {}", bare))?;
print!("{}", node.content); // Build neighbor lookup: key → strength
let mut neighbor_strengths: std::collections::HashMap<&str, f32> = std::collections::HashMap::new();
// Show links so the graph is walkable
let mut neighbors: Vec<(&str, f32)> = Vec::new();
for r in &store.relations { for r in &store.relations {
if r.deleted { continue; } if r.deleted { continue; }
if r.source_key == bare { if r.source_key == bare {
neighbors.push((&r.target_key, r.strength)); let e = neighbor_strengths.entry(&r.target_key).or_insert(0.0);
*e = e.max(r.strength);
} else if r.target_key == bare { } else if r.target_key == bare {
neighbors.push((&r.source_key, r.strength)); let e = neighbor_strengths.entry(&r.source_key).or_insert(0.0);
*e = e.max(r.strength);
} }
} }
if !neighbors.is_empty() {
neighbors.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); // Detect which neighbors are already referenced inline in the content.
neighbors.dedup_by(|a, b| a.0 == b.0); // These are omitted from the footer to avoid duplication.
let total = neighbors.len(); let mut inline_keys: std::collections::HashSet<String> = std::collections::HashSet::new();
let shown: Vec<_> = neighbors.iter().take(15) for nbr_key in neighbor_strengths.keys() {
// Match `key` (backtick-quoted) or bare key after → arrow
if node.content.contains(nbr_key) {
inline_keys.insert(nbr_key.to_string());
}
}
print!("{}", node.content);
// Footer: only show links NOT already referenced inline
let mut footer_neighbors: Vec<(&str, f32)> = neighbor_strengths.iter()
.filter(|(k, _)| !inline_keys.contains(**k))
.map(|(k, s)| (*k, *s))
.collect();
if !footer_neighbors.is_empty() {
footer_neighbors.sort_by(|a, b| b.1.total_cmp(&a.1));
let total = footer_neighbors.len();
let shown: Vec<_> = footer_neighbors.iter().take(15)
.map(|(k, s)| format!("({:.2}) `poc-memory render {}`", s, k)) .map(|(k, s)| format!("({:.2}) `poc-memory render {}`", s, k))
.collect(); .collect();
print!("\n\n---\nLinks:"); print!("\n\n---\nLinks:");

View file

@ -129,7 +129,7 @@ peg::parser! {
= "WHERE" _ e:expr() { e } = "WHERE" _ e:expr() { e }
rule field() -> String rule field() -> String
= s:$(['a'..='z' | 'A'..='Z' | '_']['a'..='z' | 'A'..='Z' | '0'..='9' | '_']*) { = s:$(['a'..='z' | 'A'..='Z' | '_']['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-']*) {
s.to_string() s.to_string()
} }