use crate::kind_expand;
use crate::types::{Edge, Payload, Symbol};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum DecodeError {
EmptyInput,
InvalidHeader(String),
InvalidField(String),
InvalidSymbolLine(String),
InvalidEdgeLine(String),
UnknownEdgeId(String),
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DecodeError::EmptyInput => write!(f, "gcf: empty input"),
DecodeError::InvalidHeader(h) => {
write!(f, "gcf: invalid header, expected 'GCF ...' got {:?}", h)
}
DecodeError::InvalidField(msg) => write!(f, "gcf: {}", msg),
DecodeError::InvalidSymbolLine(msg) => write!(f, "invalid_node_line: {}", msg),
DecodeError::InvalidEdgeLine(msg) => write!(f, "invalid_edge_syntax: {}", msg),
DecodeError::UnknownEdgeId(msg) => write!(f, "unknown_edge_reference: {}", msg),
}
}
}
impl std::error::Error for DecodeError {}
pub fn decode(input: &str) -> Result<Payload, DecodeError> {
let lines: Vec<&str> = input.split('\n').collect();
if lines.is_empty() || (lines.len() == 1 && lines[0].is_empty()) {
return Err(DecodeError::EmptyInput);
}
let header = lines[0];
if !header.starts_with("GCF ") {
return Err(DecodeError::InvalidHeader(header.to_string()));
}
let mut p = Payload {
tool: String::new(),
tokens_used: 0,
token_budget: 0,
pack_root: String::new(),
symbols: Vec::new(),
edges: Vec::new(),
};
parse_header(&header[4..], &mut p)?;
let mut symbols: Vec<Symbol> = Vec::new();
let mut sym_by_id: HashMap<usize, usize> = HashMap::new(); let mut current_distance: i32 = 0;
let mut in_edges = false;
let mut declared_edges: i64 = -1;
let mut edges_declared = false;
let is_delta = header.contains("delta=true");
let valid_delta_sections: std::collections::HashSet<&str> =
["removed", "added", "edges_removed", "edges_added"]
.iter()
.copied()
.collect();
for &line in &lines[1..] {
let line = line.trim_end_matches('\r');
if line.is_empty() {
continue;
}
if line.starts_with("##! ") {
continue;
}
if let Some(raw_group) = line.strip_prefix("## ") {
let mut declared_count: i64 = -1;
let group = match raw_group.find(" [") {
Some(idx) => {
let bracket = &raw_group[idx + 2..];
if let Some(end) = bracket.find(']') {
let cnt_str = &bracket[..end];
if cnt_str != "?" {
match cnt_str.parse::<i64>() {
Ok(n) => declared_count = n,
Err(_) => {
return Err(DecodeError::InvalidField(format!(
"count_mismatch: invalid section count {:?}",
cnt_str
)))
}
}
}
}
&raw_group[..idx]
}
None => raw_group,
};
if is_delta && !valid_delta_sections.contains(group) {
return Err(DecodeError::InvalidField(format!(
"malformed_delta: invalid delta section {:?}",
group
)));
}
if group == "edges" {
in_edges = true;
if declared_count >= 0 {
declared_edges = declared_count;
edges_declared = true;
}
} else {
in_edges = false;
match group {
"targets" => current_distance = 0,
"related" => current_distance = 1,
"extended" => current_distance = 2,
_ => {
if let Some(d) = group.strip_prefix("distance_") {
if let Ok(n) = d.parse::<i32>() {
current_distance = n;
}
}
}
}
}
continue;
}
if line.starts_with("# ") {
continue;
}
if in_edges {
let edge = parse_edge_line(line, &sym_by_id, &symbols)?;
p.edges.push(edge);
} else {
let (sym, id) = parse_symbol_line(line, current_distance)?;
symbols.push(sym);
sym_by_id.insert(id, symbols.len() - 1);
}
}
if edges_declared && p.edges.len() as i64 != declared_edges {
return Err(DecodeError::InvalidField(format!(
"count_mismatch: declared {} edges, got {}",
declared_edges,
p.edges.len()
)));
}
p.symbols = symbols;
Ok(p)
}
fn parse_header(fields: &str, p: &mut Payload) -> Result<(), DecodeError> {
for part in fields.split_whitespace() {
let mut kv = part.splitn(2, '=');
let key = match kv.next() {
Some(k) => k,
None => continue,
};
let val = match kv.next() {
Some(v) => v,
None => continue,
};
match key {
"tool" => p.tool = val.to_string(),
"budget" => {
p.token_budget = val
.parse::<i64>()
.map_err(|_| DecodeError::InvalidField(format!("invalid budget {:?}", val)))?;
}
"tokens" => {
p.tokens_used = val
.parse::<i64>()
.map_err(|_| DecodeError::InvalidField(format!("invalid tokens {:?}", val)))?;
}
"pack_root" => p.pack_root = val.to_string(),
"symbols" => { }
_ => { }
}
}
Ok(())
}
fn parse_symbol_line(line: &str, distance: i32) -> Result<(Symbol, usize), DecodeError> {
if !line.starts_with('@') {
return Err(DecodeError::InvalidSymbolLine(format!(
"expected symbol line starting with @, got {:?}",
line
)));
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
return Err(DecodeError::InvalidSymbolLine(format!(
"symbol line needs at least 5 fields, got {} in {:?}",
parts.len(),
line
)));
}
let id_str = &parts[0][1..]; let id: usize = id_str
.parse()
.map_err(|_| DecodeError::InvalidSymbolLine(format!("invalid_symbol_id: {:?}", id_str)))?;
let kind = kind_expand(parts[1]);
let qname = parts[2].to_string();
let score: f64 = parts[3]
.parse()
.map_err(|_| DecodeError::InvalidSymbolLine(format!("invalid_score: {:?}", parts[3])))?;
let provenance = parts[4].to_string();
Ok((
Symbol {
qualified_name: qname,
kind,
score,
provenance,
distance,
signature: String::new(),
components: Default::default(),
},
id,
))
}
fn parse_edge_line(
line: &str,
sym_by_id: &HashMap<usize, usize>,
symbols: &[Symbol],
) -> Result<Edge, DecodeError> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return Err(DecodeError::InvalidEdgeLine(format!(
"edge line needs at least 2 fields, got {:?}",
line
)));
}
let reference = parts[0];
let lt_idx = match reference.find('<') {
Some(i) => i,
None => {
return Err(DecodeError::InvalidEdgeLine(format!(
"edge line missing '<' separator in {:?}",
reference
)));
}
};
let target_id_str = &reference[1..lt_idx]; let source_id_str = &reference[lt_idx + 2..];
let target_id: usize = target_id_str.parse().map_err(|_| {
DecodeError::InvalidEdgeLine(format!("invalid target id {:?}", target_id_str))
})?;
let source_id: usize = source_id_str.parse().map_err(|_| {
DecodeError::InvalidEdgeLine(format!("invalid source id {:?}", source_id_str))
})?;
let target_idx = sym_by_id.get(&target_id).ok_or_else(|| {
DecodeError::UnknownEdgeId(format!(
"edge references unknown symbol id(s): target={} source={}",
target_id, source_id
))
})?;
let source_idx = sym_by_id.get(&source_id).ok_or_else(|| {
DecodeError::UnknownEdgeId(format!(
"edge references unknown symbol id(s): target={} source={}",
target_id, source_id
))
})?;
let edge_type = parts[1].to_string();
let status = if parts.len() >= 3 {
parts[2].to_string()
} else {
String::new()
};
Ok(Edge {
source: symbols[*source_idx].qualified_name.clone(),
target: symbols[*target_idx].qualified_name.clone(),
edge_type,
status,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_basic() {
let input = "\
GCF tool=context_for_task budget=5000 tokens=1847 symbols=2
## targets
@0 fn pkg.AuthMiddleware 0.78 lsp_resolved
## related
@1 fn pkg.NewServer 0.54 lsp_resolved
## edges
@0<@1 calls
";
let p = decode(input).unwrap();
assert_eq!(p.tool, "context_for_task");
assert_eq!(p.token_budget, 5000);
assert_eq!(p.tokens_used, 1847);
assert_eq!(p.symbols.len(), 2);
assert_eq!(p.symbols[0].qualified_name, "pkg.AuthMiddleware");
assert_eq!(p.symbols[0].kind, "function"); assert_eq!(p.symbols[0].distance, 0);
assert_eq!(p.symbols[1].distance, 1);
assert_eq!(p.edges.len(), 1);
assert_eq!(p.edges[0].source, "pkg.NewServer");
assert_eq!(p.edges[0].target, "pkg.AuthMiddleware");
assert_eq!(p.edges[0].edge_type, "calls");
}
#[test]
fn test_decode_rejects_bad_header() {
let result = decode("NOT_GCF tool=x");
assert!(result.is_err());
match result.unwrap_err() {
DecodeError::InvalidHeader(_) => {}
other => panic!("expected InvalidHeader, got {:?}", other),
}
}
#[test]
fn test_decode_accepts_no_tool() {
let result = decode("GCF budget=100 tokens=50 symbols=0");
assert!(result.is_ok());
assert_eq!(result.unwrap().tool, "");
}
#[test]
fn test_decode_rejects_too_few_fields() {
let input = "GCF tool=t budget=0 tokens=0 symbols=1\n## targets\n@0 fn pkg.X 0.5";
let result = decode(input);
assert!(result.is_err());
}
#[test]
fn test_decode_rejects_unknown_edge_ids() {
let input = "\
GCF tool=t budget=0 tokens=0 symbols=1
## targets
@0 fn a.B 0.50 p
## edges
@0<@99 calls
";
let result = decode(input);
assert!(result.is_err());
}
#[test]
fn test_decode_tolerates_crlf() {
let input = "GCF tool=t budget=0 tokens=0 symbols=1\r\n## targets\r\n@0 fn a.B 0.50 p\r\n";
let p = decode(input).unwrap();
assert_eq!(p.symbols.len(), 1);
assert_eq!(p.symbols[0].qualified_name, "a.B");
}
#[test]
fn test_decode_expands_kind() {
let input = "GCF tool=t budget=0 tokens=0 symbols=1\n## targets\n@0 iface a.B 0.50 p\n";
let p = decode(input).unwrap();
assert_eq!(p.symbols[0].kind, "interface");
}
#[test]
fn test_decode_empty_input() {
let result = decode("");
assert!(result.is_err());
}
#[test]
fn test_roundtrip() {
use crate::encode::encode;
use crate::types::Edge;
let p = Payload {
tool: "roundtrip_test".to_string(),
token_budget: 2000,
tokens_used: 500,
pack_root: String::new(),
symbols: vec![
Symbol {
qualified_name: "pkg.Func1".to_string(),
kind: "function".to_string(),
score: 0.95,
provenance: "lsp_resolved".to_string(),
distance: 0,
signature: String::new(),
components: Default::default(),
},
Symbol {
qualified_name: "pkg.Type1".to_string(),
kind: "type".to_string(),
score: 0.80,
provenance: "ast_inferred".to_string(),
distance: 1,
signature: String::new(),
components: Default::default(),
},
Symbol {
qualified_name: "pkg.Iface1".to_string(),
kind: "interface".to_string(),
score: 0.60,
provenance: "lsp_resolved".to_string(),
distance: 2,
signature: String::new(),
components: Default::default(),
},
],
edges: vec![Edge {
source: "pkg.Func1".to_string(),
target: "pkg.Type1".to_string(),
edge_type: "uses".to_string(),
status: String::new(),
}],
};
let encoded = encode(&p);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.tool, p.tool);
assert_eq!(decoded.token_budget, p.token_budget);
assert_eq!(decoded.tokens_used, p.tokens_used);
assert_eq!(decoded.symbols.len(), p.symbols.len());
for (i, s) in decoded.symbols.iter().enumerate() {
assert_eq!(s.qualified_name, p.symbols[i].qualified_name);
assert_eq!(s.kind, p.symbols[i].kind);
assert!((s.score - p.symbols[i].score).abs() < 0.01);
assert_eq!(s.provenance, p.symbols[i].provenance);
assert_eq!(s.distance, p.symbols[i].distance);
}
assert_eq!(decoded.edges.len(), p.edges.len());
assert_eq!(decoded.edges[0].source, p.edges[0].source);
assert_eq!(decoded.edges[0].target, p.edges[0].target);
assert_eq!(decoded.edges[0].edge_type, p.edges[0].edge_type);
}
}