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:
Sharang Parnerkar
2026-03-04 13:53:50 +01:00
parent 03ee69834d
commit cea8f59e10
69 changed files with 8745 additions and 54 deletions

View 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)
}
}

View File

@@ -0,0 +1 @@
pub mod index;