affaan-m/ECC · error · ContractError

output bundle contains a symlink

Error message

output bundle contains a symlink: {entry.path}

What it means

While walking the output tree, _validate_output_tree rejects any symlink found at any depth inside the bundle. Symlinks in a shipped bundle can escape the bundle boundary or break on copy/packaging, so the contract forbids them entirely.

Solutions

  1. Find the offending link (the message names it exactly) and replace it with a real copy of the target
  2. Re-run generation with symlinks disabled in the build tooling
  3. Copy bundle contents with cp -rL / rsync -L to dereference links before validation
  4. Exclude link-producing steps (venv, node_modules linking) from the bundle

Example fix

# before
ln -s ../shared/logo.png dist/bundle/assets/logo.png
# after
cp ../shared/logo.png dist/bundle/assets/logo.png
Defensive patterns

Strategy: validation

Validate before calling

import os
for dirpath, dirnames, filenames in os.walk(bundle_root):
    for name in dirnames + filenames:
        if os.path.islink(os.path.join(dirpath, name)):
            raise SystemExit(f'symlink in bundle: {os.path.join(dirpath, name)}')

Try / catch

try:
    validate_bundle(root)
except ContractError as e:
    if 'contains a symlink' in str(e):
        print(f'Dereference and copy: {e}')
    else:
        raise

Prevention

When it happens

Trigger: The bundle content includes a symlink — e.g. node_modules or assets symlinked in during generation, venv paths linked in, or a build tool creating links inside the output dir.

Common situations: Build scripts that symlink shared assets to save space, Python venv symlinks copied into the bundle, packaging tools creating convenience links, or copying the bundle with cp -r vs -a differences across environments.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/16e64a27bf216dc4. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/contract.py:110


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]
    if len(numbers) != len(set(numbers)):
        raise ContractError("genre numbers must be distinct")

    fingerprints = [spec.get("style_fingerprint") for spec in specs]
    signatures = [_semantic_signature(spec) for spec in specs]
    if len(fingerprints) != len(set(fingerprints)) or len(signatures) != len(set(signatures)):
        raise ContractError("genre references collapsed into a generic style; distinct specs required")

View on GitHub (pinned to 8321021c54)