{"record":{"id":"eac7f2f9cd6cb770","repo":"affaan-m/ECC","slug":"output-bundle-root-must-be-a-directory","errorCode":null,"errorMessage":"output bundle root must be a directory","messagePattern":"output bundle root must be a directory","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":103,"sourceCode":"        os.close(descriptor)\n    return digest.hexdigest()\n\n\ndef _semantic_signature(spec: dict[str, Any]) -> str:\n    signature = spec.get(\"signature\", {})\n    return json.dumps(signature, sort_keys=True, separators=(\",\", \":\"))\n\n\ndef _validate_output_tree(root: Path) -> None:\n    \"\"\"Reject symlinks and special files before parsing bundle content.\"\"\"\n    try:\n        metadata = root.lstat()\n    except FileNotFoundError:\n        raise ContractError(\"output bundle is missing\") from None\n    if stat.S_ISLNK(metadata.st_mode):\n        raise ContractError(\"output bundle root must not be a symlink\")\n    if not stat.S_ISDIR(metadata.st_mode):\n        raise ContractError(\"output bundle root must be a directory\")\n    pending = [root]\n    while pending:\n        directory = pending.pop()\n        with os.scandir(directory) as entries:\n            for entry in entries:\n                if entry.is_symlink():\n                    raise ContractError(f\"output bundle contains a symlink: {entry.path}\")\n                if entry.is_dir(follow_symlinks=False):\n                    pending.append(Path(entry.path))\n                elif not entry.is_file(follow_symlinks=False):\n                    raise ContractError(f\"output bundle contains a special file: {entry.path}\")\n\n\ndef validate_genre_specs(specs: list[dict[str, Any]]) -> None:\n    \"\"\"Require complete, semantically distinct numbered genre specs.\"\"\"\n    if not specs:\n        raise ContractError(\"at least one genre spec is required\")\n    numbers = [spec.get(\"number\") for spec in specs]","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L85-L121","documentation":"_validate_output_tree requires the bundle root to be a directory. If lstat succeeds but the path is a regular file, socket, FIFO, etc., this error is thrown. It keeps validate_bundle from being pointed at non-bundle artifacts.","triggerScenarios":"Passing a file path (e.g. a zip or a manifest JSON) as the bundle root to validate_bundle, or the bundle directory being replaced by a file during the build.","commonSituations":"Confusing the bundle directory with the receipt/manifest file inside it; a build script accidentally overwriting the output dir with a single file; shell variable pointing at the wrong artifact.","solutions":["Pass the bundle directory, not a file inside it: validate_bundle(Path('dist/bundle')) not the manifest path","Check the path with stat; recreate the directory if a build step replaced it with a file","Fix the build script so it creates the output directory rather than a file at that path"],"exampleFix":"# before\nvalidate_bundle(Path('dist/bundle/manifest.json'))\n# after\nvalidate_bundle(Path('dist/bundle'))","handlingStrategy":"validation","validationCode":"from pathlib import Path\np = Path(bundle_root)\nassert p.is_dir() and not p.is_symlink(), f'{p} must be a bundle directory'","typeGuard":"def is_dir_not_file(path) -> bool:\n    from pathlib import Path\n    p = Path(path)\n    return p.is_dir() and not p.is_file()","tryCatchPattern":"try:\n    validate_bundle(root)\nexcept ContractError as e:\n    if 'must be a directory' in str(e):\n        print(f'{root} is not a directory; pass the bundle dir, not a file inside it')\n    else:\n        raise","preventionTips":["Pass directories, not files, as bundle roots","Verify build scripts don't overwrite output dirs with files","Stat the path in wrapper scripts before validating"],"tags":["filesystem","path","validation"],"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"}