{"record":{"id":"21a354fcaea81530","repo":"coleam00/Archon","slug":"structureerror-dag-structure-validation-message","errorCode":null,"errorMessage":"structureError (DAG structure validation message, wrapped in IncludeExpansionError)","messagePattern":"structureError \\(DAG structure validation message, wrapped in IncludeExpansionError\\)","errorType":"validation","errorClass":"IncludeExpansionError","httpStatus":null,"severity":"error","filePath":"packages/workflows/src/include-expander.ts","lineNumber":1275,"sourceCode":"    // Re-validate the fully-flattened DAG. Catches a namespaced id colliding with a\n    // hand-written node, cycles introduced by edge rewiring, unknown deps, and the\n    // equivalent failures inside every recursively expanded loop_group body.\n    //\n    // Deliberately NOT re-running the workflow-class placement check here (#2707 step\n    // 2): a reusable block can legitimately author a native gate without declaring its\n    // own `interactive: true` — it is only ever a load error for the workflow ACTUALLY\n    // being loaded standalone (`parseWorkflow`'s own single-file check already covers\n    // that), not for every name `expandWorkflowIncludes` happens to also process as a\n    // `rawByName` entry. A composed gate's drivability stays exactly what it was before\n    // this PR — an invocation-time question `assertComposedGateDriveable` answers\n    // against the workflow actually being dispatched, because load time cannot tell\n    // which discovered workflow will own a given run (see that function's doc comment;\n    // `expandWorkflowIncludes — composed approval gates are stamped, not rejected\n    // (#1764)` pins this down with a \"non-interactive INTERMEDIATE block still expands\"\n    // case).\n    const structureError = validateDagStructure(expanded.nodes);\n    if (structureError) {\n      throw new IncludeExpansionError(structureError);\n    }\n\n    const dedupedRequires = [...new Set(requires)];\n    const result: WorkflowDefinition = {\n      ...collapsed,\n      nodes: expanded.nodes,\n      // `returns:` may name an include directive that no longer exists after flattening.\n      // Rebind it to the same primary sink used for `$includeId.output`; ordinary node ids\n      // pass through unchanged. Without this, a nested reusable workflow can finish with a\n      // dangling return id even though every node-level reference was rewritten correctly.\n      ...(collapsed.returns !== undefined\n        ? { returns: expanded.renameIncludeRef(collapsed.returns) }\n        : {}),\n      ...(dedupedRequires.length > 0 ? { requires: dedupedRequires } : {}),\n    };\n    const outcomeDeclarationError = validateWorkflowOutcomeDeclaration(result);\n    if (outcomeDeclarationError !== null) {\n      throw new IncludeExpansionError(outcomeDeclarationError);","sourceCodeStart":1257,"sourceCodeEnd":1293,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/include-expander.ts#L1257-L1293","documentation":"After inlining all includes, the expander validates the resulting DAG with `validateDagStructure` and wraps any structural problem in an `IncludeExpansionError`. This catches graphs that only exist after expansion — e.g. cycles or broken edges introduced by composing multiple workflows — rather than problems in any single source file.","triggerScenarios":"Calling `expandWorkflowIncludes` (or `expandWorkflowIncludesWithDiscovery`) where the fully inlined node set produces an invalid DAG: cycles across include boundaries, duplicate node ids, or edges referencing nodes that no longer exist after inlining.","commonSituations":"Two included workflows that reference each other (cycle through includes), a parent wiring `depends_on:` a node id that the included workflow renamed, include composition creating duplicate node ids.","solutions":["Read the wrapped structureError message — it names the specific structural defect and node ids","Break any cycle: if A includes B and B includes A, extract the shared node(s) into a third workflow both can include","Fix `depends_on` references to match node ids as they exist after expansion (check the included workflow's ids)","Rename conflicting node ids in one of the included workflows"],"exampleFix":"# before: cycle across include boundary\n# a.yaml includes b.yaml; b.yaml includes a.yaml\n# after\n# a.yaml includes shared.yaml; b.yaml includes shared.yaml","handlingStrategy":"validation","validationCode":"// Detect include cycles before expansion.\nfunction detectIncludeCycles(raws: Map<string, { includes?: string[] }>): string[] | null {\n  const visiting = new Set<string>(), done = new Set<string>();\n  let cycle: string[] | null = null;\n  const visit = (n: string, stack: string[]) => {\n    if (cycle) return;\n    if (visiting.has(n)) { cycle = [...stack, n]; return; }\n    if (done.has(n)) return;\n    visiting.add(n);\n    for (const inc of raws.get(n)?.includes ?? []) visit(inc, [...stack, n]);\n    visiting.delete(n); done.add(n);\n  };\n  for (const name of raws.keys()) visit(name, []);\n  return cycle;\n}","typeGuard":null,"tryCatchPattern":"try {\n  const expanded = expandWorkflowIncludes(rawByName);\n} catch (err) {\n  if (err instanceof IncludeExpansionError) {\n    // structureError is wrapped verbatim; surface it with the workflow name under expansion.\n    throw new Error(`Include expansion produced an invalid DAG: ${err.message}`, { cause: err });\n  }\n  throw err;\n}","preventionTips":["Keep include relationships acyclic — factor shared nodes into a common included workflow","Keep depends_on ids stable when refactoring included workflows; search for references before renaming node ids","Make node ids unique across workflows that will ever be composed","Add a CI step that expands and structurally validates every workflow"],"tags":["workflow","dag","includes","validation","cycle"],"backgroundTag":"invalid-dag-structure","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}