Skip to content

Cross-edge type checking silently skipped for edges sourced from single-operator (operator:) nodes #2461

Description

@phil-opp

Note: This issue was created by an automated, scheduled Claude code-review check. Please verify before acting on it.

Summary

check_type_annotations_full (the dora build / dora graph --validate type-annotation checker) never performs cross-edge type-mismatch detection or type inference for any edge whose source is a single-operator node (a node declared with the operator: block). Multi-operator (operators:) source nodes work correctly. The result is that a real type mismatch on such an edge produces no warning, and in --strict mode it produces a misleading warning instead.

Root cause: a key-shape mismatch caused by running the check pre-resolution

In libraries/core/src/descriptor/validate.rs, operator outputs are registered into output_type_map with an operator-id-prefixed key:

// single-operator node (line ~890)
let op_id = op.id.as_ref().map(|id| id.to_string())
    .unwrap_or_else(|| super::SINGLE_OPERATOR_DEFAULT_ID.to_string()); // "op"
...
output_type_map.insert((nid.clone(), format!("{op_id}/{output_id}")), urn.clone());

The consumer-side lookup uses the mapping exactly as written in the descriptor (line ~1108):

let key = (mapping.source.to_string(), mapping.output.to_string());
let upstream_urn = output_type_map.get(&key);

check_type_annotations_full runs on the unresolved descriptor — both callers (binaries/cli/src/command/validate.rs:153 and binaries/cli/src/command/build/mod.rs:376) invoke it before resolve_aliases_and_set_defaults(). In the unresolved descriptor, a consumer of a single-operator node writes the bare output name (source: A, output: out). The op-id prefix (A/op/out) is only added later, during resolution:

// libraries/core/src/descriptor/mod.rs:105-107
if let Some(op_name) = single_operator_nodes.get(&mapping.source).copied() {
    mapping.output = DataId::from(format!("{op_name}/{}", mapping.output));
}

So at check time the producer key is (A, "op/out") but the consumer lookup key is (A, "out") — they never match, and upstream_urn is always None. This drops the edge into the "no upstream type" branches:

  • non-strict: (None, Some(in_urn))_ => {} — the mismatch check and the (Some, None) inference branch are both skipped.
  • strict: (None, Some(in_urn)) → emits "upstream A/out has no type annotation" even when the operator does declare output_types for that port — a false/misleading warning.

The multi-operator (operators:) form is unaffected because there the consumer writes the operator id verbatim (A/opid/out), so the lookup key already matches the registered key. I confirmed the default op id used on both sides is the same constant (SINGLE_OPERATOR_DEFAULT_ID = "op"), so the divergence is purely bare-vs-prefixed, i.e. an ordering bug — not intentional.

Reproduction (conceptual)

A single-operator node A declaring output_types: { out: std/core/v1/Float32 }, feeding a consumer with input_types: { x: std/media/v1/Image } and inputs: { x: A/out }, produces no type-mismatch warning from dora build / dora graph --validate. The equivalent dataflow written with the operators: (multi-operator) form does warn.

Impact

Advisory subsystem only — it does not accept an invalid dataflow or reject a valid one; the dataflow still runs. But the type-annotation feature (mismatch warnings + inference) silently covers only part of the graph, and --strict emits an incorrect "no type annotation" message for these edges.

Suggested fix

Either run the annotation check after resolve_aliases_and_set_defaults() (so consumer outputs are already op-prefixed), or normalize the single-operator consumer key at lookup time in check_edge_mismatches_with_compat — apply the same {op_id}/{output} prefixing the resolver uses when mapping.source is a single-operator node before probing output_type_map. A regression test covering an operator:-sourced edge (both mismatch and inference cases) would lock this in; the existing tests appear to exercise only the operators: form.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLIrust

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions