Structurally-identical $ref definitions in schema.json abort the cache rebuild instead of resolving gracefully
### Overview Canvas has no defined behavior for the case where two Drupal extensions provide a *structurally identical* `$defs` entry in their root `schema.json`. Instead of resolving it, `PropShape::getWellKnownPropShapes()` throws: ``` LogicException: 🐛 Duplicate $ref definitions detected: json-schema-definitions://my_theme.theme/image. ``` Because this runs during shape discovery, the exception aborts the entire cache rebuild — not just Canvas component discovery — leaving the site unreachable until the offending definition is removed. It is raised for ordinary user-provided config (a `schema.json` file), yet uses a `LogicException` (a programmer-error type) with site-wide blast radius. This is reachable through normal use: copying a shared shape such as Canvas's own `image` definition into a theme so an SDC can reference `$ref: json-schema-definitions://my_theme.theme/image` is a natural DRY instinct, and the `json-schema-definitions://<extension>.theme/<def>` mechanism otherwise supports themes. Canvas should define graceful behavior for this collision instead of hard-failing. **Workaround (works today):** make the theme's definition structurally distinct from the built-in one (so it does not collide) and implement `hook_canvas_storable_prop_shape_alter()` in the theme to map its own `$ref` to a field type/widget. Verified: the component then becomes eligible and gets the media library widget. ### Proposed resolution The guard exists for a reason: `PropShape::standardize()` maps a resolved schema onto a single canonical `$ref`, so two structurally identical definitions create an ambiguous target. But two things are wrong today — it turns ordinary user config into a `LogicException` that aborts the whole cache rebuild, and it fires even when the duplication is harmless. Remove the fatal. A structural duplicate must never take the site down; at worst it leaves the affected component ineligible — a scoped, recoverable state — with a message that points at `hook_canvas_storable_prop_shape_alter()` as the way to map the `$ref`. The inline pattern (an image object declared with no `$ref`) must keep resolving to the built-in shape. How a duplicate `$ref` should *resolve* is an open design question for the MR: - **Collapse onto the built-in shape** — a structurally-identical `$ref` behaves like the built-in one automatically (the copy "just works", no hook), but an extension can never map an identical structure differently, because its `$ref` is not preserved. - **Each extension owns its `$ref`** — the definition's identity is preserved so its provider maps it via `hook_canvas_storable_prop_shape_alter()`; this allows an identical structure to be mapped differently, at the cost of requiring a hook (and changing today's behavior where an aliased `$ref` is mapped automatically). Graceful ineligibility is the floor either way; the choice above only decides whether a hook is required. ### Remaining tasks - Remove the `LogicException` in `getWellKnownPropShapes()`; a structural duplicate is no longer an error state and must not abort cache rebuild. - Decide the contract for a structurally-identical `$ref` (collapse-onto-built-in vs. each-extension-owns-its-ref-via-hook). - When a `$ref` ends up unmapped, leave the affected component ineligible with a message pointing at `hook_canvas_storable_prop_shape_alter()` — never a site-wide fatal. - Add tests: the inline object still maps to the built-in shape and the site stays up on a duplicate, plus coverage for whichever contract is chosen. ### User interface changes None in this issue. ### API changes None. `PropShape::getWellKnownPropShapes()` and `PropShape::standardize()` are internal.
issue