Skip to content

Renderer collision check ignores the ownership the host itself enforces, so correctly scoped overrides can never reach pm health ok — and an unscoped override that destroys all CLI output reports the identical warning #897

Description

@unbraind

Summary

registerRenderer(format, renderer, ownership?) takes host-enforced ownership metadata — commands and resultDiscriminator — and the host does enforce it at call time, exactly as RendererOverrideOwnership documents ("An unrelated command never reaches the callback").

The collision check ignores that metadata completely. It groups renderer overrides by format alone, so two extensions that own strictly disjoint commands still report extension_renderer_collision and still leave pm health at ok: false.

The consequence is worse than a spurious warning: the check emits the same code, byte for byte, for a correctly scoped override and for an unscoped one that silently destroys the output of every command in the CLI. The warning that should catch a catastrophe is indistinguishable from the warning that means "working as designed".

Reproduction

Two minimal extensions, both registering a json renderer, owning disjoint commands with mutually exclusive result discriminators:

// coll-ext-a/index.js  (coll-ext-b is identical with b for a)
export function activate(api) {
  api.registerRenderer(
    "json",
    () => JSON.stringify({ from: "a" }),
    {
      commands: ["coll-a"],
      resultDiscriminator: (result) =>
        Boolean(result && typeof result === "object" && "colla" in result),
    },
  );
}
pm init colltest
pm install ./ext-a --project     # ok: true, activated: true
pm install ./ext-b --project     # ok: true, activated: true
pm health --json

Result — both activate cleanly (activation_failure_count: 0), and:

ok: false
extension_renderer_collision:json:project:coll-ext-b:project:coll-ext-a

There is no overlap to contend. coll-a and coll-b are disjoint, the discriminators are mutually exclusive, and the host is enforcing both.

The control: identical warning, catastrophic behaviour

Same two extensions, ownership argument removed:

export function activate(api) {
  api.registerRenderer("json", () => JSON.stringify({ from: "b" }));
}
pm health --json
{"from":"b"}

The unscoped renderer replaced the entire output of every --json command in the CLI. pm health no longer reports on anything; it returns the extension's literal.

And the warning it produces is:

extension_renderer_collision:json:project:coll-ext-b:project:coll-ext-a

The same string. One configuration is safe by construction; the other has silently broken every machine-readable output path in the tool. The health check cannot tell them apart.

Where it comes from

The renderer collision detector groups solely on format:

function ds(e){
  let n = new Map;
  for (let t of e.overrides) { let o = n.get(t.format) ?? []; o.push(t); n.set(t.format, o); }
  // ... if (o.length <= 1) continue;
  // ... else emit `extension_renderer_collision:${format}:${winner}:${displaced}` for every non-winner
}

t.commands and t.resultDiscriminator are on those override records — registerRenderer validates them (ownership.commands must contain non-empty command paths, ownership.resultDiscriminator must be a function when provided) and stores them, and the extension report already surfaces result_discriminator as a boolean. The collision check simply never reads them.

Compare the command-override detector immediately below it, which groups by t.command and is therefore correct for its surface. Renderers are the one surface where the discriminating key was available and not used.

Why this matters beyond one warning

A user installing the published pm package catalog into one project reaches pm health ok: false with 20 collisions, of which 9 are renderer collisions between packages that already self-scope. We audited these: they are benign in effect, because each renderer declines results it does not own.

That audit is the problem. Establishing "benign" required reading the source of six packages. The health check is the tool that is supposed to answer it, and it reports the safe case and the output-destroying case identically. So the honest options available to an ecosystem today are:

  1. Ship with pm health ok: false permanently, training everyone to ignore the check — including on the day it reports the real thing.
  2. Delete correct, defensive registrations to silence a warning about scoping the host is already enforcing.

Neither is a good outcome, and no amount of care on the package side reaches ok: true.

Suggested fix

Group renderer overrides by format plus declared ownership, and only report a collision when two overrides can actually contend:

  • Both declare commands and the sets intersect → real collision, report it (this is the us() logic already used for command overrides).
  • Either declares no ownership → it claims the whole format; report the collision, ideally with a distinct, louder code, since the reproduction above shows this case can destroy all output.
  • Both declare disjoint commandsno collision. The host has already guaranteed they cannot both run.

A resultDiscriminator cannot be statically compared, so treating "both declare one" as non-contending would be unsound. But it is a strong signal that the author scoped deliberately, and it would be reasonable to downgrade the severity rather than clear it — the current behaviour gives an author who did everything right no way to express it.

Related: #890 (collision warnings name contenders but not the winner) is closed and complementary — that was about reporting a real collision better; this is about a collision being reported where none exists, while a genuinely dangerous one gets the same wording.

Environment

  • pm CLI 2026.8.3, Node 22, Linux.
  • Both extensions activate cleanly; load_failure_count: 0, activation_failure_count: 0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions