API stability policy (Phase E8)¶
Every public Ophamin symbol carries an explicit stability tier. The contract between Ophamin and downstream consumers is encoded in code via decorators in
src/ophamin/_stability.py, pinned at PR time via the regression suite intests/test_api_stability_contract.py, and auditable from a user codebase via theophamin api-stabilityCLI command. This page is the consumer-facing summary.
The four tiers¶
| Tier | What it means | Allowed changes at minor | Allowed changes at major |
|---|---|---|---|
| Stable | Semver-backed public API. The default tier for every public symbol. | Add optional parameters with defaults; tighten internal implementation that doesn't change observable behaviour. | Rename, remove, change parameter kinds — but only via a deprecation cycle. |
| Provisional | Public but subject to change. Useful for letting consumers exercise an API before its shape is frozen. | Anything except wholesale removal. | Anything, including removal. |
| Internal | NOT part of the public API. Consumers MUST NOT import these. | Anything. | Anything. |
| Deprecated | Scheduled for removal at removal_version. Calling the symbol emits a DeprecationWarning exactly once per process. |
Stays callable; behaviour preserved until removal. | Removed at the documented removal_version. |
Mutually exclusive: a symbol is Stable OR Provisional OR Internal at any one time. Deprecated composes with one of those (typically a previously-Stable symbol becomes Deprecated; never a Provisional or Internal one).
Cross-reference to the schema policy¶
The Python-API stability contract above is the runtime counterpart
of the wire-format policy in SCHEMAS.md.
Both follow the same shape:
| Runtime contract (this doc) | Wire-format contract (SCHEMAS.md) | |
|---|---|---|
| Stable surface | @Stable symbols |
CampaignRecord/2.0, EmpiricalProofRecord/1.0, etc. |
| Forward-additive at minor | New optional parameter | New optional field |
| Breaking at major | Rename / remove | Remove / rename |
| Deprecation cycle | @Deprecated(removal_version=...) |
Document in "Deprecated fields" row + 1+ minor of read-compat |
| Reader robustness | Default values for new optional params | from_dict defaults absent fields |
Interop-layer stability¶
The five interop layers shipped at 0.16.0–0.21.0 (cross-language
wire-format ports, MCP server, HTTP REST API, CloudEvents wrapper,
OpenTelemetry instrumentation) each have their own stability surface
that sits on top of the runtime + wire-format contracts above:
| Layer | @Stable surface |
@Provisional surface |
|---|---|---|
| Wire-format ports (Rust + JS) | Public exports listed in each port's lib.rs / src/index.ts — parseProof, verifySignature, canonicalBytes, signCanonical, CanonicalValue enum, PyInt wrapper |
Internal module layout under crates/ophamin-proof/src/** + packages/ophamin-proof-js/src/** |
| MCP server | The six tool names + their argument schemas (list_scenarios, get_scenario_claim, verify_proof, canonicalize_value, read_proof_index, run_scenario) |
Transport choice (stdio vs SSE vs streamable-http); server bootstrap internals |
| HTTP REST API | Endpoint paths + request/response body shapes (/health, /version, /scenarios, /scenarios/{name}/claim, /scenarios/{name}/run, /canonicalize, /verify, /proofs/index) |
Underlying FastAPI app object identity; middleware order |
| CloudEvents wrapper | Envelope attribute names emitted by wrap() (specversion, type, source, id, time, datacontenttype); wrap() / unwrap() Python signatures |
Default type value naming; per-extension future fields |
| OpenTelemetry instrumentation | Span names (ophamin.scenario.run.<name>, ophamin.proof.verify, ophamin.canonical.encode), span attribute names, metric names (ophamin_scenarios_run_total, ophamin_scenario_duration_seconds, ophamin_proofs_verified_total, ophamin_canonical_bytes_encoded) |
Metric internals (histogram bucket boundaries, exemplar policy) |
A drift in any @Stable interop surface is a major-version bump
with a documented migration path. The single-page on-ramp for any
external consumer reviewing the full surface is
INTEROP_OVERVIEW.md.
Auditing your own codebase¶
If you depend on Ophamin, run:
Reports every reachable import of an @Deprecated or @Internal
Ophamin symbol, with line numbers + removal-version metadata for the
deprecated ones. Exit code 0 = clean, 1 = at least one violation
(suitable for CI gates in downstream projects).
To list every annotated public symbol grouped by tier:
JSON output for tooling:
What the framework guarantees¶
Per the regression suite at tests/test_api_stability_contract.py:
- Every load-bearing public symbol carries a stability annotation. The test fails loud at PR time if a public symbol drifts un-tagged.
- Stable callables' signatures are pinned. Adding optional
parameters at the end with defaults passes; renames, removals,
kind changes (positional → keyword) fail. When a Stable API
genuinely extends at minor, regenerate the pin via the
OPHAMIN_REGENERATE_API_PINS=1env var (workflow documented at the bottom of the test file). StabilityInfoinvariants are enforced at construction time. You can't accidentally tag a Stable symbol with aremoval_version(would only make sense forDeprecated).
When you find a deprecation in your code¶
A @Deprecated(removal_version=..., replacement=...) symbol carries
its own migration breadcrumb:
ophamin.foo.old_function is deprecated; scheduled for removal at
version 1.0.0. Use ophamin.foo.new_function instead.
Migrate before removal_version lands. If the deprecation needs more
context than the warning message captures, check the CHANGELOG entry
for the release that introduced the @Deprecated tag.
Reference¶
- Decorator implementation:
src/ophamin/_stability.py - Pinning test suite:
tests/test_api_stability_contract.py - CLI handler:
cmd_api_stabilityinsrc/ophamin/cli.py - RFC: RFC 0002 §3.2 Phase E8