Skip to content

docs(ci-cd): how an app depends on OpenRegister (ADR-083 + ADR-084) - #466

Merged
rubenvdlinde merged 2 commits into
mainfrom
docs/adr-084-contract-and-gate-67
Aug 15, 2026
Merged

docs(ci-cd): how an app depends on OpenRegister (ADR-083 + ADR-084)#466
rubenvdlinde merged 2 commits into
mainfrom
docs/adr-084-contract-and-gate-67

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

The gap

Both ADRs shipped with the ADR in hydra/openspec and the reasoning inside the
checkers — and nothing in the CI/CD page an app developer actually reads. The
rules were enforced by two gates and written down nowhere, so the first time a
developer meets them is a red build.

What the page now says

ADR-083 — the four rules, enforced by gate-66. Inject an unconditional
dependency; never extends/implements an OpenRegister class; keep the default
route core-only; check the version floor but never install.

With the parts that are easy to get wrong:

  • the optional-capability exception, and that a guard has three valid
    spellings — ask the app manager, ask the autoloader, or try-and-degrade. A
    catch that rethrows is not a guard.
    Getting this wrong turns a clean
    "OpenRegister is not installed" message into a 500.
  • why a class header is categorically different from injection: it is fatal at
    autoload, and takes down the very route that would have explained the problem.
  • that OC\Installer is private API and bypasses admin consent — detect and
    inform, never install.

ADR-084 — type-hint the contract, gate-67. The interface, the
composition-root alias, why Nextcloud's autowiring needs it stated, and:

Keep the concrete import where the concrete NAME is still used.
ObjectService::class does not require the class to exist, so dropping that
import silently resolves the key to your app's own namespace and looks up
something nobody registered; instanceof becomes permanently false. Neither
errors at the point of the change.

"Two distribution paths in one package." hydra-gates now ships runtime
interfaces as well as gate scripts, and they reach an app differently:

what route needs a tag?
gate scripts fetched from .github@main no — live on merge
contracts/ composer, into vendor/ yes — apps pin ^1.0

Confusing the two cost a release: openregister bumped its lock specifically to
make gate-67 enforce, and the gate still said NOT APPLICABLE, because the
Hydra Gates job never runs composer install (fixed in #465).

Plus the package table row, which still claimed hydra-gates contains only the
gates and quality-config/.

Lessons recorded where they will be read

  • A gate that has only ever skipped has not been shown to work. NOT APPLICABLE is deliberately not a pass — the wording was right and the gate was
    still useless.
  • A rule derived from one observed idiom will mistake every other correct idiom
    for debt
    , at fleet scale and with a straight face. ADR-083's count went
    1263 → 1010 → 883 → 441; the 822-site difference was already-correct code.
  • The cost that motivated ADR-083, stated rather than asserted: gate-7 reported
    50 findings on pipelinq, every one a correctly-delegated endpoint, because
    a string literal is invisible to it — and a human review of the same code the
    same day concluded the opposite about who enforced authorisation.

Related

🤖 Generated with Claude Code

Conduction Release Bot added 2 commits August 15, 2026 15:56
ADR-084 landed with the ADR itself in hydra/openspec and the reasoning inside
the checker, but nothing in the CI/CD page an app developer actually reads. So
the rule — type-hint OCA\OpenRegister\Contract\ObjectServiceInterface, never the
concrete class — was enforced by a gate and written down nowhere.

Three additions:

* A section on consuming OpenRegister: the type-hint, the composition-root
  alias, why Nextcloud's autowiring needs it stated, and the trap that is easy
  to get wrong — keep the CONCRETE import where the concrete NAME is still used.
  `ObjectService::class` does not require the class to exist, so dropping that
  import silently resolves the key to the app's own namespace and looks up
  something nobody registered; `instanceof` just becomes permanently false.

* "Two distribution paths in one package". hydra-gates now ships runtime
  interfaces as well as gate scripts, and they reach an app differently: the
  scripts are fetched from `.github@main` and are live on merge, the contracts
  travel by composer and need a TAG plus a lock bump. Confusing the two cost a
  release here — openregister bumped its lock specifically to make gate-67
  enforce, and the gate still reported NOT APPLICABLE, because the Hydra Gates
  job never runs `composer install` and so has no vendor/ to read.

* The package table row, which claimed hydra-gates contains the gates and
  quality-config. It now also contains the contract.

The two lessons are stated where they will be read: a gate that has only ever
skipped has not been shown to work, and NOT APPLICABLE is deliberately not a
pass — the wording was right and the gate was still useless.
The previous commit documented what to type-hint but not when to inject at all,
which left the larger half of the rule in an ADR nobody reads before their build
goes red. ADR-083 and ADR-084 are now one section, because they are one decision
in two parts: rule 1 says inject the dependency, and it is unenforceable until
there is an interface to inject.

Adds the four rules with the reasoning that is easy to get wrong:

* the optional-capability EXCEPTION, and that a guard has three valid spellings
  — ask the app manager, ask the autoloader, or try-and-degrade. A catch that
  RETHROWS is not a guard. This is the part a reader is most likely to get
  wrong, and the part that turns a clean "not installed" message into a 500.
* why `extends`/`implements` against an OpenRegister class is categorically
  different from injection: a class header is fatal at autoload and takes down
  the route that would have explained the problem.
* the start-screen rule, and the version-floor rule (detect and inform; never
  install — `OC\Installer` is private API and bypasses admin consent).

And it records that the rule was too broad when first written and the gate
enforced it faithfully: 1263 → 1010 → 883 → 441 findings, the 822-site
difference being code that was already correct. A rule derived from one observed
idiom will mistake every other correct idiom for debt, at fleet scale.

Also names the cost that motivated the ADR rather than asserting readability:
gate-7 reported 50 findings on pipelinq, every one a correctly-delegated
endpoint, because a string literal is invisible to it — and a human review of
the same code the same day drew the opposite conclusion about who enforced
authorisation, and wrote it down.
@rubenvdlinde rubenvdlinde changed the title docs(ci-cd): the published OpenRegister contract, and gate-67 docs(ci-cd): how an app depends on OpenRegister (ADR-083 + ADR-084) Aug 15, 2026
@rubenvdlinde
rubenvdlinde merged commit b71881c into main Aug 15, 2026
35 checks passed
@rubenvdlinde
rubenvdlinde deleted the docs/adr-084-contract-and-gate-67 branch August 15, 2026 14:08
@rubenvdlinde
rubenvdlinde restored the docs/adr-084-contract-and-gate-67 branch August 19, 2026 16:45
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.

1 participant