Shared memory and context tools for agentic work.
Code Rooms
use crate::{IngestAdapter, IngestStats};
use m1nd_core::error::M1ndResult;
use m1nd_core::graph::{Graph, NodeProvenanceInput};
use m1nd_core::types::{EdgeDirection, FiniteF32, NodeType};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use walkdir::WalkDir;
// ─── Extracted Record ──────────────────────────────────────────────
#[derive(Default, Debug)]
struct BibRecord {
entry_type: String, // article, inproceedings, book, etc.
cite_key: String, // vaswani2017attention
title: String,
authors: Vec<String>,
year: String,
journal: String, // or booktitle for inproceedings
doi: String,
abstract_text: String,
keywords: Vec<String>,
}
impl BibRecord {
fn external_id(&self) -> String {
if !self.doi.is_empty() {
format!("doi::{}", self.doi)
} else {
format!("bibtex::{}", self.cite_key)
fn label(&self) -> String {
if !self.title.is_empty() {
self.title.clone()
self.cite_key.clone()
fn venue(&self) -> &str {
&self.journal
// ─── BibTeX Parser ─────────────────────────────────────────────────
fn parse_bibtex(content: &str) -> Vec<BibRecord> {
let mut records = Vec::new();
let chars: Vec<char> = content.chars().collect();
let len = chars.len();
let mut pos = 0;
while pos < len {
// Find @type{key,
if chars[pos] == '@' {
pos += 1;
// Read entry type
let type_start = pos;
while pos < len && chars[pos] != '{' && !chars[pos].is_whitespace() {
let entry_type = chars[type_start..pos]
.iter()
.collect::<String>()
.to_lowercase();
// Skip comment/string/preamble
if matches!(entry_type.as_str(), "comment" | "string" | "preamble") {
// Skip to matching brace
if let Some(end) = find_matching_brace(&chars, pos) {
pos = end + 1;
continue;
// Skip whitespace and opening brace
while pos < len && (chars[pos].is_whitespace() || chars[pos] == '{') {
// Read cite key
let key_start = pos;
while pos < len && chars[pos] != ',' && !chars[pos].is_whitespace() {
let cite_key = chars[key_start..pos].iter().collect::<String>();
if pos < len && chars[pos] == ',' {
// Find end of entry (matching brace)
let entry_end = if let Some(end) =
find_matching_brace(&chars, pos - (cite_key.len() + 2).min(pos))
{
end
};
// Parse fields between pos and entry_end
let fields = parse_fields(&chars[pos..entry_end]);
let mut rec = BibRecord {
entry_type,
cite_key,
..Default::default()
for (key, val) in &fields {
match key.as_str() {
"title" => rec.title = clean_bibtex_value(val),
"author" => rec.authors = parse_authors(val),
"year" => rec.year = val.trim().to_string(),
"journal" | "journaltitle" => rec.journal = clean_bibtex_value(val),
"booktitle" if rec.journal.is_empty() => {
rec.journal = clean_bibtex_value(val);
"doi" => rec.doi = val.trim().to_string(),
"abstract" => rec.abstract_text = clean_bibtex_value(val),
"keywords" => {
rec.keywords = val
.split([',', ';'])
.map(|k| k.trim().to_string())
.filter(|k| !k.is_empty())
.collect();
_ => {}
if !rec.title.is_empty() || !rec.cite_key.is_empty() {
records.push(rec);
pos = entry_end + 1;
records
fn find_matching_brace(chars: &[char], start: usize) -> Option<usize> {
let mut depth = 0;
let mut pos = start;
while pos < chars.len() {
match chars[pos] {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(pos);
None
fn parse_fields(chars: &[char]) -> Vec<(String, String)> {
let mut fields = Vec::new();
let content: String = chars.iter().collect();
// Simple field parser: key = {value} or key = "value" or key = number
let mut remaining = content.as_str().trim();
while !remaining.is_empty() {
// Skip whitespace and commas
remaining = remaining.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
if remaining.is_empty() {
break;
// Find key
let eq_pos = match remaining.find('=') {
Some(p) => p,
None => break,
let key = remaining[..eq_pos].trim().to_lowercase();
remaining = remaining[eq_pos + 1..].trim();
// Read value
let (val, rest) = if remaining.starts_with('{') {
read_braced_value(remaining)
} else if remaining.starts_with('"') {
read_quoted_value(remaining)
// Bare value (number or macro)
let end = remaining.find([',', '}']).unwrap_or(remaining.len());
let val = remaining[..end].trim().to_string();
let rest = if end < remaining.len() {
&remaining[end..]
""
(val, rest)
if !key.is_empty() {
fields.push((key, val));
remaining = rest;
fields
fn read_braced_value(s: &str) -> (String, &str) {
let chars: Vec<char> = s.chars().collect();
let mut start = 0;
'{' => {
start = pos + 1;
depth += 1;
let byte_start = chars[..start].iter().collect::<String>().len();
let byte_end = chars[..pos].iter().collect::<String>().len();
return (s[byte_start..byte_end].to_string(), &s[byte_end + 1..]);
(String::new(), "")
fn read_quoted_value(s: &str) -> (String, &str) {
if let Some(end) = s[1..].find('"') {
(s[1..end + 1].to_string(), &s[end + 2..])
fn clean_bibtex_value(s: &str) -> String {
s.replace(['{', '}'], "")
.replace('\n', " ")
.replace(" ", " ")
.trim()
.to_string()
fn parse_authors(s: &str) -> Vec<String> {
s.split(" and ")
.map(|a| {
let cleaned = clean_bibtex_value(a.trim());
// Handle "Last, First" format → "First Last"
if let Some(comma_pos) = cleaned.find(',') {
let last = cleaned[..comma_pos].trim();
let first = cleaned[comma_pos + 1..].trim();
format!("{} {}", first, last)
cleaned
})
.filter(|a| !a.is_empty())
.collect()
// ─── Adapter ───────────────────────────────────────────────────────
pub struct BibTexAdapter {
namespace: String,
impl BibTexAdapter {
pub fn new(namespace: Option<String>) -> Self {
let namespace = namespace
.unwrap_or_else(|| "article".to_string())
let namespace = if namespace.is_empty() {
"article".to_string()
namespace
Self { namespace }
fn accepted_extension(path: &Path) -> bool {
matches!(
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase()),
Some(ext) if matches!(ext.as_str(), "bib" | "bibtex")
)
fn collect_files(&self, root: &Path) -> Vec<PathBuf> {
if root.is_file() {
return if Self::accepted_extension(root) {
vec![root.to_path_buf()]
vec![]
WalkDir::new(root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_type().is_file() && Self::accepted_extension(e.path()))
.map(|e| e.into_path())
fn file_timestamp(path: &Path) -> f64 {
std::fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
impl IngestAdapter for BibTexAdapter {
fn domain(&self) -> &str {
"article"
fn ingest(&self, root: &Path) -> M1ndResult<(Graph, IngestStats)> {
let start = Instant::now();
let files = self.collect_files(root);
let mut stats = IngestStats {
files_scanned: files.len() as u64,
let mut node_ids: HashSet<String> = HashSet::new();
let mut edge_keys: HashSet<(String, String, String)> = HashSet::new();
struct NodeRec {
id: String,
label: String,
ntype: NodeType,
tags: Vec<String>,
ts: f64,
src: String,
excerpt: Option<String>,
ns: String,
struct EdgeRec {
source: String,
target: String,
rel: String,
w: f32,
let mut nodes: Vec<NodeRec> = Vec::new();
let mut edges: Vec<EdgeRec> = Vec::new();
for path in &files {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
let rel = path
.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/");
let ts = Self::file_timestamp(path);
let records = parse_bibtex(&content);
for rec in records {
let ext_id = rec.external_id();
if !node_ids.insert(ext_id.clone()) {
let mut tags = vec![
"article".to_string(),
format!("article:type:{}", rec.entry_type),
"article:state:published".to_string(),
format!("namespace:{}", self.namespace),
];
if !rec.year.is_empty() {
tags.push(format!("article:year:{}", rec.year));
if !rec.journal.is_empty() {
tags.push(format!(
"article:venue:{}",
rec.journal.to_lowercase().replace(' ', "_")
));
for kw in &rec.keywords {
tags.push(format!("article:keyword:{}", kw.to_lowercase()));
tags.push("article:format:bibtex".to_string());
let excerpt = if !rec.abstract_text.is_empty() {
Some(rec.abstract_text.chars().take(220).collect())
nodes.push(NodeRec {
id: ext_id.clone(),
label: rec.label(),
ntype: NodeType::File,
tags,
ts,
src: rel.clone(),
excerpt,
ns: self.namespace.clone(),
});
// Author nodes + edges
for author in &rec.authors {
let aid = format!(
"article::author::{}",
author.to_lowercase().replace(' ', "_")
);
if node_ids.insert(aid.clone()) {
id: aid.clone(),
label: author.clone(),
ntype: NodeType::Concept,
tags: vec!["article".to_string(), "article:author".to_string()],
excerpt: None,
let key = (ext_id.clone(), aid.clone(), "authored_by".to_string());
if edge_keys.insert(key) {
edges.push(EdgeRec {
source: ext_id.clone(),
target: aid,
rel: "authored_by".to_string(),
w: 1.0,
// Venue node + edge
let venue = rec.venue();
if !venue.is_empty() {
let vid = format!("article::venue::{}", venue.to_lowercase().replace(' ', "_"));
if node_ids.insert(vid.clone()) {
id: vid.clone(),
label: venue.to_string(),
tags: vec!["article".to_string(), "article:venue".to_string()],
let key = (ext_id.clone(), vid.clone(), "published_in".to_string());
target: vid,
rel: "published_in".to_string(),
w: 0.9,
stats.files_parsed += 1;
// Build graph
let mut graph = Graph::with_capacity(nodes.len(), edges.len());
for n in &nodes {
let tags: Vec<&str> = n.tags.iter().map(String::as_str).collect();
if let Ok(nid) = graph.add_node(&n.id, &n.label, n.ntype, &tags, n.ts, 0.5) {
graph.set_node_provenance(
nid,
NodeProvenanceInput {
source_path: Some(&n.src),
line_start: None,
line_end: None,
excerpt: n.excerpt.as_deref(),
namespace: Some(&n.ns),
canonical: true,
},
stats.nodes_created += 1;
for e in &edges {
if let (Some(s), Some(t)) = (graph.resolve_id(&e.source), graph.resolve_id(&e.target)) {
if graph
.add_edge(
s,
t,
&e.rel,
FiniteF32::new(e.w),
EdgeDirection::Forward,
false,
FiniteF32::new(0.7),
.is_ok()
stats.edges_created += 1;
if graph.num_nodes() > 0 {
graph.finalize()?;
stats.elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
Ok((graph, stats))
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_basic_bibtex() {
let bib = r#"
@article{vaswani2017attention,
title={Attention Is All You Need},
author={Vaswani, Ashish and Shazeer, Noam and Parmar, Niki},
journal={NeurIPS},
year={2017},
doi={10.48550/arXiv.1706.03762}
@inproceedings{devlin2019bert,
title={BERT: Pre-training of Deep Bidirectional Transformers},
author={Devlin, Jacob and Chang, Ming-Wei},
booktitle={NAACL},
year={2019}
"#;
let records = parse_bibtex(bib);
assert_eq!(records.len(), 2);
assert_eq!(records[0].title, "Attention Is All You Need");
assert_eq!(records[0].cite_key, "vaswani2017attention");
assert_eq!(records[0].year, "2017");
assert_eq!(records[0].journal, "NeurIPS");
assert_eq!(records[0].doi, "10.48550/arXiv.1706.03762");
assert_eq!(records[0].authors.len(), 3);
assert!(records[0].authors[0].contains("Ashish"));
assert_eq!(records[1].entry_type, "inproceedings");
assert_eq!(records[1].journal, "NAACL"); // booktitle fallback
assert_eq!(records[1].authors.len(), 2);
fn ingest_creates_graph() {
let adapter = BibTexAdapter::new(None);
let dir = std::env::temp_dir().join("bibtex-test");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("refs.bib"),
r#"@article{test2024,
title={Test Article},
author={Smith, John and Doe, Jane},
journal={Nature},
year={2024},
doi={10.1000/test}
}"#,
.unwrap();
let (graph, stats) = adapter.ingest(&dir).expect("ingest failed");
assert!(stats.nodes_created >= 4); // article + 2 authors + venue
assert!(stats.edges_created >= 3); // 2 authored_by + published_in
assert!(graph.resolve_id("doi::10.1000/test").is_some());
assert!(graph.resolve_id("article::author::john_smith").is_some());
assert!(graph.resolve_id("article::venue::nature").is_some());
std::fs::remove_dir_all(&dir).ok();