```python:inference/core/workflows/execution_engine/v1/compiler/graph_constructor.py
from typing import Dict, List, Set, Tuple

import networkx as nx
from inference.core.workflows.execution_engine.v1.compiler.entities import \
    StepNode
from inference.core.workflows.execution_engine.v1.compiler.utils import (
    identify_lineage, node_as)


def _collect_unique_control_flow_lineages_with_step_mapping(
    control_flow_steps_selectors: List[str],
    execution_graph: nx.DiGraph,
) -> Tuple[List[List[str]], Dict[int, List[str]]]:
    """
    Collect data lineages from control flow steps and deduplicate by lineage id.

    For each given control flow step selector, reads its data_lineage from the
    execution graph. Returns unique non-empty lineages (by identify_lineage id)
    and a mapping from lineage id to every step selector that produced that
    lineage (used e.g. for error reporting when lineage is incompatible).

    Args:
        control_flow_steps_selectors: Step selectors (node ids) of control
            flow steps whose data_lineage is to be collected.
        execution_graph: The workflow execution graph containing step nodes
            and their data_lineage.

    Returns:
        A tuple of (unique_lineages, lineage_id_to_step_selectors):
        - unique_lineages: list of distinct non-empty data lineages.
        - lineage_id_to_step_selectors: map from lineage id to list of step
          selectors whose data_lineage has that id (includes empty lineages).
    """
    unique_lineages: List[List[str]] = []
    # Use plain dict and avoid separate seen set to reduce allocations
    lineage_id_to_step_selectors: Dict[int, List[str]] = {}


    for step_selector in control_flow_steps_selectors:
        step_data = node_as(
            execution_graph=execution_graph,
            node=step_selector,
            expected_type=StepNode,
        )
        lineage = step_data.data_lineage
        lineage_id = identify_lineage(lineage)
        # If we encounter a non-empty lineage for the first time, record it.
        if lineage and lineage_id not in lineage_id_to_step_selectors:
            lineage_id_to_step_selectors[lineage_id] = []
            unique_lineages.append(lineage)
        # Append the step selector to the mapping, creating the list if needed.
        try:
            lineage_id_to_step_selectors[lineage_id].append(step_selector)
        except KeyError:
            lineage_id_to_step_selectors[lineage_id] = [step_selector]

    return unique_lineages, lineage_id_to_step_selectors




def get_lineage_derived_from_control_flow(
    control_flow_steps_selectors: List[str],
    execution_graph: nx.DiGraph,
) -> List[List[str]]:
    """
    Return unique non-empty data lineages from the given control flow steps.

    Each lineage is taken from the step's data_lineage in the execution graph.
    Lineages are deduplicated by lineage id (see identify_lineage); empty
    lineages are omitted. Used when establishing batch-oriented step lineage.

    Args:
        control_flow_steps_selectors: Step selectors (node ids) of control
            flow steps whose data_lineage is to be collected.
        execution_graph: The workflow execution graph containing step nodes
            and their data_lineage.

    Returns:
        List of distinct non-empty data lineages, one per unique lineage id.
    """
    unique_lineages, _ = _collect_unique_control_flow_lineages_with_step_mapping(
        control_flow_steps_selectors=control_flow_steps_selectors,
        execution_graph=execution_graph,
    )
    return unique_lineages
```