use crate::engine::RepositoryState;
#[cfg(feature = "search")]
use crate::operations::arg_bool;
use crate::operations::occurrence::{InspectSubject, inspect_subject, repo_relative_file};
use crate::operations::{arg_str, arg_u64, optional_u64};
use blazingly_json::{Value, json};
use std::fs;
pub fn read_source(state: &RepositoryState, args: &Value) -> Result<Value, String> {
let (relative, anchor) = if let Ok(label) = arg_str(args, "label") {
let index = state.resolve_node(label)?;
let node = state.node(index)?;
let span = node
.span
.as_ref()
.ok_or_else(|| format!("node has no source span: {}", node.id))?;
(
span.file.clone(),
Some((u64::from(span.start.line), u64::from(span.end.line))),
)
} else {
(arg_str(args, "path")?.to_owned(), None)
};
let root = state.root();
let path = repo_relative_file(root, &relative)?;
let text = fs::read_to_string(&path)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
let requested_start = arg_u64(args, "start_line").ok();
let start = requested_start
.or_else(|| anchor.map(|(start, _)| start))
.unwrap_or(1);
let before = arg_u64(args, "before").unwrap_or(3);
let after = arg_u64(args, "after").unwrap_or(40);
let first = start.saturating_sub(before).max(1);
let contextual_last = start.saturating_add(after);
let last = if requested_start.is_none() {
anchor.map_or(contextual_last, |(_, end)| contextual_last.max(end))
} else {
contextual_last
};
let lines = text
.lines()
.enumerate()
.filter_map(|(offset, line)| {
let number = u64::try_from(offset).ok()?.saturating_add(1);
(number >= first && number <= last).then(|| json!({"line": number, "text": line}))
})
.collect::<Vec<_>>();
let budget = super::token_budget::requested(args)?;
let mut report = json!({
"path": relative.replace('\\', "/"),
"start_line": first,
"end_line": lines.last().and_then(|line| line["line"].as_u64()).unwrap_or(0),
"lines": lines
});
super::token_budget::fit(&mut report, budget, &["/lines"]);
if budget.is_some() {
let end = report["lines"]
.as_array()
.and_then(|lines| lines.last())
.and_then(|line| line["line"].as_u64())
.unwrap_or(0);
if let Some(object) = report.as_object_mut() {
object.insert("end_line".to_owned(), json!(end));
}
}
Ok(report)
}
#[cfg(feature = "search")]
pub fn search(state: &RepositoryState, args: &Value) -> Result<Value, String> {
use weavatrix_search::{SearchOptions, SearchQuery, Searcher, recommended_scan_options};
let query = if arg_bool(args, "is_regex").unwrap_or(false) {
SearchQuery::regex(arg_str(args, "query")?)
} else {
SearchQuery::literal(arg_str(args, "query")?)
};
let max = usize::try_from(arg_u64(args, "max_results").unwrap_or(40)).unwrap_or(40);
let options = SearchOptions::default()
.with_context(
usize::try_from(arg_u64(args, "before").unwrap_or(0)).unwrap_or(0),
usize::try_from(arg_u64(args, "after").unwrap_or(0)).unwrap_or(0),
)
.with_max_results(max);
let mut searcher = Searcher::new(state.root(), query).options(options.clone());
if let Ok(glob) = arg_str(args, "glob") {
let roots = [state.root().to_path_buf()];
let mut scan = recommended_scan_options(&roots, &options);
scan.override_rules.push(glob.to_owned());
searcher = searcher.scan_options(scan);
}
let budget = super::token_budget::requested(args)?;
let report = searcher.search().map_err(|error| error.to_string())?;
let total_matching_lines = report.matching_lines;
let total_occurrences = report.occurrences;
let total_files_with_matches = report.files_with_matches;
let retained_matches = report.matches.len();
let returned_matches = retained_matches.min(max);
let truncated = report.truncated || retained_matches > max;
let mut rendered = json!({
"backend": format!("{:?}", report.backend).to_ascii_lowercase(),
"matches": report.matches.into_iter().take(max).map(|item| json!({
"path": item.path,
"line": item.line_number,
"end_line": item.end_line_number,
"text": item.line,
"encoding": item.encoding,
"spans": item.spans.into_iter().map(|span| json!({
"pattern": span.pattern_index, "start": span.start, "end": span.end
})).collect::<Vec<_>>()
})).collect::<Vec<_>>(),
"totals": {
"matching_lines": total_matching_lines,
"occurrences": total_occurrences,
"files_with_matches": total_files_with_matches,
"returned_matches": returned_matches
},
"matching_lines": total_matching_lines,
"occurrences": total_occurrences,
"files_with_matches": total_files_with_matches,
"returned_matches": returned_matches,
"files_searched": report.files_searched,
"bytes_searched": report.bytes_searched,
"truncated": truncated,
"warnings": report.warnings.into_iter().map(|warning| json!({
"path": warning.path,
"kind": format!("{:?}", warning.kind).to_ascii_lowercase(),
"message": warning.message
})).collect::<Vec<_>>()
});
super::token_budget::fit(&mut rendered, budget, &["/matches"]);
if budget.is_some() {
let kept = rendered["matches"].as_array().map_or(0, Vec::len);
if kept < returned_matches {
for pointer in ["/returned_matches", "/totals/returned_matches"] {
if let Some(value) = rendered.pointer_mut(pointer) {
*value = json!(kept);
}
}
if let Some(value) = rendered.pointer_mut("/truncated") {
*value = json!(true);
}
}
}
Ok(rendered)
}
#[cfg(not(feature = "search"))]
pub fn search(_state: &RepositoryState, _args: &Value) -> Result<Value, String> {
Err("search capability is not compiled".to_owned())
}
pub fn inspect(state: &RepositoryState, args: &Value) -> Result<Value, String> {
let index = match inspect_subject(state, args)? {
InspectSubject::Unresolved(report) => return Ok(report),
InspectSubject::Node(index) => index,
};
let node = state.node(index)?;
let max_references = optional_u64(args, "max_references")?;
if max_references.is_some_and(|value| value == 0 || value > 500) {
return Err("max_references must be between 1 and 500".to_owned());
}
let neighbor_args = max_references.map_or_else(
|| json!({"label": node.id.as_str()}),
|max| json!({"label": node.id.as_str(), "max_results": max}),
);
let relationships = super::graph::neighbors(state, &neighbor_args)?;
let source = node
.span
.as_ref()
.map(|_| {
read_source(
state,
&json!({
"label": node.id.as_str(),
"before": arg_u64(args, "context_lines").unwrap_or(8),
"after": arg_u64(args, "context_lines").unwrap_or(8)
}),
)
})
.transpose()?;
Ok(json!({
"state": "RESOLVED",
"node": node,
"relationships": relationships,
"source": source,
"precision": "parser_plus_graph",
"semantic_precision": "BOUNDED_STATIC"
}))
}
pub fn context(state: &RepositoryState, args: &Value) -> Result<Value, String> {
let inspection = inspect(state, args)?;
if inspection.get("state").and_then(Value::as_str) == Some("UNRESOLVED") {
return Ok(json!({
"inspection": inspection,
"related_source": [],
"bounded": true
}));
}
let related = usize::try_from(arg_u64(args, "max_related").unwrap_or(10)).unwrap_or(10);
let id = inspection
.pointer("/node/id")
.and_then(Value::as_str)
.ok_or_else(|| "resolved inspection is missing a node id".to_owned())?;
let index = state.resolve_node(id)?;
let mut sources = Vec::new();
for edge in state
.graph()
.incoming_at(index)
.chain(state.graph().outgoing_at(index))
.take(related)
{
let id = if edge.source.as_str() == state.node(index)?.id.as_str() {
&edge.target
} else {
&edge.source
};
let Some(node) = state.graph().node(id.as_str()) else {
continue;
};
if node.span.is_some()
&& let Ok(source) = read_source(
state,
&json!({"label": node.id.as_str(), "before": 2, "after": 4}),
)
{
sources.push(source);
}
}
let budget = super::token_budget::requested(args)?;
let mut report = json!({
"inspection": inspection,
"related_source": sources,
"bounded": true
});
super::token_budget::fit(
&mut report,
budget,
&["/inspection/relationships/neighbors", "/related_source"],
);
if let Some(budget) = budget {
let estimated = super::token_budget::estimate(&report);
if estimated > budget {
return Err(format!(
"token_budget {budget} is below the target symbol itself (~{estimated} tokens \
with every related item dropped); raise the budget or use read_source"
));
}
repage_relationships(&mut report);
}
Ok(report)
}
fn repage_relationships(report: &mut Value) {
let Some(returned) = report
.pointer("/inspection/relationships/neighbors")
.and_then(Value::as_array)
.map(Vec::len)
else {
return;
};
let Some(page) = report
.pointer_mut("/inspection/relationships/page")
.and_then(Value::as_object_mut)
else {
return;
};
let total = page.get("total").and_then(Value::as_u64).unwrap_or(0);
let offset = page.get("offset").and_then(Value::as_u64).unwrap_or(0);
let end = offset.saturating_add(returned as u64);
page.insert("returned".to_owned(), json!(returned));
page.insert("has_more".to_owned(), json!(end < total));
page.insert(
"next_cursor".to_owned(),
if end < total {
json!(format!("v1:{end}"))
} else {
Value::Null
},
);
}