{"record":{"id":"8181260f54de4c03","repo":"affaan-m/ECC","slug":"output-intermediate-must-be-a-directory-part","errorCode":null,"errorMessage":"output intermediate must be a directory: {part}","messagePattern":"output intermediate must be a directory: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/workflow.py","lineNumber":213,"sourceCode":"        self.close()\n\n    def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:\n        current = os.dup(self._root_fd)\n        try:\n            for part in parts:\n                if not part or part in {\".\", \"..\"} or \"/\" in part:\n                    raise ValueError(\"output path contains an invalid component\")\n                try:\n                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)\n                except FileNotFoundError:\n                    if not create:\n                        raise ValueError(f\"missing output directory: {part}\") from None\n                    os.mkdir(part, mode=0o700, dir_fd=current)\n                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)\n                if stat.S_ISLNK(metadata.st_mode):\n                    raise ValueError(f\"output directory must not be a symlink: {part}\")\n                if not stat.S_ISDIR(metadata.st_mode):\n                    raise ValueError(f\"output intermediate must be a directory: {part}\")\n                child = os.open(\n                    part,\n                    os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,\n                    dir_fd=current,\n                )\n                os.close(current)\n                current = child\n            return current\n        except Exception:\n            os.close(current)\n            raise\n\n    def prepare(self, directories: tuple[str, ...]) -> None:\n        \"\"\"Validate every known intermediate before the first artifact write.\"\"\"\n        opened: list[int] = []\n        try:\n            for directory in directories:\n                opened.append(self._open_dir((directory,), create=True))","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/workflow.py#L195-L231","documentation":"tasteforge's _open_dir walks each intermediate path component and creates directories safely with dir_fd and O_NOFOLLOW. If an intermediate component exists but is not a directory (e.g. a regular file or symlink-to-file), it refuses to proceed with this ValueError rather than overwriting or following it.","triggerScenarios":"Calling write_json or artifact_metadata with a relative path whose parent segment collides with an existing non-directory entry in the output root, e.g. artifact path 'assets/file.json' where 'assets' is a regular file.","commonSituations":"A stale artifact or lock file occupies a name that config now treats as a directory; a previous run wrote a file where a subdirectory was later expected; path segments changed between workflow versions.","solutions":["Inspect the output root and remove/rename the conflicting non-directory entry at the reported path segment","Check the workflow config's output layout for a path whose parent name collides with an existing file","Re-run the workflow with a clean output directory"],"exampleFix":"// before: output root contains a regular file named 'assets'\n// $ rm ./output/assets  (or mv it elsewhere)\nmkdir -p output/assets\n// after: re-run the workflow","handlingStrategy":"validation","validationCode":"from pathlib import Path\np = Path(output_root)\nassert p.is_dir(), f\"output root missing: {p}\"\nrel = Path(relative)\nassert not rel.is_absolute()\nfor part in rel.parts[:-1]:\n    target = p / part\n    if target.exists():\n        assert target.is_dir() and not target.is_symlink(), f\"{target} is not a directory\"","typeGuard":"def is_valid_artifact_dir(root: Path, rel: Path) -> bool:\n    if rel.is_absolute() or not rel.name:\n        return False\n    d = root / rel.parent\n    return d.exists() and d.is_dir() and not d.is_symlink()","tryCatchPattern":null,"preventionTips":["Keep artifact subdirectories dedicated to tasteforge output","Never place plain files at names used as directory segments in artifact paths","Clean the output root between runs"],"tags":["filesystem","path","output"],"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"}