use std::path::Path;
use crate::graph::pdg::{EdgeType, NodeType, ProgramDependenceGraph, TraversalConfig};
use super::enrich::{enrich_fragment, enrich_orphan, orphan_header, owner_header};
use super::orphan::{OrphanInput, orphan_fragments};
use super::sync::FragmentCandidate;
use super::{FragmentMetadata, chunk_code};
fn leading_file_doc_end(bytes: &[u8]) -> usize {
let text = String::from_utf8_lossy(bytes);
let mut offset = 0usize;
let mut count = 0usize;
for line in text.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() {
if count == 0 {
offset += line.len() + 1;
continue;
}
break;
}
if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with("/*") {
count += 1;
offset += line.len() + 1;
if count == 16 {
break;
}
} else {
break;
}
}
offset.min(bytes.len())
}
pub(super) fn line_of(bytes: &[u8], offset: usize) -> usize {
bytes[..offset.min(bytes.len())]
.iter()
.filter(|&&b| b == b'\n')
.count()
}
fn blake3_hex(text: &str) -> String {
blake3::hash(text.as_bytes()).to_hex().to_string()
}
fn node_type_to_str(node_type: &NodeType) -> &'static str {
match node_type {
NodeType::Function => "function",
NodeType::Class => "class",
NodeType::Method => "method",
NodeType::Variable => "variable",
NodeType::Module => "module",
NodeType::External => "external",
NodeType::FileSummary => "file_summary",
}
}
pub(crate) fn extract_file_fragments(
pdg: &ProgramDependenceGraph,
path: &Path,
file_bytes: &[u8],
max_bytes: usize,
orphan_enabled: bool,
naive_fallback: bool,
) -> Vec<FragmentCandidate> {
let Ok(code) = std::str::from_utf8(file_bytes) else {
return Vec::new();
};
if code.is_empty() {
return Vec::new();
}
let connectivity_config = TraversalConfig {
max_depth: Some(1),
max_nodes: Some(1000),
allowed_edge_types: Some(&[EdgeType::Call, EdgeType::DataDependency]),
excluded_node_types: Some(vec![NodeType::External]),
min_complexity: None,
min_edge_confidence: 0.0,
};
struct NodeInfo {
id: String,
name: String,
node_type: String,
language: String,
callers: usize,
callees: usize,
complexity: u32,
byte_range: (usize, usize),
}
let file_path_str = path.display().to_string();
let mut file_summary_id: Option<String> = None;
let mut nodes: Vec<NodeInfo> = Vec::new();
let mut node_ranges: Vec<(usize, usize)> = Vec::new();
for node_idx in pdg.node_indices() {
let Some(node) = pdg.get_node(node_idx) else {
continue;
};
if node.file_path.as_ref() != file_path_str {
continue;
}
if matches!(node.node_type, NodeType::FileSummary) {
file_summary_id = Some(node.id.clone());
continue;
}
if matches!(node.node_type, NodeType::External) {
continue;
}
if node.byte_range.1 <= node.byte_range.0 {
continue;
}
let callers = pdg.backward_impact(node_idx, &connectivity_config).len();
let callees = pdg.forward_impact(node_idx, &connectivity_config).len();
nodes.push(NodeInfo {
id: node.id.clone(),
name: node.name.clone(),
node_type: node_type_to_str(&node.node_type).to_string(),
language: node.language.clone(),
callers,
callees,
complexity: node.complexity,
byte_range: node.byte_range,
});
node_ranges.push(node.byte_range);
}
let mut candidates = Vec::new();
for info in &nodes {
let (start, end) = info.byte_range;
if start >= end || end > code.len() {
continue;
}
let node_code = &code[start..end];
let mut fragments = chunk_code(node_code, path, max_bytes, naive_fallback);
for frag in &mut fragments {
frag.start_byte_index += start;
frag.end_byte_index += start;
let header = owner_header(
&info.node_type,
&info.language,
info.callers,
info.callees,
info.complexity as usize,
);
let enriched = enrich_fragment(frag, file_bytes, &header, &info.name);
let content_hash = blake3_hex(&enriched);
candidates.push(FragmentCandidate {
content_hash: content_hash.clone(),
enriched_text: enriched,
meta: FragmentMetadata {
content_hash,
owner: Some(info.id.clone()),
file_path: file_path_str.clone(),
byte_range: (frag.start_byte_index, frag.end_byte_index),
line_range: (
line_of(file_bytes, frag.start_byte_index),
line_of(file_bytes, frag.end_byte_index.saturating_sub(1)),
),
embedding_offset: 0,
},
});
}
}
if orphan_enabled {
let file_doc_end = leading_file_doc_end(file_bytes);
for frag in orphan_fragments(OrphanInput {
file_bytes,
path,
node_ranges: &node_ranges,
file_doc_end,
max_bytes,
}) {
let language = nodes
.first()
.map(|n| n.language.clone())
.unwrap_or_else(|| "unknown".to_string());
let header = orphan_header(&language, path);
let enriched = enrich_orphan(&frag, &header);
let content_hash = blake3_hex(&enriched);
candidates.push(FragmentCandidate {
content_hash: content_hash.clone(),
enriched_text: enriched,
meta: FragmentMetadata {
content_hash,
owner: file_summary_id.clone(),
file_path: file_path_str.clone(),
byte_range: (frag.start_byte_index, frag.end_byte_index),
line_range: (
line_of(file_bytes, frag.start_byte_index),
line_of(file_bytes, frag.end_byte_index.saturating_sub(1)),
),
embedding_offset: 0,
},
});
}
}
candidates
}