#![allow(
clippy::collapsible_if,
clippy::collapsible_else_if,
clippy::manual_strip,
clippy::type_complexity
)]
use crate::decode::decode;
use crate::scalar::{
find_closing_brace, parse_quoted_string, parse_scalar, split_field_decl,
split_respecting_quotes, ScalarValue,
};
use serde_json::{Map, Number, Value};
pub fn decode_generic(input: &str) -> Result<Value, String> {
let input = input.trim_end_matches(['\n', '\r']);
if input.is_empty() {
return Err("missing_header: empty input".into());
}
let lines: Vec<&str> = input.split('\n').collect();
let header = lines[0].trim_end_matches('\r');
if !header.starts_with("GCF ") {
return Err("missing_header: first line does not begin with GCF".into());
}
let profile = parse_header_profile(header)?;
if profile == "graph" {
let p = decode(input).map_err(|e| e.to_string())?;
return Ok(payload_to_value(&p));
}
if profile != "generic" {
return Err(format!("unknown_profile: {}", profile));
}
let mut content_lines: Vec<String> = Vec::new();
let mut deferred_count = 0;
let mut summary_line = String::new();
for line in &lines[1..] {
let line = line.trim_end_matches('\r');
if line.is_empty() {
continue;
}
for c in line.chars() {
if c == '\t' {
return Err("tab_indentation: tabs in leading whitespace".into());
}
if c != ' ' {
break;
}
}
let trimmed = line.trim_start();
if trimmed.starts_with("# ") {
continue;
}
if trimmed.starts_with("##! ") {
summary_line = trimmed.to_string();
continue;
}
if trimmed.starts_with("## ") && trimmed.contains("[?]") {
deferred_count += 1;
}
content_lines.push(line.to_string());
}
if !summary_line.is_empty() && deferred_count > 0 {
validate_summary_counts(&summary_line, deferred_count, &content_lines)?;
}
if content_lines.is_empty() {
return Ok(Value::Object(Map::new()));
}
let first = content_lines[0].trim_start();
if first.starts_with('=') {
if content_lines.len() > 1 {
return Err("trailing_characters: extra lines after root scalar".into());
}
return scalar_to_value(&parse_scalar(first.strip_prefix("=").unwrap(), false)?);
}
if first.starts_with("## [") {
let (arr, _) = parse_array_from_header(&content_lines, 0, 0, &first[3..])?;
return Ok(arr);
}
let mut result = Map::new();
parse_object_body(&content_lines, 0, 0, &mut result)?;
Ok(Value::Object(result))
}
fn parse_header_profile(header: &str) -> Result<String, String> {
let parts: Vec<&str> = header.split_whitespace().collect();
if parts.len() < 2 {
return Err("missing_profile".into());
}
let mut seen = std::collections::HashSet::new();
let mut profile = String::new();
for p in &parts[1..] {
let eq = p
.find('=')
.ok_or_else(|| format!("malformed_header_field: {}", p))?;
let key = &p[..eq];
if !seen.insert(key.to_string()) {
return Err(format!("duplicate_header_field: {}", key));
}
if key == "profile" {
profile = p[eq + 1..].to_string();
}
}
if profile.is_empty() {
return Err("missing_profile".into());
}
Ok(profile)
}
fn scalar_to_value(sv: &ScalarValue) -> Result<Value, String> {
match sv {
ScalarValue::Null => Ok(Value::Null),
ScalarValue::Bool(b) => Ok(Value::Bool(*b)),
ScalarValue::Int(i) => Ok(Value::Number(Number::from(*i))),
ScalarValue::Float(f) => Ok(Value::Number(
Number::from_f64(*f).unwrap_or_else(|| Number::from(0)),
)),
ScalarValue::Str(s) => Ok(Value::String(s.clone())),
ScalarValue::Missing => Err("invalid_missing: ~ in non-tabular context".into()),
ScalarValue::Attachment => {
Err("invalid_attachment_marker: ^ in non-tabular context".into())
}
}
}
fn parse_object_body(
lines: &[String],
start: usize,
depth: usize,
out: &mut Map<String, Value>,
) -> Result<usize, String> {
let ind = " ".repeat(depth);
let mut i = start;
while i < lines.len() {
let line = &lines[i];
if depth > 0 && !line.starts_with(&ind) {
break;
}
let content = if depth > 0 {
&line[ind.len()..]
} else {
line.as_str()
};
if !content.is_empty() && content.starts_with(' ') {
return Err("invalid_indent: indentation increases by more than one level".into());
}
if let Some(hdr) = content.strip_prefix("## ") {
if let Some(bi) = hdr.find(" [") {
let name = parse_key_from_header(&hdr[..bi])?;
check_dup(out, &name)?;
let (arr, consumed) = parse_array_from_header(lines, i, depth, &hdr[bi..])?;
out.insert(name, arr);
i += consumed;
continue;
}
let name = parse_key_from_header(hdr)?;
check_dup(out, &name)?;
i += 1;
let mut nested = Map::new();
let consumed = parse_object_body(lines, i, depth + 1, &mut nested)?;
out.insert(name, Value::Object(nested));
i += consumed;
continue;
}
if let Some(eq_idx) = find_kv_split(content) {
if eq_idx > 0 {
let name = parse_key_from_header(&content[..eq_idx])?;
check_dup(out, &name)?;
let val = scalar_to_value(&parse_scalar(&content[eq_idx + 1..], false)?)?;
out.insert(name, val);
i += 1;
continue;
}
}
if !content.starts_with('@') && !content.starts_with("##") {
if let Some(bracket_idx) = content.find('[') {
if bracket_idx > 0 {
let rest = &content[bracket_idx..];
if let Some(close_idx) = rest.find(']') {
let after = &rest[close_idx + 1..];
if after.starts_with(": ") || after == ":" {
let name = parse_key_from_header(&content[..bracket_idx])?;
check_dup(out, &name)?;
let (arr, _) = parse_array_from_header(lines, i, depth, rest)?;
out.insert(name, arr);
i += 1;
continue;
}
}
}
}
}
i += 1;
}
Ok(i - start)
}
fn find_kv_split(s: &str) -> Option<usize> {
if s.is_empty() {
return None;
}
let bytes = s.as_bytes();
if bytes[0] == b'"' {
let mut i = 1;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == b'"' {
return if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
Some(i + 1)
} else {
None
};
}
i += 1;
}
return None;
}
let eq_idx = s.find('=')?;
if let Some(bracket_idx) = s.find('[') {
if bracket_idx < eq_idx {
return None;
}
}
Some(eq_idx)
}
fn parse_key_from_header(s: &str) -> Result<String, String> {
let s = s.trim();
if s.len() >= 2 && s.starts_with('"') {
parse_quoted_string(s)
} else {
Ok(s.to_string())
}
}
fn check_dup(map: &Map<String, Value>, key: &str) -> Result<(), String> {
if map.contains_key(key) {
Err(format!("duplicate_key: {}", key))
} else {
Ok(())
}
}
fn parse_array_from_header(
lines: &[String],
header_line: usize,
depth: usize,
bracket_part: &str,
) -> Result<(Value, usize), String> {
let bp = bracket_part.trim_start();
if !bp.starts_with('[') {
return Err("invalid_count".into());
}
let close = bp.find(']').ok_or("invalid_count")?;
let count_str = &bp[1..close];
let after = &bp[close + 1..];
let count: i64 = if count_str == "?" {
-1
} else {
parse_count(count_str)? as i64
};
if count == 0 && !after.starts_with('{') && !after.starts_with(':') {
return Ok((Value::Array(vec![]), 1));
}
if after.starts_with(": ") || after == ":" {
let vals_str = if after.starts_with(": ") {
after.strip_prefix(": ").unwrap()
} else {
""
};
if vals_str.is_empty() {
if count > 0 {
return Err(format!("count_mismatch: declared {}, got 0", count));
}
return Ok((Value::Array(vec![]), 1));
}
let vals = split_respecting_quotes(vals_str, ',');
if count >= 0 && vals.len() as i64 != count {
return Err(format!(
"count_mismatch: declared {}, got {}",
count,
vals.len()
));
}
let parsed: Result<Vec<Value>, String> = vals
.iter()
.map(|v| scalar_to_value(&parse_scalar(v.trim(), false)?))
.collect();
return Ok((Value::Array(parsed?), 1));
}
if after.starts_with('{') {
let brace_end = find_closing_brace(after).ok_or("invalid field declaration")?;
let fields = split_field_decl(&after[..brace_end + 1])?;
let (rows, consumed) = parse_tabular_body(lines, header_line + 1, depth, &fields, count)?;
if count >= 0 && rows.len() as i64 != count {
return Err(format!(
"count_mismatch: declared {}, got {}",
count,
rows.len()
));
}
return Ok((Value::Array(rows), consumed + 1));
}
let (items, consumed) = parse_expanded_body(lines, header_line + 1, depth)?;
if count >= 0 && items.len() as i64 != count {
return Err(format!(
"count_mismatch: declared {}, got {}",
count,
items.len()
));
}
Ok((Value::Array(items), consumed + 1))
}
fn parse_tabular_body(
lines: &[String],
start: usize,
depth: usize,
fields: &[String],
expected_count: i64,
) -> Result<(Vec<Value>, usize), String> {
parse_tabular_body_with_shared(lines, start, depth, fields, expected_count, None)
}
fn unflatten_paths(
path_columns: &std::collections::HashMap<String, Vec<String>>,
flat_values: &std::collections::HashMap<String, Value>,
flat_absent: &std::collections::HashSet<String>,
) -> Map<String, Value> {
let mut groups: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut group_order: Vec<String> = Vec::new();
for (field_name, paths) in path_columns {
if paths.is_empty() {
continue;
}
let top = &paths[0];
if !groups.contains_key(top) {
group_order.push(top.clone());
groups.insert(top.clone(), Vec::new());
}
groups.get_mut(top).unwrap().push(field_name.clone());
}
let mut result = Map::new();
for top in &group_order {
let field_names = &groups[top];
let all_absent = field_names.iter().all(|f| flat_absent.contains(f));
let all_null = field_names.iter().all(|f| {
if flat_absent.contains(f) {
return false; }
matches!(flat_values.get(f), Some(Value::Null) | None)
});
if all_absent {
continue; }
if all_null {
result.insert(top.clone(), Value::Null);
continue;
}
for field_name in field_names {
if flat_absent.contains(field_name) {
continue;
}
let paths = &path_columns[field_name];
let val = flat_values.get(field_name).cloned().unwrap_or(Value::Null);
let mut current = &mut result;
for k in &paths[..paths.len() - 1] {
if !current.contains_key(k) {
current.insert(k.clone(), Value::Object(Map::new()));
}
current = current.get_mut(k).unwrap().as_object_mut().unwrap();
}
current.insert(paths.last().unwrap().clone(), val);
}
}
result
}
fn parse_tabular_body_with_shared(
lines: &[String],
start: usize,
depth: usize,
fields: &[String],
expected_count: i64,
parent_shared_schemas: Option<&std::collections::HashMap<String, Vec<String>>>,
) -> Result<(Vec<Value>, usize), String> {
let ind = " ".repeat(depth);
let mut rows: Vec<Value> = Vec::new();
let mut i = start;
let mut path_column_map: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for f in fields {
if f.contains('>') {
let parts: Vec<String> = f.split('>').map(|s| s.to_string()).collect();
if parts.iter().all(|p| !p.is_empty()) {
path_column_map.insert(f.clone(), parts);
}
}
}
let mut inline_schemas: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut shared_array_schemas: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
if let Some(parent) = parent_shared_schemas {
for (k, v) in parent {
shared_array_schemas.insert(k.clone(), v.clone());
}
}
while i < lines.len() {
let line = &lines[i];
let content = if depth > 0 {
if !line.starts_with(&ind) {
break;
}
&line[ind.len()..]
} else {
line.as_str()
};
if content.starts_with("## ") || content.starts_with("##!") {
break;
}
if !content.is_empty() && content.as_bytes()[0] == b' ' {
let trimmed = content.trim_start();
if trimmed.starts_with('.') {
break; }
break;
}
let mut row_data = content;
let mut row_has_id = false;
if row_data.starts_with('@') {
if let Some(sp) = row_data.find(' ') {
let id_str = &row_data[1..sp];
let valid_id = !id_str.is_empty() && id_str.bytes().all(|b| b.is_ascii_digit());
if valid_id {
row_data = &row_data[sp + 1..];
row_has_id = true;
}
}
}
let vals = split_respecting_quotes(row_data, '|');
if vals.len() != fields.len() {
return Err(format!(
"row_width_mismatch: expected {} fields, got {}",
fields.len(),
vals.len()
));
}
let mut row: Map<String, Value> = Map::new();
let mut traditional_att_fields: Vec<String> = Vec::new();
let mut inline_att_fields: Vec<String> = Vec::new();
let mut inline_att_order: Vec<String> = Vec::new();
let mut flat_values: std::collections::HashMap<String, Value> =
std::collections::HashMap::new();
let mut flat_absent: std::collections::HashSet<String> = std::collections::HashSet::new();
for (j, f) in fields.iter().enumerate() {
let cell_val = &vals[j];
if path_column_map.contains_key(f) {
let parsed = parse_scalar(cell_val, true)?;
match parsed {
ScalarValue::Missing => {
flat_absent.insert(f.clone());
}
_ => {
flat_values.insert(f.clone(), scalar_to_value(&parsed)?);
}
}
continue;
}
if cell_val.starts_with("^{") && cell_val.ends_with('}') {
let schema_str = &cell_val[1..]; let ifs = split_field_decl(schema_str)?;
inline_schemas.insert(f.clone(), ifs);
inline_att_fields.push(f.clone());
inline_att_order.push(f.clone());
continue;
}
let parsed = parse_scalar(cell_val, true)?;
match parsed {
ScalarValue::Missing => {
}
ScalarValue::Attachment => {
if inline_schemas.contains_key(f) {
inline_att_fields.push(f.clone());
inline_att_order.push(f.clone());
} else {
traditional_att_fields.push(f.clone());
}
}
_ => {
row.insert(f.clone(), scalar_to_value(&parsed)?);
}
}
}
i += 1;
let mut all_att_fields: Vec<String> = Vec::new();
for f in fields {
let is_trad = traditional_att_fields.iter().any(|tf| tf == f);
let is_inline = inline_att_fields.iter().any(|inf| inf == f);
if is_trad || is_inline {
all_att_fields.push(f.clone());
}
}
if row_has_id {
let mut resolved_attachments: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut inline_idx: usize = 0;
while i < lines.len() {
let a_line = &lines[i];
let a_content = if a_line.starts_with(&format!("{} ", ind)) {
&a_line[ind.len() + 2..]
} else if a_line.starts_with(&ind) {
&a_line[ind.len()..]
} else {
break;
};
if a_content.starts_with('.') {
let rest = &a_content[1..];
let (att_name, after_name_raw) = parse_attachment_name(rest);
let after_name = after_name_raw.trim_start();
if resolved_attachments.contains(&att_name) {
return Err(format!("duplicate_attachment: {}", att_name));
}
if let Some(ifs) = inline_schemas.get(&att_name) {
if !after_name.starts_with("{}") && !after_name.starts_with('[') {
let inline_vals = split_respecting_quotes(after_name, '|');
if inline_vals.len() != ifs.len() {
return Err(format!(
"inline_width_mismatch: {} expected {}, got {}",
att_name,
ifs.len(),
inline_vals.len()
));
}
let mut obj = Map::new();
for (k, inf) in ifs.iter().enumerate() {
let p = parse_scalar(&inline_vals[k], true)?;
match p {
ScalarValue::Missing => {}
_ => {
obj.insert(inf.clone(), scalar_to_value(&p)?);
}
}
}
row.insert(att_name.clone(), Value::Object(obj));
resolved_attachments.insert(att_name);
i += 1;
continue;
}
}
let (att_name_t, att_val, consumed, parsed_fields) =
parse_attachment_v3(lines, i, rest, depth + 2, &shared_array_schemas)?;
if rows.is_empty() {
if let Some(pf) = parsed_fields {
shared_array_schemas.insert(att_name_t.clone(), pf);
}
}
resolved_attachments.insert(att_name_t.clone());
row.insert(att_name_t, att_val);
i += consumed;
continue;
}
let mut found_inline = false;
let mut next_inline_field = String::new();
while inline_idx < inline_att_order.len() {
let candidate = &inline_att_order[inline_idx];
if !resolved_attachments.contains(candidate) {
next_inline_field = candidate.clone();
found_inline = true;
break;
}
inline_idx += 1;
}
if !found_inline {
break; }
let ifs = inline_schemas
.get(&next_inline_field)
.ok_or_else(|| {
format!("missing inline schema for field: {}", next_inline_field)
})?
.clone();
let inline_vals = split_respecting_quotes(a_content, '|');
if inline_vals.len() != ifs.len() {
return Err(format!(
"inline_width_mismatch: {} expected {}, got {}",
next_inline_field,
ifs.len(),
inline_vals.len()
));
}
let mut obj = Map::new();
for (k, inf) in ifs.iter().enumerate() {
let p = parse_scalar(&inline_vals[k], true)?;
match p {
ScalarValue::Missing => {}
_ => {
obj.insert(inf.clone(), scalar_to_value(&p)?);
}
}
}
resolved_attachments.insert(next_inline_field.clone());
row.insert(next_inline_field, Value::Object(obj));
inline_idx += 1;
i += 1;
}
for f in &all_att_fields {
if !resolved_attachments.contains(f) {
return Err(format!("missing_attachment: {}", f));
}
}
if i < lines.len() {
let extra_line = &lines[i];
let extra_content = if extra_line.starts_with(&format!("{} ", ind)) {
&extra_line[ind.len() + 2..]
} else if extra_line.starts_with(&ind) {
&extra_line[ind.len()..]
} else {
""
};
if extra_content.starts_with('.') {
let (extra_name, _) = parse_attachment_name(&extra_content[1..]);
if resolved_attachments.contains(&extra_name) {
return Err(format!("duplicate_attachment: {}", extra_name));
}
}
}
}
if !path_column_map.is_empty() {
let nested = unflatten_paths(&path_column_map, &flat_values, &flat_absent);
for (k, v) in nested {
row.insert(k, v);
}
}
rows.push(Value::Object(row));
if expected_count >= 0 && rows.len() as i64 >= expected_count {
break;
}
}
Ok((rows, i - start))
}
fn parse_attachment_name(rest: &str) -> (String, &str) {
if rest.starts_with('"') {
let bytes = rest.as_bytes();
let mut j = 1;
while j < bytes.len() {
if bytes[j] == b'\\' {
j += 2;
continue;
}
if bytes[j] == b'"' {
if let Ok(parsed) = parse_quoted_string(&rest[..j + 1]) {
return (parsed, &rest[j + 1..]);
}
return (String::new(), rest);
}
j += 1;
}
(String::new(), rest)
} else {
if let Some(sp) = rest.find(' ') {
(rest[..sp].to_string(), &rest[sp..])
} else {
(rest.to_string(), "")
}
}
}
fn parse_attachment_v3(
lines: &[String],
line_idx: usize,
rest: &str,
depth: usize,
shared_schemas: &std::collections::HashMap<String, Vec<String>>,
) -> Result<(String, Value, usize, Option<Vec<String>>), String> {
let (name, after_name_raw) = parse_attachment_name(rest);
if name.is_empty() && !rest.starts_with("\"\"") {
return Err("invalid attachment".into());
}
let after_name = after_name_raw.trim_start();
if after_name.starts_with("{}") {
let mut nested = Map::new();
let consumed = parse_object_body(lines, line_idx + 1, depth, &mut nested)?;
return Ok((name, Value::Object(nested), consumed + 1, None));
}
if after_name.starts_with('[') {
let close_bracket = after_name
.find(']')
.ok_or_else(|| "invalid_count: missing ]".to_string())?;
let after_close = &after_name[close_bracket + 1..];
if after_close.starts_with('{') {
let end_brace = find_closing_brace(after_close);
let mut parsed_fields: Option<Vec<String>> = None;
if let Some(eb) = end_brace {
if let Ok(pf) = split_field_decl(&after_close[..eb + 1]) {
parsed_fields = Some(pf);
}
}
let (arr, consumed) = parse_array_from_header(lines, line_idx, depth, after_name)?;
return Ok((name, arr, consumed, parsed_fields));
}
if after_close.starts_with(": ") || after_close == ":" {
let (arr, consumed) = parse_array_from_header(lines, line_idx, depth, after_name)?;
return Ok((name, arr, consumed, None));
}
if let Some(sf) = shared_schemas.get(&name) {
let count_str = &after_name[1..close_bracket];
let count: i64 = if count_str == "?" {
-1
} else {
parse_count(count_str)? as i64
};
if count == 0 {
return Ok((name, Value::Array(vec![]), 1, None));
}
let mut use_shared = true;
let next_idx = line_idx + 1;
let indent_str = " ".repeat(depth);
if next_idx < lines.len() {
let next_line = &lines[next_idx];
let next_content = if depth > 0 && next_line.starts_with(&indent_str) {
&next_line[indent_str.len()..]
} else {
next_line.as_str()
};
if next_content.trim_start().starts_with('@') {
use_shared = false;
}
}
if use_shared {
let (tab_rows, consumed) = parse_tabular_body_with_shared(
lines,
line_idx + 1,
depth,
sf,
count,
Some(shared_schemas),
)?;
if count >= 0 && tab_rows.len() as i64 != count {
return Err(format!(
"count_mismatch: declared {}, got {}",
count,
tab_rows.len()
));
}
return Ok((name, Value::Array(tab_rows), consumed + 1, None));
}
}
let (arr, consumed) = parse_array_from_header(lines, line_idx, depth, after_name)?;
return Ok((name, arr, consumed, None));
}
if after_name.starts_with('=') {
let val_str = &after_name[1..];
let parsed = parse_scalar(val_str, true)?;
match parsed {
ScalarValue::Missing => Ok((name, Value::Null, 1, None)),
_ => Ok((name, scalar_to_value(&parsed)?, 1, None)),
}
} else {
Err(format!("invalid attachment form: {}", after_name))
}
}
fn parse_expanded_body(
lines: &[String],
start: usize,
depth: usize,
) -> Result<(Vec<Value>, usize), String> {
let ind = " ".repeat(depth);
let mut items: Vec<Value> = Vec::new();
let mut i = start;
while i < lines.len() {
let line = &lines[i];
let content = if depth > 0 {
if !line.starts_with(&ind) {
break;
}
&line[ind.len()..]
} else {
line.as_str()
};
if content.starts_with("## ") || content.starts_with("##!") {
break;
}
if !content.starts_with('@') {
break;
}
let sp = match content.find(' ') {
Some(s) => s,
None => break,
};
let id_str = &content[1..sp];
if let Ok(id) = id_str.parse::<usize>() {
if id != items.len() {
return Err(format!(
"invalid_item_id: expected @{}, got @{}",
items.len(),
id_str
));
}
}
let marker = &content[sp + 1..];
if marker.starts_with('=') {
let val = scalar_to_value(&parse_scalar(marker.strip_prefix("=").unwrap(), false)?)?;
items.push(val);
i += 1;
continue;
}
if marker.starts_with("{}") {
let mut nested = Map::new();
i += 1;
let consumed = parse_object_body(lines, i, depth + 1, &mut nested)?;
items.push(Value::Object(nested));
i += consumed;
continue;
}
if marker.starts_with('[') {
let (arr, consumed) = parse_array_from_header(lines, i, depth + 1, marker)?;
items.push(arr);
i += consumed;
continue;
}
break;
}
Ok((items, i - start))
}
fn parse_count(s: &str) -> Result<usize, String> {
if s == "0" {
return Ok(0);
}
if s.is_empty() || s.starts_with('0') {
return Err(format!("invalid_count: {}", s));
}
s.parse::<usize>()
.map_err(|_| format!("invalid_count: {}", s))
}
fn payload_to_value(p: &crate::types::Payload) -> Value {
let syms: Vec<Value> = p
.symbols
.iter()
.map(|s| {
serde_json::json!({
"qualifiedName": s.qualified_name,
"kind": s.kind,
"score": s.score,
"provenance": s.provenance,
"distance": s.distance,
})
})
.collect();
let edges: Vec<Value> = p
.edges
.iter()
.map(|e| {
serde_json::json!({
"source": e.source,
"target": e.target,
"edgeType": e.edge_type,
"status": e.status,
})
})
.collect();
serde_json::json!({
"tool": p.tool,
"tokenBudget": p.token_budget,
"tokensUsed": p.tokens_used,
"packRoot": p.pack_root,
"symbols": syms,
"edges": edges,
})
}
fn validate_summary_counts(
summary_line: &str,
deferred_count: usize,
content_lines: &[String],
) -> Result<(), String> {
let counts_str = summary_line
.split_whitespace()
.find(|p| p.starts_with("counts="))
.map(|p| &p[7..])
.unwrap_or("");
if counts_str.is_empty() {
return Ok(());
}
let count_vals: Vec<&str> = counts_str.split(',').collect();
if count_vals.len() != deferred_count {
return Err(format!(
"count_mismatch: summary has {} count entries but {} deferred sections",
count_vals.len(),
deferred_count
));
}
let mut actual_counts: Vec<usize> = Vec::new();
let mut in_deferred = false;
let mut current_count = 0;
for line in content_lines {
let trimmed = line.trim_start();
if trimmed.starts_with("## ") && trimmed.contains("[?]") {
if in_deferred {
actual_counts.push(current_count);
}
in_deferred = true;
current_count = 0;
continue;
}
if trimmed.starts_with("## ") {
if in_deferred {
actual_counts.push(current_count);
in_deferred = false;
}
continue;
}
if in_deferred && !trimmed.starts_with(' ') && !trimmed.starts_with('.') {
current_count += 1;
}
}
if in_deferred {
actual_counts.push(current_count);
}
for (idx, cv) in count_vals.iter().enumerate() {
let declared: usize = cv
.parse()
.map_err(|_| format!("count_mismatch: invalid count value '{}'", cv))?;
if idx < actual_counts.len() && declared != actual_counts[idx] {
return Err(format!(
"count_mismatch: section {} declared {} in summary, actual {}",
idx, declared, actual_counts[idx]
));
}
}
Ok(())
}