Skip to content

Same-named broken adapter/profile distributions collapse to one recorded error #566

Description

@pbean

Two broken adapter/profile distributions that publish the same entry-point name collapse to a single recorded error, so bmad-loop adapters and bmad-loop validate report one failure where there are two.

Where

Both external-load error maps key on the entry-point name alone:

  • src/bmad_loop/adapters/registry.py:244_EXTERNAL_ERRORS[ep.name] = f"{type(exc).__name__}: {exc}"
  • src/bmad_loop/adapters/profile.py:519_PROFILE_LOAD_ERRORS[ep.name] = f"{type(exc).__name__}: {exc}"

Why it bites

importlib.metadata.entry_points(group=...) does not deduplicate across distributions — two installed packages may both publish bmad_loop.adapters = acme. This was proved directly while reviewing #239 (real *.dist-info on both 3.11 and 3.13): a same-name tie is resolved by sys.path order, and both entry points are yielded by the scan.

So when two same-named distributions are both broken, the scan writes _EXTERNAL_ERRORS["acme"] twice and the second write wins. The operator sees one warning: external adapter 'acme' failed to load: ... and fixes it, only to hit the second package's failure on the next run with no indication it was ever there. The recorded-degrade contract — a failure is never a crash, but it is always surfaced — is silently under-delivered.

Both accessors are affected identically: external_adapter_errors() (registry) and external_profile_errors() (profile).

Why this was not folded into #239

The entry-point name is not just a dict key — it is the value of entry_point in the validate finding detail:

report.warn(
    "adapter.external",
    f"external adapter '{ep_name}' failed to load: {reason}",
    {"entry_point": ep_name, "error": reason},
)

(cli.py, for both adapter.external and adapter.external-profile), and it appears verbatim in the human-readable warning. detail is part of the --json contract in machine.py, so changing the key's shape is a compatibility call that deserves its own change rather than riding along on the registry seam.

Candidate shapes

  1. Key on (dist, name) and render as dist:name — most informative, biggest --json impact.
  2. Append reasons instead of overwriting — keeps the key stable, so no contract change; the detail string just carries both reasons.
  3. Add the distribution to the human message only, leaving detail untouched — smallest change, no contract impact, but --json consumers still see one row.

(2) and (3) are contract-preserving; (1) is not. Worth deciding explicitly.

Acceptance

Two same-named entry points from different distributions, both failing to load ⇒ both reasons reach external_adapter_errors() / external_profile_errors() and both surface through bmad-loop adapters and bmad-loop validate.

Ablation: restore the single-key write and confirm the new test fails — a test that merely asserts "two reasons present" can pass for the wrong reason if the fixture accidentally uses distinct names.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Robustness, enhancement, tests, or docs worth schedulingarea:adaptersCoding-CLI adapters and profilesbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions