# Plan: host definitions must override vendor definitions (ContainerFactory)

**Status:** implemented on V3, pending release. Written 2026-07-11 after the
io-ai SecretsInterface work surfaced the problem in Musubi. The order flip has
landed in `src/Io/factories/ContainerFactory.php::moduleDefinitions()` and the
per-project audit script ships at `bin/audit-di-collisions.php`.

## Problem

`src/Io/factories/ContainerFactory.php::moduleDefinitions()` merges PHP-DI
definition files in this order:

```php
$definitionFiles = [
    ...glob($baseDir . '/module/*/services.php'),                  // HOST first
    ...Globstar::globStar($baseDir . '/vendor/apexl/**/services.php'), // VENDOR second
];
```

The merge is an array spread where the **last occurrence of a key wins**, so a
vendor package's definition of a key silently **shadows** any host app
definition of the same key. That inverts the universal convention (Drupal,
Symfony, PHP-DI's own docs): the application should always be able to override
a package.

Empirically confirmed in Musubi (io 3.6.10) and unchanged as of io 3.8.1:
host `module/shell/services.php` loads before `vendor/apexl/io-ai/services.php`,
so a host binding for a vendor-defined key never takes effect.

Two related PHP-DI facts that shape workarounds (verified empirically):

1. **Autowiring skips optional constructor params.** `?Foo $foo = null` never
   receives a container entry; the default is always used. So "vendor ships no
   binding, host binds the interface" only works if the consuming constructor
   param is *required* — and then hosts that define nothing fatal. A vendor
   default is needed, which reintroduces the shadowing.
2. Interim pattern now used by io-ai (1.0.7 / 1.2.1): the vendor binds the
   interface to a **factory that defers to a host-only string key**
   (`SecretsInterface::HOST_BINDING = 'ai.secrets'`). The vendor never defines
   that key, so the host definition can't be shadowed. Works, but every
   package needs to hand-roll it — the framework should just fix the order.

## Fix

Reorder to vendor-first, host-last in `moduleDefinitions()`:

```php
$definitionFiles = [
    ...Globstar::globStar($baseDir . '/vendor/apexl/**/services.php'), // VENDOR first
    ...glob($baseDir . '/module/*/services.php'),                      // HOST last (wins)
];
```

One-line change. Relative order *within* each tier (alphabetical glob order)
is unchanged, so vendor-vs-vendor and host-vs-host collisions resolve exactly
as before.

## Risk

Any project that (accidentally or deliberately) defines a key in a host module
that a vendor package also defines currently gets the **vendor** behaviour;
after the fix it silently gets the **host** behaviour. Deliberate reliance on
vendor-wins is unlikely (it's backwards), but accidental duplicate keys may
exist and must be audited per project before upgrading.

## Rollout

1. **Audit script** (ship in the release notes and/or `bin/`): loads every
   `module/*/services.php` and `vendor/apexl/**/services.php`, collects
   definition keys per file, prints host↔vendor duplicate keys with both file
   paths. Definition files only reference `::class` constants and closures —
   requiring them is side-effect-free with the autoloader available. Zero
   duplicates → safe upgrade; otherwise each collision is reviewed (usually:
   delete the stale host copy, or keep it knowing it now wins).
2. **Land the reorder** on V3 with a prominent CHANGELOG entry
   ("BEHAVIOUR: host services.php definitions now override vendor packages'").
   Version: **3.9.0** (minor — behavioural, no API change).
3. **Per-project upgrade** (Musubi, Newton Trailers, Town Centre, PMW, …):
   run the audit, resolve collisions, bump `apexl/io`, smoke.
4. **After adoption:** packages like io-ai keep the HOST_BINDING indirection
   for back-compat with pre-3.9 hosts; new packages can simply ship a default
   binding and document "override the interface key in your app". Musubi's
   `SecretsInterface::HOST_BINDING` usage keeps working unchanged either way.

## Non-goals

- No change to attribute-based injection, `coreDefinitions()`, or the
  builder's `useAttributes(true)`.
- No introduction of definition priorities/decorators — plain order flip only.
