use std::fs;
use std::path::Path;
use crate::freshness::{record, RecordState};
use crate::graph::graph_json_path;
use crate::manifest::Value as JVal;
use crate::repo;
use super::jval_i64;
pub(super) fn offer_permitted(root: &Path, file_path: &Path, rel: &str, head: &str) -> bool {
let RecordState::Published(record) = record::read_record(root) else {
return false;
};
let Some(root_owner) = record::owner_of(root) else {
return false;
};
if root_owner != record.owner {
return false;
}
let Some(asking) = repo::find_repo_root(file_path).and_then(|r| record::owner_of(&r)) else {
return false;
};
if asking != record.owner {
return false;
}
if record.indexed_head.as_deref() != Some(head) {
return false;
}
if record::ArtifactIdentity::of(&graph_json_path(root)) != record.artifacts.graph {
return false;
}
let Some(table) = record::read_digests(root) else {
return false;
};
if table.generation != record.generation {
return false;
}
let Some(expected) = table.files.get(rel) else {
return false;
};
let Ok(bytes) = fs::read(file_path) else {
return false;
};
record::sha256_hex(&bytes) == *expected
}
pub(super) fn first_read_offer(root: &Path, rel: &str, input: &JVal) -> Option<String> {
let g = crate::graph::read_graph(root)?;
let defs: Vec<&crate::graph::Def> = g.defs.iter().filter(|d| d.file == rel).collect();
if defs.is_empty() {
return None;
}
let orig_file = input.get("tool_response").and_then(|r| r.get("file"));
let req_start = orig_file
.and_then(|f| f.get("startLine"))
.and_then(jval_i64)
.or_else(|| {
input
.get("tool_input")
.and_then(|t| t.get("offset"))
.and_then(jval_i64)
})
.map(|v| v.max(1) as usize);
let num_lines = orig_file.and_then(|f| f.get("numLines")).and_then(jval_i64);
let (rs, re) = match (req_start, num_lines) {
(Some(s), Some(n)) => (s, s.saturating_sub(1).saturating_add(n.max(0) as usize)),
(Some(s), None) => (s, usize::MAX),
(None, _) => (1, usize::MAX),
};
nearest_def(&defs, rs, re).map(|d| offer_text(&g, d))
}
fn nearest_def<'a>(
defs: &[&'a crate::graph::Def],
req_start: usize,
req_end: usize,
) -> Option<&'a crate::graph::Def> {
defs.iter().copied().min_by(|a, b| {
let key = |d: &crate::graph::Def| {
let ds = d.line.max(1);
let de = if d.end_line >= ds { d.end_line } else { ds };
let gap = if de < req_start {
req_start - de
} else {
ds.saturating_sub(req_end)
};
(gap, ds)
};
key(a).cmp(&key(b)).then_with(|| a.id.cmp(&b.id))
})
}
fn offer_text(g: &crate::graph::Graph, chosen: &crate::graph::Def) -> String {
let ambiguous = g
.defs
.iter()
.any(|d| d.id != chosen.id && d.name == chosen.name);
let subject = if ambiguous {
chosen.id.as_str()
} else {
chosen.name.as_str()
};
format!(
"[devscout: this file is indexed — 'devscout read {subject}' shows its declaration span and inbound references.]"
)
}