use crate::session::SessionState;
use m1nd_core::error::{M1ndError, M1ndResult};
use m1nd_core::graph::Graph;
use m1nd_core::types::{NodeId, NodeType};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};
const LEDGER_FILE_NAME: &str = "xray.ledger.jsonl";
const LEDGER_CHANGES_CAP: usize = 1000;
fn ledger_path_for(state: &SessionState) -> Option<PathBuf> {
state
.graph_path
.parent()
.map(|dir| dir.join(LEDGER_FILE_NAME))
}
fn ledger_line_count(ledger_path: &Path) -> u64 {
let Ok(file) = std::fs::File::open(ledger_path) else {
return 0;
};
io::BufReader::new(file)
.lines()
.map_while(Result::ok)
.count() as u64
}
fn append_ledger(ledger_path: &Path, record: &serde_json::Value) -> io::Result<u64> {
let seq = ledger_line_count(ledger_path) + 1;
let mut line = serde_json::to_string(record).map_err(io::Error::other)?;
line.push('\n');
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(ledger_path)?;
file.write_all(line.as_bytes())?;
Ok(seq)
}
fn now_epoch_secs() -> Option<u64> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}
fn build_ledger_record(
seq: u64,
verb: &str,
version: &str,
summary: serde_json::Value,
mut changes: Vec<serde_json::Value>,
) -> serde_json::Value {
let total = changes.len();
let truncated = total > LEDGER_CHANGES_CAP;
if truncated {
changes.truncate(LEDGER_CHANGES_CAP);
}
let mut record = serde_json::json!({
"seq": seq,
"verb": verb,
"mode": "commit",
"version": version,
"summary": summary,
"changes": changes,
});
if truncated {
record["changes_truncated"] = serde_json::json!(total);
}
if let Some(ts) = now_epoch_secs() {
record["ts"] = serde_json::json!(ts);
}
record
}
fn record_ledger(
state: &SessionState,
verb: &str,
version: &str,
summary: serde_json::Value,
changes: Vec<serde_json::Value>,
) {
let Some(path) = ledger_path_for(state) else {
return; };
let seq = ledger_line_count(&path) + 1;
let record = build_ledger_record(seq, verb, version, summary, changes);
let _ = append_ledger(&path, &record);
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XraySelector {
#[serde(default)]
pub filter_tags: Vec<String>,
#[serde(default)]
pub node_type: Option<u8>,
#[serde(default)]
pub path_prefix: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum XrayTagOp {
Add,
Remove,
Set,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum XrayMode {
#[default]
DryRun,
Commit,
}
#[derive(Debug, Clone, Deserialize)]
pub struct XrayRetagInput {
pub selector: XraySelector,
pub op: XrayTagOp,
pub tags: Vec<String>,
#[serde(default)]
pub mode: XrayMode,
#[serde(default)]
pub expect_version: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct XrayCounts {
pub selected: u32,
pub planned: u32,
pub skipped_noop: u32,
pub conflicts: u32,
pub applied: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayPlannedSample {
pub id: String,
pub before: Vec<String>,
pub after: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayRetagOutput {
pub verb: &'static str,
pub status: String,
pub counts: XrayCounts,
pub planned_sample: Vec<XrayPlannedSample>,
pub conflicts_sample: Vec<String>,
pub version: String,
}
const SAMPLE_CAP: usize = 5;
fn node_type_to_u8(nt: NodeType) -> u8 {
match nt {
NodeType::File => 0,
NodeType::Directory => 1,
NodeType::Function => 2,
NodeType::Class => 3,
NodeType::Struct => 4,
NodeType::Enum => 5,
NodeType::Type => 6,
NodeType::Module => 7,
NodeType::Reference => 8,
NodeType::Concept => 9,
NodeType::Material => 10,
NodeType::Process => 11,
NodeType::Product => 12,
NodeType::Supplier => 13,
NodeType::Regulatory => 14,
NodeType::System => 15,
NodeType::Cost => 16,
NodeType::Custom(v) => 100u8.saturating_add(v),
}
}
fn is_symbol_node_type(nt: NodeType) -> bool {
matches!(
nt,
NodeType::Function | NodeType::Class | NodeType::Struct | NodeType::Enum | NodeType::Type
)
}
fn node_to_ext_map(graph: &Graph) -> Vec<String> {
let n = graph.num_nodes() as usize;
let mut map = vec![String::new(); n];
for (&interned, &nid) in &graph.id_to_node {
let idx = nid.as_usize();
if idx < n {
map[idx] = graph.strings.resolve(interned).to_string();
}
}
for (i, entry) in map.iter_mut().enumerate().take(n) {
if entry.is_empty() {
*entry = graph.strings.resolve(graph.nodes.label[i]).to_string();
}
}
map
}
fn selection_version(graph: &Graph, ext: &[String], selected: &[usize]) -> String {
let mut fold: u64 = 0;
for &idx in selected {
let mut tags: Vec<&str> = graph.node_tags(NodeId::new(idx as u32));
tags.sort_unstable();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
ext[idx].hash(&mut hasher);
0u8.hash(&mut hasher); tags.join(",").hash(&mut hasher);
fold ^= hasher.finish();
}
format!("{fold:016x}")
}
fn plan_after(op: XrayTagOp, current: &[&str], tags: &[String]) -> Option<Vec<String>> {
let cur: Vec<String> = current.iter().map(|s| s.to_string()).collect();
let after: Vec<String> = match op {
XrayTagOp::Add => {
let mut next = cur.clone();
for t in tags {
if !next.iter().any(|c| c == t) {
next.push(t.clone());
}
}
next
}
XrayTagOp::Remove => cur
.iter()
.filter(|c| !tags.iter().any(|t| t == *c))
.cloned()
.collect(),
XrayTagOp::Set => tags.to_vec(),
};
if after == cur {
None
} else {
Some(after)
}
}
fn select_nodes(graph: &Graph, selector: &XraySelector, ext: &[String]) -> Vec<usize> {
let n = graph.num_nodes() as usize;
(0..n)
.filter(|&i| {
if let Some(prefix) = &selector.path_prefix {
if !ext[i].starts_with(prefix.as_str()) {
return false;
}
}
if let Some(want) = selector.node_type {
if node_type_to_u8(graph.nodes.node_type[i]) != want {
return false;
}
}
if !selector.filter_tags.is_empty() {
let node_tags = graph.node_tags(NodeId::new(i as u32));
if !selector
.filter_tags
.iter()
.any(|want| node_tags.contains(&want.as_str()))
{
return false;
}
}
true
})
.collect()
}
pub fn retag_graph(graph: &mut Graph, input: &XrayRetagInput) -> XrayRetagOutput {
retag_graph_inner(graph, input, None)
}
fn retag_graph_inner(
graph: &mut Graph,
input: &XrayRetagInput,
mut ledger: Option<&mut Vec<serde_json::Value>>,
) -> XrayRetagOutput {
let ext = node_to_ext_map(graph);
let selected = select_nodes(graph, &input.selector, &ext);
let version = selection_version(graph, &ext, &selected);
let commit = input.mode == XrayMode::Commit;
if commit {
if let Some(expected) = &input.expect_version {
if expected != &version {
let conflicts_sample = selected
.iter()
.take(SAMPLE_CAP)
.map(|&idx| ext[idx].clone())
.collect();
return XrayRetagOutput {
verb: "xray_retag",
status: "aborted_conflicts".to_string(),
counts: XrayCounts {
selected: selected.len() as u32,
conflicts: selected.len() as u32,
..Default::default()
},
planned_sample: Vec::new(),
conflicts_sample,
version,
};
}
}
}
let mut counts = XrayCounts {
selected: selected.len() as u32,
..Default::default()
};
let mut planned_sample: Vec<XrayPlannedSample> = Vec::new();
let tag_refs: Vec<&str> = input.tags.iter().map(String::as_str).collect();
for &idx in &selected {
let nid = NodeId::new(idx as u32);
let before = graph.node_tags(nid);
match plan_after(input.op, &before, &input.tags) {
Some(after) => {
counts.planned += 1;
let before_owned: Vec<String> = before.iter().map(|s| s.to_string()).collect();
if planned_sample.len() < SAMPLE_CAP {
planned_sample.push(XrayPlannedSample {
id: ext[idx].clone(),
before: before_owned.clone(),
after: after.clone(),
});
}
if commit {
match input.op {
XrayTagOp::Add => {
graph.add_node_tags(nid, &tag_refs);
}
XrayTagOp::Remove => {
graph.remove_node_tags(nid, &tag_refs);
}
XrayTagOp::Set => {
graph.set_node_tags(nid, &tag_refs);
}
}
counts.applied += 1;
if let Some(changes) = ledger.as_deref_mut() {
changes.push(serde_json::json!({
"node": ext[idx],
"before": before_owned,
"after": after,
}));
}
}
}
None => counts.skipped_noop += 1,
}
}
XrayRetagOutput {
verb: "xray_retag",
status: if commit { "committed" } else { "dry_run" }.to_string(),
counts,
planned_sample,
conflicts_sample: Vec::new(),
version,
}
}
pub fn handle_xray_retag(
state: &mut SessionState,
input: XrayRetagInput,
) -> M1ndResult<serde_json::Value> {
let mut changes: Vec<serde_json::Value> = Vec::new();
let output = {
let mut graph = state.graph.write();
let ledger = if input.mode == XrayMode::Commit {
Some(&mut changes)
} else {
None
};
retag_graph_inner(&mut graph, &input, ledger)
};
if input.mode == XrayMode::Commit && output.counts.applied > 0 {
state.bump_graph_generation();
state.invalidate_all_perspectives();
state.mark_all_lock_baselines_stale();
state.persist()?;
record_ledger(
state,
"xray_retag",
&output.version,
serde_json::json!({
"selected": output.counts.selected,
"planned": output.counts.planned,
"skipped_noop": output.counts.skipped_noop,
"applied": output.counts.applied,
}),
changes,
);
}
serde_json::to_value(output).map_err(m1nd_core::error::M1ndError::Serde)
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayFileSelector {
#[serde(default)]
pub path_prefix: Option<String>,
#[serde(default)]
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AnnotatePosition {
#[default]
Above,
}
fn default_annotate_position() -> AnnotatePosition {
AnnotatePosition::Above
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum XrayTransform {
EnsureHeaderTag { tag: String },
AnnotateSymbol {
annotation: String,
#[serde(default)]
node_type: Option<u8>,
#[serde(default = "default_annotate_position")]
position: AnnotatePosition,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct XrayApplyInput {
pub selector: XrayFileSelector,
pub transform: XrayTransform,
#[serde(default)]
pub mode: XrayMode,
#[serde(default)]
pub expect_version: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct XrayApplyCounts {
pub matched: u32,
pub planned: u32,
pub skipped_noop: u32,
pub skipped_binary: u32,
pub applied: u32,
pub conflicts: u32,
pub symbols_matched: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayApplyPlannedSample {
pub path: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayApplyOutput {
pub verb: &'static str,
pub status: String,
pub counts: XrayApplyCounts,
pub planned_sample: Vec<XrayApplyPlannedSample>,
pub conflicts_sample: Vec<String>,
pub version: String,
pub graph_resync_required: bool,
}
fn content_hash(bytes: &[u8]) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
fn plan_version(entries: &[(PathBuf, String)]) -> String {
let mut fold: u64 = 0;
for (path, guard) in entries {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.to_string_lossy().hash(&mut hasher);
0u8.hash(&mut hasher); guard.hash(&mut hasher);
fold ^= hasher.finish();
}
format!("{fold:016x}")
}
fn apply_transform(content: &str, transform: &XrayTransform) -> Option<String> {
match transform {
XrayTransform::AnnotateSymbol { .. } => None,
XrayTransform::EnsureHeaderTag { tag } => {
let lines: Vec<&str> = content.split_inclusive('\n').collect();
let head: String = lines.iter().take(3).copied().collect();
if head.contains(tag.as_str()) {
return None;
}
let insert_at = if lines.first().is_some_and(|l| l.starts_with("//")) {
1
} else {
0
};
let mut out = String::with_capacity(content.len() + tag.len() + 1);
for line in lines.iter().take(insert_at) {
out.push_str(line);
}
out.push_str(tag);
out.push('\n');
for line in lines.iter().skip(insert_at) {
out.push_str(line);
}
Some(out)
}
}
}
pub fn apply_files(
paths: &[PathBuf],
transform: &XrayTransform,
mode: XrayMode,
expect_version: Option<&str>,
) -> XrayApplyOutput {
apply_files_inner(paths, transform, mode, expect_version, None, None, None)
}
pub fn apply_files_with_ledger(
paths: &[PathBuf],
transform: &XrayTransform,
mode: XrayMode,
expect_version: Option<&str>,
ledger: &mut Vec<serde_json::Value>,
) -> XrayApplyOutput {
apply_files_inner(
paths,
transform,
mode,
expect_version,
None,
None,
Some(ledger),
)
}
type TamperHook<'a> = Option<&'a dyn Fn(&[PathBuf])>;
type BeforeSwapHook<'a> = Option<&'a dyn Fn(&[(PathBuf, PathBuf)])>;
fn apply_files_inner(
paths: &[PathBuf],
transform: &XrayTransform,
mode: XrayMode,
expect_version: Option<&str>,
tamper: TamperHook<'_>,
before_swap: BeforeSwapHook<'_>,
mut ledger: Option<&mut Vec<serde_json::Value>>,
) -> XrayApplyOutput {
let mut counts = XrayApplyCounts {
matched: paths.len() as u32,
..Default::default()
};
let mut plan: Vec<(PathBuf, String, Vec<u8>)> = Vec::new();
for p in paths {
let Ok(bytes) = std::fs::read(p) else {
continue;
};
let guard = content_hash(&bytes);
let Ok(current) = std::str::from_utf8(&bytes) else {
counts.skipped_binary += 1;
continue;
};
match apply_transform(current, transform) {
None => {
counts.skipped_noop += 1;
}
Some(new_content) => {
counts.planned += 1;
plan.push((p.clone(), guard, new_content.into_bytes()));
}
}
}
run_atomic_apply(
plan,
counts,
mode,
expect_version,
tamper,
before_swap,
ledger,
)
}
#[allow(clippy::too_many_arguments)]
fn run_atomic_apply(
plan: Vec<(PathBuf, String, Vec<u8>)>,
mut counts: XrayApplyCounts,
mode: XrayMode,
expect_version: Option<&str>,
tamper: TamperHook<'_>,
before_swap: BeforeSwapHook<'_>,
mut ledger: Option<&mut Vec<serde_json::Value>>,
) -> XrayApplyOutput {
let version = plan_version(
&plan
.iter()
.map(|(p, g, _)| (p.clone(), g.clone()))
.collect::<Vec<_>>(),
);
if mode != XrayMode::Commit {
let planned_sample = plan
.iter()
.take(SAMPLE_CAP)
.map(|(p, _, _)| XrayApplyPlannedSample {
path: p.to_string_lossy().into_owned(),
})
.collect();
return XrayApplyOutput {
verb: "xray_apply",
status: "dry_run".to_string(),
counts,
planned_sample,
conflicts_sample: Vec::new(),
version,
graph_resync_required: false,
};
}
if let Some(expected) = expect_version {
if expected != version {
let conflicts_sample = plan
.iter()
.take(SAMPLE_CAP)
.map(|(p, _, _)| file_label(p))
.collect();
counts.conflicts = plan.len() as u32;
counts.applied = 0;
return XrayApplyOutput {
verb: "xray_apply",
status: "aborted_conflicts".to_string(),
counts,
planned_sample: Vec::new(),
conflicts_sample,
version,
graph_resync_required: false,
};
}
}
let mut temps: Vec<(PathBuf, PathBuf)> = Vec::new(); let cleanup = |temps: &[(PathBuf, PathBuf)]| {
for (_orig, tmp) in temps {
let _ = std::fs::remove_file(tmp);
}
};
let stage_abort = |temps: &[(PathBuf, PathBuf)], mut counts: XrayApplyCounts, label: String| {
cleanup(temps);
counts.conflicts = 1;
counts.applied = 0;
XrayApplyOutput {
verb: "xray_apply",
status: "aborted_conflicts".to_string(),
counts,
planned_sample: Vec::new(),
conflicts_sample: vec![label],
version: version.clone(),
graph_resync_required: false,
}
};
for (p, _guard, new_bytes) in &plan {
let tmp = tmp_path_for(p);
if let Ok(meta) = std::fs::symlink_metadata(&tmp) {
if meta.file_type().is_symlink() {
return stage_abort(
&temps,
counts,
format!("{}: refusing pre-existing symlink temp", file_label(p)),
);
}
}
let staged = (|| -> std::io::Result<()> {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)?;
f.write_all(new_bytes)?;
f.flush()?;
f.sync_all()?; Ok(())
})();
match staged {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
return stage_abort(
&temps,
counts,
format!("{}: pre-existing temp blocks staging", file_label(p)),
);
}
Err(_) => {
let _ = std::fs::remove_file(&tmp);
return stage_abort(&temps, counts, file_label(p));
}
Ok(()) => {}
}
temps.push((p.clone(), tmp));
}
if let Some(tamper) = tamper {
let originals: Vec<PathBuf> = plan.iter().map(|(p, _, _)| p.clone()).collect();
tamper(&originals);
}
let mut conflicts: Vec<String> = Vec::new();
for (p, guard, _) in &plan {
let drifted = match std::fs::read(p) {
Ok(bytes) => content_hash(&bytes) != *guard,
Err(_) => true,
};
if drifted {
conflicts.push(file_label(p));
}
}
if !conflicts.is_empty() {
cleanup(&temps);
counts.conflicts = conflicts.len() as u32;
counts.applied = 0;
let conflicts_sample = conflicts.into_iter().take(SAMPLE_CAP).collect();
return XrayApplyOutput {
verb: "xray_apply",
status: "aborted_conflicts".to_string(),
counts,
planned_sample: Vec::new(),
conflicts_sample,
version,
graph_resync_required: false,
};
}
if let Some(before_swap) = before_swap {
before_swap(&temps);
}
let mut swapped: usize = 0;
for (i, (orig, tmp)) in temps.iter().enumerate() {
if let Err(e) = std::fs::rename(tmp, orig) {
if swapped == 0 {
cleanup(&temps);
counts.conflicts = 1;
counts.applied = 0;
return XrayApplyOutput {
verb: "xray_apply",
status: "aborted_conflicts".to_string(),
counts,
planned_sample: Vec::new(),
conflicts_sample: vec![file_label(orig)],
version,
graph_resync_required: false,
};
}
let already = &temps[..swapped];
fsync_parent_dirs(already);
counts.applied = swapped as u32;
counts.conflicts = 1;
if let Some(changes) = ledger.as_deref_mut() {
collect_apply_changes(changes, &plan[..swapped]);
}
return XrayApplyOutput {
verb: "xray_apply",
status: "partial".to_string(),
counts,
planned_sample: Vec::new(),
conflicts_sample: vec![format!("{}: rename failed: {e}", file_label(orig))],
version,
graph_resync_required: true,
};
}
swapped = i + 1;
}
fsync_parent_dirs(&temps);
counts.applied = counts.planned;
if let Some(changes) = ledger {
collect_apply_changes(changes, &plan);
}
let planned_sample = plan
.iter()
.take(SAMPLE_CAP)
.map(|(p, _, _)| XrayApplyPlannedSample {
path: p.to_string_lossy().into_owned(),
})
.collect();
let applied = counts.applied;
XrayApplyOutput {
verb: "xray_apply",
status: "committed".to_string(),
counts,
planned_sample,
conflicts_sample: Vec::new(),
version,
graph_resync_required: applied > 0,
}
}
type SymbolTarget = (PathBuf, u32);
fn collect_symbol_targets(
graph: &Graph,
root: &Path,
node_type: Option<u8>,
path_prefix: Option<&str>,
extensions: &[String],
) -> (Vec<SymbolTarget>, u32) {
let ext = node_to_ext_map(graph);
let n = graph.num_nodes() as usize;
let mut targets: Vec<SymbolTarget> = Vec::new();
let mut symbols_matched: u32 = 0;
for (i, external_id) in ext.iter().enumerate().take(n) {
let nt = graph.nodes.node_type[i];
match node_type {
Some(want) => {
if node_type_to_u8(nt) != want {
continue;
}
}
None => {
if !is_symbol_node_type(nt) {
continue;
}
}
}
if let Some(prefix) = path_prefix {
match module_of(external_id) {
Some(module) if module.starts_with(prefix) => {}
_ => continue,
}
}
let prov = graph.resolve_node_provenance(NodeId::new(i as u32));
let (Some(source), Some(line_start)) = (prov.source_path, prov.line_start) else {
continue;
};
if source.is_empty() || line_start < 1 {
continue;
}
if !extension_allowed(Path::new(source.as_str()), extensions) {
continue;
}
let abs = root.join(&source);
if is_forbidden_path(&abs) {
continue;
}
if !path_confined_to_root(&abs, root) {
continue;
}
symbols_matched += 1;
targets.push((abs, line_start));
}
(targets, symbols_matched)
}
fn path_confined_to_root(abs: &Path, root: &Path) -> bool {
let mut depth: i64 = 0;
for comp in abs
.strip_prefix(root)
.into_iter()
.flat_map(|r| r.components())
{
match comp {
std::path::Component::ParentDir => depth -= 1,
std::path::Component::CurDir => {}
_ => depth += 1,
}
if depth < 0 {
return false;
}
}
if !abs.starts_with(root) {
return false;
}
if let Ok(canon) = abs.canonicalize() {
return canon.starts_with(root);
}
let mut cur = abs.parent();
while let Some(p) = cur {
if let Ok(canon) = p.canonicalize() {
return canon.starts_with(root);
}
cur = p.parent();
}
true
}
fn build_annotate_plan(
targets: Vec<SymbolTarget>,
annotation: &str,
symbols_matched: u32,
) -> (Vec<(PathBuf, String, Vec<u8>)>, XrayApplyCounts) {
let mut by_file: BTreeMap<PathBuf, Vec<u32>> = BTreeMap::new();
for (path, line_start) in targets {
by_file.entry(path).or_default().push(line_start);
}
let mut counts = XrayApplyCounts {
matched: by_file.len() as u32,
symbols_matched,
..Default::default()
};
let mut plan: Vec<(PathBuf, String, Vec<u8>)> = Vec::new();
let annotation_trimmed = annotation.trim();
for (path, mut line_starts) in by_file {
let Ok(bytes) = std::fs::read(&path) else {
continue;
};
let guard = content_hash(&bytes);
let Ok(current) = std::str::from_utf8(&bytes) else {
counts.skipped_binary += 1;
continue;
};
let mut lines: Vec<String> = current.split_inclusive('\n').map(str::to_owned).collect();
line_starts.sort_unstable();
line_starts.dedup();
line_starts.reverse();
let mut changed = false;
for ls in line_starts {
let idx = (ls - 1) as usize;
if idx > lines.len() {
continue; }
if idx > 0 {
let prev = lines[idx - 1].trim_end_matches(['\n', '\r']).trim();
if prev == annotation_trimmed {
continue;
}
}
lines.insert(idx, format!("{annotation}\n"));
changed = true;
}
if changed {
counts.planned += 1;
plan.push((path, guard, lines.concat().into_bytes()));
} else {
counts.skipped_noop += 1;
}
}
(plan, counts)
}
fn collect_apply_changes(
changes: &mut Vec<serde_json::Value>,
swapped: &[(PathBuf, String, Vec<u8>)],
) {
for (path, before_hash, new_bytes) in swapped {
changes.push(serde_json::json!({
"path": path.to_string_lossy(),
"before_hash": before_hash,
"after_hash": content_hash(new_bytes),
}));
}
}
fn fsync_parent_dirs(swapped: &[(PathBuf, PathBuf)]) {
let mut seen: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
for (orig, _tmp) in swapped {
if let Some(parent) = orig.parent() {
if !seen.insert(parent.to_path_buf()) {
continue;
}
if let Ok(dir) = std::fs::File::open(parent) {
let _ = dir.sync_all();
}
}
}
}
fn tmp_path_for(p: &Path) -> PathBuf {
let mut s = p.as_os_str().to_os_string();
s.push(".xray.tmp");
PathBuf::from(s)
}
fn file_label(p: &Path) -> String {
p.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| p.to_string_lossy().into_owned())
}
fn is_forbidden_path(p: &Path) -> bool {
let name = p
.file_name()
.map(|n| n.to_string_lossy().to_lowercase())
.unwrap_or_default();
if name == "graph_snapshot.json"
|| name == "daemon_alerts.json"
|| name == "document_cache_index.json"
|| name == "ingest_roots.json"
|| name.ends_with("_state.json")
|| name.ends_with(".xray.tmp")
|| name == LEDGER_FILE_NAME
{
return true;
}
let path_str = p.to_string_lossy().to_lowercase();
let path_str = path_str.replace('\\', "/"); if path_str.contains("/.git/")
|| path_str.contains("/target/")
|| path_str.contains("/node_modules/")
{
return true;
}
false
}
fn collect_files(dir: &Path, root: &Path, selector: &XrayFileSelector, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(ft) = entry.file_type() else { continue };
if ft.is_dir() {
if is_forbidden_path(&path) {
continue;
}
let dir_name = path.file_name().map(|n| n.to_string_lossy().into_owned());
if matches!(
dir_name.as_deref(),
Some(".git" | "target" | "node_modules")
) {
continue;
}
collect_files(&path, root, selector, out);
} else if ft.is_file() && is_included(&path, root, selector) {
out.push(path);
}
}
}
fn extension_allowed(path: &Path, wanted: &[String]) -> bool {
if wanted.is_empty() {
return true;
}
path.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.is_some_and(|e| wanted.iter().any(|w| w.to_lowercase() == e))
}
fn is_included(path: &Path, root: &Path, selector: &XrayFileSelector) -> bool {
if is_forbidden_path(path) {
return false;
}
let Ok(canon) = path.canonicalize() else {
return false;
};
if !canon.starts_with(root) {
return false;
}
if !extension_allowed(path, &selector.extensions) {
return false;
}
if let Some(prefix) = selector.path_prefix.as_deref() {
let Ok(rel) = path.strip_prefix(root) else {
return false;
};
let rel_str = rel.to_string_lossy().replace('\\', "/");
let prefix_norm = prefix.trim_start_matches("./").replace('\\', "/");
if !rel_str.starts_with(&prefix_norm) {
return false;
}
}
true
}
pub fn handle_xray_apply(
state: &mut SessionState,
input: XrayApplyInput,
) -> M1ndResult<serde_json::Value> {
let root: PathBuf = state
.workspace_root
.as_deref()
.map(PathBuf::from)
.filter(|p| p.is_dir())
.ok_or_else(|| M1ndError::InvalidParams {
tool: "xray_apply".to_string(),
detail: "project root (workspace_root) could not be resolved; refusing to write"
.to_string(),
})?;
let root = root.canonicalize().map_err(|e| M1ndError::InvalidParams {
tool: "xray_apply".to_string(),
detail: format!("could not canonicalize project root; refusing to write: {e}"),
})?;
let mut changes: Vec<serde_json::Value> = Vec::new();
let commit = input.mode == XrayMode::Commit;
let output = match &input.transform {
XrayTransform::AnnotateSymbol {
annotation,
node_type,
position: _, } => {
let (targets, symbols_matched) = {
let graph = state.graph.read();
collect_symbol_targets(
&graph,
&root,
*node_type,
input.selector.path_prefix.as_deref(),
&input.selector.extensions,
)
};
let (plan, counts) = build_annotate_plan(targets, annotation, symbols_matched);
let ledger = commit.then_some(&mut changes);
run_atomic_apply(
plan,
counts,
input.mode,
input.expect_version.as_deref(),
None,
None,
ledger,
)
}
XrayTransform::EnsureHeaderTag { .. } => {
let walk_start = match input.selector.path_prefix.as_deref() {
Some(prefix) => {
let joined = root.join(prefix.trim_start_matches("./"));
if joined.is_dir() {
joined
} else {
root.clone()
}
}
None => root.clone(),
};
let mut matched: Vec<PathBuf> = Vec::new();
collect_files(&walk_start, &root, &input.selector, &mut matched);
matched.sort();
if commit {
apply_files_with_ledger(
&matched,
&input.transform,
input.mode,
input.expect_version.as_deref(),
&mut changes,
)
} else {
apply_files(
&matched,
&input.transform,
input.mode,
input.expect_version.as_deref(),
)
}
}
};
let wrote =
(output.status == "committed" || output.status == "partial") && output.counts.applied > 0;
if wrote {
for change in &mut changes {
if let Some(abs) = change.get("path").and_then(|p| p.as_str()) {
let rel = Path::new(abs)
.strip_prefix(&root)
.map(|r| r.to_string_lossy().replace('\\', "/"))
.unwrap_or_else(|_| abs.to_string());
change["path"] = serde_json::json!(rel);
}
}
record_ledger(
state,
"xray_apply",
&output.version,
serde_json::json!({
"matched": output.counts.matched,
"planned": output.counts.planned,
"skipped_noop": output.counts.skipped_noop,
"skipped_binary": output.counts.skipped_binary,
"applied": output.counts.applied,
"conflicts": output.counts.conflicts,
"symbols_matched": output.counts.symbols_matched,
}),
changes,
);
}
serde_json::to_value(output).map_err(M1ndError::Serde)
}
const EROSION_CAP: usize = 25;
const FILE_PREFIX: &str = "file::";
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayManifest {
#[serde(default)]
pub forbid: Vec<(String, String)>,
#[serde(default)]
pub layer_order: Vec<String>,
#[serde(default)]
pub require_exists: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayManifestFile {
#[serde(default)]
pub ratified: bool,
#[serde(flatten)]
pub manifest: XrayManifest,
}
fn load_manifest_file(path: &Path) -> Option<(XrayManifest, bool)> {
let bytes = std::fs::read(path).ok()?;
let parsed: XrayManifestFile = serde_json::from_slice(&bytes).ok()?;
Some((parsed.manifest, parsed.ratified))
}
struct ResolvedManifest {
manifest: XrayManifest,
source: String,
ratified: bool,
}
fn resolve_manifest(
inline: &XrayManifest,
manifest_path: Option<&str>,
workspace_root: Option<&str>,
) -> ResolvedManifest {
let inline_nonempty = !inline.forbid.is_empty()
|| !inline.layer_order.is_empty()
|| !inline.require_exists.is_empty();
if inline_nonempty {
return ResolvedManifest {
manifest: inline.clone(),
source: "inline".to_string(),
ratified: false,
};
}
if let Some(p) = manifest_path {
if let Some((m, ratified)) = load_manifest_file(Path::new(p)) {
return ResolvedManifest {
manifest: m,
source: format!("file:{p}"),
ratified,
};
}
} else if let Some(root) = workspace_root {
let candidate = Path::new(root).join("xray.manifest.json");
if let Some((m, ratified)) = load_manifest_file(&candidate) {
let disp = candidate.to_string_lossy().into_owned();
return ResolvedManifest {
manifest: m,
source: format!("file:{disp}"),
ratified,
};
}
}
ResolvedManifest {
manifest: XrayManifest::default(),
source: "none".to_string(),
ratified: false,
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayOrientInput {
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub manifest: XrayManifest,
#[serde(default)]
pub manifest_path: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayErosionCandidate {
pub from_module: String,
pub to_module: String,
pub rule: &'static str,
pub via: String,
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayExistence {
pub require: String,
pub state: &'static str,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct XrayOrientCounts {
pub modules: u32,
pub boundary_edges: u32,
pub erosion_candidates: u32,
pub blueprint: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayOrientOutput {
pub verb: &'static str,
pub scope: Option<String>,
pub modules: BTreeMap<String, u32>,
pub dependency_matrix: BTreeMap<String, u32>,
pub erosion_candidates: Vec<XrayErosionCandidate>,
pub existence: Vec<XrayExistence>,
pub counts: XrayOrientCounts,
pub manifest_source: String,
}
fn module_of(external_id: &str) -> Option<&str> {
let rest = external_id.strip_prefix(FILE_PREFIX)?;
let path = rest.split("::").next().unwrap_or(rest);
let seg = path.split('/').next().unwrap_or(path);
if seg.is_empty() {
None
} else {
Some(seg)
}
}
fn strip_file_prefix(id: &str) -> String {
id.strip_prefix(FILE_PREFIX).unwrap_or(id).to_string()
}
fn classify_edge(manifest: &XrayManifest, a: &str, b: &str) -> Option<&'static str> {
if manifest.forbid.iter().any(|(fa, fb)| fa == a && fb == b) {
return Some("forbid");
}
let layer_index =
|m: &str| -> Option<usize> { manifest.layer_order.iter().position(|x| x == m) };
if let (Some(ia), Some(ib)) = (layer_index(a), layer_index(b)) {
if ib > ia {
return Some("layer");
}
}
None
}
pub fn orient_graph(
graph: &Graph,
input: &XrayOrientInput,
manifest: &XrayManifest,
manifest_source: String,
) -> XrayOrientOutput {
let ext = node_to_ext_map(graph);
let n = graph.num_nodes() as usize;
let scope = input.scope.as_deref();
let in_scope = |idx: usize| -> bool {
scope.is_none_or(|p| ext.get(idx).is_some_and(|e| e.starts_with(p)))
};
let mut modules: BTreeMap<String, u32> = BTreeMap::new();
for (i, id) in ext.iter().enumerate().take(n) {
if !in_scope(i) {
continue;
}
if let Some(m) = module_of(id) {
*modules.entry(m.to_string()).or_insert(0) += 1;
}
}
let mut dependency_matrix: BTreeMap<String, u32> = BTreeMap::new();
let mut erosion_candidates: Vec<XrayErosionCandidate> = Vec::new();
let mut boundary_edges: u32 = 0;
let mut erosion_total: u32 = 0;
for i in 0..n {
if !in_scope(i) {
continue;
}
let src_mod = match module_of(&ext[i]) {
Some(m) => m,
None => continue,
};
for e in graph.csr.out_range(NodeId::new(i as u32)) {
let rel = graph.strings.resolve(graph.csr.relations[e]);
if rel != "imports" && rel != "depends_on" {
continue;
}
let dst = graph.csr.targets[e].as_usize();
let dst_id = match ext.get(dst) {
Some(d) => d.as_str(),
None => continue,
};
let dst_mod = match module_of(dst_id) {
Some(m) => m,
None => continue,
};
if src_mod == dst_mod {
continue; }
boundary_edges += 1;
*dependency_matrix
.entry(format!("{src_mod}->{dst_mod}"))
.or_insert(0) += 1;
if let Some(rule) = classify_edge(manifest, src_mod, dst_mod) {
erosion_total += 1;
if erosion_candidates.len() < EROSION_CAP {
erosion_candidates.push(XrayErosionCandidate {
from_module: src_mod.to_string(),
to_module: dst_mod.to_string(),
rule,
via: rel.to_string(),
from: strip_file_prefix(&ext[i]),
to: strip_file_prefix(dst_id),
});
}
}
}
}
let haystack: Vec<&str> = ext
.iter()
.enumerate()
.take(n)
.filter(|&(i, _)| in_scope(i))
.map(|(_, id)| id.as_str())
.collect();
let mut existence: Vec<XrayExistence> = Vec::new();
let mut blueprint: u32 = 0;
for need in &manifest.require_exists {
let present = haystack.iter().any(|id| id.contains(need.as_str()));
if !present {
blueprint += 1;
}
existence.push(XrayExistence {
require: need.clone(),
state: if present { "BEDROCK" } else { "BLUEPRINT" },
});
}
XrayOrientOutput {
verb: "xray_orient",
scope: input.scope.clone(),
counts: XrayOrientCounts {
modules: modules.len() as u32,
boundary_edges,
erosion_candidates: erosion_total,
blueprint,
},
modules,
dependency_matrix,
erosion_candidates,
existence,
manifest_source,
}
}
pub fn handle_xray_orient(
state: &mut SessionState,
input: XrayOrientInput,
) -> M1ndResult<serde_json::Value> {
let resolved = resolve_manifest(
&input.manifest,
input.manifest_path.as_deref(),
state.workspace_root.as_deref(),
);
let output = {
let graph = state.graph.read();
orient_graph(&graph, &input, &resolved.manifest, resolved.source)
};
serde_json::to_value(output).map_err(m1nd_core::error::M1ndError::Serde)
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayGateInput {
pub node: String,
#[serde(default)]
pub planned_imports: Vec<String>,
#[serde(default)]
pub manifest: XrayManifest,
#[serde(default)]
pub manifest_ratified: bool,
#[serde(default)]
pub manifest_path: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayGateViolation {
pub from_module: String,
pub to_module: String,
pub rule: &'static str,
pub kind: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayGateOutput {
pub verb: &'static str,
pub node: String,
pub node_module: String,
pub verdict: String,
pub existing_violations: Vec<XrayGateViolation>,
pub planned_violations: Vec<XrayGateViolation>,
pub reasons: Vec<String>,
pub manifest_source: String,
}
pub fn gate_graph(
graph: &Graph,
input: &XrayGateInput,
manifest: &XrayManifest,
ratified: bool,
manifest_source: String,
) -> XrayGateOutput {
let Some(nid) = graph.resolve_id(&input.node) else {
return XrayGateOutput {
verb: "xray_gate",
node: input.node.clone(),
node_module: String::new(),
verdict: "clear".to_string(),
existing_violations: Vec::new(),
planned_violations: Vec::new(),
reasons: vec!["node not in graph".to_string()],
manifest_source,
};
};
let ext = node_to_ext_map(graph);
let idx = nid.as_usize();
let node_module = ext
.get(idx)
.and_then(|id| module_of(id))
.map(|m| m.to_string())
.unwrap_or_default();
let manifest_empty = manifest.forbid.is_empty() && manifest.layer_order.is_empty();
if node_module.is_empty() || manifest_empty {
let reason = if node_module.is_empty() {
"node has no derivable module"
} else {
"manifest declares no rules"
};
return XrayGateOutput {
verb: "xray_gate",
node: input.node.clone(),
node_module,
verdict: "clear".to_string(),
existing_violations: Vec::new(),
planned_violations: Vec::new(),
reasons: vec![reason.to_string()],
manifest_source,
};
}
let mut existing_violations: Vec<XrayGateViolation> = Vec::new();
let mut reasons: Vec<String> = Vec::new();
for e in graph.csr.out_range(nid) {
let rel = graph.strings.resolve(graph.csr.relations[e]);
if rel != "imports" && rel != "depends_on" {
continue;
}
let dst = graph.csr.targets[e].as_usize();
let Some(dst_id) = ext.get(dst) else { continue };
let Some(dst_mod) = module_of(dst_id) else {
continue;
};
if dst_mod == node_module {
continue; }
if let Some(rule) = classify_edge(manifest, &node_module, dst_mod) {
reasons.push(format!(
"existing {rule}: {node_module} -> {dst_mod} (via {rel})"
));
existing_violations.push(XrayGateViolation {
from_module: node_module.clone(),
to_module: dst_mod.to_string(),
rule,
kind: "existing",
});
}
}
let mut planned_violations: Vec<XrayGateViolation> = Vec::new();
for m in &input.planned_imports {
if m == &node_module {
continue; }
if let Some(rule) = classify_edge(manifest, &node_module, m) {
reasons.push(format!("planned {rule}: {node_module} -> {m}"));
planned_violations.push(XrayGateViolation {
from_module: node_module.clone(),
to_module: m.clone(),
rule,
kind: "planned",
});
}
}
let any = !existing_violations.is_empty() || !planned_violations.is_empty();
let verdict = if any {
if ratified {
"blocked"
} else {
"caution"
}
} else {
"clear"
};
if !any {
reasons.push("no North-Star violation".to_string());
} else if !ratified {
reasons.push("manifest not ratified — caution, not blocked".to_string());
}
XrayGateOutput {
verb: "xray_gate",
node: input.node.clone(),
node_module,
verdict: verdict.to_string(),
existing_violations,
planned_violations,
reasons,
manifest_source,
}
}
pub fn handle_xray_gate(
state: &mut SessionState,
input: XrayGateInput,
) -> M1ndResult<serde_json::Value> {
let resolved = resolve_manifest(
&input.manifest,
input.manifest_path.as_deref(),
state.workspace_root.as_deref(),
);
let effective_ratified = if resolved.source == "inline" {
input.manifest_ratified
} else {
resolved.ratified
};
let output = {
let graph = state.graph.read();
gate_graph(
&graph,
&input,
&resolved.manifest,
effective_ratified,
resolved.source,
)
};
serde_json::to_value(output).map_err(m1nd_core::error::M1ndError::Serde)
}
const REFERENCE_RELATIONS: &[&str] = &["imports", "calls", "references", "depends_on"];
const STATE_TAG_PREFIX: &str = "xray:state:";
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayPaintInput {
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub manifest: XrayManifest,
#[serde(default)]
pub manifest_path: Option<String>,
#[serde(default)]
pub mode: XrayMode,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct XrayPaintCounts {
pub scanned: u32,
pub bedrock: u32,
pub overgrowth: u32,
pub unproven: u32,
pub erosion_candidate: u32,
pub painted: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct XrayPaintOutput {
pub verb: &'static str,
pub status: String,
pub counts: XrayPaintCounts,
pub version: String,
pub proof_coverage: f64,
pub manifest_source: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PaintState {
Bedrock,
Overgrowth,
Unproven,
ErosionCandidate,
}
impl PaintState {
fn tag(self) -> &'static str {
match self {
PaintState::Bedrock => "xray:state:bedrock",
PaintState::Overgrowth => "xray:state:overgrowth",
PaintState::Unproven => "xray:state:unproven",
PaintState::ErosionCandidate => "xray:state:erosion-candidate",
}
}
}
fn is_test_source(external_id: &str) -> bool {
let rest = external_id.strip_prefix(FILE_PREFIX).unwrap_or(external_id);
let path = rest.split("::").next().unwrap_or(rest);
path.contains("/tests/")
|| path.contains("/test_")
|| path.ends_with("_test.rs")
|| path.ends_with("/tests.rs")
|| path == "tests.rs"
}
fn exercised_set(graph: &Graph, ext: &[String]) -> Vec<bool> {
let n = graph.num_nodes() as usize;
let mut exercised = vec![false; n];
for i in 0..n {
let src_is_test = ext.get(i).is_some_and(|e| is_test_source(e));
for e in graph.csr.out_range(NodeId::new(i as u32)) {
let rel = graph.strings.resolve(graph.csr.relations[e]);
let dst = graph.csr.targets[e].as_usize();
if dst >= n {
continue;
}
if rel == "grounded_in" {
exercised[dst] = true;
continue;
}
if src_is_test && (rel == "imports" || rel == "calls" || rel == "references") {
exercised[dst] = true;
}
}
}
exercised
}
fn reference_indegree(graph: &Graph) -> Vec<u32> {
let n = graph.num_nodes() as usize;
let mut indeg = vec![0u32; n];
for i in 0..n {
for e in graph.csr.out_range(NodeId::new(i as u32)) {
let rel = graph.strings.resolve(graph.csr.relations[e]);
if !REFERENCE_RELATIONS.contains(&rel) {
continue;
}
let dst = graph.csr.targets[e].as_usize();
if dst < n {
indeg[dst] += 1;
}
}
}
indeg
}
fn erosion_source_set(graph: &Graph, ext: &[String], manifest: &XrayManifest) -> Vec<bool> {
let n = graph.num_nodes() as usize;
let mut flagged = vec![false; n];
for i in 0..n {
let src_mod = match module_of(&ext[i]) {
Some(m) => m,
None => continue,
};
for e in graph.csr.out_range(NodeId::new(i as u32)) {
let rel = graph.strings.resolve(graph.csr.relations[e]);
if rel != "imports" && rel != "depends_on" {
continue;
}
let dst = graph.csr.targets[e].as_usize();
let dst_mod = match ext.get(dst).and_then(|d| module_of(d)) {
Some(m) => m,
None => continue,
};
if src_mod == dst_mod {
continue;
}
if classify_edge(manifest, src_mod, dst_mod).is_some() {
flagged[i] = true;
break; }
}
}
flagged
}
fn classify_node(indegree: u32, is_exercised: bool, is_erosion_source: bool) -> PaintState {
if is_erosion_source {
PaintState::ErosionCandidate
} else if is_exercised {
PaintState::Bedrock
} else if indegree == 0 {
PaintState::Overgrowth
} else {
PaintState::Unproven
}
}
pub fn paint_graph(
graph: &mut Graph,
input: &XrayPaintInput,
manifest: &XrayManifest,
manifest_source: String,
) -> XrayPaintOutput {
paint_graph_inner(graph, input, manifest, manifest_source, None)
}
fn paint_graph_inner(
graph: &mut Graph,
input: &XrayPaintInput,
manifest: &XrayManifest,
manifest_source: String,
mut ledger: Option<&mut Vec<serde_json::Value>>,
) -> XrayPaintOutput {
let ext = node_to_ext_map(graph);
let n = graph.num_nodes() as usize;
let scope = input.scope.as_deref();
let in_scope = |idx: usize| -> bool {
scope.is_none_or(|p| ext.get(idx).is_some_and(|e| e.starts_with(p)))
};
let indeg = reference_indegree(graph);
let exercised = exercised_set(graph, &ext);
let erosion = erosion_source_set(graph, &ext, manifest);
let selected: Vec<usize> = (0..n).filter(|&i| in_scope(i)).collect();
let version = selection_version(graph, &ext, &selected);
let commit = input.mode == XrayMode::Commit;
let mut counts = XrayPaintCounts {
scanned: selected.len() as u32,
..Default::default()
};
for &i in &selected {
let state = classify_node(indeg[i], exercised[i], erosion[i]);
match state {
PaintState::Bedrock => counts.bedrock += 1,
PaintState::Overgrowth => counts.overgrowth += 1,
PaintState::Unproven => counts.unproven += 1,
PaintState::ErosionCandidate => counts.erosion_candidate += 1,
}
let nid = NodeId::new(i as u32);
let new_tag = state.tag();
let existing_state_tags: Vec<String> = graph
.node_tags(nid)
.iter()
.filter(|t| t.starts_with(STATE_TAG_PREFIX))
.map(|s| s.to_string())
.collect();
let already_correct = existing_state_tags.len() == 1 && existing_state_tags[0] == new_tag;
if already_correct {
continue;
}
if commit {
let before_owned: Option<Vec<String>> = ledger
.as_deref()
.map(|_| graph.node_tags(nid).iter().map(|s| s.to_string()).collect());
if !existing_state_tags.is_empty() {
let to_remove: Vec<&str> = existing_state_tags.iter().map(String::as_str).collect();
graph.remove_node_tags(nid, &to_remove);
}
graph.add_node_tags(nid, &[new_tag]);
counts.painted += 1;
if let (Some(changes), Some(before_owned)) = (ledger.as_deref_mut(), before_owned) {
let after_owned: Vec<String> =
graph.node_tags(nid).iter().map(|s| s.to_string()).collect();
changes.push(serde_json::json!({
"node": ext[i],
"before": before_owned,
"after": after_owned,
}));
}
}
}
let proof_coverage = if counts.scanned == 0 {
0.0
} else {
let raw = counts.bedrock as f64 / counts.scanned as f64;
(raw * 1000.0).round() / 1000.0
};
XrayPaintOutput {
verb: "xray_paint",
status: if commit { "committed" } else { "dry_run" }.to_string(),
counts,
version,
proof_coverage,
manifest_source,
}
}
pub fn handle_xray_paint(
state: &mut SessionState,
input: XrayPaintInput,
) -> M1ndResult<serde_json::Value> {
let resolved = resolve_manifest(
&input.manifest,
input.manifest_path.as_deref(),
state.workspace_root.as_deref(),
);
let mut changes: Vec<serde_json::Value> = Vec::new();
let output = {
let mut graph = state.graph.write();
let ledger = if input.mode == XrayMode::Commit {
Some(&mut changes)
} else {
None
};
paint_graph_inner(
&mut graph,
&input,
&resolved.manifest,
resolved.source,
ledger,
)
};
if input.mode == XrayMode::Commit && output.counts.painted > 0 {
state.bump_graph_generation();
state.invalidate_all_perspectives();
state.mark_all_lock_baselines_stale();
state.persist()?;
record_ledger(
state,
"xray_paint",
&output.version,
serde_json::json!({
"scanned": output.counts.scanned,
"bedrock": output.counts.bedrock,
"overgrowth": output.counts.overgrowth,
"unproven": output.counts.unproven,
"erosion_candidate": output.counts.erosion_candidate,
"painted": output.counts.painted,
}),
changes,
);
}
serde_json::to_value(output).map_err(m1nd_core::error::M1ndError::Serde)
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct XrayLedgerInput {
#[serde(default)]
pub limit: Option<usize>,
#[serde(default)]
pub verb: Option<String>,
}
const LEDGER_DEFAULT_LIMIT: usize = 20;
#[derive(Debug, Clone, Serialize)]
pub struct XrayLedgerOutput {
pub verb: &'static str,
pub entries: Vec<serde_json::Value>,
pub total_entries: usize,
pub ledger_path: Option<String>,
}
fn read_ledger(
ledger_path: &Path,
limit: usize,
verb_filter: Option<&str>,
) -> (Vec<serde_json::Value>, usize) {
let Ok(file) = std::fs::File::open(ledger_path) else {
return (Vec::new(), 0);
};
let mut total = 0usize;
let mut recent: std::collections::VecDeque<serde_json::Value> =
std::collections::VecDeque::with_capacity(limit.min(1024));
for line in io::BufReader::new(file).lines().map_while(Result::ok) {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) else {
continue;
};
total += 1;
let matches = match verb_filter {
Some(want) => value.get("verb").and_then(|x| x.as_str()) == Some(want),
None => true,
};
if !matches || limit == 0 {
continue;
}
recent.push_back(value);
if recent.len() > limit {
recent.pop_front();
}
}
let entries: Vec<serde_json::Value> = recent.into_iter().rev().collect();
(entries, total)
}
pub fn handle_xray_ledger(
state: &mut SessionState,
input: XrayLedgerInput,
) -> M1ndResult<serde_json::Value> {
let limit = input.limit.unwrap_or(LEDGER_DEFAULT_LIMIT);
let path = ledger_path_for(state);
let (entries, total_entries) = match &path {
Some(p) => read_ledger(p, limit, input.verb.as_deref()),
None => (Vec::new(), 0),
};
let output = XrayLedgerOutput {
verb: "xray_ledger",
entries,
total_entries,
ledger_path: path.map(|p| p.to_string_lossy().into_owned()),
};
serde_json::to_value(output).map_err(m1nd_core::error::M1ndError::Serde)
}
#[cfg(test)]
mod tests {
use super::*;
use m1nd_core::graph::{Graph, NodeProvenanceInput};
use m1nd_core::types::NodeType;
fn sample_graph() -> Graph {
let mut g = Graph::new();
g.add_node(
"file::a.rs::fn::foo",
"foo",
NodeType::Function,
&["rust", "rust:visibility:private"],
0.0,
0.0,
)
.unwrap();
g.add_node(
"file::b.rs::fn::bar",
"bar",
NodeType::Function,
&["rust", "rust:visibility:pub"],
0.0,
0.0,
)
.unwrap();
g.add_node(
"file::a.rs::struct::Cfg",
"Cfg",
NodeType::Struct,
&["rust"],
0.0,
0.0,
)
.unwrap();
g.finalize().unwrap();
g
}
fn input(
selector: XraySelector,
op: XrayTagOp,
tags: &[&str],
mode: XrayMode,
) -> XrayRetagInput {
XrayRetagInput {
selector,
op,
tags: tags.iter().map(|s| s.to_string()).collect(),
mode,
expect_version: None,
}
}
fn input_expect(
selector: XraySelector,
op: XrayTagOp,
tags: &[&str],
mode: XrayMode,
expect_version: Option<String>,
) -> XrayRetagInput {
XrayRetagInput {
expect_version,
..input(selector, op, tags, mode)
}
}
#[test]
fn dry_run_selects_and_plans_but_mutates_nothing() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let out = retag_graph(
&mut g,
&input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::DryRun),
);
assert_eq!(out.status, "dry_run");
assert_eq!(out.counts.selected, 3);
assert_eq!(out.counts.planned, 3);
assert_eq!(out.counts.applied, 0);
assert!(!out.planned_sample.is_empty());
let n = g.resolve_id("file::a.rs::fn::foo").unwrap();
assert!(!g.node_tags(n).contains(&"xray:bedrock"));
}
#[test]
fn commit_applies_and_node_tags_reflect_it() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let out = retag_graph(
&mut g,
&input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::Commit),
);
assert_eq!(out.status, "committed");
assert_eq!(out.counts.planned, 3);
assert_eq!(out.counts.applied, 3);
for ext in [
"file::a.rs::fn::foo",
"file::b.rs::fn::bar",
"file::a.rs::struct::Cfg",
] {
let n = g.resolve_id(ext).unwrap();
assert!(g.node_tags(n).contains(&"xray:bedrock"), "{ext} not tagged");
}
}
#[test]
fn idempotent_second_commit_plans_zero() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let first = retag_graph(
&mut g,
&input(
sel.clone(),
XrayTagOp::Add,
&["xray:bedrock"],
XrayMode::Commit,
),
);
assert_eq!(first.counts.applied, 3);
let second = retag_graph(
&mut g,
&input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::Commit),
);
assert_eq!(second.counts.selected, 3);
assert_eq!(second.counts.planned, 0);
assert_eq!(second.counts.skipped_noop, 3);
assert_eq!(second.counts.applied, 0);
}
#[test]
fn remove_op_only_counts_present_tags() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust:visibility:pub".to_string()],
..Default::default()
};
let out = retag_graph(
&mut g,
&input(
sel,
XrayTagOp::Remove,
&["rust:visibility:pub"],
XrayMode::Commit,
),
);
assert_eq!(out.counts.selected, 1);
assert_eq!(out.counts.planned, 1);
assert_eq!(out.counts.applied, 1);
let n = g.resolve_id("file::b.rs::fn::bar").unwrap();
assert!(!g.node_tags(n).contains(&"rust:visibility:pub"));
}
#[test]
fn selector_by_path_prefix_scopes_the_mutation() {
let mut g = sample_graph();
let sel = XraySelector {
path_prefix: Some("file::a.rs".to_string()),
..Default::default()
};
let out = retag_graph(
&mut g,
&input(sel, XrayTagOp::Add, &["xray:scoped"], XrayMode::Commit),
);
assert_eq!(out.counts.selected, 2);
assert_eq!(out.counts.applied, 2);
assert!(g
.node_tags(g.resolve_id("file::a.rs::fn::foo").unwrap())
.contains(&"xray:scoped"));
assert!(g
.node_tags(g.resolve_id("file::a.rs::struct::Cfg").unwrap())
.contains(&"xray:scoped"));
assert!(!g
.node_tags(g.resolve_id("file::b.rs::fn::bar").unwrap())
.contains(&"xray:scoped"));
}
#[test]
fn selector_by_node_type_filters_exactly() {
let mut g = sample_graph();
let sel = XraySelector {
node_type: Some(node_type_to_u8(NodeType::Struct)),
..Default::default()
};
let out = retag_graph(
&mut g,
&input(sel, XrayTagOp::Add, &["xray:struct"], XrayMode::Commit),
);
assert_eq!(out.counts.selected, 1);
assert_eq!(out.counts.applied, 1);
assert!(g
.node_tags(g.resolve_id("file::a.rs::struct::Cfg").unwrap())
.contains(&"xray:struct"));
}
#[test]
fn set_op_replaces_whole_tag_set() {
let mut g = sample_graph();
let sel = XraySelector {
path_prefix: Some("file::a.rs::fn::foo".to_string()),
..Default::default()
};
let out = retag_graph(
&mut g,
&input(sel, XrayTagOp::Set, &["only", "these"], XrayMode::Commit),
);
assert_eq!(out.counts.applied, 1);
let mut tags = g.node_tags(g.resolve_id("file::a.rs::fn::foo").unwrap());
tags.sort_unstable();
assert_eq!(tags, vec!["only", "these"]);
}
#[test]
fn dry_run_returns_nonempty_version() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let out = retag_graph(
&mut g,
&input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::DryRun),
);
assert_eq!(out.status, "dry_run");
assert!(!out.version.is_empty(), "dry_run must surface a version");
assert_eq!(out.version.len(), 16);
}
#[test]
fn commit_with_matching_expect_version_succeeds() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let dry = retag_graph(
&mut g,
&input(
sel.clone(),
XrayTagOp::Add,
&["xray:bedrock"],
XrayMode::DryRun,
),
);
let out = retag_graph(
&mut g,
&input_expect(
sel,
XrayTagOp::Add,
&["xray:bedrock"],
XrayMode::Commit,
Some(dry.version.clone()),
),
);
assert_eq!(out.status, "committed");
assert!(out.counts.applied > 0);
assert_eq!(out.counts.applied, 3);
assert_eq!(out.counts.conflicts, 0);
assert!(g
.node_tags(g.resolve_id("file::a.rs::fn::foo").unwrap())
.contains(&"xray:bedrock"));
}
#[test]
fn stale_expect_version_aborts_commit_and_mutates_nothing() {
let mut g = sample_graph();
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let dry = retag_graph(
&mut g,
&input(
sel.clone(),
XrayTagOp::Add,
&["xray:bedrock"],
XrayMode::DryRun,
),
);
let stale_version = dry.version.clone();
let victim = g.resolve_id("file::a.rs::fn::foo").unwrap();
g.add_node_tags(victim, &["concurrent:edit"]);
let before: Vec<Vec<String>> = [
"file::a.rs::fn::foo",
"file::b.rs::fn::bar",
"file::a.rs::struct::Cfg",
]
.iter()
.map(|ext| {
g.node_tags(g.resolve_id(ext).unwrap())
.iter()
.map(|s| s.to_string())
.collect()
})
.collect();
let out = retag_graph(
&mut g,
&input_expect(
sel,
XrayTagOp::Add,
&["xray:bedrock"],
XrayMode::Commit,
Some(stale_version),
),
);
assert_eq!(out.status, "aborted_conflicts");
assert_eq!(out.counts.applied, 0);
assert!(out.counts.conflicts >= 1);
assert!(!out.version.is_empty());
for (i, ext) in [
"file::a.rs::fn::foo",
"file::b.rs::fn::bar",
"file::a.rs::struct::Cfg",
]
.iter()
.enumerate()
{
let now: Vec<String> = g
.node_tags(g.resolve_id(ext).unwrap())
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(
now, before[i],
"{ext} must be untouched by the aborted call"
);
assert!(
!now.iter().any(|t| t == "xray:bedrock"),
"{ext} must not gain the planned tag on abort"
);
}
}
use m1nd_core::types::{EdgeDirection, FiniteF32};
fn orient_graph_fixture() -> Graph {
let mut g = Graph::new();
g.add_node(
"file::modA/src/lib.rs::fn::a_main",
"a_main",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::modA/src/util.rs::fn::a_util",
"a_util",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::modB/src/lib.rs::fn::b_core",
"b_core",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap();
g.add_edge(
NodeId::new(0),
NodeId::new(2),
"imports",
FiniteF32::new(1.0),
EdgeDirection::Forward,
false,
FiniteF32::new(0.0),
)
.unwrap();
g.add_edge(
NodeId::new(0),
NodeId::new(1),
"imports",
FiniteF32::new(1.0),
EdgeDirection::Forward,
false,
FiniteF32::new(0.0),
)
.unwrap();
g.finalize().unwrap();
g
}
fn orient_input(manifest: XrayManifest) -> XrayOrientInput {
XrayOrientInput {
scope: None,
manifest,
manifest_path: None,
}
}
fn orient_g(graph: &Graph, input: &XrayOrientInput) -> XrayOrientOutput {
let resolved = resolve_manifest(&input.manifest, input.manifest_path.as_deref(), None);
orient_graph(graph, input, &resolved.manifest, resolved.source)
}
#[test]
fn module_of_derives_first_path_segment() {
assert_eq!(
module_of("file::m1nd-core/src/x.rs::fn::y"),
Some("m1nd-core")
);
assert_eq!(module_of("file::modB/src/lib.rs::fn::b"), Some("modB"));
assert_eq!(module_of("concept::foo"), None);
assert_eq!(module_of("plain-label"), None);
}
#[test]
fn empty_manifest_reports_matrix_with_zero_erosion() {
let g = orient_graph_fixture();
let out = orient_g(&g, &orient_input(XrayManifest::default()));
assert_eq!(out.verb, "xray_orient");
assert_eq!(out.modules.get("modA"), Some(&2));
assert_eq!(out.modules.get("modB"), Some(&1));
assert_eq!(out.counts.modules, 2);
assert_eq!(out.dependency_matrix.get("modA->modB"), Some(&1));
assert_eq!(out.dependency_matrix.len(), 1);
assert_eq!(out.counts.boundary_edges, 1);
assert!(out.erosion_candidates.is_empty());
assert_eq!(out.counts.erosion_candidates, 0);
}
#[test]
fn forbid_rule_flags_one_erosion_candidate() {
let g = orient_graph_fixture();
let manifest = XrayManifest {
forbid: vec![("modA".to_string(), "modB".to_string())],
..Default::default()
};
let out = orient_g(&g, &orient_input(manifest));
assert_eq!(out.erosion_candidates.len(), 1);
assert_eq!(out.counts.erosion_candidates, 1);
let c = &out.erosion_candidates[0];
assert_eq!(c.from_module, "modA");
assert_eq!(c.to_module, "modB");
assert_eq!(c.rule, "forbid");
assert_eq!(c.via, "imports");
assert_eq!(c.from, "modA/src/lib.rs::fn::a_main");
assert_eq!(c.to, "modB/src/lib.rs::fn::b_core");
}
#[test]
fn layer_order_flags_dependency_on_higher_layer() {
let g = orient_graph_fixture();
let manifest = XrayManifest {
layer_order: vec!["modA".to_string(), "modB".to_string()],
..Default::default()
};
let out = orient_g(&g, &orient_input(manifest));
assert_eq!(out.counts.erosion_candidates, 1);
assert_eq!(out.erosion_candidates[0].rule, "layer");
let g2 = orient_graph_fixture();
let manifest2 = XrayManifest {
layer_order: vec!["modB".to_string(), "modA".to_string()],
..Default::default()
};
let out2 = orient_g(&g2, &orient_input(manifest2));
assert_eq!(out2.counts.erosion_candidates, 0);
assert!(out2.erosion_candidates.is_empty());
}
#[test]
fn require_exists_resolves_bedrock_vs_blueprint() {
let g = orient_graph_fixture();
let manifest = XrayManifest {
require_exists: vec!["modA".to_string(), "nope_absent".to_string()],
..Default::default()
};
let out = orient_g(&g, &orient_input(manifest));
assert_eq!(out.existence.len(), 2);
let bedrock = out.existence.iter().find(|e| e.require == "modA").unwrap();
assert_eq!(bedrock.state, "BEDROCK");
let blueprint = out
.existence
.iter()
.find(|e| e.require == "nope_absent")
.unwrap();
assert_eq!(blueprint.state, "BLUEPRINT");
assert_eq!(out.counts.blueprint, 1);
}
#[test]
fn scope_narrows_census_and_matrix() {
let g = orient_graph_fixture();
let input = XrayOrientInput {
scope: Some("file::modA".to_string()),
manifest: XrayManifest::default(),
manifest_path: None,
};
let out = orient_g(&g, &input);
assert_eq!(out.modules.get("modA"), Some(&2));
assert_eq!(out.modules.get("modB"), None);
assert_eq!(out.counts.modules, 1);
assert_eq!(out.dependency_matrix.get("modA->modB"), Some(&1));
}
use std::sync::atomic::{AtomicU64, Ordering};
const TEST_TAG: &str = "//! @xray:state:bedrock";
fn apply_files_with_tamper(
paths: &[PathBuf],
transform: &XrayTransform,
mode: XrayMode,
tamper: impl Fn(&[PathBuf]),
) -> XrayApplyOutput {
apply_files_inner(paths, transform, mode, None, Some(&tamper), None, None)
}
fn apply_files_with_before_swap(
paths: &[PathBuf],
transform: &XrayTransform,
mode: XrayMode,
before_swap: impl Fn(&[(PathBuf, PathBuf)]),
) -> XrayApplyOutput {
apply_files_inner(paths, transform, mode, None, None, Some(&before_swap), None)
}
fn ensure_tag() -> XrayTransform {
XrayTransform::EnsureHeaderTag {
tag: TEST_TAG.to_string(),
}
}
fn fresh_sandbox() -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let dir =
std::env::temp_dir().join(format!("xray_apply_test_{}_{}", std::process::id(), n));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn seed_untagged(dir: &Path, count: usize) -> Vec<PathBuf> {
let mut out = Vec::new();
for i in 0..count {
let p = dir.join(format!("file_{i}.rs"));
std::fs::write(&p, format!("// file {i}\nfn main() {{}}\n")).unwrap();
out.push(p);
}
out
}
fn first3_contains_tag(p: &Path) -> bool {
let content = std::fs::read_to_string(p).unwrap();
content
.split_inclusive('\n')
.take(3)
.collect::<String>()
.contains(TEST_TAG)
}
#[test]
fn dry_run_plans_but_writes_nothing() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let out = apply_files(&paths, &ensure_tag(), XrayMode::DryRun, None);
assert_eq!(out.verb, "xray_apply");
assert_eq!(out.status, "dry_run");
assert_eq!(out.counts.matched, 4);
assert_eq!(out.counts.planned, 4);
assert_eq!(out.counts.applied, 0);
assert!(!out.planned_sample.is_empty());
for p in &paths {
assert!(!first3_contains_tag(p), "dry_run must not write {p:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn commit_applies_tag_to_all_files() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let out = apply_files(&paths, &ensure_tag(), XrayMode::Commit, None);
assert_eq!(out.status, "committed");
assert_eq!(out.counts.applied, 4);
assert_eq!(out.counts.planned, 4);
assert_eq!(out.counts.conflicts, 0);
for p in &paths {
assert!(first3_contains_tag(p), "commit must tag {p:?}");
}
assert!(no_temps_remain(&dir));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn idempotent_recommit_plans_zero() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let first = apply_files(&paths, &ensure_tag(), XrayMode::Commit, None);
assert_eq!(first.counts.applied, 4);
let again = apply_files(&paths, &ensure_tag(), XrayMode::Commit, None);
assert_eq!(again.status, "committed");
assert_eq!(again.counts.planned, 0);
assert_eq!(again.counts.skipped_noop, 4);
assert_eq!(again.counts.applied, 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn contended_apply_aborts_whole_batch() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let victim = paths[2].clone();
let out = apply_files_with_tamper(&paths, &ensure_tag(), XrayMode::Commit, move |_| {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&victim)
.unwrap();
f.write_all(b"\n// concurrent edit\n").unwrap();
});
assert_eq!(out.status, "aborted_conflicts");
assert_eq!(out.counts.applied, 0);
assert!(out.counts.conflicts >= 1);
for (i, p) in paths.iter().enumerate() {
if i == 2 {
continue; }
assert!(
!first3_contains_tag(p),
"abort must leave {p:?} untouched (no tag)"
);
}
assert!(no_temps_remain(&dir));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn plan_version_keys_in_path_so_identical_content_does_not_cancel() {
let same_hash = content_hash(b"identical bytes");
let a = (PathBuf::from("/repo/a.rs"), same_hash.clone());
let b = (PathBuf::from("/repo/b.rs"), same_hash.clone());
let folded = plan_version(&[a.clone(), b.clone()]);
assert_ne!(
folded, "0000000000000000",
"path keying must prevent cancel"
);
let b_changed = (PathBuf::from("/repo/b.rs"), content_hash(b"other bytes"));
let folded2 = plan_version(&[a, b_changed]);
assert_ne!(folded, folded2, "a content change must flip the version");
}
#[test]
fn partial_swap_after_first_success_reports_partial() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let mut ordered = paths.clone();
ordered.sort();
let victim_orig = ordered[1].clone();
let out = apply_files_with_before_swap(
&ordered,
&ensure_tag(),
XrayMode::Commit,
move |_temps| {
std::fs::remove_file(&victim_orig).unwrap();
std::fs::create_dir(&victim_orig).unwrap();
},
);
assert_eq!(out.status, "partial");
assert_eq!(out.counts.applied, 1, "exactly the first file was swapped");
assert!(out.counts.conflicts >= 1);
assert!(out.graph_resync_required);
assert!(
out.conflicts_sample
.iter()
.any(|c| c.contains(&file_label(&ordered[1]))),
"conflicts_sample must name the failing path: {:?}",
out.conflicts_sample
);
assert!(first3_contains_tag(&ordered[0]), "file 0 must be swapped");
let tmp0 = tmp_path_for(&ordered[0]);
assert!(!tmp0.exists(), "swapped file's temp must be gone");
for p in [&ordered[2], &ordered[3]] {
let tmp = tmp_path_for(p);
assert!(
tmp.exists(),
"not-yet-swapped temp must be retained for retry: {tmp:?}"
);
assert!(
!first3_contains_tag(p),
"not-yet-swapped original must be untouched: {p:?}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
fn no_temps_remain(dir: &Path) -> bool {
std::fs::read_dir(dir)
.unwrap()
.flatten()
.all(|e| !e.file_name().to_string_lossy().ends_with(".xray.tmp"))
}
#[test]
fn apply_dry_run_returns_version() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let out = apply_files(&paths, &ensure_tag(), XrayMode::DryRun, None);
assert_eq!(out.status, "dry_run");
assert!(!out.version.is_empty(), "dry_run must surface a version");
assert_eq!(out.version.len(), 16);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn apply_commit_with_matching_expect_version_applies() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let dry = apply_files(&paths, &ensure_tag(), XrayMode::DryRun, None);
let out = apply_files(
&paths,
&ensure_tag(),
XrayMode::Commit,
Some(dry.version.as_str()),
);
assert_eq!(out.status, "committed");
assert_eq!(out.counts.applied, 4);
assert_eq!(out.counts.conflicts, 0);
for p in &paths {
assert!(first3_contains_tag(p), "commit must tag {p:?}");
}
assert!(no_temps_remain(&dir));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn apply_stale_expect_version_aborts_before_staging() {
let dir = fresh_sandbox();
let paths = seed_untagged(&dir, 4);
let dry = apply_files(&paths, &ensure_tag(), XrayMode::DryRun, None);
let stale_version = dry.version.clone();
{
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&paths[1])
.unwrap();
f.write_all(b"\n// concurrent edit\n").unwrap();
}
let tampered_after = std::fs::read_to_string(&paths[1]).unwrap();
let out = apply_files(
&paths,
&ensure_tag(),
XrayMode::Commit,
Some(stale_version.as_str()),
);
assert_eq!(out.status, "aborted_conflicts");
assert_eq!(out.counts.applied, 0);
assert!(out.counts.conflicts >= 1);
for p in &paths {
assert!(!first3_contains_tag(p), "aborted commit must not tag {p:?}");
}
assert_eq!(
std::fs::read_to_string(&paths[1]).unwrap(),
tampered_after,
"the concurrently edited file must be left exactly as the edit left it"
);
assert!(no_temps_remain(&dir));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn stage_refuses_preexisting_symlink_temp_and_does_not_escape_root() {
use std::os::unix::fs::symlink;
let dir = fresh_sandbox();
let outside = dir
.parent()
.unwrap()
.join(format!("xray_escape_target_{}.txt", std::process::id()));
let outside_content = b"OUTSIDE-DO-NOT-TOUCH\n";
std::fs::write(&outside, outside_content).unwrap();
let victim = dir.join("victim.rs");
std::fs::write(&victim, "// victim\nfn main() {}\n").unwrap();
let control = dir.join("control.rs");
std::fs::write(&control, "// control\nfn main() {}\n").unwrap();
let mal_tmp = tmp_path_for(&victim);
symlink(&outside, &mal_tmp).unwrap();
let mut paths = vec![victim.clone(), control.clone()];
paths.sort();
let out = apply_files(&paths, &ensure_tag(), XrayMode::Commit, None);
assert_eq!(
out.status, "aborted_conflicts",
"a pre-existing symlink temp must abort the commit"
);
assert_eq!(out.counts.applied, 0, "abort must write zero originals");
assert!(out.counts.conflicts >= 1);
assert!(
out.conflicts_sample
.iter()
.any(|c| c.contains("victim.rs") && c.contains("symlink")),
"conflict must name the refused symlink temp: {:?}",
out.conflicts_sample
);
assert_eq!(
std::fs::read(&outside).unwrap(),
outside_content,
"the symlink target OUTSIDE root must be byte-for-byte intact"
);
assert!(!first3_contains_tag(&victim), "victim.rs must be untouched");
assert!(
!first3_contains_tag(&control),
"control.rs must be untouched"
);
let meta = std::fs::symlink_metadata(&mal_tmp).unwrap();
assert!(
meta.file_type().is_symlink(),
"the refused symlink temp must be left in place, not removed"
);
let _ = std::fs::remove_file(&mal_tmp);
let _ = std::fs::remove_file(&outside);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn commit_skips_binary_file_and_tags_valid_sibling() {
let dir = fresh_sandbox();
let binary = dir.join("blob.bin");
let binary_bytes: &[u8] = &[0xff, 0xfe, 0x00, 0x01];
std::fs::write(&binary, binary_bytes).unwrap();
let valid = dir.join("ok.rs");
std::fs::write(&valid, "// ok\nfn main() {}\n").unwrap();
let mut paths = vec![binary.clone(), valid.clone()];
paths.sort();
let out = apply_files(&paths, &ensure_tag(), XrayMode::Commit, None);
assert_eq!(out.status, "committed");
assert_eq!(out.counts.skipped_binary, 1, "binary file must be skipped");
assert_eq!(out.counts.planned, 1, "only the valid sibling is planned");
assert_eq!(out.counts.applied, 1, "only the valid sibling is written");
assert_eq!(
std::fs::read(&binary).unwrap(),
binary_bytes,
"binary file must be left exactly as it was"
);
assert!(
!out.planned_sample
.iter()
.any(|s| s.path.ends_with("blob.bin")),
"a skipped binary must never appear in planned_sample"
);
assert!(first3_contains_tag(&valid), "valid .rs must be tagged");
assert!(no_temps_remain(&dir));
let _ = std::fs::remove_dir_all(&dir);
}
fn annotate(
graph: &Graph,
root: &Path,
annotation: &str,
node_type: Option<u8>,
path_prefix: Option<&str>,
mode: XrayMode,
) -> XrayApplyOutput {
annotate_ext(graph, root, annotation, node_type, path_prefix, &[], mode)
}
fn annotate_ext(
graph: &Graph,
root: &Path,
annotation: &str,
node_type: Option<u8>,
path_prefix: Option<&str>,
extensions: &[String],
mode: XrayMode,
) -> XrayApplyOutput {
let (targets, symbols_matched) =
collect_symbol_targets(graph, root, node_type, path_prefix, extensions);
let (plan, counts) = build_annotate_plan(targets, annotation, symbols_matched);
run_atomic_apply(plan, counts, mode, None, None, None, None)
}
fn annotate_fixture() -> (Graph, PathBuf, PathBuf) {
let root = fresh_sandbox();
let root = root.canonicalize().unwrap();
let rel = "modA/src/x.rs";
let file = root.join(rel);
std::fs::create_dir_all(file.parent().unwrap()).unwrap();
let body = "// header\n\
use std::io;\n\
fn foo() {}\n\
\n\
// a comment\n\
struct Cfg {}\n\
\n\
pub fn bar() {}\n\
// tail\n\
// eof\n";
std::fs::write(&file, body).unwrap();
let mut g = Graph::new();
let foo = g
.add_node(
"file::modA/src/x.rs::fn::foo",
"foo",
NodeType::Function,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
foo,
NodeProvenanceInput {
source_path: Some(rel),
line_start: Some(3),
line_end: Some(3),
..Default::default()
},
);
let cfg = g
.add_node(
"file::modA/src/x.rs::struct::Cfg",
"Cfg",
NodeType::Struct,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
cfg,
NodeProvenanceInput {
source_path: Some(rel),
line_start: Some(6),
line_end: Some(6),
..Default::default()
},
);
let bar = g
.add_node(
"file::modA/src/x.rs::fn::bar",
"bar",
NodeType::Function,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
bar,
NodeProvenanceInput {
source_path: Some(rel),
line_start: Some(8),
line_end: Some(8),
..Default::default()
},
);
g.finalize().unwrap();
(g, root, file)
}
const ANNOT: &str = "// @xray:reviewed";
#[test]
fn annotate_dry_run_matches_symbols_writes_nothing() {
let (g, root, file) = annotate_fixture();
let before = std::fs::read_to_string(&file).unwrap();
let out = annotate(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
None,
XrayMode::DryRun,
);
assert_eq!(out.status, "dry_run");
assert_eq!(out.counts.symbols_matched, 2, "AST selected 2 functions");
assert_eq!(out.counts.planned, 1, "both functions live in one file");
assert_eq!(out.counts.applied, 0);
assert_eq!(std::fs::read_to_string(&file).unwrap(), before);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn annotate_commit_inserts_above_each_function_bottom_up() {
let (g, root, file) = annotate_fixture();
let out = annotate(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
None,
XrayMode::Commit,
);
assert_eq!(out.status, "committed");
assert_eq!(out.counts.symbols_matched, 2);
assert_eq!(out.counts.applied, 1);
assert!(out.graph_resync_required, "a write must flag resync");
let lines: Vec<String> = std::fs::read_to_string(&file)
.unwrap()
.lines()
.map(str::to_owned)
.collect();
let foo_i = lines.iter().position(|l| l.contains("fn foo")).unwrap();
assert_eq!(lines[foo_i - 1].trim(), ANNOT, "annotation above fn foo");
let bar_i = lines.iter().position(|l| l.contains("pub fn bar")).unwrap();
assert_eq!(
lines[bar_i - 1].trim(),
ANNOT,
"annotation above pub fn bar"
);
let cfg_i = lines.iter().position(|l| l.contains("struct Cfg")).unwrap();
assert_ne!(lines[cfg_i - 1].trim(), ANNOT, "struct must be untouched");
let n_annot = lines.iter().filter(|l| l.trim() == ANNOT).count();
assert_eq!(n_annot, 2, "exactly 2 inserts");
assert!(no_temps_remain(&root));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn annotate_is_idempotent_on_recommit() {
let (mut g, root, file) = annotate_fixture();
let first = annotate(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
None,
XrayMode::Commit,
);
assert_eq!(first.counts.applied, 1);
let after_first = std::fs::read_to_string(&file).unwrap();
g.set_node_provenance(
g.resolve_id("file::modA/src/x.rs::fn::foo").unwrap(),
NodeProvenanceInput {
source_path: Some("modA/src/x.rs"),
line_start: Some(4),
line_end: Some(4),
..Default::default()
},
);
g.set_node_provenance(
g.resolve_id("file::modA/src/x.rs::fn::bar").unwrap(),
NodeProvenanceInput {
source_path: Some("modA/src/x.rs"),
line_start: Some(10),
line_end: Some(10),
..Default::default()
},
);
let second = annotate(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
None,
XrayMode::Commit,
);
assert_eq!(second.status, "committed");
assert_eq!(second.counts.symbols_matched, 2, "AST still matched 2");
assert_eq!(second.counts.planned, 0, "idempotent: nothing to do");
assert_eq!(second.counts.applied, 0);
assert_eq!(std::fs::read_to_string(&file).unwrap(), after_first);
let n_annot = after_first.lines().filter(|l| l.trim() == ANNOT).count();
assert_eq!(n_annot, 2, "no duplicate annotation lines");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn annotate_skips_symbol_whose_source_escapes_root() {
let (mut g, root, _file) = annotate_fixture();
let escapee = g
.add_node(
"file::escape/src/evil.rs::fn::evil",
"evil",
NodeType::Function,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
escapee,
NodeProvenanceInput {
source_path: Some("../../../../etc/evil.rs"),
line_start: Some(1),
line_end: Some(1),
..Default::default()
},
);
let (targets, symbols_matched) = collect_symbol_targets(
&g,
&root,
Some(node_type_to_u8(NodeType::Function)),
None,
&[],
);
assert_eq!(symbols_matched, 2, "escaping symbol must not be matched");
assert!(
targets.iter().all(|(p, _)| p.starts_with(&root)),
"every target must be confined under root"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn annotate_path_prefix_scopes_by_module() {
let (g, root, file) = annotate_fixture();
let hit = annotate(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
Some("modA"),
XrayMode::DryRun,
);
assert_eq!(hit.counts.symbols_matched, 2);
let miss = annotate(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
Some("modZ"),
XrayMode::DryRun,
);
assert_eq!(miss.counts.symbols_matched, 0);
assert_eq!(miss.counts.planned, 0);
assert!(std::fs::read_to_string(&file).unwrap().lines().count() >= 8);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn annotate_extensions_filter_excludes_non_matching_symbols() {
let root = fresh_sandbox();
let root = root.canonicalize().unwrap();
let rs_rel = "src/a.rs";
let rs_file = root.join(rs_rel);
std::fs::create_dir_all(rs_file.parent().unwrap()).unwrap();
std::fs::write(&rs_file, "// l1\n// l2\nfn rs_fn() {}\n// l4\n").unwrap();
let py_rel = "src/b.py";
let py_file = root.join(py_rel);
std::fs::write(&py_file, "# l1\n# l2\ndef py_fn():\n pass\n").unwrap();
let mut g = Graph::new();
let rs = g
.add_node(
"file::src/a.rs::fn::rs_fn",
"rs_fn",
NodeType::Function,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
rs,
NodeProvenanceInput {
source_path: Some(rs_rel),
line_start: Some(3),
line_end: Some(3),
..Default::default()
},
);
let py = g
.add_node(
"file::src/b.py::fn::py_fn",
"py_fn",
NodeType::Function,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
py,
NodeProvenanceInput {
source_path: Some(py_rel),
line_start: Some(3),
line_end: Some(3),
..Default::default()
},
);
g.finalize().unwrap();
let only_rs = annotate_ext(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
None,
&["rs".to_string()],
XrayMode::DryRun,
);
assert_eq!(
only_rs.counts.symbols_matched, 1,
"extensions=[rs] must exclude the .py symbol"
);
let both = annotate_ext(
&g,
&root,
ANNOT,
Some(node_type_to_u8(NodeType::Function)),
None,
&[],
XrayMode::DryRun,
);
assert_eq!(both.counts.symbols_matched, 2, "empty extensions = any");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn annotate_omitted_node_type_excludes_file_and_module_nodes() {
let root = fresh_sandbox();
let root = root.canonicalize().unwrap();
let rel = "src/c.rs";
let file = root.join(rel);
std::fs::create_dir_all(file.parent().unwrap()).unwrap();
std::fs::write(&file, "// l1\n// l2\nfn real_fn() {}\n// l4\n").unwrap();
let mut g = Graph::new();
let file_node = g
.add_node("file::src/c.rs", "c.rs", NodeType::File, &[], 0.0, 0.0)
.unwrap();
g.set_node_provenance(
file_node,
NodeProvenanceInput {
source_path: Some(rel),
line_start: Some(1),
line_end: Some(4),
..Default::default()
},
);
let mod_node = g
.add_node(
"file::src/c.rs::mod::inner",
"inner",
NodeType::Module,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
mod_node,
NodeProvenanceInput {
source_path: Some(rel),
line_start: Some(2),
line_end: Some(2),
..Default::default()
},
);
let fn_node = g
.add_node(
"file::src/c.rs::fn::real_fn",
"real_fn",
NodeType::Function,
&[],
0.0,
0.0,
)
.unwrap();
g.set_node_provenance(
fn_node,
NodeProvenanceInput {
source_path: Some(rel),
line_start: Some(3),
line_end: Some(3),
..Default::default()
},
);
g.finalize().unwrap();
let (targets, matched) = collect_symbol_targets(&g, &root, None, None, &[]);
assert_eq!(
matched, 1,
"omitted node_type must match only the symbol, not File/Module"
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].1, 3, "the one target is the function at line 3");
let (_, file_matched) =
collect_symbol_targets(&g, &root, Some(node_type_to_u8(NodeType::File)), None, &[]);
assert_eq!(file_matched, 1, "explicit node_type=File still matches it");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn safety_skips_forbidden_and_outside_root() {
assert!(is_forbidden_path(Path::new("/x/graph_snapshot.json")));
assert!(is_forbidden_path(Path::new("/x/plasticity_state.json")));
assert!(is_forbidden_path(Path::new("/x/anything_state.json")));
assert!(is_forbidden_path(Path::new("/x/daemon_alerts.json")));
assert!(is_forbidden_path(Path::new("/x/document_cache_index.json")));
assert!(is_forbidden_path(Path::new("/x/ingest_roots.json")));
assert!(is_forbidden_path(Path::new("/repo/target/debug/foo.rs")));
assert!(is_forbidden_path(Path::new("/repo/.git/config")));
assert!(is_forbidden_path(Path::new(
"/repo/node_modules/pkg/index.js"
)));
assert!(is_forbidden_path(Path::new("/repo/src/foo.rs.xray.tmp")));
assert!(!is_forbidden_path(Path::new("/repo/src/foo.rs")));
}
fn gate_input(
node: &str,
planned_imports: &[&str],
manifest: XrayManifest,
manifest_ratified: bool,
) -> XrayGateInput {
XrayGateInput {
node: node.to_string(),
planned_imports: planned_imports.iter().map(|s| s.to_string()).collect(),
manifest,
manifest_ratified,
manifest_path: None,
}
}
fn gate_g(graph: &Graph, input: &XrayGateInput) -> XrayGateOutput {
let resolved = resolve_manifest(&input.manifest, input.manifest_path.as_deref(), None);
let effective_ratified = if resolved.source == "inline" {
input.manifest_ratified
} else {
resolved.ratified
};
gate_graph(
graph,
input,
&resolved.manifest,
effective_ratified,
resolved.source,
)
}
fn forbid_a_to_b() -> XrayManifest {
XrayManifest {
forbid: vec![("modA".to_string(), "modB".to_string())],
..Default::default()
}
}
#[test]
fn gate_empty_manifest_is_clear() {
let g = orient_graph_fixture();
let out = gate_g(
&g,
&gate_input(
"file::modA/src/lib.rs::fn::a_main",
&[],
XrayManifest::default(),
false,
),
);
assert_eq!(out.verb, "xray_gate");
assert_eq!(out.node_module, "modA");
assert_eq!(out.verdict, "clear");
assert!(out.existing_violations.is_empty());
assert!(out.planned_violations.is_empty());
}
#[test]
fn gate_existing_violation_unratified_is_caution() {
let g = orient_graph_fixture();
let out = gate_g(
&g,
&gate_input(
"file::modA/src/lib.rs::fn::a_main",
&[],
forbid_a_to_b(),
false,
),
);
assert_eq!(out.verdict, "caution");
assert_eq!(out.existing_violations.len(), 1);
let v = &out.existing_violations[0];
assert_eq!(v.from_module, "modA");
assert_eq!(v.to_module, "modB");
assert_eq!(v.rule, "forbid");
assert_eq!(v.kind, "existing");
assert!(out.planned_violations.is_empty());
}
#[test]
fn gate_existing_violation_ratified_is_blocked() {
let g = orient_graph_fixture();
let out = gate_g(
&g,
&gate_input(
"file::modA/src/lib.rs::fn::a_main",
&[],
forbid_a_to_b(),
true,
),
);
assert_eq!(out.verdict, "blocked");
assert_eq!(out.existing_violations.len(), 1);
}
#[test]
fn gate_planned_import_violation_ratified_is_blocked() {
let g = orient_graph_fixture();
let out = gate_g(
&g,
&gate_input(
"file::modA/src/util.rs::fn::a_util",
&["modB"],
forbid_a_to_b(),
true,
),
);
assert_eq!(out.verdict, "blocked");
assert!(
out.existing_violations.is_empty(),
"a_util has no outgoing edges"
);
assert_eq!(out.planned_violations.len(), 1);
let v = &out.planned_violations[0];
assert_eq!(v.from_module, "modA");
assert_eq!(v.to_module, "modB");
assert_eq!(v.rule, "forbid");
assert_eq!(v.kind, "planned");
}
#[test]
fn gate_unknown_node_is_clear() {
let g = orient_graph_fixture();
let out = gate_g(
&g,
&gate_input(
"file::nope/does/not::exist",
&["modB"],
forbid_a_to_b(),
true,
),
);
assert_eq!(out.verdict, "clear");
assert_eq!(out.node_module, "");
assert!(out.existing_violations.is_empty());
assert!(out.planned_violations.is_empty());
assert!(out.reasons.iter().any(|r| r.contains("not in graph")));
}
#[test]
fn gate_layer_rule_also_routes_through_shared_predicate() {
let g = orient_graph_fixture();
let manifest = XrayManifest {
layer_order: vec!["modA".to_string(), "modB".to_string()],
..Default::default()
};
let out = gate_g(
&g,
&gate_input("file::modA/src/lib.rs::fn::a_main", &[], manifest, true),
);
assert_eq!(out.verdict, "blocked");
assert_eq!(out.existing_violations.len(), 1);
assert_eq!(out.existing_violations[0].rule, "layer");
}
fn paint_graph_fixture() -> Graph {
let mut g = Graph::new();
g.add_node(
"file::modA/src/lib.rs::fn::a_main",
"a_main",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::modA/src/util.rs::fn::a_util",
"a_util",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::modB/src/lib.rs::fn::b_core",
"b_core",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap();
g.add_edge(
NodeId::new(0),
NodeId::new(2),
"imports",
FiniteF32::new(1.0),
EdgeDirection::Forward,
false,
FiniteF32::new(0.0),
)
.unwrap();
g.finalize().unwrap();
g
}
fn paint_input(scope: Option<&str>, manifest: XrayManifest, mode: XrayMode) -> XrayPaintInput {
XrayPaintInput {
scope: scope.map(|s| s.to_string()),
manifest,
manifest_path: None,
mode,
}
}
fn paint_g(graph: &mut Graph, input: &XrayPaintInput) -> XrayPaintOutput {
let resolved = resolve_manifest(&input.manifest, input.manifest_path.as_deref(), None);
paint_graph(graph, input, &resolved.manifest, resolved.source)
}
fn state_tag_of(g: &Graph, ext: &str) -> Vec<String> {
g.node_tags(g.resolve_id(ext).unwrap())
.iter()
.filter(|t| t.starts_with(STATE_TAG_PREFIX))
.map(|s| s.to_string())
.collect()
}
#[test]
fn paint_dry_run_counts_split_and_writes_nothing() {
let mut g = paint_graph_fixture();
let out = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::DryRun),
);
assert_eq!(out.verb, "xray_paint");
assert_eq!(out.status, "dry_run");
assert_eq!(out.counts.scanned, 3);
assert_eq!(out.counts.bedrock, 0);
assert_eq!(out.counts.unproven, 1);
assert_eq!(out.counts.overgrowth, 2);
assert_eq!(out.counts.erosion_candidate, 0);
assert_eq!(out.proof_coverage, 0.0);
assert_eq!(out.manifest_source, "none");
assert_eq!(out.counts.painted, 0);
assert!(!out.version.is_empty());
assert_eq!(out.version.len(), 16);
for ext in [
"file::modA/src/lib.rs::fn::a_main",
"file::modA/src/util.rs::fn::a_util",
"file::modB/src/lib.rs::fn::b_core",
] {
assert!(state_tag_of(&g, ext).is_empty(), "{ext} must be unpainted");
}
}
#[test]
fn paint_commit_writes_state_tags() {
let mut g = paint_graph_fixture();
let out = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(out.status, "committed");
assert_eq!(out.counts.scanned, 3);
assert_eq!(out.counts.bedrock, 0);
assert_eq!(out.counts.unproven, 1);
assert_eq!(out.counts.overgrowth, 2);
assert_eq!(out.counts.painted, 3);
assert_eq!(
state_tag_of(&g, "file::modB/src/lib.rs::fn::b_core"),
vec!["xray:state:unproven".to_string()]
);
assert_eq!(
state_tag_of(&g, "file::modA/src/util.rs::fn::a_util"),
vec!["xray:state:overgrowth".to_string()]
);
}
#[test]
fn paint_repaint_is_idempotent_no_accumulation() {
let mut g = paint_graph_fixture();
let first = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(first.counts.painted, 3);
let second = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(second.counts.painted, 0);
for ext in [
"file::modA/src/lib.rs::fn::a_main",
"file::modA/src/util.rs::fn::a_util",
"file::modB/src/lib.rs::fn::b_core",
] {
assert_eq!(
state_tag_of(&g, ext).len(),
1,
"{ext} must carry exactly one xray:state:* tag after re-paint"
);
}
}
#[test]
fn paint_manifest_flags_erosion_candidate_source() {
let mut g = paint_graph_fixture();
let manifest = XrayManifest {
forbid: vec![("modA".to_string(), "modB".to_string())],
..Default::default()
};
let out = paint_g(&mut g, &paint_input(None, manifest, XrayMode::Commit));
assert_eq!(out.status, "committed");
assert_eq!(out.counts.erosion_candidate, 1);
assert_eq!(
state_tag_of(&g, "file::modA/src/lib.rs::fn::a_main"),
vec!["xray:state:erosion-candidate".to_string()]
);
assert_eq!(
state_tag_of(&g, "file::modB/src/lib.rs::fn::b_core"),
vec!["xray:state:unproven".to_string()]
);
assert_eq!(
state_tag_of(&g, "file::modA/src/util.rs::fn::a_util"),
vec!["xray:state:overgrowth".to_string()]
);
}
#[test]
fn paint_scope_narrows_the_painted_set() {
let mut g = paint_graph_fixture();
let out = paint_g(
&mut g,
&paint_input(
Some("file::modA"),
XrayManifest::default(),
XrayMode::Commit,
),
);
assert_eq!(out.counts.scanned, 2);
assert_eq!(out.counts.painted, 2);
assert!(state_tag_of(&g, "file::modB/src/lib.rs::fn::b_core").is_empty());
assert_eq!(
state_tag_of(&g, "file::modA/src/lib.rs::fn::a_main"),
vec!["xray:state:overgrowth".to_string()]
);
}
fn fresh_manifest_dir() -> PathBuf {
static MCOUNTER: AtomicU64 = AtomicU64::new(0);
let n = MCOUNTER.fetch_add(1, Ordering::SeqCst);
let dir =
std::env::temp_dir().join(format!("xray_manifest_test_{}_{}", std::process::id(), n));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn load_manifest_file_ignores_unknown_keys_and_reads_ratified() {
let dir = fresh_manifest_dir();
let path = dir.join("xray.manifest.json");
std::fs::write(
&path,
r#"{"ratified":true,"layer_order":["x","y"],"forbid":[],"require_exists":["seek"],"_about":"ignored"}"#,
)
.unwrap();
let (manifest, ratified) = load_manifest_file(&path).expect("file must parse");
assert!(ratified, "ratified flag must be read as true");
assert_eq!(manifest.layer_order, vec!["x".to_string(), "y".to_string()]);
assert_eq!(manifest.require_exists, vec!["seek".to_string()]);
assert!(manifest.forbid.is_empty());
let missing = dir.join("does_not_exist.json");
assert!(load_manifest_file(&missing).is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn resolve_manifest_file_path_vs_inline_precedence() {
let dir = fresh_manifest_dir();
let path = dir.join("xray.manifest.json");
std::fs::write(
&path,
r#"{"ratified":true,"layer_order":["a","b"],"forbid":[],"require_exists":[]}"#,
)
.unwrap();
let path_str = path.to_string_lossy().into_owned();
let empty = XrayManifest::default();
let resolved = resolve_manifest(&empty, Some(path_str.as_str()), None);
assert_eq!(resolved.source, format!("file:{path_str}"));
assert!(resolved.ratified, "file's ratified flag must be carried");
assert_eq!(
resolved.manifest.layer_order,
vec!["a".to_string(), "b".to_string()]
);
let inline = XrayManifest {
layer_order: vec!["inline_only".to_string()],
..Default::default()
};
let resolved2 = resolve_manifest(&inline, Some(path_str.as_str()), None);
assert_eq!(resolved2.source, "inline");
assert!(!resolved2.ratified, "inline source is never file-ratified");
assert_eq!(
resolved2.manifest.layer_order,
vec!["inline_only".to_string()]
);
let bad = dir.join("nope.json");
let resolved3 =
resolve_manifest(&empty, Some(bad.to_string_lossy().as_ref()), Some("/tmp"));
assert_eq!(resolved3.source, "none");
assert!(!resolved3.ratified);
let _ = std::fs::remove_dir_all(&dir);
}
fn has_state(g: &Graph, ext: &str, tag: &str) -> bool {
state_tag_of(g, ext) == vec![tag.to_string()]
}
fn evidence_graph_fixture() -> Graph {
let mut g = Graph::new();
g.add_node(
"file::m1nd-core/tests/x.rs::fn::t",
"t",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::m1nd-core/src/lib.rs::fn::real",
"real",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::m1nd-mcp/src/caller.rs::fn::caller",
"caller",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::m1nd-mcp/src/used.rs::fn::used",
"used",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::m1nd-mcp/src/orphan.rs::fn::orphan",
"orphan",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap();
g.add_edge(
NodeId::new(0),
NodeId::new(1),
"calls",
FiniteF32::new(1.0),
EdgeDirection::Forward,
false,
FiniteF32::new(0.0),
)
.unwrap();
g.add_edge(
NodeId::new(2),
NodeId::new(3),
"calls",
FiniteF32::new(1.0),
EdgeDirection::Forward,
false,
FiniteF32::new(0.0),
)
.unwrap();
g.finalize().unwrap();
g
}
#[test]
fn is_test_source_keys_off_path_after_file_prefix() {
assert!(is_test_source("file::m1nd-core/tests/x.rs::fn::t"));
assert!(is_test_source("file::pkg/test_helpers.rs::fn::h"));
assert!(is_test_source("file::pkg/src/foo_test.rs::fn::f"));
assert!(is_test_source("file::pkg/src/tests.rs::fn::f"));
assert!(is_test_source("tests.rs"));
assert!(!is_test_source("file::m1nd-core/src/lib.rs::fn::real"));
assert!(!is_test_source("file::m1nd-mcp/src/caller.rs::fn::caller"));
}
#[test]
fn paint_test_exercised_node_is_bedrock() {
let mut g = evidence_graph_fixture();
let out = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(out.status, "committed");
assert!(
has_state(
&g,
"file::m1nd-core/src/lib.rs::fn::real",
"xray:state:bedrock"
),
"test-exercised node must be bedrock"
);
}
#[test]
fn paint_used_but_not_exercised_node_is_unproven() {
let mut g = evidence_graph_fixture();
paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert!(
has_state(
&g,
"file::m1nd-mcp/src/used.rs::fn::used",
"xray:state:unproven"
),
"used-but-unexercised node must be unproven"
);
}
#[test]
fn paint_orphan_node_is_overgrowth() {
let mut g = evidence_graph_fixture();
paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert!(
has_state(
&g,
"file::m1nd-mcp/src/orphan.rs::fn::orphan",
"xray:state:overgrowth"
),
"orphan node must be overgrowth"
);
}
#[test]
fn paint_proof_coverage_is_bedrock_over_scanned() {
let mut g = evidence_graph_fixture();
let out = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::DryRun),
);
assert_eq!(out.counts.scanned, 5);
assert_eq!(out.counts.bedrock, 1);
assert_eq!(out.proof_coverage, 0.2);
}
#[test]
fn paint_grounded_in_edge_marks_target_bedrock() {
let mut g = Graph::new();
g.add_node(
"file::m1nd-core/src/a.rs::fn::claim",
"claim",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_node(
"file::m1nd-core/src/b.rs::fn::ev",
"ev",
NodeType::Function,
&["rust"],
0.0,
0.0,
)
.unwrap(); g.add_edge(
NodeId::new(0),
NodeId::new(1),
"grounded_in",
FiniteF32::new(1.0),
EdgeDirection::Forward,
false,
FiniteF32::new(0.0),
)
.unwrap();
g.finalize().unwrap();
let out = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(out.status, "committed");
assert!(
has_state(&g, "file::m1nd-core/src/b.rs::fn::ev", "xray:state:bedrock"),
"grounded_in target must be bedrock"
);
}
#[test]
fn paint_idempotent_repaint_under_new_classifier_one_tag() {
let mut g = evidence_graph_fixture();
let first = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(first.counts.scanned, 5);
let second = paint_g(
&mut g,
&paint_input(None, XrayManifest::default(), XrayMode::Commit),
);
assert_eq!(second.counts.painted, 0);
for ext in [
"file::m1nd-core/tests/x.rs::fn::t",
"file::m1nd-core/src/lib.rs::fn::real",
"file::m1nd-mcp/src/caller.rs::fn::caller",
"file::m1nd-mcp/src/used.rs::fn::used",
"file::m1nd-mcp/src/orphan.rs::fn::orphan",
] {
assert_eq!(
state_tag_of(&g, ext).len(),
1,
"{ext} must carry exactly one xray:state:* tag after re-paint"
);
}
}
use crate::server::McpConfig;
use m1nd_core::domain::DomainConfig;
fn read_all_ledger_lines(path: &Path) -> Vec<serde_json::Value> {
let content = std::fs::read_to_string(path).unwrap_or_default();
content
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| {
serde_json::from_str::<serde_json::Value>(l).expect("each line parses as JSON")
})
.collect()
}
#[test]
fn append_ledger_increments_seq_and_writes_one_line_each() {
let dir = fresh_sandbox();
let ledger = dir.join("xray.ledger.jsonl");
let rec1 = serde_json::json!({ "seq": 1, "verb": "xray_retag", "v": "a" });
let seq1 = append_ledger(&ledger, &rec1).expect("first append");
assert_eq!(seq1, 1, "first append must return seq 1");
let rec2 = serde_json::json!({ "seq": 2, "verb": "xray_paint", "v": "b" });
let seq2 = append_ledger(&ledger, &rec2).expect("second append");
assert_eq!(seq2, 2, "second append must return seq 2");
let lines = read_all_ledger_lines(&ledger);
assert_eq!(lines.len(), 2, "file must have exactly 2 lines");
assert_eq!(lines[0]["verb"], "xray_retag");
assert_eq!(lines[1]["verb"], "xray_paint");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn build_ledger_record_truncates_changes_over_cap() {
let over = LEDGER_CHANGES_CAP + 5;
let changes: Vec<serde_json::Value> = (0..over)
.map(|i| serde_json::json!({ "node": format!("n{i}") }))
.collect();
let record = build_ledger_record(
7,
"xray_retag",
"deadbeef",
serde_json::json!({ "applied": over }),
changes,
);
assert_eq!(record["seq"], 7);
assert_eq!(record["mode"], "commit");
assert_eq!(
record["changes"].as_array().unwrap().len(),
LEDGER_CHANGES_CAP
);
assert_eq!(record["changes_truncated"], serde_json::json!(over));
let small = build_ledger_record(
1,
"xray_paint",
"v",
serde_json::json!({}),
vec![serde_json::json!({ "node": "x" })],
);
assert!(small.get("changes_truncated").is_none());
assert_eq!(small["changes"].as_array().unwrap().len(), 1);
}
#[test]
fn read_ledger_filters_by_verb_and_returns_most_recent_first() {
let dir = fresh_sandbox();
let ledger = dir.join("xray.ledger.jsonl");
for (i, verb) in ["xray_retag", "xray_paint", "xray_retag", "xray_apply"]
.iter()
.enumerate()
{
let rec =
build_ledger_record((i + 1) as u64, verb, "v", serde_json::json!({}), Vec::new());
append_ledger(&ledger, &rec).unwrap();
}
let (entries, total) = read_ledger(&ledger, 2, None);
assert_eq!(total, 4, "total must count every line pre-filter");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0]["seq"], 4, "most recent first");
assert_eq!(entries[1]["seq"], 3);
let (retags, total2) = read_ledger(&ledger, 20, Some("xray_retag"));
assert_eq!(total2, 4, "total is pre-filter even when filtering");
assert_eq!(retags.len(), 2);
assert_eq!(retags[0]["seq"], 3);
assert_eq!(retags[1]["seq"], 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn read_ledger_missing_file_is_empty_not_error() {
let dir = fresh_sandbox();
let missing = dir.join("nope.ledger.jsonl");
let (entries, total) = read_ledger(&missing, 20, None);
assert!(entries.is_empty());
assert_eq!(total, 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn is_forbidden_path_excludes_the_audit_ledger() {
assert!(is_forbidden_path(Path::new("/repo/xray.ledger.jsonl")));
assert!(is_forbidden_path(Path::new("/repo/XRAY.LEDGER.JSONL"))); assert!(is_forbidden_path(Path::new("/repo/graph_snapshot.json")));
assert!(!is_forbidden_path(Path::new("/repo/src/main.rs")));
}
fn ledger_state(root: &Path, g: Graph) -> SessionState {
let runtime_dir = root.join("runtime");
std::fs::create_dir_all(&runtime_dir).expect("runtime dir");
let config = McpConfig {
graph_source: runtime_dir.join("graph_snapshot.json"),
plasticity_state: runtime_dir.join("plasticity_state.json"),
runtime_dir: Some(runtime_dir),
..Default::default()
};
SessionState::initialize(g, &config, DomainConfig::code()).expect("init session")
}
fn state_ledger_path(state: &SessionState) -> PathBuf {
ledger_path_for(state).expect("graph_path has a parent")
}
#[test]
fn retag_commit_writes_one_ledger_entry_with_exact_before_after() {
let temp = tempfile::tempdir().expect("tempdir");
let mut state = ledger_state(temp.path(), sample_graph());
let ledger = state_ledger_path(&state);
assert!(!ledger.exists(), "no ledger before any commit");
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let value = handle_xray_retag(
&mut state,
input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::Commit),
)
.expect("retag commit");
assert_eq!(value["status"], "committed");
let lines = read_all_ledger_lines(&ledger);
assert_eq!(
lines.len(),
1,
"exactly one ledger line per committed write"
);
let rec = &lines[0];
assert_eq!(rec["seq"], 1);
assert_eq!(rec["verb"], "xray_retag");
assert_eq!(rec["mode"], "commit");
assert_eq!(rec["summary"]["applied"], 3);
let changes = rec["changes"].as_array().unwrap();
assert_eq!(changes.len(), 3, "one change record per mutated node");
let foo = changes
.iter()
.find(|c| c["node"] == "file::a.rs::fn::foo")
.expect("foo recorded");
let before: Vec<String> = foo["before"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
let after: Vec<String> = foo["after"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
assert!(
!before.contains(&"xray:bedrock".to_string()),
"before is pre-state"
);
assert!(
after.contains(&"xray:bedrock".to_string()),
"after has the new tag"
);
}
#[test]
fn retag_dry_run_writes_no_ledger_entry() {
let temp = tempfile::tempdir().expect("tempdir");
let mut state = ledger_state(temp.path(), sample_graph());
let ledger = state_ledger_path(&state);
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
let value = handle_xray_retag(
&mut state,
input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::DryRun),
)
.expect("retag dry_run");
assert_eq!(value["status"], "dry_run");
assert!(!ledger.exists(), "dry_run must write NO ledger");
}
#[test]
fn xray_ledger_reads_back_entries_with_limit_and_verb_filter() {
let temp = tempfile::tempdir().expect("tempdir");
let mut state = ledger_state(temp.path(), sample_graph());
let sel = XraySelector {
filter_tags: vec!["rust".to_string()],
..Default::default()
};
handle_xray_retag(
&mut state,
input(sel, XrayTagOp::Add, &["xray:bedrock"], XrayMode::Commit),
)
.expect("retag commit");
handle_xray_paint(
&mut state,
XrayPaintInput {
scope: None,
manifest: XrayManifest::default(),
manifest_path: None,
mode: XrayMode::Commit,
},
)
.expect("paint commit");
let all = handle_xray_ledger(&mut state, XrayLedgerInput::default()).expect("ledger read");
assert_eq!(all["verb"], "xray_ledger");
assert_eq!(all["total_entries"], 2);
let entries = all["entries"].as_array().unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0]["verb"], "xray_paint", "most recent first");
assert_eq!(entries[1]["verb"], "xray_retag");
assert!(all["ledger_path"].is_string());
let limited = handle_xray_ledger(
&mut state,
XrayLedgerInput {
limit: Some(1),
verb: None,
},
)
.expect("ledger read limited");
assert_eq!(limited["entries"].as_array().unwrap().len(), 1);
assert_eq!(limited["entries"][0]["verb"], "xray_paint");
let filtered = handle_xray_ledger(
&mut state,
XrayLedgerInput {
limit: None,
verb: Some("xray_retag".to_string()),
},
)
.expect("ledger read filtered");
assert_eq!(filtered["total_entries"], 2);
let fe = filtered["entries"].as_array().unwrap();
assert_eq!(fe.len(), 1);
assert_eq!(fe[0]["verb"], "xray_retag");
}
#[test]
fn xray_ledger_missing_file_returns_empty_list() {
let temp = tempfile::tempdir().expect("tempdir");
let mut state = ledger_state(temp.path(), sample_graph());
let out = handle_xray_ledger(&mut state, XrayLedgerInput::default()).expect("ledger read");
assert_eq!(out["total_entries"], 0);
assert!(out["entries"].as_array().unwrap().is_empty());
}
}