# evolve

Source: https://apigenie.pl/techdocs/reference/cli-evolve

Plans safe API changes: `evolve plan` splits the breaking changes between two OpenAPI documents
into an expand wave (deprecate, add next to) and a contract wave (remove), writes the wave
documents and the plan, and proves the plan with `diff`.

> **(experimental, outside the 1.0 compatibility promise)** `evolve plan` is the first version of
> the planner: its recipes, the layout of `plan.yaml` and the options can still change, and
> `evolve status` and `evolve next` will join it. It says so in its `--help` and on the first line
> it prints, on the standard error. See the
> [experimental commands](/techdocs/reference/cli-overview#experimental-commands).

## Usage

```sh
apigenie evolve plan [OPTIONS] OLD_FILE NEW_FILE
```

## Description

`diff` says that a change breaks consumers. `evolve plan` says how to ship the same change without
breaking anyone, the way database migrations do it: first **expand** (add the new shape next to
the old one and deprecate the old one with a sunset date), let consumers move during a **window**,
then **contract** (remove what was deprecated). The command takes the document consumers use today
(`OLD_FILE`) and the document the change arrives at (`NEW_FILE`, usually the one in the pull
request), and writes three files to the directory of `--out` (`evolve-out/` by default):

| File | What it is | When to ship it |
| ---- | ---------- | --------------- |
| `1-expand.yaml` | The target document with every old element kept next to its replacement, `deprecated: true` and `x-sunset: <date>`; non-breaking against `OLD_FILE` | now |
| `2-contract.yaml` | The target document | on or after the sunset date |
| `plan.yaml` | The plan: the waves, the units, the residue, the server notes | |

The wave files take the format of `NEW_FILE` (`1-expand.json` for a JSON document) and keep its
comments and layout. The sunset date is `--today` plus `--window` (90 days by default).

### Recipes

Each breaking change of the diff becomes a **unit** with a recipe:

| Change | Recipe | Expand wave | Contract wave |
| ------ | ------ | ----------- | ------------- |
| An operation or a whole path is removed | `remove-operation` | the operation stays, deprecated with its sunset date | removed |
| An operation moved to another path (a hint says where, see below) | `move-path` | the old operation stays, deprecated | removed |
| A response property is removed | `remove-response-property` | the property stays, deprecated (and required when it was) | removed |
| A response property is renamed | `rename-response-property` | the old property stays, deprecated, next to the new one | removed |
| A request property is renamed | `rename-request-property` | the old property stays, deprecated; neither name is required | the old one removed, the new one required when it was in `NEW_FILE` |
| A request parameter is renamed | `rename-request-parameter` | the old parameter stays, deprecated and optional; the new one is optional | the old one removed, the new one required when it was |
| A request parameter is removed | `remove-request-parameter` | the parameter stays, deprecated | removed |
| A request property becomes required (new or existing) | `require-request-property` | present but optional | required |
| A request parameter becomes required | `require-request-parameter` | optional | required |
| A request enum value is removed | `remove-request-enum-value` | the value stays, listed in `x-deprecated-enum`, with `x-sunset` on the schema | removed |

A rename is detected when an object loses one property (or an operation one parameter) and gains
one whose schema is the same, after following `$ref`s and ignoring descriptions, titles, examples
and extensions. Removed and added members that were not paired are listed as **rename candidates**;
a hint decides them. A component schema is reported where it is used and under `components`; one
unit covers all of them, and a schema used in requests and responses gets the request recipe, the
stricter one.

Changes the differ rates non-breaking need no recipe: they are in both waves as they are in
`NEW_FILE`. Breaking changes no recipe makes safe are **residue**: a changed type or format, a
response property no longer required, a new security requirement, a replaced server, a changed
status code or media type. The plan lists them with a reason, the expand wave keeps the old shape
there, the contract wave applies the change, and the command exits with status 1: these need a
major version or a new resource.

### Server notes

A document can say that both `fullName` and `name` exist; only the implementation can emit both or
accept both. Each unit that needs it carries a **server note** (`server` in `plan.yaml`, the
"Server checklist" of the report): emit both names until the sunset, accept both, apply a default
when a soon-required property is missing, accept a deprecated enum value until the sunset. A plan
whose server does not follow its notes is a lie the documents can't catch.

### Hints

`--hints FILE` names what the diff can't see, as YAML or JSON:

```yaml
renames:
  - from: "#/components/schemas/Customer/properties/fullName"
    to: "#/components/schemas/Customer/properties/name"
  - from: "GET /v1/parcels/{id}"
    to: "GET /v1/shipments/{id}"
window: 90d
reason: Unify naming with the shipments API
```

`renames` pairs a removed element with an added one: operations as `METHOD /path`, properties and
parameters by their location, written the way the `overrides.where.location` of the
[diff configuration](/techdocs/reference/cli-diff#configuration) is
(`/components/schemas/Customer/properties/fullName`,
`/paths/~1pets/get/parameters/query:page`; a leading `#` is accepted). A hint that names no removed
and added element of the diff is an error. `window` applies when `--window` is not given, and
`reason` is written into every unit.

### Verification

Before anything is written, the plan is checked with the differ and the same configuration:

1. `diff OLD_FILE 1-expand.yaml` has no breaking change.
2. `diff 1-expand.yaml 2-contract.yaml --today <sunset>` has breaking changes only where a unit's
   contract step owns them (a `required` list it changes) or where the residue is; the scheduled
   removals are non-breaking thanks to the
   [`sunset_passed` condition](/techdocs/reference/cli-diff#configuration).
3. `2-contract.yaml` equals `NEW_FILE`, ignoring `x-sunset`, `x-deprecated-enum` and
   `x-apigenie-*` extensions.

When a check fails, the report names the change and the unit, nothing is written, and the command
exits with status 2. The same differ suggests the versions of the waves: a minor for the expand
wave (a patch when it only adds extensions), a minor for a contract wave that only removes
scheduled elements, a major for one that requires something or carries residue.

### plan.yaml

```yaml
apigenie-evolve: 1
from: {path: api.v1.yaml, version: 1.4.0}
to: {path: api.v2.yaml, version: 1.4.0}
today: 2026-10-13
window: 90d
waves:
  - id: expand
    file: 1-expand.yaml
    version: 1.5.0
    units: [remove-operation-quoteLegacy, rename-response-property-Parcel-fullName]
  - id: contract
    file: 2-contract.yaml
    version: 1.6.0
    notBefore: 2027-01-11
    units: [remove-operation-quoteLegacy, rename-response-property-Parcel-fullName]
units:
  remove-operation-quoteLegacy:
    recipe: remove-operation
    from: GET /quotes/legacy
    sunset: 2027-01-11
  rename-response-property-Parcel-fullName:
    recipe: rename-response-property
    from: /components/schemas/Parcel/properties/fullName
    to: /components/schemas/Parcel/properties/name
    detected: true
    sunset: 2027-01-11
    server: emit both fullName and name until 2027-01-11
residue: []
```

A unit's `from` is `METHOD /path` for an operation and the element's location otherwise; `to`
names the new element of a rename or a move, `value` the removed enum value, `detected` whether
the planner found a rename itself (`true`) or a hint supplied it (`false`), `reason` comes from the
hints, `sites` lists the places a component schema was reported at. Each residue item has its
`location`, the `change` as `diff` describes it, and `why`.

## Options

| Option | Description |
| ------ | ----------- |
| `--hints FILE` | The hints file: renames, the window, the reason. |
| `--window WINDOW` | How long consumers have before the contract wave: days (`90d`) or weeks (`12w`). Defaults to the hints file's window, then to `90d`. |
| `--today DATE` | The date the plan is made at (`YYYY-MM-DD`); the sunset dates are this plus the window. Defaults to the current date. |
| `--config FILE`, `--no-config` | The configuration file whose `commands.diff` block rates the changes, as for [`diff`](/techdocs/reference/cli-diff#configuration). |
| `--out DIR` | Where the plan and the wave documents go. Defaults to `evolve-out`. |
| `--format FORMAT` | The report: `text` (default) or `json`. |

## JSON output

With `--format json` the report is an object whose `formatVersion` member is the version of the
format, changed only when a member is removed or changes meaning (see
[Compatibility](/techdocs/reference/cli-compatibility)); its JSON Schema is
[apigenie-evolve-report.json](https://apigenie.pl/schemas/apigenie-evolve-report.json).

```json5
{
  "formatVersion": 1,
  "result": "planned",          // or "residue", or "failed"
  "plan": { "apigenie-evolve": 1, "...": "the same object as plan.yaml" },
  "checks": [
    { "id": "expand-non-breaking", "status": "ok" },
    { "id": "contract-scheduled-only", "status": "ok" },
    { "id": "contract-equals-target", "status": "ok" }
  ],
  "serverNotes": [{ "unit": "rename-response-property-Parcel-fullName", "note": "emit both fullName and name until 2027-01-11" }],
  "files": ["evolve-out/plan.yaml", "evolve-out/1-expand.yaml", "evolve-out/2-contract.yaml"]
}
```

## Exit status

| Status | Meaning |
| ------ | ------- |
| 0 | Every breaking change was placed in a wave; the files were written |
| 1 | The files were written, but some changes are residue (listed in the report) |
| 2 | Invalid options, a file that can't be read or isn't an OpenAPI 3.x document, a hint that names nothing, an invalid configuration, or a failed check (nothing written) |

## Examples

Plan the change of a pull request, then let `diff` confirm the waves:

```sh
apigenie evolve plan api.main.yaml api.pr.yaml --out evolve-out
apigenie diff api.main.yaml evolve-out/1-expand.yaml
apigenie diff evolve-out/1-expand.yaml evolve-out/2-contract.yaml --today 2027-01-11
```

With hints for a moved endpoint, a two-month window and a JSON report for a pipeline:

```sh
apigenie evolve plan api.main.yaml api.pr.yaml --hints evolve.yaml --window 8w --format json
```

Serve the expand wave to old consumers before shipping it:

```sh
apigenie-server evolve-out/1-expand.yaml
```
