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

  1. Pass the bundle directory, not a file inside it: validate_bundle(Path('dist/bundle')) not the manifest path
  2. Check the path with stat; recreate the directory if a build step replaced it with a file
  3. 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

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


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)