mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
## Summary Template failures from `fabro run` and structural warnings from `fabro validate` now preserve source provenance through rendering, workflow transforms, API serialization, and CLI display. Diagnostics can point at the actual workflow, import, or prompt file with node/attribute context instead of surfacing MiniJinja's generic `<string>` source. ## What Changed - Added named MiniJinja render APIs plus miette-aware `TemplateError` metadata for source names, source text, spans, and labels. - Reworked workflow template expansion so inline attributes, imported workflows, and `@prompt` files render with file and owner context. - Split strict run behavior from structural validate behavior: run-start still hard-fails on missing inputs, while validate emits source-aware warnings and continues linting. - Extended validation diagnostics through Rust structs, OpenAPI, server DTO mapping, and CLI rendering with optional source path, line, column, span, and related metadata. - Added regression coverage across template rendering, workflow transforms, CLI output, and the server validate endpoint. ## Verification - `cargo nextest run -p fabro-template` - `ulimit -n 4096 && cargo nextest run -p fabro-workflow --no-fail-fast` - `cargo nextest run -p fabro-cli bare_fabro_with_unbound_inputs_validates_structurally_with_warning run_rejects_unbound_template_inputs_before_creating_remote_run` - `cargo nextest run -p fabro-server validate_endpoint_returns_template_source_coordinates` - `cargo build -p fabro-api` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
125 lines
3.4 KiB
Rust
125 lines
3.4 KiB
Rust
use fabro_graphviz::graph::Graph;
|
|
use fabro_graphviz::stylesheet::parse_stylesheet;
|
|
|
|
use crate::{Diagnostic, LintRule, Severity};
|
|
|
|
pub(super) fn rule() -> Box<dyn LintRule> {
|
|
Box::new(Rule)
|
|
}
|
|
|
|
struct Rule;
|
|
|
|
impl LintRule for Rule {
|
|
fn name(&self) -> &'static str {
|
|
"stylesheet_syntax"
|
|
}
|
|
|
|
fn apply(&self, graph: &Graph) -> Vec<Diagnostic> {
|
|
let stylesheet = graph.model_stylesheet();
|
|
if stylesheet.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
match parse_stylesheet(stylesheet) {
|
|
Ok(_) => Vec::new(),
|
|
Err(e) => vec![Diagnostic {
|
|
rule: self.name().to_string(),
|
|
severity: Severity::Error,
|
|
message: format!("Model stylesheet parse error: {e}"),
|
|
node_id: None,
|
|
edge: None,
|
|
fix: Some("Fix the model_stylesheet syntax".to_string()),
|
|
|
|
..Diagnostic::default()
|
|
}],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use fabro_graphviz::graph::AttrValue;
|
|
|
|
use super::Rule;
|
|
use crate::rules::test_support::minimal_graph;
|
|
use crate::{LintRule, Severity};
|
|
|
|
#[test]
|
|
fn stylesheet_syntax_rule_unbalanced() {
|
|
let mut g = minimal_graph();
|
|
g.attrs.insert(
|
|
"model_stylesheet".to_string(),
|
|
AttrValue::String("* { model: foo;".to_string()),
|
|
);
|
|
let rule = Rule;
|
|
let d = rule.apply(&g);
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].severity, Severity::Error);
|
|
}
|
|
|
|
#[test]
|
|
fn stylesheet_syntax_rule_balanced() {
|
|
let mut g = minimal_graph();
|
|
g.attrs.insert(
|
|
"model_stylesheet".to_string(),
|
|
AttrValue::String("* { model: foo; }".to_string()),
|
|
);
|
|
let rule = Rule;
|
|
let d = rule.apply(&g);
|
|
assert!(d.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn stylesheet_syntax_rule_malformed_selector() {
|
|
let mut g = minimal_graph();
|
|
g.attrs.insert(
|
|
"model_stylesheet".to_string(),
|
|
AttrValue::String("* { garbage garbage }".to_string()),
|
|
);
|
|
let rule = Rule;
|
|
let d = rule.apply(&g);
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].severity, Severity::Error);
|
|
}
|
|
|
|
// --- Additional coverage: condition_syntax invalid case ---
|
|
|
|
#[test]
|
|
fn stylesheet_syntax_rule_no_stylesheet() {
|
|
let g = minimal_graph();
|
|
let rule = Rule;
|
|
let d = rule.apply(&g);
|
|
assert!(d.is_empty());
|
|
}
|
|
|
|
// --- Additional coverage: type_known no type attr ---
|
|
|
|
#[test]
|
|
fn stylesheet_syntax_rule_multi_rule_valid() {
|
|
let mut g = minimal_graph();
|
|
g.attrs.insert(
|
|
"model_stylesheet".to_string(),
|
|
AttrValue::String(
|
|
"* { model: gpt-4; } .fast { model: gpt-3.5; reasoning_effort: low; }".to_string(),
|
|
),
|
|
);
|
|
let rule = Rule;
|
|
let d = rule.apply(&g);
|
|
assert!(d.is_empty());
|
|
}
|
|
|
|
// --- fidelity_valid: multiple simultaneous violations ---
|
|
|
|
#[test]
|
|
fn stylesheet_syntax_rule_empty_string() {
|
|
let mut g = minimal_graph();
|
|
g.attrs.insert(
|
|
"model_stylesheet".to_string(),
|
|
AttrValue::String(String::new()),
|
|
);
|
|
let rule = Rule;
|
|
let d = rule.apply(&g);
|
|
assert!(d.is_empty());
|
|
}
|
|
|
|
// --- type_known: start and exit types from shape are not flagged ---
|
|
}
|