affaan-m/ECC · error · ValueError

Dependency cycle

Error message

Dependency cycle

What it means

validate_graph walks each node's dependency chain recursively via visit(); when a node name is already on the current traversal stack, the graph contains a circular depends relationship, so ValueError('Dependency cycle') is raised to prevent infinite recursion and an unsatisfiable workflow ordering.

Solutions

  1. Inspect the workflow JSON's node depends arrays and remove the edge that closes the loop (typically the most recently added one)
  2. Topologically list intended stages on paper (distill -> apply) and confirm each depends entry only points to earlier stages
  3. Rename a node instead of re-declaring it if the cycle came from duplicating an existing stage to add a variant
  4. If a shared computation is needed by two branches, extract it into a third node that both depend on rather than making the branches depend on each other

Example fix

// before (cycle)
{"nodes": {"apply": {"depends": ["distill"]}, "distill": {"depends": ["apply"]}}}
// after (acyclic)
{"nodes": {"distill": {"depends": []}, "apply": {"depends": ["distill"]}}}
Defensive patterns

Strategy: validation

Validate before calling

def is_acyclic(nodes):
    state = {}
    def dfs(n):
        if state.get(n) == 1: return False
        if state.get(n) == 2: return True
        state[n] = 1
        for d in nodes[n].get('depends', []):
            if not dfs(d): return False
        state[n] = 2
        return True
    return all(dfs(n) for n in nodes)

Try / catch

try:
    graph = load_graph(kind)
except ValueError as e:
    if 'Dependency cycle' in str(e):
        # surface the offending workflow file and abort config load
        ...

Prevention

When it happens

Trigger: A taste-*.json workflow graph defines nodes where node A depends on B and B (directly or transitively) depends again on A — e.g. 'apply' depends on 'distill' and 'distill' depends on 'apply'.

Common situations: Hand-editing a workflow JSON and adding a depends entry to reuse another node's output; copy-pasting node definitions between workflows so mutual references survive; renaming nodes so an intended dependency now resolves to an ancestor.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/417de95084de12ea. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/workflow_graphs.py:90

def validate_graph(graph):
    """Verify dependencies and every declared input; endpoint schemas need live QA."""
    contents = graph['contents']
    nodes, inputs = contents['nodes'], contents['schema']['input']
    used = set()
    for name, node in nodes.items():
        if node['id'] != name:
            raise ValueError(f'Node id mismatch: {name}')
        for dependency in node.get('depends', []):
            if dependency != 'input' and dependency not in nodes:
                raise ValueError(f'Unknown dependency: {dependency}')
    ancestors = {}

    def visit(name, stack):
        if name == 'input':
            return set()
        if name in stack:
            raise ValueError('Dependency cycle')
        if name not in ancestors:
            deps = nodes[name].get('depends', [])
            ancestors[name] = set(deps).union(*(visit(dep, stack | {name}) for dep in deps))
        return ancestors[name]

    for name, node in nodes.items():
        reachable = visit(name, set())
        for ref in references_in({'input': node.get('input'), 'fields': node.get('fields')}):
            if ref[0] not in reachable:
                raise ValueError(f'{name} references undeclared dependency: {ref[0]}')
            if ref[0] == 'input':
                if len(ref) < 2 or ref[1] not in inputs:
                    raise ValueError('Unknown workflow input')
                used.add(ref[1])
    for ref in references_in(contents.get('output', {})):
        if ref[0] not in nodes:
            raise ValueError('Unknown output node')
    if used != set(inputs):

View on GitHub (pinned to 8321021c54)