use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct KnowledgeTile {
pub anchor: String,
pub header: String,
pub body: String,
pub position: usize,
pub word_anchors: Vec<String>,
}
impl KnowledgeTile {
fn slugify(header: &str) -> String {
header
.trim_start_matches('#')
.trim()
.split_whitespace()
.map(|w| {
let mut c = w.chars();
match c.next() {
Some(f) => f.to_uppercase().to_string() + c.as_str(),
None => String::new(),
}
})
.collect::<String>()
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-')
.collect()
}
fn extract_word_anchors(body: &str) -> Vec<String> {
let mut anchors = Vec::new();
let mut chars = body.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '[' {
let word: String = chars.by_ref().take_while(|&c| c != ']').collect();
if !word.is_empty() && word.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
anchors.push(word);
}
}
}
anchors
}
}
#[derive(Debug, Default)]
pub struct TileRegistry {
tiles: Vec<KnowledgeTile>,
anchor_index: HashMap<String, usize>, }
impl TileRegistry {
pub fn parse(content: &str) -> Self {
let mut tiles = Vec::new();
let mut current_header = String::from("Preamble");
let mut current_body = String::new();
let mut position = 0usize;
for line in content.lines() {
if line.starts_with("## ") {
if !current_body.trim().is_empty() || !tiles.is_empty() || current_header != "Preamble" {
let anchor = KnowledgeTile::slugify(¤t_header);
let word_anchors = KnowledgeTile::extract_word_anchors(¤t_body);
tiles.push(KnowledgeTile {
anchor,
header: current_header.clone(),
body: current_body.clone(),
position,
word_anchors,
});
position += 1;
}
current_header = line.to_string();
current_body = String::new();
} else {
current_body.push_str(line);
current_body.push('\n');
}
}
if !current_body.trim().is_empty() {
let anchor = KnowledgeTile::slugify(¤t_header);
let word_anchors = KnowledgeTile::extract_word_anchors(¤t_body);
tiles.push(KnowledgeTile {
anchor,
header: current_header,
body: current_body,
position,
word_anchors,
});
}
let anchor_index = tiles
.iter()
.enumerate()
.map(|(i, t)| (t.anchor.clone(), i))
.collect();
Self { tiles, anchor_index }
}
pub fn get_at(&self, position: usize) -> Option<&KnowledgeTile> {
self.tiles.get(position)
}
pub fn get_by_anchor(&self, anchor: &str) -> Option<&KnowledgeTile> {
if let Some(&idx) = self.anchor_index.get(anchor) {
return self.tiles.get(idx);
}
let lower = anchor.to_lowercase();
self.tiles.iter().find(|t| t.anchor.to_lowercase() == lower)
}
pub fn inject_context(
&self,
current_position: usize,
lookbehind: usize,
lookahead: usize,
) -> Vec<&KnowledgeTile> {
let start = current_position.saturating_sub(lookbehind);
let end = (current_position + lookahead + 1).min(self.tiles.len());
self.tiles[start..end].iter().collect()
}
pub fn all(&self) -> &[KnowledgeTile] {
&self.tiles
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn is_empty(&self) -> bool {
self.tiles.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"
## Payment Flow
The user initiates a [PaymentFlow] request.
Funds move through [Settlement].
## Settlement
Settlement occurs after [PaymentFlow] clears.
## Refund Policy
Refunds reference the original [PaymentFlow].
"#;
#[test]
fn test_parse_tiles() {
let reg = TileRegistry::parse(SAMPLE);
assert_eq!(reg.len(), 3);
assert_eq!(reg.get_at(0).unwrap().anchor, "PaymentFlow");
assert_eq!(reg.get_at(1).unwrap().anchor, "Settlement");
}
#[test]
fn test_anchor_lookup() {
let reg = TileRegistry::parse(SAMPLE);
assert!(reg.get_by_anchor("PaymentFlow").is_some());
assert!(reg.get_by_anchor("paymentflow").is_some()); }
#[test]
fn test_word_anchors_extracted() {
let reg = TileRegistry::parse(SAMPLE);
let tile = reg.get_at(0).unwrap();
assert!(tile.word_anchors.contains(&"PaymentFlow".to_string()));
assert!(tile.word_anchors.contains(&"Settlement".to_string()));
}
#[test]
fn test_inject_context_window() {
let reg = TileRegistry::parse(SAMPLE);
let ctx = reg.inject_context(1, 1, 1);
assert_eq!(ctx.len(), 3);
}
}