Compatibility and deprecation policy¶
Chassis is pre-1.0. Until 1.0, a minor release may break the documented surface — and 0.9.0 is the last planned release with deliberate pre-1.0 compatibility changes. This page is the policy those releases follow: what is covered by compatibility, what may change, how an API is retired, and how persisted data is versioned.
Surface classes¶
Every documented import path and every name in chassis.__all__ belongs to
exactly one class:
| Class | Meaning | Compatibility promise |
|---|---|---|
| Stable public API | The documented surface: everything in chassis.__all__ and the module entry points listed in docs/README.md |
Pre-1.0: may break in a minor release, but only with a changelog entry and a migration note. From 1.0: breaking changes require a major release |
| Provisional API | New surface still settling (marked provisional in the API baseline) | May change or disappear in any release, with a changelog entry |
| Internal API | Everything else under src/chassis, including module-private names (_-prefixed) and undocumented modules |
No promise. It may change in a patch release |
| Persisted/serialized formats | Data written out and read back across a process, test run, deployment, or package-version boundary | Versioned explicitly (see below); supported for at least two minor releases |
Only documented names are public. Importing an internal module may work and is
not covered: chassis.plugins.registry internals, helper modules, and
_-prefixed names are free to change. Tooling is never made public to simplify
its own implementation.
The documented surface — the audit of every top-level export and documented
import path — is tests/compat/public-api.json, a machine-readable map of
module to name to stability class. It is the single source of truth: it drives
the public API tests (so an accidental addition to __all__ or removal from a
documented module fails the suite) and the compatibility checker below.
Provisional names are listed there with "stability": "provisional".
Everything not listed is internal by default. As of 0.9.0 every documented name
is stable; provisional is reserved for surface that must ship before it can
settle.
The API baseline and the compatibility check¶
The API baseline is a machine-readable description of a released surface:
documented module, name, callable signature, enum values, and public dataclass
or Pydantic fields, each with its stability class. It is generated from the
released implementation — never hand-edited — by running describe in an
environment where that release is installed:
# in an environment with chassis-harness 0.8.1 installed
python scripts/api_compat.py describe --write tests/compat/api-baseline-0.8.1.json
The same tool checks the current tree against a baseline:
uv run python scripts/api_compat.py check tests/compat/api-baseline-0.8.1.json
It exits non-zero and prints a deterministic, machine-readable report when it finds:
- a removed public name;
- a moved public name (gone from one documented module, present in another);
- an incompatible callable signature (removed/renamed parameter without a default, changed parameter kind, removed default where one existed);
- a changed enum value (removed member or changed member value);
- a changed public dataclass or model field (removed, renamed, or retyped).
Purely additive names and additive keyword parameters with defaults are
compatible changes; they still require a changelog entry and a stability
classification. tests/test_api_compat.py runs the same comparison in CI, so an
accidental addition, removal, or signature change fails the suite.
Deprecations¶
A stable public API is never removed silently. It is deprecated first, through
the typed ChassisDeprecationWarning (chassis.compat), which always states:
- the deprecated API;
- its replacement (or an explicit "no replacement");
- the version in which it became deprecated;
- the earliest version in which it may be removed.
from chassis.compat import ChassisDeprecationWarning, deprecated
@deprecated(since="0.9.0", remove_in="1.0.0", replacement="Harness.preview()")
def old_plan(self) -> object: ...
The warning is raised at the caller's frame, so application warnings filters
and logs point at the code that needs to change. Because it subclasses the
standard DeprecationWarning, Python hides it outside __main__ unless the
application opts in — for example python -W once::chassis.compat.ChassisDeprecationWarning
or a filterwarnings entry in pyproject.toml. The test suite runs with
warnings as errors, so an undeclared deprecation use fails CI.
The 1.0 deprecation window¶
- An API deprecated in 0.9.x may be removed no earlier than 1.0.0.
- Anything still needed at 1.0 must be marked deprecated before 1.0 ships; 1.0.0 removes only what this window covers.
- From 1.0.0 on, a stable public API is removed only in a major release, after at least one minor release carrying the deprecation warning.
- Provisional APIs may be removed in any release without a deprecation period; their removal is still listed in the changelog and migration notes.
0.9.0 deprecates nothing itself: the machinery and the window exist so that 1.0 can freeze a clean surface.
Persisted formats¶
Data that is written out and read back later is versioned per format family, with its own integer format version — never the package version, because a format's compatibility lifecycle is independent of the code's:
| Format family | Written by | Read by | Version constant |
|---|---|---|---|
| Runtime snapshot records | RuntimeSnapshot.to_dict() |
RuntimeSnapshot.from_dict() |
SNAPSHOT_FORMAT_VERSION |
| Replay recordings | ReplaySession.to_dict()/save() |
ReplaySession.from_dict()/load() |
REPLAY_FORMAT_VERSION |
| Declarative configuration | configuration documents | load_config/parse_config |
schema version: 1 |
| Planning exports | PlanResult.to_dict() |
documented shape | PLAN_FORMAT_VERSION |
| Reconciliation and diagnostics exports | ReconcileResult, ConfigApplyResult, GenerationPressureReport to_dict() |
documented shape | DIAGNOSTICS_FORMAT_VERSION |
The constants and the dispatcher live in chassis.persistence /
chassis.persistence.formats. In-process diagnostic views (status(),
plugins(), explain(), metrics() and friends) are deliberately not
versioned: they are read in-process from authoritative state and never written
out for later reading. A new document family that crosses a boundary must
declare a format version of its own.
Every serialized document declares its version. Readers dispatch explicitly:
- a known older version is migrated by a named migration step;
- the current version is read directly;
- a future version is rejected (
future_version); - a malformed version value is rejected (
malformed_version); - a corrupted payload is rejected (
corrupted); - a payload whose meaning cannot be recovered safely is rejected
(
unmigratable) — a missing migration step is a refusal, never a guess.
Rejections raise FormatError, whose structured context carries the
machine-readable format, reason, found, and supported values; the
meaning of an unknown version or field is never guessed.
What migrates from 0.8.1¶
A payload written by 0.8.1 declares no format version (it predates versioning) and is read as format 0, then migrated explicitly. Migration preserves semantic attribution exactly: generation identity and sequence, agent identity and revision, capability versions, replay boundary kind and key, redaction status, tool and model result semantics, and every attribution field.
What cannot be migrated: the semantic scope provider map of a runtime
snapshot. 0.8.1 recorded provider instance ids and persisted no
instance-to-entry mapping, so a migrated snapshot reconstructs the semantic
scope tree from the recorded topology and reports empty provider maps rather
than inventing identity. semantic_digest() of a migrated record therefore
covers the reconstructed view; compare digests within one format version.
Sanitized fixtures produced by the released 0.8.1 implementation live in
tests/compat/v0.8.1/, and tests/test_format_compat.py asserts this contract
against them field by field.
Support horizon. A persisted format version stays readable — directly or by migration — for at least two minor releases after the release that superseded it. Within 0.9.x, artifacts written by 0.8.1 are supported as described above and in migration.md; 1.0.0 will state the first permanent format baseline.
What compatibility does not cover¶
- Behavior of internal modules and undocumented names.
- Exact wording of error messages, prose explanations, and text renderings
(
to_text()); branch on types and reason codes instead. - Digest values across package versions: a digest covers a record that includes
the producing
chassis_version, so digests are comparable within one version. Semantic comparison across versions usessemantic_digest(). - Timing, scheduling, and ordering of concurrent runs beyond the documented determinism of resolved composition (G12).