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
- Find the offending link (the message names it exactly) and replace it with a real copy of the target
- Re-run generation with symlinks disabled in the build tooling
- Copy bundle contents with cp -rL / rsync -L to dereference links before validation
- 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
- Copy bundles with cp -rL / rsync -L to dereference symlinks
- Disable link-creating steps (venv, node_modules links) in bundle builds
- Sweep with find -type l before packaging
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
- gate.variant_invalid
- output artifact must be a regular file
- output bundle root must not be a symlink
- output destination must not be a symlink
- output directory must not be a symlink
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)