ADR 127 · Accepted
ADR-127: A marked, versioned API surface
Context
The roadmap asks for a "public versioned API surface". The extension already has most of the ingredients: ADR-028/065/101 govern which services are container-public, the Documentation/Api/ pages describe the consumer services, and semantic versioning is practised in releases. What is missing is the identity of the API: nothing in the code says which classes the semver promise covers. A downstream developer whose autocompletion offers AgentRunPersister next to CompletionServiceInterface has no signal that one is a contract and the other an implementation detail that may vanish in a minor release.
Decision
Every class-level docblock carries one of three markers, and the marker — not the container visibility, not the documentation — is the authority on what semver covers:
``@api`` — the consumer surface. Calling these is covered by semver: no removal, no signature break, no behavioural contract break within a major version. Membership is the signature-transitive closure of the entry points: every type that appears in an @api method signature is itself @api (the response objects, option classes, value objects and typed exceptions a caller necessarily touches). A hand-curated list inevitably drifts; the closure rule is checkable.
``@api Extension point`` (the marker's literal casing) — interfaces and attributes third parties implement rather than call (tool, guardrail, provider, translator, search-backend, preset, evaluation and middleware contracts). These carry a stricter promise, forced by the direction of implementation: no new abstract member within a major version, because adding one breaks every existing implementor, not just callers.
``@internal`` — everything else, explicitly. Controllers, widgets, hooks, upgrade wizards, commands, DI passes, form elements, repositories and the setup wizard may change without notice in any release.
What this deliberately is not
Not a phpat rule. phpat selects by namespace and inheritance, not by docblock tag, and cannot assert "signature types of @api methods are @api". The closure property is instead asserted by the API snapshot test (follow-up to this ADR): the snapshot renders every @api signature, so an out-of-closure type surfaces as an unmarked name in a rendered signature.
Not a change to container visibility. public: true in Services.yaml remains governed by ADR-028/065; ADR-101 remains the count authority. The two sets overlap but are not equal: a service can be container-public for a TCA itemsProcFunc (Category E) and still @internal, and a value object can be @api without being a service at all.
Not a compatibility promise for protected members. The promise covers what a consumer calls and what an implementor must provide. Subclassing internals of @api classes is out of contract.
Consequences
128 classes are @api (85 entry points and hand-verified members plus 43 added by running the closure to its fixpoint), 20 are extension points, and every previously unmarked class in the internal directories carries @internal (82 new markers; the rest existed). IDEs and PHPStan surface @internal usage from outside the package.
Documentation/Api/Stability.rst states the promise in consumer terms and is the first page of the API reference.
A follow-up PR adds the snapshot test that freezes the rendered @api signatures in-repo, so an unintended break fails CI before review.
New code must pick a marker at creation time; the snapshot test's file list makes an unmarked new public-namespace class visible in review.