{"record":{"id":"ff96d0b470351733","repo":"affaan-m/ECC","slug":"output-root-must-be-a-directory","errorCode":null,"errorMessage":"output root must be a directory","messagePattern":"output root must be a directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/workflow.py","lineNumber":180,"sourceCode":"        for sample in samples\n    ):\n        raise ValueError(\"reference style evidence times must be finite and within duration\")\n    return float(duration)\n\n\nclass _SafeOutput:\n    \"\"\"Descriptor-bound output tree with no-follow traversal and atomic writes.\"\"\"\n\n    def __init__(self, root: Path) -> None:\n        self._root_fd = -1\n        if not hasattr(os, \"O_NOFOLLOW\") or not hasattr(os, \"O_DIRECTORY\"):\n            raise RuntimeError(\"secure output requires O_NOFOLLOW and O_DIRECTORY\")\n        if root.exists() or root.is_symlink():\n            metadata = root.lstat()\n            if stat.S_ISLNK(metadata.st_mode):\n                raise ValueError(\"output root must not be a symlink\")\n            if not stat.S_ISDIR(metadata.st_mode):\n                raise ValueError(\"output root must be a directory\")\n        else:\n            if not root.parent.is_dir():\n                raise ValueError(\"output parent directory must already exist\")\n            root.mkdir(mode=0o700)\n        self.root = root\n        self._root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)\n        self._written: list[str] = []\n\n    def close(self) -> None:\n        if self._root_fd >= 0:\n            os.close(self._root_fd)\n            self._root_fd = -1\n\n    def __del__(self) -> None:\n        self.close()\n\n    def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:\n        current = os.dup(self._root_fd)","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/workflow.py#L162-L198","documentation":"_SafeOutput requires the output root, if it already exists, to be a regular directory (verified via lstat). Any other node type — regular file, FIFO, socket, device — is rejected because the writer opens the root as a directory descriptor and creates children beneath it. The lstat check also ensures symlinked roots were rejected in the prior branch.","triggerScenarios":"Constructing _SafeOutput(Path('results')) where 'results' is an existing regular file (e.g. left over from a previous run that wrote a file at that name) or any non-directory filesystem object.","commonSituations":"A prior tool run crashed and left a file named like the output dir; a user passed a file path (e.g. results.tar.gz) instead of a directory; build artifacts colliding with the output path.","solutions":["Delete or rename the conflicting file at the output path, then rerun","Choose a different, directory-shaped output root","Correct the caller/config so the value passed is a directory path, not a file path","mkdir the directory yourself beforehand if you want to guarantee the type"],"exampleFix":"# before\nout = _SafeOutput(Path('results'))  # results is a regular file\n\n# after\nif Path('results').is_file():\n    Path('results').unlink()\nout = _SafeOutput(Path('results'))","handlingStrategy":"validation","validationCode":"def ensure_output_root_free(p):\n    if p.exists() and not p.is_dir():\n        raise ValueError(f'{p} exists and is not a directory; remove or rename it')","typeGuard":"def root_is_dir_or_absent(p) -> bool:\n    return (not p.exists() and not p.is_symlink()) or (not p.is_symlink() and p.is_dir())","tryCatchPattern":"try:\n    out = _SafeOutput(root)\nexcept ValueError as e:\n    if str(e) == 'output root must be a directory':\n        print(f'{root} is a file or special node — remove it or choose another path')\n    else:\n        raise","preventionTips":["Verify the output path is not occupied by a leftover file before running","Never pass a file path where a directory path is expected","Clean failed runs fully so output names do not collide","Check for special files (FIFOs/sockets) in artifact directories"],"tags":["filesystem","validation","path"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}