fix: tighten anonymous vertex filter for edges and file paths Edges where EITHER endpoint is a $N anonymous node are now filtered (was: only filtered when BOTH were anonymous). File paths like "crates/foo/bar.rs" are no longer considered meaningful vertices (they're just the file-level root node, not a program element). This eliminates noise like "Removed edge structural.rs -> $392" which has no semantic value for developers.
Author: Aaron Steven White
Commit
3017d114e49b92d70285c91a3239e68de32f851cParent: 6ce78f3ef5
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan1 file changed +27 -22
@@ -363,8 +363,8 @@ pub fn structural_diff_to_json(diff: &StructuralDiff) -> Value {
363363 .collect(); 364364 365365 // Filter raw vertex lists to only named vertices 366- let added_vertices: Vec<&String> = diff.raw_diff.added_vertices.iter().filter(|v| has_named_segment(v)).collect(); 367- let removed_vertices: Vec<&String> = diff.raw_diff.removed_vertices.iter().filter(|v| has_named_segment(v)).collect(); 366+ let added_vertices: Vec<&String> = diff.raw_diff.added_vertices.iter().filter(|v| is_meaningful_vertex(v)).collect(); 367+ let removed_vertices: Vec<&String> = diff.raw_diff.removed_vertices.iter().filter(|v| is_meaningful_vertex(v)).collect(); 368368 369369 let compatible = breaking.is_empty(); 370370
@@ -381,17 +381,17 @@ pub fn structural_diff_to_json(diff: &StructuralDiff) -> Value {
381381 "addedVertices": added_vertices, 382382 "removedVertices": removed_vertices, 383383 "kindChanges": diff.raw_diff.kind_changes.iter() 384- .filter(|kc| has_named_segment(&kc.vertex_id)) 384+ .filter(|kc| is_meaningful_vertex(&kc.vertex_id)) 385385 .map(|kc| json!({ 386386 "vertexId": kc.vertex_id, 387387 "oldKind": kc.old_kind, 388388 "newKind": kc.new_kind, 389389 })).collect::<Vec<_>>(), 390390 "addedEdges": diff.raw_diff.added_edges.iter() 391- .filter(|e| has_named_segment(&e.src) || has_named_segment(&e.tgt)) 391+ .filter(|e| is_meaningful_vertex(&e.src) || is_meaningful_vertex(&e.tgt)) 392392 .map(edge_json).collect::<Vec<_>>(), 393393 "removedEdges": diff.raw_diff.removed_edges.iter() 394- .filter(|e| has_named_segment(&e.src) || has_named_segment(&e.tgt)) 394+ .filter(|e| is_meaningful_vertex(&e.src) || is_meaningful_vertex(&e.tgt)) 395395 .map(edge_json).collect::<Vec<_>>(), 396396 "addedNsids": diff.raw_diff.added_nsids, 397397 "removedNsids": diff.raw_diff.removed_nsids,
@@ -403,43 +403,48 @@ pub fn structural_diff_to_json(diff: &StructuralDiff) -> Value {
403403 }) 404404 } 405405 406-/// Check if a vertex ID has at least one named (non-$N, non-file-path) segment. 407-fn has_named_segment(id: &str) -> bool { 406+/// Check if a vertex ID represents a meaningful named program element 407+/// (not an anonymous AST node like $N, and not just a file path). 408+fn is_meaningful_vertex(id: &str) -> bool { 408409 if id.contains("::") { 410+ // Tree-sitter style: "file.rs::FunctionName::field" 411+ // Must have at least one named segment that's not a file path 409412 id.split("::").any(|s| { 410413 !s.starts_with('$') && !s.is_empty() && !s.contains('/') && !s.contains('.') 411414 }) 412- } else if id.contains(':') { 413- // Lexicon-style IDs are always named 415+ } else if id.contains(':') && !id.contains("::") { 416+ // Lexicon-style: "dev.cospan.repo:body.field" - always meaningful 414417 true 415418 } else { 416- !id.starts_with('$') && !id.is_empty() 419+ // Bare ID: only meaningful if not $N and not a file path 420+ !id.starts_with('$') && !id.is_empty() && !id.contains('/') && !id.contains('.') 417421 } 418422 } 419423 420-/// Check if a breaking change references only anonymous vertices. 424+/// Check if a breaking change should be hidden (references anonymous/internal vertices). 421425 fn is_anonymous_change_breaking(b: &BreakingChange) -> bool { 422426 match b { 423- BreakingChange::RemovedVertex { vertex_id } => !has_named_segment(vertex_id), 427+ BreakingChange::RemovedVertex { vertex_id } => !is_meaningful_vertex(vertex_id), 424428 BreakingChange::RemovedEdge { src, tgt, .. } => { 425- !has_named_segment(src) && !has_named_segment(tgt) 429+ // Filter if either end is anonymous (an edge from a file to $392 is noise) 430+ !is_meaningful_vertex(src) || !is_meaningful_vertex(tgt) 426431 } 427- BreakingChange::KindChanged { vertex_id, .. } => !has_named_segment(vertex_id), 428- BreakingChange::ConstraintTightened { vertex_id, .. } => !has_named_segment(vertex_id), 429- BreakingChange::ConstraintAdded { vertex_id, .. } => !has_named_segment(vertex_id), 430- _ => false, // keep other change types 432+ BreakingChange::KindChanged { vertex_id, .. } => !is_meaningful_vertex(vertex_id), 433+ BreakingChange::ConstraintTightened { vertex_id, .. } => !is_meaningful_vertex(vertex_id), 434+ BreakingChange::ConstraintAdded { vertex_id, .. } => !is_meaningful_vertex(vertex_id), 435+ _ => false, 431436 } 432437 } 433438 434-/// Check if a non-breaking change references only anonymous vertices. 439+/// Check if a non-breaking change should be hidden. 435440 fn is_anonymous_change_non_breaking(nb: &NonBreakingChange) -> bool { 436441 match nb { 437- NonBreakingChange::AddedVertex { vertex_id } => !has_named_segment(vertex_id), 442+ NonBreakingChange::AddedVertex { vertex_id } => !is_meaningful_vertex(vertex_id), 438443 NonBreakingChange::AddedEdge { src, tgt, .. } => { 439- !has_named_segment(src) && !has_named_segment(tgt) 444+ !is_meaningful_vertex(src) || !is_meaningful_vertex(tgt) 440445 } 441- NonBreakingChange::ConstraintRelaxed { vertex_id, .. } => !has_named_segment(vertex_id), 442- NonBreakingChange::ConstraintRemoved { vertex_id, .. } => !has_named_segment(vertex_id), 446+ NonBreakingChange::ConstraintRelaxed { vertex_id, .. } => !is_meaningful_vertex(vertex_id), 447+ NonBreakingChange::ConstraintRemoved { vertex_id, .. } => !is_meaningful_vertex(vertex_id), 443448 _ => false, 444449 } 445450 }