# Versioning

> What 1.x promises, how the packages of the family are tied together by versions, and what the library does not promise yet.

- Page: https://granularity.tech/docs/versioning
- Last reviewed: 2026-10-16

An adopter’s question is not "what is the current version" but "how long will
this version live". The honest answer today: **no support window is declared**.
Below is what exists instead, which of it is checkable, and what to do while
there is no promise.

## Since 1.0

The library numbers its releases by [Semantic Versioning](https://semver.org/),
and since 1.0 the public surface — the components, their subpaths, the tokens
and the registries — breaks only in a major. On 0.x it was different: a minor
release could break compatibility, and did. Version 0.41.0 moved `GrCodeBlock`
out of the core into `@feugene/granularity-code`, and the
`@feugene/granularity/components/GrCodeBlock` subpath disappeared:

```diff
- import { GrCodeBlock } from '@feugene/granularity'
+ import { GrCodeBlock } from '@feugene/granularity-code/components/GrCodeBlock'
```

The reason is named in the changelog and it is about 1.0: taking something out
of a frozen core costs a major, so moves of that kind were made **before** the
freeze rather than after it.

:::callout{type="info"}
On major 1, `^` and `~` differ. `^1.0.0` expands to `>=1.0.0 <2.0.0` and accepts
new minors; `~1.0.0` expands to `>=1.0.0 <1.1.0`. On a zero major —
`@feugene/granularity-datasource` 0.1.x and the `@feugene/fint-i18n` peer — `^`
still means the same as `~`.
:::

## Every package is versioned on its own

The family has no shared version, and that is not an oversight. The core and the
companions run on different cycles: the core ships patch after patch, a companion
may sit on its version for weeks, and `@feugene/granularity-datasource` is still
on 0.x. An extra companion release for the sake of a core release would mean an
update with not a single change behind it.

Three things follow, and all three are visible in the repository:

:::steps
1. **Every package has its own changelog.** There is deliberately no shared
   changelog at the root — [the changelogs on this portal](/changelog) are
   assembled from them.
2. **Every package has its own tag.** The core is released with a `vX.Y.Z` tag,
   a companion with `<directory-name>-vX.Y.Z`, for instance
   `granularity-charts-v1.0.4`.
3. **Publishing happens from the tag.** Pushing a tag triggers publication to
   npm with `--provenance`: the package on npm has traceable provenance, and
   your scanner verifies it rather than us. The GitHub Packages mirror is
   published under the `@efureev` scope and without provenance.
:::

## The compatibility matrix

A companion declares which version of the core and of the build pipeline it works
with — not in words but as a range in `peerDependencies`. Your package manager reads
it by itself and speaks up about an incompatible pair: npm with a warning, pnpm and
Yarn Berry with strict settings by refusing to install. Here is the same range in
readable form:

| Package | Core `@feugene/granularity` | Pipeline `@feugene/granum` | Vue |
|---|---|---|---|
| `@feugene/granularity-charts` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-chrono` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-code` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-dashboard` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-datasource` | — | — | `^3.5.0` |
| `@feugene/granularity-devtools` | `>=1.0.0 <2.0.0` | — | `^3.5.0` |
| `@feugene/granularity-editor` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-forms-schema` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-media` | `>=1.0.0 <2.0.0` | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/granularity-test-kit` | — | `>=2.0.0 <3.0.0` | `^3.5.0` |
| `@feugene/unplugin-granularity` | `>=1.0.0 <2.0.0` | — | — |

The table covers the core, the pipeline and Vue. The other peer dependencies —
for instance the optional `@feugene/fint-i18n` `>=0.9.0 <1.0.0` and
`@feugene/unplugin-granularity`, `@feugene/granularity-chrono` for `forms-schema`,
the editor and schema libraries of their companions — are in the package's
`package.json`. For `test-kit` both the pipeline and Vue are optional.

A dash means the package does not declare that dependency at all:
`granularity-datasource` knows about neither the core nor the pipeline — it is
about data rather than markup — `test-kit` works with any core, because it checks
CSS and the DOM rather than importing components, and `unplugin-granularity`
resolves components without generating a single rule.

The family reached 1.0 together, and the ranges say so: the lower bound is
`>=1.0.0`, the upper `<2.0.0`. Before that the bound was `<1.0.0` and meant
"beyond that there are no promises" — 1.0 has redefined the contract, and the
ranges were rewritten with it, as promised.

The pipeline column moves on its own major: since the 1.1.0 release the companions
are built with granum 2 and declare `>=2.0.0 <3.0.0` — granum 1 does not carry out the
theming recipes and the ban on root imports that the documentation relies on.

:::callout{type="warning"}
The table is typed by hand but verified against the manifests in the portal’s
CI: a core or Vue range that has diverged from a companion’s `package.json` fails
the run. The pipeline column is not verified by the gate yet. And the check runs
against the tag the library submodule is pinned to — not against what sits in npm
this very second. The source of truth is the `peerDependencies` of the installed
package.
:::

## Environment requirements

| What | Version | Where from |
|---|---|---|
| Node | `>=22` | `engines` in every package of the family |
| Vue | `^3.5.0` | a peer dependency |
| Module format | ESM only | `"type": "module"`, there is no CommonJS build |

Those three lines are part of compatibility too, and they change by the same
rules: raising the lower bound of Node is a breaking change, and since 1.0 it
arrives only in a major.

## Documentation for one version

The documentation describes one version — the one the portal is pinned to,
currently the 1.x line. There is no `/v0/` archive and no version switcher: the
0.x pages are not kept on the portal, and moving off them is covered in
[migration](/docs/migration).

The version in the header is read from the installed core's manifest at build
time and is never typed by hand.

## What the library does not promise yet

This section exists because silence here reads as a promise. Not one of the
following exists today:

- **the lifetime of a version.** How long the 1.x line will receive fixes after
  2.0, and how long a minor lives after the next one, is not declared;
- **a deprecation period.** A subpath may disappear in the same release in which
  its replacement appears: that is what happened with `GrCodeBlock`;
- **backports of security fixes** into previous minors;
- **`since` and `deprecated` metadata on components.** A component page does not
  say which version a prop appeared in: the library does not carry that data yet,
  and the portal will not invent it.

What to do instead:

:::steps
1. **Pin the minor if you need predictability:** `~1.0.0` rather than `^1.0.0`
   — see the callout above — and check that `package.json` holds a range rather
   than `latest`.
2. **Read the changelog of the package you are raising.** Not a shared one: every
   package has its own, and a breaking change is described there with a diff.
3. **Raise them one at a time.** A companion and the core are tied by a range
   rather than by a shared version: updating the core does not require updating
   the companions as long as the range still holds.
:::

## Where to see the changes

[The changelogs](/changelog) on this portal are assembled from the changelogs of
the packages in the library repository — one per package, with diffs and
explanations. It is the same text as in the repository: the portal does not retell
it.
