From b6ccbdc56c7ac1ce981647b6569d3fe71388f2eb Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Sat, 22 Aug 2026 01:51:51 +0200 Subject: [PATCH] fix(hydra-gates): stop claiming OCA\OpenRegister\Contract\ as a runtime psr-4 prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package declared "autoload": { "psr-4": { "OCA\\OpenRegister\\Contract\\": "hydra-gates/contracts/" } } That prefix is LONGER than openregister's own `OCA\OpenRegister\` -> `lib/`, PSR-4 is longest-prefix-wins, and the package installs into every consumer's vendor/ — so whichever app's autoloader registered first defined OpenRegister's contract FOR THE WHOLE PROCESS. Measured on a real instance: softwarecatalog's vendored copy was supplying openregister's own interface, and a v1.8.0 copy without patchObject() broke callers of the real one (#514, #531). The contracts still ship. A consumer that needs them now requires them from its own test bootstrap behind interface_exists(), which is order-independent — it asks whether the interface is RESOLVABLE rather than who registered first. Appending a fallback autoloader does NOT work: spl_autoload_register appends relative to registration order, and that order across independently loaded apps is precisely what nobody controls. THE MIGRATION IS FOUR APPS, NOT THE FOURTEEN #531 ESTIMATED. Measured by cloning each of the eighteen and grepping, rather than by code search (which rate-limits and returns unreliable zeros): needs the opt-in buildiq, decidiq, filinq, stackiq ships its own dossiq, pipelinq, shillinq owns the real one openregister — resolves from lib/Contract/ before AND after never references the remaining ten Each of the four is verified both directions — prefix present (guard no-ops) and prefix removed (guard supplies) — by editing the vendored package's entry in vendor/composer/installed.json. Editing the vendored composer.json does nothing; Composer reads installed.json, which cost one invalid experiment before I noticed. stackiq turned out not to be preparation at all: its bootstrap-unit.php maps only `OCA\OpenRegister\Db\` and `...\Service\`, never `...\Contract\`, so its standalone unit suite was ALREADY failing on this — 131 errors before the guard, 1 after (and that one is an unrelated absent Symfony class). ⚠️ DO NOT MERGE BEFORE THOSE FOUR. This composer.json reaches an app only when it bumps to a hydra-gates release that contains it, so the guards must be in first — but merging this before them puts the fleet one `composer update` away from a bootstrap fatal. --- composer.json | 5 ---- docs/hydra/operations/app-health.md | 17 ++++++++--- hydra-gates/README.md | 46 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 6cb96acd..3d185aea 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,6 @@ "bin": [ "hydra-gates/bin/hydra-gates" ], - "autoload": { - "psr-4": { - "OCA\\OpenRegister\\Contract\\": "hydra-gates/contracts/" - } - }, "require": { "php": ">=8.1" }, diff --git a/docs/hydra/operations/app-health.md b/docs/hydra/operations/app-health.md index 850cffd3..6975abca 100644 --- a/docs/hydra/operations/app-health.md +++ b/docs/hydra/operations/app-health.md @@ -287,10 +287,19 @@ day. Neither number was a decision; both were the absence of one. The cost was not theoretical. hydra-gates v1.8.0 shipped an `ObjectServiceInterface` **without `patchObject()`**, and because the package -claims `OCA\OpenRegister\Contract\` in its composer autoload — a *longer* -psr-4 prefix than openregister's own — a stale copy in **any** app's vendor -directory defined that contract for the whole instance. Measured: -softwarecatalog's vendor was supplying openregister's interface. Separately, +*used to claim* `OCA\OpenRegister\Contract\` in its composer autoload — a +*longer* psr-4 prefix than openregister's own — a stale copy in **any** app's +vendor directory defined that contract for the whole instance. Measured: +softwarecatalog's vendor was supplying openregister's interface. + +> ✅ **That mechanism is gone.** The prefix was removed from the package +> (ConductionNL/.github#531); the contracts still ship, but a consumer now opts +> in from its own test bootstrap behind `interface_exists()`, which is +> order-independent. Four apps needed the opt-in — buildiq, decidiq, filinq and +> stackiq. The others either ship their own contract stubs (dossiq, pipelinq, +> shillinq), own the real one (openregister), or never reference it. + +Separately, nc-vue's AI-companion singleton landed in 2.7.0, so every app below it rendered a **second** companion hex beside hermiq's on every page. Both fixes had existed upstream for weeks. diff --git a/hydra-gates/README.md b/hydra-gates/README.md index 5f346332..b2aa053b 100644 --- a/hydra-gates/README.md +++ b/hydra-gates/README.md @@ -146,6 +146,52 @@ gap, not a pass — see *Reading a green*. --- +## The shipped OpenRegister contracts are opt-in, not autoloaded + +`hydra-gates/contracts/` ships `ObjectServiceInterface` and +`ObjectEntityInterface` so a leaf app can typehint OpenRegister's data-access +surface under PHPUnit without the OpenRegister app installed. + +They are **not** autoloaded. This package used to declare + +```json +"autoload": { "psr-4": { "OCA\\OpenRegister\\Contract\\": "hydra-gates/contracts/" } } +``` + +and that was a bug, not a convenience. The prefix is *longer* than +openregister's own `OCA\OpenRegister\` → `lib/`, PSR-4 is longest-prefix-wins, +and the package installs into every consumer's `vendor/` — so whichever app's +autoloader registered first defined OpenRegister's contract **for the whole +process**. Measured on a real instance: softwarecatalog's vendored copy was +supplying openregister's own interface, and a v1.8.0 copy without +`patchObject()` broke callers of the real one (ConductionNL/.github#531). + +A consumer that needs these interfaces requires them from its own test +bootstrap, behind a guard: + +```php +foreach (['ObjectEntityInterface', 'ObjectServiceInterface'] as $contract) { + if (interface_exists('\\OCA\\OpenRegister\\Contract\\' . $contract) === false) { + $shipped = __DIR__ . '/../vendor/conduction/hydra-gates/hydra-gates/contracts/' . $contract . '.php'; + if (file_exists($shipped) === true) { + require_once $shipped; + } + } +} +``` + +Put it **before** anything that implements the interface — a stub entity that +`implements \OCA\OpenRegister\Contract\ObjectEntityInterface` fatals inside the +bootstrap otherwise, which is a dead run rather than a failed test. And put it +in the bootstrap `phpunit.xml` actually loads: several apps ship two or three. + +`interface_exists()` rather than a fallback autoloader, because +`spl_autoload_register` appends relative to *registration order*, and +registration order across independently loaded apps is exactly the thing nobody +controls. Asking whether the interface is resolvable is order-independent. + +--- + ## What it needs at runtime `bash`, `git`, `python3` (about twenty gates are Python helpers) and `node`