affaan-m/ECC · error · ValueError

input_artifacts must be a list of local paths

Error message

input_artifacts must be a list of local paths

What it means

ingest_assets validates the optional 'input_artifacts' key of the ingest config: it must be a list (of local path strings, each later fingerprinted). A non-list value (string, dict, null, number) is rejected before receipt construction.

Solutions

  1. Wrap the value in a list: input_artifacts must be an array of path strings.
  2. If there are no input artifacts, omit the key entirely (it defaults to []).
  3. Validate the config shape before calling ingest_assets.

Example fix

# before
{"input_artifacts": "refs/style.png"}
# after
{"input_artifacts": ["refs/style.png"]}
Defensive patterns

Strategy: validation

Validate before calling

cfg = json.loads(open('config.json').read())
inputs = cfg.get('input_artifacts', [])
assert isinstance(inputs, list) and all(isinstance(p, str) for p in inputs)

Type guard

def valid_inputs(cfg) -> bool:
    v = cfg.get('input_artifacts', [])
    return isinstance(v, list) and all(isinstance(p, str) for p in v)

Prevention

When it happens

Trigger: Passing config with input_artifacts set to a single string path instead of a list, a comma-joined string, null, or an object mapping names to paths; YAML/JSON configs where a single-entry list was written without brackets.

Common situations: Hand-written configs: input_artifacts: "data/ref.png" instead of ["data/ref.png"]; config generators emitting an object for named artifacts; null when the key is present but no artifacts exist (omit the key instead).

Related errors


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

Appendix: source

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

    config = _load(config_file)
    base = config_file.parent
    output = _path(out_receipt, Path.cwd())
    if output.exists():
        raise ValueError(f'Receipt output already exists: {output}')
    binding, requests = (_bundle(config['bundle_dir'], base, False)
                         if 'bundle_dir' in config else (None, {}))
    records = []
    for asset in _assets(config.get('assets')):
        record = {key: asset[key] for key in ('id', 'modality', 'origin')}
        record.update(_taste(asset, requests, False))
        record.update(_fingerprint(_path(asset.get('path'), base), asset['modality']))
        provenance = _provenance(asset, base, False)
        if provenance is not None:
            record['provider_provenance'] = provenance
        records.append(record)
    inputs = config.get('input_artifacts', [])
    if not isinstance(inputs, list):
        raise ValueError('input_artifacts must be a list of local paths')
    receipt = dict(schema=SCHEMA, provider_calls=0, provider_execution=False,
                   assets=records, input_artifacts=[_fingerprint(_path(p, base)) for p in inputs])
    if binding is not None:
        receipt['bundle_receipt'] = binding
    if 'genre_spec' in config:
        receipt['genre_spec'] = _fingerprint(_path(config['genre_spec'], base))
    # All inputs already exist, so the output-exists gate also prevents collisions.
    try:
        with output.open('x', encoding='utf-8') as stream:
            json.dump(receipt, stream, indent=2, sort_keys=True)
            stream.write('\n')
    except OSError as exc:
        raise ValueError(f'Cannot create exclusive receipt: {output}') from exc
    return receipt


def validate_assets(receipt_path: str | Path) -> dict[str, Any]:
    """Re-hash every bound file and validate receipt semantics; no remote calls."""

View on GitHub (pinned to 8321021c54)