fabro/lib/foundation/fabro-template/src/static_reference.rs
Bryan Helmkamp 467087998d
Read workflow graphs through Petri's DOT parser
Fabro's own DOT parser was left with one job after create-time compile
moved to Petri: walking a workflow's file references for the bundler and
the workflow-version store, and reading a name, a goal and two counts.
Petri's frontend parses the same language, so the parser goes and a small
crate reads the graph through Petri's.

`fabro-dot` is that crate: `WorkflowGraph::parse` over
`petri_frontend_attractor::dot` and its semantic model (defaults applied,
subgraphs flattened, chains expanded), `references(position)` as the one
walker over the static-reference vocabulary (each reference with its node
and position, file references checked to be template-free), and
`normalize_for_graphviz`, the re-emit of Fabro DOT with dotted attribute
keys quoted, which the SVG render needs. It sits beside `fabro-petri`
rather than inside it because `fabro-petri` depends on `fabro-workflow`,
which depends on `fabro-workflow-version`: the version store cannot reach
`fabro-petri` without a cycle, and the bundler should not pull the engine
in to read a graph.

Deleted: `fabro-graphviz`'s lexer, grammar, AST, semantic pass and
`parse_ast` (1,829 lines, plus the `nom` dependency); the DOT model in
`fabro-types::graph` (`Graph`, `Node`, `Edge`, `AttrValue`,
`shape_to_handler_type`), with only `ReferenceKind` kept, moved to
`fabro_types::reference`; `fabro-template`'s `visit_graph_references` and
the `GraphReference`/`GraphPosition` types, with the template-syntax rule
(`validate_static_reference`) kept there; the pull-request body's DOT
fallback summary, which was unreachable because the DOT source only
travels with the run spec whose display graph the summary already reads.
`fabro-graphviz` is now the render alone, over `fabro-dot`.

Parity: the old and new walkers were run over every `.fabro` and `.dot`
file in the repository (118) before the deletion. Every reference set is
identical. Five files differ in what Petri reads more correctly: a
backslash before a newline inside a quoted string is a line continuation
(four files, inline prompt text only), and a node named only by an edge
counts as a node (`test/edge_only_node.fabro`, 3 nodes rather than 2, so
the `fabro validate` snapshot moves). The checked-in bundles' shapes and
references are pinned by a snapshot in `fabro-dot`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 12:23:57 -04:00

81 lines
2.3 KiB
Rust

//! Static file references in workflow packages.
//!
//! A workflow names other files from its graph (`import`,
//! `stack.child_workflow`, `@`-prefixed `prompt`, `output_schema` and `goal`
//! values) and from its `workflow.toml` (a Dockerfile, a run goal file).
//! These references are *static*: they may not contain template syntax,
//! because they are resolved before template rendering. The graph walker that
//! finds them lives in `fabro-dot`; this module owns the one rule every
//! consumer applies to a reference before resolving it.
use fabro_types::ReferenceKind;
use crate::contains_template_syntax;
/// A static file reference that unexpectedly contains template syntax.
#[derive(Debug, thiserror::Error)]
#[error("templates are not supported in {kind}s: {value}")]
pub struct StaticReferenceError {
kind: ReferenceKind,
value: String,
}
impl StaticReferenceError {
#[must_use]
pub fn new(kind: ReferenceKind, value: impl Into<String>) -> Self {
Self {
kind,
value: value.into(),
}
}
#[must_use]
pub fn kind(&self) -> ReferenceKind {
self.kind
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
/// Reject static file references (imports, child workflows, `@` file values)
/// that contain template syntax.
pub fn validate_static_reference(
value: &str,
kind: ReferenceKind,
) -> Result<(), StaticReferenceError> {
if contains_template_syntax(value) {
return Err(StaticReferenceError::new(kind, value));
}
Ok(())
}
#[cfg(test)]
mod tests {
use fabro_types::ReferenceKind;
use super::validate_static_reference;
#[test]
fn static_reference_rejects_template_syntax() {
let error = validate_static_reference(
"@schemas/{{ inputs.schema }}.json",
ReferenceKind::FileInline,
)
.unwrap_err();
assert_eq!(error.kind(), ReferenceKind::FileInline);
assert_eq!(error.value(), "@schemas/{{ inputs.schema }}.json");
assert!(
error
.to_string()
.contains("templates are not supported in file inline references"),
"unexpected error: {error}",
);
assert!(
validate_static_reference("@schemas/result.json", ReferenceKind::FileInline).is_ok()
);
}
}