ADR 0013: Freeze the stable surface and its versioning rules¶
Status: accepted
Context¶
ADR 0003 deferred one consequence to this phase: "Public
contracts require explicit compatibility/versioning decisions before 1.0.0." Phases 1–6 built six
documented surfaces and froze each one's behaviour in an ADR, but never answered the question a
user actually has before adopting any of it: which names are promised, and what happens when one has
to change?
An audit of the surface found the answer was partly accidental. Five documented modules declared no
__all__, so their public surface was whatever their imports left behind — Mapping, dataclass,
json, sys, and a private type variable were importable as API. Two package surfaces advertised
internals (xyberos.http exported PATH, REQUEST, HandlerParameter, and the error payload
helpers; xyberos.plugins exported PLUGIN_API_MAJOR). None of that was decided; it was drift, and
an internal refactor could have changed the public API without anyone noticing.
Decision¶
The public surface is what a guide names, or what a module's __all__ declares, and nothing else.
Every documented module declares __all__, and tests/test_public_surface.py freezes the exact set
per module. Adding or removing a name is therefore an edit to that file — a deliberate act with a
diff — rather than a consequence of moving an import.
Names that were never promised are removed from __all__ rather than frozen for compatibility:
the handler-compilation internals in xyberos.http, PLUGIN_API_MAJOR in xyberos.plugins. They
remain importable from the module that defines them, which is what internal code and tests use. This
is a 0.x change and is recorded as such.
Three tiers, defined by what is promised rather than by quality. Stable is the contract the
project intends to keep. Preview is shipped, documented, and tested but may still move as real use
reveals the right shape. Experimental carries stated non-guarantees that are part of its contract
and warns at its entry point. The tier of every surface is listed in
docs/stability.md, which is the user-facing version of this decision; this ADR
records the rules behind it.
Versioning follows PEP 440 spelling of SemVer concepts, starting at 0.0.0. A 0.x minor
release may break a stable name with upgrade notes; a patch release does not. 1.0.0 is reserved for
the first release that promises the stable tier. Four things are versioned independently of the
package and are surfaced in error messages rather than in the version number: the plugin API, the
configuration schema, Workflow definition versions, and the journal schema.
Later decision (release preparation). The rules above stand; the numbers they hang on changed. This line publishes as
2.0.0and its version base is2.x, because thexyberosname is held on the package index by the already-published 1.x line. ADR 0017 records why, and2.0.0takes over the role this ADR gives to1.0.0below. The distinction drawn here — that the leading digit is a promise about compatibility while the tier table is a promise about change — is unchanged, except that the leading digit now encodes lineage rather than maturity.
Deprecation is a process with mechanics, not an improvisation. A deprecated path warns, names its
replacement, and states the release that will remove it; it keeps working for at least one minor
release after the announcement. xyberos.deprecation provides warn_deprecated and a deprecated
decorator, and the warning class derives from DeprecationWarning so standard warning filters apply.
It ships before the first deprecation exists on purpose: inventing a warning style at the moment of
the first break is how inconsistent warnings happen.
Compatibility is tested, not asserted in prose. Four surfaces are pinned by tests: public imports, configuration fields, the plugin protocol, and serialized Workflow metadata. The fingerprint test is the sharpest, because the durable journal persists a definition fingerprint that gates resumption of in-flight executions: changing how it is computed invalidates work already in a journal, so a golden value turns that into a reviewed diff.
Consequences¶
1.0.0now means something specific: the release that starts promising the stable tier. Until then, "stability" is this project's declared intent plus tested pinning, not a release guarantee.- The tier of a surface is a promise about change, so it must be revisited when a preview surface becomes depended upon. Promoting a preview surface to stable is a deliberate decision, not a natural consequence of time passing.
- Freezing
__all__makes intentional additions slightly noisier (one test edit per change) and makes accidental ones impossible. That trade is the point. - The
deprecateddecorator wraps a callable, so it is appropriate for functions and methods. A removed attribute, a changed default, or a changed exception type cannot be deprecated by warning and must be documented as a breaking change instead. - Compatibility tests for configuration and the plugin protocol assert shapes, not behaviour; they cannot detect a change that keeps the same field names but alters their meaning.
- Two module-level constants remain in
xyberos.runtime.context(CORRELATION_ID_KEY,DEADLINE_KEY) that nothing outside the module uses. They are not public surface, and renaming them would be churn without benefit; they are recorded here so the next audit does not re-litigate it. - Phase 7's release notes and changelog now have a defined shape to write into: added, changed,
deprecated, removed, fixed, and security. Satisfied:
CHANGELOG.mduses exactly those sections, anddocs/release.mdnames them as the shape of a release entry.