Skip to content

fix(hydra-gates): stop claiming OCA\OpenRegister\Contract\ as a runtime psr-4 prefix - #547

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/531-drop-contract-psr4-prefix
Aug 22, 2026
Merged

fix(hydra-gates): stop claiming OCA\OpenRegister\Contract\ as a runtime psr-4 prefix#547
rubenvdlinde merged 1 commit into
mainfrom
fix/531-drop-contract-psr4-prefix

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Blocked on the four consumer guards below. This composer.json reaches an app only when that app bumps to a release containing it — so merging early leaves the fleet one composer update from a bootstrap fatal.


Closes #531.

The package declared:

"autoload": { "psr-4": { "OCA\\OpenRegister\\Contract\\": "hydra-gates/contracts/" } }

That prefix is longer than openregister own OCA\OpenRegister\ to lib/, PSR-4 is longest-prefix-wins, and the package installs into every consumer vendor/ — so whichever app autoloader registered first defined OpenRegister contract for the whole process. Measured on a real instance: softwarecatalog vendored copy was supplying openregister own interface, and a v1.8.0 copy without patchObject() broke callers of the real one (#514).

The contracts still ship; consumers opt in from their own test bootstrap behind interface_exists(), which is order-independent. A fallback autoloader does not work — spl_autoload_register appends relative to registration order, and that order across independently loaded apps is exactly what nobody controls.

The migration is FOUR apps, not the fourteen #531 estimated

Measured by cloning all eighteen and grepping, rather than by code search (which rate-limits and returns unreliable zeros):

category apps
needs the opt-in buildiq, decidiq, filinq, stackiq
ships its own contract stubs dossiq, pipelinq, shillinq
owns the real one openregister — resolves from lib/Contract/ before and after
never references it 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 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 was not 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:

state result
before Tests: 715, Assertions: 2573, Errors: 131, Failures: 1
after Tests: 715, Assertions: 2993, Errors: 1, Failures: 0

The remaining error is unrelated (absent Symfony\Component\HttpFoundation\HeaderUtils).

Docs updated: hydra-gates/README.md gains the opt-in snippet with the two traps (put it before anything implementing the interface; put it in the bootstrap phpunit.xml actually loads — several apps ship two or three), and docs/hydra/operations/app-health.md records that the mechanism is gone.

…me psr-4 prefix

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.
@rubenvdlinde
rubenvdlinde merged commit 9801ffd into main Aug 22, 2026
36 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/531-drop-contract-psr4-prefix branch August 22, 2026 00:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hydra-gates claims OCA\OpenRegister\Contract\ as a RUNTIME psr-4 prefix, so any app vendor can define another app contract

1 participant