fabro/lib/crates/fabro-validate/src/rules/stylesheet_syntax.rs
Bryan Helmkamp 2ba04be181
feat(template): add source-aware diagnostics (#292)
## 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`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](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>
2026-05-16 18:47:37 -04:00

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