AGENTS.md is the single repository instruction file for all coding agents. Update it directly; do not create a separate agent-specific guide.
.specify/memory/constitution.md is this repository's highest-authority engineering policy; read it before changing primitives, serialization, dependencies, CI, or publication. Spec Kit skills are installed for Codex in .agents/skills/ and per machine for Claude in .claude/skills/.
astralprims: composable, serializable UI primitives for Python. Each primitive is a
pydantic-v2 model that validates on construction and serializes to a plain wire dict. JSON
is the wire format; pydantic is only the authoring layer.
This package is the "define" stage of AstralDeep's server-driven UI: AstralPrimitives defines → AstralProjection renders and adapts → AstralDeep orchestrates. It is consumed there as an ordinary pip dependency, and it releases on its own train.
Start with these files:
src/astralprims/base.py—Primitive(the base),SerModel(nested non-primitive helpers), the_REGISTRYtype→class map, andPrimitive.from_dict.src/astralprims/primitives.py— the concrete primitive definitions; derive the current vocabulary from their type literals.src/astralprims/__init__.py— public surface,__version__, and theAnyPrimitivediscriminated union +primitive_adapter.src/astralprims/response.py—create_ui_response().tests/test_primitives.py— serialization shape, nesting,from_dictround-trips, adapter validation.
Dependencies stay thin on purpose: pydantic>=2, requires-python >=3.9, hatchling.
The code documents itself (Constitution Principle VI). Each file opens with a header of at most three sentences on what it does and how it connects to other files. Add no other comments or docstrings except a one-line why where absolutely necessary; primitive documentation lives in README.md. No spec IDs, history, TODOs, or narration in source. Tool directives (# noqa, # type: ignore, # pragma: no cover) stay.
pip install -e ".[dev]"
pytest.github/workflows/ci.yml qualifies pull requests with locked tooling, including
branch and changed-line coverage, both distribution formats, and a clean-install smoke test.
It compares shipped Python behavior and released exports with the immutable event base,
checks independent wire fixtures, and proves focused tests catch maintained broken guards.
The test/qualification review receipt is diagnostic evidence for maintainer review.
.github/workflows/python-publish.yml runs only on push to main; its unprivileged job
tests and builds before the environment-protected publisher receives OIDC and uploads the
verified artifact. It asks PyPI whether the current pyproject.toml version already exists
and uploads only if it does not (OIDC trusted publishing; no stored token).
Two consequences that bite:
versioninpyproject.tomlis the sole release marker. There are no git tags. Merging a primitive does not ship it — the bump does. Bump in the same PR that adds it.- Publication does not replace PR qualification. The release workflow repeats the locked tests and package checks without OIDC before its isolated publisher runs.
Consumers pin their own package/component revisions. Check their current manifests and installed package versions before importing a newly added class; a running image can lag this repository. New wire types may use an approved plain dict until the matching package is installed. A documentation-only change does not require a package version bump.
Primitive._serialize is a @model_serializer(mode="plain"), so .to_dict() /
.model_dump() bypass pydantic's default serializer and emit exactly this shape:
"type"first, from the subclass'sLiteraldefault.Nonefields are dropped.- An empty
cssblock is omitted (onlycss— an emptypayloadstill appears). class_nameserializes as"class";populate_by_namelets you construct with either.- Nested models serialize themselves —
children,content,tabsrecurse. attributesis merged last, at the top level.
Gotchas:
to_dict()returns a dict;to_json()returns a string. Passing the latter where a component dict is expected is the classic error.- The styling field is
css(kebab-case CSS properties), neverstyle. - Because
attributesmerges last, it can silently overwrite any field, includingtype. It is a trusted-input escape hatch — do not splat user data into it. model_configsetsextra="ignore", so a typo'd constructor kwarg is silently dropped (Button(labl="x")raises nothing). Note the asymmetry:from_dictfunnels unknown dict keys intoattributes, but the constructor discards unknown kwargs.Gridis an alias for the classGrids, whose wiretypeis"grid".
Subclassing is registration: __pydantic_init_subclass__ validates the type field and
registers _REGISTRY[default] = cls. So:
- Pick a
typestring that does not collide with a built-in or existing extension. Collisions fail fast withPrimitiveTypeCollisionErrorbefore modifying the registry. - Custom primitives must specify a default string for
typethat matches anyLiteralannotation. - The union snapshots
_REGISTRYat import time. After defining a custom primitive, callrebuild_primitive_union()orprimitive_adapterwill not see it. - Extension lifecycle: Subclasses register automatically upon definition. Extensions can remove
custom types using
unregister_primitive(type_name). Deliberate aliases (such asGrid = Grids) are preserved. Re-registering the same class object is permitted, but a module reload creates a different class object and is rejected as a collision; unregister the type before reloading. Failed registration leaves the registry and existing adapter unchanged. - Adding a primitive to the Astral vocabulary additionally requires the owner approval,
documentation, and coordinated changes of Constitution Principle IV: AstralProjection's
contracts/ui_protocol.json, renderers, ROTE, affected clients, and drift guards, followed by the consuming AstralDeep composition pin. Keep the repositories' release trains distinct; a package-only change does not update a deployed client. - Update the primitive table in
README.md. It has drifted before.
Use the kos-wiki repository supplied by the current workspace, not historical absolute
paths. Read its AGENTS.md and index.md before editing. Relevant curated pages are:
wiki/astral-primitives.md— package vocabulary and release model.wiki/astral-primitive-serialization-contract.md— serialization rules.wiki/astral-sdui-pipeline.md— ownership and cross-repository flow.
Refresh the vault at major checkpoints, especially a version bump, a primitive addition
or removal, a PR merge, or a durable decision. Re-anchor the reviewed repository commit,
revise affected claims against the live tree, update index.md, append to log.md, then
commit and push the vault separately under its standing authorization. Never add raw
artifacts, credentials, or user data. Report any blocked vault commit or push.
When checking vocabulary parity, compare type: Literal[...] declarations in
src/astralprims/primitives.py with component_types in the pinned AstralProjection
contracts/ui_protocol.json. Resolve both repositories from the current workspace,
check that both inputs are nonempty, and distinguish manifest-only renderer types from
primitive classes. Do not use historical class counts as the current expected result.