Skip to content

Stability — what is frozen

Every @aparte/* package is a 0.x, and the roadmap names the beta as the moment a declared surface stops moving without notice. This page is that declaration, and the surface below is frozen as of 0.17.0, the beta. From that version on, a name listed here leaves only through a notice release: a changeset that says it is deprecated and names it, one release before the removal. Below this surface the alpha rule still holds: a rename lands as a rename, without an alias, and the changeset says so.

Nothing below is typed by hand. Every list on this page is the snapshot in apps/docs/src/data/frozen-surface.json, which scripts/frozen-surface.mjs takes from the tree itself — the custom-elements manifests, the typed event map, the built barrels, AparteLocale, the stylesheets. And pnpm check:frozen-surface runs in the gate and on every pull request: it re-measures the tree, and it fails on a name that left this snapshot without a notice. The figures on the roadmap are counted the same way.

From the beta on, a frozen element, event, export or token does not change meaning or name in one release. A change goes through two:

  1. A notice release. The old name still works and is marked deprecated, with the release it disappears in and the replacement, in the changelog and in the JSDoc.
  2. A removal release, at least one minor later.

What is not listed here stays free to move, and that is the point of a list: a default colour, a spacing, an English label, the content of a docs page, the example apps, @aparte/docs-mcp (a tool, not an API), and anything whose JSDoc says @experimental.

Adding is always allowed: a new optional attribute, a new optional field on an event’s detail, a new export. Removing or renaming is what the two-release rule governs. And read name strictly: what a list below holds is a name, so an exported interface’s fields are not on it — see The exports.

The tag name and the attributes the manifest documents — and a property that mirrors one of those attributes freezes with it, since the attribute is the frozen name. The --aparte-* tokens elements read are frozen as a family of their own, in The tokens below. 27 elements across 4 packages, 93 documented attributes between them — the figure after each tag is its own count:

@aparte/plugin-approval — 1 element: aparte-approval-mode (0 attributes)

@aparte/plugin-ask-user — 1 element: aparte-ask-user (0 attributes)

@aparte/core — 24 elements: aparte-chat (8 attributes), aparte-chat-bubble (6 attributes), aparte-chat-status (2 attributes), aparte-chat-viewport (5 attributes), aparte-composer (7 attributes), aparte-composer-action (4 attributes), aparte-composer-add-attachment (3 attributes), aparte-composer-attachments (0 attributes), aparte-composer-cancel (0 attributes), aparte-composer-input (4 attributes), aparte-composer-send (0 attributes), aparte-composer-toolbar (1 attribute), aparte-context (8 attributes), aparte-conversation-list (4 attributes), aparte-elicitation (0 attributes), aparte-icon (1 attribute), aparte-optgroup (4 attributes), aparte-option (4 attributes), aparte-progress-spinner (1 attribute), aparte-scroll-rail (3 attributes), aparte-select (5 attributes), aparte-sidebar (3 attributes), aparte-split (10 attributes), aparte-suggestions (5 attributes)

@aparte/plugin-model-selector — 1 element: aparte-model-selector (5 attributes)

The components reference is generated from the same manifests, page by page.

40 typed events, all kebab-case and prefixed aparte-. Frozen: the name — that is what the snapshot measures. The shape of detail follows the same rule by policy (a field may be added if optional; none is removed or renamed), but no guard reads it, so a change there is announced in the changeset, like any other member of an exported type — see The exports.

aparte-abort, aparte-action, aparte-action-click, aparte-approval-mode-change, aparte-attachment-preview, aparte-branch-navigate, aparte-compact, aparte-compact-done, aparte-compact-error, aparte-compact-start, aparte-composer-change, aparte-config-change, aparte-context-threshold, aparte-conversation-archive, aparte-conversation-delete, aparte-conversation-pin, aparte-conversation-rename, aparte-conversation-select, aparte-conversation-unarchive, aparte-conversation-unpin, aparte-edit, aparte-feedback, aparte-link-click, aparte-message-aborted, aparte-message-done, aparte-message-error, aparte-message-info, aparte-message-start, aparte-model-change, aparte-optgroup-toggle, aparte-path-changed, aparte-retry, aparte-scroll-rail-jump, aparte-segment-update, aparte-select-change, aparte-send, aparte-sidebar-toggle, aparte-split-resize, aparte-suggestion, aparte-tool-approval-request

Another 6 carry no detail at all and are deliberately absent from AparteEventMapaparte-cancel, aparte-composer-submit, aparte-reset, aparte-reset-done, aparte-select-close, aparte-select-open — because an entry would type e.detail as null and gain nothing. Their names freeze the same way, which is what matters for aparte-reset, a command your app dispatches by name. That is 46 event names in all.

The events reference carries each one’s detail type and the element that fires it. The wrapper bindings (onConversationSelect in React, (conversationSelect) in Angular, and so on) follow from these names and freeze with them.

The values each package exports from its barrel — 204 across 20 packages. @aparte/core publishes 86 of them, from AparteClient and AparteConversationManager to registerSegmentRenderer and registerDefaultRenderers.

What freezes here is the name. The guard reads the built barrels, where TypeScript has erased the types, so the members of an exported interface — the fields of a setup* options object, of a request, of an AparteConversation row — are not measured and not promised by this page. A field can still be added, renamed or removed; when one is, the changeset says so, and that is where to look before you implement one of these types yourself.

PackageFrozen exports
@aparte/angular32
@aparte/core86
@aparte/engine1
@aparte/locale-fr1
@aparte/plugin-approval7
@aparte/plugin-artifacts13
@aparte/plugin-ask-user8
@aparte/plugin-compaction10
@aparte/plugin-marked1
@aparte/plugin-model-selector1
@aparte/plugin-shiki2
@aparte/plugin-streaming-markdown1
@aparte/plugin-titler2
@aparte/provider-ai-sdk5
@aparte/provider-openai-compat3
@aparte/provider-scenario3
@aparte/provider-transformers13
@aparte/react5
@aparte/svelte5
@aparte/vue5

Each package’s README and the reference pages list them, and the gate holds it: no value export of @aparte/core’s runtime barrel may go unmentioned, and every other barrel’s unmentioned count is pinned to a recorded ceiling that may fall but never rise (scripts/check-export-mentions.mjs).

Each plugin and provider freezes the same way: its setup* entry, the options that entry takes, and the types it exports. @aparte/engine freezes runStreamAgent, its options and the stream event vocabulary. The fields of those option types carry the caveat above — the name is frozen, the members are not.

91 keys on AparteLocale. Frozen: the key names, which is what a translation bundle and every t('…') call are written against. The English strings behind them are not — a label can be reworded in a patch. Localization is the guide; a bundle that misses a key falls through to English, and the gate refuses that too.

462 --aparte-* custom properties, every one of them on the CSS variables reference, frozen by name and by what it controls. Its default value is not frozen: a palette can be retuned in a minor.

CSS classes are not frozen. They all carry the aparte- prefix, and that prefix is the promise: nothing core emits will collide with a class of yours. Style them if you like, but a class is a rendering detail, and a rename there is a patch.

There is no list to maintain here — only a snapshot to re-take, and one guard that refuses the rest:

Terminal window
pnpm check:frozen-surface # in the gate, and on every pull request
pnpm frozen:update # re-take the snapshot, then commit it with the change

An addition — a new element, attribute, event, export, locale key or token — is always allowed by the promise. The guard still fails on it, with a different sentence, because a snapshot that lags the tree stops describing anything: run pnpm frozen:update and commit the JSON alongside your change.

A removal or a rename needs the notice release first. The guard looks for a changeset that names the exact name and deprecates it; it then prints deprecated with notice and lets the change through:

---
'@aparte/core': minor
---
`aparte-old-name` is deprecated and will be removed in 0.18 — use `aparte-new-name`.

Without one, pnpm check:frozen-surface fails, names what disappeared, and the pull request stays red. That is the whole mechanism: the promise is a file, and the gate reads it.