clio-schemas
The single source of truth for the record shapes shared across the CLIO
system. clio-agent, clio-relay, and gact-tui all speak the same wire
records — artifact versions/chains, transform provenance records, the closed GACT 0.3
message-block union, and the official A2UI 0.9.1 protocol shapes (envelopes,
capabilities, catalog files). Historically
each service hand-wrote its own copy of these types and
they drifted. This package makes the shapes canonical: they are defined once as
pydantic v2 models here and exported to JSON
Schema. Python consumers import the models; TypeScript consumers generate
types from the JSON Schema shipped inside the package. Nobody hand-writes a
shared shape again.
Status: canonical records, live UI vocabularies, and A2UI 0.9.1 catalog files (version 0.3.2).
ArtifactVersion,ArtifactRecord,ProvEdge,TransformRecord, the 13 GACT 0.3 message blocks, the official A2UI 0.9.1 envelope/capability/catalog-file models, and the two builtin catalog files (clio-workspace's 30 CLIO components, the vendoredbasiccatalog) are canonical. There is no closed component/action Python union anymore — a catalog is a JSON Schema document, validated withclio_schemas.a2ui.validation(jsonschema+referencing), not a pydantic discriminated union.
Why the schemas are immutable package resources (design decision)
An exact clio-schemas pin, on its own, does not guarantee two services see
the same schema bytes: if each consumer regenerated JSON Schema from the
pydantic models under its own resolved pydantic version, a pydantic point release
could subtly change the output and drift would return through the back door.
So the canonical JSON Schemas are built once, committed, and shipped as
immutable package resources inside the wheel (clio_schemas/schemas/*.json),
alongside a canonical hash manifest (HASHES.json). Consumers copy those
committed bytes — they never regenerate. An exact pin therefore does determine
the bytes. Regenerating from the models is a repo-local developer command
(--regenerate), gated to the single locked pydantic version, so the
committed artifacts can only change deliberately, in this repo.
What's in the box
clio-schemas/
├── pyproject.toml # uv-compatible, py>=3.12, pydantic v2, hatchling
├── src/clio_schemas/
│ ├── __init__.py # public exports + __version__
│ ├── constants.py # LOCKED_PYDANTIC_VERSION, file names
│ ├── models.py # canonical models + ClioSchemaBase + registry
│ ├── export.py # copy / check / regenerate / verify
│ ├── py.typed # ships type information
│ ├── a2ui/ # A2UI 0.9.1: official shapes + catalog rendering
│ │ ├── v0_9_1/ # messages, capabilities, data model, catalog file,
│ │ │ # the 30 CLIO component models (components.py /
│ │ │ # bounded_components.py)
│ │ ├── sidecar.py # CLIO catalog packaging metadata (never on the wire)
│ │ ├── validation.py # jsonschema + referencing validators
│ │ ├── catalog_render.py # canonicaliser: pydantic spec -> official JSON Schema
│ │ ├── catalog_bounded.py # hand-authored map/time-series/workflow definitions
│ │ └── catalog_export.py # assembles + renders a2ui/catalogs/**
│ └── schemas/ # COMMITTED immutable artifacts (in the wheel)
│ ├── artifact_version.json # per-model, self-contained
│ ├── artifact_record.json # per-model, self-contained
│ ├── transform_record.json # plus provenance/value model schemas
│ ├── index.json # aggregate: shared $defs emitted once
│ ├── HASHES.json # canonical sha256 manifest (covers a2ui/catalogs/** too)
│ └── a2ui/
│ ├── v0_9_1/ # VENDORED spec + basic catalog (SOURCE.json-tracked)
│ └── catalogs/ # RENDERED: clio-workspace/v1 + basic sidecars
├── tools/ts-gen/
│ ├── schemas-to-ts.mjs # JSON Schema dir -> TS module graph (deterministic)
│ ├── tsconfig.check.json # isolated strict typecheck of generated TS
│ └── package.json / package-lock.json
├── scripts/check_version_bump.py # CI: schema change ⇒ version bump
├── tests/
│ ├── test_roundtrip.py # JSON/TS golden anti-drift tests
│ ├── test_legacy_parity.py # original clio-agent byte fixtures
│ ├── test_version_bump.py # unit tests for the bump-enforcement core
│ └── golden/ # committed golden TS directory
│ ├── _models.ts index.ts artifact_version.ts transform_record.ts ...
└── ci-drafts/ # workflow drafts for this repo + both consumers
The pipeline
pydantic models ──render (locked pydantic)──▶ committed *.json + HASHES.json ──ship in wheel──▶
consumer copies bytes ──schemas-to-ts.mjs──▶ TypeScript module graph
Every hop is deterministic: JSON is emitted with sorted keys and a stable
$defs order; the TypeScript generator uses a fixed banner (no timestamp),
sorted file order, and pinned formatting. The aggregate index.json carries all
models under a single shared $defs, so the generator emits each shared
definition (e.g. the ArtifactKind enum and nested IdentityEvidence) exactly
once — the generated module graph has no duplicate declarations.
Export command modes
# Consumer: copy the immutable committed schemas into ./schemas
uv run python -m clio_schemas.export --out schemas
# Consumer CI: verify a directory matches the committed bytes exactly
# (rejects stale, missing, AND unexpected/orphaned files)
uv run python -m clio_schemas.export --out schemas --check
# DEV: re-render the committed package resources from the models
# (refuses unless the installed pydantic == LOCKED_PYDANTIC_VERSION)
uv run python -m clio_schemas.export --regenerate
# REPO CI: assert committed artifacts are canonical (match models + hashes)
uv run python -m clio_schemas.export --verify
Quick start
uv sync --extra dev # install (locked pydantic)
uv run python -m clio_schemas.export --verify # artifacts are canonical
uv run pytest # golden round-trip + conformance
cd tools/ts-gen && npm ci && npm run check # generate TS + strict typecheck
Legacy extraction compatibility
The P2.1 artifact/provenance records use LegacyToleranceBase so their
clio-agent behavior remains byte- and validation-compatible: unknown keys are
ignored, ordinary Pydantic coercion remains enabled, immutable records stay
frozen, and ArtifactRecord stays mutable. Moving existing records to the
strict ClioSchemaBase contract would be a wire change; that coordinated
convergence is tracked in iowarp/clio-agent#1121.
How to add a model
- Define the pydantic v2 model in
src/clio_schemas/models.py, inheritingClioSchemaBase(strict wire semantics:extra="forbid",strict=True,frozen=True). Give every field aField(description=...)— descriptions flow into the JSON Schema and the generated TS doc comments. - Append the class to the
EXPORTED_MODELStuple in the same file. - Regenerate the committed artifacts and the golden TS, then bump the version:
uv run python -m clio_schemas.export --regenerate # updates schemas/ + HASHES.json cd tools/ts-gen && node schemas-to-ts.mjs \ --in ../../src/clio_schemas/schemas --out ../../tests/golden
- Bump
__version__insrc/clio_schemas/__init__.pyandversioninpyproject.tomlin lockstep (CI enforces a bump wheneverHASHES.jsonchanges), thenuv run pytest.
Versioning policy — exact-pin lockstep
Consumers pin an exact version (clio-schemas==X.Y.Z, not >=). A schema is
a contract between multiple services; a range would let two services resolve
different shapes and reintroduce drift. Therefore:
- Any change to a committed schema (detected via
HASHES.json) requires aclio-schemasversion bump — enforced mechanically in CI byscripts/check_version_bump.py, which compares the hash manifest against the merge base and fails if the version was not incremented. - All consumers (
clio-agent,clio-relay,gact-tui) update their pin to the new exact version in lockstep, in the same coordinated change. - The version lives in exactly two places that must agree:
pyproject.tomlversionandclio_schemas.__version__; CI verifies they match.
Wire evolution
Exact-pin lockstep is the bootstrap rule. As the real records land and the system runs mixed versions during rollouts, the intended evolution discipline is:
- Additive, reader-first staging. Additive changes (new optional field, new
enum member) ship to readers before writers: deploy the version that can
accept the new shape everywhere first, then deploy the writers that emit
it. Readers must ignore-or-tolerate unknown-but-optional additions during the
transition. Extracted P2.1 records preserve their legacy reader-tolerant
behavior through
LegacyToleranceBase; new records useClioSchemaBase. - N / N-1 compatibility. Once real schemas exist, adjacent versions are expected to interoperate: a service on version N and one on N-1 must be able to exchange the records they share for the duration of a rollout. Breaking (non-additive) changes require a two-step migration across at least one intermediate version, never a flag-day.
- Rollback ordering. Roll back in the reverse of deploy order: retire the writers of a new shape before the readers that understand it, so a rolled -back writer never emits a shape an already-rolled-back reader would reject.
Convergence of the extracted records on stricter validation is tracked by iowarp/clio-agent#1121 and requires a coordinated consumer migration.
How consumers regenerate
- clio-agent (Python): pin
clio-schemas==X.Y.Z,import clio_schemas. Its CI copies the committed schemas out of the installed package and--checks any vendored copy for exact-byte + file-set equality (seeci-drafts/clio-agent-schema-check.yml). - clio-relay (Python): same as clio-agent — import the models, pin exact.
- gact-tui (TypeScript): pin
clio-schemas==X.Y.Z, copy the shipped JSON Schema out of the package, and run the in-repo generator (tools/ts-gen, pinned via its lockfile) to produce.ts. Its CI regenerates into a clean dir and compares exact file sets + bytes so untracked/orphaned files are caught (seeci-drafts/gact-tui-ts-gen.yml).
Release files for clio-schemas 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| clio_schemas-0.3.2.tar.gz | 142.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clio_schemas-0.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 246.3 kB
Release files / clio_schemas-0.3.2.tar.gz
| Download URL | clio_schemas-0.3.2.tar.gz |
|---|---|
| Size | 142.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
072672ec4fc5e870500613c396547a3b38f74d07f5426ca3843b656886341ec3
|
|
BLAKE2b-256 checksum How to use checksums |
f6d59ea5fea296328d5e647530c5ccf18354f7813445de19939806acbcdfad83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / clio_schemas-0.3.2-py3-none-any.whl
| Download URL | clio_schemas-0.3.2-py3-none-any.whl |
|---|---|
| Size | 103.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ed1ddb5cd9d354ea8f0b1911011369391323498d0a2fe8937c78d54fc2e30992
|
|
BLAKE2b-256 checksum How to use checksums |
03702144bccdb17cc5808fabbf2172336bb678eeff4752cb9d9be3919ec94b38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|