mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
Add stylesheet_model_known lint rule to validate model/provider names
Checks llm_model and llm_provider values in model_stylesheet against the built-in catalog during `arc validate`. Unknown models or providers emit warnings (not errors) since the catalog is advisory, not restrictive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5fd4a5ea2e
commit
9556dcdcb1
1 changed files with 144 additions and 1 deletions
|
|
@ -1,11 +1,12 @@
|
|||
use std::collections::{HashSet, VecDeque};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::condition::parse_condition;
|
||||
use crate::graph::{AttrValue, Graph};
|
||||
|
||||
use super::{Diagnostic, LintRule, Severity};
|
||||
|
||||
/// Returns all 19 built-in lint rules.
|
||||
/// Returns all 20 built-in lint rules.
|
||||
#[must_use]
|
||||
pub fn built_in_rules() -> Vec<Box<dyn LintRule>> {
|
||||
vec![
|
||||
|
|
@ -28,6 +29,7 @@ pub fn built_in_rules() -> Vec<Box<dyn LintRule>> {
|
|||
Box::new(AllConditionalEdgesRule),
|
||||
Box::new(OrphanCustomOutcomeRule),
|
||||
Box::new(ScriptAbsoluteCdRule),
|
||||
Box::new(StylesheetModelKnownRule),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -898,6 +900,82 @@ impl LintRule for ScriptAbsoluteCdRule {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Rule 20: stylesheet_model_known (WARNING) ---
|
||||
|
||||
struct StylesheetModelKnownRule;
|
||||
|
||||
impl StylesheetModelKnownRule {
|
||||
fn selector_label(selector: &crate::stylesheet::Selector) -> String {
|
||||
match selector {
|
||||
crate::stylesheet::Selector::Universal => "*".to_string(),
|
||||
crate::stylesheet::Selector::Shape(s) => s.clone(),
|
||||
crate::stylesheet::Selector::Class(c) => format!(".{c}"),
|
||||
crate::stylesheet::Selector::Id(id) => format!("#{id}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LintRule for StylesheetModelKnownRule {
|
||||
fn name(&self) -> &'static str {
|
||||
"stylesheet_model_known"
|
||||
}
|
||||
|
||||
fn apply(&self, graph: &Graph) -> Vec<Diagnostic> {
|
||||
let stylesheet_str = graph.model_stylesheet();
|
||||
if stylesheet_str.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let stylesheet = match crate::stylesheet::parse_stylesheet(stylesheet_str) {
|
||||
Ok(ss) => ss,
|
||||
Err(_) => return Vec::new(), // syntax errors caught by stylesheet_syntax rule
|
||||
};
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
for rule in &stylesheet.rules {
|
||||
let label = Self::selector_label(&rule.selector);
|
||||
for decl in &rule.declarations {
|
||||
match decl.property.as_str() {
|
||||
"llm_model" => {
|
||||
if arc_llm::catalog::get_model_info(&decl.value).is_none() {
|
||||
diagnostics.push(Diagnostic {
|
||||
rule: self.name().to_string(),
|
||||
severity: Severity::Warning,
|
||||
message: format!(
|
||||
"Unknown model '{}' in stylesheet rule '{label}'. Run `arc models list` to see available models",
|
||||
decl.value
|
||||
),
|
||||
node_id: None,
|
||||
edge: None,
|
||||
fix: Some("Use a model ID from `arc models list`".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
"llm_provider" => {
|
||||
if arc_llm::Provider::from_str(&decl.value).is_err() {
|
||||
let valid: Vec<&str> =
|
||||
arc_llm::Provider::ALL.iter().map(|p| p.as_str()).collect();
|
||||
diagnostics.push(Diagnostic {
|
||||
rule: self.name().to_string(),
|
||||
severity: Severity::Warning,
|
||||
message: format!(
|
||||
"Unknown provider '{}' in stylesheet rule '{label}'. Valid providers: {}",
|
||||
decl.value,
|
||||
valid.join(", ")
|
||||
),
|
||||
node_id: None,
|
||||
edge: None,
|
||||
fix: Some(format!("Use one of: {}", valid.join(", "))),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -2643,4 +2721,69 @@ mod tests {
|
|||
let d = rule.apply(&g);
|
||||
assert!(d.is_empty());
|
||||
}
|
||||
|
||||
// stylesheet_model_known rule tests
|
||||
|
||||
#[test]
|
||||
fn stylesheet_model_known_rule_valid() {
|
||||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String(
|
||||
"* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }".to_string(),
|
||||
),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
assert!(d.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_model_known_rule_unknown_model() {
|
||||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("#opus { llm_model: claude-opus-4-5; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
assert_eq!(d.len(), 1);
|
||||
assert_eq!(d[0].severity, Severity::Warning);
|
||||
assert!(d[0].message.contains("claude-opus-4-5"));
|
||||
assert!(d[0].message.contains("#opus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_model_known_rule_unknown_provider() {
|
||||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_provider: google; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
assert_eq!(d.len(), 1);
|
||||
assert_eq!(d[0].severity, Severity::Warning);
|
||||
assert!(d[0].message.contains("google"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_model_known_rule_alias() {
|
||||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_model: opus; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
assert!(d.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_model_known_rule_no_stylesheet() {
|
||||
let g = minimal_graph();
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
assert!(d.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue