Add DAST, graph modules, toast notifications, and dashboard enhancements
Add DAST scanning and code knowledge graph features across the stack: - compliance-dast and compliance-graph workspace crates - Agent API handlers and routes for DAST targets/scans and graph builds - Core models and traits for DAST and graph domains - Dashboard pages for DAST targets/findings/overview and graph explorer/impact - Toast notification system with auto-dismiss for async action feedback - Button click animations and disabled states for better UX Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
128
compliance-graph/src/search/index.rs
Normal file
128
compliance-graph/src/search/index.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use compliance_core::error::CoreError;
|
||||
use compliance_core::models::graph::CodeNode;
|
||||
use tantivy::collector::TopDocs;
|
||||
use tantivy::query::QueryParser;
|
||||
use tantivy::schema::{Schema, Value, STORED, TEXT};
|
||||
use tantivy::{doc, Index, IndexWriter, ReloadPolicy};
|
||||
use tracing::info;
|
||||
|
||||
/// BM25 text search index over code symbols
|
||||
pub struct SymbolIndex {
|
||||
index: Index,
|
||||
#[allow(dead_code)]
|
||||
schema: Schema,
|
||||
qualified_name_field: tantivy::schema::Field,
|
||||
name_field: tantivy::schema::Field,
|
||||
kind_field: tantivy::schema::Field,
|
||||
file_path_field: tantivy::schema::Field,
|
||||
language_field: tantivy::schema::Field,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SearchResult {
|
||||
pub qualified_name: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub file_path: String,
|
||||
pub language: String,
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
impl SymbolIndex {
|
||||
/// Create a new in-memory symbol index
|
||||
pub fn new() -> Result<Self, CoreError> {
|
||||
let mut schema_builder = Schema::builder();
|
||||
let qualified_name_field = schema_builder.add_text_field("qualified_name", TEXT | STORED);
|
||||
let name_field = schema_builder.add_text_field("name", TEXT | STORED);
|
||||
let kind_field = schema_builder.add_text_field("kind", TEXT | STORED);
|
||||
let file_path_field = schema_builder.add_text_field("file_path", TEXT | STORED);
|
||||
let language_field = schema_builder.add_text_field("language", TEXT | STORED);
|
||||
let schema = schema_builder.build();
|
||||
|
||||
let index = Index::create_in_ram(schema.clone());
|
||||
|
||||
Ok(Self {
|
||||
index,
|
||||
schema,
|
||||
qualified_name_field,
|
||||
name_field,
|
||||
kind_field,
|
||||
file_path_field,
|
||||
language_field,
|
||||
})
|
||||
}
|
||||
|
||||
/// Index a set of code nodes
|
||||
pub fn index_nodes(&self, nodes: &[CodeNode]) -> Result<(), CoreError> {
|
||||
let mut writer: IndexWriter = self
|
||||
.index
|
||||
.writer(50_000_000)
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to create index writer: {e}")))?;
|
||||
|
||||
for node in nodes {
|
||||
writer
|
||||
.add_document(doc!(
|
||||
self.qualified_name_field => node.qualified_name.as_str(),
|
||||
self.name_field => node.name.as_str(),
|
||||
self.kind_field => node.kind.to_string(),
|
||||
self.file_path_field => node.file_path.as_str(),
|
||||
self.language_field => node.language.as_str(),
|
||||
))
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to add document: {e}")))?;
|
||||
}
|
||||
|
||||
writer
|
||||
.commit()
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to commit index: {e}")))?;
|
||||
|
||||
info!(nodes = nodes.len(), "Symbol index built");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Search for symbols matching a query
|
||||
pub fn search(&self, query_str: &str, limit: usize) -> Result<Vec<SearchResult>, CoreError> {
|
||||
let reader = self
|
||||
.index
|
||||
.reader_builder()
|
||||
.reload_policy(ReloadPolicy::Manual)
|
||||
.try_into()
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to create reader: {e}")))?;
|
||||
|
||||
let searcher = reader.searcher();
|
||||
let query_parser =
|
||||
QueryParser::for_index(&self.index, vec![self.name_field, self.qualified_name_field]);
|
||||
|
||||
let query = query_parser
|
||||
.parse_query(query_str)
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to parse query: {e}")))?;
|
||||
|
||||
let top_docs = searcher
|
||||
.search(&query, &TopDocs::with_limit(limit))
|
||||
.map_err(|e| CoreError::Graph(format!("Search failed: {e}")))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for (score, doc_address) in top_docs {
|
||||
let doc: tantivy::TantivyDocument = searcher
|
||||
.doc(doc_address)
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to retrieve doc: {e}")))?;
|
||||
|
||||
let get_field = |field: tantivy::schema::Field| -> String {
|
||||
doc.get_first(field)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
};
|
||||
|
||||
results.push(SearchResult {
|
||||
qualified_name: get_field(self.qualified_name_field),
|
||||
name: get_field(self.name_field),
|
||||
kind: get_field(self.kind_field),
|
||||
file_path: get_field(self.file_path_field),
|
||||
language: get_field(self.language_field),
|
||||
score,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
1
compliance-graph/src/search/mod.rs
Normal file
1
compliance-graph/src/search/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod index;
|
||||
Reference in New Issue
Block a user