ADR 0009: v1.0 Public API Scope and Stability Guarantee
Status
accepted
Context
NENE2 reached v0.7.0 with full CRUD example endpoints, MCP integration, JWT auth, and Packagist publication. The CHANGELOG.md carries the notice "breaking changes may occur between minor versions until v1.0.0."
Before cutting v1.0, the project needs to answer two questions:
- What is the "public API"? — which classes, interfaces, and constants does NENE2 commit to not breaking between major versions?
- What is
src/Example/? — framework core or reference implementation? The ADR 0006 decision deferred this to post-v0.2.x.
Options evaluated for src/Example/
| Option | Pros | Cons |
|---|---|---|
| Separate into its own repository | Clean library boundary | Two repos to maintain; MySQL integration tests depend on Note/Tag repositories; significantly higher solo-dev overhead |
| Keep in core, mark stable | No split cost | Pollutes the version signal — changes to example code would require major-version bumps |
| Keep in core, mark as reference implementation (unstable) | No split cost; tests stay intact; AI tools can read usage patterns from vendor | Requires explicit documentation that src/Example/ is not covered by the stability promise |
Scope of "public API"
The project uses semantic versioning. For v1.0+, a breaking change is any modification that:
- Removes or renames a public class, interface, method, or constant that application code is expected to reference.
- Changes a method signature in a way that breaks existing call sites.
- Changes the behavior of a method in a way that violates its documented contract.
Decision
1. src/Example/ stays in the core repository
src/Example/Note/ and src/Example/Tag/ remain part of the hideyukimori/nene2 package through v1.0 and beyond. Separating them into a standalone repository is deferred indefinitely unless team size or adoption patterns make it necessary.
Rationale: The MySQL integration tests in tests/Database/MySql/ use the Note/Tag repositories as their fixture layer. Splitting the repository would either duplicate the test infrastructure or create a cross-repository test dependency — both are worse than the namespace cost.
2. src/Example/ is explicitly outside the stability guarantee
src/Example/ is a reference implementation — code that demonstrates how to use the framework, not code that applications are expected to depend on directly.
- Changes to
src/Example/classes, method signatures, or DTOs do not trigger a major version bump, even after v1.0. - Application code should copy and adapt the example patterns, not import the example classes as library dependencies.
- This policy is analogous to a framework's scaffold generator: the generated code belongs to the application, not to the framework.
3. The stable public API surface
The following namespaces and their public members are covered by the v1.0 stability guarantee (no breaking changes without a major version bump):
| Namespace | Key public surfaces |
|---|---|
Nene2\Routing | Router (methods: get, post, put, patch, delete, handle, param), PARAMETERS_ATTRIBUTE, RouteNotFoundException, MethodNotAllowedException |
Nene2\Http | RuntimeApplicationFactory (constructor params and create()), JsonResponseFactory, ResponseEmitter, HealthCheckInterface, HealthStatus, JsonRequestBodyParser, JsonBodyParseException, PaginationQuery, PaginationQueryParser, QueryStringParser, ConditionalGetHelper, ConditionalWriteHelper |
Nene2\DependencyInjection | ContainerBuilder (set, value, addProvider, build), ServiceProviderInterface |
Nene2\Config | AppConfig (incl. $problemDetailsBaseUrl, $allowDevSecret, $demo), DatabaseConfig, AppEnvironment, ConfigException |
Nene2\Database | All *Interface types (DatabaseConnectionFactoryInterface, DatabaseQueryExecutorInterface, DatabaseTransactionManagerInterface), DatabaseConnectionException, DatabaseConstraintException |
Nene2\Error | DomainExceptionHandlerInterface, ProblemDetailsResponseFactory |
Nene2\Auth | TokenVerifierInterface, TokenIssuerInterface, TokenVerificationException, BearerTokenMiddleware, LocalBearerTokenVerifier (constructor accepts an optional Nene2\Http\ClockInterface $clock, default UtcClock — non-breaking addition; verify() evaluates exp/nbf against the injected clock so tests can pin time deterministically), CompositeAuthMiddleware, TotpAuthenticator, RecoveryCodes, GuardedJwtSecretResolver (constructor, resolve(), fromConfig()), JwtSecretException — see ADR 0013 |
Nene2\Middleware | All shipped middleware classes, RateLimitStorageInterface, ThrottleMiddleware |
Nene2\Validation | ValidationException, ValidationError |
Nene2\View | NativePhpViewRenderer, HtmlResponseFactory, HtmlEscaper, TemplateNotFoundException |
Nene2\Mcp | LocalMcpServer, LocalMcpToolCatalog, LocalMcpHttpClientInterface, LocalMcpHttpResponse, LocalMcpException |
Nene2\Testing | DatabaseTestKit (constructor, public readonly properties connectionFactory / queryExecutor / transactionManager, factories sqlite / fromConfig) — see ADR 0012 |
Nene2\Audit | AuditEvent, AuditQuery, AuditPayloadMode, AuditTableConfig (VOs/enum/config — stable), and the contracts AuditRecorderInterface, AuditRecorderFactoryInterface, AuditEventRepositoryInterface — see ADR 0014. Not stable: the concrete AuditRecorder, AuditRecorderFactory, PdoAuditEventRepository, and AuditServiceProvider (see decision 5) |
Nene2\Export | CsvWriter (constructor $stream / $headers / $bom / $sanitizeFormulas, methods writeRow, writeAll) — safe-by-default CSV writer: formula-injection neutralisation, RFC 4180 quoting, optional BOM, generator streaming. See ADR 0015 |
Nene2\Demo | The contracts DisposableOrgProvisionerInterface, DisposableOrgReaperInterface, DemoSessionSeaterInterface, DemoDataSeederInterface, DemoTemplateKeyInterface, DemoCapacityGuardInterface, DemoErrorPageRendererInterface; the VOs ProvisionedDemoOrg, DemoOrgRecord, SweepReport, DemoConfig; the exceptions SlugConflictException, DemoCapacityExceededException, DemoThrottledException; and the concretes StartDisposableDemoHandler (incl. TEMPLATE_PARAMETER), DisposableDemoSweeper, CountingDemoCapacityGuard, DemoRouteRegistrar (constructor accepts any PSR-15 RequestHandlerInterface since ADR 0018), MinimalDemoErrorPageRenderer (class name and constructor only — its rendered markup and copy are presentation, not contract) — see ADR 0017 / ADR 0018 |
4. Stable request attributes set by authentication middleware
Authentication middleware communicates identity to downstream handlers via PSR-7 request attributes. The following attribute names are part of the stable public API:
| Attribute | Set by | Value |
|---|---|---|
nene2.auth.credential_type | BearerTokenMiddleware, ApiKeyAuthenticationMiddleware | 'bearer' or 'api_key' |
nene2.auth.claims | BearerTokenMiddleware (after successful token verification) | array<string, mixed> — the JWT claims returned by TokenVerifierInterface::verify() |
Handlers read these attributes via $request->getAttribute('nene2.auth.credential_type') and $request->getAttribute('nene2.auth.claims'). The attribute names will not change in a backward-incompatible way within the v1.x line.
5. Explicitly outside the stability guarantee
Nene2\Example\*— reference implementation (see decision 2)Nene2\Audit\AuditRecorder/AuditRecorderFactory/PdoAuditEventRepository— default implementations; the*Interfacetypes, VOs, andAuditTableConfigare the stable boundary (ADR 0014)Nene2\Audit\AuditServiceProvider— reference wiring (copy and adapt, likesrc/Example/)Nene2\Log\*— internal logging plumbing; PSR-3 (LoggerInterface) is the stable boundaryNene2\Http\RuntimeServiceProvider— internal wiring; application code should not reference it directlyNene2\Config\ConfigLoader— internal; applications receiveAppConfig/DatabaseConfigDTOs- Concrete PDO adapters (
PdoConnectionFactory,PdoDatabaseQueryExecutor,PdoDatabaseTransactionManager) — implementation detail; the*Interfacetypes are the stable boundary tools/scripts — developer tooling, not part of the library APIdocs/mcp/tools.json— catalog for the shipped example; applications define their own
5. MCP stability caveat
LocalMcpServer and LocalMcpToolCatalog track the MCP specification, which is externally controlled. If the MCP protocol introduces a breaking change, NENE2 will adapt and document it as an exception to the breaking-change rule, noting the upstream cause.
Consequences
Benefits
- Application authors know exactly which surfaces they can depend on across major versions.
src/Example/can evolve freely as a living tutorial without creating version pressure.- The MySQL integration test suite remains intact with no cross-repository dependency.
Costs / follow-up work
- The
composer.jsondescription or README should note thesrc/Example/status so adopters understand when they install the package. - Future additions to the stable surface require a conscious decision (and ideally an ADR or at minimum a CHANGELOG entry).
- PHPDoc
@internalannotations should be added to classes in the "outside the guarantee" category to help IDEs surface the boundary.
Related
- Issue:
#334 - Supersedes: ADR 0006 (deferred
src/Example/decision) - See also:
CHANGELOG.md,docs/development/release-checklist.md