{"record":{"id":"4f46f7c38af61702","repo":"unclecode/crawl4ai","slug":"circular-include-p","errorCode":null,"errorMessage":"Circular include {p}","messagePattern":"Circular include (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"crawl4ai/script/c4ai_script.py","lineNumber":350,"sourceCode":"        # Handle list input by joining with newlines\n        if isinstance(text, list):\n            text = '\\n'.join(text)\n        \n        ir = self._parse_with_includes(text)\n        ir = self._collect_procs(ir)\n        ir = self._inline_calls(ir)\n        ir = self._apply_set_vars(ir)\n        return [self._emit_js(c) for c in ir if isinstance(c,Cmd) and c.op!=\"NOP\"]\n\n    # passes\n    def _parse_with_includes(self,txt,seen=None):\n        seen=seen or set()\n        cmds=ASTBuilder().transform(self.parser.parse(txt))\n        out=[]\n        for c in cmds:\n            if isinstance(c,Cmd) and c.op==\"INCLUDE\":\n                p=(self.root/c.args[0]).resolve()\n                if p in seen: raise ValueError(f\"Circular include {p}\")\n                seen.add(p); out+=self._parse_with_includes(p.read_text(),seen)\n            else: out.append(c)\n        return out\n\n    def _collect_procs(self,ir):\n        out=[]\n        for i in ir:\n            if isinstance(i,Proc): self.procs[i.name]=i\n            else: out.append(i)\n        return out\n\n    def _inline_calls(self,ir):\n        out=[]\n        for c in ir:\n            if isinstance(c,Cmd) and c.op==\"CALL\":\n                if c.args[0] not in self.procs:\n                    raise ValueError(f\"Unknown procedure {c.args[0]!r}\")\n                out+=self._inline_calls(self.procs[c.args[0]].body)","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/script/c4ai_script.py#L332-L368","documentation":"ValueError from the C4A-Script compiler's _parse_with_includes pass: an INCLUDE command resolves (relative to the compile root) to a file already present in the current include chain ('seen' set), which means the include graph is cyclic. Compilation stops immediately because inlining would recurse forever.","triggerScenarios":"Script A includes B and B includes A (directly or through a longer chain), or a script includes itself. The check is per-chain: the resolved path appearing anywhere in the active 'seen' set raises.","commonSituations":"Shared snippet files that include each other's helpers, refactoring where two partial scripts were made mutually inclusive, a file including itself as a default-header habit, or symlinks/paths that resolve to the same file via different spellings.","solutions":["Break the cycle: move shared commands into a third file both scripts include, with no back-edges.","Remove self-includes — a file never needs to include itself.","Check for accidental identical includes of the same file twice in one chain; hoist the duplicate include to the top-level script.","Map the include graph quickly (grep INCLUDE lines) to find the loop before editing."],"exampleFix":"# before\n# a.c4a:  INCLUDE \"b.c4a\"\n# b.c4a:  INCLUDE \"a.c4a\"   → ValueError: Circular include .../a.c4a\n\n# after\n# common.c4a: CLICK \"#login\"; WAIT_FOR navigation\n# a.c4a:     INCLUDE \"common.c4a\"\n# b.c4a:     INCLUDE \"common.c4a\"","handlingStrategy":"validation","validationCode":"def check_includes_acyclic(entry: Path, root: Path) -> bool:\n    import re\n    def walk(p, seen):\n        if p in seen:\n            return False\n        seen = seen | {p}\n        for m in re.finditer(r'INCLUDE\\s+\"([^\"]+)\"', p.read_text()):\n            if not walk((root / m.group(1)).resolve(), seen):\n                return False\n        return True\n    return walk(entry.resolve(), set())","typeGuard":null,"tryCatchPattern":"try:\n    js = compile_string(script, root=root)\nexcept ValueError as e:\n    if 'Circular include' in str(e):\n        raise ValueError(f\"Fix include cycle: {e}\") from e","preventionTips":["Keep includes strictly one-directional: entry → partials → shared snippets, never back.","Never let a file include itself or a file that (transitively) includes it.","Lint INCLUDE graphs in CI with the acyclicity walker above."],"tags":["c4a-script","compile","circular-dependency","includes","validation"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}