---
title: "ADR-127: A marked, versioned API surface"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-127@0.35"
source: "Adr/Adr127ApiSurfaceMarkers.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-127: A marked, versioned API surface

-   *Status:* Accepted
-   *Date:* 2026-08-05

## 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:

1.  **\`\`@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.
1.  **\`\`@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.
1.  **\`\`@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](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-101@0.35)
    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.
