Classify graph attributes with a graph-only reference kind

reference_kind_for_attribute returned the full ReferenceKind, which
includes the config-sourced Dockerfile kind the classifier can never
yield, so the shared graph walker carried a silent `continue` and an
`unreachable!` for impossible kinds; each new config-sourced kind widens
those filler arms, and a classifier extension that reuses an existing
kind would be dropped by the walker without validation, visitation, or a
compiler error. Return a GraphReferenceKind subset instead (converting
into ReferenceKind for validation), making the walker's matches total
with every arm meaningful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-08-17 17:58:46 -04:00
parent 4e48d2887e
commit 8dfbfb9aa5
3 changed files with 43 additions and 20 deletions

View file

@ -519,7 +519,7 @@ impl TemplateTransform {
continue;
}
if let Some(kind) = reference_kind_for_attribute(scope, attr_name, text) {
validate_static_reference(text, kind)
validate_static_reference(text, kind.into())
.map_err(|error| Error::Validation(error.to_string()))?;
continue;
}

View file

@ -11,7 +11,9 @@
//! reference-bearing attribute is added here once instead of drifting between
//! per-crate walkers.
use fabro_types::graph::{AttributeScope, Graph, ReferenceKind, reference_kind_for_attribute};
use fabro_types::graph::{
AttributeScope, Graph, GraphReferenceKind, ReferenceKind, reference_kind_for_attribute,
};
use crate::contains_template_syntax;
@ -119,20 +121,19 @@ pub fn visit_graph_references<'graph, E>(
continue;
};
let reference = match kind {
ReferenceKind::Import | ReferenceKind::ChildWorkflow => value,
// Classification only yields FileInline for `@` values.
ReferenceKind::FileInline => value
GraphReferenceKind::Import | GraphReferenceKind::ChildWorkflow => value,
// Classification only yields these kinds for `@` values.
GraphReferenceKind::FileInline | GraphReferenceKind::GraphGoalFile => value
.strip_prefix('@')
.expect("file inline classification requires a leading '@'"),
ReferenceKind::Dockerfile | ReferenceKind::GraphGoalFile => continue,
.expect("file reference classification requires a leading '@'"),
};
validate_static_reference(reference, kind)
validate_static_reference(reference, kind.into())
.map_err(GraphReferenceError::StaticReference)?;
let event = match kind {
ReferenceKind::Import => GraphReference::Import { reference },
ReferenceKind::ChildWorkflow => GraphReference::ChildWorkflow { reference },
ReferenceKind::FileInline => GraphReference::FileInline { key, reference },
ReferenceKind::Dockerfile | ReferenceKind::GraphGoalFile => unreachable!(),
GraphReferenceKind::Import => GraphReference::Import { reference },
GraphReferenceKind::ChildWorkflow => GraphReference::ChildWorkflow { reference },
GraphReferenceKind::FileInline => GraphReference::FileInline { key, reference },
GraphReferenceKind::GraphGoalFile => GraphReference::GoalFile { reference },
};
visit(event).map_err(GraphReferenceError::Visit)?;
}

View file

@ -598,8 +598,7 @@ pub enum AttributeScope {
Edge,
}
/// Kinds of static (non-templated) file references a graph attribute can
/// carry.
/// Kinds of static (non-templated) workflow-owned file references.
#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display)]
pub enum ReferenceKind {
#[strum(to_string = "file inline reference")]
@ -614,25 +613,48 @@ pub enum ReferenceKind {
GraphGoalFile,
}
/// Kinds of static file references that graph attributes can carry: the
/// subset of [`ReferenceKind`] that [`reference_kind_for_attribute`] can
/// classify. Config-sourced kinds (Dockerfiles) are unrepresentable here by
/// construction.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GraphReferenceKind {
FileInline,
Import,
ChildWorkflow,
GraphGoalFile,
}
impl From<GraphReferenceKind> for ReferenceKind {
fn from(kind: GraphReferenceKind) -> Self {
match kind {
GraphReferenceKind::FileInline => Self::FileInline,
GraphReferenceKind::Import => Self::Import,
GraphReferenceKind::ChildWorkflow => Self::ChildWorkflow,
GraphReferenceKind::GraphGoalFile => Self::GraphGoalFile,
}
}
}
/// Classify a graph attribute as a static file reference, if it is one.
#[must_use]
pub fn reference_kind_for_attribute(
scope: AttributeScope,
key: &str,
value: &str,
) -> Option<ReferenceKind> {
) -> Option<GraphReferenceKind> {
match key {
"import" if matches!(scope, AttributeScope::Node) => Some(ReferenceKind::Import),
"import" if matches!(scope, AttributeScope::Node) => Some(GraphReferenceKind::Import),
"stack.child_workflow" if matches!(scope, AttributeScope::Node) => {
Some(ReferenceKind::ChildWorkflow)
Some(GraphReferenceKind::ChildWorkflow)
}
"goal" if matches!(scope, AttributeScope::Graph) && value.starts_with('@') => {
Some(ReferenceKind::GraphGoalFile)
Some(GraphReferenceKind::GraphGoalFile)
}
"prompt" | "output_schema"
if matches!(scope, AttributeScope::Node) && value.starts_with('@') =>
{
Some(ReferenceKind::FileInline)
Some(GraphReferenceKind::FileInline)
}
_ => None,
}
@ -1168,7 +1190,7 @@ mod tests {
"output_schema",
"@schemas/result.schema.json",
),
Some(ReferenceKind::FileInline),
Some(GraphReferenceKind::FileInline),
);
}