use std::collections::HashMap;
use std::sync::Arc;
use crate::runtime::mesh::build_conformance_vector;
use dashmap::DashMap;
use lsp_max_ast::AutoLspAdapter;
use lsp_max_protocol::{ConformanceVector, LawAxis, MaxDiagnostic};
use lsp_types_max::{
Diagnostic, DiagnosticOptions, DiagnosticServerCapabilities, DocumentDiagnosticReport,
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport,
RelatedFullDocumentDiagnosticReport,
};
use lsp_types_max::{
DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DocumentUri, InitializeResult, NumberOrString, Position, Range,
ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind,
WorkDoneProgressOptions,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
pub type Finding = (MaxDiagnostic, Diagnostic);
pub type ClassifiedFindings = (Vec<Finding>, Vec<Finding>);
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Rule {
pub id: String,
pub name: String,
pub severity: String,
pub pattern: String,
pub path_globs: Vec<String>,
pub exclude_globs: Vec<String>,
pub message: String,
pub rationale: String,
#[serde(default)]
pub eval_budget: EvalBudget,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EvalBudget {
#[default]
Sync,
Background,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RulePack {
pub rules: Vec<Rule>,
#[serde(default)]
pub id: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub depends_on: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ComposedPacks {
pub ordered: Vec<RulePack>,
pub conflicts: Vec<PackConflict>,
}
#[derive(Debug, Clone)]
pub struct PackConflict {
pub rule_id: String,
pub pack_a: String,
pub pack_b: String,
}
pub fn compose_packs(packs: &[RulePack]) -> ComposedPacks {
let by_id: HashMap<&str, &RulePack> = packs
.iter()
.filter(|p| !p.id.is_empty())
.map(|p| (p.id.as_str(), p))
.collect();
let mut ordered: Vec<RulePack> = Vec::with_capacity(packs.len());
let mut visited: std::collections::HashSet<&str> = Default::default();
let mut in_stack: std::collections::HashSet<&str> = Default::default();
fn visit<'a>(
id: &'a str,
by_id: &HashMap<&'a str, &'a RulePack>,
visited: &mut std::collections::HashSet<&'a str>,
in_stack: &mut std::collections::HashSet<&'a str>,
ordered: &mut Vec<RulePack>,
) {
if visited.contains(id) {
return;
}
if in_stack.contains(id) {
return;
}
if let Some(pack) = by_id.get(id) {
in_stack.insert(id);
for dep in &pack.depends_on {
visit(dep.as_str(), by_id, visited, in_stack, ordered);
}
in_stack.remove(id);
visited.insert(id);
ordered.push((*pack).clone());
}
}
for pack in packs.iter().filter(|p| p.id.is_empty()) {
ordered.push(pack.clone());
}
for pack in packs.iter().filter(|p| !p.id.is_empty()) {
visit(&pack.id, &by_id, &mut visited, &mut in_stack, &mut ordered);
}
let mut seen_rules: HashMap<&str, &str> = Default::default();
let mut conflicts = Vec::new();
for pack in &ordered {
for rule in &pack.rules {
if let Some(owner) = seen_rules.get(rule.id.as_str()) {
conflicts.push(PackConflict {
rule_id: rule.id.clone(),
pack_a: owner.to_string(),
pack_b: pack.id.clone(),
});
} else {
seen_rules.insert(rule.id.as_str(), pack.id.as_str());
}
}
}
ComposedPacks { ordered, conflicts }
}
#[derive(Debug, Clone, Default)]
pub struct ValidatedRulePackSet {
ordered: Vec<RulePack>,
}
impl ValidatedRulePackSet {
pub fn new(packs: &[RulePack]) -> Result<Self, Vec<PackConflict>> {
let composed = compose_packs(packs);
if composed.conflicts.is_empty() {
Ok(Self {
ordered: composed.ordered,
})
} else {
Err(composed.conflicts)
}
}
pub fn empty() -> Self {
Self::default()
}
pub fn packs(&self) -> &[RulePack] {
&self.ordered
}
pub fn merge(mut self, other: ValidatedRulePackSet) -> Result<Self, Vec<PackConflict>> {
let combined: Vec<RulePack> = self.ordered.drain(..).chain(other.ordered).collect();
Self::new(&combined)
}
}
#[derive(Clone, Debug)]
pub struct RulePackSnapshot {
pub index: Arc<DashMap<String, IndexedDoc>>,
pub packs: Arc<Vec<RulePack>>,
pub seq: u64,
}
#[derive(Debug, Clone)]
pub struct IndexedDoc {
pub content: String,
pub conformance: Option<ConformanceVector>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossFileRule {
pub id: String,
pub name: String,
pub severity: String,
pub source_glob: String,
pub source_pattern: String,
pub target_glob: String,
pub target_pattern: String,
pub message: String,
pub rationale: String,
}
#[derive(Debug, Clone)]
pub struct CrossFileViolation {
pub source_uri: String,
pub line: u32,
pub col_start: u32,
pub col_end: u32,
pub matched_text: String,
pub rule: CrossFileRule,
}
#[derive(Clone, Debug, Default)]
pub struct WorkspaceIndex {
docs: Arc<DashMap<String, IndexedDoc>>,
seq: Arc<std::sync::atomic::AtomicU64>,
}
impl WorkspaceIndex {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, uri: &str) -> Option<IndexedDoc> {
self.docs
.get(uri)
.map(|ref_multi| ref_multi.value().clone())
}
pub fn upsert(&self, uri: String, content: String, version: i32) {
self.docs.insert(
uri,
IndexedDoc {
content,
conformance: None,
version,
},
);
self.seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn set_conformance(&self, uri: &str, cv: ConformanceVector) {
if let Some(mut entry) = self.docs.get_mut(uri) {
entry.conformance = Some(cv);
}
}
pub fn remove(&self, uri: &str) {
self.docs.remove(uri);
self.seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn snapshot(&self, packs: Arc<Vec<RulePack>>) -> RulePackSnapshot {
RulePackSnapshot {
index: Arc::clone(&self.docs),
packs,
seq: self.seq.load(std::sync::atomic::Ordering::Relaxed),
}
}
#[tracing::instrument(
name = "workspace_conformance",
skip(self),
fields(
doc_count = self.docs.len(),
admitted_count = tracing::field::Empty,
refused_count = tracing::field::Empty,
score = tracing::field::Empty,
)
)]
pub fn workspace_conformance(&self) -> ConformanceVector {
let mut workspace_refused: std::collections::HashSet<LawAxis> = Default::default();
let mut workspace_admitted: std::collections::HashSet<LawAxis> = Default::default();
for entry in self.docs.iter() {
if let Some(cv) = &entry.value().conformance {
for axis in &cv.refused {
workspace_refused.insert(axis.clone());
workspace_admitted.remove(axis);
}
for axis in &cv.admitted {
if !workspace_refused.contains(axis) {
workspace_admitted.insert(axis.clone());
}
}
}
}
let refused: Vec<LawAxis> = workspace_refused.into_iter().collect();
let admitted: Vec<LawAxis> = workspace_admitted.into_iter().collect();
let covered: std::collections::HashSet<&LawAxis> =
refused.iter().chain(admitted.iter()).collect();
let unknown: Vec<LawAxis> = LawAxis::all_named()
.iter()
.filter(|a| !covered.contains(a))
.cloned()
.collect();
let cv_admitted_len = admitted.len();
let cv_refused_len = refused.len();
let total = cv_admitted_len + cv_refused_len;
let score = if total == 0 {
None
} else {
Some(cv_admitted_len as f64 / total as f64 * 100.0)
};
let span = tracing::Span::current();
span.record("admitted_count", cv_admitted_len);
span.record("refused_count", cv_refused_len);
if let Some(s) = score {
span.record("score", s);
}
let mut cv = ConformanceVector {
admitted,
refused,
unknown,
score,
strict_mode: true,
process_quality: None,
..Default::default()
};
cv.sync_bits_from_vecs();
cv
}
}
#[derive(Debug)]
pub struct WorkspaceRuleEvaluator;
impl WorkspaceRuleEvaluator {
pub fn evaluate(
snapshot: &RulePackSnapshot,
rules: &[CrossFileRule],
) -> Vec<CrossFileViolation> {
let mut violations = Vec::new();
for rule in rules {
let source_re = match Regex::new(&rule.source_pattern) {
Ok(r) => r,
Err(e) => {
tracing::warn!(
rule_id = %rule.id,
error = %e,
"CrossFileRule: skipping rule with invalid source_pattern regex"
);
continue;
}
};
let target_evidence: Option<bool> = if rule.target_pattern.is_empty() {
None
} else {
let target_re = match Regex::new(&rule.target_pattern) {
Ok(r) => r,
Err(e) => {
tracing::warn!(
rule_id = %rule.id,
error = %e,
"CrossFileRule: skipping rule with invalid target_pattern regex"
);
continue;
}
};
let found = snapshot.index.iter().any(|entry| {
let uri = entry.key();
glob_matches(&rule.target_glob, uri)
&& target_re.is_match(&entry.value().content)
});
Some(found)
};
for entry in snapshot.index.iter() {
let uri = entry.key();
if !glob_matches(&rule.source_glob, uri) {
continue;
}
for (line_idx, line) in entry.value().content.lines().enumerate() {
for mat in source_re.find_iter(line) {
let violated = match target_evidence {
Some(found) => !found,
None => true, };
if violated {
violations.push(CrossFileViolation {
source_uri: uri.clone(),
line: line_idx as u32,
col_start: mat.start() as u32,
col_end: mat.end() as u32,
matched_text: mat.as_str().to_string(),
rule: rule.clone(),
});
}
}
}
}
}
violations
}
}
pub fn glob_matches(glob: &str, uri: &str) -> bool {
if glob.is_empty() || glob == "**" {
return true;
}
let escaped = regex::escape(glob)
.replace(r"\*\*", ".*")
.replace(r"\*", "[^/]*");
Regex::new(&format!("(?i){}", escaped))
.map(|re| re.is_match(uri))
.unwrap_or(false)
}
pub fn severity_to_law_axis(severity: &str) -> LawAxis {
match severity {
"error" => LawAxis::Domain,
"warning" => LawAxis::Protocol,
"info" => LawAxis::Documentation,
"hint" => LawAxis::Fixture,
other => LawAxis::Custom(other.to_string()),
}
}
pub fn severity_to_lsp(severity: &str) -> DiagnosticSeverity {
match severity {
"error" => DiagnosticSeverity::ERROR,
"warning" => DiagnosticSeverity::WARNING,
"info" => DiagnosticSeverity::INFORMATION,
"hint" => DiagnosticSeverity::HINT,
_ => DiagnosticSeverity::WARNING,
}
}
#[allow(async_fn_in_trait)]
pub trait RulePackServer {
fn rule_packs(&self) -> &ValidatedRulePackSet;
fn cross_file_rules(&self) -> &[CrossFileRule] {
&[]
}
fn grammar(&self) -> tree_sitter::Language;
fn server_name(&self) -> &'static str;
fn client(&self) -> &crate::service::Client;
fn adapter(&self) -> &AutoLspAdapter;
fn workspace_index(&self) -> Option<&WorkspaceIndex> {
None
}
fn spc_monitor(&self) -> Option<&std::sync::Mutex<crate::primitives::SpcMonitor>> {
None
}
fn latency_trackers(
&self,
) -> Option<&Arc<DashMap<String, crate::primitives::RuleLatencyTracker>>> {
None
}
fn rule_circuit_breaker(
&self,
) -> Option<&Arc<parking_lot::Mutex<crate::primitives::CircuitBreaker>>> {
None
}
fn server_capabilities(&self) -> ServerCapabilities {
let has_cross_file =
!self.cross_file_rules().is_empty() || self.workspace_index().is_some();
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
identifier: Some(self.server_name().to_string()),
inter_file_dependencies: has_cross_file,
workspace_diagnostics: has_cross_file,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
})),
..ServerCapabilities::default()
}
}
fn build_initialize_result(&self) -> InitializeResult {
InitializeResult {
capabilities: self.server_capabilities(),
server_info: Some(ServerInfo {
name: self.server_name().to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
offset_encoding: None,
}
}
async fn handle_did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri.clone();
let content = params.text_document.text.clone();
if let Some(idx) = self.workspace_index() {
idx.upsert(uri.as_str().to_string(), content.clone(), 0);
}
self.adapter()
.handle_did_open(params.clone(), self.grammar());
self.publish_findings_classified(uri, &content).await;
}
async fn handle_did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri.clone();
if let Some(change) = params.content_changes.last() {
let content = change.text.clone();
let version = params.text_document.version;
if let Some(idx) = self.workspace_index() {
idx.upsert(uri.as_str().to_string(), content.clone(), version);
}
self.adapter().handle_did_change(params, self.grammar());
self.publish_findings_classified(uri, &content).await;
} else {
self.adapter().handle_did_change(params, self.grammar());
}
}
fn handle_did_close(&self, params: DidCloseTextDocumentParams) {
if let Some(idx) = self.workspace_index() {
idx.remove(params.text_document.uri.as_str());
}
self.adapter().handle_did_close(params);
}
#[tracing::instrument(
name = "scan_uri_classified",
skip(self, content),
fields(
uri = ?uri,
content_bytes = content.len(),
sync_count = tracing::field::Empty,
bg_count = tracing::field::Empty,
)
)]
fn scan_uri_classified(&self, uri: &DocumentUri, content: &str) -> ClassifiedFindings {
if let Some(cb_arc) = self.rule_circuit_breaker() {
if !cb_arc.lock().is_allowed() {
tracing::warn!(uri = ?uri, "CircuitBreaker: scan short-circuited (Open)");
return (Vec::new(), Vec::new());
}
}
let mut sync_r = Vec::new();
let mut bg_r = Vec::new();
for pack in self.rule_packs().packs() {
for rule in &pack.rules {
let re = match Regex::new(&rule.pattern) {
Ok(r) => r,
Err(e) => {
tracing::warn!(
rule_id = %rule.id,
error = %e,
"RulePackServer: skipping rule with invalid regex"
);
if let Some(cb) = self.rule_circuit_breaker() {
cb.lock().record_failure();
}
continue;
}
};
let mut rule_findings = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
for mat in re.find_iter(line) {
let line_u32 = line_idx as u32;
let start_char = mat.start() as u32;
let end_char = mat.end() as u32;
let lsp_diag = Diagnostic {
range: Range::new(
Position::new(line_u32, start_char),
Position::new(line_u32, end_char),
),
severity: Some(severity_to_lsp(&rule.severity)),
code: Some(NumberOrString::String(rule.id.clone())),
source: Some(self.server_name().to_string()),
message: format!("{} — {}", rule.message, mat.as_str()),
related_information: None,
code_description: None,
tags: None,
data: Some(serde_json::json!({
"rule_id": rule.id,
"rule_name": rule.name,
"rationale": rule.rationale,
"uri": uri.as_str(),
})),
};
let max_diag = MaxDiagnostic {
lsp: lsp_diag.clone(),
diagnostic_id: format!("{}-{}:{}", rule.id, line_u32, start_char),
law_id: rule.id.clone(),
law_axis: severity_to_law_axis(&rule.severity),
violated_invariant: rule.rationale.clone(),
..MaxDiagnostic::default()
};
rule_findings.push((max_diag, lsp_diag));
}
}
let effective_budget = if rule.eval_budget == EvalBudget::Sync {
if self
.latency_trackers()
.and_then(|m| m.get(&rule.id))
.map(|t| t.is_reclassified())
.unwrap_or(false)
{
EvalBudget::Background
} else {
EvalBudget::Sync
}
} else {
EvalBudget::Background
};
match effective_budget {
EvalBudget::Sync => {
let t0 = std::time::Instant::now();
sync_r.extend(rule_findings);
let elapsed = t0.elapsed();
if let Some(trackers) = self.latency_trackers() {
let mut entry = trackers.entry(rule.id.clone()).or_default();
let promoted = entry.record(elapsed);
if promoted {
tracing::info!(
rule_id = %rule.id,
elapsed_ms = elapsed.as_millis(),
"EvalBudget: Sync → Background (3 consecutive slow evals)"
);
}
}
if let Some(cb) = self.rule_circuit_breaker() {
cb.lock().record_success();
}
}
EvalBudget::Background => {
bg_r.extend(rule_findings);
if let Some(cb) = self.rule_circuit_breaker() {
cb.lock().record_success();
}
}
}
}
}
let span = tracing::Span::current();
span.record("sync_count", sync_r.len());
span.record("bg_count", bg_r.len());
(sync_r, bg_r)
}
fn scan_uri(&self, uri: &DocumentUri, content: &str) -> Vec<Finding> {
let (mut sync_r, bg_r) = self.scan_uri_classified(uri, content);
sync_r.extend(bg_r);
sync_r
}
async fn publish_findings_classified(&self, uri: DocumentUri, content: &str) {
let t_scan = std::time::Instant::now();
let (sync_findings, bg_findings) = self.scan_uri_classified(&uri, content);
let scan_ms = t_scan.elapsed().as_secs_f64() * 1000.0;
if let Some(mon) = self.spc_monitor() {
if let Ok(mut guard) = mon.lock() {
match guard.push(scan_ms) {
Some(crate::primitives::SpcAlert::Rule1(v)) => {
tracing::warn!(duration_ms = v, "SPC Rule1: scan latency spike");
}
Some(crate::primitives::SpcAlert::Rule2)
| Some(crate::primitives::SpcAlert::Rule3) => {
tracing::warn!(duration_ms = scan_ms, "SPC: structural scan latency drift");
}
Some(crate::primitives::SpcAlert::Rule4) => {
tracing::warn!(
duration_ms = scan_ms,
"SPC Rule4: latency near control limit"
);
}
None => {}
}
}
}
let mut all_lsp: Vec<Diagnostic> = sync_findings.iter().map(|(_, d)| d.clone()).collect();
all_lsp.extend(bg_findings.iter().map(|(_, d)| d.clone()));
let old_score: f64 = self
.workspace_index()
.map(|idx| idx.workspace_conformance().score.unwrap_or(100.0))
.unwrap_or(100.0);
if let Some(idx) = self.workspace_index() {
let all_max: Vec<MaxDiagnostic> = sync_findings
.iter()
.chain(bg_findings.iter())
.map(|(m, _)| m.clone())
.collect();
let cv = build_conformance_vector(&all_max);
idx.set_conformance(uri.as_str(), cv);
}
let new_score: f64 = self
.workspace_index()
.map(|idx| idx.workspace_conformance().score.unwrap_or(100.0))
.unwrap_or(100.0);
#[allow(clippy::float_cmp)]
if (old_score - new_score).abs() > f64::EPSILON {
if let Ok(mut registry) = crate::lock_registry() {
registry.action_seq = registry.action_seq.saturating_add(1);
let seq = registry.action_seq;
const MAX_DELTA_LOG: usize = 4096;
registry.conformance_delta_log.push_back(
crate::max_runtime::ConformanceDeltaEntry {
seq,
instance_id: uri.to_string(),
old_score,
new_score,
timestamp: crate::rfc3339_now(),
},
);
if registry.conformance_delta_log.len() > MAX_DELTA_LOG {
registry.conformance_delta_log.pop_front();
}
}
}
self.client().publish_diagnostics(uri, all_lsp, None).await;
}
async fn publish_findings(&self, uri: DocumentUri, content: &str) {
self.publish_findings_classified(uri, content).await;
}
fn pull_document_diagnostics(&self, uri: &DocumentUri) -> DocumentDiagnosticReportResult {
let rule_diags: Vec<Diagnostic> = self
.adapter()
.get_document(uri, |doc| {
let content = doc.as_str().to_string();
self.scan_uri(uri, &content)
.into_iter()
.map(|(_, d)| d)
.collect::<Vec<_>>()
})
.unwrap_or_default();
DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items: rule_diags,
},
},
))
}
fn conformance_for_content(&self, uri: &DocumentUri, content: &str) -> ConformanceVector {
let max_diags: Vec<MaxDiagnostic> = self
.scan_uri(uri, content)
.into_iter()
.map(|(m, _)| m)
.collect();
build_conformance_vector(&max_diags)
}
fn snapshot_conformance(&self, uri: &DocumentUri) -> ConformanceVector {
let opt_vec: Option<ConformanceVector> = self.adapter().get_document(uri, |doc| {
let content = doc.as_str().to_string();
self.conformance_for_content(uri, &content)
});
opt_vec.unwrap_or_else(|| ConformanceVector {
admitted: vec![],
refused: vec![],
unknown: lsp_max_protocol::LawAxis::all_named().to_vec(),
score: None,
strict_mode: true,
process_quality: None,
..Default::default()
})
}
fn workspace_conformance(&self) -> ConformanceVector {
self.workspace_index()
.map(|idx| idx.workspace_conformance())
.unwrap_or_else(|| ConformanceVector {
admitted: vec![],
refused: vec![],
unknown: lsp_max_protocol::LawAxis::all_named().to_vec(),
score: None,
strict_mode: true,
process_quality: None,
..Default::default()
})
}
fn evaluate_cross_file_rules(&self) -> Option<Vec<CrossFileViolation>> {
let idx = self.workspace_index()?;
let packs = Arc::new(self.rule_packs().packs().to_vec());
let snapshot = idx.snapshot(packs);
let violations = WorkspaceRuleEvaluator::evaluate(&snapshot, self.cross_file_rules());
Some(violations)
}
fn cross_file_violations_as_diagnostics(
&self,
violations: &[CrossFileViolation],
) -> HashMap<String, Vec<Diagnostic>> {
let mut by_uri: HashMap<String, Vec<Diagnostic>> = Default::default();
for v in violations {
let lsp_diag = Diagnostic {
range: Range::new(
Position::new(v.line, v.col_start),
Position::new(v.line, v.col_end),
),
severity: Some(severity_to_lsp(&v.rule.severity)),
code: Some(NumberOrString::String(v.rule.id.clone())),
source: Some(format!("{}/cross-file", self.server_name())),
message: format!("{} — `{}`", v.rule.message, v.matched_text),
related_information: None,
code_description: None,
tags: None,
data: Some(serde_json::json!({
"rule_id": v.rule.id,
"cross_file": true,
"rationale": v.rule.rationale,
})),
};
by_uri
.entry(v.source_uri.clone())
.or_default()
.push(lsp_diag);
}
by_uri
}
async fn publish_cross_file_diagnostics(&self) {
if let Some(violations) = self.evaluate_cross_file_rules() {
let by_uri = self.cross_file_violations_as_diagnostics(&violations);
for (uri_str, diags) in by_uri {
if let Ok(uri) = uri_str.parse::<lsp_types_max::Uri>() {
self.client().publish_diagnostics(uri, diags, None).await;
}
}
}
}
}
#[derive(Debug)]
pub struct PrimitivesBundle {
pub spc_monitor: std::sync::Mutex<crate::primitives::SpcMonitor>,
pub latency_trackers: Arc<DashMap<String, crate::primitives::RuleLatencyTracker>>,
pub circuit_breaker: Arc<parking_lot::Mutex<crate::primitives::CircuitBreaker>>,
}
impl Default for PrimitivesBundle {
fn default() -> Self {
Self {
spc_monitor: std::sync::Mutex::new(crate::primitives::SpcMonitor::default()),
latency_trackers: Arc::new(DashMap::new()),
circuit_breaker: Arc::new(parking_lot::Mutex::new(
crate::primitives::CircuitBreaker::default(),
)),
}
}
}
impl PrimitivesBundle {
pub fn new() -> Self {
Self::default()
}
pub fn spc_monitor_ref(&self) -> &std::sync::Mutex<crate::primitives::SpcMonitor> {
&self.spc_monitor
}
pub fn latency_trackers_ref(
&self,
) -> &Arc<DashMap<String, crate::primitives::RuleLatencyTracker>> {
&self.latency_trackers
}
pub fn circuit_breaker_ref(
&self,
) -> &Arc<parking_lot::Mutex<crate::primitives::CircuitBreaker>> {
&self.circuit_breaker
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_max_protocol::LawAxis;
#[test]
fn test_severity_error_maps_to_domain() {
assert_eq!(severity_to_law_axis("error"), LawAxis::Domain);
}
#[test]
fn test_severity_warning_maps_to_protocol() {
assert_eq!(severity_to_law_axis("warning"), LawAxis::Protocol);
}
#[test]
fn test_severity_info_maps_to_documentation() {
assert_eq!(severity_to_law_axis("info"), LawAxis::Documentation);
}
#[test]
fn test_severity_hint_maps_to_fixture() {
assert_eq!(severity_to_law_axis("hint"), LawAxis::Fixture);
}
#[test]
fn test_severity_unknown_maps_to_custom() {
let axis = severity_to_law_axis("critical");
assert!(matches!(axis, LawAxis::Custom(_)));
if let LawAxis::Custom(s) = axis {
assert_eq!(s, "critical");
}
}
#[test]
fn test_eval_budget_default_is_sync() {
let rule: Rule = serde_json::from_str(
r#"{
"id":"X","name":"X","severity":"error","pattern":"x",
"path_globs":[],"exclude_globs":[],"message":"m","rationale":"r"
}"#,
)
.unwrap();
assert_eq!(rule.eval_budget, EvalBudget::Sync);
}
#[test]
fn test_eval_budget_background_deserialises() {
let rule: Rule = serde_json::from_str(
r#"{
"id":"X","name":"X","severity":"error","pattern":"x",
"path_globs":[],"exclude_globs":[],"message":"m","rationale":"r",
"eval_budget":"background"
}"#,
)
.unwrap();
assert_eq!(rule.eval_budget, EvalBudget::Background);
}
struct TestServer {
packs: ValidatedRulePackSet,
grammar: tree_sitter::Language,
adapter: AutoLspAdapter,
index: WorkspaceIndex,
primitives: PrimitivesBundle,
}
impl TestServer {
fn new(packs: Vec<RulePack>) -> Self {
Self {
packs: ValidatedRulePackSet::new(&packs).unwrap_or_default(),
grammar: tree_sitter_rust::LANGUAGE.into(),
adapter: AutoLspAdapter::new_default(),
index: WorkspaceIndex::new(),
primitives: PrimitivesBundle::new(),
}
}
#[allow(dead_code)]
fn with_cross_file_rules(packs: Vec<RulePack>, _cross: Vec<CrossFileRule>) -> Self {
Self::new(packs)
}
}
impl RulePackServer for TestServer {
fn rule_packs(&self) -> &ValidatedRulePackSet {
&self.packs
}
fn grammar(&self) -> tree_sitter::Language {
self.grammar.clone()
}
fn server_name(&self) -> &'static str {
"test-server"
}
fn client(&self) -> &crate::service::Client {
panic!("TestServer::client() not used in unit tests")
}
fn adapter(&self) -> &AutoLspAdapter {
&self.adapter
}
fn workspace_index(&self) -> Option<&WorkspaceIndex> {
Some(&self.index)
}
fn spc_monitor(&self) -> Option<&std::sync::Mutex<crate::primitives::SpcMonitor>> {
Some(self.primitives.spc_monitor_ref())
}
fn latency_trackers(
&self,
) -> Option<&Arc<DashMap<String, crate::primitives::RuleLatencyTracker>>> {
Some(self.primitives.latency_trackers_ref())
}
fn rule_circuit_breaker(
&self,
) -> Option<&Arc<parking_lot::Mutex<crate::primitives::CircuitBreaker>>> {
Some(self.primitives.circuit_breaker_ref())
}
}
fn make_pack(id: &str, severity: &str, pattern: &str) -> RulePack {
RulePack {
rules: vec![Rule {
id: id.to_string(),
name: id.to_string(),
severity: severity.to_string(),
pattern: pattern.to_string(),
path_globs: vec![],
exclude_globs: vec![],
message: "violation".to_string(),
rationale: "testing".to_string(),
eval_budget: EvalBudget::Sync,
}],
id: id.to_string(),
version: "1.0.0".to_string(),
depends_on: vec![],
}
}
#[test]
fn test_scan_uri_detects_match() {
let pack = make_pack("TEST-001", "error", r"unwrap\(\)");
let server = TestServer::new(vec![pack]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let content = "let x = foo.unwrap();\n";
let findings = server.scan_uri(&uri, content);
assert_eq!(findings.len(), 1);
let (max_diag, lsp_diag) = &findings[0];
assert_eq!(max_diag.law_axis, LawAxis::Domain);
assert_eq!(lsp_diag.severity, Some(DiagnosticSeverity::ERROR));
assert_eq!(
lsp_diag.code,
Some(NumberOrString::String("TEST-001".to_string()))
);
assert_eq!(lsp_diag.range.start.line, 0);
assert_eq!(lsp_diag.range.start.character, 12);
}
#[test]
fn test_scan_uri_no_match_returns_empty() {
let pack = make_pack("TEST-002", "warning", r"panic!");
let server = TestServer::new(vec![pack]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let findings = server.scan_uri(&uri, "fn clean() {}\n");
assert!(findings.is_empty());
}
#[test]
fn test_scan_uri_invalid_regex_skipped() {
let bad = make_pack("BAD-001", "error", r"[invalid(regex");
let good = make_pack("GOOD-001", "warning", r"todo!");
let server = TestServer::new(vec![bad, good]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let findings = server.scan_uri(&uri, concat!("let _ = todo", "!();\n"));
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].0.law_id, "GOOD-001");
}
#[test]
fn test_scan_uri_multiple_matches_on_one_line() {
let pack = make_pack("TEST-003", "error", r"unwrap\(\)");
let server = TestServer::new(vec![pack]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let findings = server.scan_uri(&uri, "let a = x.unwrap(); let b = y.unwrap();\n");
assert_eq!(findings.len(), 2);
}
#[test]
fn test_scan_uri_classified_splits_by_budget() {
let mut sync_rule = make_pack("SYNC-001", "error", r"unwrap\(\)");
sync_rule.rules[0].eval_budget = EvalBudget::Sync;
let mut bg_rule = make_pack("BG-001", "warning", r"todo!");
bg_rule.rules[0].eval_budget = EvalBudget::Background;
let server = TestServer::new(vec![sync_rule, bg_rule]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let content = concat!("x.unwrap(); todo", "!()\n");
let (sync_r, bg_r) = server.scan_uri_classified(&uri, content);
assert_eq!(sync_r.len(), 1);
assert_eq!(sync_r[0].0.law_id, "SYNC-001");
assert_eq!(bg_r.len(), 1);
assert_eq!(bg_r[0].0.law_id, "BG-001");
}
#[test]
fn test_conformance_unknown_never_collapses() {
let pack = make_pack("TEST-004", "error", r"THIS_NEVER_MATCHES_XYZZY");
let server = TestServer::new(vec![pack]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let vec = server.conformance_for_content(&uri, "fn clean() {}\n");
assert!(vec.admitted.is_empty());
assert!(vec.refused.is_empty());
for axis in LawAxis::all_named() {
assert!(vec.unknown.contains(axis));
}
}
#[test]
fn test_conformance_error_refuses_domain_axis() {
let pack = make_pack("TEST-005", "error", r"unwrap\(\)");
let server = TestServer::new(vec![pack]);
let uri: DocumentUri = "file:///tmp/test.rs".parse().unwrap();
let vec = server.conformance_for_content(&uri, "x.unwrap();\n");
assert!(vec.refused.contains(&LawAxis::Domain));
assert!(!vec.admitted.contains(&LawAxis::Domain));
}
#[test]
fn test_snapshot_conformance_all_unknown_when_not_open() {
let pack = make_pack("TEST-006", "error", r"unwrap\(\)");
let server = TestServer::new(vec![pack]);
let uri: DocumentUri = "file:///tmp/not_opened.rs".parse().unwrap();
let vec = server.snapshot_conformance(&uri);
assert!(vec.admitted.is_empty());
assert!(vec.refused.is_empty());
for axis in LawAxis::all_named() {
assert!(vec.unknown.contains(axis));
}
}
#[test]
fn test_workspace_index_upsert_and_remove() {
let idx = WorkspaceIndex::new();
idx.upsert("file:///a.rs".to_string(), "let x = 1;".to_string(), 1);
assert!(idx.docs.contains_key("file:///a.rs"));
idx.remove("file:///a.rs");
assert!(!idx.docs.contains_key("file:///a.rs"));
}
#[test]
fn test_workspace_index_seq_increments() {
let idx = WorkspaceIndex::new();
let s0 = idx.seq.load(std::sync::atomic::Ordering::Relaxed);
idx.upsert("file:///a.rs".to_string(), "x".to_string(), 1);
let s1 = idx.seq.load(std::sync::atomic::Ordering::Relaxed);
assert!(s1 > s0);
}
#[test]
fn test_workspace_conformance_refused_propagates() {
let idx = WorkspaceIndex::new();
idx.upsert("file:///a.rs".to_string(), "fn clean() {}".to_string(), 1);
idx.set_conformance(
"file:///a.rs",
ConformanceVector {
admitted: vec![LawAxis::Protocol],
refused: vec![],
unknown: LawAxis::all_named()
.iter()
.filter(|a| **a != LawAxis::Protocol)
.cloned()
.collect(),
score: Some(100.0),
strict_mode: true,
process_quality: None,
..Default::default()
},
);
idx.upsert("file:///b.rs".to_string(), "x.unwrap()".to_string(), 1);
idx.set_conformance(
"file:///b.rs",
ConformanceVector {
admitted: vec![],
refused: vec![LawAxis::Domain],
unknown: LawAxis::all_named()
.iter()
.filter(|a| **a != LawAxis::Domain)
.cloned()
.collect(),
score: Some(0.0),
strict_mode: true,
process_quality: None,
..Default::default()
},
);
let wc = idx.workspace_conformance();
assert!(wc.refused.contains(&LawAxis::Domain));
assert!(wc.admitted.contains(&LawAxis::Protocol));
assert!(!wc.admitted.contains(&LawAxis::Domain));
}
#[test]
fn test_snapshot_is_isolated_from_subsequent_mutations() {
let idx = WorkspaceIndex::new();
idx.upsert("file:///a.rs".to_string(), "original".to_string(), 1);
let packs = Arc::new(vec![]);
let snap = idx.snapshot(packs.clone());
idx.upsert("file:///a.rs".to_string(), "mutated".to_string(), 2);
let snap_seq = snap.seq;
let live_seq = idx.seq.load(std::sync::atomic::Ordering::Relaxed);
assert!(live_seq > snap_seq, "seq must advance after mutation");
}
#[test]
fn test_compose_packs_no_deps_preserves_order() {
let a = make_pack("alpha", "error", "x");
let b = make_pack("beta", "warning", "y");
let composed = compose_packs(&[a, b]);
assert!(composed.conflicts.is_empty());
assert_eq!(composed.ordered.len(), 2);
}
#[test]
fn test_compose_packs_dep_ordering() {
let mut base = make_pack("base", "error", "x");
let mut derived = make_pack("derived", "warning", "y");
derived.depends_on = vec!["base".to_string()];
let composed = compose_packs(&[derived, base.clone()]);
assert!(composed.conflicts.is_empty());
assert_eq!(composed.ordered[0].id, "base");
assert_eq!(composed.ordered[1].id, "derived");
let _ = &mut base; }
#[test]
fn test_compose_packs_detects_conflict() {
let mut a = make_pack("pack-a", "error", "x");
let mut b = make_pack("pack-b", "error", "y");
a.rules[0].id = "CONFLICT-001".to_string();
b.rules[0].id = "CONFLICT-001".to_string();
let composed = compose_packs(&[a, b]);
assert_eq!(composed.conflicts.len(), 1);
assert_eq!(composed.conflicts[0].rule_id, "CONFLICT-001");
}
#[test]
fn test_cross_file_rule_fires_when_target_missing() {
let idx = WorkspaceIndex::new();
idx.upsert(
"file:///src/main.rs".to_string(),
"// Uses RULE-001".to_string(),
1,
);
idx.upsert(
"file:///rules/policy.toml".to_string(),
"# no rules here".to_string(),
1,
);
let rule = CrossFileRule {
id: "XF-001".to_string(),
name: "rule-id-must-be-defined".to_string(),
severity: "error".to_string(),
source_glob: "**/*.rs".to_string(),
source_pattern: r"RULE-\d+".to_string(),
target_glob: "**/*.toml".to_string(),
target_pattern: r"RULE-\d+".to_string(),
message: "Rule ID referenced but not defined".to_string(),
rationale: "Every referenced rule must have a definition".to_string(),
};
let packs = Arc::new(vec![]);
let snapshot = idx.snapshot(packs);
let violations = WorkspaceRuleEvaluator::evaluate(&snapshot, &[rule]);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule.id, "XF-001");
assert!(violations[0].matched_text.starts_with("RULE-"));
}
#[test]
fn test_cross_file_rule_no_violation_when_target_present() {
let idx = WorkspaceIndex::new();
idx.upsert(
"file:///src/main.rs".to_string(),
"// Uses RULE-001".to_string(),
1,
);
idx.upsert(
"file:///rules/policy.toml".to_string(),
"id = \"RULE-001\"".to_string(),
1,
);
let rule = CrossFileRule {
id: "XF-002".to_string(),
name: "rule-id-must-be-defined".to_string(),
severity: "error".to_string(),
source_glob: "**/*.rs".to_string(),
source_pattern: r"RULE-\d+".to_string(),
target_glob: "**/*.toml".to_string(),
target_pattern: r"RULE-\d+".to_string(),
message: "Rule ID referenced but not defined".to_string(),
rationale: "Every referenced rule must have a definition".to_string(),
};
let packs = Arc::new(vec![]);
let snapshot = idx.snapshot(packs);
let violations = WorkspaceRuleEvaluator::evaluate(&snapshot, &[rule]);
assert!(
violations.is_empty(),
"no violation when target evidence exists"
);
}
#[test]
fn test_workspace_conformance_all_unknown_when_no_docs() {
let pack = make_pack("TEST-007", "error", r"x");
let server = TestServer::new(vec![pack]);
let wc = server.workspace_conformance();
assert!(wc.admitted.is_empty());
assert!(wc.refused.is_empty());
for axis in LawAxis::all_named() {
assert!(wc.unknown.contains(axis));
}
}
#[test]
fn test_glob_matches_double_star() {
assert!(glob_matches("**/*.rs", "file:///src/main.rs"));
assert!(glob_matches("**/*.toml", "file:///rules/policy.toml"));
}
#[test]
fn test_glob_empty_matches_all() {
assert!(glob_matches("", "anything"));
assert!(glob_matches("**", "file:///src/main.rs"));
}
}