affaan-m/ECC · error · ValueError

Duplicate asset id

Error message

Duplicate asset id: {asset_id}

What it means

_assets collects each asset's validated id into a set and raises 'Duplicate asset id: {asset_id}' when the same id appears more than once. Asset ids must be unique within a manifest because they are the identity key for provenance, binding, and bundling.

Solutions

  1. Rename the duplicate entries so each id is unique (e.g., hero-v1, hero-v2).
  2. Add a pre-flight dedupe check in any script that generates manifests.
  3. If the same file is needed twice, reference it by the same path but give distinct ids only if the tooling supports it — otherwise reuse one entry.
  4. Grep the manifest for repeated ids: python -c "import json,collections;d=json.load(open('m.json'));print([i for i,c in collections.Counter(a['id'] for a in d['assets']).items() if c>1])".

Example fix

// before
[{"id": "hero", ...}, {"id": "hero", "path": "hero-alt.png", ...}]

// after
[{"id": "hero", ...}, {"id": "hero-alt", "path": "hero-alt.png", ...}]
Defensive patterns

Strategy: validation

Validate before calling

import json, collections
data = json.load(open(path))
ids = [a['id'] for a in data['assets']]
dupes = [i for i, c in collections.Counter(ids).items() if c > 1]
assert not dupes, f'duplicate ids: {dupes}'

Try / catch

try:
    ingest_assets(path)
except ValueError as e:
    if str(e).startswith('Duplicate asset id'):
        rekey_duplicates(path)

Prevention

When it happens

Trigger: Two entries in the assets list share the same 'id' value — e.g., copy-pasted entries, or a merge of two manifests without re-keying ids.

Common situations: Duplicating an entry to add a second file but forgetting to change the id; programmatic manifest generation appending the same default id ('asset1') repeatedly; merging branches that each added an asset with the same id.

Related errors


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

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/assets.py:97

    try:
        data = json.loads(path.read_text(encoding='utf-8'))
    except (ValueError, OSError) as exc:
        raise ValueError(f'Cannot read JSON object: {path}') from exc
    if not isinstance(data, dict):
        raise ValueError('Expected a JSON object')
    return data


def _assets(value: Any) -> list[dict[str, Any]]:
    if not isinstance(value, list) or not value:
        raise ValueError('assets must be a nonempty list')
    ids = set()
    for asset in value:
        if not isinstance(asset, dict):
            raise ValueError('Each asset must be an object')
        asset_id = _text(asset.get('id'), 'asset id')
        if asset_id in ids:
            raise ValueError(f'Duplicate asset id: {asset_id}')
        ids.add(asset_id)
        if asset.get('modality') not in tuple(MODALITIES):
            raise ValueError('modality must be image, video or 3d_asset')
        if asset.get('origin') not in tuple(ORIGINS):
            raise ValueError('Invalid asset origin')
    return value


def _provenance(asset: dict[str, Any], base: Path, verify: bool) -> dict[str, Any] | None:
    source = asset.get('provider_provenance')
    if asset['origin'] != 'external_result':
        if source is not None:
            raise ValueError('Provider provenance requires external_result origin')
        return None
    if not isinstance(source, dict):
        raise ValueError('external_result requires provider provenance and local evidence')
    provider = _text(source.get('provider'), 'provider')
    identifiers = {key: _text(source[key], key) for key in ('request_id', 'workflow_id')

View on GitHub (pinned to 8321021c54)