Artifact Format Versioning¶
PyCharter persists several artifact documents — data contracts, concept
schemes, lineage records — that downstream consumers read and write. As pycharter
evolves, the shape of those documents can change. DG-2 adds a
format_version header to each persisted artifact plus a read-time registry
so only the current document shape is accepted, or a clear versioned error
is raised for anything else.
This is distinct from two existing mechanisms:
- Database schema migrations (alembic, under
db/migrations/) reshape tables. Format versioning validates documents. Alembic covers the SQL backends only — see Store backend support tiers for what that means on MongoDB / Redis. - Content versions (
scheme.version,field_bindings.version,KBVersion.version) track what changed in the data.format_versiontracks how the document is serialised.
The manifest envelope (current)¶
Every pycharter artifact file carries the unified manifest envelope (KM, 2026-07-12):
api_version: pycharter.io/v2 # the format marker (replaces format_version)
kind: StreamingWorker # DataContract | Pipeline | PipelinePlan | PipelineBundle |
# StreamingWorker | Bridge | ConceptScheme
metadata:
name: orders # identity slug
namespace: example # identity scope
version: "1.0.0" # content version
spec: # the kind body
streams: [...]
Identity is (metadata.namespace, metadata.name, metadata.version)
(KM-1, NS-1). For data contracts this is enforced end-to-end: the store
keys rows on it (uq_data_contracts_ns_name_version), the API accepts a
namespace parameter and returns 409 with the candidate namespaces
when a name-only lookup is ambiguous, and the editor threads the
namespace through load and save. A null/absent namespace means the
global namespace.
The artifact layer (NS-9). A contract composes five separately
stored artifacts. metadata_records is the contract's identity
record — keyed (namespace, name, version), in lockstep with the
contract row. The other four (json_schemas, coercion_rules,
validation_rules, field_bindings) form an immutable global
pool keyed (name, version) with no namespace: any contract may
reuse them by reference (*_name + *_version request fields; the
retired *_title spellings are rejected with a 422). Writing an existing
(name, version) with identical content is a no-op; different
content is rejected with a 409 (ArtifactContentConflictError) —
bump the version or pick another name. Inline-authored artifacts
derive namespace-prefixed pool names (logistics_order_coercion_rules)
so contract twins never collide by default. Pool artifacts record
their creator as nullable provenance (SET NULL on contract delete) —
deleting a contract never destroys artifacts other contracts or
versions reference; orphaned-artifact GC is an explicit non-goal.
Verdict-deciding sections. A contract version's data_quality.rules
and strict_types also decide whether data passes, so they follow the
same rule: they may be written once (a contract saved without rules can
gain them later), and a later write that changes them is rejected with
the same 409 (ArtifactContentConflictError); bump the version instead.
An update whose metadata omits strict_types keeps the stored value.
data_quality.targets (edited by the quality-threshold writeback), the
SLA sections, the policy sections and the rest of the metadata stay
editable at a version.
Serialization preserves supplied extraction, AI-usage, and KB-capture policy
mappings, including {}, without expanding their defaults. Absent and null
policies are omitted from canonical exports. Preserving an existing declaration
does not change the document format or any artifact version; no format migration
is required. Earlier parsed-contract exports could lose empty declarations, which
cannot be recovered from an already incomplete stored record. Re-export the
original reviewed configuration under the existing version/update rules; see
Retained pipeline components.
Current authored manifests use metadata.name as both identity and display
label; metadata.title is retired. Use the explicit migration command for
historical documents rather than authoring a second display/identity field.
PipelinePlan is a generated offline inspection artifact with the same envelope.
Its owning definition's metadata.version is distinct from the format version.
It has no historical format, store representation, or migration chain. The
migration CLI skips generated plans. Its scoped structural digest does not
identify executable content; see Offline pipeline plans
for the separate definition, bundle, deployment, and invocation identities.
PipelineBundle is an additive generated artifact for retained native components.
It shares the current envelope and likewise has no historical format, store
representation, or migration chain. Its reader requires explicit api_version
and kind; the migration CLI skips it, including explicit kind overrides.
Scoped component checksums identify admitted retained behavior, while the full
bundle_digest remains null and frozen/verified/executable claims remain false.
Generated bundles declare a closed inner capability scope. The original
supplied_native_components_v1 representation remains unchanged. Explicit
contract-file capture uses resolved_file_components_v1 and a required
capture: supplied_file_contract_v1 attachment variant, keeping authored
unknown pins separate from caller-expected and resolved identities. New readers
accept both scopes; older readers reject the new one. This additive inner mode
does not change the authored manifest format or artifact/constituent versions.
See Retained pipeline components for supported
rules, credentials, unresolved bindings, and execution limits. Pipeline,
contract, and constituent artifact versions retain their existing meanings.
The internal registry token for pycharter.io/v2 is km2. Per D8,
the runtime carries no backward-compatibility chains: an authored
file declares api_version: pycharter.io/v2 or loading fails with
UnsupportedFormatVersion. Historical formats and where they convert:
| Kind | Historical formats | How they reach km2 |
|---|---|---|
| Data contract | "1" (canonical sections) |
pycharter migrate only |
| Concept scheme | "1" (scheme: header + concepts:) |
pycharter migrate only |
| Pipeline | "1", "2", "3" (TH envelope) |
pycharter migrate only |
| Streaming config | "1", "2" (TH envelope) |
pycharter migrate only |
| Bridge config | "1", "2" (TH envelope) |
pycharter migrate only |
| Lineage record | "1" (stamped on write only) |
— |
(The former ConceptSchemeTemplate kind was retired; concept-scheme
templates are ordinary ConceptScheme manifests.)
Stores are envelope-free (KM-6): persisted store documents keep the
canonical section shape stamped format_version: "1"; the manifest is
the file format. The single runtime step per kind is the store-document
→ manifest wrap — not a legacy upgrade. Loaders normalize everything
through upgrade(kind, doc) followed by unwrap_manifest(kind, doc).
A camelCase apiVersion + kind: DataContract document is a foreign
ODCS document, not a pycharter manifest — it loads through the ODCS
codec (contract_io.from_odcs) anywhere a contract is accepted.
The TH-1 harmonized envelope (top-level format_version/id/version,
body at the root) is historical: the runtime rejects it, and
pycharter migrate rewrites files in place.
Read-time validation¶
pycharter._format_migrations holds the registry. Each artifact kind
declares a current version with read-time upgrade steps:
from pycharter._format_migrations import upgrade, current_version
from pycharter._format_migrations.manifest import unwrap_manifest
doc = upgrade("contract", raw_dict) # -> manifest (api_version stamped)
sections = unwrap_manifest("contract", doc) # -> canonical sections
current_version("contract") # -> "km2"
upgrade:
- reads
api_versionwhen present (elseformat_version; a document with neither is treated as the newest pre-manifest format, since manifests always declareapi_version), - wraps store-shape documents and stamps
api_version: pycharter.io/v2, - raises
UnsupportedFormatVersion(kind, saw, expected)for unknown versions or groups.
Loaders that consume the body models call unwrap_manifest after the
upgrade — the Pydantic body models validate the internal section shape
verbatim (KM-6), and unwrap(wrap(doc)) == doc is pinned per kind in
tests/unit/test_manifest_envelope.py.
Changing the document shape¶
When an artifact's body shape changes:
- Update templates, seeds, and serializers to the new shape.
- Bump the envelope group version for the kind
(
pycharter.io/v2) and register the read-time step inpycharter/_format_migrations/registry.py+ the converter chain insteps.py. - Rewrite repo artifacts with
pycharter migrate— the CI format ratchet (tests/unit/test_artifact_format_ratchet.py) fails until every file is current.
Breaking release: field bindings terminology (2026-05)¶
The following renames apply after pycharter db upgrade (revision
rename_field_bindings):
| Before | After |
|---|---|
Table field_mappings |
field_bindings |
Column data_contracts.field_mapping_id |
field_bindings_id |
Contract YAML field_mapping / ontology artifact |
field_bindings |
store.get_field_mapping / store_field_mapping |
get_field_bindings / store_field_bindings |
versions.field_mapping |
versions.field_bindings |
Legacy contract keys field_mapping and ontology at the top level are
rejected at parse time.
Stability commitment¶
The format_version header is part of pycharter's stable persisted contract
(see the Stability Policy). Within a major version,
artifacts must be written at the current format version.