affaan-m/ECC · error · ContractError
output bundle root must be a directory
Error message
output bundle root must be a directory
What it means
_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.
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
Example fix
# before
validate_bundle(Path('dist/bundle/manifest.json'))
# after
validate_bundle(Path('dist/bundle')) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
p = Path(bundle_root)
assert p.is_dir() and not p.is_symlink(), f'{p} must be a bundle directory' Type guard
def is_dir_not_file(path) -> bool:
from pathlib import Path
p = Path(path)
return p.is_dir() and not p.is_file() Try / catch
try:
validate_bundle(root)
except ContractError as e:
if 'must be a directory' in str(e):
print(f'{root} is not a directory; pass the bundle dir, not a file inside it')
else:
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Nasiko cannot install directly into a filesystem root.
- output root must be a directory
- all overlays must be readable local files
- all takes must be readable local files
- artifact path must stay beneath output root
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/eac7f2f9cd6cb770.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/contract.py:103
os.close(descriptor)
return digest.hexdigest()
def _semantic_signature(spec: dict[str, Any]) -> str:
signature = spec.get("signature", {})
return json.dumps(signature, sort_keys=True, separators=(",", ":"))
def _validate_output_tree(root: Path) -> None:
"""Reject symlinks and special files before parsing bundle content."""
try:
metadata = root.lstat()
except FileNotFoundError:
raise ContractError("output bundle is missing") from None
if stat.S_ISLNK(metadata.st_mode):
raise ContractError("output bundle root must not be a symlink")
if not stat.S_ISDIR(metadata.st_mode):
raise ContractError("output bundle root must be a directory")
pending = [root]
while pending:
directory = pending.pop()
with os.scandir(directory) as entries:
for entry in entries:
if entry.is_symlink():
raise ContractError(f"output bundle contains a symlink: {entry.path}")
if entry.is_dir(follow_symlinks=False):
pending.append(Path(entry.path))
elif not entry.is_file(follow_symlinks=False):
raise ContractError(f"output bundle contains a special file: {entry.path}")
def validate_genre_specs(specs: list[dict[str, Any]]) -> None:
"""Require complete, semantically distinct numbered genre specs."""
if not specs:
raise ContractError("at least one genre spec is required")
numbers = [spec.get("number") for spec in specs]View on GitHub (pinned to 8321021c54)